霍夫圆检测的准确性非常低

3
我会尝试从一张非常清晰的图像中检测一个圆形。我知道部分圆形的形状可能会失真,但是根据我对霍夫变换的了解,这不应该导致我遇到的问题。
输入: Output image 输出: enter image description here 代码:
// Read the image
Mat src = Highgui.imread("input.png");

// Convert it to gray
Mat src_gray = new Mat();
Imgproc.cvtColor(src, src_gray, Imgproc.COLOR_BGR2GRAY);

// Reduce the noise so we avoid false circle detection
//Imgproc.GaussianBlur( src_gray, src_gray, new Size(9, 9), 2, 2 );

Mat circles = new Mat();

/// Apply the Hough Transform to find the circles
Imgproc.HoughCircles(src_gray, circles, Imgproc.CV_HOUGH_GRADIENT, 1, 1, 160, 25, 0, 0);

// Draw the circles detected
for( int i = 0; i < circles.cols(); i++ ) {
    double[] vCircle = circles.get(0, i);

    Point center = new Point(vCircle[0], vCircle[1]);
    int radius = (int) Math.round(vCircle[2]);

    // circle center
    Core.circle(src, center, 3, new Scalar(0, 255, 0), -1, 8, 0);
    // circle outline
    Core.circle(src, center, radius, new Scalar(0, 0, 255), 3, 8, 0);
}

// Save the visualized detection.
String filename = "output.png";
System.out.println(String.format("Writing %s", filename));
Highgui.imwrite(filename, src);

我将高斯模糊注释掉了,因为(直觉上)它会大大增加找到的同样不准确的圆的数量。

我的输入图像有什么问题会导致霍夫变换的效果不如我预期吗?我的参数是否严重偏离?

编辑:第一个答案提出了关于Hough最小/最大半径的提示。我抵制添加这些参数,因为本帖子中的示例图像只是成千上万个具有不同半径的图像之一,范围从约20到无限大。


1
我认为问题在于,Imgproc.HoughCircles 内部使用了梯度过滤器,并且不假定输入是线数据。因此,在梯度计算之后,您的某些线条可能会消失或重复出现。 - Micka
1
现在我想起来感觉很有道理!我原以为在黑色背景上给它简单的白线条会是最理想的,但现在我明白可能并非如此。有没有一种方法告诉OpenCV我不想让HoughCircles()对输入进行预处理?我现在打算使用未经过Canny处理的原始图像尝试一下,看看是否有明显的差异。 - Zeb Barnett
理论上,openCV有不同的方法来代替CV_HOUGH_GRADIENT,但根据文档,它们尚未实现。我曾经在GIMP中尝试过填充圆形,但结果并不好。也许你的圆形不够完美或者其他原因。我以前用RANSAC实现了半圆检测,我可以尝试为你的任务进行调整,但目前不能保证什么(而且需要使用c++)。 - Micka
我也曾经得到过非常糟糕的结果。问题在于,我像你一样将HoughCircles累加器数组分辨率(即dp设置)保留为默认值“1”。将其增加到1.7后,我对令人惊叹的亚像素完美精度感到非常满意。不过,HoughCircles有点挑剔,一些杂散的锯齿状像素可能会通过边缘梯度(从而投票)将切线偏离错误方向,导致找到的圆形偏离半个半径。如果您在开始之前不知道半径,则必须处理放置不正确的圆。 - Eric Leschinski
2个回答

6
我从这个答案调整了我的RANSAC算法:在opencv中检测半圆 思路:
  1. 从二进制边缘图像中随机选择3个点
  2. 用这3个点创建一个圆
  3. 测试这个圆的“好坏”程度
  4. 如果它比先前在这幅图像中找到的最佳圆更好,则记住

  5. 循环1-4,直到达到一定次数。然后接受找到的最佳圆。

  6. 从图像中删除该接受的圆

  7. 重复执行1-6,直到找到所有圆

问题:
  1. 目前必须知道要在图像中找到多少个圆
  2. 仅针对那张图片进行了测试。
  3. c++代码
结果:

enter image description here

