现在广泛使用的霍夫变换是由 Richard Duda 和 Peter Hart 在公元1972年发明,并称之为广义霍夫变换,广义霍夫变换和更早前1962年的Paul Hough 的专利有关,这也是其名称的又来 。 经典的霍夫变换是侦测图片中的直线,之后,霍夫变换不仅能识别直线,也能够识别任何形状,常见的有圆形、椭圆形。霍夫变换在1962年申请为专利U.S. Patent 3,069,654,其专利名为"辨识复杂图案的方法及手段"(Method and Means for Recognizing Complex Patterns)。 任一条直线可以由斜率和截距来表示,在该专利中,利用斜率和截距来将一条直线参数化,然而这会导致无界的转换空间(unbounded transform space),因为斜率有可能是无限大。1981年,因为Dana H. Ballard 的一篇期刊论文 "Generalizing the Hough transform to detect arbitrary shapes",让霍夫变换开始流行于电脑视觉界。
OpenCV实现了以下两种霍夫线变换:
- 原理在上面的部分已经说明了. 它能给我们提供一组参数对
的集合来表示检测到的直线
- 在OpenCV 中通过函数 HoughLines 来实现
- 这是执行起来效率更高的霍夫线变换. 它输出检测到的直线的端点
![]()
- 在OpenCV 中它通过函数 HoughLinesP 来实现
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
void help(){
cout << "
This program demonstrates line finding with the Hough transform.
"
"Usage:
"
"./houghlines <image_name>, Default is pic1.jpg
" << endl;}
int main(int argc, char** argv){
const char* filename = argc >= 2 ? argv[1] : "pic1.jpg";
Mat src = imread(filename, 0);
if(src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
Mat dst, cdst;
Canny(src, dst, 50, 200, 3);
cvtColor(dst, cdst, CV_GRAY2BGR);
#if 0
vector<Vec2f> lines;
HoughLines(dst, lines, 1, CV_PI/180, 100, 0, 0 );
for( size_t i = 0; i < lines.size(); i++ )
{
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA);
}
#else
vector<Vec4i> lines;
HoughLinesP(dst, lines, 1, CV_PI/180, 50, 50, 10 );
for( size_t i = 0; i < lines.size(); i++ )
{
Vec4i l = lines[i];
line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);
}
#endif
imshow("source", src);
imshow("detected lines", cdst);
waitKey();
return 0;}
加载图片
Mat src = imread(filename, 0);if(src.empty()){
help();
cout << "can not open " << filename << endl;
return -1;}
用Canny算子对图像进行边缘检测
Canny(src, dst, 50, 200, 3);
现在我们将要执行霍夫线变换. 我们将会说明怎样使用OpenCV的函数做到这一点:
标准霍夫线变换
首先, 你要执行变换:
vector<Vec2f> lines;
HoughLines(dst, lines, 1, CV_PI/180, 100, 0, 0 );
带有以下自变量:
通过画出检测到的直线来显示结果.
for( size_t i = 0; i < lines.size(); i++ ){
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line( cdst, pt1, pt2, Scalar(0,0,255), 3, CV_AA);}
统计概率霍夫线变换
1.首先, 你要执行变换:
vector<Vec4i> lines;
HoughLinesP(dst, lines, 1, CV_PI/180, 50, 50, 10 );
带有以下自变量:
通过画出检测到的直线来显示结果.
for( size_t i = 0; i < lines.size(); i++ ){
Vec4i l = lines[i];
line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 3, CV_AA);}
显示原始图像和检测到的直线:
imshow("source", src);imshow("detected lines", cdst);
等待用户按键推出程序
waitKey();
代码:
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <math.h>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat img, gray;
if( argc != 2 || !(img=imread(argv[1], 1)).data)
return -1;
cvtColor(img, gray, COLOR_BGR2GRAY);
// smooth it, otherwise a lot of false circles may be detected
GaussianBlur( gray, gray, Size(9, 9), 2, 2 );
vector<Vec3f> circles;
HoughCircles(gray, circles, HOUGH_GRADIENT,2, gray.rows/4, 200, 100 );
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]);
// draw the circle center
circle( img, center, 3, Scalar(0,255,0), -1, 8, 0 );
// draw the circle outline
circle( img, center, radius, Scalar(0,0,255), 3, 8, 0 );
}
namedWindow( "circles", 1 );
imshow( "circles", img );
waitKey(0);
return 0;
}
2.3相关函数具体调用
void HoughLines(InputArray image,OutputArray lines, double rho, double theta, int threshold, double srn=0,double stn=0 )
第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量水平线)。
第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。PS:Latex中/rho就表示 。
第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
C++: void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0 )
第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量是每个检测到的线段的结束点。
第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。
C++: void HoughCircles(InputArray image,OutputArray circles, int method, double dp, double minDist, double param1=100,double param2=100, int minRadius=0, int maxRadius=0)
第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。
第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。
第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。
第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。
第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。
在自动化分析数位图片的问题里,其中一个常有的子问题是侦测某些简单的直线、圆形、椭圆形。在多数情况下,边缘侦测器(edge detector)会先用来做图片前处理,将原本的图片变成只含有边缘的图片。 因为图片的不完美或是边缘侦测的不完美,导致有些点(point)或像素(pixel)缺漏,或是有噪声使得边缘侦测器所得的边界偏离了实际的边界。所以无法直观的将检测出的边缘分成直线、圆形、椭圆形的集合, 而霍夫变换解决上述问题,借由霍夫变换算法中的投票步骤,在复杂的参数空间中找到图形的参数,电脑可以由参数得知该边缘(edge)是哪种形状。
3.1 霍夫线变换原理。
3.1.1 在平面直角坐标系(x-y)中,一条直线可以用方程式
表示,而
可以视为参数空间
中的一点。当直线垂直于轴时,斜率为无限大, 若用电脑数值计算时会很不方便。
3.1.2 Duda 和 Hart 提出使用Hesse normal form来表示直线的参数,但是这个本身的原理比较繁琐,我们可以通过图像和极坐标的方法来进行简化。
另一方面,这条直线的斜率(红色)是tan(PI-Θ) = tan(PI/2 + Θ),根据诱导公式: tan(π/2+α)=-cotα ,也就是斜率为:-cot(Θ)
根据2.1.1中得到的定义,这条直线可以表示为:
那么给定一个点,通过该点的所有直线的参数
的集合,会在
平面上形成一个三角函数,可由下方程式证明
因此,给定很多点,判断这些点是否共线(concurrent lines)的问题,经由霍夫变换之后,变成判断一堆曲线(每一个点在平面上代表一条曲线)是否 在
平面上相交于同一点的问题(concurrent curves)。继续上面那张抽象的图,如果两个不同点进行上述操作后得到的曲线在平面相交, 这就意味着它们通过同一条直线. 例如,接上面的例子我们继续绘图, 得到下图:
For each pixel(x,y)
For each radius r = 10 to r = 60 // the possible radius
For each theta t = 0 to 360 // the possible theta 0 to 360
a = x – r * cos(t * PI / 180); //polar coordinate for center
b = y – r * sin(t * PI / 180); //polar coordinate for center
A[a,b,r] +=1; //voting
end
end
end
// 霍夫空间,局部最大点,采用四领域判断,比较。(也可以使8邻域或者更大的方式),如果不判断局部最大值,同时选用次大值与最大值,就可能会是两个相邻的直线,但实际是一条直线。
// 选用最大值,也是去除离散的近似计算带来的误差,或合并近似曲线。
static void
findLocalMaximums( int numrho, int numangle, int threshold,
const int *accum, std::vector<int>& sort_buf )
{
for(int r = 0; r < numrho; r++ )
for(int n = 0; n < numangle; n++ )
{
//得到当前值在累加器数组的位置
int base = (n+1) * (numrho+2) + r+1;
//得到计数值,并以它为基准,看看它是不是局部极大值
if( accum[base] > threshold &&
accum[base] > accum[base - 1] && accum[base] >= accum[base + 1] &&
accum[base] > accum[base - numrho - 2] && accum[base] >= accum[base + numrho + 2] )
//把极大值位置存入排序数组内——sort_buf
sort_buf.push_back(base);
}
}
标准霍夫变换本质上是把图像映射到它的参数空间上,它需要计算所有的M个边缘点,这样它的运算量和所需内存空间都会很大。如果在输入图像中只是处理HoughLinesP函数就是利用概率霍夫变换来检测直线的。它的一般步骤为:
static void
icvHoughLinesProbabilistic( CvMat* image,
float rho, float theta, int threshold,
int lineLength, int lineGap,
CvSeq *lines, int linesMax )
{
//accum为累加器矩阵,mask为掩码矩阵
cv::Mat accum, mask;
cv::vector<float> trigtab; //用于存储事先计算好的正弦和余弦值
//开辟一段内存空间
cv::MemStorage storage(cvCreateMemStorage(0));
//用于存储特征点坐标,即边缘像素的位置
CvSeq* seq;
CvSeqWriter writer;
int width, height; //图像的宽和高
int numangle, numrho; //角度和距离的离散数量
float ang;
int r, n, count;
CvPoint pt;
float irho = 1 / rho; //距离分辨率的倒数
CvRNG rng = cvRNG(-1); //随机数
const float* ttab; //向量trigtab的地址指针
uchar* mdata0; //矩阵mask的地址指针
//确保输入图像的正确性
CV_Assert( CV_IS_MAT(image) && CV_MAT_TYPE(image->type) == CV_8UC1 );
width = image->cols; //提取出输入图像的宽
height = image->rows; //提取出输入图像的高
//由角度和距离分辨率,得到角度和距离的离散数量
numangle = cvRound(CV_PI / theta);
numrho = cvRound(((width + height) * 2 + 1) / rho);
//创建累加器矩阵,即霍夫空间
accum.create( numangle, numrho, CV_32SC1 );
//创建掩码矩阵,大小与输入图像相同
mask.create( height, width, CV_8UC1 );
//定义trigtab的大小,因为要存储正弦和余弦值,所以长度为角度离散数的2倍
trigtab.resize(numangle*2);
//累加器矩阵清零
accum = cv::Scalar(0);
//避免重复计算,事先计算好所需的所有正弦和余弦值
for( ang = 0, n = 0; n < numangle; ang += theta, n++ )
{
trigtab[n*2] = (float)(cos(ang) * irho);
trigtab[n*2+1] = (float)(sin(ang) * irho);
}
//赋值首地址
ttab = &trigtab[0];
mdata0 = mask.data;
//开始写入序列
cvStartWriteSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage, &writer );
// stage 1. collect non-zero image points
//收集图像中的所有非零点,因为输入图像是边缘图像,所以非零点就是边缘点
for( pt.y = 0, count = 0; pt.y < height; pt.y++ )
{
//提取出输入图像和掩码矩阵的每行地址指针
const uchar* data = image->data.ptr + pt.y*image->step; // step是每一行的字节大小 此行代码就转到当前遍历的这一行
uchar* mdata = mdata0 + pt.y*width;
for( pt.x = 0; pt.x < width; pt.x++ )
{
if( data[pt.x] ) //是边缘点
{
mdata[pt.x] = (uchar)1; //掩码的相应位置置1
CV_WRITE_SEQ_ELEM( pt, writer ); 把该坐标位置写入序列
}
else //不是边缘点
mdata[pt.x] = 0; //掩码的相应位置清0
}
}
//终止写序列,seq为所有边缘点坐标位置的序列
seq = cvEndWriteSeq( &writer );
count = seq->total; //得到边缘点的数量
// stage 2. process all the points in random order
//随机处理所有的边缘点
for( ; count > 0; count-- )
{
// choose random point out of the remaining ones
//步骤1,在剩下的边缘点中随机选择一个点,idx为不大于count的随机数
int idx = cvRandInt(&rng) % count;
//max_val为累加器的最大值,max_n为最大值所对应的角度
int max_val = threshold-1, max_n = 0;
//由随机数idx在序列中提取出所对应的坐标点
CvPoint* point = (CvPoint*)cvGetSeqElem( seq, idx );
//定义直线的两个端点
CvPoint line_end[2] = {{0,0}, {0,0}};
float a, b;
//累加器的地址指针,也就是霍夫空间的地址指针
int* adata = (int*)accum.data;
int i, j, k, x0, y0, dx0, dy0, xflag;
int good_line;
const int shift = 16; //
//提取出坐标点的横、纵坐标
i = point->y;
j = point->x;
// "remove" it by overriding it with the last element
//用序列中的最后一个元素覆盖掉刚才提取出来的随机坐标点
*point = *(CvPoint*)cvGetSeqElem( seq, count-1 );
// check if it has been excluded already (i.e. belongs to some other line)
//检测这个坐标点是否已经计算过,也就是它已经属于其他直线
//因为计算过的坐标点会在掩码矩阵mask的相对应位置清零
if( !mdata0[i*width + j] ) //该坐标点被处理过
continue; //不做任何处理,继续主循环
// update accumulator, find the most probable line
//步骤2,更新累加器矩阵,找到最有可能的直线
for( n = 0; n < numangle; n++, adata += numrho )
{
//由角度计算距离
r = cvRound( j * ttab[n*2] + i * ttab[n*2+1] );
r += (numrho - 1) / 2; //防止r为负数
//在累加器矩阵的相应位置上数值加1,并赋值给val
int val = ++adata[r];
//更新最大值,并得到它的角度
if( max_val < val )
{
max_val = val;
max_n = n;
}
}
// if it is too "weak" candidate, continue with another point
//步骤3,如果上面得到的最大值小于阈值,则放弃该点,继续下一个点的计算
if( max_val < threshold )
continue;
// from the current point walk in each direction
// along the found line and extract the line segment
//步骤4,从当前点出发,沿着它所在直线的方向前进,直到达到端点为止
a = -ttab[max_n*2+1]; //a=-sinθ
b = ttab[max_n*2]; //b=cosθ
//当前点的横、纵坐标值
x0 = j;
y0 = i;
//确定当前点所在直线的角度是在45度~135度之间,还是在0~45或135度~180度之间
if( fabs(a) > fabs(b) ) //在45度~135度之间
{
xflag = 1; //置标识位,标识直线的粗略方向
//确定横、纵坐标的位移量
dx0 = a > 0 ? 1 : -1;
dy0 = cvRound( b*(1 << shift)/fabs(a) );
//确定纵坐标
y0 = (y0 << shift) + (1 << (shift-1));
}
else //在0~45或135度~180度之间
{
xflag = 0; //清标识位
//确定横、纵坐标的位移量
dy0 = b > 0 ? 1 : -1;
dx0 = cvRound( a*(1 << shift)/fabs(b) );
//确定横坐标
x0 = (x0 << shift) + (1 << (shift-1));
}
//搜索直线的两个端点
for( k = 0; k < 2; k++ )
{
//gap表示两条直线的间隙,x和y为搜索位置,dx和dy为位移量
int gap = 0, x = x0, y = y0, dx = dx0, dy = dy0;
//搜索第二个端点的时候,反方向位移
if( k > 0 )
dx = -dx, dy = -dy;
// walk along the line using fixed-point arithmetics,
// stop at the image border or in case of too big gap
//沿着直线的方向位移,直到到达图像的边界或大的间隙为止
for( ;; x += dx, y += dy )
{
uchar* mdata;
int i1, j1;
//确定新的位移后的坐标位置
if( xflag )
{
j1 = x;
i1 = y >> shift;
}
else
{
j1 = x >> shift;
i1 = y;
}
//如果到达了图像的边界,停止位移,退出循环
if( j1 < 0 || j1 >= width || i1 < 0 || i1 >= height )
break;
//定位位移后掩码矩阵位置
mdata = mdata0 + i1*width + j1;
// for each non-zero point:
// update line end,
// clear the mask element
// reset the gap
//该掩码不为0,说明该点可能是在直线上
if( *mdata )
{
gap = 0; //设置间隙为0
//更新直线的端点位置
line_end[k].y = i1;
line_end[k].x = j1;
}
//掩码为0,说明不是直线,但仍继续位移,直到间隙大于所设置的阈值为止
else if( ++gap > lineGap ) //间隙加1
break;
}
}
//步骤5,由检测到的直线的两个端点粗略计算直线的长度
//当直线长度大于所设置的阈值时,good_line为1,否则为0
good_line = abs(line_end[1].x - line_end[0].x) >= lineLength ||
abs(line_end[1].y - line_end[0].y) >= lineLength;
//再次搜索端点,目的是更新累加器矩阵和更新掩码矩阵,以备下一次循环使用
for( k = 0; k < 2; k++ )
{
int x = x0, y = y0, dx = dx0, dy = dy0;
if( k > 0 )
dx = -dx, dy = -dy;
// walk along the line using fixed-point arithmetics,
// stop at the image border or in case of too big gap
for( ;; x += dx, y += dy )
{
uchar* mdata;
int i1, j1;
if( xflag )
{
j1 = x;
i1 = y >> shift;
}
else
{
j1 = x >> shift;
i1 = y;
}
mdata = mdata0 + i1*width + j1;
// for each non-zero point:
// update line end,
// clear the mask element
// reset the gap
if( *mdata )
{
//if语句的作用是清除那些已经判定是好的直线上的点对应的累加器的值,避免再次利用这些累加值
if( good_line ) //在第一次搜索中已经确定是好的直线
{
//得到累加器矩阵地址指针
adata = (int*)accum.data;
for( n = 0; n < numangle; n++, adata += numrho )
{
r = cvRound( j1 * ttab[n*2] + i1 * ttab[n*2+1] );
r += (numrho - 1) / 2;
adata[r]--; //相应的累加器减1
}
}
//搜索过的位置,不管是好的直线,还是坏的直线,掩码相应位置都清0,这样下次就不会再重复搜索这些位置了,从而达到减小计算边缘点的目的
*mdata = 0;
}
//如果已经到达了直线的端点,则退出循环
if( i1 == line_end[k].y && j1 == line_end[k].x )
break;
}
}
//如果是好的直线
if( good_line )
{
CvRect lr = { line_end[0].x, line_end[0].y, line_end[1].x, line_end[1].y };
//把两个端点压入序列中
cvSeqPush( lines, &lr );
//如果检测到的直线数量大于阈值,则退出该函数
if( lines->total >= linesMax )
return;
}
}
}
HoughCircles函数实现了圆形检测,它使用的算法也是改进的霍夫变换——2-1霍夫变换(21HT)。也就是把霍夫变换分为两个阶段,从而减小了霍夫空间的维数。第一阶段用于检测圆心,第二阶段从圆心推导出圆半径。检测圆心的原理是圆心是它所在圆周所有法线的交汇处,因此只要找到这个交点,即可确定圆心,该方法所用的霍夫空间与图像空间的性质相同,因此它仅仅是二维空间。检测圆半径的方法是从圆心到圆周上的任意一点的距离(即半径)是相同,只要确定一个阈值,只要相同距离的数量大于该阈值,我们就认为该距离就是该圆心所对应的圆半径,该方法只需要计算半径直方图,不使用霍夫空间。圆心和圆半径都得到了,那么通过公式1一个圆形就得到了。从上面的分析可以看出,2-1霍夫变换把标准霍夫变换的三维霍夫空间缩小为二维霍夫空间,因此无论在内存的使用上还是在运行效率上,2-1霍夫变换都远远优于标准霍夫变换。但该算法有一个不足之处就是由于圆半径的检测完全取决于圆心的检测,因此如果圆心检测出现偏差,那么圆半径的检测肯定也是错误的。
version 1:
2-1霍夫变换的具体步骤为:
1)首先对图像进行边缘检测,调用opencv自带的cvCanny()函数,将图像二值化,得到边缘图像。
2)对边缘图像上的每一个非零点。采用cvSobel()函数,计算x方向导数和y方向的导数,从而得到梯度。从边缘点,沿着梯度和梯度的反方向,对参数指定的min_radius到max_radium的每一个像素,在累加器中被累加。同时记下边缘图像中每一个非0点的位置。
3)从(二维)累加器中这些点中选择候选中心。这些中心都大于给定的阈值和其相邻的四个邻域点的累加值。
4)对于这些候选中心按照累加值降序排序,以便于最支持的像素的中心首次出现。
5)对于每一个中心,考虑到所有的非0像素(非0,梯度不为0),这些像素按照与其中心的距离排序,从最大支持的中心的最小距离算起,选择非零像素最支持的一条半径。
6)如果一个中心受到边缘图像非0像素的充分支持,并且到前期被选择的中心有足够的距离。则将圆心和半径压入到序列中,得以保留。
/****************************************************************************************
* Circle Detection *
****************************************************************************************/
/*------------------------------------霍夫梯度法------------------------------------------*/
static void
icvHoughCirclesGradient( CvMat* img, float dp, float min_dist,
int min_radius, int max_radius,
int canny_threshold, int acc_threshold,
CvSeq* circles, int circles_max )
{
const int SHIFT = 10, ONE = 1 << SHIFT, R_THRESH = 30; //One=1024,1左移10位2*10,R_THRESH是起始值,赋给max_count,后续会被覆盖。
cv::Ptr<CvMat> dx, dy; //Ptr是智能指针模板,将CvMat对象封装成指针
cv::Ptr<CvMat> edges, accum, dist_buf;//edges边缘二值图像,accum为累加器图像,dist_buf存放候选圆心到满足条件的边缘点的半径
std::vector<int> sort_buf;//用来进行排序的中间对象。在adata累加器排序中,其存放的是offset即偏移位置,int型。在ddata距离排序中,其存储的和下标是一样的值。
cv::Ptr<CvMemStorage> storage;//内存存储器。创建的序列用来向其申请内存空间。
int x, y, i, j, k, center_count, nz_count;//center_count为圆心数,nz_count为非零数
float min_radius2 = (float)min_radius*min_radius;//最小半径的平方
float max_radius2 = (float)max_radius*max_radius;//最大半径的平方
int rows, cols, arows,acols;//rows,cols边缘图像的行数和列数,arows,acols是累加器图像的行数和列数
int astep, *adata;//adata指向累加器数据域的首地址,用位置作为下标,astep为累加器每行的大小,以字节为单位
float* ddata;//ddata即dist_data,距离数据
CvSeq *nz, *centers;//nz为非0,即边界,centers为存放的候选中心的位置。
float idp, dr;//idp即inv_dp,dp的倒数
CvSeqReader reader;//顺序读取序列中的每个值
edges = cvCreateMat( img->rows, img->cols, CV_8UC1 );//边缘图像
cvCanny( img, edges, MAX(canny_threshold/2,1), canny_threshold, 3 );//调用canny,变为二值图像,0和非0即0和255
dx = cvCreateMat( img->rows, img->cols, CV_16SC1 );//16位单通道图像,用来存储二值边缘图像的x方向的一阶导数
dy = cvCreateMat( img->rows, img->cols, CV_16SC1 );//y方向的
cvSobel( img, dx, 1, 0, 3 );//计算x方向的一阶导数
cvSobel( img, dy, 0, 1, 3 );//计算y方向的一阶导数
if( dp < 1.f )//控制dp不能比1小
dp = 1.f;
idp = 1.f/dp;
accum = cvCreateMat( cvCeil(img->rows*idp)+2, cvCeil(img->cols*idp)+2, CV_32SC1 );//cvCeil返回不小于参数的最小整数。32为单通道
cvZero(accum);//初始化累加器为0
storage = cvCreateMemStorage();//创建内存存储器,使用默认参数0.默认大小为64KB
nz = cvCreateSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage );//创建序列,用来存放非0点
centers = cvCreateSeq( CV_32SC1, sizeof(CvSeq), sizeof(int), storage );//用来存放圆心
rows = img->rows;
cols = img->cols;
arows = accum->rows - 2;
acols = accum->cols - 2;
adata = accum->data.i;//cvMat对象的union对象的i成员成员
//step是矩阵中行的长度,单位为字节。我们使用到的矩阵是accum它的深度是CV_32SC1即32位的int 型。
//如果我们知道一个指针如int* p;指向数组中的一个元素, 则可以通过p+accum->step/adata[0]来使指针移动到p指针所指元素的,正对的下一行元素
astep = accum->step/sizeof(adata[0]);
for( y = 0; y < rows; y++ )
{
const uchar* edges_row = edges->data.ptr + y*edges->step; //边界存储的矩阵的每一行的指向行首的指针。
const short* dx_row = (const short*)(dx->data.ptr + y*dx->step);//存储 x方向sobel一阶导数的矩阵的每一行的指向第一个元素的指针
const short* dy_row = (const short*)(dy->data.ptr + y*dy->step);//y
//遍历边缘的二值图像和偏导数的图像
for( x = 0; x < cols; x++ )
{
float vx, vy;
int sx, sy, x0, y0, x1, y1, r, k;
CvPoint pt;
vx = dx_row[x];//访问每一行的元素
vy = dy_row[x];
if( !edges_row[x] || (vx == 0 && vy == 0) )//如果在边缘图像(存储边缘的二值图像)某一点如A(x0,y0)==0则对一下点进行操作。vx和vy同时为0,则下一个
continue;
float mag = sqrt(vx*vx+vy*vy);//求梯度图像
assert( mag >= 1 );//如果mag为0,说明没有边缘点,则stop。这里用了assert宏定义
sx = cvRound((vx*idp)*ONE/mag);// vx为该点的水平梯度(梯度幅值已经归一化);ONE为为了用整数运算代替浮点数引入的一个因子,为2^10
sy = cvRound((vy*idp)*ONE/mag);
x0 = cvRound((x*idp)*ONE);
y0 = cvRound((y*idp)*ONE);
for( k = 0; k < 2; k++ )//k=0在梯度方向,k=1在梯度反方向对累加器累加。这里之所以要反向,因为对于一个圆上一个点,从这个点沿着斜率的方向的,最小半径到最大半径。在圆的另一边与其相对应的点,有对应的效果。
{
x1 = x0 + min_radius * sx;
y1 = y0 + min_radius * sy;
for( r = min_radius; r <= max_radius; x1 += sx, y1 += sy, r++ )//x1=x1+sx即,x1=x0+min_radius*sx+sx=x0+(min_radius+1)*sx求得下一个点。sx为斜率
{
int x2 = x1 >> SHIFT, y2 = y1 >> SHIFT; //变回真实的坐标
if( (unsigned)x2 >= (unsigned)acols || //如果x2大于累加器的行
(unsigned)y2 >= (unsigned)arows )
break;
adata[y2*astep + x2]++;//由于c语言是按行存储的。即等价于对accum数组进行了操作。
}
sx = -sx; sy = -sy;
}
pt.x = x; pt.y = y;
cvSeqPush( nz, &pt );//把非零边缘并且梯度不为0的点压入到堆栈
}
}
nz_count = nz->total;
if( !nz_count )//如果nz_count==0则返回
return;
for( y = 1; y < arows - 1; y++ ) //这里是从1到arows-1,因为如果是圆的话,那么圆的半径至少为1,即圆心至少在内层里面
{
for( x = 1; x < acols - 1; x++ )
{
int base = y*(acols+2) + x;//计算位置,在accum图像中
if( adata[base] > acc_threshold &&
adata[base] > adata[base-1] && adata[base] > adata[base+1] &&
adata[base] > adata[base-acols-2] && adata[base] > adata[base+acols+2] )
cvSeqPush(centers, &base);//候选中心点位置压入到堆栈。其候选中心点累加数大于阈值,其大于四个邻域
}
}
center_count = centers->total;
if( !center_count ) //如果没有符合条件的圆心,则返回到函数。
return;
sort_buf.resize( MAX(center_count,nz_count) );//重新分配容器的大小,取候选圆心的个数和非零边界的个数的最大值。因为后面两个均用到排序。
cvCvtSeqToArray( centers, &sort_buf[0] ); //把序列转换成数组,即把序列centers中的数据放入到sort_buf的容器中。
icvHoughSortDescent32s( &sort_buf[0], center_count, adata );//快速排序,根据sort_buf中的值作为下标,依照adata中对应的值进行排序,将累加值大的下标排到前面
cvClearSeq( centers );//清空序列
cvSeqPushMulti( centers, &sort_buf[0], center_count );//重新将中心的下标存入centers
dist_buf = cvCreateMat( 1, nz_count, CV_32FC1 );//创建一个32为浮点型的一个行向量
ddata = dist_buf->data.fl;//使ddata执行这个行向量的首地址
dr = dp;
min_dist = MAX( min_dist, dp );//如果输入的最小距离小于dp,则设在为dp
min_dist *= min_dist;
for( i = 0; i < centers->total; i++ ) //对于每一个中心点
{
int ofs = *(int*)cvGetSeqElem( centers, i );//获取排序的中心位置,adata值最大的元素,排在首位 ,offset偏移位置
y = ofs/(acols+2) - 1;//这里因为edge图像比accum图像小两个边。
x = ofs - (y+1)*(acols+2) - 1;//求得y坐标
float cx = (float)(x*dp), cy = (float)(y*dp);
float start_dist, dist_sum;
float r_best = 0, c[3];
int max_count = R_THRESH;
for( j = 0; j < circles->total; j++ )//中存储已经找到的圆;若当前候选圆心与其中的一个圆心距离<min_dist,则舍弃该候选圆心
{
float* c = (float*)cvGetSeqElem( circles, j );//获取序列中的元素。
if( (c[0] - cx)*(c[0] - cx) + (c[1] - cy)*(c[1]