OpenCV grooving detection

2019-03-11 09:22发布

问题:

I have pictures of a surface with many grooves. In most cases the edges of the grooving form parallel lines so Canny and Hough transformation work very good to detect the lines and to do some characterization. However, at several places the grooving is demaged and the edges aren't parallel anymore.

I am looking for an easy way to check if a certain edge is a straight line or if there are any gaps or deviations from a straight line. I am thinking of something like the R square parameter in linear interpolation, but here I need a parameter which is more location-dependent. Do you have any other thougts how to characterize the edges?

I attached a picture of the grooving after canny edge detection. Here, the edges are straight lines and the grooving is fine. Unfortunately I don't have access to pictures with damaged grooving at the moment. However, in pictures with damaged grooving, the lines would have major gaps (at least 10% of the picture's size) or wouldn't be parallel.

回答1:

The core of the technique I'm sharing below uses cv::HoughLinesP() to find line segments in a grayscale image.

The application starts by loading the input image as grayscale. Then it performs a basic pre-processing operation to enhance certain characteristics of the image, aiming to improve the detection performed by cv::HoughLinesP():

#include <cv.h>
#include <highgui.h>

#include <algorithm>

// Custom sort method adapted from: http://stackoverflow.com/a/328959/176769
// This is used later by std::sort()
struct sort_by_y_coord 
{
    bool operator ()(cv::Vec4i const& a, cv::Vec4i const& b) const 
    {
        if (a[1] < b[1]) return true;

        if (a[1] > b[1]) return false;

        return false;
    }
};


int main()
{
    /* Load input image as grayscale */

    cv::Mat src = cv::imread("13531682.jpg", 0);

    /* Pre-process the image to enhance the characteristics we are interested at */

    medianBlur(src, src, 5);

    int erosion_size = 2;
    cv::Mat element = cv::getStructuringElement(cv::MORPH_CROSS,
                                       cv::Size(2 * erosion_size + 1, 2 * erosion_size + 1),
                                       cv::Point(erosion_size, erosion_size) );
    cv::erode(src, src, element);
    cv::dilate(src, src, element);

    /* Identify all the lines in the image */

    cv::Size size = src.size();
    std::vector<cv::Vec4i> total_lines;
    cv::HoughLinesP(src, total_lines, 1, CV_PI/180, 100, size.width / 2.f, 20);

    int n_lines = total_lines.size();
    std::cout << "* Total lines: "<< n_lines << std::endl;

    cv::Mat disp_lines(size, CV_8UC1, cv::Scalar(0, 0, 0));

    // For debugging purposes, the block below writes all the lines into disp_lines
    // for (unsigned i = 0; i < n_lines; ++i)
    // {
    //     cv::line(disp_lines, 
    //              cv::Point(total_lines[i][0], total_lines[i][2]),
    //              cv::Point(total_lines[i][3], total_lines[i][4]), 
    //              cv::Scalar(255, 0 ,0));
    // }
    // cv::imwrite("total_lines.png", disp_lines);

At this point, all the line segments detected can be written to a file for visualization purposes:

At this point we need to sort our vector of lines because cv::HoughLinesP() doesn't do that, and we need the vector sorted to be able to identify groups of lines, by measuring and comparing the distance between the lines:

    /* Sort lines according to their Y coordinate. 
       The line closest to Y == 0 is at the first position of the vector.
    */

    sort(total_lines.begin(), total_lines.end(), sort_by_y_coord());

    /* Separate them according to their (visible) groups */ 

    // Figure out the number of groups by distance between lines
    std::vector<int> idx_of_groups;   // stores the index position where a new group starts
    idx_of_groups.push_back(0); // the first line indicates the start of the first group 

    // The loop jumps over the first line, since it was already added as a group
    int y_dist = 35; // the next groups are identified by a minimum of 35 pixels of distance  
    for (unsigned i = 1; i < n_lines; i++)  
    {
        if ((total_lines[i][5] - total_lines[i-1][6]) >= y_dist) 
        {
            // current index marks the position of a new group
            idx_of_groups.push_back(i); 
            std::cout << "* New group located at line #"<< i << std::endl;           
        }
    }

    int n_groups = idx_of_groups.size();
    std::cout << "* Total groups identified: "<< n_groups << std::endl;

The last part of the code above simply stores the index positions of the vector of lines in a new vector<int> so we know which lines starts a new group.

For instance, assume that the indexes stored in the new vector are: 0 4 8 12. Remember: they define the start of each group. That means that the ending lines of the groups are: 0, 4-1, 4, 8-1, 8, 12-1, 12.

Knowing that, we write the following code:

    /* Mark the beginning and end of each group */

    for (unsigned i = 0; i < n_groups; i++)  
    {
        // To do this, we discard the X coordinates of the 2 points from the line, 
        // so we can draw a line from X=0 to X=size.width

        // beginning
        cv::line(disp_lines, 
                 cv::Point(0, total_lines[ idx_of_groups[i] ][7]),
                 cv::Point(size.width, total_lines[ idx_of_groups[i] ][8]), 
                 cv::Scalar(255, 0 ,0));

        // end      
        if (i != n_groups-1)
        {
            cv::line(disp_lines, 
                     cv::Point(0, total_lines[ idx_of_groups[i+1]-1 ][9]),
                     cv::Point(size.width, total_lines[ idx_of_groups[i+1]-1 ][10]), 
                     cv::Scalar(255, 0 ,0));
        }
    }
    // mark the end position of the last group (not done by the loop above)    
    cv::line(disp_lines, 
             cv::Point(0, total_lines[n_lines-1][11]),
             cv::Point(size.width, total_lines[n_lines-1][12]), 
             cv::Scalar(255, 0 ,0));

    /* Save the output image and display it on the screen */

    cv::imwrite("groups.png", disp_lines);

    cv::imshow("groove", disp_lines);
    cv::waitKey(0);
    cv::destroyWindow("groove");

    return 0;
}

And the resulting image is:

It's not a perfect match, but it's close. With a little bit of tweaks here and there this approach can get much better. I would start by writing a smarter logic for sort_by_y_coord, which should discard lines that have small distances between the X coordinates (i.e. small line segments), and also lines that are not perfectly aligned on the X axis (like the one from the second group in the output image). This suggestion makes much more sense after you take the time to evaluate the first image generated by the application.

Good luck.



回答2:

What immediately comes to mind would be a Hough Transform. This is a voting scheme in line space, which takes each possible line and gives you a score for it. In the code I linked to above, you could simply set a threshold that approximates ~10% of screwed up grooves/lines.