霍夫圆变换
基本原理
关于基本原理,其思想大概跟霍夫线变换相似,但是有两种说法。
第一种:
在霍夫线变换中,笛卡尔X-Y直角坐标系中的直线,变换到霍夫空间中则为1个点
因此类比可得,笛卡尔X-Y直角坐标系中的圆,变换到abr空间中,则为一条曲线,具体如下:
X-Y直角坐标系下圆方程:
对应的参数方程为:
所以在abr组成的三维坐标系中,一个点可以唯一确定一个圆。
那么,当我们固定(x,y),选取(a,b,r) 的不同组合,可以得到xy直角坐标系中经过某点(X,Y)的所有圆对应,
而在笛卡尔的xy坐标系中经过某一点的所有圆映射到abr坐标系中就是一条三维的曲线。【这句话应该是不严谨的,正确的说法看后面的第二种叙述】
个人认为,上面的话,应该是对于某点(x,y),
固定X时,选取不同(a,r)组合,可以得到一条(a,r)坐标系下的曲线
固定Y时,选取不同(b,r)组合,可以得到一条(b,r)坐标系下的曲线
这里,我们先继续认为上面的叙述没有问题,即经过点(X,Y)的所有圆变换到(a,b,r)空间下是一条空间曲线。那么这条曲线上的一点,则对应一个圆。
这时候,看(a,b,r)坐标系,若该坐标系中有某点(a,b,r)固定,那么经过该点的空间曲线越多,说明对应笛卡尔X-Y直角坐标系中共圆的点越多。当设定一个阈值,则可判断X-Y坐标系中是否有圆了。
第二种:
X-Y直角坐标系下圆方程:
现假设(x,y)是参数,即(x,y)固定,假设为(1,1)
那么以(a,b,r)为变量的方程,对应的空间图形是一个这样一个曲面,貌似是圆锥?
matlab画图代码:
>> a=-8:0.5:8; >> b=a; >> [A,B]=meshgrid(a,b); >> R=sqrt((1-A).^2+(1-B).^2); >> mesh(A,B,R);
这就跟上面第一种说法的叙述有矛盾了,这里表明在笛卡尔的xy坐标系中经过某一点的所有圆映射到abr坐标系中是一个圆锥体。
那么,在这个曲面上的每一个点,则对应 X-Y坐标系下的一个经过点(x,y)的圆。
也就是说,在(a,b,r)坐标系中,假设有固定的某点(a0,b0,r0),若经过该点的曲面越多,则表示对应X-Y直角坐标系中有多个点共这个圆。
原理部分就先到这里,希望有大神指出纰漏。
霍夫梯度法
上述描述的是标准霍夫圆变换的原理,由于三维空间的计算量大大增大的原因, 标准霍夫圆变化很难被应用到实际中。
实际上,一般用霍夫梯度法来解决这个问题。
基本原理:依据是圆心一定是在圆上的每个点的模向量上(圆上该点切线的法向量), 这些圆上点模向量的交点就是圆心, 霍夫梯度法的
第一步就是找到这些圆心, (圆心包含了圆心处的x和y坐标)这样三维的累加平面就又转化为二维累加平面.
第二步根据所有候选中心的边缘非0像素对其的支持程度来确定半径。简单来说,就是从圆心到圆周上的任意一点的距离(即半径)是相同,只要确定一个阈值,只要相同距离的数量大于该阈值,我们就认为该距离就是该圆心所对应的圆半径,该方法只需要计算半径直方图,不使用霍夫空间。
下引用给出两个版本解释的霍夫梯度法操作具体过程:
引用参考:https://blog.csdn.net/qq_37059483/article/details/77916655
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、考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度,即得到切线的法向量【指向圆心】
(这里容易把偏导、切线、梯度、法向量混淆,为此我还回去看了几天的高数下册,在下面给出一些归纳)
1.3、在二维霍夫空间内,绘出所有图形的梯度直线,某坐标点上累加和的值越大,说明在该点上直线相交的次数越多,也就是越有可能是圆心;(备注:这只是直观的想法,实际源码并没有划线)
1.4、在霍夫空间的4邻域内进行非最大值抑制;
1.5、设定一个阈值,霍夫空间内累加和大于该阈值的点就对应于圆心。
第二阶段:检测圆半径
2.1、计算某一个圆心到所有圆周线(假定为边缘检测出来的边缘)的距离,这些距离中就有该圆心所对应的圆的半径的值,这些半径值当然是相等的,并且这些圆半径的数量要远远大于其他距离值相等的数量
2.2、设定两个阈值,定义为最大半径和最小半径,保留距离在这两个半径之间的值,这意味着我们检测的圆不能太大,也不能太小
2.3、对保留下来的距离进行排序
2.4、找到距离相同的那些值,并计算相同值的数量
2.5、设定一个阈值,只有相同值的数量大于该阈值,才认为该值是该圆心对应的圆半径
2.6、对每一个圆心,完成上面的2.1~2.5步骤,得到所有的圆半径
番外:切向量&&法向量
先给出切向量的大概推导:
意思是:对于2维下参数方程,某点处的切向量为分量对参数求导(x'(t0),y'(t0))
由此,可以得到以下归纳:
在非参数方程的归纳中,将x看作是参数,将问题转化为参数方程下的切向量和法向量问题,
最终加上隐函数求导法则,可以推出2维下某点的梯度即为该点处的法向量
另外还可以参见:https://wenku.baidu.com/view/7cd9ee6d7e21af45b307a8a1.html
不过该文章全篇基本用的是偏导和隐函数求导,与上述归纳有所出入,实质是一样的。
核心函数:
void HoughCircles( InputArray image, OutputArray circles,
int method, double dp, double minDist,
double param1=100, double param2=100,
int minRadius=0, int maxRadius=0 );
- 第一个参数image是输入图像矩阵,要求是灰度图像;
- 第二个参数 circles是输出,是一个包含检测到的圆的信息的向量,向量内第一个元素是圆的横坐标,第二个是纵坐标,第三个是半径大小;
- 第三个参数 methodmethod是所使用的圆检测算法,目前只有CV_HOUGH_GRADIENT一个可选;
- 第四个参数 dp是累加面与原始图像相比的分辨率的反比参数,dp=2时累计面分辨率是元素图像的一半,宽高都缩减为原来的一半,dp=1时,两者相同。(关于这个分辨率的概念没有理解透,按道理低分辨率应该意味着更快的检测速度,然而实测恰恰相反)
- 第五个参数 minDist定义了两个圆心之间的最小距离;
- 第六个参数param1是Canny边缘检测的高阈值,低阈值被自动置为高阈值的一半;
- 第七个参数param2是累加平面对是否是圆的判定阈值;
- 第八和第九个参数定义了检测到的圆的半径的最大值和最小值;
测试代码:
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<iostream>
#include<stdio.h>
using namespace std;
using namespace cv;
void main()
{
Mat srcImage = imread("F:\\opencv_re_learn\\circle.jpg");
if (!srcImage.data){
cout << "failed to read" << endl;
system("pause");
return;
}
imshow("srcImage", srcImage);
//转换为灰度图像
Mat src_gray;
cvtColor(srcImage, src_gray, CV_BGR2GRAY);
//高斯滤波
GaussianBlur(src_gray, src_gray, Size(9, 9), 2, 2);
vector<Vec3f>circles;
//霍夫圆检测
HoughCircles(src_gray, circles, CV_HOUGH_GRADIENT, 1, src_gray.rows / 8,
200, 100, 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(srcImage, center, 3, Scalar(0, 255, 0),
-1, 8, 0);
//绘制圆
circle(srcImage, center, radius, Scalar(0, 0, 255), 3, 8, 0);
}
imshow("HoughResult", srcImage);
waitKey(0);
}
实现效果:
快速调参版:
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<iostream>
#include<string>
using namespace std;
using namespace cv;
//bgr图像
Mat bgr;
Mat srcImage;
Mat srcGray;
//HSV图像
Mat hsv;
//分辨率
int px = 1;//分辨率取值
int px_Max = 5; //分辨率可取的最大值
//圆心最小距离
int center_distance = 10;//圆心最小距离取值
int center_distance_Max = 200; //圆心最小距离取值可取的最大值
//Canny边缘检测高阈值 【值越大,找到的边缘越少】
int Canny_value = 200;//
int Canny_value_Max = 300;//Canny边缘检测高阈值可取的最大值
//圆的判定阈值
int acc_circle = 100;
int acc_circle_Max = 300;//圆的判定阈值可取的最大值
//圆的半径最小值
int r_min = 0;
int r_min_Max = 300;//圆的半径最小值可取的最大值
//圆的半径最大值
int r_max= 0;
int r_max_Max = 500;//圆的半径最大值可取的最大值
//显示原图的窗口
string windowName = "src";
//输出图像的显示窗口
string dstName = "dst";
//输出图像
Mat dst;
//回调函数
void callBack(int, void*)
{
bgr = srcImage.clone();
vector<Vec3f>circles;
//霍夫圆检测
HoughCircles(srcGray, circles, CV_HOUGH_GRADIENT, px, center_distance,
Canny_value, acc_circle, r_min, r_max);
//将得到的结果绘图
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(bgr, center, 3, Scalar(0, 255, 0),
-1, 8, 0);
//绘制圆
circle(bgr, center, radius, Scalar(0, 0, 255), 3, 8, 0);
}
imshow(dstName, bgr);
}
int main()
{
//输入图像
srcImage = imread("F:\\opencv_re_learn\\circle2.jpg");
if (!srcImage.data){
cout << "falied to read" << endl;
system("pause");
return -1;
}
imshow(windowName, srcImage);
//颜色空间转换
cvtColor(srcImage, srcGray, CV_BGR2GRAY);
//高斯滤波
GaussianBlur(srcGray, srcGray, Size(9, 9), 2, 2);
//定义输出图像的显示窗口
namedWindow(dstName, CV_WINDOW_AUTOSIZE);
//分辨率
createTrackbar("px", dstName, &px, px_Max, callBack);
//圆心最小距离
createTrackbar("distance", dstName, ¢er_distance, center_distance_Max, callBack);
//Canny边缘检测高阈值
createTrackbar("Canny_value", dstName, &Canny_value, Canny_value_Max, callBack);
//圆的判定阈值
createTrackbar("acc_circle", dstName, &acc_circle, acc_circle_Max, callBack);
//圆的半径最小值
createTrackbar("r_min", dstName, &r_min, r_min_Max, callBack);
//圆的半径最大值
createTrackbar("r_max", dstName, &r_max, r_max_Max, callBack);
callBack(0, 0);
waitKey(0);
return 0;
}
参考文章:
https://blog.csdn.net/xia316104/article/details/44781157