Detect semicircle in OpenCV
Asked Answered
F

6

38

I am trying to detect full circles and semicircles in an image.enter image description here

I am following the below mentioned procedure: Process image (including Canny edge detection). Find contours and draw them on an empty image, so that I can eliminate unwanted components (The processed image is exactly what I want). Detect circles using HoughCircles. And, this is what I get:

enter image description here

I tried varying the parameters in HoughCircles but the results are not consistent as it varies based on lighting and the position of circles in the image. I accept or reject a circle based on its size. So, the result is not acceptable. Also, I have a long list of "acceptable" circles. So, I need some allowance in the HoughCircle params. As for the full circles, it's easy - I can simply find the "roundness" of the contour. The problem is semicircles!

Please find the edited image before Hough transformenter image description here

Faintheart answered 20/12, 2013 at 7:42 Comment(5)
Is it right that you CAN detect which edges (after canny and hough) in the image belong to circles/semi-circles? And your problem is, that Hough results' circle locations aren't good enough? What about fitting a parametric circle with all the circle edges (or at least 3 correct circle edge pixels)? (Three points define a circle!) ... to make that more robust you could use a RANSAC algorithm (inlier/outlier counting => huge missing parts = semi-circle). Didnt try it, but might work?!?Shuffleboard
Thanks Micka! What you say about parametric circle is Hough Transform algorithm itself, isn't it? And if you see the results of Hough, more than 3 points lie on the detected circle! I have no clue about RANSAC, I will check it outFaintheart
can you provide the edge image before computing/plotting hough circles? And did you try drawing the contours with less line-thickness?Shuffleboard
ah, I see... HoughTransform uses canny internally, so the line-thickness is needed, but for full circles it would be better to fill the contour.Shuffleboard
I see that I do not need to draw contours on a blank image. This is Canny + Gaussian blur.Faintheart
S
48

Use houghCircle directly on your image, don't extract edges first. Then test for each detected circle, how much percentage is really present in the image:

int main()
{
    cv::Mat color = cv::imread("../houghCircles.png");
    cv::namedWindow("input"); cv::imshow("input", color);

    cv::Mat canny;

    cv::Mat gray;
    /// Convert it to gray
    cv::cvtColor( color, gray, CV_BGR2GRAY );

    // compute canny (don't blur with that image quality!!)
    cv::Canny(gray, canny, 200,20);
    cv::namedWindow("canny2"); cv::imshow("canny2", canny>0);

    std::vector<cv::Vec3f> circles;

    /// Apply the Hough Transform to find the circles
    cv::HoughCircles( gray, circles, CV_HOUGH_GRADIENT, 1, 60, 200, 20, 0, 0 );

    /// Draw the circles detected
    for( size_t i = 0; i < circles.size(); i++ ) 
    {
        Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
        int radius = cvRound(circles[i][2]);
        cv::circle( color, center, 3, Scalar(0,255,255), -1);
        cv::circle( color, center, radius, Scalar(0,0,255), 1 );
    }

    //compute distance transform:
    cv::Mat dt;
    cv::distanceTransform(255-(canny>0), dt, CV_DIST_L2 ,3);
    cv::namedWindow("distance transform"); cv::imshow("distance transform", dt/255.0f);

    // test for semi-circles:
    float minInlierDist = 2.0f;
    for( size_t i = 0; i < circles.size(); i++ ) 
    {
        // test inlier percentage:
        // sample the circle and check for distance to the next edge
        unsigned int counter = 0;
        unsigned int inlier = 0;

        cv::Point2f center((circles[i][0]), (circles[i][1]));
        float radius = (circles[i][2]);

        // maximal distance of inlier might depend on the size of the circle
        float maxInlierDist = radius/25.0f;
        if(maxInlierDist<minInlierDist) maxInlierDist = minInlierDist;

        //TODO: maybe paramter incrementation might depend on circle size!
        for(float t =0; t<2*3.14159265359f; t+= 0.1f) 
        {
            counter++;
            float cX = radius*cos(t) + circles[i][0];
            float cY = radius*sin(t) + circles[i][1];

            if(dt.at<float>(cY,cX) < maxInlierDist) 
            {
                inlier++;
                cv::circle(color, cv::Point2i(cX,cY),3, cv::Scalar(0,255,0));
            } 
           else
                cv::circle(color, cv::Point2i(cX,cY),3, cv::Scalar(255,0,0));
        }
        std::cout << 100.0f*(float)inlier/(float)counter << " % of a circle with radius " << radius << " detected" << std::endl;
    }

    cv::namedWindow("output"); cv::imshow("output", color);
    cv::imwrite("houghLinesComputed.png", color);

    cv::waitKey(-1);
    return 0;
}