代码:
    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;
    }


    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);
    }

    float evaluateCircle(cv::Mat dt, cv::Point2f center, float radius)
    {

        float completeDistance = 0.0f;
        int counter = 0;

        float maxDist = 1.0f;   //TODO: this might depend on the size of the circle!

        float minStep = 0.001f;
        // choose samples along the circle and count inlier percentage

        //HERE IS THE TRICK that no minimum/maximum circle is used, the number of generated points along the circle depends on the radius.
        // if this is too slow for you (e.g. too many points created for each circle), increase the step parameter, but only by factor so that it still depends on the radius

        // the parameter step depends on the circle size, otherwise small circles will create more inlier on the circle
        float step = 2*3.14159265359f / (6.0f * radius);
        if(step < minStep) step = minStep; // TODO: find a good value here.

        //for(float t =0; t<2*3.14159265359f; t+= 0.05f) // this one which doesnt depend on the radius, is much worse!
        for(float t =0; t<2*3.14159265359f; t+= step)
        {
            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) <= maxDist)
                            {
                                completeDistance += dt.at<float>(cY,cX);
                                counter++;
                            }

        }

        return counter;
    }


    int main()
    {
    //RANSAC

    cv::Mat color = cv::imread("HoughCirclesAccuracy.png");

    // convert to grayscale
    cv::Mat gray;
    cv::cvtColor(color, gray, CV_RGB2GRAY);

    // get binary image
    cv::Mat mask = gray > 0;

    unsigned int numberOfCirclesToDetect = 2;   // TODO: if unknown, you'll have to find some nice criteria to stop finding more (semi-) circles

    for(unsigned int j=0; j<numberOfCirclesToDetect; ++j)
    {
        std::vector<cv::Point2f> edgePositions;
        edgePositions = getPointPositions(mask);

        std::cout << "number of edge positions: " << edgePositions.size() << std::endl;

        // create distance transform to efficiently evaluate distance to nearest edge
        cv::Mat dt;
        cv::distanceTransform(255-mask, dt,CV_DIST_L1, 3);



        unsigned int nIterations = 0;

        cv::Point2f bestCircleCenter;
        float bestCircleRadius;
        //float bestCVal = FLT_MAX;
        float bestCVal = -1;

        //float minCircleRadius = 20.0f; // TODO: if you have some knowledge about your image you might be able to adjust the minimum circle radius parameter.
        float minCircleRadius = 0.0f;

        //TODO: implement some more intelligent ransac without fixed number of iterations
        for(unsigned int i=0; i<2000; ++i)
        {
            //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);

            if(radius < minCircleRadius)continue;


            //verify or falsify the circle by inlier counting:
            //float cPerc = verifyCircle(dt,center,radius, inlierSet);
            float cVal = evaluateCircle(dt,center,radius);

            if(cVal > bestCVal)
            {
                bestCVal = cVal;
                bestCircleRadius = radius;
                bestCircleCenter = center;
            }

            ++nIterations;
        }
        std::cout << "current best circle: " << bestCircleCenter << " with radius: " << bestCircleRadius << " and nInlier " << bestCVal << std::endl;
        cv::circle(color,bestCircleCenter,bestCircleRadius,cv::Scalar(0,0,255));

        //TODO: hold and save the detected circle.

        //TODO: instead of overwriting the mask with a drawn circle it might be better to hold and ignore detected circles and dont count new circles which are too close to the old one.
        // in this current version the chosen radius to overwrite the mask is fixed and might remove parts of other circles too!

        // update mask: remove the detected circle!
        cv::circle(mask,bestCircleCenter, bestCircleRadius, 0, 10); // here the radius is fixed which isnt so nice.
    }

    cv::namedWindow("edges"); cv::imshow("edges", mask);
    cv::namedWindow("color"); cv::imshow("color", color);

    cv::imwrite("detectedCircles.png", color);
    cv::waitKey(-1);
    return 0;
    }

哇!这看起来非常类似于我们目前正在使用的方法。我没有意识到它有一个名字。你知道图像中大量随机噪声是否会以负面方式影响这种方法的结果吗? - Zeb Barnett
有噪音可能会导致一些变化:更难获得一个好的二进制边缘图像。带距离变换的圆形测试可能会得到虚假的内点。而且你需要比没有噪音时更多的RANSAC迭代次数。但是真正的圆仍然应该比噪声样本给出更好的圆形测试,所以我想这种方法仍然可以工作。 - Micka
1
看起来RANSAC算法正是我所需要的,我将在我的程序中使用它的一个变体。然而,我会接受其他答案,因为它实际上解释了为什么我的Hough圆结果不够好以及如何改进它们。非常感谢您的建议! - Zeb Barnett

2
如果您正确设置了minRadiusmaxRadius参数,它将给您带来良好的结果。
对于您的图像,我尝试了以下参数。
method - CV_HOUGH_GRADIENT
minDist - 100
dp - 1
param1 - 80
param2 - 10
minRadius - 250
maxRadius - 300

我得到了以下输出 enter image description here 注意:我在C++中尝试过这个。

1
谢谢回复!我可能应该在原帖中提到最小和最大半径,但我发布的图片只是成千上万张图片中的一个示例,所有这些图片的半径都不同,从约20到约500不等。此外,虽然您的结果比我的好得多,但仍然不是我认为“好”的程度。我正在尝试确定两个圆的交点处的确切角度,因此底部圆形左侧的误差会带来相当大的问题。再次感谢您的贡献! - Zeb Barnett
霍夫变换并不是完美的,最小和最大半径是重要参数。我只是在这里留下一个链接。我最近在图像中检测了大约200个圆,结果非常准确。http://ceng.anadolu.edu.tr/CV/EDCircles/demo.aspx 目前该链接似乎无法访问。我上周才使用过它,请看看是否有效。这是一个很好的解决方案。EDCircles - Froyo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接