iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0

一、介紹

在我們找到影像的輪廓點之後,已經可以做出很多應用了,像是輪廓匹配、尋找ROI、幾何測試等。但如果我們想要描述影像上的線要怎麼辦,雖然我們可以透過顯示視窗辨識影像中的線,但對電腦來說只是一張有很多輪廓點的影像,並不知道線的存在。那我們該如何透過影像處理找到影像中的線呢,這時候就需要使用到霍夫轉換。我們就可以透過極座標的方式表示出影像中的線,對其做近似。

二、原理

1. 霍夫線轉換 (Hough Line Transform)

下圖顯示了一條直線 y=f(x),每個位於直線上的點都以 (xi, yi) 表示。

我們可以通過座標轉換,將這些點從笛卡爾坐標系(Cartesian Coordinate System),也就是下圖的X-Y平面,轉換為極座標系(Polar Coordinate System)。在這裡,ri 代表原點O到直線上的某一點(xi, yi)的距離,而θ表示與線垂直並從原點到直線上的點(xi, yi)的線,與X軸之間的夾角,透過這兩個參數可以形成一個極座標系

其中,ri 可以使用以下數學公式表示:
https://ithelp.ithome.com.tw/upload/images/20230927/201617320TUOLuLQ6N.png

https://ithelp.ithome.com.tw/upload/images/20230927/20161732djyAeHn0eC.png

經過座標轉換後,我們可以將每一條穿過點(xi, yi)的線表示為ri(θ)=xi*cos(θ)+yi*sin(θ);反之,我們可以使用一對 (ri, θ) 坐標來表示穿過點(xi, yi)的一條線。極座標系 (ri, θ) 坐標系提供了一種新的方式,可以更方便的描述直線和點之間的關係。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732ZM0bWMxZnC.png

那經過轉換過後的座標(ri,θ)有什麼特性呢,我們可以看到下面的四張圖,假設有兩函數分別為f(x)和g(x),這兩個函數代表直線方程式,而每一個圖的方程式有三個點被直線穿越,我們將這先點(xi,yi)帶入ri(θ)=xi*cos(θ)+yi*sin(θ),並畫出極座標的r-θ關係圖,如右邊兩個圖表。注意這些弦波的交點,只要(xi,yi)為f(x)的其中一個點,在極座標的r-θ關係圖中,0°<θ<180°並且r>0,就會有交點的產生,注意這裡的θ並不包含0°、180°

https://ithelp.ithome.com.tw/upload/images/20230927/20161732F6v23EQlx4.png

接著,我們從函數 f(x) 和 g(x) 中各選取兩個點,分別標記為 p 和 q。然後,我們將這兩個點的極座標化出在同一個平面上。你會發現,當這兩條弦波相交時,出現了一個額外的焦點(8.04, 153.44°)。這是因為點 p (-5, 8) 和點 q (-3, 12) 也共線於同一條直線。這個平面圖告訴我們所有弦波交叉的位置,並且這些交叉點表示這些點都位於同一條直線上,如前文所述。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732vcxALJUjxW.png

霍夫線轉換會將這些交點放到一個投票的平面,我們稱之為累加器(accumulator) ,當累加器的某個座標 (ri, θ)累計的點越多或越密集,代表這條直線方程式存在這張影像的機率越大,我們透過這種"投票"的方式,求出r、θ,使用座標轉換把極座標系轉回笛卡爾坐標系,求出我們的直線方程式。
https://ithelp.ithome.com.tw/upload/images/20230927/20161732jxvyc6uOKU.png

好了,現在我們了解了霍夫線轉換的數學定義,現在可以開始使用霍夫了嗎?答案是還不行,上述的數學解釋只著重在一個象限例子,無法完整描述存在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°)的箭頭同方向。這代表的我們終於可以透過極座標完整描述存在影像中的所有點。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732x9XbHOjNWQ.png

三、程式碼

1. 逐行解釋

1) 使用OpenCV實現霍夫線轉換

這行程式碼執行了線偵測(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的線才會被保留。

2) 畫出檢測到的線

這段程式碼是一個回調函數,當"Index"滑動條的值變化時,將自動調用。該函數執行以下操作:

  1. lines向量中取出選定索引position處的線段的rhotheta值。
  2. 計算rhotheta所對應的X-Y平面上的兩個點(x0, y0),其中:
    • x0計算為 rho * cos(theta),表示rho在X軸上的投影。
    • y0計算為 rho * sin(theta),表示rho在Y軸上的投影。
  3. 設定兩個cv::Point變數pt1pt2,用於表示線段的兩個端點。這些端點通過 (x0, y0) 和角度 theta 計算。
  4. 在影像上化出計算出的線段,並將有關選定線段的資訊(索引、rho、theta、x0和y0)輸出到控制台。
  5. 最後,使用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);
}

2. 完整程式碼

  1. 建立視窗命名為"Line",並調整大小。
  2. 使用Canny邊緣檢測算法對grayImage進行邊緣檢測。並使用霍夫轉換檢測影像中的線段,檢測結果存儲在lines向量中。
  3. 建立一個滑動條"Index",用於選擇lines向量中的線段。
  4. 當"Index"滑動條的值發生變化時,顯示Index選擇的線條。
#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);
}

四、測試結果

1) 測試圖

https://ithelp.ithome.com.tw/upload/images/20231009/201617327lw2FtmnBz.jpg

https://ithelp.ithome.com.tw/upload/images/20230927/201617328AtwMT5xhY.png

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

五、參考資料


上一篇
【Day25】OpenCV 使用Hu矩:比較形狀相似度
下一篇
【Day27】使用OpenCV進行霍夫圓轉換(Hough Circle Transform)
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Hell Kiki
iT邦新手 4 級 ‧ 2023-10-07 19:33:38

真的是好硬核 >_<

霍夫線變換我花了三天的時間才看懂數學的定義,文章花了兩天寫,真的硬

Hell Kiki iT邦新手 4 級 ‧ 2023-10-08 00:31:04 檢舉

感謝大大認真講解

0
philipshen
iT邦新手 5 級 ‧ 2023-10-09 00:48:04

大大真強,

中秋雙十連假文章照常發送,
請問一下,本次測試圖檔要去那下載呢?
我試用別的圖檔出現下列錯誤訊息。

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_'

Aborted (core dumped)

OS: ubuntu 20.04

感謝你的回覆

看更多先前的回應...收起先前的回應...

單從錯誤訊息看起來,應該是在這行程式碼有問題,由於這行程式碼建立一個滑動條,滑動條的初始最大值設定為0,最後才設定滑條最大值。推測有可能是滑動條初始最大值設定為負數造成Bad trackbar maximal value in function 'createTrackbar_'

cv::createTrackbar("Index", "Line", NULL, 0, on_line_index_change); // 建立一個滑動條用於選擇線的索引

解決方案

  1. 嘗試使用Debug斷點或是printf抓出lines.size() - 1的值有可能lines.size()-1為-1,這也就代表著在前面霍夫線變換沒有找到任何線,所以lines.size()為0。

  2. 試試直接在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);

我要留言

立即登入留言