For this input:

enter image description here

It gives this output:

enter image description here

The red circles are Hough results.

The green sampled dots on the circle are inliers.

The blue dots are outliers.

Console output:

100 % of a circle with radius 27.5045 detected
100 % of a circle with radius 25.3476 detected
58.7302 % of a circle with radius 194.639 detected
50.7937 % of a circle with radius 23.1625 detected
79.3651 % of a circle with radius 7.64853 detected

If you want to test RANSAC instead of Hough, have a look at this.

Shuffleboard answered 20/12, 2013 at 14:43 Comment(10)
Excellent! It worked :) but why doesn't it work on edges? My objective was to reduce the wrong circle identification by having a lower limit on the contour size... that way I get a clean processed image.Faintheart
I'm not sure. I guess that's because of openCv's houghCircle using canny internally. If you work on edges, canny doesnt work. If you work on thick edges, canny gives 2 circles close to each other which interfere. But that's just a guess.Shuffleboard
@harsh: added another answer using RANSAC (much work to be done yet) that works on the edge image directlyShuffleboard
Hello there, has been a while. I found this solution, but I do not undestand two main things: - Why the minInlierDist is 2 at start? - And why you decide to divide by 25.0 the radius to get maxInlierDist? I hope you or anybody could answer this.Laconia
@CésarHoyos those values were chosen "empirically". The reason to switch to a radius dependend maxInlierDist is because the quality of the circle fitting the edges might depend ob the circle size.Shuffleboard
Ok ok you were right. I must look for this parameters values to my own project then. Thanks!Laconia
@CésarHoyos it depends on your needs about precision. If your circles are without any distortion and well detectable, low constant thresholds might be fine.Shuffleboard
I know this is an old answer, but after converting this sample to C# I was unable to detect the outliers. All circles were detected as 100% and I can't quite figure out why.Tungstic
... and it figures I found the issue immediately after typing that. A subtle language difference in C# vs C++. Original cv::distanceTransform(255-(canny>0), dt, CV_DIST_L2 ,3); should have been translated to Cv2.DistanceTransform(255-canny, dt, DistanceTypes.L2, DistanceTransformMasks.Mask3); - I missed the 255-canny part as I didn't know you could do that to a Mat!Tungstic
@MichaelBrown nice work! Feel free to share the C# code as an additional answer. I will surely upvote and you can link the answer in a comment here and/or I will add a link to your answer to my answer.Shuffleboard
S
15

Here is another way to do it, a simple RANSAC version (much optimization to be done to improve speed), that works on the Edge Image.

