霍夫线条变换
原理
霍夫直线变换一般用来检测直线,它要在边缘检测之后才能应用。
在数学上要定义一条直线通常有2种方法:
- 笛卡尔坐标系:两点确定一条直线,即 ( x 0 , y 0 ) , ( x 1 , y 1 ) (x_0,y_0), (x_1, y_1) (x0,y0),(x1,y1);
- 极坐标:极角和极径确定一个向量 ( r , θ ) (r,\theta) (r,θ),与该向量垂直,且经过其端点的直线只有一条,即:
y = ( − cos θ sin θ ) x + ( r sin θ ) y=(-\frac{\cos \theta}{\sin \theta})x+(\frac{r}{\sin \theta}) y=(−sinθcosθ)x+(sinθr)
该式变换可得:
r = x cos θ + y sin θ r=x \cos \theta + y \sin \theta r=xcosθ+ysinθ
因此对于极坐标系中的任意一点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),经过该点的所有直线都适用下面的等式:
r θ = x 0 ⋅ cos θ + y 0 ⋅ sin θ ( 1 ) r_{\theta}=x_0 \cdot \cos \theta + y_0 \cdot \sin \theta \qquad(1) rθ=x0⋅cosθ+y0⋅sinθ(1)
r θ r_{\theta} rθ表示垂直于直线的极径, θ \theta θ表示极径的极角,如下图:
对于任何确定的点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0), ( 1 ) (1) (1)式中的 x , y x, y x,y都是确定的,但是 r θ r_{\theta} rθ和 θ \theta θ是有很多解的,如果将这些解画出来,以 θ \theta θ为横坐标、 r θ r_{\theta} rθ为纵坐标作图,可以得到一个正弦曲线,这就是霍夫变换 ,这个坐标系也叫作霍夫空间 。比如,当 x 0 = 8 , y 0 = 6 x_0=8, y_0=6 x0=8,y0=6时,可以得到下图:
这里只考虑 r > 0 r>0 r>0且 0 < θ < 2 π 0<\theta<2 \pi 0<θ<2π的情况
在图片操作中,也是如此,每个像素的 ( x , y ) (x,y) (x,y)是确定的,因此对多个像素点进行霍夫变换就可以得到一组正弦曲线。如果这些曲线有交叉,说明这些像素点在同一条直线上。比如,在上图中在加上点 x 1 = 4 , y − 1 = 9 x_1 = 4, y-1=9 x1=4,y−1=9和 x 2 = 12 , y 2 = 3 x_2=12, y_2=3 x2=12,y2=3的正弦曲线,可以得到下图:
可以看到3条曲线都相交于点 ( 0.925 , 9.6 ) (0.925, 9.6) (0.925,9.6),也就是说点 ( x 0 , y 0 ) , ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_0, y_0), (x_1, y_1), (x_2, y_2) (x0,y0),(x1,y1),(x2,y2)都在同一条直线上,而这条直线对应的极角和极径分别为: θ = 0.925 π , r θ = 9.6 \theta = 0.925 \pi, r_{\theta}=9.6 θ=0.925π,rθ=9.6。
从上面的例子可以看到,通过正弦曲线的交点可以找到对应点的共同直线。在某个交点相交的曲线越多,说明这个交点对应的直线经过更多的点。这样的话,就可以设置一个阈值来确定相交的曲线的最小数量。
以上就是霍夫直线变换的思路。总结定义如下:
- 霍夫空间中的所有点称为accumulator;
- 每个点上经过的曲线的数量,即累计器的值称为votes;
- 曲线交点的纵坐标,即上例中的极径 r θ r_{\theta} rθ,定义为rho
- 曲线交点的横坐标,即上例中的极角 θ \theta θ,定义为theta
标准和概率霍夫变换
OpenCV实现了两种霍夫直线变换:
- 标准霍夫变换:
- 包括了上述例子中的大部分步骤,并最终给出向量 ( θ , r θ ) (\theta, r_{\theta}) (θ,rθ)的结果,需要画出检测出的直线,需要自己求出 ( θ , r θ ) (\theta, r_{\theta}) (θ,rθ)对应的直线上的两个点;
- 在OpenCV中使用
HoughLines()
函数实现
- 概率霍夫直线变换
- 霍夫直线变换的更有效率的版本,最终给出直线的两个端点 ( x 0 , y 0 , x 1 , y 1 ) (x_0, y_0, x_1, y_1) (x0,y0,x1,y1),可以直接根据这两个坐标画线;
- 在OpenCV中使用
HoughLinesP()
函数实现
API
上述OpenCV中的2个函数的原型分别如下:
标准霍夫变换
cpp
void cv::HoughLines(InputArray image, //8位单通道二值化原图(即,经过边缘检测后的图片)
OutputArray lines, //线条向量数组
double rho, //霍夫空间中的纵坐标的最小单位
double theta, //霍夫空间中的横坐标的最小单位
int threshold, //accumulator上经过的曲线的数量的阈值,即votes的阈值
double srn = 0, //极径rho的除数
double stn = 0, //极角theta的除数
double min_theta = 0, //检测直线的最小角度
double max_theta = CV_PI) //检测直线的最大角度,不超过一个圆周率
- 返回的数组
lines
中为检测到的线条的向量,每个线条向量都有2-3个元素,即 ( ρ , θ ) (\rho, \theta) (ρ,θ)或 ( ρ , θ , v o t e s ) (\rho, \theta, votes) (ρ,θ,votes)rho
和theta
分别为上述霍夫空间中的纵横坐标的最小单位,这两个参数确定了霍夫空间中的accumulator的精度,即rho
和theta
越小,霍夫空间中的accumulator就越多。srn
和stn
分别为rho和theta的除数。在经典霍夫变换中,rho和theta就是累加器accumulator的极径和极角,但是在多尺度霍夫变换中,精确的accumulator的极径和极角则分别为 r h o / s r n , t h e t a / s t n rho/srn,theta/stn rho/srn,theta/stn。如果将这个两个参数都设置为0,则使用的就是经典霍夫变换;如果设为正数,则为多尺度霍夫变换。- 只有 v o t e s > t h r e s h o l d votes>threshold votes>threshold的线条才会被返回到输出结果中
概率霍夫直线变换
cpp
void cv::HoughLinesP( InputArray image, //8位单通道二值化原图(即,经过边缘检测后的图片)
OutiputArray lines, //线条向量数组
double rho, //霍夫空间中纵坐标的最小单位
double theta, //霍夫空间中横坐标的最小单位
int threshold, //votes的阈值
double minLineLength = 0, //直线识别的最短长度
double maxLineGap = 0) //同一直线上的最大间隔
- 返回的数组
lines
中为检测到的线条的向量,每个线条向量都有4个元素, ( x 1 , y 1 , x 2 , y 2 ) (x_1, y_1, x_2, y_2) (x1,y1,x2,y2),即直线的两个端点的坐标miniLineLength
参数确定了最少多长的直线能放到检测结果中,如果没有达到这个长度,就不会识别为有效直线,默认值为0,即只要有长度都会被识别;maxLineGap
参数确定在一条直线上的同一系列点之间,多大的距离才不会被用同一条直线连接;如果两个点之间的距离大于这个参数,即使它们属于同一条直线也不会连在一起。
实例
对示例图片:"..\opencv\sources\samples\data\sudoku.png"
进行直线检测:
- 导入图片后灰度化(可选),然后进行边缘检测;
- 接着进行标准霍夫直线变换,并将直线检测结果绘制在边缘检测后的图上;
- 最后进行概率霍夫直线变换,并同样将结果绘制在边缘检测后的图上。
具体实现件代码及注释:
cpp
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src{ imread("sudoku.png") };
//Canny边缘检测
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
Mat canny;
Canny(gray, canny, 50, 200, 3);
//霍夫变换
vector<Vec2f> lines;
HoughLines(canny, //输入图
lines, //线条结果
1, //rho = 1
CV_PI / 180, //theta = 1弧度制
150, //votes阈值
0, //srn默认为0
0); //stn默认为0
Mat dst, dstP;
cvtColor(canny, dst, COLOR_GRAY2BGR); //转变回BGR图片
dstP = dst.clone();
//显示检测到的线条为红色
for (size_t i{ 0 }; i < lines.size(); i++) {
float rho{ lines[i][0] }, theta{ lines[i][1] }; //分别获取rho和theta值
Point pt1, pt2;
double a{ cos(theta) }, b{ sin(theta) };
double x0 = a * rho, y0 = b * rho; //通过rho和theta计算极坐标向量的端点
//计算直线上两点的坐标
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(dst, pt1, pt2, Scalar(0, 0, 255), 3, LINE_AA);
}
//概率霍夫变换
vector<Vec4i> linesP;
HoughLinesP(canny, //输入图
linesP, //线条结果
1, //rho = 1
CV_PI / 180, //theta = 1弧度制
50, //votes阈值
50, //直线最短长度
10); //直线最大间隔
//显示结果线条
for (size_t i{ 0 }; i < linesP.size(); i++) {
Vec4i l{ linesP[i] };
line(dstP, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 3, LINE_AA);
}
imshow("原图", src);
imshow("标准霍夫变换", dst);
imshow("概率霍夫变换", dstP);
waitKey(0);
}
运行结果:
中间是标准霍夫变换,右边是概率霍夫变换。
可以看到因为标准霍夫变换需要我们自己来确定直线的端点,所以一般只能将整条直线都连起来;但是概率霍夫变换能够根据图片中的信息确定直线的起点和终点。