###### tags: `東京威力 TEL` {%hackmd DzaUykeiRfWOMkjFL_QkCQ %} # II. Webcam ## <font color="orange"> 01. OpenCV on ROS</font> ### <font color="pink">1-1. 安裝 OpenCV package</font> ```= sudo apt update sudo apt install libopencv-dev python3-opencv ``` 安裝完後查看版本確認安裝完成: ```= python3 -c "import cv2; print(cv2.__version__)" ``` ### <font color="pink">1-2. CmakeList 的處理</font> :::spoiler cam.cpp ```cpp= #include <iostream> #include <opencv2/opencv.hpp> #include <ros/ros.h> 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); // 更完整的過濾字型 Image -- OptimV2 int main(int argc, char** argv){ ros::init(argc, argv, "E_cam"); ros::NodeHandle nh; Mat src; VideoCapture cap(0); /// 需要調整的變數 /// double epsilon = 3; // DP Algorithm 的參數 int minContour = 5; // 邊數小於 minContour 會被遮罩 int maxContour = 20; // 邊數大於 maxContour 會遮罩 double lowerBondArea = 45; // 面積低於 lowerBondArea 的輪廓會被遮罩 /// if(!cap.isOpened()) ROS_INFO("Cannot open capture\n"); while(ros::ok()){ bool ret = cap.read(src); if(!ret){ ROS_INFO("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, mask1, mask2, result; cvtColor(img, img_hsv, COLOR_BGR2HSV); // range 1 (dark) int hue_m1 = 90; int hue_M1 = 110; int sat_m1 = 143; int sat_M1 = 255; int val_m1 = 100; int val_M1 = 255; Scalar lower1(hue_m1, sat_m1, val_m1); Scalar upper1(hue_M1, sat_M1, val_M1); inRange(img_hsv, lower1, upper1, mask1); // range 2 (light) int hue_m2 = 87; int hue_M2 = 102; int sat_m2 = 57; int sat_M2 = 160; int val_m2 = 227; int val_M2 = 255; Scalar lower2(hue_m2, sat_m2, val_m2); Scalar upper2(hue_M2, sat_M2, val_M2); inRange(img_hsv, lower2, upper2, mask2); bitwise_or(mask1, mask2, 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); // 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); // 放回原圖比較 // 標示 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("dp_optim_v2_image", dp_image_2); imshow("original image (comp)", original_image); } ``` ::: 這裡先隨便丟一個沒有用到 Pub 和 Sub 指令的程式,然後更改 CmakeList.txt: ```make= ### find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs sensor_msgs OpenCV REQUIRED ) ### include_directories( include ${catkin_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ) ### add_executable(E_cam src/E_cam.cpp) target_link_libraries(E_cam ${OpenCV_LIBS} ${catkin_LIBRARIES}) ``` 先測試 `catkin_make` ,沒問題後再開始寫有 Pub 和 Sub 圖片或影像的 ROS 程式。 ### <font color="pink">1-3. 編譯錯誤 -- 找不到 OpenCVconfig.cmake</font> 在虛擬機中我沒有遇過下面這個錯誤,但在派上有遇到。 ![](https://hackmd.io/_uploads/SkIwuVt6q.png) 這個錯誤原因是 CMake 找不到 OpenCV pcakage,所以我們需要透過 `set` 指令來主動設定路徑。 參考解決方法: <font color="yellow">1. 找到 OpenCVConfig.cmake 的路徑: `find / -name "OpenCVConfig.cmake"`。</font> $\;\;$ ![](https://hackmd.io/_uploads/rycGPNK69.png) :::info 黃色底線的就是該檔案絕對路徑,下一步需要輸入它的資料夾位置。 每個人的位置不一定一樣,我的是在 opencv4 這個資料夾下。 ::: <font color="yellow">2. 在 CmakeList 中設定路徑:`set(OpenCV_DIR /usr/lib/aarch64-linux-gnu/cmake/opencv4)`</font> $\;\;\;\;$ ![](https://hackmd.io/_uploads/H1hW9Vt69.png) ### <font color="pink">1-4. Publisher x OpenCV</font> ``` cpp= #include <ros/ros.h> #include <iostream> #include <opencv2/opencv.hpp> #include <image_transport/image_transport.h> #include <sensor_msgs/image_encodings.h> #include <cv_bridge/cv_bridge.h> using namespace std; using namespace cv; // global variables image_transport::Publisher pub; sensor_msgs::ImagePtr msg; // 更完整的過濾字型 Image -- OptimV2 int main(int argc, char** argv){ ros::init(argc, argv, "E_cam_pub"); ros::NodeHandle nh; image_transport::ImageTransport it(nh); pub = it.advertise("E_cam", 1); Mat src; VideoCapture cap(1); /// 需要調整的變數 /// double epsilon = 3; // DP Algorithm 的參數 int minContour = 5; // 邊數小於 minContour 會被遮罩 int maxContour = 20; // 邊數大於 maxContour 會遮罩 double lowerBondArea = 45; // 面積低於 lowerBondArea 的輪廓會被遮罩 /// if(!cap.isOpened()) ROS_ERROR_STREAM("Cannot open capture\n"); while(ros::ok()){ bool ret = cap.read(src); if(!ret){ ROS_ERROR_STREAM("Cant receive frame\n"); break; } msg = cv_bridge::CvImage(std_msgs::Header(), "bgr8", src).toImageMsg(); pub.publish(msg); if(waitKey(1) == 'q') break; } cap.release(); return 0; } ``` ### <font color="pink">1-5. Subscriber x OpenCV</font> ```cpp= #include <ros/ros.h> #include <iostream> #include <opencv2/opencv.hpp> #include <image_transport/image_transport.h> #include <sensor_msgs/image_encodings.h> #include <cv_bridge/cv_bridge.h> 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); // callback function void image_callback(const sensor_msgs::ImageConstPtr& msg); // ROS global variables image_transport::Subscriber sub; sensor_msgs::ImagePtr img; /// 需要調整的變數 /// double epsilon = 3; // DP Algorithm 的參數 int minContour = 5; // 邊數小於 minContour 會被遮罩 int maxContour = 20; // 邊數大於 maxContour 會遮罩 double lowerBondArea = 45; // 面積低於 lowerBondArea 的輪廓會被遮罩 /// // 更完整的過濾字型 Image -- OptimV2 int main(int argc, char** argv){ ros::init(argc, argv, "E_cam_sub"); ros::NodeHandle nh; image_transport::ImageTransport it(nh); sub = it.subscribe("E_cam", 1, image_callback); ROS_INFO("banana"); ros::spin(); cv::destroyWindow("original image (comp)"); return 0; } void image_callback(const sensor_msgs::ImageConstPtr& msg){ cv_bridge::CvImagePtr cv_ptr; Mat src = cv_ptr->image; Mat original_image = src.clone(); ROS_INFO("banana"); src = filt_letter(src); filt_contour(original_image, src, epsilon, minContour, maxContour, lowerBondArea); ROS_INFO("candy"); } Mat filt_letter(Mat img){ Mat img_hsv, mask, mask1, mask2, result; cvtColor(img, img_hsv, COLOR_BGR2HSV); // range 1 (dark) int hue_m1 = 90; int hue_M1 = 110; int sat_m1 = 143; int sat_M1 = 255; int val_m1 = 100; int val_M1 = 255; Scalar lower1(hue_m1, sat_m1, val_m1); Scalar upper1(hue_M1, sat_M1, val_M1); inRange(img_hsv, lower1, upper1, mask1); // range 2 (light) int hue_m2 = 87; int hue_M2 = 102; int sat_m2 = 57; int sat_M2 = 160; int val_m2 = 227; int val_M2 = 255; Scalar lower2(hue_m2, sat_m2, val_m2); Scalar upper2(hue_M2, sat_M2, val_M2); inRange(img_hsv, lower2, upper2, mask2); bitwise_or(mask1, mask2, 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); // 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); // 放回原圖比較 // 標示 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("dp_optim_v2_image", dp_image_2); imshow("original image (comp)", original_image); } ``` ### <font color="pink">1-6. OpenCV x Launch </font> ```launch= <launch> <node pkg="testing" type="E_cam_pub" name="E_cam_pub" output="screen" required="true"/> <node pkg="testing" type="E_cam_sub" name="E_cam_sub" output="screen"/> </launch> ```