关于统计概率的霍夫线变换源码在下篇补上(我还没来得及去看),这次直接按照流程把霍夫圆变换撸一遍。
主要参考博客:
原理分析: (是一篇文章,带有源码分析的)
下载链接如下:http://download.csdn.net/download/xuluhui123/9647021 (代码注释的非常详细)
该博主链接如下:http://blog.csdn.net/raby_gyl/article/details/52800798
Samples:http://blog.csdn.net/poem_qianmo/article/details/26977557 (浅墨)
源码分析:http://blog.csdn.net/zhaocj/article/details/50454847 (赵老师,原理部分析的也很好)
霍夫圆变换和霍夫线变换的原理类似。霍夫线变换是两个参数(r,θ),霍夫圆需要三个参数,圆心的x,y坐标和圆的半径.如下对应的三个参数c1,c2,c3。
例如:
其形状和
类似,该函数是由z=x沿z轴旋转而成的圆锥曲面。(原谅我不会画图,不够深动形象)
对于xy平面的一个点x0,y0(上述对应的点为(1,1)),则对应的由c1,c2,c3组成三维空间的空间曲面。对于c1,c2,c3平面的一个点,则对应的在xy平面它是一个圆。
对于在x,y平面上的三个点(x0,y0),(x1,y1),(x2,y2),在c1,c2,c3三维空间是对应的三个空间曲面(此时c1,c2,c3相当于常量)
求解这三个方程,我们可以得到c1,c2,c3的值。这说明(x0,y0),(x1,y1),(x2,y2)这三个点在由c1,c2,c3所确定的圆上(即c1,c2,c3分别表示圆的圆心x坐标、圆心y坐标以及圆的半径),且三个点对应由c1,c2,c3确定的空间的三个空间曲面。进一步说明,在xy平面,三个点在同一个圆上,则它们对应的空间曲面相交于一点(即点(c1,c2,c3))。
故我们如果知道一个边界上的点的数目,足够多,且这些点与之对应的空间曲面相交于一点。则这些点构成的边界,就接近一个圆形。
上述描述的是标准霍夫圆变换的原理,由于三维空间的计算量大大增大的原因, 标准霍夫圆变化很难被应用到实际中。
OpenCV实现的是一个比标准霍夫圆变换更为灵活的检测方法: 霍夫梯度法, 也叫2-1霍夫变换(21HT),
它的原理依据是圆心一定是在圆上的每个点的模向量上, 这些圆上点模向量的交点就是圆心, 霍夫梯度法的第一步就是找到这些圆心, (圆心包含了圆心处的x和y坐标)这样三维的累加平面就又转化为二维累加平面. 第二步根据所有候选中心的边缘非0像素对其的支持程度来确定半径。(备注:降维的思想很重要)
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像素的充分支持,并且到前期被选择的中心有足够的距离。则将圆心和半径压入到序列中,得以保留。
version 2:
第一阶段:检测圆心
1.1、对输入图像边缘检测;
1.2、计算图形的梯度,并确定圆周线,其中圆周的梯度就是它的法线;
1.3、在二维霍夫空间内,绘出所有图形的梯度直线,某坐标点上累加和的值越大,说明在该点上直线相交的次数越多,也就是越有可能是圆心;(备注:这只是直观的想法,实际源码并没有划线)
1.4、在霍夫空间的4邻域内进行非最大值抑制;
1.5、设定一个阈值,霍夫空间内累加和大于该阈值的点就对应于圆心。
第二阶段:检测圆半径
2.1、计算某一个圆心到所有圆周线的距离,这些距离中就有该圆心所对应的圆的半径的值,这些半径值当然是相等的,并且这些圆半径的数量要远远大于其他距离值相等的数量
2.2、设定两个阈值,定义为最大半径和最小半径,保留距离在这两个半径之间的值,这意味着我们检测的圆不能太大,也不能太小
2.3、对保留下来的距离进行排序
2.4、找到距离相同的那些值,并计算相同值的数量
2.5、设定一个阈值,只有相同值的数量大于该阈值,才认为该值是该圆心对应的圆半径
2.6、对每一个圆心,完成上面的2.1~2.5步骤,得到所有的圆半径
先给出OpenCV的霍夫圆检测函数以及参数详解。
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 )
#include
#include
#include
#include
using namespace cv;
using namespace std;
Mat g_scrImage,g_scrImagecopy, g_dstImage, g_midImage;
vectorcircles;
int g_nthreshold = 15;
static void on_HoughCircles(int, void*);
void showText();
int main(){
g_scrImage = imread("circle.jpg");
g_scrImagecopy = imread("circle.jpg");
imshow("SourceImage", g_scrImage);
namedWindow("Design Sketch");
createTrackbar("yuzhi", "Design Sketch", &g_nthreshold, 30, on_HoughCircles);
on_HoughCircles(1.5, 0);
while (char(waitKey()) != 'q'){}
return 0;
}
static void on_HoughCircles(int, void*){
cvtColor(g_scrImage, g_midImage, CV_BGR2GRAY);
GaussianBlur(g_midImage, g_midImage, Size(9,9), 2, 2);//必须要用高斯blur。
double g_nthresholdcopy = (double)g_nthreshold / 10;
HoughCircles( g_midImage, circles, CV_HOUGH_GRADIENT, g_nthresholdcopy, g_midImage.rows/20, 100, 60, 0, 0 );//输入图像为灰度图
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]);
//圆心
circle( g_scrImage, center, 3, Scalar(0,0,0), -1, 8, 0 );
//圆
circle( g_scrImage, center, radius, Scalar(0,0,255), 3, 8, 0 );
}
imshow("Design Sketch", g_scrImage);
g_scrImage = g_scrImagecopy.clone();
}
void showText(){
cout << "\n\n\n\t请调整滚动条观察图像效果~\n\n"
"\n\n\t\t\t\t\t\t\t\t by浅墨";
}
备注:
1.我加了一个参数来控制霍夫空间分辨率,只是为了看一下参数对检测效果的影响,后面还有几个参数,而且每个参数的改变对结果影响都很大,即漏检和错检的几率很大。(感兴趣可以试一下)
2.画圆函数详解
cvCircle(CvArr* img, CvPoint center, int radius, CvScalar color, int thickness=1, int lineType=8, int shift=0)
img为源图像指针
center为画圆的圆心坐标
radius为圆的半径
color为设定圆的颜色,规则根据B(蓝)G(绿)R(红)
thickness 如果是正数,表示组成圆的线条的粗细程度。否则,表示圆是否被填充
line_type 线条的类型。默认是8
shift 圆心坐标点和半径值的小数点位数
浅墨总结的不错:截下来,如图所示
效果如下:
原图:
dp = 1.5
dp = 1.9
4.源码分析
HoughCircles函数在sources/modules/imgproc/src/hough.cpp文件内被定义:
void cv::HoughCircles( InputArray _image, OutputArray _circles,
int method, double dp, double min_dist,
double param1, double param2,
int minRadius, int maxRadius )
{
//定义一段内存
Ptr storage = cvCreateMemStorage(STORAGE_SIZE);
Mat image = _image.getMat(); //提取输入图像矩阵
CvMat c_image = image; //矩阵转换
//调用cvHoughCircles函数
CvSeq* seq = cvHoughCircles( &c_image, storage, method,
dp, min_dist, param1, param2, minRadius, maxRadius );
//把序列转换为矩阵
seqToMat(seq, _circles);
}
cvHoughCircles函数为:
CV_IMPL CvSeq*
cvHoughCircles( CvArr* src_image, void* circle_storage,
int method, double dp, double min_dist,
double param1, double param2,
int min_radius, int max_radius )
{
CvSeq* result = 0;
CvMat stub, *img = (CvMat*)src_image;
CvMat* mat = 0;
CvSeq* circles = 0;
CvSeq circles_header;
CvSeqBlock circles_block;
int circles_max = INT_MAX; //输出最多圆形的数量,设为无穷多
//canny边缘检测中双阈值中的高阈值
int canny_threshold = cvRound(param1);
//累加器阈值
int acc_threshold = cvRound(param2);
img = cvGetMat( img, &stub );
//确保输入图像是灰度图像
if( !CV_IS_MASK_ARR(img))
CV_Error( CV_StsBadArg, "The source image must be 8-bit, single-channel" );
//内存空间是否存在
if( !circle_storage )
CV_Error( CV_StsNullPtr, "NULL destination" );
//确保参数的正确性
if( dp <= 0 || min_dist <= 0 || canny_threshold <= 0 || acc_threshold <= 0 )
CV_Error( CV_StsOutOfRange, "dp, min_dist, canny_threshold and acc_threshold must be all positive numbers" );
//圆的最小半径要大于0
min_radius = MAX( min_radius, 0 );
//圆的最大半径如果小于0,则设最大半径为图像宽和长度的最大值,
//如果最大半径小于最小半径,则设最大半径为最小半径加两个像素的宽度
if( max_radius <= 0 )
max_radius = MAX( img->rows, img->cols );
else if( max_radius <= min_radius )
max_radius = min_radius + 2;
if( CV_IS_STORAGE( circle_storage ))
{
circles = cvCreateSeq( CV_32FC3, sizeof(CvSeq),
sizeof(float)*3, (CvMemStorage*)circle_storage );
}
else if( CV_IS_MAT( circle_storage ))
{
mat = (CvMat*)circle_storage;
if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 && mat->cols != 1) ||
CV_MAT_TYPE(mat->type) != CV_32FC3 )
CV_Error( CV_StsBadArg,
"The destination matrix should be continuous and have a single row or a single column" );
circles = cvMakeSeqHeaderForArray( CV_32FC3, sizeof(CvSeq), sizeof(float)*3,
mat->data.ptr, mat->rows + mat->cols - 1, &circles_header, &circles_block );
circles_max = circles->total;
cvClearSeq( circles );
}
else
CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* nor CvMat*" );
//选择哪种算法检测圆,目前只有2-1霍夫变换
switch( method )
{
case CV_HOUGH_GRADIENT:
//调用icvHoughCirclesGradient函数
icvHoughCirclesGradient( img, (float)dp, (float)min_dist,
min_radius, max_radius, canny_threshold,
acc_threshold, circles, circles_max );
break;
default:
CV_Error( CV_StsBadArg, "Unrecognized method id" );
}
if( mat )
{
if( mat->cols > mat->rows )
mat->cols = circles->total;
else
mat->rows = circles->total;
}
else
result = circles;
//输出圆
return result;
}
也有两个版本的注释,一样的,结合来看
version1:论文中的
/****************************************************************************************\
* 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 dx, dy; //Ptr是智能指针模板,将CvMat对象封装成指针
cv::Ptr edges, accum, dist_buf;//edges边缘二值图像,accum为累加器图像,dist_buf存放候选圆心到满足条件的边缘点的半径
std::vector sort_buf;//用来进行排序的中间对象。在adata累加器排序中,其存放的是offset即偏移位置,int型。在ddata距离排序中,其存储的和下标是一样的值。
cv::Ptr 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++ )//中存储已经找到的圆;若当前候选圆心与其中的一个圆心距离total )//当前候选圆心与任意已检测的圆心距离不小于min_dist时,才有j==circles->total
continue;
cvStartReadSeq( nz, &reader );
for( j = k = 0; j < nz_count; j++ )//每个候选圆心,对于所有的点
{
CvPoint pt;
float _dx, _dy, _r2;
CV_READ_SEQ_ELEM( pt, reader );
_dx = cx - pt.x; _dy = cy - pt.y; //中心点到边界的距离
_r2 = _dx*_dx + _dy*_dy;
if(min_radius2 <= _r2 && _r2 <= max_radius2 )
{
ddata[k] = _r2; //把满足的半径的平方存起来
sort_buf[k] = k;//sort_buf同上,但是这里的sort_buf的下标值和元素值是相同的,重新利用
k++;//k和j是两个游标
}
}
int nz_count1 = k, start_idx = nz_count1 - 1;
if( nz_count1 == 0 )
continue; //如果一个候选中心到(非零边界且梯度>0)确定的点的距离中,没有满足条件的,则从下一个中心点开始。
dist_buf->cols = nz_count1;//用来存放真是的满足条件的非零元素(三个约束:非零点,梯度不为0,到圆心的距离在min_radius和max_radius中间)
cvPow( dist_buf, dist_buf, 0.5 );//对dist_buf中的元素开根号.求得半径
icvHoughSortDescent32s( &sort_buf[0], nz_count1, (int*)ddata );////对与圆心的距离按降序排列,索引值在sort_buf中
dist_sum = start_dist = ddata[sort_buf[nz_count1-1]];//dist距离,选取半径最小的作为起始值
//下边for循环里面是一个算法。它定义了两个游标(指针)start_idx和j,j是外层循环的控制变量。而start_idx为控制当两个相邻的数组ddata的数据发生变化时,即d-start_dist>dr时,的步进。
for( j = nz_count1 - 2; j >= 0; j-- )//从小到大。选出半径支持点数最多的半径
{
float d = ddata[sort_buf[j]];
if( d > max_radius )//如果求得的候选圆点到边界的距离大于参数max_radius,则停止,因为d是第一个出现的最小的(按照从大到小的顺序排列的)
break;
if( d - start_dist > dr )//如果当前的距离减去最小的>dr(==dp)
{
float r_cur = ddata[sort_buf[(j + start_idx)/2]];//当前半径设为符合该半径的中值,j和start_idx相当于两个游标
if( (start_idx - j)*r_best >= max_count*r_cur || //如果数目相等时,它会找半径较小的那个。这里是判断支持度的算法
(r_best < FLT_EPSILON && start_idx - j >= max_count) ) //程序这个部分告诉我们,无法找到同心圆,它会被外层最大,支持度最多(支持的点最多)所覆盖。
{
r_best = r_cur;//如果 符合当前半径的点数(start_idx - j)/ 当前半径>= 符合之前最优半径的点数/之前的最优半径 || 还没有最优半径时,且点数>30时;其实直接把r_best初始值置为1即可省去第二个条件
max_count = start_idx - j;//maxcount变为符合当前半径的点数,更新max_count值,后续的支持度大的半径将会覆盖前面的值。
}
start_dist = d;
start_idx = j;
dist_sum = 0;//如果距离改变较大,则重置distsum为0,再在下面的式子中置为当前值
}
dist_sum += d;//如果距离改变较小,则加上当前值(dist_sum)在这里好像没有用处。
}
if( max_count > R_THRESH )//符合条件的圆周点大于阈值30,则将圆心、半径压栈
{
c[0] = cx;
c[1] = cy;
c[2] = (float)r_best;
cvSeqPush( circles, c );
if( circles->total > circles_max )//circles_max是个很大的数,其值为INT_MAX
return;
}
}
}
CV_IMPL CvSeq*
cvHoughCircles1( CvArr* src_image, void* circle_storage,
int method, double dp, double min_dist,
double param1, double param2,
int min_radius, int max_radius )
{
CvSeq* result = 0;
CvMat stub, *img = (CvMat*)src_image;
CvMat* mat = 0;
CvSeq* circles = 0;
CvSeq circles_header;
CvSeqBlock circles_block;
int circles_max = INT_MAX;
int canny_threshold = cvRound(param1);//cvRound返回和参数最接近的整数值,对一个double类型进行四舍五入
int acc_threshold = cvRound(param2);
img = cvGetMat( img, &stub );//将img转化成为CvMat对象
if( !CV_IS_MASK_ARR(img)) //图像必须为8位,单通道图像
CV_Error( CV_StsBadArg, "The source image must be 8-bit, single-channel" );
if( !circle_storage )
CV_Error( CV_StsNullPtr, "NULL destination" );
if( dp <= 0 || min_dist <= 0 || canny_threshold <= 0 || acc_threshold <= 0 )
CV_Error( CV_StsOutOfRange, "dp, min_dist, canny_threshold and acc_threshold must be all positive numbers" );
min_radius = MAX( min_radius, 0 );
if( max_radius <= 0 )//用来控制当使用默认参数max_radius=0的时候
max_radius = MAX( img->rows, img->cols );
else if( max_radius <= min_radius )
max_radius = min_radius + 2;
if( CV_IS_STORAGE( circle_storage ))//如果传入的是内存存储器
{
circles = cvCreateSeq( CV_32FC3, sizeof(CvSeq),
sizeof(float)*3, (CvMemStorage*)circle_storage );
}
else if( CV_IS_MAT( circle_storage ))//如果传入的参数时数组
{
mat = (CvMat*)circle_storage;
//数组应该是CV_32FC3类型的单列数组。
if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 && mat->cols != 1) ||//连续,单列,CV_32FC3类型
CV_MAT_TYPE(mat->type) != CV_32FC3 )
CV_Error( CV_StsBadArg,
"The destination matrix should be continuous and have a single row or a single column" );
//将数组转换为序列
circles = cvMakeSeqHeaderForArray( CV_32FC3, sizeof(CvSeq), sizeof(float)*3,
mat->data.ptr, mat->rows + mat->cols - 1, &circles_header, &circles_block );//由于是单列,故elem_size为mat->rows+mat->cols-1
circles_max = circles->total;
cvClearSeq( circles );//清空序列的内容(如果传入的有数据的话)
}
else
CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* nor CvMat*" );
switch( method )
{
case CV_HOUGH_GRADIENT:
icvHoughCirclesGradient( img, (float)dp, (float)min_dist,
min_radius, max_radius, canny_threshold,
acc_threshold, circles, circles_max );
break;
default:
CV_Error( CV_StsBadArg, "Unrecognized method id" );
}
if( mat )//给定一个指向圆存储的数组指针值,则返回0,即NULL
{
if( mat->cols > mat->rows )//因为不知道传入的是列向量还是行向量。
mat->cols = circles->total;
else
mat->rows = circles->total;
}
else//如果是传入的是内存存储器,则返回一个指向一个序列的指针。
result = circles;
return result;
}
解释了一些我这种刚入门的很多不太知道的变量以及函数的意义,但是深入原理还是赵老师讲的好,
两个结合起来看更加容易理解,本想综合在一起,但是弄在一起觉得很别扭。还是分开吧!
version2:(赵老师)
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;
//定义水平梯度和垂直梯度矩阵的地址指针
cv::Ptr dx, dy;
//定义边缘图像、累加器矩阵和半径距离矩阵的地址指针
cv::Ptr edges, accum, dist_buf;
//定义排序向量
std::vector sort_buf;
cv::Ptr storage;
int x, y, i, j, k, 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;
int astep, *adata;
float* ddata;
//nz表示圆周序列,centers表示圆心序列
CvSeq *nz, *centers;
float idp, dr;
CvSeqReader reader;
//创建一个边缘图像矩阵
edges = cvCreateMat( img->rows, img->cols, CV_8UC1 );
//第一阶段
//步骤1.1,用canny边缘检测算法得到输入图像的边缘图像
cvCanny( img, edges, MAX(canny_threshold/2,1), canny_threshold, 3 );
//创建输入图像的水平梯度图像和垂直梯度图像
dx = cvCreateMat( img->rows, img->cols, CV_16SC1 );
dy = cvCreateMat( img->rows, img->cols, CV_16SC1 );
//步骤1.2,用Sobel算子法计算水平梯度和垂直梯度
cvSobel( img, dx, 1, 0, 3 );
cvSobel( img, dy, 0, 1, 3 );
/确保累加器矩阵的分辨率不小于1
if( dp < 1.f )
dp = 1.f;
//分辨率的倒数
idp = 1.f/dp;
//根据分辨率,创建累加器矩阵
accum = cvCreateMat( cvCeil(img->rows*idp)+2, cvCeil(img->cols*idp)+2, CV_32SC1 );
//初始化累加器为0
cvZero(accum);
//创建两个序列,
storage = cvCreateMemStorage();
nz = cvCreateSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage );
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; //累加器的地址指针
astep = accum->step/sizeof(adata[0]); /累加器的步长
// Accumulate circle evidence for each edge pixel
//步骤1.3,对边缘图像计算累加和
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);
const short* dy_row = (const short*)(dy->data.ptr + y*dy->step);
for( x = 0; x < cols; x++ )
{
float vx, vy;
int sx, sy, x0, y0, x1, y1, r;
CvPoint pt;
//当前的水平梯度值和垂直梯度值
vx = dx_row[x];
vy = dy_row[x];
//如果当前的像素不是边缘点,或者水平梯度值和垂直梯度值都为0,则继续循环。因为如果满足上面条件,该点一定不是圆周上的点
if( !edges_row[x] || (vx == 0 && vy == 0) )
continue;
//计算当前点的梯度值
float mag = sqrt(vx*vx+vy*vy);
assert( mag >= 1 );
//定义水平和垂直的位移量
sx = cvRound((vx*idp)*ONE/mag);
sy = cvRound((vy*idp)*ONE/mag);
//把当前点的坐标定位到累加器的位置上
x0 = cvRound((x*idp)*ONE);
y0 = cvRound((y*idp)*ONE);
// Step from min_radius to max_radius in both directions of the gradient
//在梯度的两个方向上进行位移,并对累加器进行投票累计
for(int k1 = 0; k1 < 2; k1++ )
{
//初始一个位移的启动
//位移量乘以最小半径,从而保证了所检测的圆的半径一定是大于最小半径
x1 = x0 + min_radius * sx;
y1 = y0 + min_radius * sy;
//在梯度的方向上位移
// r <= max_radius保证了所检测的圆的半径一定是小于最大半径
for( r = min_radius; r <= max_radius; x1 += sx, y1 += sy, r++ )
{
int x2 = x1 >> SHIFT, y2 = y1 >> SHIFT;
//如果位移后的点超过了累加器矩阵的范围,则退出
if( (unsigned)x2 >= (unsigned)acols ||
(unsigned)y2 >= (unsigned)arows )
break;
//在累加器的相应位置上加1
adata[y2*astep + x2]++;
}
//把位移量设置为反方向
sx = -sx; sy = -sy;
}
//把输入图像中的当前点(即圆周上的点)的坐标压入序列圆周序列nz中
pt.x = x; pt.y = y;
cvSeqPush( nz, &pt );
}
}
//计算圆周点的总数
nz_count = nz->total;
//如果总数为0,说明没有检测到圆,则退出该函数
if( !nz_count )
return;
//Find possible circle centers
//步骤1.4和1.5,遍历整个累加器矩阵,找到可能的圆心
for( y = 1; y < arows - 1; y++ )
{
for( x = 1; x < acols - 1; x++ )
{
int base = y*(acols+2) + x;
//如果当前的值大于阈值,并在4邻域内它是最大值,则该点被认为是圆心
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] )
//把当前点的地址压入圆心序列centers中
cvSeqPush(centers, &base);
}
}
//计算圆心的总数
center_count = centers->total;
//如果总数为0,说明没有检测到圆,则退出该函数
if( !center_count )
return;
//定义排序向量的大小
sort_buf.resize( MAX(center_count,nz_count) );
//把圆心序列放入排序向量中
cvCvtSeqToArray( centers, &sort_buf[0] );
//对圆心按照由大到小的顺序进行排序
//它的原理是经过icvHoughSortDescent32s函数后,以sort_buf中元素作为adata数组下标,adata中的元素降序排列,即adata[sort_buf[0]]是adata所有元素中最大的,adata[sort_buf[center_count-1]]是所有元素中最小的
icvHoughSortDescent32s( &sort_buf[0], center_count, adata );
//清空圆心序列
cvClearSeq( centers );
//把排好序的圆心重新放入圆心序列中
cvSeqPushMulti( centers, &sort_buf[0], center_count );
//创建半径距离矩阵
dist_buf = cvCreateMat( 1, nz_count, CV_32FC1 );
//定义地址指针
ddata = dist_buf->data.fl;
dr = dp; //定义圆半径的距离分辨率
//重新定义圆心之间的最小距离
min_dist = MAX( min_dist, dp );
//最小距离的平方
min_dist *= min_dist;
// For each found possible center
// Estimate radius and check support
//按照由大到小的顺序遍历整个圆心序列
for( i = 0; i < centers->total; i++ )
{
//提取出圆心,得到该点在累加器矩阵中的偏移量
int ofs = *(int*)cvGetSeqElem( centers, i );
//得到圆心在累加器中的坐标位置
y = ofs/(acols+2);
x = ofs - (y)*(acols+2);
//Calculate circle's center in pixels
//计算圆心在输入图像中的坐标位置
float cx = (float)((x + 0.5f)*dp), cy = (float)(( y + 0.5f )*dp);
float start_dist, dist_sum;
float r_best = 0;
int max_count = 0;
// Check distance with previously detected circles
//判断当前的圆心与之前确定作为输出的圆心是否为同一个圆心
for( j = 0; j < circles->total; j++ )
{
//从序列中提取出圆心
float* c = (float*)cvGetSeqElem( circles, j );
//计算当前圆心与提取出的圆心之间的距离,如果两者距离小于所设的阈值,则认为两个圆心是同一个圆心,退出循环
if( (c[0] - cx)*(c[0] - cx) + (c[1] - cy)*(c[1] - cy) < min_dist )
break;
}
//如果j < circles->total,说明当前的圆心已被认为与之前确定作为输出的圆心是同一个圆心,则抛弃该圆心,返回上面的for循环
if( j < circles->total )
continue;
// Estimate best radius
//第二阶段
//开始读取圆周序列nz
cvStartReadSeq( nz, &reader );
for( j = k = 0; j < nz_count; j++ )
{
CvPoint pt;
float _dx, _dy, _r2;
CV_READ_SEQ_ELEM( pt, reader );
_dx = cx - pt.x; _dy = cy - pt.y;
//步骤2.1,计算圆周上的点与当前圆心的距离,即半径
_r2 = _dx*_dx + _dy*_dy;
//步骤2.2,如果半径在所设置的最大半径和最小半径之间
if(min_radius2 <= _r2 && _r2 <= max_radius2 )
{
//把半径存入dist_buf内
ddata[k] = _r2;
sort_buf[k] = k;
k++;
}
}
//k表示一共有多少个圆周上的点
int nz_count1 = k, start_idx = nz_count1 - 1;
//nz_count1等于0也就是k等于0,说明当前的圆心没有所对应的圆,意味着当前圆心不是真正的圆心,所以抛弃该圆心,返回上面的for循环
if( nz_count1 == 0 )
continue;
dist_buf->cols = nz_count1; //得到圆周上点的个数
cvPow( dist_buf, dist_buf, 0.5 ); //求平方根,得到真正的圆半径
//步骤2.3,对圆半径进行排序
icvHoughSortDescent32s( &sort_buf[0], nz_count1, (int*)ddata );
dist_sum = start_dist = ddata[sort_buf[nz_count1-1]];
//步骤2.4
for( j = nz_count1 - 2; j >= 0; j-- )
{
float d = ddata[sort_buf[j]];
if( d > max_radius )
break;
//d表示当前半径值,start_dist表示上一次通过下面if语句更新后的半径值,dr表示半径距离分辨率,如果这两个半径距离之差大于距离分辨率,说明这两个半径一定不属于同一个圆,而两次满足if语句条件之间的那些半径值可以认为是相等的,即是属于同一个圆
if( d - start_dist > dr )
{
//start_idx表示上一次进入if语句时更新的半径距离排序的序号
// start_idx – j表示当前得到的相同半径距离的数量
//(j + start_idx)/2表示j和start_idx中间的数
//取中间的数所对应的半径值作为当前半径值r_cur,也就是取那些半径值相同的值
float r_cur = ddata[sort_buf[(j + start_idx)/2]];
//如果当前得到的半径相同的数量大于最大值max_count,则进入if语句
if( (start_idx - j)*r_best >= max_count*r_cur ||
(r_best < FLT_EPSILON && start_idx - j >= max_count) )
{
r_best = r_cur; //把当前半径值作为最佳半径值
max_count = start_idx - j; //更新最大值
}
//更新半径距离和序号
start_dist = d;
start_idx = j;
dist_sum = 0;
}
dist_sum += d;
}
// Check if the circle has enough support
//步骤2.5,最终确定输出
//如果相同半径的数量大于所设阈值
if( max_count > acc_threshold )
{
float c[3];
c[0] = cx; //圆心的横坐标
c[1] = cy; //圆心的纵坐标
c[2] = (float)r_best; //所对应的圆的半径
cvSeqPush( circles, c ); //压入序列circles内
//如果得到的圆大于阈值,则退出该函数
if( circles->total > circles_max )
return;
}
}
}
还有一些问题,赵老师的评论解释的很好
霍夫空间是用于统计坐标点,分辨率没有必要与原图像一致,所以用dp来调整霍夫空间的大小,从而提高运算速度。而ONE的作用是为了减小运算过程中的累积误差,提高精度,如:1/3=0.333333,如果保留小数点后5位,则为0.33333,但如果向左位移3位,则为333.33333,这个数的精度要比前一个高,只要我们把所有的数都向左位移3位,最终的结果再向右位移3位,最后的数值精度会提高不少的。基本思想就是这个。
其他应该没啥了,不过霍夫也算是复杂了,建议先看完线变换,再来看霍夫圆变换,思想以及源码都会好理解很多。
折腾了几天,总算是弄完了,很多也米有解释很好,欢迎留言讨论,any problem is ok!