霍夫变换(Hough Transform)是Paul Hough于1962年提出来的,一开始是用于检测图像中的直线的,后来还扩展到检测圆、检测任意形状的物体等。关于霍夫变换的博客多得不胜枚举,而且很多都是很厉害,解释的非常详细而且通俗。现在我就写一下我自己的理解,个人笔记,不一定准确。
一、原理理解部分
首先,我们关于平面中的一条直线有如下表达
在笛卡尔坐标中表示为
这里,我们知道平面中一条直线会经过无数个离散点,而经过某个点的直线会有N多条:
如上图所示,二维平面有P1~P7共7个点,然后就来拟合下,假设就拟合出上面L1~L5共5条直线,如果按照投票来算的话,那肯定是L1最有可能存在,因为直线L1经过图上的4个点,而其他直线基本就经过2个点,所以假设图像只有这几个点的话,那最有可能存在的直线就是L1啦。此时,如果我们知道直线L1的两个参数那么这条直线基本就确定了。然而,我们应该是不知道的吧,至少算法不是向我们人眼一样,一眼就觉得P1、P2、P5和P6可以拟合一条直线,直接取其中两个点计算斜率和截距就得到直线了。我们需要遍历斜率和截距的所有可能值,然后计算图上的点是否在某个斜率和截距表示的直线上,如果是的话,那么这条直线的投票数量加1,最后票数最多的直线的参数就是所要拟合的直线的参数了。
要遍历的话,我们就把斜截式表示为:
然后假设斜率w的取值为[0,10],那就遍历w和图上的点的坐标,根据上式计算截距b,对应的[0,10]投票数加1,最后票数最多的就可以拟合一条直线了。
我们知道,斜截式可以表示绝大部分的直线,但是有一种情况是斜截式表示不了的,就是直线垂直x轴的情况,此时斜率为无穷,好像用斜截式表示不了吧。所以就想到用极坐标的形式来表示,但是实际上应该不能算是极坐标吧,因为依然还是笛卡尔坐标,只不过用了角度和距离来表示罢了,看起来有点类似极坐标的表示罢了(挺多博客都直接说是极坐标表示,我觉得应该不准确吧)。这里还是先上图再推出公式的表达:
图中直线过了点,原点到直线的距离是可以计算的,这个距离用公式表达就是:
并且原点到直线的距离是唯一的,也就是说,如果角度θ确定了,那么无论取直线上的哪个点都是可以取得一个固定的距离ρ,不在同一直线上的点计算出来的ρ是不同的,所以在遍历角度的时候,就统计一次θ确定下不同ρ得到的票数,然后改变θ,再次统计不同ρ得到的票数,这样遍历完所有θ后我们就有很多组(θ,ρ)的组合以及他们得到的票数,当票数超过一定阈值或者取最大值的作为直线的参数,得到检测到的直线。
二、OpenCV实现
OpenCV提供的实现霍夫变换的函数有两个,一个是标准霍夫变换HoughLines,一个是概率霍夫变换HoughLinesP,HoughLines的函数原型为:
void HoughLines( InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0 );
其参数意义如下:
image:输入图像,灰度图
lines:直线集合,每个直线包含两个值,分别是ρ和θ
rho:距离精度,以像素为单位
theta:角度精度
threshold:识别阈值,也就是累积超过这个值的才会被认为是直线的参数
srn:对于多尺度霍夫变换,它是距离精度rho的除数,粗距离的精度为rho,精细的距离精度为rho/srn;
stn:对于多尺度霍夫变换,它是角度精度theta的除数,粗角度精度为theta,精细的角度精度为theta/srn;
当srn和stn都设置为0的时候,使用标准的霍夫变换。实现的例子(来自OpenCV官网例子修改)如下:
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
void help()
{
cout <<
"
This program demonstrates line finding with the Hough transform.
"
"Usage:
"
"./houghlines <image_name>, Default is pic1.jpg
" << endl;
}
int main(int argc, char** argv)
{
const char* filename = argc >= 2 ? argv[1] : "D:/building.jpg";
Mat src = imread(filename, 0);
if (src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
Mat dst, cdst;
Canny(src, dst, 50, 200, 3);
cvtColor(dst, cdst, CV_GRAY2BGR);
vector<Vec2f> lines;
HoughLines(dst, lines, 1, CV_PI / 180, 250);
for (size_t 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(cdst, pt1, pt2, Scalar(0, 0, 255), 1, CV_AA);
}
imshow("source", src);
imshow("detected lines", cdst);
waitKey();
return 0;
}
原图和检测图如下:
HoughLinesP的函数原型如下:
void HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0 );
前面的参数和标准霍夫变换是一样的,后面两个参数的意义如下:
minLineLength:最小线长,线长小于这个值的会被忽略
maxLineGap:同一条直线上,连接各点的最大允许间隔例子如下,同样来自OpenCV官网例子修改:
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
void help()
{
cout << "
This program demonstrates line finding with the Hough transform.
"
"Usage:
"
"./houghlines <image_name>, Default is pic1.jpg
" << endl;
}
int main(int argc, char** argv)
{
const char* filename = argc >= 2 ? argv[1] : "D:/building.jpg";
Mat src = imread(filename, 0);
if (src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
Mat dst, cdst;
Canny(src, dst, 50, 200, 3);
cvtColor(dst, cdst, CV_GRAY2BGR);
vector<Vec4i> lines;
HoughLinesP(dst, lines, 1, CV_PI / 180, 250, 50, 10);
for (size_t i = 0; i < lines.size(); i++)
{
Vec4i l = lines[i];
line(cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 1, CV_AA);
}
imshow("source", src);
imshow("detected lines", cdst);
waitKey();
return 0;
}
检测结果如下:
莫唱当年长恨歌,
人间亦自有银河。
石壕村里夫妻别,
泪比长生殿上多。
-- 袁枚 《马嵬》