###### tags: `東京威力 TEL` {%hackmd DzaUykeiRfWOMkjFL_QkCQ %} # I. 影像辨識 OpenCV ## <font color="orange"> 02. 影像讀取與輸出</font> ### <font color="pink">2-1. 讀圖片</font> 我放了一個圖片 `smile.jpg` 在工作區中,建立 `readPicture.cpp` 檔案: ```c++= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ string path = "smile.jpg"; Mat img = imread(path); //讀路徑中的圖片到 Mat 型別的變數 img 中 imshow("smiling", img); // image show,引號是圖片名(可自取) cout << "Width : " << img.cols << endl; //顯示圖片寬度 cout << "Height: " << img.rows << endl; //顯示圖片高度 waitKey(0); //輸入任一個字元結束 } ``` 執行後應該會像下圖中一樣跳出「圖片的長寬」以及「圖片本身」。 ![](https://i.imgur.com/HSoiYif.png) 圖片太大的話可用 `resize()` 函式: > <font color="magenza">resize(原圖像, 輸出圖像, 目標圖像大小, X軸縮放比例, Y軸縮放比例, 插值方式)</font> > 插值沒有要用到可不寫上去,默認值為 `INTER_LINEAR`。 ```c++= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ string path = "smile.jpg"; Mat img = imread(path); resize(img, img, Size(img.cols/2, img.rows/2), 0, 0); imshow("image", img); cout << "Width : " << img.cols << endl; //顯示圖片寬度 cout << "Height: " << img.rows << endl; //顯示圖片高度 waitKey(0); return 0; } ``` ### <font color="pink">2-2. 讀影片</font> 處理影片時想成它是由很多個圖片連接而成,播放影片就像是快速的播放圖片。 我放了一個 `game.mp4` 的影片在工作區中,然後新建一個 `readVideo.cpp`: ```c++= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ string path = "game.mp4"; VideoCapture cap(path); // VideoCapture類別,建構子參數是影片路徑 Mat img; while(true){ cap.read(img); // 將影片分割出的圖片丟到 img 變數裡 imshow("image", img); // 展示圖片(一直變換圖片就會像影片) waitKey(10); // 每 10 毫秒換一張 } return 0; } ``` 但播放完影片時,因為 img 抓不到圖片了,會跳出 Error訊息: ![](https://i.imgur.com/Vj2tOLq.png) 所以可以用 `VideoCapture` 類別的 `empty()` 函式特別處理這個 exception;另外,降低 `waitKey()` 中的參數讓影片看起來更順暢。 > `VideoCapture.empty()` 讀不到圖片時回傳 `true`,有讀到為 `false`。 ```c++= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ string path = "game.mp4"; VideoCapture cap(path); Mat img; while(true){ cap.read(img); if(img.empty()){ // handle exception cout << "Video End\n"; break; } imshow("image", img); waitKey(1); // 讓影片更順暢(每1000秒比原影片慢1秒) } return EXIT_SUCCESS; } ``` ### <font color="pink">2-3. 讀 Webcam</font> 電腦若有內置鏡頭,其編號為0;外接鏡頭則為1,以此類推。 電腦如果沒有鏡頭,則外接鏡頭編號為0,以此類推。 ```cpp= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ VideoCapture cap(0); // 鏡頭編號依序從 012... Mat img; if(!cap.isOpened()){ // 確認有連線到該編號的鏡頭 cout << "Cannot open capture\n"; } while(true){ bool ret = cap.read(img); if(!ret){ // 確認有影像傳輸 cout<<"cant receive frame\n"; break; } imshow("Image", img); if(waitKey(1) == 27) break; // 27 是 Escape } return 0; } ``` ### <font color="pink">2-4. 彩色轉灰階</font> > <font color="magenza">cvtColor(輸入圖片, 輸出圖片, 轉換方法)</font> 將原本彩色圖片轉成灰階圖片: ```cpp= #include <opencv2/opencv.hpp> #include <iostream> using namespace cv; int main(){ std::string path = "smile.jpg"; Mat img = imread(path); Mat gray_img; cvtColor(img, gray_img, COLOR_BGR2GRAY); // 參數(輸入圖片, 輸出圖片, 轉換方式) imshow("Gray Image", gray_img); waitKey(2000); return 0; } ``` ### <font color="pink">2-5. 高斯模糊</font> 效果如下: ![](https://i.imgur.com/gWzd2KQ.png =85%x) ![](https://i.imgur.com/SEdPSTz.png =85%x) 高斯模糊的內建函式為 `GaussianBlur()`。 > <font color="magenza">GaussianBlur(輸入圖片, 輸出圖片, kernal大小, anchor大小, 邊界類型)</font> :::spoiler 我不是很清楚最後兩個參數的詳細原理和使用時機,基本上都寫0。 ::: 關於高斯模糊所用到的「Kernal(捲積核)」和「高斯分布」可以看[這部影片](https://www.youtube.com/watch?v=C_zFhWdM4ic&t=324&ab_channel=Computerphile)。 ```cpp= #include <opencv2/opencv.hpp> #include <iostream> using namespace cv; int main(){ std::string path = "smile.jpg"; Mat img = imread(path); Mat blur_img; GaussianBlur(img, blur_img, Size(11,11), 0, 0); imshow("Original Image", img); imshow("Blur Image", blur_img); waitKey(2000); return 0; } ``` 簡單的結論是,<font color = "magenza">Kernal必須是奇數</font>,因為這樣才有中心點。 高斯模糊的原理大致上是以每個像素為中心對周圍 N\*N 個點依照常態分布去做權重分配。 所以<font color = "magenza">越大的Kernal就會使圖片越模糊</font>,因為單個像素吃了越多越遠的像素值。 ### <font color="pink">2-6. 邊緣偵測</font> 原圖: ![](https://i.imgur.com/gWzd2KQ.png =85%x) Canny 圖: ![](https://i.imgur.com/joXego6.png =85%x) 關於 Canny 實作原理可參考[這裡](https://medium.com/@pomelyu5199/canny-edge-detector-%E5%AF%A6%E4%BD%9C-opencv-f7d1a0a57d19),邊緣偵測的步驟如下: 1. 彩色轉灰階 2. 高斯模糊,以去除雜訊 3. 用 Sobel Operator 找像素梯度值和梯度方向 5. 非最大值抑制尋找邊緣(同行列中只保留最大值者) 6. 根據閥值(threshold)尋找 strong edge 和 weak edge。 ><font color = "magenza">void Canny(原圖片, 輸出圖片, 低門檻, 高門檻, 光圈大小, 平均計算方式)</font> >光圈(Aperture)大小指的是 Sobel operator 的 kernal size。 ```c++= #include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; int main(){ string path = "smile.jpg"; Mat img = imread(path); Mat canny_img; Canny(img, canny_img, 120, 180); imshow("Original image", img); imshow("Canny image", canny_img); waitKey(12000); return 0; } ``` ### <font color="pink">2-7. 膨脹</font> Canny 圖: ![](https://i.imgur.com/joXego6.png =85%x) Canny + Dilate 圖: ![](https://i.imgur.com/8aUkubb.png =85%x) ><font color = "magenza">dilate(const Mat &src, Mat &dst, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)</font> src:輸入圖。 dst:輸出圖,和輸入圖尺寸、型態相同。 kernel:結構元素,如果kernel=Mat()則為預設的3×3矩形,越大膨脹效果越明顯。 anchor:原點位置,預設為結構元素的中央。 iterations:執行次數,預設為1次,執行越多次膨脹效果越明顯。 ```c++= #include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; int main(){ string path = "smile.jpg"; Mat img = imread(path); Mat img_canny; Mat img_dilate; Canny(img, img_canny, 120, 180); dilate(img_canny, img_dilate, Mat()); imshow("canny image", img_canny); imshow("dilate image", img_dilate); waitKey(0); return 0; } ``` ### <font color="pink">2-8. 侵蝕</font> Canny + Dilate 圖: ![](https://i.imgur.com/8aUkubb.png =85%x) Canny + Dilate + Erode 圖: ![](https://i.imgur.com/bfHfSsR.png =85%x) ><font color = "magenza">erode(const Mat &src, Mat &dst, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)</font> src:輸入圖。 dst:輸出圖,和輸入圖尺寸、型態相同。 kernel:結構元素,如果kernel=Mat()則為預設的3×3矩形,越大侵蝕效果越明顯。 anchor:原點位置,預設為結構元素的中央。 iterations:執行次數,預設為1次,執行越多次侵蝕效果越明顯。