###### tags: `東京威力 TEL` # I. 影像辨識 OpenCV ## <font color="orange"> 05. 輪廓處理</font> 我們先看這張原圖: ![](https://i.imgur.com/do7ITgK.png =90%x) 找字母邊緣,會想到使用之前提及的 `Canny()`: ( 左為灰階,右為 `Canny()` 後的 ) ![](https://i.imgur.com/cSh6riW.png =45%x) ![](https://i.imgur.com/bRJrFjw.png =45%x) 不管是從原圖或是灰階圖中都可以看出<font color="yellow">每個字母本身都有些微的顏色深淺差異</font>,所以在做邊緣偵測時字母內部的深淺差也會被考慮進來。 當然可以透過調整 Canny的閥值(threshold)來改善,但根據我自己的經驗是不好找到恰當的參數值。 ### <font color="pink">5-1. threshold() 函式</font> 對我來說一個更好上手的方法是先使用 `threshold()` 來過濾圖片,再去做 `Canny()`。 ( 左為 `threshold()`,右為對左圖做 `Canny()` ) ![](https://i.imgur.com/iMusSVw.png =45%x) ![](https://i.imgur.com/ugrNP5A.png =45%x) :::success 注意 threshold() 是一個獨立的函式喔,跟 Canny() 參數中的 threshold 沒有關係。 ::: threshold() 會把閥值以上和以下的值分開處理,例如保留或是統一改成某個特定值。 上例中,我對原先的灰階圖做一次 `threshold()` ,某亮度以上統一設成255,低於就都改為0。 所以最後只會看到白和色兩色,注意規則是由 `threshold()` 的最後一個參數決定。 :::spoiler <font color = "gray">參數細節可以參考 [這裡](https://blog.csdn.net/u012566751/article/details/77046445)。</font> ::: ><font color="magenza">threshold(輸入圖, 輸出圖, 閥值, dst中圖像最大值, 閥值類型)</font> 上圖程式碼: ```cpp= #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main(){ string path = "alphabat.jpg"; Mat src = imread(path); Mat grayImg(src.rows, src.cols, CV_8UC1); Mat cannyImg(src.rows, src.cols, CV_8UC3); cvtColor(src, grayImg, COLOR_RGB2GRAY); threshold(grayImg, grayImg, 50, 255, THRESH_BINARY); Canny(grayImg, cannyImg, 150, 200, 3); while(waitKey(1)!=27){ imshow("source", src); imshow("gray", grayImg); imshow("canny", cannyImg); } return 0; } ``` ### <font color="pink">5-2. 辨識邊界</font> 辨識邊界要先 `findContours()` 再根據找到的邊界點集去 `drawContours()`。 :::spoiler <font color = "gray"> 這兩個函式的[官網](https://docs.opencv.org/3.4/df/d0d/tutorial_find_contours.html)函式說明。 </font> ::: ><font color="magenza">findContours ( 輸入圖, 輸出陣列 contour, 輸出陣列 hierarchy, 模式, 方法, offset ) </font> ><font color="magenza">drawContours ( 輸出圖, 陣列 contours, contouridx, 顏色, 粗細, 陣列 hierarchy, 等級, offset ) </font> ```cpp= #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main(){ string path = "alphabat.jpg"; Mat src = imread(path); Mat grayImg(src.rows, src.cols, CV_8UC1); Mat contoursImg(src.rows, src.cols, CV_8UC3); cvtColor(src, grayImg, COLOR_RGB2GRAY); threshold(grayImg, grayImg, 50, 255, THRESH_BINARY); // find contours vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(grayImg, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); // draw contours Mat drawing = Mat::zeros(src.size(), CV_8UC3); for( size_t i = 0; i<contours.size(); i++){ Scalar color(0,0,255); drawContours( drawing, contours, -2, color, 1, 0, hierarchy); } while(waitKey(1)!=27){ imshow("source", src); imshow("gray", grayImg); imshow( "Contours", drawing ); } return 0; } ``` 效果如下:(注意這是對灰階圖片找邊界,不是對 `Canny()` 後的圖片 `findContours()`) ![](https://i.imgur.com/NNzMnJ6.png =75%x) :::info 目前看起來 findContours() 和 Canny() 的效果沒有太大差別,但我們日後可以自己寫想要什麼樣的邊界條件(也就是第四和第五個參數),而且要怎麼框住想要的邊界(邊界粗細、大小、顏色等等)也可以在 drawContours() 中去定義,功能性比 Canny() 多。 ::: --- `findContours()` 的第四、五個參數蠻重要的,會影響邊界怎麼被決定。這邊額外整理出來。 :yellow_heart: <font color = "yellow"> findContours() 參數4 -- mode </font> | enum | 代號|意義 | |:----:|:-------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 0 | RETR_EXTERNAL | retrieves all of the contours without establishing any hierarchical relationships. | | 1 | RETR_LIST | retrieves all of the contours and organizes them into a two-level hierarchy. At the top level, there are external boundaries of the components. At the second level, there are boundaries of the holes. If there is another contour inside a hole of a connected component, it is still put at the top level. | | 2 | RETR_CCOMP | retrieves all of the contours and reconstructs a full hierarchy of nested contours | :yellow_heart: <font color = "yellow"> findContours() 參數5 -- method </font> | enum | 代號 | 意義 | |:----:|:--------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | CHAIN_APPROX_NONE | compresses horizontal, vertical, and diagonal segments and leaves only their end points. For example, an up-right rectangular contour is encoded with 4 points. | | 2 | CHAIN_APPROX_SIMPLE | applies one of the flavors of the Teh-Chin chain approximation algorithm | | 3 | CHAIN_APPROX_TC89_L1 | retrieves all of the contours and reconstructs a full hierarchy of nested contours | ### <font color="pink">5-3. 計算輪廓與像素個數</font> :::spoiler <font color = "gray">OpenCV Contours 的 [參考資料](https://docs.opencv.org/4.x/d4/d73/tutorial_py_contours_begin.html)。</font> ::: 在計算前要先認識 `findContours()` 參數中的 `vector<vector<Point>> contours`。 `contours` 是元素為 `Point` 型別的二維陣列,記錄了所有邊界資訊。 `contours[i]` 代表第 $i$ 個輪廓,`contours[i][j]` 代表地 $i$ 個輪廓中的第 $j$ 個點。 :::info 點集連成一個封閉區域後,這些點集會叫做 contour(邊界),否則就只是一個 edge(邊)。 ::: #### 印出輪廓個數 ```cpp= Mat image = imread("example.jpg"); // 灰階二分法 cvtColor(image, image, COLOR_BGR2GRAY); threshold(image, image, 40, 255, THRESH_BINARY); vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE); printf("Number of Contours: %d\n", contours.size()); // 印出輪廓個數 ``` #### 印出所有點個數 注意如果要列印點個數,必須在 `findContours()` 中的第五個參數 `method` 中選擇用 `CHAIN_APPROX_NONE`。 ```cpp= Mat image = imread("example.jpg"); // 灰階二分法 cvtColor(image, image, COLOR_BGR2GRAY); threshold(image, image, 40, 255, THRESH_BINARY); vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE); size_t numOfpoint =0; // 點個數 for(size_t a; a<contours.size(); a++){ for(size_t b; b<contours[a].size(); b++){ numOfpoint++; } } printf("Number of points: %d\n", numOfpoint); // 印出點個數 ``` ### <font color="pink">5-4. 簡化邊界</font> 如果在 `findContours()` 的 `method` 參數是 `CHAIN_APPROX_NONE`,但後續想 <font color = "yellow">彈性調整邊的簡化程度</font>,可以透過 [Douglas–Peucker 演算法](https://zh.wikipedia.org/zh-tw/%E9%81%93%E6%A0%BC%E6%8B%89%E6%96%AF-%E6%99%AE%E5%85%8B%E7%AE%97%E6%B3%95) 來達到目的,OpenCV 有內建函式 [approxPolyDP()](org/3.4/d3/dc0/group__imgproc__shape.html#ga0012a5fdaea70b8a9970165d98722b4c) 。 :::success 演算法中唯一的變數 $\epsilon$,這個值越大,圖形就會越簡單,即邊數 $n$ 降低。 ::: ![](https://hackmd.io/_uploads/BJ_989Toq.gif) :::spoiler approxPolyDp函式的[官方資料](https://docs.opencv.org/3.4/d3/dc0/group__imgproc__shape.html#ga0012a5fdaea70b8a9970165d98722b4c)。 ::: ><font color="magenza">approxPolyDP(輸入曲線, 輸出曲線, epsilon, 是否輸出閉曲線)</font> ```cpp= Mat image = imread("example.jpg"); cvtColor(image, image, COLOR_BGR2GRAY); threshold(image, image, 40, 255, THRESH_BINARY); vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE); imshow("image", image); // Douglas–Peucker algorithm vector<vector<Point>> polyContours(contours.size()); // 存放折線點的集合 for(size_t i=0; i < contours.size(); i++){ approxPolyDP(Mat(contours[i]), polyContours[i], 2.5, true); } Mat dst_image = Mat::zeros(image.size(), CV_8UC3); drawContours(dst_image, polyContours, -2, Scalar(255,0,255), 1, 0); imshow("DP_image", dst_image); ```