霍夫变换常用于检测直线特征,经扩展后的霍夫变换也可以检测其他简单的图像结构。
霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,它通过一种投票算法检测具有特定形状的物体。该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。
如上图,假定在一个88的平面像素中有一条直线,并且从左上角(1,8)像素点开始分别计算θ为0°、45°、90°、135°、180°时的ρ,图中可以看出ρ分别为1、(9√2)/2、8、(7√2)/2、-1,并给这5个值分别记一票,同理计算像素点(3,6)点θ为0°、45°、90°、135°、180°时的ρ,再给计算出来的5个ρ值分别记一票,此时就会发现ρ = (9√2)/2的这个值已经记了两票了,以此类推,遍历完整个88的像素空间的时候ρ = (9√2)/2就记了5票, 别的ρ值的票数均小于5票,所以得到该直线在这个88的像素坐标中的极坐标方程为 (9√2)/2=xCos45°+y*Sin45°,到此该直线方程就求出来了。(PS:但实际中θ的取值不会跨度这么大,一般是PI/180)。
在霍夫变换中我们常用公式
ρ = xcosθ + ysinθ
表示直线,其中ρ是圆的半径(也可以理解为原点到直线的距离),θ是直线与水平线所成的角度(0~180°),确定了它们,也就确定一条直线了,和下图略有出入的是实际的原点定在图片左上角。
原理是对于输入的二值图像中的像素点(有值的),按照步长(参数三参数四对应rho和theta的步长)分别计算出每个点上的所有可能的直线。记录下每条直线经过的点数(即存在多个点计算出的直线有交集),按照阈值(参数五)筛选符合条件的图像,下面给出基本霍夫变换的由来,原文见:霍夫变换。
基本原理
一条直线可由两个点A=(X1,Y1)和B=(X2,Y2)确定(笛卡尔坐标)
另一方面,也可以写成关于(k,q)的函数表达式(霍夫空间):
对应的变换可以通过图形直观表示:
变换后的空间成为霍夫空间。即:笛卡尔坐标系中一条直线,对应霍夫空间的一个点。
反过来同样成立(霍夫空间的一条直线,对应笛卡尔坐标系的一个点):
再来看看A、B两个点,对应霍夫空间的情形:
一步步来,再看一下三个点共线的情况:
可以看出如果笛卡尔坐标系的点共线,这些点在霍夫空间对应的直线交于一点:这也是必然,共线只有一种取值可能。
如果不止一条直线呢?再看看多个点的情况(有两条直线):
其实(3,2)与(4,1)也可以组成直线,只不过它有两个点确定,而图中A、B两点是由三条直线汇成,这也是霍夫变换的后处理的基本方式:选择由尽可能多直线汇成的点。
看看,霍夫空间:选择由三条交汇直线确定的点(中间图),对应的笛卡尔坐标系的直线(右图)。
到这里问题似乎解决了,已经完成了霍夫变换的求解,但是如果像下图这种情况呢?
k=∞是不方便表示的,而且q怎么取值呢,这样不是办法。因此考虑将笛卡尔坐标系换为:极坐标表示。
在极坐标系下,其实是一样的:极坐标的点→霍夫空间的直线,只不过霍夫空间不再是[k,q]的参数,而是的参数,给出对比图:
是不是就一目了然了?
1.霍夫变换
标准霍夫变换:HoughLines()函数
void HoughLines(InputArray image, OutputArray lines,double rho, double theta,
int threshold,double srn = 0, double stn = 0)
霍夫变换接收二值化的输入,即已经进行初步的轮廓检测之后,才进行直线检测;输出一组cv::Vec2f,通常用vectorCV::Vec2f接收,所以我们通常使用Canny检测之后进行霍夫变换。
输出的两个float数字表示(rho, theta),使用cv::line绘图,因其参数需要的是线段的两个端点,所以我们不得不进行还原操作。
作用:
霍夫线变换是一种来寻找直线的方法。
参数解析:
代码实例:
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
using namespace std;
int main(){
Mat srcImage,midImage,dstImage;
srcImage=imread("/Users/oumoemoe/Downloads/timg-2.jpeg");
Canny(srcImage, midImage, 50, 200,3);
cvtColor(midImage, dstImage, CV_GRAY2BGR);
vector<Vec2f> lines;
HoughLines(midImage, lines, 1, CV_PI/180, 150,0,0);
for(size_t i=0;i<lines.size();i++){
float rho=lines[i][0],theta=lines[i][1];
//lines是一个矢量,它有两个参数,第一个参数是rho也就是当前点到坐标原点的距离,第二个参数是角度。
//用i做变量是因为我们要获取每个点的信息,对每个点进行处理。
double a=cos(theta),b=sin(theta);
double x0=rho*a,y0=rho*b;
Point pt1,pt2;
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(dstImage, pt1, pt2, Scalar(55, 10,195),1,LINE_AA);
//pt1,pt2分为代表直线的起点和终点,毕竟两个点就能确定一条直线
}
imshow("1", midImage);
imshow("2", dstImage);
waitKey(0);
return 0;
}
---------------------
作者:酸甜味儿的萌萌酱
来源:CSDN
原文:https://blog.csdn.net/weixin_38969235/article/details/77498922
版权声明:本文为博主原创文章,转载请附上博文链接!
上面这些式子,可以用下面这个图来解释,其中这个1000,是随意取得,就是说上下到(x0,y0)这个点各1000
pt1,pt2两点的坐标就是通过下面的几何变换推倒出来的
效果图:
2.累计概率霍夫变换
void HoughLinesP(InputArray image, OutputArray lines,double rho,double theta,
int threshold, double minLineLength =0 , double maxLineGap = 0)
概率霍夫变换输出Vec4i,直接输出了每一条线段的首尾,绘图更加方便。它是霍夫变换的改进版,由于算法的改进(会沿着搜寻到的直线扫描图像),可以进一步检测到线段的长度,除了最小投票数(参数五)外,可以额外限制最小线段长度(参数六)和同一线段最大像素间距(参数七)。
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace std;
using namespace cv;
//mian()函数
int main()
{
//载入原始图
Mat srcImage = imread("1.jpg");
Mat midImage, dstImage;
//canny检测边缘转化为灰度图
Canny(srcImage, midImage, 50, 200, 3);
cvtColor(midImage, dstImage, CV_GRAY2BGR);
//进行霍夫变换
vector<Vec4i>lines;
HoughLinesP(midImage, lines, 1, CV_PI / 180, 80, 50, 10);
//一次在图中绘制出每条线段
for (size_t i = 0; i < lines.size(); i++)
{
Vec4i l = lines[i];
line(dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186, 88, 255), 1, LINE_AA);
}
//显示效果图
imshow("【原始图】", srcImage);
//边缘检测之后的图
imshow("【边缘检测后的图】", midImage);
//显示效果图
imshow("【效果图】", dstImage);
waitKey(0);
return 0;
}
3.霍夫圆变换
霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是非常相似的,仅仅是点相应的二维极径极角空间被三维的圆心点x, y还有半径r空间代替。说“大体上相似”的原因是。假设全然用同样的方法的话。累加平面会被三维的累加容器所代替:在这三维中,一维是x,一维是y。另外一维是圆的半径r。这就意味着须要大量的内存而且执行效率会非常低,速度会非常慢。
对直线来说, 一条直线能由參数极径极角(r,θ)表示. 而对圆来说, 我们须要三个參数来表示一个圆, 也就是:
这里的 表示圆心的位置 (下图中的绿点) 而 r 表示半径, 这样我们就能唯一的定义一个圆了, 见下图:
在OpenCV中。我们一般通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。
霍夫梯度法
【1】首先对图像应用边缘检測,比方用canny边缘检測。
【2】然后,对边缘图像中的每个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度。
【3】利用得到的梯度。由斜率指定的直线上的每个点都在累加器中被累加。这里的斜率是从一个指定的最小值到指定的最大值的距离。
【4】同一时候,标记边缘图像中每个非0像素的位置。
【5】然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈值而且大于其全部近邻。
这些候选的中心依照累加值降序排列,以便于最支持像素的中心首先出现。
【6】接下来对每个中心。考虑全部的非0像素。
【7】这些像素依照其与中心的距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径。
【8】假设一个中心收到边缘图像非0像素最充分的支持,而且到前期被选择的中心有足够的距离。那么它就会被保留下来。
这个实现能够使算法执行起来更高效,也许更加重要的是,能够帮助解决三维累加器中会产生很多噪声而且使得结果不稳定的稀疏分布问题。
人无完人,金无足赤。
同样,这个算法也并非十全十美的,还有很多须要指出的缺点。
缺点:
<1>在霍夫梯度法中,我们使用Sobel导数来计算局部梯度,那么随之而来的假设是,其能够视作等同于一条局部切线,并这个不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果。但也许会在输出中产生一些噪声。
<2>在边缘图像中的整个非0像素集被看做每个中心的候选部分。
因此,假设把累加器的阈值设置偏低。算法将要消耗比較长的时间。
第三。由于每个中心仅仅选择一个圆,假设有同心圆,就仅仅能选择当中的一个。
<3>由于中心是依照其关联的累加器值的升序排列的,而且假设新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有很多同心圆或者是近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。能够说这是一种比較极端的做法,由于在这里默认Sobel导数会产生噪声。若是对于无穷分辨率的平滑图像而言的话。这才是必须的。
HoughCircles( )函数具体解释
HoughCircles函数能够利用霍夫变换算法检測出灰度图中的圆。它和之前的HoughLines和HoughLinesP比較明显的一个差别是它不须要源图是二值的,而HoughLines和HoughLinesP都须要源图为二值图像。
void HoughCircles(InputArray image,OutputArray circles,int method, int dp,
int minDist,double param1 = 100,double param2 = 100,int minRadius = 0,int maxRadius = 0)
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace std;
using namespace cv;
//mian()函数
int main()
{
//载入原始图
Mat srcImage = imread("1.jpg");
Mat midImage, dstImage;
//转化为灰度图并平滑
cvtColor(srcImage, midImage, CV_BGR2GRAY);
GaussianBlur(midImage, midImage, Size(9, 9), 2, 2);
//进行霍夫圆变换
vector<Vec3f>circles;
HoughCircles(midImage, circles, HOUGH_GRADIENT, 1.5, 10, 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(dstImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
circle(dstImage, center, radius, Scalar(155, 50, 255), 3, 8, 0);
}
//显示效果图
imshow("【原始图】", srcImage);
//显示效果图
imshow("【效果图】", srcImage);
waitKey(0);
return 0;
}