the method loops these steps until it is cancelled

  1. choose randomly 3 edge pixel
  2. estimate circle from them (3 points are enough to identify a circle)
  3. verify or falsify that it's really a circle: count how much percentage of the circle is represented by the given edges
  4. if a circle is verified, remove the circle from input/egdes

    int main()
    {
    //RANSAC
    
    //load edge image
    cv::Mat color = cv::imread("../circleDetectionEdges.png");
    
    // convert to grayscale
    cv::Mat gray;
    cv::cvtColor(color, gray, CV_RGB2GRAY);
    
    // get binary image
    cv::Mat mask = gray > 0;
    //erode the edges to obtain sharp/thin edges (undo the blur?)
    cv::erode(mask, mask, cv::Mat());
    
    std::vector<cv::Point2f> edgePositions;
    edgePositions = getPointPositions(mask);
    
    // create distance transform to efficiently evaluate distance to nearest edge
    cv::Mat dt;
    cv::distanceTransform(255-mask, dt,CV_DIST_L1, 3);
    
    //TODO: maybe seed random variable for real random numbers.
    
    unsigned int nIterations = 0;
    
    char quitKey = 'q';
    std::cout << "press " << quitKey << " to stop" << std::endl;
    while(cv::waitKey(-1) != quitKey)
    {
        //RANSAC: randomly choose 3 point and create a circle:
        //TODO: choose randomly but more intelligent, 
        //so that it is more likely to choose three points of a circle. 
        //For example if there are many small circles, it is unlikely to randomly choose 3 points of the same circle.
        unsigned int idx1 = rand()%edgePositions.size();
        unsigned int idx2 = rand()%edgePositions.size();
        unsigned int idx3 = rand()%edgePositions.size();
    
        // we need 3 different samples:
        if(idx1 == idx2) continue;
        if(idx1 == idx3) continue;
        if(idx3 == idx2) continue;
    
        // create circle from 3 points:
        cv::Point2f center; float radius;
        getCircle(edgePositions[idx1],edgePositions[idx2],edgePositions[idx3],center,radius);
    
        float minCirclePercentage = 0.4f;
    
        // inlier set unused at the moment but could be used to approximate a (more robust) circle from alle inlier
        std::vector<cv::Point2f> inlierSet;
    
        //verify or falsify the circle by inlier counting:
        float cPerc = verifyCircle(dt,center,radius, inlierSet);
    
        if(cPerc >= minCirclePercentage)
        {
            std::cout << "accepted circle with " << cPerc*100.0f << " % inlier" << std::endl;
            // first step would be to approximate the circle iteratively from ALL INLIER to obtain a better circle center
            // but that's a TODO
    
            std::cout << "circle: " << "center: " << center << " radius: " << radius << std::endl;
            cv::circle(color, center,radius, cv::Scalar(255,255,0),1);
    
            // accept circle => remove it from the edge list
            cv::circle(mask,center,radius,cv::Scalar(0),10);
    
            //update edge positions and distance transform
            edgePositions = getPointPositions(mask);
            cv::distanceTransform(255-mask, dt,CV_DIST_L1, 3);
        }
    
        cv::Mat tmp;
        mask.copyTo(tmp);
    
        // prevent cases where no fircle could be extracted (because three points collinear or sth.)
        // filter NaN values
        if((center.x == center.x)&&(center.y == center.y)&&(radius == radius))
        {
            cv::circle(tmp,center,radius,cv::Scalar(255));
        }
        else
        {
            std::cout << "circle illegal" << std::endl;
        }
    
        ++nIterations;
        cv::namedWindow("RANSAC"); cv::imshow("RANSAC", tmp);
    }
    
    std::cout << nIterations <<  " iterations performed" << std::endl;
    
    
    cv::namedWindow("edges"); cv::imshow("edges", mask);
    cv::namedWindow("color"); cv::imshow("color", color);
    
    cv::imwrite("detectedCircles.png", color);
    cv::waitKey(-1);
    return 0;
    }
    
    
    float verifyCircle(cv::Mat dt, cv::Point2f center, float radius, std::vector<cv::Point2f> & inlierSet)
    {
     unsigned int counter = 0;
     unsigned int inlier = 0;
     float minInlierDist = 2.0f;
     float maxInlierDistMax = 100.0f;
     float maxInlierDist = radius/25.0f;
     if(maxInlierDist<minInlierDist) maxInlierDist = minInlierDist;
     if(maxInlierDist>maxInlierDistMax) maxInlierDist = maxInlierDistMax;
    
     // choose samples along the circle and count inlier percentage
     for(float t =0; t<2*3.14159265359f; t+= 0.05f)
     {
         counter++;
         float cX = radius*cos(t) + center.x;
         float cY = radius*sin(t) + center.y;
    
         if(cX < dt.cols)
         if(cX >= 0)
         if(cY < dt.rows)
         if(cY >= 0)
         if(dt.at<float>(cY,cX) < maxInlierDist)
         {
            inlier++;
            inlierSet.push_back(cv::Point2f(cX,cY));
         }
     }
    
     return (float)inlier/float(counter);
    }
    
    
    inline void getCircle(cv::Point2f& p1,cv::Point2f& p2,cv::Point2f& p3, cv::Point2f& center, float& radius)
    {
      float x1 = p1.x;
      float x2 = p2.x;
      float x3 = p3.x;
    
      float y1 = p1.y;
      float y2 = p2.y;
      float y3 = p3.y;
    
      // PLEASE CHECK FOR TYPOS IN THE FORMULA :)
      center.x = (x1*x1+y1*y1)*(y2-y3) + (x2*x2+y2*y2)*(y3-y1) + (x3*x3+y3*y3)*(y1-y2);
      center.x /= ( 2*(x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2) );
    
      center.y = (x1*x1 + y1*y1)*(x3-x2) + (x2*x2+y2*y2)*(x1-x3) + (x3*x3 + y3*y3)*(x2-x1);
      center.y /= ( 2*(x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2) );
    
      radius = sqrt((center.x-x1)*(center.x-x1) + (center.y-y1)*(center.y-y1));
    }
    
    
    
    std::vector<cv::Point2f> getPointPositions(cv::Mat binaryImage)
    {
     std::vector<cv::Point2f> pointPositions;
    
     for(unsigned int y=0; y<binaryImage.rows; ++y)
     {
         //unsigned char* rowPtr = binaryImage.ptr<unsigned char>(y);
         for(unsigned int x=0; x<binaryImage.cols; ++x)
         {
             //if(rowPtr[x] > 0) pointPositions.push_back(cv::Point2i(x,y));
             if(binaryImage.at<unsigned char>(y,x) > 0) pointPositions.push_back(cv::Point2f(x,y));
         }
     }
    
     return pointPositions;
    }
    

