在 OpenCV 中检测半圆
Posted
技术标签:
【中文标题】在 OpenCV 中检测半圆【英文标题】:Detect semicircle in OpenCV 【发布时间】:2014-01-09 00:28:54 【问题描述】:我正在尝试检测图像中的全圆和半圆。
我正在遵循以下提到的程序: 过程图像(包括 Canny 边缘检测)。 找到轮廓并将它们绘制在空白图像上,这样我就可以消除不需要的组件 (处理后的图像正是我想要的)。 使用 HoughCircles 检测圆圈。而且,这就是我得到的:
我尝试改变 HoughCircles 中的参数,但结果并不一致,因为它会根据光照和图像中圆圈的位置而变化。 我根据其大小接受或拒绝一个圆圈。所以,结果是不能接受的。另外,我有一长串“可接受的”圈子。所以,我需要在 HoughCircle 参数中留一些余量。 至于完整的圆圈,这很容易——我可以简单地找到轮廓的“圆度”。问题是半圆!
请在霍夫变换之前找到编辑后的图像
【问题讨论】:
您可以检测图像中的哪些边缘(在精明和霍夫之后)属于圆形/半圆形是否正确?你的问题是,霍夫结果的圆圈位置不够好?用所有圆边(或至少 3 个正确的圆边像素)拟合一个参数圆怎么样? (三个点定义一个圆!)...为了使其更健壮,您可以使用 RANSAC 算法(内点/异常值计数 => 巨大的缺失部分 = 半圆)。没试过,但可能有用?!? 谢谢米卡!你说的参数圆是霍夫变换算法本身,不是吗?如果你看到 Hough 的结果,超过 3 个点位于检测到的圆圈上!我对RANSAC一无所知,我去看看 你能在计算/绘制霍夫圆之前提供边缘图像吗?您是否尝试过用较少的线粗绘制轮廓? 啊,我明白了... HoughTransform 内部使用 canny,因此需要线粗,但对于完整的圆圈,最好填充轮廓。 我看到我不需要在空白图像上绘制轮廓。这是 Canny + 高斯模糊。 【参考方案1】:直接在图像上使用houghCircle
,不要先提取边缘。
然后测试每个检测到的圆圈,图像中实际存在多少百分比:
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;
对于这个输入:
它给出了这个输出:
红色圆圈是霍夫结果。
圆圈上的绿色采样点是内点。
蓝点是异常值。
控制台输出:
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
如果您想测试 RANSAC 而不是 Hough,请查看 this。
【讨论】:
太棒了!它有效:) 但为什么它不能在边缘工作?我的目标是通过对轮廓大小设置下限来减少错误的圆圈识别......这样我就可以得到一个干净的处理图像。 我不确定。我猜这是因为 openCv 的 houghCircle 在内部使用了 canny。如果你在边缘工作,canny 不起作用。如果您在较厚的边缘上工作,canny 会给出 2 个相互靠近的圆圈,这些圆圈会相互干扰。但这只是猜测。 @CésarHoyos 这些值是“凭经验”选择的。切换到半径依赖 maxInlierDist 的原因是因为适合边缘的圆的质量可能取决于圆的大小。 好吧好吧你是对的。然后我必须为我自己的项目寻找这个参数值。谢谢! @CésarHoyos 这取决于您对精度的需求。如果您的圈子没有任何失真并且可以很好地检测到,那么较低的常数阈值可能会很好。【参考方案2】:这是另一种方法,一个简单的 RANSAC 版本(需要进行大量优化以提高速度),适用于边缘图像。
该方法循环这些步骤直到被取消
-
随机选择 3 个边缘像素
从中估计圆(3 个点足以识别一个圆)
验证或伪造它确实是一个圆:计算给定边代表圆的百分比
如果验证了一个圆圈,则从 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;
输入:
输出:
控制台输出:
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`
优化应包括:
使用所有内点来拟合更好的圆
不要在每个检测到的圆圈之后计算距离变换(这非常昂贵)。直接从点/边集计算内点,并从该列表中删除内点。
如果图像中有许多小圆圈(和/或大量噪点),则不太可能随机命中 3 个边缘像素或一个圆圈。 => 首先尝试轮廓检测并检测每个轮廓的圆圈。之后尝试检测图像中剩下的所有“其他”圆圈。
很多其他的东西
【讨论】:
谢谢 Micka...我也会尝试这个...但是您之前的解决方案效果很好...我会尝试修改 HoughCircles 以消除 Canny 边缘检测。【参考方案3】:我知道这有点晚了,但我使用了不同的方法,这更容易。
从cv2.HoughCircles(...)
你得到圆心和直径 (x,y,r)。所以我只是简单地遍历圆的所有中心点,然后检查它们是否比它们的直径更远离图像边缘。
这是我的代码:
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), :]
【讨论】:
【参考方案4】:霍夫算法检测到的半圆很可能是正确的。这里的问题可能是,除非您严格控制场景的几何形状,即相机相对于目标的精确位置,以便图像轴垂直于目标平面,否则您将得到椭圆而不是圆形投影在图像上飞机。更不用说由光学系统引起的畸变,这进一步退化了几何图形。如果您在这里依赖精度,我会推荐camera calibration。
【讨论】:
对象保持在与相机平行安装的玻璃表面上。我同意图像中存在一些校准问题...但更多的是与圆心被检测为 [90.3, 87.5] 而不是 [90, 87]...【参考方案5】:您最好尝试使用不同的内核来实现高斯模糊。这将对您有所帮助
GaussianBlur( src_gray, src_gray, Size(11, 11), 5,5);
所以改变size(i,i),j,j)
【讨论】:
我会考虑使用多个高斯内核,如果你想走这条路,也许可以制作一个比例空间。对内核进行一次更改是不够的。以上是关于在 OpenCV 中检测半圆的主要内容,如果未能解决你的问题,请参考以下文章