前面几次的内容,包括基础知识介绍和综合训练,基本上都是围绕着霍夫变换检测直线来进行展开的。那么这次我要来探讨霍夫变换检测圆。话不多说,首先我们来简要地看看霍夫变换检测圆是什么原理。
霍夫变换检测圆的原理及其实现
前面的学习,让我对霍夫变换有了这样一种理解-----实际上就是坐标变换,是一种数学上的变换,然后再转换到参数坐标系进行讨论,最终确定待检测圆(或者其他形状)的数学方程,那么圆在直角坐标系中的数学表达式我相信小学生都知道:
R^2 = (X - a)^2 + (Y - b)^2
我们看到了这个式子是比较复杂的,我首先要明确目标,这个式子的参数有三个:R、a、b,我们实际上是要根据已知的X,Y来将上述未知的三个参数求出来,可想而知,我们要是用上面这个式子就麻烦了,三个参数在一个式子中混为一谈,所以,我们要找另外的数学表达式,这样我们自然而然想到一般要使用的坐标系---极坐标系,在极坐标系下,圆的数学表达式如下:
X0 = X + R * cos(Theta)
Y0 = Y + R * sin(Theta)
这样,三个参数就成功分离了两个X0、Y0,此处若我们给定检测的R,再利用Theta的有界性,我们就可以得到一组合适的X0,Y0,R,于是圆就找到了!
类似于霍夫变换直线检测,这里我们也定义一个累加器,但是与直线检测不同的是,直线检测参数是二维的,而圆的参数是三维的,于是,我们可以定义一个三维的累加器,实现代码(关键函数)如下:
//Hough变换检测圆的函数
IplImage* HoughForCircle(IplImage* img, int radius, int interval)
{
IplImage* gray = cvCreateImage(cvGetSize(img), 8, 1);
IplImage* result = cvCreateImage(cvGetSize(img), 8, 3);
cvCopy(img, result);
cvCvtColor(img, gray, CV_BGR2GRAY);
//定义并初始化累加器
int ***count;
count = new int**[radius];
for (int i = 0; i < radius; ++i)
{
count[i] = new int*[img->height];
}
for (int i = 0; i < radius; ++i)
{
for (int j = 0; j < img->width; ++j)
{
count[i][j] = new int[img->width];
}
}
for (int k = 0; k < radius; ++k)
{
for (int i = 0; i < img->height; i++)
{
for (int j = 0; j < img->width; j++)
{
count[k][i][j] = 0;
}
}
}
/*
已知半径,遍历角度,遍历图像像素,得到圆心坐标
*/
int x0 = 0, y0 = 0;
for (int k = 0; k < radius; ++k)
{
for (int i = 0; i < img->height; ++i)
{
for (int j = 0; j < img->width; ++j)
{
//判断是不是前景点,默认黑色为背景色,白色为前景色
CvScalar s = cvGet2D(img, i, j);
int data = s.val[0];//用该方法,牺牲时间,但是比较保险,不容易出错
if (data > 0)//是前景点
{
for (int theta = 0; theta < (1 / interval) * 360; theta++)
{
double t = ((1 / interval) * theta * CV_PI) / 180;
x0 = (int)cvRound(j - k * cos(t));
y0 = (int)cvRound(i - k * sin(t));
if (x0 < img->width && x0 > 0 && y0 < img->height && y0 > 0)
{
count[k][y0][x0] = count[k][y0][x0] + 1;
}
}
}
}
}
}
/*
寻找累加器中的最大值
*/
int max = 0;
int r = 0;
int x = 0, y = 0;
for (int k = 1; k < radius; ++k)
{
for (int i = 0; i < img->height; ++i)
{
for (int j = 0; j < img->width; ++j)
{
if (count[k][i][j] > max)
{
max = count[k][i][j];
x = j;
y = i;
r = k;
}
}
}
}
cout << x << endl;
cout << y << endl;
cout << r << endl;
CvPoint point;
point.x = x;
point.y = y;
//画圆
cvCircle(result, point, r, CV_RGB(0, 255, 0));
//释放三维数组
for (int i = 1; i < radius; ++i)
{
for (int j = 0; j < img->width; ++j)
{
delete [] count[i][j];
count[i][j] = NULL;
}
}
for (int i = 1; i < radius; ++i)
{
delete[] count[i];
count[i] = NULL;
}
delete[] count;
count = NULL;
return result;
}
这里要注意几点:1.上述只是我自己写的一个简单的例子,细心的读者会发现,这里只检测了累加器的最大值,也就是只检测了一个圆,这显然是不合理的。但是,要想改进也非常容易,可以规定检测几个圆或者通过最大值制定阈值···实现起来也就是稍微改进一下,并不复杂;
2.这里new了一个三维数组,那么一定要记得delete,否则会造成内存泄露,前面几次我都忽视了这个问题,这是不对的。
霍夫变换检测圆openCV实现
openCV提供了Hough变换检测圆的函数:
CvSeq* cvHoughCircles (
CvArr* image,
void circle_storage,
int method,
double dp,
double min_dist,
double param1 = 100,
double param2 = 300,
int min_radius = 0,
int max_radius = 0
);
输入参数解释如下:
image:当然是输入图像,这里和霍夫变换直线检测不一样的是,霍夫变换检测圆的输入图像允许是8位图像,而检测直线是需要输入二值图像;
circle_storage:这里类似于HoughLines2,既可以是数组也可以是内存的存储器,这取决于我们希望返回什么结果;
method:检测方法,这里只能是CV_HOUGH_GRADIENT
dp:指累加器图像的分辨率,必须要大于等于1。这个参数实际上允许创建一个比输入图像分辨率低的累加器;
min_dist:区分两个不同圆之间的最小距离
param1:canny算法的高阈值,低阈值设为其的一半
param2:累加器的阈值
使用代码参考如下:
IplImage* img = cvLoadImage("5.png");
IplImage* dst = cvCreateImage(cvGetSize(img), 8, 1);
cvCvtColor(img, dst, CV_RGB2GRAY);
CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* result = cvHoughCircles(dst, storage, CV_HOUGH_GRADIENT, 1, 10, 100, 36);
for (int i = 0; i < result->total; ++i)
{
float* p = (float*)cvGetSeqElem(result, i);
CvPoint point = cvPoint(cvRound(p[0]), cvRound(p[1]));
cvCircle(img, point, cvRound(p[2]), CV_RGB(0, 255, 0));
}
cvNamedWindow("dst");
cvShowImage("dst", img);
cvWaitKey(0);
cvDestroyAllWindows();
cvReleaseImage(&img);
cvReleaseImage(&dst);
惊奇的发现,openCV自带的函数居然检测效果还没有自己写的要好,这也说明了openCV的函数不是万能的。简单的了解下,我知道了openCV的函数使用了比较巧妙的方法,利用圆心必在经过圆上一点的切线的垂线方向这一定理,避免了x-y-r三维寻找圆速度慢的弊端。具体的实现方法(个人有没有想明白的地方,需要进一步想清楚再做实现):
1.首先确定前景点,即用canny算法边缘化,有必要的话要使用到图像细化
2.要找到每一个前景点切线的垂线方向,实际上就是找这个前景点的梯度方向,而这个梯度方向配合该前景点坐标可以确定该点切线的垂线方向的直线方程,通过这个直线方程遍历图像,是累加器加1
3.候选点必须大于给定阈值并且是近邻的局部最大值
4.比较累加器,将每个元素按降序排列,方便得出最有可能得中心
5.(不太明白)如何找半径
·······
虽然有不太明白的地方,但是,已经可以看出比上文中自写的程序思路要巧妙多了,这很好地解决了事先不知道圆半径的问题
(未完待续)