在图像处理中,Hough变换(霍夫变换)主要用来识别已知的几何形状,最常见的比如直线、线段、圆形、椭圆、矩形等。如果要检测比较复杂的曲线图形,就需要利用广义霍夫变换。
霍夫变换的原理是根据参数空间的统计规律进行参数估计。
具体说来就是,将直角坐标系中的图形(x,y)变换到参数空间(k1,...,kn),对直角坐标系中的每一个像素点,计算它在参数空间里的所有可能的参数向量。处理完所有像素点后,把出现次数(频率)最多的(一个或几个)参数向量的坐标作为参数代入直角坐标方程,即检测到的图形方程。
以直线检测为例,详细讲一下步骤:(圆和直线的原理相同,只是直线的公式比较好打~)
1.图像二值化,待检测的线变为黑色,背景置为白色。既然是形状检测,这步是必不可少的。
2.假设直线的参数方程为p=x*cosa+y*sina,对于直线上的某个点(x,y)来说,变换到参数空间的坐标就是(p,a),而且这条直线上的所有点都对应于(p,a)。对于一个固定点(x,y)来说,经过它的直线系可以表示为p=(x^2+y^2)^1/2*sin(a+b),其中tanb=x/y,对应参数空间里的一条正弦曲线。也就是说,图像中的一条直线对应参数空间的一点,图像中的一点对应参数空间的一条正弦曲线。
关于参数变换,我再白话几句。如果直线方程写成y=k*x-b,则对应于参数空间里的点(k,-b),这就有点像图论中的对偶变换了。在写图的程序时有时会遇到若干半平面求交的问题(整张平面被一条直线分割后得到两张半平面)。半平面求交关键在于找到求交后的边界(如果交集非空),既可以使用递增式算法(在已经找到的一部分边界基础上引入下一张半平面的直线求下一步的边界),也可以使用上面提到的参数变换方法。
比如我想求几张方向都朝上(y轴正方向)的半平面的交,我想得到的应该是一个下侧以向下凸的折线为边界的上侧无穷的区域。我的问题关键在于找到这条下凸的折线。直线y=k*x-b做参数变换,得到点(k,-b),所有半平面的边界直线经变换得到若干个点。这些点形成的点集存在一个凸包(包含点集的最小凸多边形,而且该多边形每个顶点都来自点集),其中构成折线的直线所对应的点恰好是凸包的上半部分,也就是“下包络”变换成上凸包。而求点集的上凸包可是很简单的(也是增量式算法)。
3.把参数空间分割为n*m个格子,得到参数矩阵,矩阵元(pi,aj)的初始值均为0,用来对参数计数。计数值代表这个参数是最终结果的可能性,计数值越大,说明落在这条直线上的像素点越多,也就说明它越有可能是我们想找到的参数。p的范围可以是[0,图像对角线长度],a的范围可以是[0,PI/2](如果取左上角为原点的话),但要包含整个图像。
4.按照栅格顺序扫描图像,遇到黑色像素就做如下操作:
pi的i从0取到n-1,对每一个pi,把它和像素点的坐标(x,y)代入参数方程,计算得到相应的ai,如果ai在定义域范围内(或者在图像内),将矩阵元(pi,ai)加一。
处理完所有像素后,如果想识别d条直线,就在参数矩阵中找到前d个数值最大的矩阵元,他们的坐标作为方程参数,在直角坐标系绘制出直线就可以了。
OpenCV中提供了计算霍夫变换的库函数HoughLines和HoughLinesP,想知道怎样使用,请戳传送门。
圆形检测的过程很类似,只是参数方程有变化,而且参数空间增加了一个维度(圆心坐标x,y和半径r)。
霍夫变换的一个好处就是不需要图像中出现完整的圆,只要落在一个圆上的像素数量足够多,就能正确识别。
关于误差的问题:如果待检测像素没有严格落在同一个圆上,比如构成圆的圆弧彼此有些错位,如果依据参数点最多准则,只会识别出弧长最长的圆弧而忽略其他本来也属于同一个圆的圆弧。如果目标是检测不止一个圆,这种误差可能会使得程序依据同一个圆上的几个圆弧识别到几个不同的圆。解决这个问题一种方法是仍然采用参数点最多准则,但减小参数空间分割的份数,让错位圆弧的圆心落在同一个参数矩阵元上,但这样做会使检测到的圆心位置有比较大的误差。另一种方法是仍然把参数空间细密分割,用聚类算法寻找可能的圆心,因为错位圆弧的圆心彼此靠得很近而且计数值都很大,只要找到这些点的外接圆圆心就可以了。
下面为了计算简便,我给出只检测一个半径为100的圆形的代码(要想采用聚类算法,只需修改第71-81行的代码块):
#include "stdafx.h" #include "highgui.h" #include "cv.h" #include <math.h> #define X_MAX 400 #define Y_MAX 400 #define TO_BE_BLACK 40 //radius of circles is known int houghTrans_r(IplImage *src, IplImage *dst, IplImage *tmp, float r, int xstep, int ystep) { int width = src->width; int height = src->height; int channel = src->nChannels; int xmax = width%xstep ? width/xstep+1 : width/xstep; int ymax = height%ystep ? height/ystep+1 : height/ystep; int i,j,x,y; int para[X_MAX][Y_MAX] = {0}; //i,j are in the pixel space //x,y are in the parameter space for(j=0; j<height; j++) { uchar* pin = (uchar*)(src->imageData + j*src->widthStep); for(i=0; i<width; i++) { //pixel is black if(pin[channel*i] < TO_BE_BLACK) { float temp; //calculate every probable y-cord based on x-cord for(x=0; x<xmax; x++) { temp = r*r - (i-x*xstep)*(i-x*xstep); temp = sqrt(temp); y = j - (int)temp; if(y>=0 && y<height){ para[x][y/ystep]++; } y = j + (int)temp; if(y>=0 && y<height){ para[x][y/ystep]++; } } } } } //find circle in parameter space int paramax=0,findx=-1,findy=-1; for(y=0; y<ymax; y++) { for(x=0; x<xmax; x++) { if(para[x][y] > paramax) { paramax=para[x][y]; findx=x; findy=y; } } } //draw the parameter space image int ii,jj; for(y=0; y<ymax; y++) { uchar* pout = (uchar*)(tmp->imageData + y*tmp->widthStep); for(x=0; x<xmax; x++) { pout[channel*x]=para[x][y]*255/paramax; pout[channel*x+1]=para[x][y]*255/paramax; pout[channel*x+2]=para[x][y]*255/paramax; } } //draw the found circle if(findx>=0 && findy>=0) { for(j=0;j<height;j++) { uchar* pin=(uchar*)(src->imageData+j*src->widthStep); uchar* pout=(uchar*)(dst->imageData+j*dst->widthStep); for(i=0;i<width;i++) { pout[3*i]=128+pin[3*i]/2; pout[3*i+1]=128+pin[3*i+1]/2; pout[3*i+2]=128+pin[3*i+2]/2; } } cvCircle(dst,cvPoint(findx*xstep+xstep/2.0,findy*ystep+ystep/2.0),r,cvScalar(255,0,0),1,8,0); } return 1; } int main() { IplImage *srcImg=cvLoadImage("H:\circle_4.jpg"); cvNamedWindow("Src",CV_WINDOW_AUTOSIZE); cvNamedWindow("Result",CV_WINDOW_AUTOSIZE); cvNamedWindow("Temp",CV_WINDOW_AUTOSIZE); IplImage *houghImg = cvCreateImage(cvGetSize(srcImg),IPL_DEPTH_8U,3); IplImage *houghTmp = cvCreateImage(cvGetSize(srcImg),IPL_DEPTH_8U,3); houghTrans_r(srcImg,houghImg,houghTmp,100.0,1,1); cvShowImage("Src",srcImg); cvShowImage("Temp",houghTmp); cvShowImage("Result",houghImg); cvWaitKey(0); cvReleaseImage(&srcImg); cvReleaseImage(&houghImg); cvDestroyWindow("Src"); cvDestroyWindow("Result"); return 0; }
由于固定半径r,所以参数就是圆心位置(x,y),绘制的点代表圆心的可能位置,颜色越浅,可能性越大。
检测单独的圆
在很乱的线中检测不完整的圆
检测彼此错位的圆弧(参数划分扩大为5*5)