霍夫线变换是一种用来寻找直线的方法,在用霍夫线变换之前,需要先对图像进行边缘检测的处理,即霍夫线变换的直接输入只能是边缘二值图像,如果不这样处理,那么霍夫线变换检测出来的直线将不是我们所期望的,会出现各种横七竖八的直线。
Hough变换的主要思想是:假定二维数字图像用直角坐标系来表示,任一像素点可以用 (x,y) 来表示,一条直线在图像二维空间可由两个变量表示。例如:
a. 在笛卡尔坐标系:可由参数(m,b)斜率和截距来表示。
b. 在极坐标系:可由参数 极径和极角表示
我们先讨论笛卡尔坐标系(霍夫变换用的是极坐标形式)的情况,也就是斜截式方程 y = mx + b 的情况,同一条直线(m,b)是相同的,我们变换空间参数,将直线方程改为 b = -mx + y,这样在参数坐标系(m,b)中,同一条直线必经过(m0,b0),即在参数坐标系中真正的直线相交于同一点,基于此我们可认为相交点越多,那么这是直线的可能性就越大,这个阈值是需要我们设置的。当直线垂直于x轴时,它的斜率会是无穷大,这样会给我们编程实现带来极大的不便,所以我们在实际应用中采用的是直线的极坐标方式。
针对上图我们可以变换直线方程为
其中斜率m = -cos(theta) / sin(theta), 截值b = r / sin(theta)。
化简得: ,对于每一点(x0,y0),对应的参数空间通过这一点的直线可以定义为。
在极坐标中,同一条直线将相交于(r0,theta0)。这就意味着,一条直线能够通过平面 theta-r 寻找交于一点的曲线数量来检测,(在直角坐标空间转换后是直线,在极坐标空间转换后就是曲线了),越多的曲线交于一点也就意味着这个交点表示的直线由更多的点组成,我们可以通过设置直线上点的阈值来定义多少条直线交于一点我们才认为检测到了一条直线。所以霍夫线变换要做的,就是追踪图像中每个点对应曲线间的交点,如果交于一点的曲线的数量超过了阈值,那么可认为这个交点所代表的的参数对(theta0,r0)在原图像中为一条直线。
下面来看看OpenCV中的Hough变换。OpenCV中霍夫线变换是通过HoughLines或HoughLinesP来检测直线的。
//标准霍夫线变换,提供一组参数对(r,theta)的集合来表示检测到的直线 void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = 0, double stn = 0) //image:Hough变换的输入图像,也就是边缘检测的输出图像,是8位单通道的二值图像 //lines:存储检测到的直线的参数对(r,theta)的容器 //rho:参数极径r以像素值为单位的分辨率,我们常使用1像素,就是极径的长度用像素多少值表示 //theta:参数极角theta以弧度为单位的分辨率,我们常使用1度(CV_PI/180) //threshold:要“检测”一条直线所需最少的曲线交点,就是这个点对应一条直线所需最少的相交曲线数 //srn和stn:步长,多尺度霍夫变换用于得到精确rho和theta的除数因子
其思想大致是:
1) 将所有非零像素点逐个变换到霍夫空间,并累加到霍夫表中,统计累加值;
2) 找出累加值中大于阈值并且为邻域内的最大值的点,存入缓存中;
3) 排序通过霍夫变换检测到的直线段;
4) 将排序好的直线段从小到大存入输出缓存。
下面通过编程来对比这两种实现。先看HoughLines
int main() { Mat img_src, img_gray, img_canny; img_src = imread("F.jpg"); cvtColor(img_src, img_gray, CV_BGR2GRAY); Canny(img_gray, img_canny, 50, 200, 3); vector<Vec2f> lines; //threshold = 36 HoughLines(img_canny, lines, 1, CV_PI / 180, 36, 0, 0); //lines.size():检测到的直线的条数 for (size_t i = 0; i < lines.size(); ++i) { /*第i条直线的参数对,和HoughLines中的参数是不一样的*/ float rho = lines[i][0], theta = lines[i][1]; double a = cos(theta), b = sin(theta); double x0 = a*rho, y0 = b*rho; Point pt1(cvRound(x0 + 500 * (-b)), cvRound(y0 + 500 * (a))); Point pt2(cvRound(x0 - 500 * (-b)), cvRound(y0 - 500 * (a))); line(img_src, pt1, pt2, Scalar(0, 0, 255), 2, CV_AA); } imshow("figure", img_src); waitKey(0); return 0; }
先看看效果
HoughLines确实检测出了直线,但也仅仅是检测出线段所在的直线而已,并没有检测出线段端点。对于上面程序,有个地方需要注意一下
double a = cos(theta), b = sin(theta); double x0 = a*rho, y0 = b*rho; Point pt1(cvRound(x0 + 500 * (-b)), cvRound(y0 + 500 * (a))); Point pt2(cvRound(x0 - 500 * (-b)), cvRound(y0 - 500 * (a))); line(img_src, pt1, pt2, Scalar(0, 0, 255), 2, CV_AA);
上面的pt1和pt2是line函数描绘的线段的两个端点,这么说来,上面不能说是直线,应该是比较长的线段,还是有端点的,不过不是我们所感兴趣的。通过HoughLines获得的是直线的(r,theta),由此我们可以确定直线上的一点,然后通过theta来获取line要描绘的线段的两个端点,这两个端点的距离这里是通过数值500来指定的(其实是1000个像素点距离),直接看下图就都明白了
所以这种方法就会有几个很明显的问题:
1、距离值取大,检测出来的直线长于我们感兴趣的线段,即包含需要检测的线段
2、距离值取小,检测出来的直线还没有我们感兴趣的线段长(把500设置100,就会发现只是直线的一部分)
3、同一条直线检测出多条直线,存在直线误判
4、直线的端点它不能给出,不能确定直线是从哪开始从哪结束
当然知道(r,theta),我们有很多几何方法可以确定直线,但是结果需是两点式。
针对这个问题,OpenCV又给出了另一个函数HoughLinesP,这个方法是通过概率霍夫变换实现的
//统计概率霍夫线变换,输出检测到的直线的端点(x0,y0,x1,y1) void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0) //lines:存储检测到的直线的参数对(x0,y0,x1,y1) //image、rho、theta、threshold同HoughLines //threshold:判断是直线的最小投票数,就是映射空间中至少要有多少个相交点 //minLineLength:判断是直线的最短长度,不足长度的将被舍弃 //maxLineGap:同一直线上连接的点的最大长度,大于这个长度的不连接成直线
HoughLinesP在
其大致步骤为:
1) 统计图像中非零像素点点的个数;
2) 随机处理所有非零像素点,先随机检测出一部分直线,然后排出已检测出的直线上的点,在进行其他直线的检测;
3) 更新霍夫累加器,找到最可能的直线,就是找到大于阈值且为最大值的点变换,如果小于阈值,继续下个点的变换;
4) 如果大于阈值,则沿着直线向每个方向提取直线段,并计算出直线段参数(长度,端点),如果符合条件,则保存此线段,
然后过滤掉位于该直线上的点,不再参与其余线段的检测。
using namespace std; using namespace cv; class FindLines { private: //存储直线参数对的容器 vector<Vec4i> lines; //参数对 double rho; double theta; //判断直线的最小投票数 int minVote; //在判断直线的最短长度 double minLinesLength; //直线段允许的点之间的最大距离 double maxLineGap; public: //构造函数 FindLines() : rho(1), theta(CV_PI / 180), minVote(20), minLinesLength(50), maxLineGap(10) {} //设置精度(步长) void setAccResolution(double urho, double utheta) { rho = urho; theta = utheta; } //设置阈值 void setThreshold(int uminVote) { minVote = uminVote; } //设置直线最短长度和点之间最大距离 void setLineLengthAndGap(double uminLinesLength, double umaxLineGap) { minLinesLength = uminLinesLength; maxLineGap = umaxLineGap; } //霍夫变换检测直线 vector<Vec4i> findLines(Mat &binary) { lines.clear(); HoughLinesP(binary, lines, rho, theta, minVote, minLinesLength, maxLineGap); return lines; } //绘制检测出来的直线 void drawDetectedLines(Mat &image, Scalar color = Scalar(0, 0, 255), int thickness = 2) { vector<Vec4i>::const_iterator it_lines = lines.begin(); while (it_lines != lines.end()) { Point pt1((*it_lines)[0], (*it_lines)[1]); Point pt2((*it_lines)[2], (*it_lines)[3]); line(image, pt1, pt2, color, thickness); ++it_lines; } } }; int main() { Mat img_src, img_gray, img_canny; FindLines linefinder; img_src = imread("F.jpg"); cvtColor(img_src, img_gray, CV_BGR2GRAY); Canny(img_gray, img_canny, 50, 200, 3); linefinder.setThreshold(20); linefinder.setLineLengthAndGap(35, 10); linefinder.findLines(img_canny); linefinder.drawDetectedLines(img_src); imshow("figure", img_src); waitKey(0); return 0; }
对比这两个函数,HoughLines需要逐个的将非零像素点转换到霍夫空间,然后再逐个去统计;而HoughLinesP则先随便检测部分直线,然后排出直线上的点,像上面那种同一边缘检测出多条直线的情况就可以很好地避免,另外还可以检测出直线段的端点信息,执行效率更高。