###### tags: `東京威力 TEL`
# I. 影像辨識 OpenCV
## <font color="orange"> 05. 輪廓處理</font>
我們先看這張原圖:

找字母邊緣,會想到使用之前提及的 `Canny()`:
( 左為灰階,右為 `Canny()` 後的 )
 
不管是從原圖或是灰階圖中都可以看出<font color="yellow">每個字母本身都有些微的顏色深淺差異</font>,所以在做邊緣偵測時字母內部的深淺差也會被考慮進來。
當然可以透過調整 Canny的閥值(threshold)來改善,但根據我自己的經驗是不好找到恰當的參數值。
### <font color="pink">5-1. threshold() 函式</font>
對我來說一個更好上手的方法是先使用 `threshold()` 來過濾圖片,再去做 `Canny()`。
( 左為 `threshold()`,右為對左圖做 `Canny()` )
 
:::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()`)

:::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$ 降低。
:::

:::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);
```