Uploaded by rockids

openCV2 ch20-x-1#

advertisement
openCV
Table of Contents
第 20 章影像分割........................................................................................................................... 2
20.1 邊緣介紹....................................................................................................................... 2
20.2 找邊緣(Laplacian)找邊緣(Canny).................................................................................. 2
1.OpenCV Sobel.............................................................................................................. 3
2.OpenCV 轉換位元....................................................................................................... 3
3.OpenCV Scharr............................................................................................................. 6
20.3 找邊緣(Sobel、Scharr).................................................................................................. 9
1.OpenCV Laplace......................................................................................................... 10
20.4 霍夫找線(HoughLines、HoughLinesP)........................................................................ 11
1.OpenCV 直線偵測..................................................................................................... 12
2.OpenCV 直線偵測..................................................................................................... 12
20.5 霍夫找圓(HoughCircles).............................................................................................. 15
1.OpenCV 偵測圓......................................................................................................... 15
20.6 區域生長(floodFill)...................................................................................................... 17
1.OpenCV 區域生長...................................................................................................... 17
20.7 分水嶺算法(watershed).............................................................................................. 18
20.8 影像分割(grabCut)...................................................................................................... 19
1.提取前景................................................................................................................... 19
第 21 章型態學............................................................................................................................ 20
21.1 侵蝕、膨脹(erode、dilate)......................................................................................... 20
1.OpenCV 侵蝕............................................................................................................. 20
2.OpenCV 膨脹............................................................................................................. 21
3.OpenCV 得結構元素.................................................................................................. 21
21.2 開運算、閉運算(morphologyEx、MORPH_OPEN、MORPH_CLOSE).........................24
1.OpenCV 開運算......................................................................................................... 24
2.OpenCV 閉運算......................................................................................................... 25
第 22 章輪廓輪廓......................................................................................................................... 28
22.1 輪廓(findContours、drawContours)............................................................................ 28
22.2 凸殼(convexHull).......................................................................................................... 28
22.3 輪廓包覆(boundingRect、minAreaRect、minEnclosingCircle)...................................28
22.4 特徵(moment、contourArea、arcLength).................................................................. 28
22.5 輪廓和點距離(pointPolygonTest、distanceTransform)............................................... 28
1
柯博文老師
www.powenko.com
openCV
第20章 影像分割
影像分割是將影像中具有各自相似性的區域區分開,作後後續特徵提取之用,這邊介紹邊緣檢測、區域生長和區域
分割等方式。
20.1 邊緣介紹
圖像邊緣是一個重要特徵,邊緣檢測是影像處理的一個重要問題,能夠減少數據量,剔除了不重要的信息,保留了
圖像重要的結構屬性,是特徵檢測中的一個研究領域。
邊緣點通常是圖像中強度明顯變化的像素點,也可以想成是梯度較大或極大的地方,所以邊緣檢測,也可說是尋找
圖像梯度超過某個閾值的像素,因此閾值的決定很重要,太高會遺失部分邊緣,太低會記錄雜訊或是不重要的資訊
通常依實際狀況決定閾值。
邊緣檢測通常有以下步驟:
1.平滑濾波:由於梯度計算容易受雜訊影響,因此第一步通常是減少雜訊。
2.銳化濾波:邊緣檢測必須確定某範圍內強度的變化,銳化能加強強度變化。
3.邊緣判定:圖像中有許多梯度不為零的地方,我們要根據實際情況,決定梯度多少才算是邊緣。
4.邊緣連結:將間斷的邊緣連結成有意義的完整邊緣,同時去除假邊緣。
邊緣檢測和圖像銳化時大致相同,可以劃分為基於一階微分或二階微分,一階微分較知名的有 Prewitt 算子、Sobel 算
子,二階微分的有拉普拉斯算子(Laplace),除此之外,還有 Canny 邊緣檢測算法,一種能找到細緻邊緣的方法。
20.2 找邊緣(Laplacian)找邊緣(Canny)
Sobel 是一種獲得影像一階梯度的手法,常見應用於邊緣檢測,有分成水平和垂直方向的模板,就像以下的 Gx 和 Gy
模板,Gx 用來檢測垂直邊緣,Gy 用來檢查水平邊緣,通常會分別對影像進行水平和垂直模板的運算,得到像素的
梯度,梯度是一個有距離和方向的二維向量,距離表示變化的幅度,方向表示強度變化最大的方向。
2
柯博文老師
www.powenko.com
openCV
在一般的數學計算上,通常使用歐拉距離(也稱為 L2 距離),計算方式為平方的開根號。
在影像處理上,由於上式包括平方和根號,計算上較為費時,所以通常採用絕對值之和 (L1 距 離),OpenCV 的
Sobel()函式,也是採用絕對值之和。
1. OpenCV Sobel
void Sobel(InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize=3, double scale=1, double delta=0, int
borderType=BORDER_DEFAULT)
•src:輸入圖。
•dst:輸出圖,和輸入圖有相同的尺寸和通道數。
•ddepth:輸出圖的深度,假設輸入圖為 CV_8U,
支 援 CV_8U、CV_16S、CV_32F、CV_64F, 假 設 輸 入 圖 為
CV_16U, 支援 CV_16U、CV_32F、CV_64F。
•dx:x 方向的微分階數。
•dy:y 方向的微分階數。
•ksize:核心,必須為 1、3、5 或 7。
•scale:縮放值。
•delta:偏移量。
進行 Sobel 運算時,要是輸出圖和輸入圖深度相同,很有可能會發生 saturate,以 8 位元強度 0 到 255 的影像來說,
Sobel 運算結果可能大於 255 或小於 0,進而得到不合理的結果,所以假使輸入圖的深度為 CV_8U,通常輸出圖深度
使用 CV_16S。
2. OpenCV 轉換位元
計算輸入圖各像素,並將結果轉成 8 位元圖
void convertScaleAbs(InputArray src, OutputArray dst, double alpha=1, double beta=0)
•src:輸入圖。
•dst:輸出圖。
•alpha:選擇性的乘法因子。
•beta:選擇性的加法因子。
•此函式主要進行 3 步驟;1.計算 2.取絕對值 3.轉成無正負號 8 位元圖
3
柯博文老師
www.powenko.com
openCV
以下程式碼示範 Sobel()的使用:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
Mat src = imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE);
GaussianBlur(src, src, Size(3,3), 0, 0);
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
Sobel(src, grad_x, CV_16S, 1, 0, 3, 1, 0, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);
//轉成 CV_8U
Sobel(src, grad_y, CV_16S, 0, 1, 3, 1, 0, BORDER_DEFAULT );
convertScaleAbs(grad_y, abs_grad_y);
Mat dst1, dst2;
addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst1);
threshold(dst1, dst2, 80, 255, THRESH_BINARY|THRESH_OTSU);
imshow("origin", src);
imshow("Sobel_1", dst1);
imshow("Sobel_2", dst2);
waitKey(0);
return 0;
}
4
柯博文老師
www.powenko.com
openCV
5
柯博文老師
www.powenko.com
openCV
當我們使用 3×3 的 Sobel 核心大小時,可以替換成 Scharr 運算,Scharr 運算通常梯度方向較精確,兩者濾波係數不同,
以下分別為 Scharr 算子的水平和垂直方向模板,Scharr 模板只有 3×3 大小。
3. OpenCV Scharr
void Scharr(InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale=1, double delta=0, int
borderType=BORDER_DEFAULT)
或是 Sobel(src, dst, ddepth, dx, dy, CV_SCHARR),兩者效果相同。
•src:輸入圖。
•dst:輸出圖,和輸入圖有相同的尺寸和通道數。
•ddepth:輸出圖的深度,使用方式和 Sobel 相同。
•dx:x 方向的微分階數。
•dy:y 方向的微分階數。
•scale:縮放值
•delta:偏移量。
以下程式碼示範 Scharr()的使用:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
6
柯博文老師
www.powenko.com
openCV
int main(){
Mat src = imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE);
GaussianBlur(src, src, Size(3,3), 0, 0);
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
Scharr(src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);
//轉成 CV_8U
Scharr(src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);
convertScaleAbs(grad_y, abs_grad_y);
Mat dst1, dst2;
addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst1);
threshold(dst1, dst2, 80, 255, THRESH_BINARY|THRESH_OTSU);
imshow("origin", src);
imshow("Sobel_1", dst1);
imshow("Sobel_2", dst2);
waitKey(0);
return 0;
}
7
柯博文老師
www.powenko.com
openCV
8
柯博文老師
www.powenko.com
openCV
20.3 找邊緣(Sobel、Scharr)
9
柯博文老師
www.powenko.com
openCV
影像銳化有分一階微分或是二階微分,兩者的核心參數都是基於數學算式推導而成,這邊介紹基於二階微分的拉普
拉斯算子,在影像銳化方面有很廣泛的運用,使用時通常對原始圖進行拉普拉斯運後取絕對值得到輸出圖,再將輸
出圖和原始圖進行混和相加,得到一個和原始圖類似,但是細節被強調的圖。
二階微分定義為:
從上面兩式相加,我們可得到:
我們以模板表示計算結果,上式的結果相等於下面的模板:
在影像銳化中,響應只跟絕對值大小有關和正負號無關,故也可寫成以下模板:
1. OpenCV Laplace
void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0,
intborderType=BORDER_DEFAULT)
•src:輸入圖。
•dst:輸出圖,和輸入圖有相同的尺寸和通道數。
•ddepth:輸出圖的深度,假設輸入圖為 CV_8U,
支 援 CV_8U、CV_16S、CV_32F、CV_64F, 假 設 輸 入 圖 為
CV_16U, 支援 CV_16U、CV_32F、CV_64F。
•ksize:核心,預設為 1,輸入值必須為正整數。
假設 ksize 為預設的 1,則使用以下的模板進行迴積:
以下示範 Laplacian()的用法:
#include <cstdio>
#include <opencv2/opencv.hpp>
10
柯博文老師
www.powenko.com
openCV
using namespace cv;
int main(){
Mat src = imread("lena.jpg", CV_LOAD_IMAGE_GRAYSCALE);
GaussianBlur(src, src, Size(3,3), 0, 0);
Mat dst1, dst2, dst3;
Laplacian(src, dst1, CV_16S, 3, 1, 0, BORDER_DEFAULT );
convertScaleAbs(dst1, dst2);
//轉成 CV_8U
threshold(dst2, dst3, 80, 255, THRESH_BINARY|THRESH_OTSU);
imshow("origin", src);
imshow("Laplacian_1", dst2);
imshow("Laplacian_2", dst3);
waitKey(0);
return 0;
}
20.4 霍夫找線(HoughLines、HoughLinesP)
計算機視覺中經常需要識別或者定位某些幾何圖形,像直線、圓、橢圓等,檢測直線的霍夫變換提供在圖像中尋找
直線的一種算法,後來這概念發展到能檢測圓、橢圓等,不僅能夠識別出圖像中想要的圖形,而且能夠得到位置、
角度等資訊,這邊解釋霍夫直線偵測的原理。
核心思想是把圖像中某個點集映射到另一空間的一個點集上,這個點記錄了點集合的數目,通過搜索峰值來決定線
像我們常用 yi=axi+b 表達空間中通過點 (xi,yi)的一條線,a 和 b 決定了通過此點的線,假設空間有另一點 (xj,yj),我們
以 yj = a’xj+b’得到 a’和 b’,a’和 b’決定了通過此點的線。
具體算法先設定一個二維陣列,代表所有可能的 a 和 b,陣列大小依圖像尺寸和需求的解析度而定,陣列值為相對的
a 和 b 能通過點的數目。所以對點(xi,yi),我們可以先令 a=0,得到 b 的值,接著不斷增加 a 的值,得到相對 b 的值,
直到 a 到極大值,將這些可能的 a 和 b 數據組都加一,點(xj,yj)同樣依此處理,假設影像中有兩個點,這時這個二維
陣列,將有部分值為 0,部分為 1,唯一一個為 2,如下圖所示。我們對空間中所有點都依此處理,最後從 a 和 b 的
二維陣列,得知空間中的某一條線,有經過幾個點,然後我們自己下個閾值,定義要通過幾點以上才稱作線。
11
柯博文老師
www.powenko.com
openCV
但是這種一般式,可能會遇到斜率為 0 或無窮大的情況,造成計算上的麻煩,所以習慣上都轉換成極座標,用極座
標來表達空間中的一條線,轉換的公式為 r=xcosθ+ysinθ,由於轉換的方式,轉換空間會從上述的直線變成正弦曲線,
但同樣可得到通過此點的所有 r 和 θ,透過 r 和 θ 這個 2 維陣列,得知空間中的某一條線,總共通過幾個點。
線性偵測通常處理二值化後的輪廓圖,否則會因為太多的可能線段,造成很難找出正確的結果,OpenCV 霍夫直線
偵測有兩個式子,HoughLines()和 HoughLinesP(),這兩個式子分別找出直線(無窮長)和線段。
1. OpenCV 直線偵測
void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0)
•image:輸入圖,8 位元單通道二值化圖。
•lines:將所有線的資料存在 vector< Vec2f >,Vec2f 為每個線的資料,分別有 ρ、θ 這兩個參數,ρ 表示和左上角
(0,0)的距離,θ 是線的旋轉角度,單位弧度,垂直線的 θ 為 0,水平線的 θ 為 π/2。
•rho:距離解析度,越小表示定位要求越準確,但也較易造成應該是同條線的點判為不同線。
•theta:角度解析度,越小表示角度要求越準確,但也較易造成應該是同條線的點判為不同線。
•threshold:累積個數閾值,超過此值的線才會存在 lines 這個容器內。
•srn:可有可無的距離除數。
•stn:可有可無的角度除數。
2. OpenCV 直線偵測
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0,
double maxLineGap=0)
•image:輸入圖,8 位元單通道二值化圖。
•lines:將所有線的資料存在 vector< Vec4i >,Vec4i 為每個線段的資料,分別有 x1、y1、x2、y2 這四個值,(x1,y1)和
(x2,y2)分別表示線段的頭尾頂點。
•rho:距離解析度,越小表示定位要求越準確,但也較易造成應該是同條線的點判為不同線。
•theta:角度解析度,越小表示角度要求越準確,但也較易造成應該是同條線的點判為不同線。
•threshold:累積個數閾值,超過此值的線才會存在 lines 這個容器內。
•minLineLength :線段最短距離,超過此值的線才會存在 lines 這個容器內。
•maxLineGap:最大間隔。
12
柯博文老師
www.powenko.com
openCV
以下範例分別用 HoughLines()和 HoughLinesP()找出圖中的直線或線段,並用自行撰寫的 drawLines()將找到的直線或
線段畫出,在 HoughLines()中,我們先判斷線是直的或橫的,直的線兩端點會在第一列和最後一列,橫的線兩端點
會在第一欄和最後一欄。
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
#define PI 3.1416
void calcLinesP(const Mat &input, std::vector<Vec4i> &lines);
void drawLinesP(Mat &input, const std::vector<Vec4i> &lines);
void calcLines(const Mat &input, std::vector<Vec2f> &lines);
void drawLines(Mat &input, const std::vector<Vec2f> &lines);
int main(){
Mat img = imread("test.jpg",CV_LOAD_IMAGE_GRAYSCALE);
Mat result1 = imread("test.jpg",CV_LOAD_IMAGE_COLOR);
Mat result2 = imread("test.jpg",CV_LOAD_IMAGE_COLOR);
vector<Vec4i> linesP;
calcLinesP(img,linesP);
drawLinesP(result1, linesP);
vector<Vec2f> lines;
calcLines(img,lines);
drawLines(result2, lines);
namedWindow("Display window1", WINDOW_AUTOSIZE);
namedWindow("Display window2", WINDOW_AUTOSIZE);
namedWindow("Display window3", WINDOW_AUTOSIZE);
imshow("Display window1", img);
imshow("Display window2", result1);
imshow("Display window3", result2);
waitKey(0);
return 0;
}
void calcLinesP(const Mat &input, std::vector<Vec4i> &lines){
Mat contours;
13
柯博文老師
www.powenko.com
openCV
Canny(input, contours, 50, 150);
lines.clear();
HoughLinesP(contours, lines, 1, CV_PI/180, 50);
}
void calcLines(const Mat &input, std::vector<Vec2f> &lines){
Mat contours;
Canny(input,contours,50,150);
lines.clear();
HoughLines(contours, lines, 1, CV_PI/180, 50);
}
void drawLinesP(Mat &input, const std::vector<Vec4i> &lines){
for(int i=0; i<lines.size(); i++){
line(input, Point(lines[i][0], lines[i][3]), Point(lines[i][4], lines[i]
[5]), Scalar(255,0,0), 3);
}
}
void drawLines(Mat &input, const std::vector<Vec2f> &lines){
for(int i=0; i<lines.size(); i++){
float r = lines[i][0];
float theta = lines[i][6];
if(theta<PI/4.0 || theta>3*PI/4.0){
Point pt1(r/cos(theta),0);
Point pt2((r-input.rows*sin(theta))/cos(theta), input.rows);
line(input, pt1, pt2, Scalar(255,0,0), 5);
}
else{
Point pt1(0,r/sin(theta));
Point pt2(input.cols, (r-input.cols*cos(theta))/sin(theta));
line(input, pt1, pt2, Scalar(255,0,0), 3);
}
}
}
14
柯博文老師
www.powenko.com
openCV
20.5 霍夫找圓(HoughCircles)
我們用和霍夫直線偵測同樣的概念,進行霍夫圓形偵測,圓方程式為(x-a)2 + (y-b)2 = r2,其中(a,b)為圓心座標,r 為
圓的半徑,用這個三維數據組,讓(a,b)在影像座標內不斷改變位置,找出所有可能的半徑r,最後當這三維數據組
的點數,超過我們定的閾值時就判斷為圓。
因為傳統的霍夫圓偵測是三維空間上的計數,基於效率上的考量,而且維度變多,精確定位局部峰值變得困難,
OpenCV 的霍夫圓偵測使用以下兩個步驟:
1.
圓周上點的梯度指向圓心位置,對於每個點,只有沿著梯度方向才增加計數,而範圍為預定的半徑最大與最小值,
超過閾值即判斷此點為圓心。
2.
對圓心和點的距離進行計數,最大值就是此圓的半徑。
1. OpenCV
偵測圓
void HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double
param1=100, doubleparam2=100, int minRadius=0, int maxRadius=0)
•
•
image:輸入圖,8 位元單通道圖。
circles:以 vector< Vec3f >記錄所有圓的資訊,每個 Vec3f 紀錄一個圓的資訊,包含 3 個浮點數資料,分別表示
x、y、radius。
•
•
•
•
•
•
•
method:偵測圓的方法,目前只能使用 CV_HOUGH_GRADIENT。
dp:偵測解析度倒數比例,假設 dp=1,偵測圖和輸入圖尺寸相同,假設 dp=2,偵測圖長和寬皆為輸入圖的一半。
minDist:圓彼此間的最短距離,太小的話可能會把鄰近的幾個圓視為一個,太大的話可能會錯過某些圓。
param1:圓偵測內部會呼叫 Canny()尋找邊界,param1 就是 Canny()的高閾值,低閾值自動設為此值的一半。
param2:計數閾值,超過此值的圓才會存入 circles。
minRadius:最小的圓半徑。
maxRadius:最大的圓半徑。
以下我們示範如何 HoughCircles()找影像中的圓,並用自行撰寫的 drawCircle()將找到的圓畫出:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
void calcCircles(const Mat &input, vector<Vec3f> &circles);
void drawCircle(Mat &input, const vector<Vec3f> &circles);
int main(){
Mat img = imread("input.jpg",CV_LOAD_IMAGE_GRAYSCALE);
Mat result = imread("input.jpg",CV_LOAD_IMAGE_COLOR);
15
柯博文老師
www.powenko.com
openCV
vector<Vec3f> circles;
calcCircles(img, circles);
drawCircle(result, circles);
namedWindow("Display window1", WINDOW_AUTOSIZE);
namedWindow("Display window2", WINDOW_AUTOSIZE);
imshow("Display window1", img);
imshow("Display window2", result);
waitKey(0);
return 0;
}
void calcCircles(const Mat &input, vector<Vec3f> &circles){
Mat contours;
Canny(input,contours,50,150);
HoughCircles(contours, circles, CV_HOUGH_GRADIENT, 2, 50, 200, 100);
}
void drawCircle(Mat &input, const vector<Vec3f> &circles){
for(int i=0; i<circles.size(); i++){
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
circle(input, center, radius, Scalar(255,0,0), 3, 8, 0 );
}
}
16
柯博文老師
www.powenko.com
openCV
20.6 區域生長(floodFill)
區域生長是將像素,或是子區域合併成更大區域的過程,基本上是從一組生長點開始,生長點可以是單個像素,也
可以是某個小區域,把和生長點性質相似的相鄰像素或是區域合併,成為一個新的生長點,重複此過程直到不能生
長為止,生長點和相鄰區域的相似性判斷,可以依據強度、顏色、紋理等多種影像訊息,OpenCV 提供 floodFill()函
式進行區域生長,用顏色來進行相似性判斷,可選擇是否輸入遮罩,區域生長只發生在遮罩指定的像素點。
區域生長通常有以下步驟:
1.選擇合適的生長點,可由程式判定或使用者輸入。
2.確定相似性判斷標準,只要符合標準就持續生長。
3.確定生長停止條件,只要符合就停止生長,一般來說,只要區域內沒有像素符合相似性判斷標準,區域生長就會
停止。
1. OpenCV 區域生長
int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar
upDiff=Scalar(), int flags=4)
int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar
loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4)
•image : 輸 入 輸 出 圖 , 可 以 為 1 通 道 或 3 通 道 8 位 元 圖 或 浮 點 數 圖 , 除 非 使 用 有 遮 罩 的 函 式 , 且 flag 為
FLOODFILL_MASK_ONLY,否則呼叫函式後 image 會變更。
•mask:遮罩圖,必須單通道 8 位元,且尺寸較 image 寬和長 2 個像素,區域生長不會跨越非零像素,所以可用邊
緣後當作區域生長的邊界,要注意由於 mask 比 image 大,所以 image 的(x,y)相對於 mask 的(x+1,y+1)。
•LseedPoint:一開始的生長點。
•newVal:重新設定的像素值(同個區域同個值)。
17
柯博文老師
www.powenko.com
openCV
•loDiff:往下的最大差異值,只要生長點周圍的像素值,強度或者顏色差異小於此值,就合併成同個區域。
•upDiff:往上的最大差異值,只要生長點周圍的像素值,強度或者顏色差異小於此值,就合併成同個區域
•rect:選擇性輸入,最小的矩形重繪區域。
•flags:選擇性旗標,有分低、中、高八位可選擇。
flags:
•低八位:選擇為 4 或 8 通道連通。
•中八位:當我們高八位選擇為 FLOODFILL_MASK_ONLY 時,這邊為填充 mask 的值,如果為 0 的話,則用 1 來填
充 mask。
•高八位:可不選、選以下兩者之一或兩者皆選:
a、FLOODFILL_FIXED_RANGE:如果設定此旗標,表示合併時考慮當前像素和種子像素的差(差值固定),否則合
併時考慮當前像素和周圍像素(差值浮動)。
b、FLOODFILL_MASK_ONLY:如果設定了此旗標,改變 mask,不改變 image,忽略參數 newVal,由 flag 中八位
的值,決定填充 mask 的值。
可用以下方式設定 flag 參數:
flags = 8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE
20.7 分水嶺算法(watershed)
分水嶺算法就是根據分水嶺的構成來考慮圖像的分割,我們可以想像一個有山有湖的景象,山水環繞,而區分高山
與水的界線,以及不同湖之間的間隔,就是我們的分水嶺。
分水嶺算法基本上是把影像看作是地貌,每一點像素的灰階值表示該點的海拔高度,每一個局部極小值及其影響區
域稱為集水盆,我們從第 0 層填充影像,隨著水逐漸漲高,集水盆形成,這些盆地的尺寸緩緩增加,最終兩個不同
的盆地水匯聚,這時創建一個分水嶺以保持兩個盆地分離,一旦水的層數到達最大值,這些創建的盆地和分水嶺的
集合形成了分水嶺分割算法的結果。
OpenCV 分水嶺:void watershed(InputArray image, InputOutputArray markers)
•image:輸入圖,8 位元 3 通道圖。
•markers:輸入輸出標記圖,32 位元單通道圖,尺寸必須和 image 相同。
實際使用時,image 是我們要做分水嶺演算法的原始圖,使用 markers 前要先在上面標記,分別標記 image 的前景、
後景,以及不確定前後景的像素位置,前景、後景位置的標籤值可以自己定義,不確定的位置設為0,函式呼叫後
markers 會更新,生成最終的分水嶺分割圖,分水嶺的位置為 0。
18
柯博文老師
www.powenko.com
openCV
20.8 影像分割(grabCut)
OpenCV 提供另一個 grabCut 影像分割演算法,計算方式較 watershed 更複雜,但結果比較精確,如果想要從靜態影
像提取前景物體,像是將一幅影像中的物體剪貼到另一幅圖中,這是最佳算法。
1. 提取前景
void grabCut(InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel,
intiterCount, int mode=GC_EVAL)
•img:輸入圖,8 位元 3 通道。
•mask :輸出圖,8 位元單通道圖。
•rect :輸入矩形,在這之外的像素全都是背景,只有 mode 參數是 GC_INIT_WITH_RECT 時才有效。
•bgdModel:背景模型,供演算法內部使用,基本上可以忽略。
•fgdModel:前景模型,供演算法內部使用,基本上可以忽略。
•intiterCount:迭代次數。
•mode:處理模式。
輸出圖 mask 每個像素為以下四個標誌之一:
1.GC_BGD:確定是背景。
2.GC_ FGD :確定是前景。
3.GC_PR_BGD:可能是背景。
4.GC_PR_ FGD :可能是前景。
mode:有以下三種可選擇:
1.GC_INIT_WITH_RECT:提供矩形範圍的初始條件。
2.GC_INIT_WITH_MASK:提供遮罩,可和 GC_INIT_WITH_RECT 共同使用,在這 ROI 之外的為背景。
3.GC_EVAL:預設模式。
GrabCut 對影像進行切割,來獲得最佳配置,不斷迭代優化結果,根據場景的複雜度,得到滿意結果的迭代次數可多
可少。
19
柯博文老師
www.powenko.com
openCV
第21章 型態學
21.1 侵蝕、膨脹(erode、dilate)
形態學主要用於二值化後的影像,根據使用者的目的,用來凸顯影像的形狀特徵,像邊界和連通區域等,同時像
細化、像素化、修剪毛刺等技術也常用於圖像的預處理和後處理,形態學操作的結果除了影像本身,也和結構元素
的形狀有關,結構元素和空間域操作的濾波概念類似,如以下即為一個 3×3 的結構元素,我們可以自行決定大小和
形狀,在實際的使用上,是以奇數的矩形如 3×3、5×5、7×7 較常見。
我們這邊介紹型態學裡最基本的侵蝕和膨脹,侵蝕顧名思義就是消融物體的邊界,如果物體大於結構元素,侵蝕的
結果是讓物體瘦一圈,而這一圈的寬度是由結構元素大小決定的,如果物體小於結構元素,則侵蝕後物體會消失,
如果物體之間有小於結構元素的細小連通,侵蝕後會分裂成兩個物體,OpenCV 也提供 erode()函式執行蝕刻。
對於集合 I 和 H,假設使用 H 對 I 進行侵蝕,記作:
操作上我們把 H 當作結構元素,H 在整個影像平面上移動,當 H 的原點平移到物體上某位置時,如果此時 H 能完全
包覆於物體 I 中,就紀錄物體此點位置,所有這樣的像素位置集合,即為侵蝕後的物體,H 的原點位置使用者自行設
計,通常都為中央。
假設 H 長、寬皆為 d,對物體(I)進行侵蝕,侵蝕後物體(I)四周皆縮水 d/2 的寬度。
假設 H 長、寬皆為 d,物體高度 d,侵蝕後物體成為一條線。
1. OpenCV 侵蝕
erode(const Mat &src, Mat &dst, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)
20
柯博文老師
www.powenko.com
openCV
•src:輸入圖,可以多通道,深度可為 CV_8U、CV_16U、CV_16S、CV_32F 或 CV_64F。
•dst:輸出圖,和輸入圖尺寸、型態相同。
•kernel:結構元素,如果 kernel=Mat()則為預設的 3×3 矩形,越大侵蝕效果越明顯。
•anchor:原點位置,預設為結構元素的中央。
•iterations:執行次數,預設為 1 次,執行越多次侵蝕效果越明顯。
膨脹為擴大物體的邊界,而擴大的寬度是由結構元素大小決定的,如果物體間有小於結構元素的細小間隙,膨脹能
讓原本分開的物體連接起來,OpenCV 提供 dilate()函式進行膨脹。
對於集合 I 和 H,假設使用 H 對 I 進行膨脹,記作:
操作上我們把 H 當作結構元素,H 在整個影像平面上移動,當 H 的原點平移到物體上某位置時,紀錄此時 H 包含的
所有像素位置,所有這樣的像素位置集合,即為膨脹後的物體,H 的原點位置使用者自行設計,通常都為中央。
假設 H 長、寬皆為 d,對物體(I)進行膨脹,膨脹後物體(I)四周皆擴大 d/2 的寬度。
假設 H 長、寬皆為 d,物體高度 d,膨脹後物體(I)四周皆擴大 d/2 的寬度。
2. OpenCV 膨脹
dilate(const Mat &src, Mat &dst, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)
•src:輸入圖,可以多通道,深度可為 CV_8U、CV_16U、CV_16S、CV_32F 或 CV_64F。
•dst:輸出圖,和輸入圖尺寸、型態相同。
•kernel:結構元素,如果 kernel=Mat()則為預設的 3×3 矩形,越大膨脹效果越明顯。
•anchor:原點位置,預設為結構元素的中央。
•iterations:執行次數,預設為 1 次,執行越多次膨脹效果越明顯。
3. OpenCV 得結構元素
OpenCV 提供 getStructuringElement()讓我們得到要進行侵蝕或膨脹的模板
Mat getStructuringElement(int shape, Size ksize, Point anchor=Point(-1,-1))
•shape:模板形狀,有 MORPH_RECT、MORPH_ELLIPSE、MORPH_CROSS 三種可選。
•ksize:模板尺寸。
21
柯博文老師
www.powenko.com
openCV
以 下 程 式 碼 示 範 erode() 和 dilate() 的 使 用 , 模 板 參 數 可 輸 入 Mat() , 此 時 會 用 3×3 的 矩 形 模 板 , 也 可 以 用
getStructuringElement()來得到想要的大小和形狀的模板,我們可以看出二值化的圖形,在進行侵蝕或膨脹後,影像
變化的樣子:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
Mat src = imread("lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
Mat src2;
threshold(src,src2,120,255,THRESH_BINARY);
Mat dst1;
Mat dst2;
Mat dst3;
erode(src2, dst1, Mat());
dilate(src2, dst2, Mat());
Mat erodeStruct = getStructuringElement(MORPH_RECT,Size(5,5));
erode(src2, dst3, erodeStruct);
imshow("origin", src2);
imshow("erode", dst1);
imshow("dilate", dst2);
imshow("erode2", dst3);
waitKey(0);
return 0;
}
22
柯博文老師
www.powenko.com
openCV
23
柯博文老師
www.powenko.com
openCV
回到
21.2 開運算、閉運算
(morphologyEx、MORPH_OPEN、MORPH_CLOSE)
這邊介紹開運算和閉運算,這兩種都是侵蝕和膨脹複合而成,開運算是先侵蝕後膨脹,閉運算是先膨脹後侵蝕。
對於集合 I 和 H,假設使用 H 對 I 進行開運算,代表 H 對 I 進行侵蝕後膨脹,記作:
開運算可以使物體輪廓變得光滑,還能使狹窄的連結斷開,以及消除外觀上的毛刺,但在物體大於結構元素的情況
下,開運算與侵蝕並不相同,圖像的輪廓並沒有產生整體的收縮,物體位置也沒有發生任何變化,假如我們對一幅
影像重複進行開運算,不會產生任何變化,這點和重複進行侵蝕會加強程度的現象不同。
有兩種方式進行開運算,一種是先呼叫 erode()將影像進行侵蝕,接著將侵蝕後的圖,當作 dilate()膨脹運算的原始圖,
最後的輸出圖即為結果,另外一種方式是使用 morphologyEx()函式,將其中某個參數輸入 MORPH_OPEN。
1. OpenCV 開運算
morphologyEx(const Mat &src, Mat &dst, int op, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)
•src:輸入圖,可以多通道,深度可為 CV_8U、CV_16U、CV_16S、CV_32F 或 CV_64F。
•dst:輸出圖,和輸入圖尺寸、型態相同。
•op:操作種類,決定要進行何種型態學操作,在開運算時輸入 MORPH_OPEN。
•kernel:結構元素。
•anchor:原點位置,預設為結構元素的中央。
•iterations:執行次數,預設為 1 次。
24
柯博文老師
www.powenko.com
openCV
對 於 集 合I 和 H , 假 設 使 用H 對 I 進 行 閉 運 算 , 代 表H 對 I 進 行 膨 脹 後 侵 蝕 , 記 作 :
閉運算也可以使物體輪廓變得光滑,和開運算不同,閉運算通常能彌補狹窄的間斷,假如我們對一幅影像重複進行
閉運算,不會產生任何變化,這點和重複進行膨脹會加強程度的現象不同。
有兩種方式進行開運算,一種是先呼叫 dilate()將影像進行膨脹,接著將膨脹後的圖,當作 erode()侵蝕運算的原始圖,
最後的輸出圖即為結果,另外一種方式是使用 morphologyEx(),將其中某個參數輸入 MORPH_CLOSE。
2. OpenCV 閉運算
morphologyEx(const Mat &src, Mat &dst, int op, Mat kernel, Point anchor=Point(-1,-1), int iterations=1)
•src:輸入圖,可以多通道,深度可為 CV_8U、CV_16U、CV_16S、CV_32F 或 CV_64F。
•dst:輸出圖,和輸入圖尺寸、型態相同。
•op:操作種類,決定要進行何種型態學操作,在閉運算時輸入 MORPH_CLOSE。
•kernel:結構元素。
•anchor:原點位置,預設為結構元素的中央。
•iterations:執行次數,預設為 1 次。
以 下 程 式 碼 示 範 morphologyEx() 的 使 用 , 模 板 參 數 可 輸 入 Mat() , 此 時 會 用 3×3 的 矩 形 模 板 , 也 可 以 用
getStructuringElement()來得到想要的大小和形狀的模板,我們可以看出二值化的圖形,在進行開運算或閉運算後,
影像變化的樣子:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
Mat src = imread("lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
Mat src2;
threshold(src,src2,120,255,THRESH_BINARY);
Mat dst1;
Mat dst2;
Mat dst3;
morphologyEx(src2, dst1, MORPH_OPEN, Mat());
morphologyEx(src2, dst2, MORPH_CLOSE, Mat());
Mat erodeStruct = getStructuringElement(MORPH_RECT,Size(5,5));
morphologyEx(src2, dst3, MORPH_OPEN, erodeStruct);
imshow("origin", src2);
imshow("open", dst1);
imshow("close", dst2);
25
柯博文老師
www.powenko.com
openCV
imshow("open2", dst3);
waitKey(0);
return 0;
}
26
柯博文老師
www.powenko.com
openCV
27
柯博文老師
www.powenko.com
openCV
第22章 輪廓輪廓
可提供物件的資訊,這邊介紹如何找輪廓,以及輪廓相關的物件特徵
22.1 輪廓(findContours、drawContours)
當我們做物件辨識時,透過輪廓可得到特定物件的資訊,協助我們做判斷,OpenCV 的 findContours()函式可找到
影像的輪廓,依實際需求調整參數輸入,而這邊的輪廓和 Sobel 這些找邊緣的處理不同, Sobel 是將物件內部消除,
只保留物件邊緣,findContours 是在經過 Sobel 處理之後,將這個只有邊緣的影像,把各個邊緣點做分類,連結的邊
緣點儲存在同個容器內,當我們找到輪廓後,可用 drawContours()劃出輪廓線,檢查是否取得正確或適合的輪廓。
以下列出兩個多載使用的函式:
1. OpenCV 找輪廓
void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method,
Pointoffset=Point())
void findContours(InputOutputArray image, OutputArrayOfArrays contours, int mode, int method, Point offset=Point())
•image:輸入圖,使用八位元單通道圖,所有非零的像素都會列入考慮,通常為二極化後的圖。
•contours:包含所有輪廓的容 器(vector),每個輪廓都是儲存點的容器(vector), 所 以 contours 的 資 料 結 構 為
vector< vector
•hierarchy:可有可無的輸出向量,以階層的方式記錄所有輪廓。
•mode:取得輪廓的模式。
•method:儲存輪廓點的方法。
mode:取得輪廓的模式,有以下幾種可選擇:
•CV_RETR_EXTERNAL:只取最外層的輪廓。
•CV_RETR_LIST:取得所有輪廓,不建立階層(hierarchy)。
•CV_RETR_CCOMP:取得所有輪廓,儲存成兩層的階層,首階層為物件外圍,第二階層為內部空心部分的輪廓,
如果更內部有其餘物件,包含於首階層。
•CV_RETR_TREE:取得所有輪廓,以全階層的方式儲存。
28
柯博文老師
www.powenko.com
openCV
method:儲存輪廓點的方法,有以下幾種可選擇:
•CV_CHAIN_APPROX_NONE:儲存所有輪廓點。
•CV_CHAIN_APPROX_SIMPLE:對水平、垂直、對角線留下頭尾點,所以假如輪廓為一矩形,只儲存對角的四個頂
點。
2. OpenCV 畫輪廓線
void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int
thickness=1, int lineType=8, InputArray hierarchy=noArray(), int maxLevel=INT_MAX, Point offset=Point())
•image:輸入輸出圖,會將輪廓畫在此影像上。
•contours:包含所有輪廓的容器(vector),也就是 findContours()所找到的 contours。
•contourIdx:指定畫某個輪廓。
•color:繪製的顏色。
•lineType:繪製的線條型態。
•hierarchy:輪廓階層,也就是 findContours()所找到的 hierarchy。
•maxLevel:最大階層的輪廓,可以指定想要畫的輪廓,有輸入 hierarchy 時才會考慮,輸入的值代表繪製的層數。
maxLevel:
•0:繪製指定階層的輪廓。
•1:繪製指定階層的輪廓,和他的一階子階層。
•2:繪製指定階層的輪廓,和他的一階、二階子階層。
•剩下數字依此類推。
以下程式碼我們先用 Canny()標出物體的邊緣,接著呼叫 findContours()將邊緣歸類為輪廓,呼叫 drawContours()讓我
們確認找到哪些輪廓:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
Mat src = imread("input.jpg", CV_LOAD_IMAGE_COLOR);
Mat src_gray = imread("input.jpg", CV_LOAD_IMAGE_GRAYSCALE);
Mat contoursImg = src.clone();
Mat edge;
blur(src_gray, src_gray, Size(3,3));
29
柯博文老師
www.powenko.com
openCV
Canny(src_gray, edge, 50, 150, 3);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
RNG rng(12345);
findContours(edge, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);
for(int i = 0; i<contours.size(); i++){
Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0, 255), 255);
drawContours(contoursImg, contours, i, color, 2, 8, hierarchy);
}
imshow("origin", src);
imshow("result", contoursImg);
waitKey(0);
return 0;
}
22.2 凸殼(convexHull)
凸殼(Convex Hull)是一個計算幾何中的概念,簡單的說,在給定二維平面上的點集合,凸殼就是將最外層的點連
接起來的凸多邊型,它能包含點集合中的所有點,在影像處理中,通常是找到某個物件後,用來填補空隙,或者是
進一步的進行物件辨識。
1. OpenCV 凸殼
void convexHull(InputArray points, OutputArray hull, bool clockwise=false, bool returnPoints=true)
•points:輸入資訊,可以為包含點的容器(vector)或是 Mat。
•hull:輸出資訊,包含點的容器(vector)。
•lockwise:方向旗標,如果 true 是順時針,false 是逆時針。
30
柯博文老師
www.powenko.com
openCV
22.3 輪廓包覆
(boundingRect、minAreaRect、minEnclosingCircle
)
當我們得到物件輪廓後,可用 boundingRect()得到包覆此輪廓的最小正矩形,minAreaRect()得到包覆輪廓的最小
斜矩形,minEnclosingCircle()得到包覆此輪廓的最小圓形,這些函式協助我們填補空隙,或者作進一步的物件辨識,
boundingRect()函式返回的是正矩形,所以如果物件有傾斜的情形,返回的可能不是我們想要的結果。
1. OpenCV 求包覆矩形
Rect boundingRect(InputArray points)
•points:輸入資訊,可以為包含點的容器(vector)或是 Mat。
•返回包覆輸入資訊的最小正矩形。
2. OpenCV 求包覆矩形
RotatedRect minAreaRect(InputArray points)
•points:輸入資訊,可以為包含點的容器(vector)或是 Mat。
•返回包覆輸入資訊的最小斜矩形。
3. OpenCV 求包覆圓形
void minEnclosingCircle(InputArray points, Point2f& center, float& radius)
•points:輸入資訊,可以為包含點的容器(vector)或是 Mat。
•center:包覆圓形的圓心。
•radius:包覆圓形的半徑。
31
柯博文老師
www.powenko.com
openCV
22.4 特徵(moment、contourArea、arcLength)
找 出 物 體 輪 廓 後 , 我 們 可 以 根 據 這 個 輪 廓 , 找 出 這 個 物 體 的 一 些 特 徵 , 這 邊 用 OpenCV 的
moment()、contourArea()、arcLength()函式,來找輪廓的質心、周長、面積,而這些特徵可以作為物件辨識的資訊。
1. OpenCV 計算矩
Moments moments(InputArray array, bool binaryImage=false)
•array:來源圖,可以輸入 8 位元單通道圖、浮點數 2 維陣列,或 1xN、Nx1 的 Point 或 Point2f 陣列。
•binaryImage:影像設定,只有 array 為影像時才有效果,如果設定為 true,所有非零的像素都列入計算。
•可從 Moments 計算質心位置。
假設返回一個 Moments mu,我們可依據下式,從 mu 計算質心位置,m10、m00、m01、m00 都是 Moments 的類別
成員。
Point2f center = Point2f(mu.m10/mu.m00 , mu.m01/mu.m00);
2. OpenCV 計算面積
double contourArea(InputArray contour, bool oriented=false)
•contour:輸入輪廓,一個含有 2 維點的 vector。
•oriented:輪廓方向,如果設為 ture 的話除了面積還會記錄方向,順時鐘和逆時鐘會有正負號的差異,預設為
false,不論輪廓方向都返回正的面積值。
3. OpenCV 計算周長:
double arcLength(InputArray curve, bool closed)
•curve:輸入輪廓,一個含有 2 維點的 vector。
•closed:輪廓封閉,指定 curve 是否封閉,
•返回曲線的長度或封閉輪廓的周長。
32
柯博文老師
www.powenko.com
openCV
22.5 輪廓和點距離
(pointPolygonTest、distanceTransform)
當我們有一個物體的輪廓後,我們可以從 OpenCV 的 pointPolygonTest()函式,得到輸入點和這個輪廓的關係,比
如這個點在輪廓的內側、線上或外側,以及和這輪廓的距離。
1. OpenCV 輪廓距離
double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)
•contour:輸入的輪廓。
•pt:想要知道距離關係的輸入點。
•measureDist:測試方式,可輸入 true 或 false。
measureDist:
•true:函式返回和輪廓的最近距離,在輪廓內部返回正值,外部返回負值,線上返回 0。
•false:單純確認輸入點位置,在輪廓內部返回+1,外部返回-1,線上返回 0。
OpenCV 有另一個和距離有關的函式 distanceTransform(),輸入一個二值化的圖,計算每個像素和最近的零像素的距
離,如果此像素強度為 0,得到的距離仍為 0,可以把這概念擴充到和輪廓的距離,輸入單純的輪廓圖,可從這函式
得到內部像素到輪廓的距離。
2. OpenCV 輪廓距離
void distanceTransform(InputArray src, OutputArray dst, int distanceType, int maskSize)
•src:輸入圖,8 位元單通道(通常為二值化圖)。
•dst:輸出圖,32 位元單通道浮點數圖,和 src 的尺寸相同。
•distanceType:距離型態,可以選擇 CV_DIST_L1、CV_DIST_L2 或 CV_DIST_C。
•maskSize : 遮 罩 尺 寸 , 可 以 選 3 、 5 或 CV_DIST_MASK_PRECISE , 當 distanceType 為
CV_DIST_C,這個參數限制為 3(因為 3 和 5 的結果相同)。
33
柯博文老師
www.powenko.com
CV_DIST_L1 或
openCV
第23章 影片、事件
23.1 讀取影片(VideoCapture)影片(video)是由連續的影像(image)組成,組成影片
的影像稱為影格(frame),影片播放時會不斷呈現新的影格,影格間的時間稱作更新頻率(frame rate)。
由於人類眼睛的結構,通常頻率高於每秒約 12 個影格的時候,就會認為是連貫的,而電影的拍攝及播
放影格率通常為每秒 24 個影格,對一般人而言算可以接受,當然如果有更快的更新頻率,看起來會更
流暢。
OpenCV 有兩種影片來源,分別是讀取硬碟裡的影像檔,或者是用電腦鏡頭讀取的即時影像,兩者使用方式類似,
皆使用 OpenCV 的 VideoCapture 物件進行這類型的操作,這邊介紹如何使用 VideoCapture 物件。
1. VideoCapture 建構式
VideoCapture::VideoCapture()
VideoCapture::VideoCapture(const string& filename)
VideoCapture::VideoCapture(int device)
•filename:影像檔名。
•device:裝置(像攝影機)的編號。
•透過建構式不同的輸入參數,指定 VideoCapture()的來源為影片檔或攝影機。
2. VideoCapture 初始化
bool VideoCapture::open(const string& filename)
bool VideoCapture::open(int device)
•可以在建構式就指定來源,也可以先用 VideoCapture()這個建構式,接著用 open()設定來源。
34
柯博文老師
www.powenko.com
openCV
bool VideoWriter::isOpened()
•檢查是否初始化成功,如果成功返回 true,否則返回 false。
3. VideoCapture 讀取影像
讀取影像:VideoCapture& VideoCapture::operator>>(Mat& image)
讀取影像:bool VideoCapture::read(Mat& image)
•透過這個函式不斷讀取來源影格,把資訊寫進 image。
4. VideoCapture 影像格式
double VideoCapture::get(int propId)
•得到影像設定,propId 代表返回哪個設定。
bool VideoCapture::set(int propId, double value)
•進行影像設定,propId 代表針對哪個設定。
以下程式碼使用電腦的攝影機讀取影像,接著及時秀出影像,用 VideoCapture::get()讀取影像的尺寸,waitKey(33)模
擬每秒 30 個 frame 的效果:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
VideoCapture video(0);
if (!video.isOpened()){
return -1;
}
Size
videoSize
=
Size((int)video.get(CV_CAP_PROP_FRAME_WIDTH),
(int)video.get(CV_CAP_PROP_FRAME_HEIGHT));
namedWindow("video demo", CV_WINDOW_AUTOSIZE);
Mat videoFrame;
while(true){
video >> videoFrame;
if(videoFrame.empty()){
35
柯博文老師
www.powenko.com
openCV
break;
}
imshow("video demo", videoFrame);
waitKey(33);
}
return 0;
}
以下程式碼改為讀取 avi 檔播放,VideoCapture 的建構式改為要讀取的 avi 檔的檔名:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
VideoCapture video("VideoTest.avi");
if (!video.isOpened()){
return -1;
}
Size
videoSize
=
Size((int)video.get(CV_CAP_PROP_FRAME_WIDTH),
(int)video.get(CV_CAP_PROP_FRAME_HEIGHT));
36
柯博文老師
www.powenko.com
openCV
namedWindow("video demo", CV_WINDOW_AUTOSIZE);
Mat videoFrame;
while(true){
video >> videoFrame;
if( videoFrame.empty()){
break;
}
imshow("video demo", videoFrame);
waitKey(33);
}
return 0;
}
23.2 製作影片(VideoWriter)
OpenCV 用 VideoWriter 類別來製作影像檔,一般的影像檔除了影像壓縮與編碼規格之外,還有聲音與字幕,但
OpenCV 開發時為求簡化,所以並沒有納入音軌與字幕,只單純處理影像,現在所有影像編碼都有獨一的短名,像
XVID、CIVX 和 H264 等,在使用 VideoWriter 時,我們可指定編碼方式。
1. VideoWriter 建構式
VideoWriter::VideoWriter()
VideoWriter::VideoWriter(const string& filename, int fourcc, double fps, Size frameSize, bool isColor=true)
•filename:輸出影像檔的檔名。
•fourcc:編碼方式,舉例來說 CV_FOURCC(‘P’,’I’,’M’,’1′)是 MPEG-1,CV_FOURCC(‘M’,’J’,’P’,’G’)是 motion-jpeg。
•fps:更新頻率。
•frameSize:影像檔的影像尺寸。
•isColor:是否為彩色影像。
2. VideoWriter 初始化
bool:VideoWriter::open(const string& filename, int fourcc, double fps, Size frameSize, bool isColor=true)
37
柯博文老師
www.powenko.com
openCV
•初始化參數和建構式相同,功用也一樣,我們可以建構式就指定影像檔各設定,也可以先用 VideoWriter()這個建構
式,接著用 open()設定影像檔。
bool VideoWriter::isOpened()
•檢查是否初始化成功,如果成功返回 true,否則返回 false。
3. VideoWriter 寫入影像
VideoWriter& VideoWriter::operator<<(const Mat& image)
void VideoWriter::write(const Mat& image)
•透過這個函式,將 image 寫入要輸出的影片檔。
以下程式碼開啟攝影機鏡頭讀取影像,並將這些及時影像存成 avi 檔:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
VideoCapture capture(0);
if(!capture.isOpened()){
return -1;
}
Size
videoSize
=
Size((int)capture.get(CV_CAP_PROP_FRAME_WIDTH),
(int)capture.get(CV_CAP_PROP_FRAME_HEIGHT));
VideoWriter writer;
writer.open("VideoTest.avi", CV_FOURCC('M', 'J', 'P', 'G'), 30, videoSize);
namedWindow("show image",0);
while(true){
Mat frame;
capture >> frame;
if(!frame.empty()){
writer.write(frame);
imshow("show image", frame);
if(waitKey(33) == 27){
break;
}
}
38
柯博文老師
www.powenko.com
openCV
}
return 0;
}
23.3 滑桿(createTrackbar)
OpenCV 提供 createTrackbar()函式,可以在視窗上產生滑桿,讓使用者自己調整輸入,接著用這輸入值執行預計
的操作,另外有 getTrackbarPos()和 setTrackbarPos()函式,讓我們對滑桿進行進一步的操作。
1. OpenCV 產生滑桿
int createTrackbar(const string& trackbarname, const string& winname, int* value, int count, TrackbarCallback onChange=0,
void*userdata=0)
•trackbarname:滑桿名稱。
•winname:滑桿的父視窗名稱。
•value:滑桿所在位置的值。
•count:滑桿允許的最大值,最小值為 0。
•onChange:自定義函式的名稱,當滑桿值變動時,會呼叫此函式。
當我們創建滑桿時,會搭配一個自定義函式,當使用者改變滑桿的值時,OpenCV 會自動呼叫此函式。我們自行決
定這個函式名稱和內容,把這名稱作為 createTrackbar()裡的 onChange 參數,當然,通常這函式內部會使用到滑桿輸
入值。
2. OpenCV 得到滑桿位置
int getTrackbarPos(const string& trackbarname, const string& winname)
•trackbarname:滑桿名稱。
•winname:滑桿的父視窗名稱。
39
柯博文老師
www.powenko.com
openCV
3. OpenCV 設定滑桿位置
void setTrackbarPos(const string& trackbarname, const string& winname, int pos)
•trackbarname:滑桿名稱。
•winname:滑桿的父視窗名稱。
•pos:滑桿位置。
以 下 我 們 程 式 碼 創 建 滑 桿 ,sliderValue 為 滑 桿 的 值 , 初 始 為 0 , 使 用 者 可 透 過 拉 動 滑 桿 更 改 sliderValue 的 值,
sliderMaxValue 為滑桿的最大值,我們這邊設為 100,當使用者拉動滑桿時,程式呼叫 on_trackbar()函式,此時讀取
sliderValue 的值當作影像的混和比例:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int sliderValue;
Mat src1, src2;
void on_trackbar(int, void*){
double alpha = (double) sliderValue/100.0 ;
double beta = ( 1.0 - alpha );
Mat dst;
addWeighted( src1, alpha, src2, beta, 0.0, dst);
imshow("trackbar demo", dst);
}
int main(){
src1 = imread("beach.jpg",CV_LOAD_IMAGE_UNCHANGED);
src2 = imread("cat.jpg",CV_LOAD_IMAGE_UNCHANGED);
sliderValue = 0;
int sliderMaxValue = 100;
namedWindow("trackbar demo", 0);
createTrackbar("Ratio",
"trackbar
on_trackbar);
on_trackbar(sliderValue, 0 );
40
柯博文老師
www.powenko.com
demo",
&sliderValue,
sliderMaxValue,
openCV
waitKey(0);
return 0;
}
23.4 滑鼠事件(setMouseCallback)
OpenCV 用 setMouseCallback()函式處理滑鼠事件,能夠偵測使用者滑鼠的行為,並呼叫我們寫的函式來做相關的
處理,使用 setMouseCallback()時要一個函式名當參數,且這個函式要有一定的引數格式(int event, int x, int y, int flags,
void* param),名稱則可以自己定義。
void setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata=0)
•winname:滑桿的視窗名稱。
•onMouse:自定義函式的名稱,當發生滑鼠事件時,會呼叫此函式。
•userdata:選擇性要傳給 onMouse 自定義函式的參數。
我們透過自定義的函式 onMouse(),來得到滑鼠事件的資訊。
void onMouse(int event, int x, int y, int flags, void* param)
•event:事件代號,代表滑鼠的動作。
•x:事件發生的 x 座標。
•y:事件發生的 y 座標。
•flags:旗標代號,代表拖曳事件。
•param:事件代號名稱,自己定義的事件 ID。
event 有以下幾種:
•CV_EVENT_MOUSEMOVE:滑動
41
柯博文老師
www.powenko.com
openCV
•CV_EVENT_LBUTTONDOWN:左鍵點擊
•CV_EVENT_RBUTTONDOWN:右鍵點擊
•CV_EVENT_MBUTTONDOWN:中鍵點擊
•CV_EVENT_LBUTTONUP:左鍵放開
•CV_EVENT_RBUTTONUP:右鍵放開
•CV_EVENT_MBUTTONUP:中鍵放開
•CV_EVENT_LBUTTONDBLCLK:左鍵雙擊
•CV_EVENT_RBUTTONDBLCLK:右鍵雙擊
•CV_EVENT_MBUTTONDBLCLK:中鍵雙擊
flags 有以下幾種:
•CV_EVENT_FLAG_LBUTTON:左鍵拖曳
•CV_EVENT_FLAG_RBUTTON:右鍵拖曳
•CV_EVENT_FLAG_MBUTTON:中鍵拖曳
•CV_EVENT_FLAG_CTRLKEY:Ctrl 不放事件
•CV_EVENT_FLAG_SHIFTKEY:Shift 不放事件
•CV_EVENT_FLAG_ALTKEY:Alt 不放事件
以下程式碼示範 setMouseCallback()的使用,先在視窗內秀出影像,當使用者在影像上拖曳矩形,此時在滑鼠點擊拖
曳的地方,會畫出藍色邊框的矩形:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
void onMouse(int Event,int x,int y,int flags,void* param);
Point VertexLeftTop(-1,-1);
Point VertexRightDown(-1,-1);
int main(){
Mat src = imread("lena.jpg",CV_LOAD_IMAGE_UNCHANGED);
namedWindow("image",0);
setMouseCallback("image",onMouse,NULL);
while(true){
if(VertexLeftTop.x==-1 && VertexRightDown.x==-1){
imshow("image", src);
}
if(VertexLeftTop.x!=-1 && VertexRightDown.x!=-1){
42
柯博文老師
www.powenko.com
openCV
rectangle(src,
Rect(VertexLeftTop,VertexRightDown),Scalar(255,0,0),2);
VertexLeftTop.x = -1;
VertexLeftTop.y = -1;
VertexRightDown.x = -1;
VertexRightDown.y = -1;
imshow("image", src);
}
if(cvWaitKey(33)==27){
break;
}
}
return 0;
}
void onMouse(int Event,int x,int y,int flags,void* param){
if(Event==CV_EVENT_LBUTTONDOWN){
VertexLeftTop.x = x;
VertexLeftTop.y = y;
}
if(Event==CV_EVENT_LBUTTONUP){
VertexRightDown.x = x;
VertexRightDown.y = y;
}
}
43
柯博文老師
www.powenko.com
openCV
第24章 特徵與機器學習
24.1 特徵介紹
特徵提取
電腦不認識圖像,只知道數值,為了使電腦能夠像人類的視覺一樣,透過觀察理解圖像,我們研究如何從圖像的像
素值,抽取有用的數據或訊息,來描述這個圖像或物體,這個過程就是特徵提取,這些描述就是所謂的特徵,我們
可以透過這些特徵,通過訓練來讓電腦如何懂得這些特徵。
1. 特徵
特徵是某個物體或圖像,能夠和其他物體或圖像區分的特點或特性,或是這些特點或特性的集合,有一些是透過像
素值,很直接就能得到的,比如說強度、邊緣、顏色等,有些是要透過變換或計算才能得到的,比如說矩(moment)、
直方圖、主成分等,這概念用於物體識別、影像匹配、視覺跟蹤等。
2. 特徵向量
我們常將一類對象的單個或多種特性結合起來,形成一個特徵向量來表示這個對象,如果只有一個特徵,則特徵向
量為一維向量,如果是 n 個特性的組合,則為 n 維向量,我們可把特徵向量想成是空間中的一個點,n 個特徵就是在
n 維空間的一個點,而分類的行為就是對這個空間的一種劃分。
3. 描述方式
經過圖像分割得到感興趣的區域之後,以下為一些常用來描述的特徵,通常會將這些組合成特徵向量以供分類使用:
1.周長:區域邊界上的像素數目。
44
柯博文老師
www.powenko.com
openCV
2.面積:區域的像素總數。
3.質心位置。
4.平均強度:區域所有像素的平均值。
5.包含區域的最小矩形。
6.最大強度。
7.最小強度。
8.大於或小於平均強度的個數。
24.2 Harris 角點
在影像中檢測特徵點時,角點可以做為一個重要的參考,因為角點是兩條邊緣的交點處,可以被精確定位,這和位
於相同強度的區域不同,與物體輪廓的點也不同,輪廓點難以在其他影像的相同物體進行精確定位。
Harris 特徵檢測器是一個經典的角點檢測方法,OpenCV 使用 cornerHarris()實現 Harris 角點偵測演算法,輸出結果為
浮點數類型的影像,每個像素值為相對位置的角點強度,之後再用閾值進行二值化即可得到角點。
1. OpenCV Harris 角點檢測
void cornerHarris(InputArray src, OutputArray dst, int blockSize, int ksize, double k, int borderType=BORDER_DEFAULT)
•src:輸入圖,8 位元或浮點數單通道圖。
•dst:輸出圖,儲存 Harris 檢測結果,型態為 CV_32FC1,尺寸和輸入圖相同。
•blockSize:相鄰像素的尺寸。
•ksize:Sobel 算子的濾波器模板大小。
•k:Harris 參數,即為下面方程式的 k 值。
•borderType:邊緣擴充方式。
為了偵測影像中的角點,Harris 觀察一個假定點周圍小窗口內強度差的平方和,窗口大小為 cornerHarris()的第三個參
數 blockSize,因為無法確定高強度變化的方向,因此在所有可能的方向計算,過程首先獲得強度變化最大的方向,
接著檢查它垂直方向的變化是否也很強烈,同時滿足的話便是一個角點。
小窗口強度差平方和:
用泰勒展開式對算式進行近似:
對算式化簡:
45
柯博文老師
www.powenko.com
openCV
轉換成矩陣型式:
矩陣 M 是一個斜方差(Covariance)矩陣,代表所有方向上的強度變化率,矩陣內的一階微分通常是 Sobel 算子計算結
果,cornerHarris()的第四個參數 ksize 為 Sobel 的模板尺寸,斜方差矩陣的特徵值,代表最大強度變化以及和它垂直
的方向,如果這兩個特徵值都低,代表此點位於變化不大的區域,要是其中一個較高另一個較低,代表此點位於邊
上,如果兩個特徵值都較高,代表此點位於角點上。所以我們尋找角點的方式就是擁有超過閾值的斜方差矩陣特徵
值:
用下式驗證兩個特徵值是否足夠高,當兩個特徵值都高時,此計算結果也高,這也是 cornerHarris()在每個位置得到
的 分 數 ,k 的 值 為 cornerHarris() 的 第 五 個 參 數 , 實 務 上 通 常 在 0.05~0.5 之 間 能 得 到 滿 意 的 結 果 :
以下我們用 cornerHarris()得到 harris 的分數,接著自定義閾值求出原始圖的角點:
#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(){
Mat src = imread("church.jpg",CV_LOAD_IMAGE_GRAYSCALE);
Mat harrisStrength;
cornerHarris(src, harrisStrength, 3, 3, 0.01);
Mat corners;
double thres = 0.0001;
threshold(harrisStrength, corners, thres, 255, THRESH_BINARY);
imshow("原始圖", src);
imshow("角點圖", corners);
waitKey(0);
return 0;
}
由上面結果可看出,實際呼叫 cornerHarris()找角點時,偵測的結果可能在鄰近區域內有許多角點,而不容易進行精
確定位,以下程式碼對 Harris 結果進行改進,角點不只需要結果高於閾值,還必須為局部最大值。所以對Harris 結
果圖進行膨脹,膨脹運算只有在局部最大值的地方維持原值,之後輸出結果為維持原值的位置才是角點,以下為詳
細的程式碼:
46
柯博文老師
www.powenko.com
openCV
關 於 特 徵 點 聚 集 的 問 題 , 除 了 用 局 部 極 大 值 的 方 式 , 也 可 以 指 定 兩 個 特 徵 點 的 最 小 距 離 ,OpenCV 另 外 有
goodFeaturesToTrack(),從 Harris 得分最高的點開始,僅接受距離大於最小允許距離的特徵點,檢測的結果可用於視
覺跟蹤的特徵集合。
24.3 FAST 特徵
Harris 算法提出了搜尋角點(或者說是特徵點)的方式,基於兩個正交方向上的強度變化率,能夠得到不錯的結果,但
由於需要耗時的計算影像的一階微分,而特徵點檢測通常只是整個複雜算法的第一步,所以這邊另外介紹FAST 檢
測器,正如名字所示,此算法能快速進行檢測,依賴較少像素來確定是否為特徵點。
FAST(Features from Accelerated Segment Test) 算法,和 Harris 算法同樣要定義甚麼是角點,FAST 的角點定義基於和
周圍像素的強度關係,檢查候選像素周圍一圈的像素,與中心點差異較大的像素如果組成連續的圓弧,並且弧長大
於圓周長的 3/4,那麼就把這個點視為特徵點。
FAST 算法使用額外的技巧進行加速,一開始先測試周圍圓上被 90。分割的四個點,比如說測試點的上、下、左、右
四個像素,因為滿足 FAST 對角點的定義,則這四個點至少有三個和中心點的強度差異須大於閾值,實務上大部分
的像素都可用這方法進行移除,因此使用上非常高效,原則上測試上的半徑應該是一個參數,實際上半徑為 3 可以
兼顧結果及效率。
和 Harris 找角點相同,可以在找到的角點上執行非極大值抑制,而由於 FAST 算法能快速的檢測特徵點,在對執行
速度要求的環境下,比如在高 FPS 的影片上進行視覺跟蹤時,此時可以考慮用 FAST 來搜尋特徵點。
24.4 SIFT 特徵
在不同影像上進行特徵匹配時,常會遇到尺度變化的問題,也就是要分析的物體,可能在不同張影像的大小是不同
的,當我們實際上要進行匹配時,由於尺度的差異,同個物體的特徵並不會匹配。
為了解決這個問題,有些算法用來尋找尺度不變的特徵,主要是基於每個檢測到的特徵點都伴隨著對應的尺寸,這
邊介紹一種尺度不變的特徵,SIFT(Scale-Invariant Feature Transform) 特徵,和另一個著名的尺度不變特徵檢測器
SURF(Speeded Up Robust Features)相比,SURF 速度較快,SIFT 搜尋的特徵較準確。
以下使用程式碼使用 SiftFeatureDetector 找到影像的 SURF 特徵,接著用 drawKeyPoints()劃出特徵點,旗標使用
DRAW_RICH_KEYPOINTS,可看出圓圈的尺寸與特徵的尺度成正比,同時由於 SURF 算法將方向與每個特徵作關
聯,使得特徵具有旋轉無關性:
47
柯博文老師
www.powenko.com
openCV
影像匹配除了需要檢測圖像的特徵點,還需要向量來描述特徵才能進行比較,OpenCV 裡使用 SiftDescriptorExtractor
來得到特徵點的 SIFT 描述子,描述子是一個 Mat,行數與特徵點個數相同,每行都是一個 N 維的特徵向量,SIFT 的
描述子維度為 128,特徵向量描述特徵點周圍的強度樣式,兩個特徵點越類似,特徵向量的距離越近。
假使我們想匹配在不同圖像的兩個相同物體,首先取得每個圖像的特徵,然後提取他們的描述子,將第一幅圖像和
第二幅圖像中的每個特徵向量做比較,距離最近的特徵向量即為那個特徵的最佳匹配,對第一幅圖像的所有特徵都
進行此處理,這也是 BruteForceMatcher 所採用的方法。
以下範例使用 SIFT 演算法檢測特徵點,並呼叫 drawMatches()看特徵點的匹配情形:
24.5 SURF 特徵
在不同影像上進行特徵匹配時,常會遇到尺度變化的問題,也就是要分析的物體,可能在不同張影像的大小是不同
的,當我們實際上要進行匹配時,由於尺度的差異,同個物體的特徵並不會匹配。
為了解決這個問題,有些算法用來尋找尺度不變的特徵,主要是基於每個檢測到的特徵點都伴隨著對應的尺寸,這
邊介紹一種尺度不變的特徵,SURF(Speeded Up Robust Features)特徵,不僅具有尺度和旋轉的不變性,而且非常高
效,和另一個著名的尺度不變特徵檢測器 SIFT(Scale-Invariant Feature Transform))相比,SURF 速度較快,SIFT 搜尋
的特徵較準確。
以下使用程式碼使用 SurfFeatureDetector 找到影像的 SURF 特徵,接著用 drawKeyPoints()劃出特徵點,旗標使用
DRAW_RICH_KEYPOINTS,可看出圓圈的尺寸與特徵的尺度成正比,同時由於 SURF 算法將方向與每個特徵作關
聯,使得特徵具有旋轉無關性:
影像匹配除了需要檢測圖像的特徵點,還需要向量來描述特徵才能進行比較,OpenCV 裡使用 SurfDescriptorExtractor
來得到特徵點的 SURF 描述子,描述子是一個 Mat,行數與特徵點個數相同,每行都是一個 N 維的特徵向量,SURF
的默認維度為 64,特徵向量描述特徵點周圍的強度樣式,兩個特徵點越類似,特徵向量的距離越近。
假使我們想匹配在不同圖像的兩個相同物體,首先取得每個圖像的特徵,然後提取他們的描述子,將第一幅圖像和
第二幅圖像中的每個特徵向量做比較,距離最近的特徵向量即為那個特徵的最佳匹配,對第一幅圖像的所有特徵都
進行此處理,這也是 BruteForceMatcher 所採用的方法。
以下範例使用 SURF 演算法檢測特徵點,並呼叫 drawMatches()看特徵點的匹配情形:
24.6 支撐向量機(SVM)
機器學習(Machine Learning)主要是設計算法,讓電腦能透過資料而有像人類的學習行為,算法通常是自動分析數
據,獲得規律,並利用規律對未知數據進行預測,進而達到分類、回歸分析等目的,在影像處理上則可能是影像辨
識。
48
柯博文老師
www.powenko.com
openCV
依據輸入資料是否有標籤,我們分監督式學習和非監督式學習,資料有標籤的為監督式學習,沒有標籤的為非監督
式學習,舉例來說,假如輸入臉的輪廓,輪廓本身沒有標籤,但加入每個輪廓年齡多少這個資料就是標籤。
這邊介紹支撐向量機 SVM(Support Vector Machine),這是一種監督式的機器學習算法,原先用於二元分類,比如說
這封郵件是否為垃圾郵件,或是這個人是男是女,這種二個類別的問題,但現在已擴展且廣泛應用於統計分類和回
歸分析。
SVM 建構多維的超平面來分類資料點,這個超平面即為分類邊界,直觀來說,好的分類邊界要距離最近的訓練資料
點越遠越好,因為這樣可以減低判斷錯誤的機率,而 SVM 的目標即為找出間隔最大的超平面來作為分類邊界,下面
為 SVM 的示意圖,綠線為分類邊界,分類邊界與最近的訓練資料點之間的距離稱為間隔(margin)。
以下我們示範 OpenCV SVM 的使用方式,大概可分以下幾個步驟:
1.在空間中選擇六個點作為輸入資料。
2.給這些點相對的標籤,對輸入資料進行分類。
3.設置 CvSVMParams 作為 SVM 的參數。
4.將資料和參數輸入 SVM::train(),進行訓練後即可求得分類邊界。
5.之後可輸入新的資料,由 SVM::predict()看此筆資料屬於哪一類。
以下為實際程式碼:
# include <cstdio>
# include <opencv2/opencv.hpp>
# include <vector>
using namespace cv;
int main(){
int width = 300;
int height = 300;
Mat image = Mat::zeros(height, width, CV_8UC3);
float trainingData[6][2] = {{250,250},{200,100},{260,180},{140,10},{30,70},
{50,50}};
Mat trainingDataMat(6, 2, CV_32FC1, trainingData);
49
柯博文老師
www.powenko.com
openCV
float labels[6] = {1.0, 1.0, 1.0, -1.0, -1.0, -1.0};
Mat labelsMat(6, 1, CV_32FC1, labels);
CvSVMParams params;
params.svm_type
= CvSVM::C_SVC;
params.kernel_type = CvSVM::LINEAR;
params.term_crit
= cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);
CvSVM SVM;
SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);
Vec3b green(0,255,0), red (0,0,255);
for (int i=0; i<image.rows; ++i){
for (int j=0; j<image.cols; ++j){
Mat sampleMat = (Mat_<float>(1,2) << j,i);
float response = SVM.predict(sampleMat);
if(response == 1){
image.at<Vec3b>(i,j)=green;
}
else if(response == -1){
image.at<Vec3b>(i,j)=red;
}
}
}
circle(image, Point(250, 250), 3, Scalar(0, 0, 0));
circle(image, Point(200, 100), 3, Scalar(0, 0, 0));
circle(image, Point(260, 180), 3, Scalar(0, 0, 0));
circle(image, Point(140, 10), 3, Scalar(255, 255, 255));
circle(image, Point(30, 70), 3, Scalar(255, 255, 255));
circle(image, Point(50, 50), 3, Scalar(255, 255, 255));
imshow("SVM 示範", image);
waitKey(0);
return 0;
}
50
柯博文老師
www.powenko.com
openCV
有時因為資料的關係,無法取得完美的分類邊界,以下示範如何用 SVM 取得相對好的分類邊界,使用方式和上述例
子差不多:
#include <cstdio>
#include <opencv2/opencv.hpp>
#include <vector>
using namespace cv;
int main(){
int width = 300;
int height = 300;
Mat I = Mat::zeros(height, width, CV_8UC3);
Mat trainData(100, 2, CV_32FC1);
Mat labels (100, 1, CV_32FC1);
//設 100 個隨機點
RNG rng;
for(int i=0; i<50; i++){
labels.at<float>(i,0) = 1.0;
int tempY = rng.uniform(0,299);
int tempX = rng.uniform(0,170);
trainData.at<float>(i,0) = tempX;
trainData.at<float>(i,1) = tempY;
}
for(int i=50; i<99; i++){
labels.at<float>(i,0) = -1.0;
51
柯博文老師
www.powenko.com
openCV
int tempY = rng.uniform(0,299);
int tempX = rng.uniform(130,299);
trainData.at<float>(i,0) = tempX;
trainData.at<float>(i,1) = tempY;
}
CvSVMParams params;
params.svm_type
= SVM::C_SVC;
params.C
= 0.1;
params.kernel_type = SVM::LINEAR;
params.term_crit
= TermCriteria(CV_TERMCRIT_ITER, (int)1e7, 1e-6);
CvSVM svm;
svm.train(trainData, labels, Mat(), Mat(), params);
Vec3b green(0,100,0), blue (100,0,0);
for (int i = 0; i < I.rows; ++i){
for (int j = 0; j < I.cols; ++j){
Mat sampleMat = (Mat_<float>(1,2) << i, j);
float response = svm.predict(sampleMat);
if(response == 1){
I.at<Vec3b>(j, i)=green;
}
else if (response == 2){
I.at<Vec3b>(j, i)=blue;
}
}
}
float px, py;
for (int i=0; i<50; ++i){
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point((int)px, (int)py), 3, Scalar(0, 0, 255));
}
for (int i=50; i<100; ++i){
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
52
柯博文老師
www.powenko.com
openCV
circle(I, Point((int)px, (int)py), 3, Scalar(255, 255,0));
}
imshow("SVM 示範", I);
waitKey(0);
return 0;
}
第25章 影像間的投影關係
25.1 相機校正(Camera calibration)
這邊介紹如何使用 OpenCV 提供的函式,對照相機進行校正。校正時我們必須先準備圖表,並用照相機在不同視
角和距離對這個圖表拍照,使用時大約拍攝 10 到 20 張,以這些影像當作我們的輸入資料,目前 OpenCV 的支持三
種類型的校準圖表,分別是:1.經典的黑白色棋盤,2.對稱圓形圖案,3.非對稱圓形圖案。
OpenCV 在作相機校正時,有考量到徑向(radial)和切向(tangential)兩個因素,徑向失真會產生桶狀或類似魚眼效果的
失真,對於一個校正前的像素點(X,Y),校正後的位置在(Xcorrected ,Ycorrected),以下為徑向失真校正前後位置的關
係:
53
柯博文老師
www.powenko.com
openCV
切向失真主要發生在鏡頭和拍攝物體面不是平行,由以下的公式進行切向失真校正:
綜 合 以 上 兩 個 公 式 ,OpenCV 主 要 考 量 以 下 五 個 參 數 的 矩 陣 進 行 校 正 :
1. OpenCV 找棋盤角點
bool findChessboardCorners(InputArray image, Size patternSize, OutputArray corners, int flags =
CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE)
•image:輸入圖,必須為 8 位元的灰階或彩色影像。
•patternSize:棋盤的尺寸,patternSize = Size(points_per_row,points_per_colum) = Size(columns,rows)。
•corners:輸出角點。
•flags : 旗 標 ,CV_CALIB_CB_ADAPTIVE_THRESH 表 示 使 用 區 域 自 適 應 濾 波 進 行 二
值 化,
CV_CALIB_CB_NORMALIZE_IMAGE 表示二值化前先呼叫 equalizeHist()。
•有找到棋盤角點返回 true,否則返回 false。
2. OpenCV 找精確角點
void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria)
•image:輸入圖。
•corners:輸入角點,會更新成更精確的位置。
•winSize:搜尋範圍,假設輸入 Size(5,5),則會搜尋 5*2+1 的範圍,也就是 11×11 的尺寸
•criteria:迭代搜尋的終止條件。
3. OpenCV 相機校正
double calibrateCamera(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize,
InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs, int
flags=0, TermCriteria criteria=TermCriteria( TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON))
•objectPoints:校正座標系統的位置,輸入型態為 std::vector< std::vector< cv::Vec3f > >。
•imagePoints : 校 正 點 的 投 影 , 輸 入 型 態 為 std::vector< std::vector< cv::Vec2f > > , imagePoints.size() 和
objectPoints.size()必須相同,且 imagePoints[i].size()和 objectPoints[i].size()也必須相同。
•imageSize:影像尺寸。
•cameraMatrix:輸出 3×3 浮點數的相機矩陣。
•distCoeffs:輸出的畸變參數,(k_1, k_2, p_1, p_2[, k_3[, k_4, k_5, k_6]]) 的 4、5 或 8 個元素。
54
柯博文老師
www.powenko.com
openCV
4. OpenCV 校正矩陣
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, InputArray newCameraMatrix,
Size size, int m1type, OutputArray map1, OutputArray map2)
•cameraMatrix:輸入的相機矩陣。
•distCoeffs:輸入的畸變參數。
•newCameraMatrix:新的相機矩陣。
•size:沒有畸變影像的尺寸。
•m1type:map1 的型態,可以為 CV_32FC1 或 CV_16SC2。
•map1:第一個輸出矩陣。
•map2:第二個輸出矩陣。
我們程式碼用以下步驟進行相機校正:
1.多次拍攝圖表,並將影像存在硬碟作為輸入資料,這邊選用 14 張圖表影像。
2.尋找每張影像的校正點,為了得到更精確的位置,另外呼叫 cornerSubPix()函式,TermCriteria 代表終止條件,
這邊是用迭代次數 30 次,最小精度 0.1 當作終止條件。
3.輸入每張影像校正後的校正點位置。
4.假設這張影像的每個校正點都有找到,依我們的輸入圖來說,也就是 24 個點都有找到的話,就把這張影像的校正
點存入當作輸入的資料點。
5.呼叫 calibrateCamera()得到相機矩陣 cameraMatrix,以及畸變矩陣 distCoeffs,這兩個矩陣會在後續校正時使用。
6.呼叫 initUndistortRectifyMap()得到映射位置矩陣 map1、map2。
7.得到 map1 和 map2 後,即可對實際輸入的影像進行映射得到校正後的影像。
8.可到此處下載圖檔:下載圖檔
5. calibration.h
#include <cstdio>
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
using namespace cv;
using namespace std;
class CameraCalibrator{
private:
55
柯博文老師
www.powenko.com
openCV
vector<string> m_filenames;
Size m_borderSize;
vector<vector<Point2f> > m_srcPoints;
vector<vector<Point3f> > m_dstPoints;
public:
void setFilename();
void setBorderSize(const Size &borderSize);
void addChessboardPoints();
void addPoints(const vector<Point2f> &imageCorners, const vector<Point3f>
&objectCorners);
void calibrate(const Mat &src, Mat &dst);
};
6. calibration.cpp
#include "calibration.h"
void CameraCalibrator::setFilename(){
m_filenames.clear();
m_filenames.push_back("chessboard01.jpg");
m_filenames.push_back("chessboard02.jpg");
m_filenames.push_back("chessboard03.jpg");
m_filenames.push_back("chessboard04.jpg");
m_filenames.push_back("chessboard05.jpg");
m_filenames.push_back("chessboard06.jpg");
m_filenames.push_back("chessboard07.jpg");
m_filenames.push_back("chessboard08.jpg");
m_filenames.push_back("chessboard09.jpg");
m_filenames.push_back("chessboard10.jpg");
m_filenames.push_back("chessboard11.jpg");
m_filenames.push_back("chessboard12.jpg");
m_filenames.push_back("chessboard13.jpg");
m_filenames.push_back("chessboard14.jpg");
}
void CameraCalibrator::setBorderSize(const Size &borderSize){
56
柯博文老師
www.powenko.com
openCV
m_borderSize = borderSize;
}
void CameraCalibrator::addChessboardPoints(){
vector<Point2f> srcCandidateCorners;
vector<Point3f> dstCandidateCorners;
for(int i=0; i<m_borderSize.height; i++){
for(int j=0; j<m_borderSize.width; j++){
dstCandidateCorners.push_back(Point3f(i, j, 0.0f));
}
}
for(int i=0; i<m_filenames.size(); i++){
Mat image=imread(m_filenames[i],CV_LOAD_IMAGE_GRAYSCALE);
findChessboardCorners(image, m_borderSize, srcCandidateCorners);
TermCriteria param(TermCriteria::MAX_ITER + TermCriteria::EPS, 30, 0.1);
cornerSubPix(image, srcCandidateCorners, Size(5,5), Size(-1,-1), param);
if(srcCandidateCorners.size() == m_borderSize.area()){
addPoints(srcCandidateCorners, dstCandidateCorners);
}
}
}
void
CameraCalibrator::addPoints(const
vector<Point2f>
&srcCorners,
const
vector<Point3f> &dstCorners){
m_srcPoints.push_back(srcCorners);
m_dstPoints.push_back(dstCorners);
}
void CameraCalibrator::calibrate(const Mat &src, Mat &dst){
Size imageSize = src.size();
Mat cameraMatrix, distCoeffs, map1, map2;
vector<Mat> rvecs, tvecs;
calibrateCamera(m_dstPoints,
m_srcPoints,
imageSize,
cameraMatrix,
distCoeffs, rvecs, tvecs);
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(), Mat(), imageSize,
CV_32F, map1, map2);
57
柯博文老師
www.powenko.com
openCV
remap(src, dst, map1, map2, INTER_LINEAR);
}
7. main.cpp
#include "calibration.h"
int main(){
CameraCalibrator myCameraCalibrator;
myCameraCalibrator.setFilename();
myCameraCalibrator.setBorderSize(Size(6,4));
myCameraCalibrator.addChessboardPoints();
Mat src = imread("chessboard09.jpg",0);
Mat dst;
myCameraCalibrator.calibrate(src, dst);
imshow("Original Image", src);
imshow("Undistorted Image", dst);
waitKey();
return 0;
}
58
柯博文老師
www.powenko.com
openCV
第26章 其他
26.1 XML 檔操作(FileStorage)
透過標記式語言,電腦之間可以傳輸各種資訊,標記式語言概分成兩類,一種像 HTML,使用國際通用的名詞,
不 能 自 行 創 建 標 記 , 另 一 種 像 XML, 可 自 行 定 義 各 標 籤 名 稱 的 標 記 式 語 言 , 這 邊 我 們 使 用 OpenCV 提 供 的
FileStorage 進行 XML 檔的儲存和寫入。
這 邊 我 們 示 範 七 種 資 料 結 構 的 讀 取 和 寫 入 , 分 別 為 int 、 string 、 int array 、 string array 、 STL map 、 自 創 類 別
Person、OpenCV 的 Mat 類別。
1. 創建 FileStorage 類別
OpenCV 用 FileStorage 類別寫入或讀取 xml 檔的資料,所以操作 xml 檔前須先創建此類別物件,並指明是讀取還是寫
入,使用完畢後關閉:
FileStorage fs;
fs.open(filename, FileStorage::WRITE);
//寫入數據
fs.open(filename, FileStorage::READ);
//讀取數據
fs.release();
59
柯博文老師
www.powenko.com
openCV
2. 操作 int、string
寫入資料時,先輸入一個 string 結構,當作此資料的 key,接著輸入相對的 value。讀取資料時透過先前輸入的 key,
讀到目標資料,以下兩個讀取方式結果相同。
fs << "name" << "John";
fs << "age"
<< 27;
std:: string _name;
fs["name"] >> _name;
//方法一
_name = (string) fs["name"];
//方法二
int _age;
fs["age"] >> _age;
3. 操作 array
寫入 array 時,一開始輸入此陣列的 key 名稱,輸入實際資料前,需加上"[“,完成後加上"]",我們使用 FileNode 和
FileNodeIterator 讀取資料,用 FileNode 找到這個 array,再用 FileNodeIterator 尋訪 array 內部所有資料。
fs << "hobby" << "[" << "basketball" << "swimming" << "shopping" << "]";
FileNode hobbyNode = fs["hobby"];
FileNodeIterator it = hobbyNode.begin();
std::vector<std::string> _hobby;
while(it != hobbyNode.end()){
_hobby.push_back((string)*it);
it ++;
}
4. 操作 map
寫入 map 時,一開始輸入此 map 的 key 名稱,輸入實際資料前,需加上"{“,接著分別以 key-value 的順序,依序輸
入完 map 所有的資料,最後加上"}",使用 FileNode 找到這個 map 資料,再用個別的 key 找到目標資料。
fs << "salary" << "{" << "engineer" << 1000 << "cashier" << 700 << "}";
FileNode salaryNode = fs["salary"];
std::map<std::string, int> _salary;
_salary.insert(std::make_pair("engineer",(int)salaryNode["engineer"]));
_salary.insert(std::make_pair("cashier",(int)salaryNode["cashier"]));
60
柯博文老師
www.powenko.com
openCV
5. 操作 Mat
OpenCV 已經幫我們封裝好 Mat 的讀取和寫入,當作類似 int 的資料結構操作即可。
Mat R(3,3,CV_32F,1.0);
fs << "Mat" << R;
Mat T;
fs["Mat"] >> T;
6. 操作自建類別
操作自建類別時,除了要在內別內定義 write 和 read 函式,需要在類別外覆寫 OpenCV 原本的 write 和 read 函式,才
能有和 Mat 類似的操作方式。
Person John;
John.m_age = 30;
John.m_name = "John";
fs << "guest" << John;
Person _guest;
fs["guest"] >> _guest;
以下程式碼為將資料儲存成一個 xml 檔:
#include <opencv2/core/core.hpp>
#include <iostream>
#include <string>
using namespace cv;
using namespace std;
class Person{
public:
int m_age;
string m_name;
void write(FileStorage& fs) const {
fs << "{" << "age" << m_age << "name" << m_name << "}";
}
61
柯博文老師
www.powenko.com
openCV
void read(const FileNode& node){
m_age = (int)node["age"];
m_name = (string)node["name"];
}
};
static void write(FileStorage& fs, const std::string&, const Person& x){
x.write(fs);
}
static void read(const FileNode& node, Person& x, const Person& default_value =
Person()){
if(node.empty())
x = default_value;
else{
x.read(node);
}
}
int main(){
FileStorage fs("demo.xml", FileStorage::WRITE);
fs << "name" << "John";
fs << "age"
<< 27;
fs << "hobby" << "[" << "basketball" << "swimming" << "shopping" << "]";
fs << "salary" << "{" << "engineer" << 1000 << "cashier" << 700 << "}";
Mat R(3,3,CV_32F,1.0);
fs << "Mat" << R;
Person John;
John.m_age = 30;
John.m_name = "John";
fs << "guest" << John;
fs.release();
return 0;
62
柯博文老師
www.powenko.com
openCV
}
以下程式碼為讀取一個 xml 檔:
#include <opencv2/core/core.hpp>
#include <iostream>
#include <string>
using namespace cv;
using namespace std;
class Person{
public:
int m_age;
string m_name;
void write(FileStorage& fs) const {
fs << "{" << "age" << m_age << "name" << m_name << "}";
}
void read(const FileNode& node){
63
柯博文老師
www.powenko.com
openCV
m_age = (int)node["age"];
m_name = (string)node["name"];
}
};
static void write(FileStorage& fs, const std::string&, const Person& x){
x.write(fs);
}
static void read(const FileNode& node, Person& x, const Person& default_value =
Person()){
if(node.empty())
x = default_value;
else{
x.read(node);
}
}
static ostream& operator<<(ostream& out, const Person& m){
out << "age = " << m.m_age << endl;
out << "name = " << m.m_name << endl;
return out;
}
int main(){
FileStorage fs("demo.xml", FileStorage::READ);
int _age;
fs["age"] >> _age;
std:: string _name;
fs["name"] >> _name;
cout << _age << endl;
cout << _name << endl;
cout << endl;
FileNode hobbyNode = fs["hobby"];
FileNodeIterator it = hobbyNode.begin();
std::vector<std::string> _hobby;
64
柯博文老師
www.powenko.com
openCV
while(it != hobbyNode.end()){
_hobby.push_back((string)*it);
cout << (string)*it << endl;
it ++;
}
cout << endl;
FileNode salaryNode = fs["salary"];
std::map<std::string, int> _salary;
_salary.insert(std::make_pair("engineer",(int)salaryNode["engineer"]));
_salary.insert(std::make_pair("cashier",(int)salaryNode["cashier"]));
cout << "engineer:" << (int)(_salary["engineer"]) << endl;
cout << "cashier:" << (int)(_salary["cashier"]) << endl;
cout << endl;
Mat T;
fs["Mat"] >> T;
cout << "T = " << T << endl;
cout << endl;
Person _guest;
fs["guest"] >> _guest;
cout << "guest:" << "\n" << _guest << endl;
cout << endl;
system("PAUSE");
fs.release();
return 0;
}
65
柯博文老師
www.powenko.com
openCV
26.2 GPU 平行運算
GPU(graphics processing unit)一開始是為渲染圖形場景而建立,這些場景的數據不需要以串列的方式一步一步執行,
而是用並行的方式一次性渲染大量的數據,從結構上來說,GPU 不像 CPU 基於數個寄存器和高速指令集,GPU 一
般有數百個較小的處理單元。這些處理單元每一個都比CPU 的核心慢很多,然而由於它的數目眾多,能夠同時進行
大量運算,已經有越來越多的人嘗試用 GPU 來進行圖形渲染以外的工作,這就產生了 GPGPU(general-purpose
computation on graphics processing units)的概念。
圖像處理器有它自己的內存,一般稱呼為顯存,當我們從硬碟讀數據並產生一個 Mat 對象的時候,數據是放在普通
內存當中的(由 CPU 掌管),CPU 可以直接操作內存,然而 GPU 不能這樣,它只能操作它自己的顯存,所以需要先讓
CPU 將用於計算的信息轉移到 GPU 掌管的顯存上。完成這一個上傳過程,需要比訪問內存多很多的時間,而且最終
的計算結果需要回傳到系統內存,由於傳輸的代價高昂,所以耗時太少的函數到 GPU 上計算只會造成反效果。
Mat 對象僅僅存儲在內存中,GPU 上的計算必須使用 GpuMat,它的工作方式類似 Mat,唯一的限制是不能直接引用
GPU 函數,使用時要傳輸 Mat 對像到GPU 上,且計算時使用 gpu::內的相對函式,計算完成需再回傳結果,可使用簡
單的重載操作符號或者調用下載函數。
以下我們比較傳統的
66
柯博文老師
www.powenko.com
Download