在我們找到影像的輪廓點之後,已經可以做出很多應用了,像是輪廓匹配、尋找ROI、幾何測試等。但如果我們想要描述影像上的線要怎麼辦,雖然我們可以透過顯示視窗辨識影像中的線,但對電腦來說只是一張有很多輪廓點的影像,並不知道線的存在。那我們該如何透過影像處理找到影像中的線呢,這時候就需要使用到霍夫轉換。我們就可以透過極座標的方式表示出影像中的線,對其做近似。
下圖顯示了一條直線 y=f(x),每個位於直線上的點都以 (xi, yi) 表示。
我們可以通過座標轉換,將這些點從笛卡爾坐標系(Cartesian Coordinate System),也就是下圖的X-Y平面,轉換為極座標系(Polar Coordinate System)。在這裡,ri 代表原點O到直線上的某一點(xi, yi)的距離,而θ表示與線垂直並從原點到直線上的點(xi, yi)的線,與X軸之間的夾角,透過這兩個參數可以形成一個極座標系。
其中,ri 可以使用以下數學公式表示:
經過座標轉換後,我們可以將每一條穿過點(xi, yi)的線表示為ri(θ)=xi*cos(θ)+yi*sin(θ)
;反之,我們可以使用一對 (ri, θ) 坐標來表示穿過點(xi, yi)的一條線。極座標系 (ri, θ) 坐標系提供了一種新的方式,可以更方便的描述直線和點之間的關係。
那經過轉換過後的座標(ri,θ)有什麼特性呢,我們可以看到下面的四張圖,假設有兩函數分別為f(x)和g(x),這兩個函數代表直線方程式,而每一個圖的方程式有三個點被直線穿越,我們將這先點(xi,yi)帶入ri(θ)=xi*cos(θ)+yi*sin(θ)
,並畫出極座標的r-θ關係圖,如右邊兩個圖表。注意這些弦波的交點,只要(xi,yi)為f(x)的其中一個點,在極座標的r-θ關係圖中,0°<θ<180°並且r>0,就會有交點的產生,注意這裡的θ並不包含0°、180°。
接著,我們從函數 f(x) 和 g(x) 中各選取兩個點,分別標記為 p 和 q。然後,我們將這兩個點的極座標化出在同一個平面上。你會發現,當這兩條弦波相交時,出現了一個額外的焦點(8.04, 153.44°)。這是因為點 p (-5, 8) 和點 q (-3, 12) 也共線於同一條直線。這個平面圖告訴我們所有弦波交叉的位置,並且這些交叉點表示這些點都位於同一條直線上,如前文所述。
霍夫線轉換會將這些交點放到一個投票的平面,我們稱之為累加器(accumulator) ,當累加器的某個座標 (ri, θ)累計的點越多或越密集,代表這條直線方程式存在這張影像的機率越大,我們透過這種"投票"的方式,求出r、θ,使用座標轉換把極座標系轉回笛卡爾坐標系,求出我們的直線方程式。
好了,現在我們了解了霍夫線轉換的數學定義,現在可以開始使用霍夫了嗎?答案是還不行,上述的數學解釋只著重在一個象限例子,無法完整描述存在f(x,y)影像中的直線,而且別忘記影像平面的Y座標和上述的數學式子式相反的。
下圖為影像平面以及延伸過後的X、Y軸,分別有四個象限(Quadrant)。影像平面f(x,y)位於第1象限(Quadrant 1)。在第1象限裡的極座標可畫出斜率為負的直線。而第2象限、第4象限裡的級座標用於畫出斜率為正的直線。而在第3象限裡的極座標不管在座標為何,直線永遠不會穿過第一象限,也就是影像f(x,y)平面。如此沒用到派蒙決定給他取一個難聽的綽號,就叫他沒用的平面(No Function Plane)吧!
θ的區間為[0°,180°),那我們要怎麼描述第四象限的極座標呢,畢竟θ沒辦法表示180度以上的極座標?其實很簡單,可以看到下圖極座標(1,150°),有沒有發現其實只要將極座標(1,150°)中的r加上一個負號,就可以將極座標的方向映射到第四象限,也就是極座標(-1,150°),和極座標(-0.5,150°)的箭頭同方向。這代表的我們終於可以透過極座標完整描述存在影像中的所有點。
這行程式碼執行了線偵測(Line Detection)的操作,使用的是OpenCV的cv::HoughLines
函數。讓我們一步一步來解釋這行程式碼:
cv::HoughLines(edge_image, lines, 1, CV_PI / 180, 150);
edge_image
:是邊緣檢測後的二值化影像,使用Canny邊緣檢測函數處理灰階影像後得到。lines
:是一個向量(vector
),用來存儲極座標。每個線都以一個cv::Vec2f
表示極座標,其中包含兩個值:(rho, theta)。1
:是rho
的解析度,表示在影像中以1像素為單位搜索rho
的值。CV_PI / 180
:是極座標角度的解析度,表示以弧度為單位,這裡的解析度使用1度。150
:是閾值,用於確定一條線是否被認為是有效的。只有累積器中的值大於等於150的線才會被保留。這段程式碼是一個回調函數,當"Index"滑動條的值變化時,將自動調用。該函數執行以下操作:
lines
向量中取出選定索引position
處的線段的rho和theta值。rho
和theta
所對應的X-Y平面上的兩個點(x0, y0)
,其中:
x0
計算為 rho * cos(theta)
,表示rho在X軸上的投影。y0
計算為 rho * sin(theta)
,表示rho在Y軸上的投影。cv::Point
變數pt1
和pt2
,用於表示線段的兩個端點。這些端點通過 (x0, y0)
和角度 theta
計算。cv::imshow
在"Line"視窗中顯示包含線段的output
影像,讓使用者查看選定線段。void on_line_index_change(int position, void*) {
cv::Mat output;
cv::cvtColor(grayImage, output, cv::COLOR_GRAY2BGR);
float rho = lines[position][0];
float theta = lines[position][1];
cv::Point pt1, pt2;
//(r,theta)轉到X-Y平面的點(xi,yi)
double x0 = rho * cos(theta);
double y0 = rho * sin(theta);
pt1.x = cvRound(x0 + 1000 * (-sin(theta)));
pt1.y = cvRound(y0 + 1000 * (cos(theta)));
pt2.x = cvRound(x0 - 1000 * (-sin(theta)));
pt2.y = cvRound(y0 - 1000 * (cos(theta)));
cv::line(output, pt1, pt2, cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
printf("index:%d\tr:%.2f\ttheta:%.2f\tx0:%.2f\ty0:%.2f\n", position, rho, 180 * theta / CV_PI, x0, y0);
cv::imshow("Line", output);
}
grayImage
進行邊緣檢測。並使用霍夫轉換檢測影像中的線段,檢測結果存儲在lines
向量中。lines
向量中的線段。#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/core/utils/logger.hpp"
using namespace std;
void on_line_index_change(int position, void*);
// 存儲灰度影像的變數
cv::Mat grayImage;
vector<cv::Vec2f> lines;
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_SILENT);
grayImage = cv::imread("C:\\Users\\vince\\Downloads\\test_image6.jpg", cv::IMREAD_GRAYSCALE); // 讀取灰度圖像
cv::namedWindow("Line", cv::WindowFlags::WINDOW_NORMAL); // 建立一個視窗用於顯示線的詳細信息
cv::resizeWindow("Line", 512.0f * ((float)grayImage.cols / grayImage.rows), 512);
cv::Mat edge_image;
cv::Canny(grayImage, edge_image, 28, 16); // 使用Canny邊緣檢測
cv::HoughLines(edge_image, lines, 1, CV_PI / 180, 150); // 應用霍夫變換來檢測線段
cv::createTrackbar("Index", "Line", NULL, lines.size() - 1, on_line_index_change); // 建立一個滑動條用於選擇線的索引
cv::waitKey(0);
return 0;
}
// 當滑動條"Index"的值發生變化時調用的函數
void on_line_index_change(int position, void*) {
cv::Mat output;
cv::cvtColor(grayImage, output, cv::COLOR_GRAY2BGR);
float rho = lines[position][0];
float theta = lines[position][1];
cv::Point pt1, pt2;
//(r,theta)轉到X-Y平面的點(xi,yi)
double x0 = rho * cos(theta);
double y0 = rho * sin(theta);
pt1.x = cvRound(x0 + 1000 * (-sin(theta)));
pt1.y = cvRound(y0 + 1000 * (cos(theta)));
pt2.x = cvRound(x0 - 1000 * (-sin(theta)));
pt2.y = cvRound(y0 - 1000 * (cos(theta)));
cv::line(output, pt1, pt2, cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
printf("index:%d\tr:%.2f\ttheta:%.2f\tx0:%.2f\ty0:%.2f\n", position, rho, 180 * theta / CV_PI, x0, y0);
cv::imshow("Line", output);
}
index:0 r:57.00 theta:0.00 x0:57.00 y0:0.00
index:1 r:63.00 theta:0.00 x0:63.00 y0:0.00
index:2 r:70.00 theta:90.00 x0:-0.00 y0:70.00
index:3 r:76.00 theta:90.00 x0:-0.00 y0:76.00
index:4 r:533.00 theta:60.00 x0:266.50 y0:461.59
index:5 r:539.00 theta:60.00 x0:269.50 y0:466.79
index:6 r:373.00 theta:30.00 x0:323.03 y0:186.50
index:7 r:367.00 theta:30.00 x0:317.83 y0:183.50
index:8 r:-70.00 theta:150.00 x0:60.62 y0:-35.00
index:9 r:-64.00 theta:150.00 x0:55.43 y0:-32.00
index:10 r:228.00 theta:120.00 x0:-114.00 y0:197.45
index:11 r:222.00 theta:120.00 x0:-111.00 y0:192.26
真的是好硬核 >_<
霍夫線變換我花了三天的時間才看懂數學的定義,文章花了兩天寫,真的硬
感謝大大認真講解
大大真強,
terminate called after throwing an instance of 'cv::Exception'
what(): OpenCV(4.5.5) /home/philphoenix/projects/OpenCV/opencv-4.5.5/modules/highgui/src/window_gtk.cpp:1559: error: (-211:One of the arguments' values is out of range) Bad trackbar maximal value in function 'createTrackbar_'
OS: ubuntu 20.04
感謝你的回覆
單從錯誤訊息看起來,應該是在這行程式碼有問題,由於這行程式碼建立一個滑動條,滑動條的初始最大值設定為0
,最後才設定滑條最大值。推測有可能是滑動條初始最大值設定為負數造成Bad trackbar maximal value in function 'createTrackbar_'
。
cv::createTrackbar("Index", "Line", NULL, 0, on_line_index_change); // 建立一個滑動條用於選擇線的索引
嘗試使用Debug斷點或是printf抓出lines.size() - 1
的值有可能lines.size()-1
為-1,這也就代表著在前面霍夫線變換沒有找到任何線,所以lines.size()
為0。
試試直接在cv::createTrackbar
完成最大值設定,cv::setTrackbarMax
註解掉。
cv::createTrackbar("Index", "Line", NULL, lines.size() - 1, on_line_index_change); // 建立一個滑動條用於選擇線的索引
//cv::setTrackbarMax("Index", "Line", ); // 設定滑動條的最大值
然後為了方便測試,我後續有補上測試圖,可以先使用測試圖測試。
感謝Vincent大,
改成你修改過的code及使用你提供的測試圖檔,已經能正常執行了。
好奇的問一下,你的測試圖檔,是為了測試之用用程式特定作出來嗎?
為什麼不同index值,紅線都可以對應到每條直綫呢。
感謝回覆
測試圖檔是為了這篇文章做出來的,因為顏色只有黑(0)白(255),對於霍夫變換的運算較為單純,是一個很好的範例測試圖。
透過cv::createTrackbar
,所有檢測到的線都會儲存在陣列lines,透過index索引抓取lines的特定元素,找到線條。
vector<cv::Vec2f> lines;
cv::HoughLines(edge_image, lines, 1, CV_PI / 180, 150);
cv::createTrackbar("Index", "Line", NULL, lines.size() - 1, on_line_index_change);