【问题标题】:Hough circle detection accuracy very low霍夫圆检测精度很低
【发布时间】:2014-06-01 04:28:25
【问题描述】:

我正在尝试从看起来具有非常好的清晰度的图像中检测圆形。我确实意识到圆圈的一部分丢失了,但从我所读到的关于霍夫变换的内容看来,这似乎不会导致我遇到的问题。

输入:

输出:

代码:

// 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 不能像我预期的那样工作吗?我的参数有问题吗?

编辑:第一个答案提出了一个关于 Hough 的最小/最大半径提示的好点。我拒绝添加这些参数,因为这篇文章中的示例图像只是数千张图像中的一张,这些图像的半径从大约 20 到几乎无穷大不等。

【问题讨论】:

  • 我认为问题在于,Imgproc.HoughCircles 内部使用了梯度过滤器并且不假设行数据输入。所以在梯度计算之后,你的一些行可能会消失或加倍。
  • 现在想想,这很有意义!我曾认为在黑色背景上给它简单的白线是理想的,但我现在发现它可能不是。有没有办法告诉 OpenCV 我不希望 HoughCircles() 预处理输入?我现在将尝试使用 Canny 处理之前的原始图像,看看是否有明显差异。
  • 理论上openCV有不同的方法而不是CV_HOUGH_GRADIENT,但是根据文档,它们还没有实现。我玩了一些填充圆圈(在 GIMP 中),但也没有得到好的结果。也许你的圈子不够完美之类的。我过去使用 RANSAC 实现了半圆检测,我可以尝试针对您的任务进行调整,但目前不能承诺任何事情(它会在 c++ 中)。
  • 我也得到了这些非常糟糕的结果。问题是我像您一样将 HoughCircles 累加器数组分辨率(又名(dp 设置))保留为默认的1。增加到 1.7,我对惊人的亚像素完美精度感到高兴。尽管如此,HoughCircles 还是有点挑剔,一些杂散的混叠像素可以通过边缘梯度(以及因此的选票)将切线发送到错误的方向,导致在半个半径之外找到圆。如果您在开始之前不知道半径,那么您将不得不处理放置错误的圆。

标签: java opencv image-processing computer-vision edge-detection


【解决方案1】:

我已经根据这个答案调整了我的 RANSAC 算法:Detect semi-circle in opencv

想法:

  1. 从二值边缘图像中随机选择 3 个点
  2. 从这 3 个点创建一个圆
  3. 测试这个圈子有多“好”
  4. 如果它比这张图片中之前最好找到的圆圈更好,请记住

  5. 循环 1-4 直到达到一定数量的迭代。然后接受最好的圈子。

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

  7. 重复 1-6 直到找到所有圆圈

问题:

  1. 此时您必须知道要在图像中找到多少个圆圈
  2. 仅针对那一张图片进行了测试。
  3. c++ 代码

结果:

代码:

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

【讨论】:

  • 哇!这看起来与我们目前使用的方法非常相似。我没有意识到它有一个名字。您知道图像中的大量随机噪声是否会对这种方法的结果产生负面影响?
  • 有了噪声,一些事情可能会发生变化:获得好的二进制边缘图像变得更加困难。带有距离变换的圆形测试可能会出现错误的内点。与没有噪声相比,您将需要更多的 RANSAC 迭代。但是真正的圆圈仍然应该比噪声样本提供更好的圆圈测试,所以我猜这个方法应该仍然有效。
  • 看起来 RANSAC 算法是我正在寻找的,我将在我的程序中使用它的变体。但是,我接受了另一个答案,因为它实际上解释了为什么我的霍夫圈结果乏善可陈以及如何改进它们。非常感谢您的意见!
【解决方案2】:

如果你正确设置minRadiusmaxRadius 参数,它会给你带来好的结果。

对于您的图像,我尝试了以下参数。

method - CV_HOUGH_GRADIENT
minDist - 100
dp - 1
param1 - 80
param2 - 10
minRadius - 250
maxRadius - 300

我得到以下输出

  • 注意:我在 C++ 中尝试过。

【讨论】:

  • 感谢您的回复!我可能应该在 OP 中提到最小和最大半径,但我发布的图像只是数千张图像中的一个示例,这些图像的半径从 ~20 到 ~500 不等。此外,与您的结果相匹配,虽然肯定比我的要好,但仍然不是我认为的“好”。我正在尝试确定两个圆圈相交处的确切角度,因此底部圆圈左侧的错误会造成很大的问题。再次感谢您的贡献!
  • 霍夫并不是那么完美,最小最大半径是重要的参数。我只是把这个链接留在这里。我最近用它来检测图像中大约 200 个圆圈。这是非常准确的。 ceng.anadolu.edu.tr/CV/EDCircles/demo.aspx 该链接目前似乎不可用。我上周才用过。看看它是否有效。这是一个很好的解决方案。 EDCircles
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-29
  • 2019-04-14
  • 2015-05-03
相关资源
最近更新 更多