二值图像分析:霍夫直线检测
1.霍夫变换的基本原理
图像霍夫(Hough
)变换是一种特别有用的图像变换,它的基本思想是将图像的2D平面坐标空间,也叫图像空间 X-Y ,变换到参数空间 P-Q,经过变换后原来在平面坐标空间难以提取的几何特征信息,比如直线和圆等在参数空间内可以很容易的提取出来。图像中的直线与圆检测就是典型的利用霍夫空间特性实现二值图像几何分析的例子。
1.1 图像空间下一条直线变换到参数空间中的一点
首先数学知识告诉我们,在一般的2D平面坐标空间 X-Y 中,可以用下面的式子来表示一条直线:
y
=
p
x
+
q
y=px+q
y=px+q
其中 p p p 为斜率, q q q 为截距。
也就是说,在2D平面坐标空间 X-Y 中,任意一条直线我们都可以用斜率和截距这两个值来表示。这样我们就可以专门建立一个斜率和截距的 P-Q 坐标系,用这个坐标系中的一个点(点的坐标分别代表斜率和截距)来表示图像空间中的一条直线。
举个栗子:
- 当 k=0,b=2 时,在图像空间中可以得到直线 y = 2 y=2 y=2,在参数空间中对应一点(0,2)。
- 当 k=-1,b=3 时,在图像空间中可以得到直线 y = − x + 3 y=-x+3 y=−x+3,在参数空间中对应一点(-1,3)。
- 当 k= 1,b=1 时,在图像空间中可以得到直线 y = x + 1 y=x+1 y=x+1,在参数空间中对应一点(1,1)。
经过上面的变换可以发现,在2D平面坐标空间 X-Y 中的一条直线和参数空间 P-Q 中的一个点对应。
1.2 图像坐标空间下一点变换到参数空间中的一条直线
当我们给出一个点
(
x
0
,
y
0
)
(x_0,y_0)
(x0,y0) 时,在图像空间中可以有无数条直线经过该点,但是所有经过该点的直线都满足下面的方程:
y
−
y
0
=
k
(
x
−
x
0
)
y-y_0=k(x-x_0)
y−y0=k(x−x0)
即:
y
=
k
x
−
k
x
0
+
y
0
y=kx-kx_0+y_0
y=kx−kx0+y0
令 p = k , q = y o − k x 0 p=k,q=y_o-kx_0 p=k,q=yo−kx0 ,则 p p p 为直线的斜率, q q q 为直线的截距, p , q p,q p,q 取不同的值对应不同的直线,但是在图像空间中这些直线都经过点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0) 。
对于每一条可能的直线
y
=
p
i
x
+
q
i
y=p_ix+q_i
y=pix+qi,通过上面图像空间下一条直线变换到参数空间中的一点转换方法,都可以表示成参数空间中的一个点
(
p
i
,
q
i
)
(p_i,q_i)
(pi,qi)。由于
(
x
0
,
y
0
)
(x_0,y_0)
(x0,y0) 是给定的,它们的值都已经固定下来,所以可以把
p
,
q
p,q
p,q 作为变量,不妨以
p
p
p 为自变量,
q
q
q 为因变量,则可以建立P-Q坐标系并且得到在该坐标系下关于变量
p
,
q
p,q
p,q 方程:
q
=
−
x
0
p
+
y
0
q=-x_0p+y_0
q=−x0p+y0
这样就可以得到一个P-Q坐标系中以 − x 0 -x_0 −x0为斜率, y 0 y_0 y0 为截距的直线。这条直线上每一点都可以代表图像空间中一条经过点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0) 的直线。
例如给定一个点A(1,2),则经过该点有无数条直线:
- 当 p = 0,q=2时,可以得到直线 y = 2 y=2 y=2,在参数空间中点为(0,2)。
- 当 p=-1,q=3时,可以得到直线 y = − x + 3 y=-x+3 y=−x+3,在参数空间中点为(-1,3)。
- 当p = 1,q=1时,可以得到直线 y = x + 1 y=x+1 y=x+1,在参数空间中点为(1,1)。
- …等。
在P-Q空间下,(0,2),(-1,3),(1,1)均在直线 q = p + 2 q=p+2 q=p+2上。
经过上面的变换可以发现,在图像空间中的一个点可以变换成参数空间中的一条直线,该条直线可以表示图像空间中所有经过图像空间中给定点的直线集合。
1.3 霍夫直线检测
现在我们从图像空间中选取任意一条直线:
y
=
k
x
+
b
y=kx+b
y=kx+b
则对于该直线上任意取两点
(
x
i
,
y
i
)
(x_i,y_i)
(xi,yi) 和
(
x
j
,
y
j
)
(x_j,y_j)
(xj,yj),则这两点应的满足:
y
i
=
k
x
i
+
b
,
y
j
=
k
x
j
+
b
y_i=kx_i+b,y_j=kx_j+b
yi=kxi+b,yj=kxj+b
按照上面将一点转换成参数空间中一条直线的方法,可以得到参数空间中的两条直线:
q
=
−
x
i
p
+
y
i
,
q
=
−
x
j
p
+
y
j
q=-x_ip+y_i,q=-x_jp+y_j
q=−xip+yi,q=−xjp+yj
显然,这两条直线相交于一点 ( k , b ) (k,b) (k,b)。
以此进行推广,如果图像空间中一条直线上有n个点,则这些点对应参数空间上的一个由n条直线组成的直线簇,并且这些直线相交于一点,该点的坐标即代表原直线的斜率和截距。
1.4 经典霍夫变换
上面的变换存在一个问题,就是当图像空间中的直线为竖直的时候,也就是其斜率为无限大的时候,则在参数空间中 p p p 的值会接近无限大从而没办法表示,所以一般通过极坐标方程来进行霍夫变换。
直线
L
L
L 的法向参数在极坐标中表示为:
ρ
=
x
c
o
s
θ
+
y
s
i
n
θ
=
x
2
+
y
2
s
i
n
(
θ
+
a
r
c
t
a
n
x
y
)
ρ=xcosθ+ysinθ=\sqrt{x^2+y^2}sin(θ+arctan\frac{x}{y})
ρ=xcosθ+ysinθ=x2+y2sin(θ+arctanyx)
其中 ρ ρ ρ 是直角坐标系原点到直线 L L L 的法线距离, θ θ θ 是该法线与 x x x 轴的正向夹角。
在极坐标参数空间变换下,图像空间中一条直线就和参数空间下一组曲线的交点相对应,也就是说,图像空间中一点对应参数空间中一条曲线。这样就把图像空间中直线的检测转换成极坐标参数空间中曲线交点的检测了。
1.5 用霍夫变换提取直线的方法
实际上图像空间中的直线可能并不是完全直的,不同部位的像素值粗细也可能不同,所以在实现直线检测算法时,要根据精度要求将参数空间 ρ-θ 离散化成一个二维的累加器阵列,也就是划分成一个个网格,每个网格近似看成一个点,经过同一个网格的曲线近似认为交于一点。
初始时累加器阵列中每个值都被置为0,将图像空间中每一点都转换成参数空间中的一条曲线,曲线经过一个格子则该位置的累加器值+1,由于通过同一个格子所对应的点近似认为共线,所以每个格子上累加器的值就代表了图像空间中有多少个点共线,该点的极坐标就代表了所共直线的信息。
这样,如果图像空间中包含若干直线,则在参数空间中就会有同样数量的格子出现局部极大值,通过检测这些极大直就可以检测出各个直线。
2.OpenCV提供的函数
OpenCV
中提供了不同种类的霍夫直线检测API,其中标准与多尺度霍夫变换都是通过HoughLines()
函数来提供,它是提取到直线在霍夫空间得几何特征,然后输出直线得两个极坐标参数,根据这两个参数我们可以计算得到空间坐标直线。
HoughLines()
函数原型如下:
void cv::HoughLines(InputArray image,OutputArray lines,
double rho,double theta,int threshold,double srn = 0,
double stn = 0,double min_theta = 0,double max_theta = CV_PI)
参数解释:
image
:输入图像,应当为单通道二值图像。lines
: 输出直线,里面的元素是Vec2f
类型,存放直线的 ρ和θ信息。rho
:极坐标 ρ 的划分步长。theta
:极坐标 θ 的划分步长。threshold
:累加器阈值,达到该阈值认为是一条直线。srn
、stn
:多尺度霍夫变换时候需要得参数,经典霍夫变换不需要。min_theta
: 最小角度。max_theta
:最大角度。
代码实践:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, const char **argv)
{
Mat srcImage = imread("/mnt/hgfs/winshare/images/line.png");
if (srcImage.empty()) {
cout<<"load image failed."<<endl;
return -1;
}
// 二值化
Mat dst, gray, binaryImage;
Canny(srcImage, binaryImage, 80, 160, 3, false);
imshow("binaryImage", binaryImage);
imwrite("/mnt/hgfs/winshare/images/canny_result.jpg", binaryImage);
// 标准霍夫直线检测
vector<Vec2f> lines;
HoughLines(binaryImage, lines, 1, CV_PI / 180, 150, 0, 0);
// 绘制直线
Point pt1, pt2;
for (int i = 0; i < lines.size(); ++i)
{
float rho = lines[i][0];
float theta = lines[i][1];
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
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(srcImage, pt1, pt2, Scalar(0, 0, 255), 3, LINE_AA);
}
imshow("result", srcImage);
imwrite("/mnt/hgfs/winshare/images/detect_result.jpg", srcImage);
waitKey(0);
return 0;
}
运行结果:
原图:
Canny
检测后:
提取出的直线:
另一个更加常用的函数是:
void HoughLinesP( InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double minLineLength = 0, double maxLineGap = 0 );
参数解释:
image
:输入图像,应当为单通道二值图像。lines
: 输出直线,里面的元素是Vec4i
类型,存放直线的坐标信息。rho
:极坐标 ρ 的划分步长。theta
:极坐标 θ 的划分步长。threshold
:累加器阈值,达到该阈值认为是一条直线。minLineLength
:最小线段长度,达到该长度才认为是一条直线。maxLineGap
: 最大线段间隔,两条共线的线段距离小于该值时认为是一个直线。