在研究一幅圖像時,常常會遇到一些平面或線性問題,直線在圖像中頻繁可見。這些富有意義的特征在物體識别等圖像處理過程中扮演着重要的角色。本節主要記錄一種經典的檢測直線算法——霍夫變換(Hough Transform),用Hough變換檢測圖像中的直線和圓,開發平台為Qt5.3.2+OpenCV2.4.9。
一:Hough變換檢測圖像的直線
1.基礎Hough變換
在霍夫變換中,直線用以下方程表示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICO4EzM1AzM0EzMwMDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
其中,參數
表示一條直線到圖像原點(左上角)的距離,
表示與直線垂直的角度。如下圖所示,直線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", );
cv::Mat contours;
cv::Canny(image,contours,,);
// Hough變換檢測直線
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours,lines,
,PI/, // 步進尺寸
); // 最小投票數
實作基礎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", );
cv::Mat contours;
cv::Canny(image,contours,,);
// Hough變換檢測直線
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours,lines,
,PI/, // 步進尺寸
); // 最小投票數
// 以下步驟繪制直線
cv::Mat result(contours.rows,contours.cols,CV_8U,cv::Scalar());
image.copyTo(result);
// 以下周遊圖像繪制每一條線
std::vector<cv::Vec2f>::const_iterator it= lines.begin();
while (it!=lines.end())
{
// 以下兩個參數用來檢測直線屬于垂直線還是水準線
float rho= (*it)[]; // 表示距離
float theta= (*it)[]; // 表示角度
if (theta < PI/ || theta > *PI/) // 若檢測為垂直線
{
// 得到線與第一行的交點
cv::Point pt1(rho/cos(theta),);
// 得到線與最後一行的交點
cv::Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
// 調用line函數繪制直線
cv::line(result, pt1, pt2, cv::Scalar(), );
} else // 若檢測為水準線
{
// 得到線與第一列的交點
cv::Point pt1(,rho/sin(theta));
// 得到線與最後一列的交點
cv::Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
// 調用line函數繪制直線
cv::line(result, pt1, pt2, cv::Scalar(), );
}
++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(), deltaTheta(PI/), minVote(), minLength(), maxGap() {}
// 設定累加器的分辨率
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(,,));
};
#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)[],(*it2)[]);
cv::Point pt2((*it2)[],(*it2)[]);
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", );
if (!image.data)
{
qDebug() << "No Input Image";
return ;
}
// 首先應用Canny算法檢測出圖像的邊緣部分
cv::Mat contours;
cv::Canny(image, contours, , );
LineFinder finder; // 建立一對象
// 設定機率Hough參數
finder.setLineLengthAndGap(, );
finder.setMinVote(); //最小投票數
// 以下步驟檢測并繪制直線
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(,),);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
, // 累加器的分辨率(圖像尺寸/2)
, // 兩個圓之間的最小距離
, // Canny中的高門檻值
, // 最小投票數
, ); // 有效半徑的最小和最大值
完整代碼如下,隻需在main函數中添加:
// 檢測圖像中的圓形
image= cv::imread("c:/44.png",);
if (!image.data)
{
qDebug() << "No Input Image";
return ;
}
// 在調用cv::HoughCircles函數前對圖像進行平滑,減少誤差
cv::GaussianBlur(image,image,cv::Size(,),);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
, // 累加器的分辨率(圖像尺寸/2)
, // 兩個圓之間的最小距離
, // Canny中的高門檻值
, // 最小投票數
, ); // 有效半徑的最小和最大值
// 繪制圓圈
image= cv::imread("c:/44.png",);
if (!image.data)
{
qDebug() << "No Input Image";
return ;
}
// 一旦檢測到圓的向量,周遊該向量并繪制圓形
// 該方法傳回cv::Vec3f類型向量
// 包含圓圈的圓心坐标和半徑三個資訊
std::vector<cv::Vec3f>::const_iterator itc= circles.begin();
while (itc!=circles.end())
{
cv::circle(image,
cv::Point((*itc)[], (*itc)[]), // 圓心
(*itc)[], // 圓的半徑
cv::Scalar(), // 繪制的顔色
); // 圓形的厚度
++itc;
}
cv::namedWindow("Detected Circles");
cv::imshow("Detected Circles",image);
效果:
三:廣義Hough變換
對于一些形狀,其函數表達式比較複雜,如三角形、多邊形等,但還是可能使用霍夫變換定位這些形狀,其原理與以上的檢測是一樣的。先建立一個二維的累加器,用于表示所有可能存在目标形狀的位置。是以必須定義一個參考點,圖像上的每個特征點都對可能的參考點坐标進行投票。由于一個點可能位于形狀輪廓内的任意位置,所有可能的參考位置将在累加器中描繪出一個形狀,它将是目标形狀的鏡像。