###### 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$ 地。 ![](https://hackmd.io/_uploads/SywJv3Cs9.jpg =48%x) ![](https://hackmd.io/_uploads/BJLlv2Ao9.jpg =48%x) ![](https://hackmd.io/_uploads/ryixwnCjc.jpg =32%x) ![](https://hackmd.io/_uploads/HkGbv3Asq.jpg =32%x) ![](https://hackmd.io/_uploads/HknZDhRsq.jpg =32%x) 如果是判斷圖像的文字難度不高,用 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 個資料夾分別是各方塊的各種角度照片。 ![](https://hackmd.io/_uploads/SkLqV44h9.png =80%x) :::success 測試圖形辨識的時候,如果能夠成功過濾大部分的圖片,基本上用攝影機也不會有太大落差。 但偶爾還是有沒過濾到的情況(通常是太白或太暗),這時候我就會把攝影機的畫面截圖到 Problem 資料夾,重新對有問題的照片調整參數。 ::: 相片的命名建議用有規律的,這樣之後可以用 `for()` 去看每個照片的過濾。 ![](https://hackmd.io/_uploads/SyDZH4Nnq.png) ### <font color="pink"> 6-4. 辨識 C、F、T、L 木塊</font> 因為辨識 T 的程式碼套用了辨識 CFTL 的模板,所以先寫這個。完成結果如下: ![](https://hackmd.io/_uploads/ry_2OENhq.png) :::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); } ``` ::: 效果是這樣:(可以看到我故意在背景放了幾乎同色的干擾,之後可以想辦法過濾它們) ![](https://hackmd.io/_uploads/SJO_ZPVhq.gif =70%x) :::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 大小。 ![](https://i.imgur.com/MVe8X36.png =70%x) :::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); } ``` :::