Hough变换是图像处理中从图像中识别几何形状的基本方法之一。Hough变换的基本原理在于利用点与线的对偶性,将原始图像空间的给定的曲线通过曲线表达形式变为参数空间的一个点。这样就把原始图像中给定曲线的检测问题转化为寻找参数空间中的峰值问题。
在一个xOy 的坐标系空间里,经过(x1,y1)的直线有无数条,我们可以用a(斜率)和b(截距)来表示 yi=axi+b,如果我们将这个(x1,y1)当成一个常量,那么我们可以用(x1,y1)来表示a和b,因此我们吧aOb构成的空间称之为霍夫空间。
b=-x1a+y1,图像表示如下,右边的就是霍夫空间,我们也称之为参数空间
假如有直线是y=x, 取上面的三个点:A(0,0), B(1,1), C(22)。
那么过A点的直线的参数是:0=0+b--L1,
过B点的直线的参数要满足方程:1=k+b --L2,
过C点的直线的参数要满足方程:2=2k+b--L3,
L1、L2、L3 对应着参数平面上的三条直线,而这三条直线会相交于一点(k=1,b=0)。把图像平面上的点对应到参数平面上的线,若xOy上有两条直线,那么在霍夫空间就会看到两个峰值点。
在xOy坐标系下的一个点在参数坐标系中的一条直线,反之一样,在xOy坐标系下一条直线的所有点,它们的斜率和截距是相同的,所以它们在参数坐标系下对应于同一个点。
如上图所示,可以转换到极坐标空间(数学推导就不写了)。
只要曲线ρ=xi cosθ+y i sinθ,i=1,2,…在θoρ平面内相交于一个点 就可以了,如下图
若xoy 平面内有5个点,对应到θoρ平面(霍夫空间)的5条 曲线,可以看出1、2、3、4点对应的曲线是相交于一个点的,所以xoy 平面内的1、2、 3、4点是共线的;同样,5点和4点对应的曲线也相交于一个点,所以xoy 平面内的5点和4 点是共线的,如下图:
要判断xoy 平面内哪些点是共线的,首先求出每一个点对应到霍夫空间的曲线,然后判断哪几条曲线相交于一点,最后将相交于一点的曲线反过来对应到xoy 平面内 的点,这些点就是共线的,这就是在图像中进行标准霍夫直线检测的核心思想
首先,霍夫直线处理的是一个二值图像,并且一般都是黑色背景
那么如何利用计算出的θoρ空间中的这些点去验证哪些像素点是共线的呢?是用曲线相交方式来验证的,但是这里只是从曲线上取了一些离散的点,所以需要 引入一个工具,称为“计数器”,或者“投票器”,或者二维直方图。
若在xoy 平面内有任意一点(x1,y 1),过该点有无数条直线,但是原点到这些直线的距离不会超过sqrt(pow(x1,2)+pow(y1,2))=r。图像矩阵宽度为W、高度为H,那么可以构造以下计数器,用L代表整数r+1:
若图像宽度为10、高度为10,所以L=r+1=15,四个前景像素点的坐标分别为(6,4)、(5,5)、(3,7)、(2,8),那么所有霍夫 空间中的点坐标一共有4×180个(步长=1),(45°,round(6 cos 45°+4 sin 45°))=(45°, 7),(45°,round(5 cos 45°+5 sin 45°))=(45°,7),(45°,round(3 cos 45°+7 sin 45°))=(45°,7),(45°,round(2 cos 45°+8 sin 45°))=(45°,7)等,投票计数器中,在计数器(45°,7)这个位置的计数是4,这里的(45°,7)出现了4次,表明有四个像素点是共线的。
1)读取一幅带处理二值图像,最好背景为黑色;
2)获取图像空间的源像素数据;
3)通过量化霍夫参数空间为有限个值间隔等分或者累加格子,即r,θ;
4)霍夫变换算法开始,每个像素坐标点P(x, y)被转换到(r, θ)的曲线点上面,并累加到对应的格子数据点;
5)寻找最大霍夫值,设置阈值,反变换到图像空间
//void HoughLinesP(
// InputArray image, //输入图像,即源图像,需为8位的单通道二进制图像
// OutputArray lines,//OutputArray类型的lines ,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1,y_1, x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点
// double rho, // 角度步长
// double theta, // 以弧度为单位的角度精度的
// int threshold, //累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中
// double minLineLength = 0,//有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
// double maxLineGap = 0)//有默认值0,允许将同一行点与点之间连接起
效果如下
代码显示:(虽然出来了,但是没有阈值控制并且运行速度很慢,等了15秒)
map, vector> HTline(Mat I, Mat& accumulator, float steptheta = 1, float stepRho = 1)
{
// 1.投票器的创建与初始化
int rows = I.rows;
int cols = I.cols;
// 可能出现得到最大的垂线的长度 就是在博客中写得L=r+1;
int L = round(sqrt(pow(rows - 1, 2.0) + pow(cols - 1, 2.0)));
// 创建一个投票器
int numtheta = int(180.0 / steptheta);// 也就是说将这个分成了180分角度来计算投票的结果
int numrho = int(2 * L / stepRho + 1);
// 投票初始化
accumulator = Mat::zeros(Size(numtheta, numrho),CV_32SC1);
//2、初始化一个map ,用来存放共线的点
map, vector> lines;
for ( int i = 0; i < numrho ; i++)
{
for (int j = 0; j < numtheta ; j++)
{
// template pair make_pair(T1 a, T2 b) { return pair(a, b); }
lines.insert(make_pair(vector(j,i),vector()));
}
}
// 3、开始投票计数
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{// 循环遍历每一个像素
if (I.at(Point(col,row))==255)// 我么默认是黑色背景下的找白色
{ // 相当于是遍历180度,对每一度计算rho的值
for (int i = 0; i < numtheta; i++)
{
float rhocol = col * cos(steptheta * i / 180.0 * CV_PI);
float rhorow = row * sin(steptheta * i / 180.0 * CV_PI);
float rho = rhocol + rhorow;
// 计算投票到那个区域了
int n = int(round(rho + L) / stepRho);
// 累加1
accumulator.at(n,i)+=1;
// 将这个点放到lines 中记录起来
lines.at(vector(i, n)).push_back(Point(col, row));
}
}
}
}
return lines;
}
Mat grayACCU;
accum.convertTo(grayACCU,CV_32FC1,1.0/ maxvalue);
imshow("投票器的灰度级别显示",grayACCU);
// 显示投票器大于某一个阈值的直线
int vote = 150;
for (int r = 0; r < accum.rows; r++)
{
for (int c = 0; c < accum.cols; c++)
{
int current = accum.at(r,c);
//画直线 line 的首尾元素为起始点
if (current>vote)
{
int lt = accum.at(r - 1, c - 1);// 左上
int t= accum.at(r - 1, c );//正上方的
int rt = accum.at(r - 1, c + 1);// 右上角
int l = accum.at(r , c - 1);//左
int right = accum.at(r , c + 1);// 右
int lb = accum.at(r + 1, c - 1);//左下角的
int b = accum.at(r - 1, c );// 下
int rb = accum.at(r + 1, c + 1);//右下
// 判断这个位置是不是局部的最大值
if (current > lt && current > t&&
current > rt && current > l &&
current > right && current > lb &&
current > b && current > rb)
{
vector line_s=lines.at(vector(c,r));
int s = line_s.size();
// 划线
line(src, line_s.at(0), line_s.at(s-1),Scalar(255),2);
}
}
}
}
imshow("test_hough_line",src);
若圆的圆心坐标是(a,b),半径为r,则圆在xoy平面内的方程可表示为:(x- a)2+(y -b)2=r2,那反过来呢?
若有三个点(1,3)、(2,2)、(3,3)分别带入到上面的(x- a)2+(y -b)2=r2中,得到三个关于aOb的方程式,在霍夫空间中画出这三个圆得到下图:
当发生上图中右边的这种情况的时候我们是无法判断那个才是我们想要的圆心,这个时候我们需要引进第三维度的r来讨论一下,所以对于任意一个点(xi,y i)对应到abr空间中的锥面r2=(a- xi)2+(b-y i)2,那么如果多个锥面相交于一点(a′ ,b′ ,r′ ),则说明这个锥面对应着xoy平面内的点是共圆的并且圆心是(a′ ,b′ ),半径是r'
该过程相当于先固定r,然后转换为以上讨论的已知r的情况,即第二个问题是第三个 问题的一种特殊情况。与霍夫直线检测类似,图像的霍夫圆检测就是检测哪些前景或边缘像素点在同一个 圆上,并给出对应圆的圆心坐标及圆的半径;而且仍然需要计数器来完成该过程,只是 这里的计数器从二维变成了三维。
若图像I,宽度为W、高度为H, 一 般首先指定需要检测到的圆的半径范围 ,寄假设人[rmin,rmax],这样的话,我们大概可以计算出圆心(a,b)的大致取值范围,这样的话(a,b,r)的大致范围都会有了,除此之外我们还需要一个参数-》变换的步长 若有个亮点像素坐标(x1,y1),每次固定一个r,可以用极坐标公式转换计算a=x1-r cosθ,b=y 1-r sinθ。 但是三维的投票器计算就很拖时间,我记得上次做直线检测的时候时间大概是15s,那个是二维的,检测圆是三维时间不是更慢,所以看到书上第二种方法。
基于梯度的霍夫圆检测的大体步骤是,一、定位圆心(两个参数),二、计算半径 (一个参数)。在代码实现中,首先构造一个二维计数器,然后再构造一个一维计数器
输入的图像不用像函数 HoughLinesP和HoughLines一样必须是二值图
void HoughCircles(
InputArray image,// 输入图像矩阵,要求是灰度图像但是不一定是二值图像
OutputArray circles,// circles是一个包含检测到的圆的信息的向量(a,b,r)
int method,//圆检测算法CV_HOUGH_GRADIENT 就是上面介绍的梯度算法
double dp,//投票器的分辨率,dp=2时投票器是元素图像的一半,宽高都缩减为原来的一半,dp=1时,1:1
double minDist, //两个圆心之间的最小距离
double param1=100, //canny 算子的高阈值 低阈值为高阈值的一半;
double param2=100, //最小投票数,(就是那个投票数量,大于设的设个值就当成是是圆上的一个像素)
int minRadius=0, 检测到的圆的半径最小值
int maxRadius=0 );检测到的圆的半径最大值
代码显示:
int main(int args, char* arg)
{
const char* OUTPUT_TITLE = "Hough Reult";
Mat src, src_gray, dst,srcout;
// point
src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\116.jpg");
if (!src.data)
{
printf("could not load image....\n");
}
namedWindow("input_demo", CV_WINDOW_AUTOSIZE);
namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
imshow("input_demo", src);
// 1 做一个中值滤波 去噪
medianBlur(src, srcout,3);
// 2 转为8位的灰度图
cvtColor(srcout, srcout,CV_BGR2GRAY);
// 3 做霍夫圆变换
vector pCircles;// (a,b,r)
HoughCircles(srcout, pCircles,CV_HOUGH_GRADIENT,
1,// 分辨率
20,//两个圆心之间的最小距离
100,//canny 算子的高阈值 低阈值为高阈值的一半;
30, //最小投票数,(就是那个投票数量,大于设的设个值就当成是是圆上的一个像素)
5,//检测到的圆的半径最小值
60);//检测到的圆的半径最da 值
//void HoughCircles(
// InputArray image,// 输入图像矩阵,要求是灰度图像但是不一定是二值图像
// OutputArray circles,// circles是一个包含检测到的圆的信息的向量(a,b,r)
// int method,//圆检测算法CV_HOUGH_GRADIENT 就是上面介绍的梯度算法
// double dp,//投票器的分辨率,dp=2时投票器是元素图像的一半,宽高都缩减为原来的一半,dp=1时,1:1
// double minDist, //两个圆心之间的最小距离
// double param1 = 100, //canny 算子的高阈值 低阈值为高阈值的一半;
// double param2 = 100, //最小投票数,(就是那个投票数量,大于设的设个值就当成是是圆上的一个像素)
// int minRadius = 0, 检测到的圆的半径最小值
// int maxRadius = 0, 检测到的圆的半径最da 值
src.copyTo(dst);
Scalar color = Scalar(0, 0, 255);
for (size_t i = 0; i < pCircles.size(); i++)
{
Vec3f cc = pCircles[i];
circle(dst, Point(cc[0],cc[1]),cc[2],color,2,LINE_AA); // 显示圆
circle(dst, Point(cc[0], cc[1]), 2, Scalar(0, 255,0), 2, LINE_AA); // 检测圆心
}
imshow(OUTPUT_TITLE, dst);
waitKey(0);
return 0;
待续。。。。。