###### 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次,執行越多次侵蝕效果越明顯。