###### tags: `東京威力 TEL`
{%hackmd DzaUykeiRfWOMkjFL_QkCQ %}
# I. 影像辨識 OpenCV
## <font color="orange"> 06. TEL 任務 -- 過程記錄</font>
### <font color="pink"> 6-1. 任務簡介</font>
今年的任務是從五個 $5cm * 5cm * 5cm$ 方塊中將 $T$、$E$、$L$ 三個木塊從 $A$ 地夾到 $B$ 地。
 
  
如果是判斷圖像的文字難度不高,用 OpenCV 和 Tesseract 去做 OCR 就可以了 (可以參考[這裡](https://medium.com/ntt-data-idi-platform/%E4%BD%BF%E7%94%A8-opencv-%E5%8F%8A-tesseract-%E9%80%B2%E8%A1%8C-ocr-%E8%BE%A8%E8%AD%98-2-%E4%BD%BF%E7%94%A8-opencv-%E9%80%B2%E8%A1%8C%E5%BD%B1%E5%83%8F%E5%89%8D%E8%99%95%E7%90%86-7c81120a4608))。
但如果要判斷自然環境下的文字就產很多問題:<font color ="yellow">
鏡頭影像扭曲(影響座標轉換計算)、拍攝角度(如何抓方塊中心)、反光</font>等等。
### <font color="pink"> 6-2. 我的作法</font>
這裡只寫大概的邏輯思路,後面會補上實際的過濾流程等等。
<font color="magenza"> Step 1.</font>
首先五個方塊中只有 E 有綠色的色塊,是最好辨識出來的,會最優先抓取。
<font color="magenza"> Step 2.</font>
對剩下的 C、F、T、L 做 Douglas-Peucker Algorithm,用 `Canny()` 後的輪廓邊數來區別字母。
關於 C 的邊數,我的演算法會落在 15 ~ 25 左右,適當的 $\epsilon$ 值只能靠經驗法則。
<font color="yellow">注意到 T 和 E 的邊數一樣,這是讓 E 最優先離開場上的原因。</font>
<font color="magenza"> Step 3.</font>
辨識出 T、L 後,分別抓走,完成任務。
接下來是座標轉換的部分。
<font color="magenza"> Step 4.</font>
如果使用吸盤吸取方塊,我們只關心方塊頂部的面。因為所有方塊的頂部是共平面的,所以可以從相機的像素座標 1-1 對應到世界座標中的某平面上的座標。
<font color ="grey">※ 如果採用夾爪可能會需要方塊的世界座標(三維),必須用深度相機或是利用兩顆單眼相機套用 [camera pinhole model](https://jeremy455576.medium.com/pinhole-camera-model%E7%90%86%E8%AB%96%E8%88%87%E5%AF%A6%E5%8B%99-7c331ed79fdb) 去計算。</font>
<font color="magenza"> Step 5.</font>
校正相機,去除 distortion 後,用 [ROS::tf package](http://wiki.ros.org/tf) 去做座標轉換,最後在 [rviz](http://wiki.ros.org/rviz) 上測試模型。
<font color="yellow">※ 這部分後來改成直接手刻座標點。</font>
### <font color="pink"> 6-3. 資料夾</font>
我在 TEL 資料夾內放了 5 個資料夾分別是各方塊的各種角度照片。

:::success
測試圖形辨識的時候,如果能夠成功過濾大部分的圖片,基本上用攝影機也不會有太大落差。
但偶爾還是有沒過濾到的情況(通常是太白或太暗),這時候我就會把攝影機的畫面截圖到 Problem 資料夾,重新對有問題的照片調整參數。
:::
相片的命名建議用有規律的,這樣之後可以用 `for()` 去看每個照片的過濾。

### <font color="pink"> 6-4. 辨識 C、F、T、L 木塊</font>
因為辨識 T 的程式碼套用了辨識 CFTL 的模板,所以先寫這個。完成結果如下:

:::warning
實際上辨識結果會一直跳動,而 C、F 被誤判為 target 的機率也不低,但這些未定的參數、浮動的結果等確定好相機位置及拍攝角度後再來調整就好。一開始先把整個架構寫好就可,不用太執著。
:::
<font color="yellow">**Step1. 找 inrange() 的參數**</font>
在上圖中,會看到第一層過濾後只剩下藍色字體,用了遮罩 hsv 範圍的 `inrange()` 函式。
因為要找到好的 range 很花時間,常常需要反覆調整,我寫了一個小程式專門來檢測用。
:::spoiler 尋找 HSV 參數,單一照片
```cpp=
/** 尋找 inrange 的 hsv 參數 **/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
// 字母過濾
void filt_letter(Mat img);
//// 對單一圖片過濾 ////
int main(){
// 圖片路徑
string path = "TEL/00_Problem/4.jpg";
Mat src = imread(path);
filt_letter(src);
return 0;
}
void filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
namedWindow("track_bar");
resizeWindow("track_bar", 500, 500);
createTrackbar("Hue Min", "track_bar", 0, 179);
createTrackbar("Hue Max", "track_bar", 0, 179);
createTrackbar("Sat Min", "track_bar", 0, 255);
createTrackbar("Sat Max", "track_bar", 0, 255);
createTrackbar("Val Min", "track_bar", 0, 255);
createTrackbar("Val Max", "track_bar", 0, 255);
setTrackbarPos("Hue Min", "track_bar", 0);
setTrackbarPos("Hue Max", "track_bar", 179);
setTrackbarPos("Sat Min", "track_bar", 0);
setTrackbarPos("Sat Max", "track_bar", 255);
setTrackbarPos("Val Min", "track_bar", 0);
setTrackbarPos("Val Max", "track_bar", 255);
// 按 q 退出視窗
while(waitKey(1) != 'q'){
int hue_m = getTrackbarPos("Hue Min", "track_bar");
int hue_M = getTrackbarPos("Hue Max", "track_bar");
int sat_m = getTrackbarPos("Sat Min", "track_bar");
int sat_M = getTrackbarPos("Sat Max", "track_bar");
int val_m = getTrackbarPos("Val Min", "track_bar");
int val_M = getTrackbarPos("Val Max", "track_bar");
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
imshow("Img", img);
result = Mat::zeros(img.size(), CV_8UC3);
// 遮罩原圖
bitwise_and(img, img, result, mask);
imshow("Result", result);
}
}
```
:::
我每次只會從一張照片中,去調整 `imrange()` 的參數。
然後再把這組新的參數丟到下面的程式中,去用這組參數跑每張圖片。
:::spoiler 固定 HSV 參數,跑多張照片
```cpp=
/** 觀察對多張圖片用 inrange() 後的過濾圖 **/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
void filt_letter(Mat img);
//// 觀察多圖片過濾情形 ////
int main(){
// 照片數量
int numOfPhotos = 15;
// 字母
string letter = "L";
Mat src;
for(int i=1; i<numOfPhotos; i++){
string path = "TEL/" + letter + "/" + letter + to_string(i) + ".jpg";
src = imread(path);
resize(src, src, Size(src.cols/3, src.rows/3));
filt_letter(src);
if(waitKey(0) == 'q') break; // 按 q 結束程式
}
return 0;
}
void filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
// 輸入 inrange() 的 hsv 參數
int hue_m = 90;
int hue_M = 110;
int sat_m = 143;
int sat_M = 255;
int val_m = 100;
int val_M = 255;
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
imshow("Img", img);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_and(img, img, result, mask);
imshow("Result", result);
}
```
:::
效果是這樣:(可以看到我故意在背景放了幾乎同色的干擾,之後可以想辦法過濾它們)

:::success
實際上我用了兩個 inrange() 來分別過濾亮暗,如果直接塞在同個 range 很容易吃到背景干擾。
:::spoiler 兩層 HSV,單一照片
```c=
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
void letter_filter_track(Mat img);
int main(){
/// 需要調整的變數 ///
string path = "D:\\pomelo\\code\\C++\\OpenCV\\TEL\\tel_ros\\opencv\\opencv_frame_0.png";
Mat src = imread(path);
letter_filter_track(src);
return 0;
}
void letter_filter_track(Mat img){
Mat img_hsv, mask, mask1, mask2, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
// "track_bar (dark)"
namedWindow("track_bar (dark)");
resizeWindow("track_bar (dark)", 500, 500);
createTrackbar("Hue Min", "track_bar (dark)", 0, 179);
createTrackbar("Hue Max", "track_bar (dark)", 0, 179);
createTrackbar("Sat Min", "track_bar (dark)", 0, 255);
createTrackbar("Sat Max", "track_bar (dark)", 0, 255);
createTrackbar("Val Min", "track_bar (dark)", 0, 255);
createTrackbar("Val Max", "track_bar (dark)", 0, 255);
setTrackbarPos("Hue Min", "track_bar (dark)", 90);
setTrackbarPos("Hue Max", "track_bar (dark)", 110);
setTrackbarPos("Sat Min", "track_bar (dark)", 143);
setTrackbarPos("Sat Max", "track_bar (dark)", 255);
setTrackbarPos("Val Min", "track_bar (dark)", 140);
setTrackbarPos("Val Max", "track_bar (dark)", 255);
// "track_bar (light)"
namedWindow("track_bar (light)");
resizeWindow("track_bar (light)", 500, 500);
createTrackbar("Hue Min", "track_bar (light)", 0, 179);
createTrackbar("Hue Max", "track_bar (light)", 0, 179);
createTrackbar("Sat Min", "track_bar (light)", 0, 255);
createTrackbar("Sat Max", "track_bar (light)", 0, 255);
createTrackbar("Val Min", "track_bar (light)", 0, 255);
createTrackbar("Val Max", "track_bar (light)", 0, 255);
setTrackbarPos("Hue Min", "track_bar (light)", 87);
setTrackbarPos("Hue Max", "track_bar (light)", 102);
setTrackbarPos("Sat Min", "track_bar (light)", 75);
setTrackbarPos("Sat Max", "track_bar (light)", 160);
setTrackbarPos("Val Min", "track_bar (light)", 227);
setTrackbarPos("Val Max", "track_bar (light)", 255);
while(waitKey(1) != 'q'){
// track_bar (dark)
int hue_m1 = getTrackbarPos("Hue Min", "track_bar (dark)");
int hue_M1 = getTrackbarPos("Hue Max", "track_bar (dark)");
int sat_m1 = getTrackbarPos("Sat Min", "track_bar (dark)");
int sat_M1 = getTrackbarPos("Sat Max", "track_bar (dark)");
int val_m1 = getTrackbarPos("Val Min", "track_bar (dark)");
int val_M1 = getTrackbarPos("Val Max", "track_bar (dark)");
Scalar lower1(hue_m1, sat_m1, val_m1);
Scalar upper1(hue_M1, sat_M1, val_M1);
inRange(img_hsv, lower1, upper1, mask1);
// imshow("mask1 (dark)", mask1);
// track_bar (light)
int hue_m2 = getTrackbarPos("Hue Min", "track_bar (light)");
int hue_M2 = getTrackbarPos("Hue Max", "track_bar (light)");
int sat_m2 = getTrackbarPos("Sat Min", "track_bar (light)");
int sat_M2 = getTrackbarPos("Sat Max", "track_bar (light)");
int val_m2 = getTrackbarPos("Val Min", "track_bar (light)");
int val_M2 = getTrackbarPos("Val Max", "track_bar (light)");
Scalar lower2(hue_m2, sat_m2, val_m2);
Scalar upper2(hue_M2, sat_M2, val_M2);
inRange(img_hsv, lower2, upper2, mask2);
imshow("mask2 (light)", mask2);
// bitwise
imshow("Img", img);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_or(mask1, mask2, mask);
bitwise_and(img, img, result, mask);
imshow("Result", result);
}
}
```
:::
<font color="yellow">**Step 2. 對過濾出的字母做一堆處理**</font>
我的整個流程是
1. <font color="magenza">先過濾字母</font>:即 Step 1。
2. <font color="magenza">找出邊緣</font>:對字母 `threshold()` 後 `findContours()`。
3. <font color="magenza">簡化邊緣</font>:對邊緣做 `approxPolyDP()`,需要找到適當參數。
4. <font color="magenza">過濾不好的邊緣</font>:將太少、太多點或面積太小的輪廓遮罩。
5. <font color="magenza">再從好的邊緣圖中找出邊緣</font>:得到儲存好邊緣的 `vector<Point>` 來標示好輪廓的中心點。
6. <font color="magenza">簡化好輪廓</font>:一樣是用 `approxPolyDP()`。
7. <font color="magenza"> 擬和旋轉矩形 + 邊長數量判斷字型 + 標示方塊中心點</font>:後來覺得擬和矩形其實可有可無,邊長數量判斷和標示中心點都是在一個雙重 for 迴圈中就可以寫完的。
相同的演算法,提供讀圖片和影片兩版:
:::spoiler CTFL 照片版本
```cpp=
/** 判斷 CTFL,E 也會被判成 T **/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 過濾字型,已固定 hsv 參數
Mat filt_letter(Mat img);
// 篩選出好的 contour,並判斷字母+標示中心點
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea);
// 印輪廓和點個數在圖上
Mat contours_info(Mat image, vector<vector<Point>> contours);
int main(){
/// 需要調整的變數
double epsilon = 5.5; // DP Algorithm 的參數
int minContour = 4; // 邊數小於 minContour 會被遮罩
int maxContour = 8; // 邊數大於 maxContour 會遮罩
double lowerBondArea = 20; // 面積低於 lowerBondArea 的輪廓會被遮罩
///
string path = "D:\\pomelo\\code\\C++\\OpenCV\\TEL\\tel_ros\\opencv\\opencv_frame_1.png";
Mat src = imread(path);
resize(src, src, Size(src.cols, src.rows));
Mat original_image = src.clone();
// imshow("original",original_image);
src = filt_letter(src);
filt_contour(original_image, src, epsilon, minContour, maxContour, lowerBondArea);
if(waitKey(0) == 'q') return 0;
}
Mat filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
// range 2 (light)
int hue_m = 82;
int hue_M = 110;
int sat_m = 73;
int sat_M = 255;
int val_m = 255;
int val_M = 255;
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_and(img, img, result, mask);
// imshow("Letter Filted", result);
return result;
}
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea){
cvtColor(image, image, COLOR_BGR2GRAY);
threshold(image, image, 40, 255, THRESH_BINARY);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// 1) 找出邊緣
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
// imshow("Contours Image (before DP)", image);
vector<vector<Point>> polyContours(contours.size()); // polyContours 用來存放折線點的集合
// 2) 簡化邊緣: DP Algorithm
for(size_t i=0; i < contours.size(); i++){
approxPolyDP(Mat(contours[i]), polyContours[i], epsilon, true);
}
Mat dp_image = Mat::zeros(image.size(), CV_8UC3); // 初始化 Mat 後才能使用 drawContours
drawContours(dp_image, polyContours, -2, Scalar(255,0,255), 1, 0);
imshow("Contours Image (After DP):", dp_image);
// 3) 過濾不好的邊緣,用 badContour_mask 遮罩壞輪廓
Mat badContour_mask = Mat::zeros(image.size(), CV_8UC3);
for (size_t a=0; a < polyContours.size(); a++){
// if 裡面如果是 true 代表該輪廓是不好的,會先被畫在 badContour)mask 上面
if(polyContours[a].size()<minContour || polyContours[a].size()>maxContour ||
contourArea(polyContours[a])<lowerBondArea){
for(size_t b=0; b < polyContours[a].size()-1; b++){
line(badContour_mask, polyContours[a][b], polyContours[a][b+1], Scalar(0,255,0), 3);
}
line(badContour_mask, polyContours[a][0], polyContours[a][polyContours[a].size()-1], Scalar(0,255,0), 1, LINE_AA);
}
}
// 進行壞輪廓的遮罩
Mat dp_optim_v1_image = Mat::zeros(image.size(), CV_8UC3);
cvtColor(badContour_mask, badContour_mask, COLOR_BGR2GRAY);
threshold(badContour_mask, badContour_mask, 0, 255, THRESH_BINARY_INV);
bitwise_and(dp_image, dp_image, dp_optim_v1_image, badContour_mask);
// imshow("DP image (Optim v1): ", dp_optim_v1_image);
// 4) 再從好的邊緣圖中找出邊緣
cvtColor(dp_optim_v1_image, dp_optim_v1_image, COLOR_BGR2GRAY);
threshold(dp_optim_v1_image, dp_optim_v1_image, 0, 255, THRESH_BINARY);
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(dp_optim_v1_image, contours2, hierarchy2, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 5) 簡化好輪廓 DP演算法
vector<vector<Point>> polyContours2(contours2.size()); // 存放折線點的集合
Mat dp_image_2 = Mat::zeros(dp_optim_v1_image.size(), CV_8UC3);
for(size_t i=0; i < contours2.size(); i++){
approxPolyDP(Mat(contours2[i]), polyContours2[i], epsilon, true);
}
drawContours(dp_image_2, polyContours2, -2, Scalar(255,0,255), 1, 0);
Mat dp_image_text = dp_image_2.clone();
dp_image_text = contours_info(dp_image_text, polyContours2);
imshow("Contours Image (After DP):", dp_image_text);
// 7) 擬和旋轉矩形 + 邊長數量判斷字型 + 標示方塊中心點
RotatedRect box; // 旋轉矩形 class
Point2f vertices[4]; // 旋轉矩形四頂點
vector<Point> pt; // 存一個contour中的點集合
for(int a=0; a<polyContours2.size(); a++){
// A) 旋轉矩形
pt.clear();
for(int b=0; b<polyContours2[a].size(); b++){
pt.push_back(polyContours2[a][b]);
}
box = minAreaRect(pt); // 找到最小矩形,存到 box 中
box.points(vertices); // 把矩形的四個頂點資訊丟給 vertices,points()是 RotatedRect 的函式
for(int i=0; i<4; i++){
line(dp_image_2, vertices[i], vertices[(i+1)%4], Scalar(0,255,0), 2); // 描出旋轉矩形
}
// 標示
circle(dp_image_2, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 繪製中心點
circle(original_image, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 與原圖比較
// B) 判斷字母(用邊長個數篩選)
if(polyContours2[a].size() == 6){ // L
// 標示
putText(dp_image_2, "L", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 255, 255), 3);
putText(original_image, "L", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 1, Scalar(0, 0, 255), 2);
}
if(polyContours2[a].size() == 8){ // T、E (此時場上不會有 E)
// 標示
putText(dp_image_2, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 255, 255), 3);
putText(original_image, "T", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 0, 255),2);
}
}
// imshow("Contours Filted", dp_image_2);
imshow("Original Image (highlight)", original_image);
}
Mat contours_info(Mat image, vector<vector<Point>> contours){
Mat info_image = Mat::zeros(image.size(), CV_8UC3);
// 輪廓數量
string name1 = "Number of Contours: " + to_string(contours.size());
// 點數量
int pt_count =0;
for(size_t a=0; a < contours.size(); a++){
for(size_t b=0; b < contours[a].size(); b++){
pt_count ++;
}
}
string name2 = "Number of Contours Points: " + to_string(pt_count);
putText(info_image, name1, Point(10,25), 0, 0.8, Scalar(0,255,0), 1, 1, false);
putText(info_image, name2, Point(10,60), 0, 0.8, Scalar(0,255,0), 1, 1, false);
drawContours( info_image, contours, -2, Scalar(0,0,255), 1, 0);
return info_image;
}
```
:::
:::spoiler CTFL 影片版本
```cpp=
/** 判斷 CTFL,E 也會被判成 T **/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 過濾字型,已固定 hsv 參數
Mat filt_letter(Mat img);
// 篩選出好的 contour,並判斷字母+標示中心點
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea);
int main(){
/// 需要調整的變數 ///
double epsilon = 5.5; // DP Algorithm 的參數
int minContour = 4; // 邊數小於 minContour 會被遮罩
int maxContour = 8; // 邊數大於 maxContour 會遮罩
double lowerBondArea = 20; // 面積低於 lowerBondArea 的輪廓會被遮罩
///
Mat src;
VideoCapture cap(0);
if(!cap.isOpened()) cout << "Cannot open capture\n";
while(true){
bool ret = cap.read(src);
if(!ret){
cout << "Cant receive frame\n";
break;
}
Mat original_image = src.clone();
src = filt_letter(src);
filt_contour(original_image, src, epsilon, minContour, maxContour, lowerBondArea);
if(waitKey(1) == 'q') break;
}
return 0;
}
Mat filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
int hue_m = 82;
int hue_M = 110;
int sat_m = 73;
int sat_M = 255;
int val_m = 255;
int val_M = 255;
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_and(img, img, result, mask);
// imshow("Letter Filted", result);
return result;
}
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea){
cvtColor(image, image, COLOR_BGR2GRAY);
threshold(image, image, 40, 255, THRESH_BINARY);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// 1) 找出邊緣
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
// imshow("Contours Image (before DP)", image);
vector<vector<Point>> polyContours(contours.size()); // polyContours 用來存放折線點的集合
// 2) 簡化邊緣: DP Algorithm
for(size_t i=0; i < contours.size(); i++){
approxPolyDP(Mat(contours[i]), polyContours[i], epsilon, true);
}
Mat dp_image = Mat::zeros(image.size(), CV_8UC3); // 初始化 Mat 後才能使用 drawContours
drawContours(dp_image, polyContours, -2, Scalar(255,0,255), 1, 0);
imshow("Contours Image (After DP):", dp_image);
// 3) 過濾不好的邊緣,用 badContour_mask 遮罩壞輪廓
Mat badContour_mask = Mat::zeros(image.size(), CV_8UC3);
for (size_t a=0; a < polyContours.size(); a++){
// if 裡面如果是 true 代表該輪廓是不好的,會先被畫在 badContour)mask 上面
if(polyContours[a].size()<minContour || polyContours[a].size()>maxContour ||
contourArea(polyContours[a])<lowerBondArea){
for(size_t b=0; b < polyContours[a].size()-1; b++){
line(badContour_mask, polyContours[a][b], polyContours[a][b+1], Scalar(0,255,0), 3);
}
line(badContour_mask, polyContours[a][0], polyContours[a][polyContours[a].size()-1], Scalar(0,255,0), 1, LINE_AA);
}
}
// 進行壞輪廓的遮罩
Mat dp_optim_v1_image = Mat::zeros(image.size(), CV_8UC3);
cvtColor(badContour_mask, badContour_mask, COLOR_BGR2GRAY);
threshold(badContour_mask, badContour_mask, 0, 255, THRESH_BINARY_INV);
bitwise_and(dp_image, dp_image, dp_optim_v1_image, badContour_mask);
// imshow("DP image (Optim v1): ", dp_optim_v1_image);
// 4) 再從好的邊緣圖中找出邊緣
cvtColor(dp_optim_v1_image, dp_optim_v1_image, COLOR_BGR2GRAY);
threshold(dp_optim_v1_image, dp_optim_v1_image, 0, 255, THRESH_BINARY);
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(dp_optim_v1_image, contours2, hierarchy2, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 5) 簡化好輪廓 DP演算法
vector<vector<Point>> polyContours2(contours2.size()); // 存放折線點的集合
Mat dp_image_2 = Mat::zeros(dp_optim_v1_image.size(), CV_8UC3);
for(size_t i=0; i < contours2.size(); i++){
approxPolyDP(Mat(contours2[i]), polyContours2[i], epsilon, true);
}
drawContours(dp_image_2, polyContours2, -2, Scalar(255,0,255), 1, 0);
Mat dp_image_text = dp_image_2.clone();
imshow("Contours Image (After DP):", dp_image_text);
// 7) 擬和旋轉矩形 + 邊長數量判斷字型 + 標示方塊中心點
RotatedRect box; // 旋轉矩形 class
Point2f vertices[4]; // 旋轉矩形四頂點
vector<Point> pt; // 存一個contour中的點集合
for(int a=0; a<polyContours2.size(); a++){
// A) 旋轉矩形
pt.clear();
for(int b=0; b<polyContours2[a].size(); b++){
pt.push_back(polyContours2[a][b]);
}
box = minAreaRect(pt); // 找到最小矩形,存到 box 中
box.points(vertices); // 把矩形的四個頂點資訊丟給 vertices,points()是 RotatedRect 的函式
for(int i=0; i<4; i++){
line(dp_image_2, vertices[i], vertices[(i+1)%4], Scalar(0,255,0), 2); // 描出旋轉矩形
}
// 標示
circle(dp_image_2, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 繪製中心點
circle(original_image, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 與原圖比較
// B) 判斷字母(用邊長個數篩選)
if(polyContours2[a].size() == 6){ // L
// 標示
putText(dp_image_2, "L", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 255, 255), 3);
putText(original_image, "L", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 1, Scalar(0, 0, 255), 2);
}
if(polyContours2[a].size() == 8){ // T、E (此時場上不會有 E)
// 標示
putText(dp_image_2, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 255, 255), 3);
putText(original_image, "T", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0, 0, 255),2);
}
}
// imshow("Contours Filted", dp_image_2);
imshow("Original Image (highlight)", original_image);
}
```
:::
### <font color="pink"> 6-5. 辨識 E 木塊</font>
辨識 E 只在乎綠色的方塊中心,其實不用像上面那麼複雜,但索性就沿用上面的模板了。
:::spoiler E 照片版本
```cpp=
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 過濾字型,已固定 hsv 參數
Mat filt_letter(Mat img);
// 篩選出好的 contour,標示中心點E
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea);
int main(){
/// 需要調整的變數
double epsilon = 2; // DP Algorithm 的參數
int minContour = 3; // 邊數小於 minContour 會被遮罩
int maxContour = 6; // 邊數大於 maxContour
double lowerBondArea = 10; // 面積低於 lowerBondArea 的輪廓會被遮罩
string path = "D:\\pomelo\\code\\C++\\OpenCV\\TEL\\tel_ros\\opencv\\opencv_frame_2.png";
Mat src = imread(path);
resize(src, src, Size(src.cols/2, src.rows/2));
Mat original_image = src.clone();
src = filt_letter(src);
filt_contour(original_image, src, epsilon, minContour, maxContour, lowerBondArea);
if(waitKey(0) == 'q') return 0;
}
Mat filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
// green range
int hue_m = 62;
int hue_M = 88;
int sat_m = 60;
int sat_M = 255;
int val_m = 137;
int val_M = 255;
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_and(img, img, result, mask);
// imshow("Letter Filted", result);
return result;
}
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea){
cvtColor(image, image, COLOR_BGR2GRAY);
threshold(image, image, 40, 255, THRESH_BINARY);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// 1) 找出邊緣
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
imshow("A", image);
vector<vector<Point>> polyContours(contours.size()); // polyContours 用來存放折線點的集合
// 2) 簡化邊緣: DP Algorithm
for(size_t i=0; i < contours.size(); i++){
approxPolyDP(Mat(contours[i]), polyContours[i], epsilon, true);
}
Mat dp_image = Mat::zeros(image.size(), CV_8UC3); // 初始化 Mat 後才能使用 drawContours
drawContours(dp_image, polyContours, -2, Scalar(255,0,255), 1, 0);
// imshow("B", dp_image);
// 3) 過濾不好的邊緣,用 badContour_mask 遮罩壞輪廓
Mat badContour_mask = Mat::zeros(image.size(), CV_8UC3);
for (size_t a=0; a < polyContours.size(); a++){
// if 裡面如果是 true 代表該輪廓是不好的,會先被畫在 badContour_mask 上面
if(polyContours[a].size()<minContour || polyContours[a].size()>maxContour ||
contourArea(polyContours[a])<lowerBondArea){
for(size_t b=0; b < polyContours[a].size()-1; b++){
line(badContour_mask, polyContours[a][b], polyContours[a][b+1], Scalar(0,255,0), 3);
}
line(badContour_mask, polyContours[a][0], polyContours[a][polyContours[a].size()-1], Scalar(0,255,0), 1, LINE_AA);
}
}
// 4) 進行壞輪廓的遮罩
Mat dp_optim_v1_image = Mat::zeros(image.size(), CV_8UC3);
cvtColor(badContour_mask, badContour_mask, COLOR_BGR2GRAY);
threshold(badContour_mask, badContour_mask, 0, 255, THRESH_BINARY_INV);
bitwise_and(dp_image, dp_image, dp_optim_v1_image, badContour_mask);
// imshow("C", dp_optim_v1_image);
// 5) 再從好的邊緣圖中找出邊緣
cvtColor(dp_optim_v1_image, dp_optim_v1_image, COLOR_BGR2GRAY);
threshold(dp_optim_v1_image, dp_optim_v1_image, 0, 255, THRESH_BINARY);
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(dp_optim_v1_image, contours2, hierarchy2, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 6) 簡化好輪廓 DP演算法
vector<vector<Point>> polyContours2(contours2.size()); // 存放折線點的集合
Mat dp_image_2 = Mat::zeros(dp_optim_v1_image.size(), CV_8UC3);
for(size_t i=0; i < contours2.size(); i++){
approxPolyDP(Mat(contours2[i]), polyContours2[i], epsilon, true);
}
drawContours(dp_image_2, polyContours2, -2, Scalar(255,0,255), 1, 0);
// 7) 擬和旋轉矩形 + 標示方塊中心點
RotatedRect box; // 旋轉矩形 class
Point2f vertices[4]; // 旋轉矩形四頂點
vector<Point> pt; // 存一個contour中的點集合
for(int a=0; a<polyContours2.size(); a++){
pt.clear();
for(int b=0; b<polyContours2[a].size(); b++){
pt.push_back(polyContours2[a][b]);
}
box = minAreaRect(pt); // 找到最小矩形,存到 box 中
box.points(vertices); // 把矩形的四個頂點資訊丟給 vertices,points()是 RotatedRect 的函式
for(int i=0; i<4; i++){
line(dp_image_2, vertices[i], vertices[(i+1)%4], Scalar(0,255,0), 2); // 描出旋轉矩形
}
circle(dp_image_2, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 繪製中心點
circle(original_image, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 放回原圖比較
putText(dp_image_2, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0,0,255), 2);
putText(original_image, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0,0,255), 2);
}
// imshow("D", dp_image_2);
imshow("E", original_image);
}
```
:::
:::spoiler E 影片版本
```cpp=
/** 辨識 E 方塊,讀影片 **/
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 過濾字型,已固定 hsv 參數
Mat filt_letter(Mat img);
// 篩選出好的 contour,標示中心點E
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea);
int main(){
/// 需要調整的變數 ///
double epsilon = 2; // DP Algorithm 的參數
int minContour = 3; // 邊數小於 minContour 會被遮罩
int maxContour = 6; // 邊數大於 maxContour 會遮罩
double lowerBondArea = 10; // 面積低於 lowerBondArea 的輪廓會被遮罩
///
Mat src;
VideoCapture cap(0);
if(!cap.isOpened()) cout << "Cannot open capture\n";
while(true){
bool ret = cap.read(src);
if(!ret){
cout << "Cant receive frame\n";
break;
}
Mat original_image = src.clone();
src = filt_letter(src);
filt_contour(original_image, src, epsilon, minContour, maxContour, lowerBondArea);
if(waitKey(1) == 'q') break;
}
return 0;
}
Mat filt_letter(Mat img){
Mat img_hsv, mask, result;
cvtColor(img, img_hsv, COLOR_BGR2HSV);
// green range
int hue_m = 62;
int hue_M = 88;
int sat_m = 60;
int sat_M = 255;
int val_m = 137;
int val_M = 255;
Scalar lower(hue_m, sat_m, val_m);
Scalar upper(hue_M, sat_M, val_M);
inRange(img_hsv, lower, upper, mask);
result = Mat::zeros(img.size(), CV_8UC3);
bitwise_and(img, img, result, mask);
// imshow("Letter Filted", result);
return result;
}
void filt_contour(Mat original_image, Mat image, double epsilon, int minContour, int maxContour, double lowerBondArea){
cvtColor(image, image, COLOR_BGR2GRAY);
threshold(image, image, 40, 255, THRESH_BINARY);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
// 1) 找出邊緣
findContours(image, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
// imshow("A", image);
vector<vector<Point>> polyContours(contours.size()); // polyContours 用來存放折線點的集合
// 2) 簡化邊緣: DP Algorithm
for(size_t i=0; i < contours.size(); i++){
approxPolyDP(Mat(contours[i]), polyContours[i], epsilon, true);
}
Mat dp_image = Mat::zeros(image.size(), CV_8UC3); // 初始化 Mat 後才能使用 drawContours
drawContours(dp_image, polyContours, -2, Scalar(255,0,255), 1, 0);
// imshow("B", dp_image);
// 3) 過濾不好的邊緣,用 badContour_mask 遮罩壞輪廓
Mat badContour_mask = Mat::zeros(image.size(), CV_8UC3);
for (size_t a=0; a < polyContours.size(); a++){
// if 裡面如果是 true 代表該輪廓是不好的,會先被畫在 badContour_mask 上面
if(polyContours[a].size()<minContour || polyContours[a].size()>maxContour ||
contourArea(polyContours[a])<lowerBondArea){
for(size_t b=0; b < polyContours[a].size()-1; b++){
line(badContour_mask, polyContours[a][b], polyContours[a][b+1], Scalar(0,255,0), 3);
}
line(badContour_mask, polyContours[a][0], polyContours[a][polyContours[a].size()-1], Scalar(0,255,0), 1, LINE_AA);
}
}
// 4) 進行壞輪廓的遮罩
Mat dp_optim_v1_image = Mat::zeros(image.size(), CV_8UC3);
cvtColor(badContour_mask, badContour_mask, COLOR_BGR2GRAY);
threshold(badContour_mask, badContour_mask, 0, 255, THRESH_BINARY_INV);
bitwise_and(dp_image, dp_image, dp_optim_v1_image, badContour_mask);
// imshow("C", dp_optim_v1_image);
// 5) 再從好的邊緣圖中找出邊緣
cvtColor(dp_optim_v1_image, dp_optim_v1_image, COLOR_BGR2GRAY);
threshold(dp_optim_v1_image, dp_optim_v1_image, 0, 255, THRESH_BINARY);
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(dp_optim_v1_image, contours2, hierarchy2, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 6) 簡化好輪廓 DP演算法
vector<vector<Point>> polyContours2(contours2.size()); // 存放折線點的集合
Mat dp_image_2 = Mat::zeros(dp_optim_v1_image.size(), CV_8UC3);
for(size_t i=0; i < contours2.size(); i++){
approxPolyDP(Mat(contours2[i]), polyContours2[i], epsilon, true);
}
drawContours(dp_image_2, polyContours2, -2, Scalar(255,0,255), 1, 0);
// 7) 擬和旋轉矩形 + 標示方塊中心點
RotatedRect box; // 旋轉矩形 class
Point2f vertices[4]; // 旋轉矩形四頂點
vector<Point> pt; // 存一個contour中的點集合
for(int a=0; a<polyContours2.size(); a++){
pt.clear();
for(int b=0; b<polyContours2[a].size(); b++){
pt.push_back(polyContours2[a][b]);
}
box = minAreaRect(pt); // 找到最小矩形,存到 box 中
box.points(vertices); // 把矩形的四個頂點資訊丟給 vertices,points()是 RotatedRect 的函式
for(int i=0; i<4; i++){
line(dp_image_2, vertices[i], vertices[(i+1)%4], Scalar(0,255,0), 2); // 描出旋轉矩形
}
circle(dp_image_2, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 繪製中心點
circle(original_image, (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 0, Scalar(0,255,255), 8); // 放回原圖比較
putText(dp_image_2, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0,0,255), 2);
putText(original_image, "E", (vertices[0]+vertices[1]+vertices[2]+vertices[3])/4, 1, 3, Scalar(0,0,255), 2);
}
// imshow("D", dp_image_2);
imshow("E", original_image);
}
```
:::
### <font color="pink"> 6-6. 顯示輪廓個數、點個數</font>
這是寫辨識時拿來確認 DP algorithm 的 epsilon 值用的。
例如下圖是三個輪廓(T、E、L),總共是 8+8+6=22 個點。
這樣就是一個恰當的 epsilon 大小。

:::spoiler contours_info() 函式
```cpp=
Mat contours_info(Mat image, vector<vector<Point>> contours){
Mat info_image = Mat::zeros(image.size(), CV_8UC3);
// 輪廓數量
string name1 = "Number of Contours: " + to_string(contours.size());
// 點數量
int pt_count =0;
for(size_t a=0; a < contours.size(); a++){
for(size_t b=0; b < contours[a].size(); b++){
pt_count ++;
}
}
string name2 = "Number of Contours Points: " + to_string(pt_count);
putText(info_image, name1, Point(10,25), 0, 0.8, Scalar(0,255,0), 1, 1, false);
putText(info_image, name2, Point(10,60), 0, 0.8, Scalar(0,255,0), 1, 1, false);
drawContours( info_image, contours, -2, Scalar(0,0,255), 1, 0);
return info_image;
}
```
:::
:::spoiler contours_info() 用法
```cpp=
//...
// 5) 簡化好輪廓 DP演算法
vector<vector<Point>> polyContours2(contours2.size()); // 存放折線點的集合
Mat dp_image_2 = Mat::zeros(dp_optim_v1_image.size(), CV_8UC3);
for(size_t i=0; i < contours2.size(); i++){
approxPolyDP(Mat(contours2[i]), polyContours2[i], epsilon, true);
}
drawContours(dp_image_2, polyContours2, -2, Scalar(255,0,255), 1, 0);
Mat dp_image_text = dp_image_2.clone();
dp_image_text = contours_info(dp_image_text, polyContours2);
imshow("Contours Image (After DP):", dp_image_text);
//...
```
:::
### <font color="pink"> 6-7. 沒用到的外積函式</font>
當初有想說利用輪廓上的點,按照順序外積一圈,然後計算外積正負的數量來區別 T、E。
結果寫完函式後才發現兩個字母的外積結果也是一樣的 = =
在這裡放上他的遺體。
(這段程式碼沒有詳細檢查,但應該沒錯)
:::spoiler 外積遺體
```java=
// 輸入輪廓,return 外積為正的數量。
int crossContour(vector<Point> contour){
int max = contour.size();
int positiveCount = 0;
for(int i=0; i<max-1; i++){
positiveCount += ( crossProduct(contour[i], contour[i+1])>0 )? 1:0;
}
positiveCount += ( crossProduct(contour[0], contour[max-1])>0 )? 1:0;
return positiveCount;
}
float crossProduct(const Point &v1, const Point &v2){
return (v1.x*v2.y) - (v1.y*v2.x);
}
```
:::