input:

enter image description here

output:

enter image description here

console output:

    press q to stop
    accepted circle with 50 % inlier
    circle: center: [358.511, 211.163] radius: 193.849
    accepted circle with 85.7143 % inlier
    circle: center: [45.2273, 171.591] radius: 24.6215
    accepted circle with 100 % inlier
    circle: center: [257.066, 197.066] radius: 27.819
    circle illegal
    30 iterations performed`

optimization should include:

  1. use all inlier to fit a better circle

  2. dont compute distance transform after each detected circles (it's quite expensive). compute inlier from point/edge set directly and remove the inlier edges from that list.

  3. if there are many small circles in the image (and/or a lot of noise), it's unlikely to hit randomly 3 edge pixels or a circle. => try contour detection first and detect circles for each contour. after that try to detect all "other" circles left in the image.

  4. a lot of other stuff

Shuffleboard answered 22/12, 2013 at 21:32 Comment(1)
Thanks Micka... I will try this one as well... but your earlier solution is working great.. I will try to modify HoughCircles to eliminate Canny edge detection.Faintheart
C
2

@Micka's answer is great, here it is roughly translated into python

The method takes a thresholded image mask as an argument, leaving that part as an exercise for the reader

def get_circle_percentages(image):
    #compute distance transform of image
    dist = cv2.distanceTransform(image, cv2.DIST_L2, 0)
    rows = image.shape[0]
    circles = cv2.HoughCircles(image, cv2.HOUGH_GRADIENT, 1, rows / 8, 50, param1=50, param2=10, minRadius=40, maxRadius=90)      
    minInlierDist = 2.0
    for c in circles[0, :]:
        counter = 0
        inlier = 0

        center = (c[0], c[1])
        radius = c[2]

        maxInlierDist = radius/25.0

        if maxInlierDist < minInlierDist: maxInlierDist = minInlierDist

        for i in np.arange(0, 2*np.pi, 0.1):
            counter += 1
            x = center[0] + radius * np.cos(i)
            y = center[1] + radius * np.sin(i)

            if dist.item(int(y), int(x)) < maxInlierDist:
                inlier += 1
            print(str(100.0*inlier/counter) + ' percent of a circle with radius ' + str(radius) + " detected")
Cay answered 13/8, 2022 at 0:4 Comment(0)
L
1

I know that it's little bit late, but I used different approach which is much easier. From the cv2.HoughCircles(...) you get centre of the circle and the diameter (x,y,r). So I simply go through all centre points of the circles and I check if they are further away from the edge of the image than their diameter.

Here is my code:

        height, width = img.shape[:2]

        #test top edge
        up = (circles[0, :, 0] - circles[0, :, 2]) >= 0

        #test left edge
        left = (circles[0, :, 1] - circles[0, :, 2]) >= 0

        #test right edge
        right = (circles[0, :, 0] + circles[0, :, 2]) <= width

        #test bottom edge
        down = (circles[0, :, 1] + circles[0, :, 2]) <= height

        circles = circles[:, (up & down & right & left), :]
Lampoon answered 31/1, 2017 at 17:6 Comment(0)
S
0

The semicircle detected by the hough algorithm is most probably correct. The issue here might be that unless you strictly control the geometry of the scene, i.e. exact position of the camera relative to the target, so that the image axis is normal to the target plane, you will get ellipsis rather than circles projected on the image plane. Not to mention the distortions caused by the optical system, which further degenerate the geometric figure. If you rely on precision here, I would recommend camera calibration.

Succursal answered 20/12, 2013 at 12:59 Comment(1)
The object is kept on a glass surface which is mounted parallel to the camera. I agree there's some calibration issues in the image... but it's more to do with centre of circle being detected as [90.3, 87.5] instead of [90, 87]...Faintheart
D
0

You better try with different kernel for gaussian blur.That will help you

GaussianBlur( src_gray, src_gray, Size(11, 11), 5,5);

so change size(i,i),j,j)

Distasteful answered 5/4, 2014 at 7:57 Comment(1)
I would consider using multiple Gaussian kernels and perhaps making a scale-space if you want to go this route. A single change to the kernel would not be enough.Infirm

© 2022 - 2024 — McMap. All rights reserved.