在研究一幅图像时,常常会遇到一些平面或线性问题,直线在图像中频繁可见。这些富有意义的特征在物体识别等图像处理过程中扮演着重要的角色。本节主要记录一种经典的检测直线算法——霍夫变换(Hough Transform),用Hough变换检测图像中的直线和圆,开发平台为Qt5.3.2+OpenCV2.4.9。
一:Hough变换检测图像的直线
1.基础Hough变换
在霍夫变换中,直线用以下方程表示:
其中,参数表示一条直线到图像原点(左上角)的距离, 表示与直线垂直的角度。如下图所示,直线1的垂直线的角度 等于0,而水平线5的 等于二分之π。同时,直线3的角度 等于四分之π,而直线4的角度大约是0.7π。为了能够在 为区间[0,π]之间得到所有可能的直线,半径值可取为负数,这正是直线2的情况。
OpenCV提供两种霍夫变换的实现,基础版本是:cv::HoughLines。它的输入为一幅包含一组点的二值图像,其中一些排列后形成直线,通常这是一幅边缘图像,比如来自Sobel算子或Canny算子。cv::HoughLines函数的输出是cv::Vec2f向量,每个元素都是一对代表检测到的直线的浮点数(p, r0)。在设计程序时,先求出图像中每点的极坐标方程,若相交于一点的极坐标曲线的个数大于最小投票数,则将该点所对应的(p, r0)放入vector中,即得到一条直线。cv::HoughLines函数的调用方法如下:
// 基础版本的Hough变换
// 首先应用Canny算子获取图像的轮廓
cv::Mat image = cv::imread("c:/031.jpg", 0);
cv::Mat contours;
cv::Canny(image,contours,125,350);
// Hough变换检测直线
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours,lines,
1,PI/180, // 步进尺寸
80); // 最小投票数
实现基础Hough变换的完整代码如下,直接在main函数中添加:
#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 基础版本的Hough变换
// 首先应用Canny算子获取图像的轮廓
cv::Mat image = cv::imread("c:/031.jpg", 0);
cv::Mat contours;
cv::Canny(image,contours,125,350);
// Hough变换检测直线
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours,lines,
1,PI/180, // 步进尺寸
80); // 最小投票数
// 以下步骤绘制直线
cv::Mat result(contours.rows,contours.cols,CV_8U,cv::Scalar(255));
image.copyTo(result);
// 以下遍历图像绘制每一条线
std::vector<cv::Vec2f>::const_iterator it= lines.begin();
while (it!=lines.end())
{
// 以下两个参数用来检测直线属于垂直线还是水平线
float rho= (*it)[0]; // 表示距离
float theta= (*it)[1]; // 表示角度
if (theta < PI/4. || theta > 3.*PI/4.) // 若检测为垂直线
{
// 得到线与第一行的交点
cv::Point pt1(rho/cos(theta),0);
// 得到线与最后一行的交点
cv::Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
// 调用line函数绘制直线
cv::line(result, pt1, pt2, cv::Scalar(255), 1);
} else // 若检测为水平线
{
// 得到线与第一列的交点
cv::Point pt1(0,rho/sin(theta));
// 得到线与最后一列的交点
cv::Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
// 调用line函数绘制直线
cv::line(result, pt1, pt2, cv::Scalar(255), 1);
}
++it;
}
// 显示结果
cv::namedWindow("Detected Lines with Hough");
cv::imshow("Detected Lines with Hough",result);
return a.exec();
}
改变cv::HoughLines中的最小投票数,可以得到不同的检测结果,因此可知道投票数对于直线的判决具有重要意义。最小投票数为100时:
最小投票数为60时:
注意hough变换要求输入的是包含一组点的二值图像。
2.概率Hough变换
由输出结果可知,基础Hough变换检测出图像中的直线,但在许多应用场合中,我们需要的是局部的线段而非整条直线。正如代码中的情况,需要判定直线与图像边界的交点,才能确定一条线段,否则绘制的直线将穿过整幅图像。其次,霍夫变换仅仅查找边缘点的一种排列方式,由于意外的像素排列或是多条线穿过同一组像素,很有可能带来错误的检测。
为了克服这些难题,人们提出了改进后的算法,即概率霍夫变换,在OpenCV中对应函数cv::HoughLineP,以下给出该算法的实现过程,首先将其封装在类LineFinder中,linefinder.h中添加:
#ifndef LINEFINDER_H
#define LINEFINDER_H
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926
class LineFinder
{
private:
// 存放原图像
cv::Mat img;
// 向量中包含检测到的直线的端点
std::vector<cv::Vec4i> lines;
// 累加器的分辨率参数
double deltaRho; // 距离
double deltaTheta; // 角度
// 被判定为直线所需要的投票数
int minVote;
// 直线的最小长度
double minLength;
// 沿直线方向的最大缺口
double maxGap;
public:
// 默认的累加器分辨率为单个像素,角度为1度
// 不设置缺口和最小长度的值
LineFinder() : deltaRho(1), deltaTheta(PI/180), minVote(10), minLength(0.), maxGap(0.) {}
// 设置累加器的分辨率
void setAccResolution(double dRho, double dTheta);
// 设置最小投票数
void setMinVote(int minv);
// 设置缺口和最小长度
void setLineLengthAndGap(double length, double gap);
// 使用概率霍夫变换
std::vector<cv::Vec4i> findLines(cv::Mat& binary);
// 绘制检测到的直线
void drawDetectedLines(cv::Mat &image, cv::Scalar color=cv::Scalar(255,255,255));
};
#endif // LINEFINDER_H
接着对各个函数进行定义,在linefinder.cpp中添加:
#include "linefinder.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926
// 设置累加器的分辨率
void LineFinder::setAccResolution(double dRho, double dTheta) {
deltaRho= dRho;
deltaTheta= dTheta;
}
// 设置最小投票数
void LineFinder::setMinVote(int minv) {
minVote= minv;
}
// 设置缺口和最小长度
void LineFinder::setLineLengthAndGap(double length, double gap) {
minLength= length;
maxGap= gap;
}
// 使用概率霍夫变换
std::vector<cv::Vec4i> LineFinder::findLines(cv::Mat& binary)
{
lines.clear();
// 调用概率霍夫变换函数
cv::HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote, minLength, maxGap);
return lines;
}
// 绘制检测到的直线
void LineFinder::drawDetectedLines(cv::Mat &image, cv::Scalar color)
{
std::vector<cv::Vec4i>::const_iterator it2= lines.begin();
while (it2!=lines.end()) {
cv::Point pt1((*it2)[0],(*it2)[1]);
cv::Point pt2((*it2)[2],(*it2)[3]);
cv::line( image, pt1, pt2, color);
++it2;
}
}
最后简单修改main函数即可:
#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#include "linefinder.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = cv::imread("c:/031.jpg", 0);
if (!image.data)
{
qDebug() << "No Input Image";
return 0;
}
// 首先应用Canny算法检测出图像的边缘部分
cv::Mat contours;
cv::Canny(image, contours, 125, 350);
LineFinder finder; // 创建一对象
// 设置概率Hough参数
finder.setLineLengthAndGap(100, 20);
finder.setMinVote(80); //最小投票数
// 以下步骤检测并绘制直线
std::vector<cv::Vec4i>lines = finder.findLines(contours);
finder.drawDetectedLines(image);
cv::namedWindow("Detected Lines");
cv::imshow("Detected Lines", image);
return a.exec();
}
得到的结果如下:
简而言之,霍夫变换的目的是找到二值图像中经过足够多数量的点的所有直线,它分析每个单独的像素点,并识别出所有可能经过它的直线。当同一直线穿过许多点,便意味着这条线的存在足够明显。
而概率霍夫变换在原算法的基础上增加了少许改动,这些改动主要体现在:
1. 不再系统地逐行扫描图像,而是随机挑选像素点,一旦累加器中的某一项达到给定的最小值,就扫描沿着对应直线的像素并移除所有经过的点(即使它们并未投过票);
2. 概率霍夫变换定义了两个额外的参数:一个是可以接受的线段的最小长度,另一个是允许组成连续线段的最大像素间隔,虽然这些额外的步骤必然增加算法的复杂度,但由于参与投票的点数量有所减少,因此得到了一些补偿。
二:Hough变换检测图像中的圆
霍夫变换可以用于检测其他几何体,事实上,可以用参数方程表示的几何体都可以尝试用霍夫变换进行检测,比如圆形,它对应的参数方程为:
该函数包含三个参数,分别是圆心的坐标和圆的半径,这意味着需要三维的累加器。OpenCV中实现的霍夫圆检测算法通常使用两个步骤进行检测:
1. 二维累加器用于寻找可能为圆的位置。由于在圆周上的点的梯度应该指向半径的方向,因此对于每一个点,只有沿着梯度方向的项才得到增加(这需要预先设定最大和最小的半径);
2. 若找到了圆心,则构建一维的半径的直方图,这个直方图的峰值对应的是检测到的圆的半径。
以下给出霍夫变换检测圆形的实现方法,主要使用了函数cv::HoughCircles,它整合了Canny检测和霍夫变换,同时,在进行霍夫变换之前,建议对操作图像进行平滑,以减少可能引起误检测的噪声点:
// 在调用cv::HoughCircles函数前对图像进行平滑,减少误差
cv::GaussianBlur(image,image,cv::Size(7,7),1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
2, // 累加器的分辨率(图像尺寸/2)
50, // 两个圆之间的最小距离
200, // Canny中的高阈值
100, // 最小投票数
20, 80); // 有效半径的最小和最大值
完整代码如下,只需在main函数中添加:
// 检测图像中的圆形
image= cv::imread("c:/44.png",0);
if (!image.data)
{
qDebug() << "No Input Image";
return 0;
}
// 在调用cv::HoughCircles函数前对图像进行平滑,减少误差
cv::GaussianBlur(image,image,cv::Size(7,7),1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
2, // 累加器的分辨率(图像尺寸/2)
50, // 两个圆之间的最小距离
200, // Canny中的高阈值
100, // 最小投票数
20, 80); // 有效半径的最小和最大值
// 绘制圆圈
image= cv::imread("c:/44.png",0);
if (!image.data)
{
qDebug() << "No Input Image";
return 0;
}
// 一旦检测到圆的向量,遍历该向量并绘制圆形
// 该方法返回cv::Vec3f类型向量
// 包含圆圈的圆心坐标和半径三个信息
std::vector<cv::Vec3f>::const_iterator itc= circles.begin();
while (itc!=circles.end())
{
cv::circle(image,
cv::Point((*itc)[0], (*itc)[1]), // 圆心
(*itc)[2], // 圆的半径
cv::Scalar(255), // 绘制的颜色
6); // 圆形的厚度
++itc;
}
cv::namedWindow("Detected Circles");
cv::imshow("Detected Circles",image);
效果:
三:广义Hough变换
对于一些形状,其函数表达式比较复杂,如三角形、多边形等,但还是可能使用霍夫变换定位这些形状,其原理与以上的检测是一样的。先创建一个二维的累加器,用于表示所有可能存在目标形状的位置。因此必须定义一个参考点,图像上的每个特征点都对可能的参考点坐标进行投票。由于一个点可能位于形状轮廓内的任意位置,所有可能的参考位置将在累加器中描绘出一个形状,它将是目标形状的镜像。