OpenCV-霍夫变换

简介

霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。

霍夫变换在OpenCV中分为霍夫线变换和霍夫圆变换两种,我们下面将分别进行介绍。

霍夫线变换

OpenCV支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform,SHT)和多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)累计概率霍夫变换(Progressive Probabilistic Hough Transform ,PPHT)。

其中,多尺度霍夫变换(MSHT)为标准霍夫变换(SHT)在多尺度下的一个变种。累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。

其中

  1. 标准霍夫变换(Standard Hough Transform,SHT),由HoughLines函数调用
  2. 多尺度霍夫变换(Multi-Scale Hough Transform,MSHT),由HoughLines函数调用
  3. 累计概率霍夫变换(ProgressiveProbabilistic Hough Transform,PPHT),由HoughLinesP函数调用

原理

对于霍夫变换, 我们将采用极坐标系方式来表示直线. 因此, 直线的表达式可为:

1
2
y = 
(- \frac{\cos \theta}{\sin \theta})x + \frac{r}{\sin \theta}

化简为

1
r = x\cos \theta + y\sin \theta

HoughLines

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* image 输入图像,需为8-bit单通道,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
* lines 经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量(ρ,θ)表示
其中,ρ是离坐标原点((0,0)(也就是图像的左上角)的距离;θ是弧度线条旋转角度(0~垂直线,π/2~水平线)。
* rho 以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
* theta 以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
* threshold 累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值 threshold 的线段才可以被检测通过并返回到结果中。
* srn 对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
* stn 对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
* min_theta 对于标准和多尺度Hough变换,检查直线的最小角度。必须介于0和最大θ之间。
* max_theta 对于标准和多尺度Hough变换,检查直线的最大角度。必须介于min_theta和CV_PI之间。
*/
void 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 );

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 Mat mat_base = imread("26.jpg");
Mat mat_mid, mat_dst;
Canny(mat_base, mat_mid, 50, 200, 3);
cvtColor(mat_mid, mat_dst, COLOR_GRAY2BGR);
vector<Vec2f>lines;
HoughLines(mat_mid, lines, 1, CV_PI / 180, 150);
for (auto i = 0; i < lines.size(); ++i)
{
float rho = lines[i][0], theta = lines[i][1];
Point pt1, pt2;
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(mat_dst, pt1, pt2, Scalar(55, 100, 195), LINE_AA);
}

HoughLinesP

此函数在HoughLines的基础上末尾加了一个代表Probabilistic(概率)的P,表明它可以采用累计概率霍夫变换(PPHT)来找出二值图像中的直线。

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* image 输入图像,需为8-bit单通道,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
* lines 经过调用 HoughLinesP 函数后储存了霍夫线变换检测到线条的输出矢量。
每一条线由具有四个元素的矢量(x_1,y_1, x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
* rho 以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
* theta 以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
* threshold 累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值 threshold 的线段才可以被检测通过并返回到结果中。
* minLineLength 表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
* maxLineGap 允许将同一行点与点之间连接起来的最大的距离。
*/
void HoughLinesP( InputArray image,
OutputArray lines,
double rho,
double theta,
int threshold,
double minLineLength = 0,
double maxLineGap = 0 );

实例

1
2
3
4
5
6
7
8
9
10
11
Mat mat_base = imread("lou.png");
Mat mat_mid, mat_dst;
Canny(mat_base, mat_mid, 50, 200, 3);
cvtColor(mat_mid, mat_dst, COLOR_GRAY2BGR);
vector<Vec4i>lines;
HoughLinesP(mat_mid, lines, 1, CV_PI / 80, 50, 10);
for (auto i = 0; i < lines.size(); ++i)
{
Vec4i l = lines[i];
line(mat_dst, Point(l[0], l[1]), Point(l[1], l[2]), Scalar(55, 100, 195), LINE_8);
}

霍夫圆变换

霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径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

函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* image 输入图像,需为8位单通道
* circles 经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。
* method 使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。
* dp 用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。
例如,如果 dp = 1时,累加器和输入图像具有相同的分辨率。如果 dp = 2,累加器便有输入图像一半那么大的宽度和高度。
* minDist 为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。
这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
* param1 它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
* param2 它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。
它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
* minRadius 表示圆半径的最小值。
* maxRadius 表示圆半径的最小值。
*/
void HoughCircles( InputArray image,
OutputArray circles,
int method,
double dp,
double minDist,
double param1 = 100,
double param2 = 100,
int minRadius = 0,
int maxRadius = 0 );

需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。我们可以通过第八个参数 minRadius 和第九个参数 maxRadius 指定最小和最大的圆半径,来辅助圆检测的效果。或者,我们可以直接忽略返回半径,因为它们都有着默认值 0,单单用 HoughCircles 函数检测出来圆心,然后用额外的一些步骤来进一步确定半径。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
Mat mat_base = imread("ball.png");
Mat mat_mid, mat_dst = mat_base.clone();
cvtColor(mat_base, mat_mid, COLOR_BGR2GRAY);
GaussianBlur(mat_mid, mat_mid, Size(9, 9), 2, 2);
vector<Vec3f>circles;
HoughCircles(mat_mid, circles, HOUGH_GRADIENT, 1.5, 10, 200);
for (auto i = 0; i < circles.size(); ++i)
{
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
circle(mat_dst, center, 3, Scalar(0, 255, 0), -1, 8, 0);
circle(mat_dst, center, radius, Scalar(0, 0, 0), 3, 8, 0);
}