8.1 查找并繪制輪廓
一個輪廓一般對應一系列的點,也就是圖像中的一條曲線。其表示方法可能 根據不同的情況而有所不同。在OpenCV 中,可以用findContours()函數從二值圖 像中查找輪廓
8.1.1 尋找輪廓: findContours() 函數
findContours) 函數用于在二值圖像中尋找輪廓。
void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, intmethod, Point offset = Point())
- 第一個參數,InputArray類型的image, 輸入圖像,即源圖像,填Mat 類的 對象即可,且需為8位單通道圖像。圖像的非零像素被視為1,0像素值被 保留為0,所以圖像為二進制。我們可以使用 compare() 、inrange()、
threshold() 、adaptivethreshold() 、cannyO 等函數由灰度圖或彩色圖創建二進 制圖像。此函數會在提取圖像輪廓的同時修改圖像的內容。 - 第二個參數,OutputArrayOfArrays 類 型 的contours、檢測到的輪廓、函數 調用后的運算結果存在這里。每個輪廓存儲為一個點向量,即用point 類 型 的 vector表示。
- 第三個參數,OutputArray 類型的hierarchy,可選的輸出向量,包含圖像的 拓撲信息。其作為輪廓數量的表示,包含了許多元素。每個輪廓contours[i] 對 應 4 個hierarchy 元 素hierarchy[i][0]~hierarchy[i][3], 分別表示后一 個輪廓、前一個輪廓、父輪廓、內嵌輪廓的索引編號。如果沒有對應項,
對應的hierarchy[i] 值設置為負數。 - 第四個參數,int 類型的mode, 輪廓檢索模式,取值如表8.1所示。
表8.1 findContours函數可選的輪廓檢索模式
標識符 | 含義 |
---|---|
RETR_EXTERNAL | 表示只檢測最外層輪廓。對所有輪廓,設置 hierarchy[i][2]=hierarchy[i][3]=-1 |
RETR_LIST | 提取所有輪廓,并且放置在list中。檢測的輪廓 不建立等級關系 |
RETR_CCOMP | 提取所有輪廓,并且將其組織為雙層結構(two-level hierarchy:頂層為連通域的外圍邊界, 次層為孔的內層邊界 |
RETR_TREE | 提取所有輪廓,并重新建立網狀的輪廓結構 |
- 第五個參數,int類 型 的method, 為輪廓的近似辦法,取值如表8.2所示。
表8.2 findContours函數可選的輪廓近似辦法
標識符 | 含義 |
---|---|
CHAIN_APPROX NONE | 獲取每個輪廓的每個像素,相鄰的兩個點的像素位置差不超過 1,即max(abs(xl-x2),abs(y2-y1))=1 |
CHAIN_APPROX_SIMPLE | 壓縮水平方向,垂直方向,對角線方向的元素,只保留該方向 的終點坐標,例如一個矩形輪廓只需4個點來保存輪廓信息 |
CHAIN_APPROX_TC89_L1 ,CHAIN_APPROX_TC89_KCOS | 使用Teh-Chinl鏈逼近算法中的一個 |
同樣地,在表8.1和8.2中列出的宏之前加上”CV_” 前綴,便是OpenCV2 中可以使用的宏。如"RETR_CCOMP” 宏 的OpenCV2 版 為“CV_RETR_CCOMP"。
- 第六個參數,Point 類 型 的 offset,每個輪廓點的可選偏移量,有默認值 Point()。對 ROI 圖像中找出的輪廓,并要在整個圖像中進行分析時,這個 參數便可排上用場。
findContours 經 常 與drawContours 配合使用一使用用findContours (函數檢測 到圖像的輪廓后,便可以用drawContours (函數將檢測到的輪廓繪制出來。接下來, 讓我們一起看看drawContours() 函數的用法。
8.1.2 繪 制 輪 廓 :drawContours ()函 數
drawContours() 函數用于在圖像中繪制外部或內部輪廓。
void drawContours(InputoutputArray image, InputArrayofArrays contours, int contourIdx, const Scalar &color, int thickness = 1, int lineType = 8, InputArray hierarchy = noArray(0), int maxLevel = INT_MAX, Point offset = Point())
- 第 一 個參數,InputArray類 型 的image, 目標圖像,填Mat 類的對象即可。
- 第二個參數,InputArrayOfArrays類型的contours,所有的輸入輪廓。每個 輪廓存儲為一個點向量,即用point類 型 的vector表示。
- 第三個參數,int類 型 的contourldx,輪廓繪制的指示變量。如果其為負值, 則繪制所有輪廓。
- 第四個參數,const Scalar&類型的color, 輪廓的顏色。
- 第五個參數,int thickness,輪廓線條的粗細度,有默認值1。如果其為負 值(如 thickness=cv_filled), 便會繪制在輪廓的內部。可選為FILLED 宏(OpenCV2版為CV_FILLED)。
- 第六個參數,int 類型的lineType,線條的類型,有默認值8。取值類型如 表8 . 3所示。
表8.3 可選線性
lineType線性 | 含義 |
---|---|
8(默認值) | 8連通線型 |
4 | 4連通線型 |
LINE_AA(OpenCV2版為CV_AA) | 抗鋸齒線型 |
- 第七個參數,InputArray 類型的hierarchy,可選的層次結構信息,有默認 值noArray()。
- 第八個參數,int類型的maxLevel,表示用于繪制輪廓的最大等級,有默認 值INT_MAX
- 第九個參數,Point類型的offset,可選的輪廓偏移參數,用指定的偏移量 offset=(dx,dy) 偏移需要繪制的輪廓,有默認值Point()。
下面是 一 個調用小示例。 //在白色圖像上繪制黑色輪廓
Mat result(image.size(),CV_8U,cv::Scalar(255));
drawContours(result,contours,- 1,Scalar(0),3);
8.1.3 基礎示例程序:輪廓查找
void Test52() {Mat srcImage = imread("image.jpg", 0); //灰度圖讀入imshow("src", srcImage);Mat dstImage = Mat::zeros(srcImage.rows, srcImage.cols, CV_8UC3);srcImage = srcImage > 119;imshow("mid", srcImage); //取閾值后的圖像//定義輪廓和層次結構std::vector<std::vector<Point>>contours;std::vector<Vec4i>hierachy;//查找輪廓findContours(srcImage, contours, hierachy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);//遍歷所有頂層的羅坤,隨機顏色繪制出每個連接組件顏色int index = 0;for (; index >= 0; index = hierachy[index][0]) {Scalar color(rand() & 255, rand() & 255, rand() & 255);drawContours(dstImage, contours, index, color, FILLED, 8, hierachy);imshow("dst", dstImage);}waitKey(0);
}
8.1.4 綜合示例程序:查找并繪制輪廓
除了上述這個精簡版的示例程序,還為大家準備了一個更加復雜一些的關 于查找并繪制輪廓的綜合示例程序。此程序利用了圖像平滑技術(blur() 函數)和邊緣檢測技術(cannyO 函數),根據滑動條的調節,可以動態地檢測出圖形的 輪 廓 。
namespace test53 {Mat g_srcImage, g_grayImage;int g_nThresh = 80;int g_nThresh_max = 255;RNG g_rng(12345);Mat g_cannyMat_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;void on_ThreshChange(int, void*) {Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2, 3); //Canny算子邊緣檢測findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));//輪廓提取Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//隨機值drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());}imshow("drawing", drawing);}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3)); //降噪namedWindow("window1");imshow("window1", g_srcImage);createTrackbar("value", "window1", &g_nThresh, g_nThresh_max, on_ThreshChange);on_ThreshChange(0, 0);waitKey(0);}
}void Test53() {test53::Test();
}
8.2 尋找物體的凸包
8.2.1 凸 包
凸包(Convex Hull) 是一個計算幾何(圖形學)中常見的概念。簡單來說, 給定二維平面上的點集,凸包就是將最外層的點連接起來構成的凸多邊型,它是 能包含點集中所有點的。理解物體形狀或輪廓的一種比較有用的方法便是計算一 個物體的凸包,然后計算其凸缺陷(convexity defects)。很多復雜物體的特性能很 好地被這種缺陷表現出來。
如圖8.9所示,我們用人手圖來舉例說明凸缺陷這一概念。手周圍深色的線 描畫出了凸包,A 到 H 被標出的區域是凸包的各個“缺陷”。正如看到的,這些 凸度缺陷提供了手以及手狀態的特征表現的方法。
新版OpenCV 中 ,convexHull 函數用于尋找圖像點集中的凸包,我們一起來 看一下這個函數。
8.2.2 尋找凸包:convexHullO函數
上文已經提到過,convexHullO 函數用于尋找圖像點集中的凸包,其原型聲明 如 下 。
void convexHull(InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true)
- 第一個參數,InputArray類型的points,輸入的二維點集,可以填Mat 類型 或者std::vector。
- 第二個參數,OutputArray類型的hull,輸出參數,函數調用后找到的凸包。
- 第三個參數,bool 類型的clockwise,操作方向標識符。當此標識符為真時, 輸出的凸包為順時針方向。否則,就為逆時針方向。并且是假定坐標系的 x 軸指向右,y 軸指向上方。
- 第四個參數,bool 類型的returnPoints,操作標志符,默認值true。當 標 志 符為真時,函數返回各凸包的各個點。否則,它返回凸包各點的指數。當 輸出數組是std::vector 時,此標志被忽略。
8.2.3 基礎示例程序:凸包檢測基礎
為了理解凸包檢測的運用方法,下面放出一個完整的示例程序。程序中會首 先隨機生成3~103個坐標值隨機的彩色點,然后利用convexHull, 對由這些點鏈 接起來的圖形求凸包。
void Test54() {Mat image(600, 600, CV_8UC3);RNG& rng = theRNG();while (1) {char key;int count = (unsigned)rng % 100 + 3;std::vector<Point>points;//隨機生成坐標for (int i = 0; i < count; ++i) {Point point;point.x = rng.uniform(image.cols / 4, image.cols * 3 / 4);point.y = rng.uniform(image.rows / 4, image.rows * 3 / 4);points.push_back(point);}//檢測凸包std::vector<int>hull;convexHull(Mat(points), hull, true);//繪制出隨機顏色的點image = Scalar::all(0);for (int i = 0; i < count; ++i) {circle(image, points[i], 3, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),FILLED,LINE_AA);}int hullcount = hull.size();Point point0 = points[hull.back()];//依次連接凸包的點for (int i = 0; i < hullcount; ++i) {Point point = points[hull[i]];line(image, point0, point, Scalar(255, 255, 255), 2, LINE_AA);point0 = point;}imshow("hull", image);key = waitKey(0);if (key == 27) break;}}
8.2.4 綜合示例程序:尋找和繪制物體的凸包
這一節的綜合示例程序,依然是結合滑動條,通過滑動條控制閾值,來得到 不同的凸包檢測效果圖。程序詳細注釋的源代碼如下。
namespace test55 {Mat g_srcImage, g_grayImage;int g_nThresh = 50;int g_maxThresh = 255;RNG g_rng(12345);Mat srcImage_copy = g_srcImage.clone();Mat g_thresholdImage_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;void on_ThreshChange(int, void*) {// 二值化threshold(g_grayImage, g_thresholdImage_output, g_nThresh, 255, THRESH_BINARY);findContours(g_thresholdImage_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //輪廓std::vector<std::vector<Point>>hull(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {convexHull(Mat(g_vContours[i]), hull[i], false);}//繪制出輪廓及凸包Mat drawing = Mat::zeros(g_thresholdImage_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)); drawContours(drawing, g_vContours, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //輪廓drawContours(drawing, hull, i, color, 1, 8, std::vector<Vec4i>(), 0, Point()); //凸包}imshow("drawing", drawing);}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3));//降噪//創建窗口namedWindow("src");imshow("src", g_srcImage);//創建滾動條createTrackbar("value", "src", &g_nThresh, g_maxThresh, on_ThreshChange);on_ThreshChange(0, nullptr);waitKey(0);}
}void Test55() {test55::Test();
}
8.3 使用多邊形將輪廓包圍
在實際應用中,常常會有將檢測到的輪廓用多邊形表示出來的需求。本節就 為大家講解如何用多邊形來表示出輪廓,或者說如何根據輪廓提取出多邊形。先 讓我們一起學習用OpenCV 創建包圍輪廓的多邊形邊界時會接觸到的一些函數。
8.3.1 返回外部矩形邊界:boundingRect(O函數
此函數計算并返回指定點集最外面(up-right)的矩形邊界。 C++:Rect boundingRect(InputArray points)
其唯一的一個參數為輸入的二維點集,可以是std::vector 或 Mat 類型。
8.3.2 尋找最小包圍矩形:minAreaRect(函數
此函數用于對給定的2D 點集,尋找可旋轉的最小面積的包圍矩形。 C++:RotatedRect minAreaRect(InputArray points)
其唯一的一個參數為輸入的二維點集,可以為std::vector> 或 Mat 類型。
8.3.3尋找最小包圍圓形:minEnclosingCircle(函數
minEnclosingCircle函數的功能是利用一種迭代算法,對給定的2D 點集,去 尋找面積最小的可包圍它們的圓形。
void minEnclosingCircle(InputArray points, Point2f ¢er, float &radius)
- 第 一 個 參 數 ,InputArray 類 型 的points,輸入的二維點集,可以為std::vector◇ 或Mat 類型。
- 第二個參數,Point2f&類型的center,圓的輸出圓心。
- 第三個參數,float&類型的radius,圓的輸出半徑。
8.3.4 用橢圓擬合二維點集:fitEllipse ( 函 數
此函數的作用是用橢圓擬合二維點集。
RotatedRect fitEllipse(InputArray points)
其唯一的一個參數為輸入的二維點集,可以為std::vector<>或Mat 類型。
8.3.5 逼近多邊形曲線: approxPolyDPO 函 數
approxPolyDP函數的作用是用指定精度逼近多邊形曲線。
void approxPolyDP(InputArray curve, OutputArray approxCurve,double epsilon, bool closed)
- 第一個參數,InputArray類型的curve,輸入的二維點集,可以為std::vecto 或Mat 類型。
- 第二個參數,OutputArray類型的approxCurve,多邊形逼近的結果,其類 型應該和輸入的二維點集的類型 一 致。
- 第三個參數,double類型的epsilon,逼近的精度,為原始曲線和即近似曲 線間的最大值。
- 第四個參數,bool 類型的closed,如果其為真,則近似的曲線為封閉曲線 (第一個頂點和最后一個頂點相連),否則,近似的曲線曲線不封閉。
8.4 圖像的矩
矩函數在圖像分析中有著廣泛的應用,如模式識別、目標分類、目標識別與 方位估計、圖像編碼與重構等。一個從一幅數字圖形中計算出來的矩集,通常描 述了該圖像形狀的全局特征,并提供了大量的關于該圖像不同類型的幾何特性信 息,比如大小、位置、方向及形狀等。圖像矩的這種特性描述能力被廣泛地應用 在各種圖像處理、計算機視覺和機器人技術領域的目標識別與方位估計中。一階 矩與形狀有關,二階矩顯示曲線圍繞直線平均值的擴展程度,三階矩則是關于平 均值的對稱性的測量。由二階矩和三階矩可以導出一組共7個不變矩。而不變矩 是圖像的統計特性,滿足平移、伸縮、旋轉均不變的不變性,在圖像識別領域得 到了廣泛的應用。
那 么 , 在OpenCV 中,如何計算 一個圖像的矩呢? 一般由 moments、 contourArea 、arcLength 這三個函數配合求取。
- 使用moments 計算圖像所有的矩(最高到3階)
- 使用contourArea來計算輪廓面積
- 使用arcLength來計算輪廓或曲線長度 下面對其進行一一剖析。
8.4.1 矩的計算:momentsO函數
moments()函數用于計算多邊形和光柵形狀的最高達三階的所有矩。矩用來計 算形狀的重心、面積,主軸和其他形狀特征,如7Hu不變量等。
Moments moments(InputArray array, bool binaryImage = false)
- 第一個參數,InputArray類型的array,輸入參數,可以是光柵圖像(單通 道,8位或浮點的二維數組)或二維數組 (IN 或 N1)。
- 第二個參數,bool類型的binaryImage,有默認值false。若此參數取 true, 則所有非零像素為1。此參數僅對于圖像使用。
需要注意的是,此參數的返回值返回運行后的結果。
8.4.2計算輪廓面積:contourArea(函數
contourArea()函數用于計算整個輪廓或部分輪廓的面積
double contourArea(InputArray contour,bool oriented=false)
- 第一個參數,InputArray類型的contour,輸入的向量,二維點(輪廓頂點), 可以為std::vector 或 Mat 類型。
- 第二個參數,bool類型的oriented,面向區域標識符。若其為true,該函數 返回一個帶符號的面積值,其正負取決于輪廓的方向(順時針還是逆時針)。 根據這個特性我們可以根據面積的符號來確定輪廓的位置。需要注意的是, 這個參數有默認值false, 表示以絕對值返回,不帶符號。
8.4.3 計算輪廓長度:arcLengthO函數
arcLength(函數用于計算封閉輪廓的周長或曲線的長度。
double arcLength(InputArray curve,bool closed)
- 第一個參數,InputArray類型的curve,輸入的二維點集,可以為std:vector或Mat 類型。
- 第二個參數,bool 類型的closed, 一個用于指示曲線是否封閉的標識符, 有默認值closed, 表示曲線封閉。
8.4.4 綜合示例程序:查找和繪制圖像輪廓矩
學習完函數的講解,讓我們一起通過一個綜合的示例程序,真正了解本節內 容的實戰用法 。
namespace test56{Mat g_srcImage, g_grayImage;int g_nThresh = 100;int g_nMaxThresh = 255;RNG g_rng(12345);Mat g_cannyMat_output;std::vector<std::vector<Point>>g_vContours;std::vector<Vec4i>g_vHierarchy;//回調函數void on_ThreshChange(int, void*) {Canny(g_grayImage, g_cannyMat_output, g_nThresh, g_nThresh * 2); //邊緣檢測findContours(g_cannyMat_output, g_vContours, g_vHierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); //找到輪廓//計算矩std::vector<Moments>mu(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {mu[i] = moments(g_vContours[i], false);}//計算中心矩std::vector<Point2f>mc(g_vContours.size());for (int i = 0; i < g_vContours.size(); ++i) {mc[i] = Point2f(static_cast<float>(mu[i].m10 / mu[i].m00), static_cast<float>(mu[i].m01 / mu[i].m00));}//繪制輪廓Mat drawing = Mat::zeros(g_cannyMat_output.size(), CV_8UC3);for (int i = 0; i < g_vContours.size(); ++i) {Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point()); //繪制內層和外層輪廓circle(drawing, mc[i], 4, color, -1, 8, 0);//繪制圓}imshow("drawing", drawing);//計算輪廓面積std::cout << "\t";for (int i = 0; i < g_vContours.size(); ++i) {std::cout << ">Through m00 Areas[" << i << "]:" << mu[i].m00 << "Length = "<< arcLength(g_vContours[i], true)<< std::endl;std::cout << ">Through OpenCV m_00 Areas[" << i << "]" <<contourArea(g_vContours[i]) <<"Length = " << arcLength(g_vContours[i], true) << std::endl;Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));drawContours(drawing, g_vContours, i, color, 2, 8, g_vHierarchy, 0, Point());circle(drawing, mc[i], 4, color, -1, 8, 0);}}void Test() {g_srcImage = imread("image.jpg");cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);blur(g_grayImage, g_grayImage, Size(3, 3));namedWindow("srcImage");imshow("srcImage", g_srcImage);createTrackbar("value:", "srcImage", &g_nThresh, g_nMaxThresh,on_ThreshChange);waitKey(0);}}void Test56() {test56::Test();
}
8.5 分水嶺算法
在許多實際運用中,我們需要分割圖像,但無法從背景圖像中獲得有用信 息。分水嶺算法 (watershed algorithm) 在這方面往往是非常有效的。此算法可 以將圖像中的邊緣轉化成“山脈”,將均勻區域轉化為“山谷”,這樣有助于分 割目標。
分水嶺算法,是一種基于拓撲理論的數學形態學的分割方法,其基本思想 是把圖像看作是測地學上的拓撲地貌,圖像中每一點像素的灰度值表示該點的 海拔高度,每一個局部極小值及其影響區域稱為集水盆,而集水盆的邊界則形 成分水嶺。分水嶺的概念和形成可以通過模擬浸入過程來說明:在每一個局部 極小值表面,刺穿一個小孔,然后把整個模型慢慢浸入水中,隨著浸入的加深, 每一個局部極小值的影響域慢慢向外擴展,在兩個集水盆匯合處構筑大壩,即 形成分水嶺。
分水嶺的計算過程是一個迭代標注過程。分水嶺比較經典的計算方法是由 L.Vincent 提出的。在該算法中,分水嶺計算分兩個步驟: 一個是排序過程, 一個是淹沒過程。首先對每個像素的灰度級進行從低到高的排序,然后在從低 到高實現淹沒的過程中,對每一個局部極小值在h 階高度的影響域采用先進先 出 (FIFO) 結構進行判斷及標注。分水嶺變換得到的是輸入圖像的集水盆圖像, 集水盆之間的邊界點,即為分水嶺。顯然,分水嶺表示的是輸入圖像的極大值 點。
也就是說,分水嶺算法首先計算灰度圖像的梯度;這對圖像中的“山谷”或 沒有紋理的“盆地”(亮度值低的點)的形成是很有效的,也對“山頭”或圖像中 有主導線段的“山脈”(山脊對應的邊緣)的形成有效。然后開始從用戶指定點(或 者算法得到點)開始持續“灌注”盆地直到這些區域連成一片。基于這樣產生的 標記就可以把區域合并到0一起,合并后的區域又通聚集的方式進行分割,好像 圖像被“填充”起來一樣。
8.5.1 實現分水嶺算法:watershedO 函 數
函 數watershed 實現的分水嶺算法是基于標記的分割算法中的 一種。在把圖像 傳給函數之前,我們需要大致勾畫標記出圖像中的期望進行分割的區域,它們被 標記為正指數。所以,每 一 個區域都會被標記為像素值1、2、3等,表示成為 一 個或者多個連接組件。這些標記的值可以使用findContours() 函 數 和drawContours()
函數由二進制的掩碼檢索出來。不難理解,這些標記就是即將繪制出來的分割區 域的“種子”,而沒有標記清楚的區域,被置為0。在函數輸出中,每 一 個標記中 的像素被設置為“種子”的值,而區域間的值被設置為- 1。
void watershed(InputArray image, InputOutputArray markers)
- 第 一 個 參 數 ,InputArray 類 型 的src, 輸入圖像,即源圖像,填Mat 類 的 對 象
即可,且需為8位三通道的彩色圖像。 - 第 二 個 參 數 ,InputOutputArray 類 型 的markers, 函數調用后的運算結果存在 這里,輸入/輸出32位單通道圖像的標記結果。即這個參數用于存放函數調用后 的輸出結果,需和源圖片有 一 樣的尺寸和類型。
8.5.2 綜合示例程序:分水嶺算法
namespace test57 {Mat g_maskImage, g_srcImage;Point prevPt(-1, -1);static void on_Mouse(int event, int x, int y, int flags, void*) {if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {return;}if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {prevPt = Point(-1, -1);}else if (event == EVENT_LBUTTONDOWN) {prevPt = Point(x, y);}else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {Point pt(x, y);if (prevPt.x < 0) {prevPt = pt;}line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0);prevPt = pt;imshow("srcImage", g_srcImage);}}void Test() {g_srcImage = imread("mountain.jpg");imshow("srcImage", g_srcImage);Mat srcImage, grayImage;g_srcImage.copyTo(srcImage);cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);g_maskImage = Scalar::all(0);setMouseCallback("srcImage", on_Mouse, 0);while (1) {int c = waitKey(1); // 改為 1 來更流暢地顯示if (c == 27) {break;}//恢復原圖if ((char)c == '2') {g_maskImage = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow("srcImage", g_srcImage);}else if ((char)c == '1') {int i, j, compCount = 0;std::vector<std::vector<Point>>contours;std::vector<Vec4i>hierarchy;//尋找輪廓findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);if (contours.empty()) {continue;}Mat maskImage(g_maskImage.size(), CV_32S); //復制掩膜maskImage = Scalar::all(0);for (int index = 0; index >= 0; index = hierarchy[index][0]) {compCount++;drawContours(maskImage, contours, index, Scalar::all(compCount), -1, 8, hierarchy, INT_MAX);}if (compCount == 0) continue;//生成隨機顏色std::vector<Vec3b>colorTab;for (int i = 0; i < compCount; ++i) {uchar b = theRNG().uniform(0, 255);uchar g = theRNG().uniform(0, 255);uchar r = theRNG().uniform(0, 255);colorTab.push_back(Vec3b(b, g, r));}watershed(srcImage, maskImage); //分水嶺算法//將分水嶺圖像遍歷存入watershedImage中Mat watershedImage(maskImage.size(), CV_8UC3);for (int i = 0; i < maskImage.rows; ++i) {for (int j = 0; j < maskImage.cols; ++j) {int index = maskImage.at<int>(i, j);if (index == -1) {watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0); }else if (index <= 0 || index > compCount) {watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);}else {watershedImage.at<Vec3b>(i, j) = colorTab[index - 1];}}}//混合灰度圖和分水嶺效果圖watershedImage = watershedImage * 0.5 + grayImage * 0.5;imshow("watershed transform", watershedImage);}}}
}void Test57() {test57::Test();
}
8.6 圖像修補
在實際應用中,我們的圖像常常會被噪聲腐蝕,這些噪聲或者是鏡頭上的灰 塵或水滴,或者是舊照片的劃痕,或者由于圖像的部分本身已經損壞。而“圖像 修復”(Inpainting), 就是妙手回春,解決這些問題的良方。圖像修復技術簡單來說,就是利用那些已經被破壞區域的邊緣,即邊緣的顏色和結構,繁殖和混合到 損壞的圖像中,以達到圖像修補的目的。圖8.34~8.36就是示例程序截圖,演示 將圖像中的字跡移除的效果。
8.6.1 實現圖像修補:inpaint ( 函 數
在新版OpenCV 中,圖像修補技術由inpaint 函數實現,它可以用來從掃描的 照片中清除灰塵和劃痕,或者從靜態圖像或視頻中去除不需要的物體。其原型聲 明如下。
C++:void inpaint(InputArray src,InputArray inpaintMask,OutputArray
dst,double inpaintRadius,int flags)
- 第 一 個參數,InputArray類 型 的src, 輸入圖像,即源圖像,填Mat 類的對 象即可,且需為8位單通道或者三通道圖像。
- 第二個參數,InputArray類型的inpaintMask, 修復掩膜,為8位的單通道 圖像。其中的非零像素表示需要修補的區域。
- 第三個參數,OutputArray 類 型 的dst, 函數調用后的運算結果存在這里, 和源圖片有一樣的尺寸和類型。
- 第四個參數,double 類型的 inpaintRadius,需要修補的每個點的圓形鄰域, 為修復算法的參考半徑。
- 第五個參數,int 類型的flags,修補方法的標識符,可以是表8.4所示兩者 之一。
標識符 | 說明 |
---|---|
INPAINT_NS | 基于Navier-Stokes方程的方法 |
INPAINT_TELEA | Alexandru Telea方法 |
OpenCV2 中INPAINT_NS 和INPAINT_TELEA 標識符可以分別寫作CV_ INPAINT_NS 和 CV_INPAINT_TELEA
8.6.2 綜合示例程序:圖像修補
函數和概念講解完畢,下面我們依然是學習 一 個以本節所講內容為核心的示 例程序,將本節所學內容付諸實踐,融會貫通。此示例程序會先讓我們在圖像中 用鼠標繪制出白色的線條破壞圖像,然后按下鍵盤按鍵【1】或【SPACE】 進 行 圖 像修補操作。且如果對自己的繪制不夠滿意,可以按下鍵盤按鍵【2】恢復原始圖 像。
namespace test58 {Mat g_srcImage, inpaintMask,srcImage;Point previousPoint(-1, -1);static void On_Mouse(int event,int x,int y,int flags,void*) {if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows) {return;}if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) {previousPoint = Point(-1, -1);}else if (event == EVENT_LBUTTONDOWN) {previousPoint = Point(x, y);}else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {Point pt(x, y);if (previousPoint.x < 0) {previousPoint = pt;}line(inpaintMask, previousPoint, pt, Scalar::all(255), 5, 8, 0);line(g_srcImage, previousPoint, pt, Scalar::all(255), 5, 8, 0);previousPoint = pt;imshow("srcImage", g_srcImage);}}void Test() {g_srcImage = imread("sky.jpg");srcImage = g_srcImage.clone();inpaintMask = Mat::zeros(srcImage.size(), CV_8U);imshow("srcImage", g_srcImage);setMouseCallback("srcImage", On_Mouse, nullptr);while (1) {char c = waitKey(1);if (c == 27) break;if (c == '2') {inpaintMask = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow("srcImage", g_srcImage);}if (c == '1') {Mat inpaintedImage;inpaint(g_srcImage, inpaintMask, inpaintedImage, 3, INPAINT_TELEA); imshow("fixed", inpaintedImage);}}}}void Test58() {test58::Test();
}
8.7 本章小結
本章中,我們先學習了查找輪并繪制輪廓,然后學習了如何尋找到物體的凸 包,接著是使用多邊形來包圍輪廓,以及計算一個圖像的矩。在本章后面幾節, 還學習了分水嶺算法和圖像修補操作的實現方法。
函數名稱 | 說明 | 對應講解章節 |
---|---|---|
BoundingRect | 計算并返回指定點集最外面(up-right)的矩形邊 界 | 8.3.1 |
minAreaRect | 尋找可旋轉的最小面積的包圍矩形 | 8.3.2 |
minEnclosingCircle | 利用一種迭代算法,對給定的2D點集,尋找面 積最小的可包圍他們的圓形 | 8.3.3 |
fitEllipse | 用橢圓擬合二維點集 | 8.3.4 |
approxPolyDP | 用指定精度逼近多邊形曲線 | 8.3.5 |
moments | 計算多邊形和光柵形狀的最高達三階的所有矩 | 8.4.1 |
contourArea | 計算整個輪廓或部分輪廓的面積 | 8.4.2 |
arcLength | 計算封閉輪廓的周長或曲線的長度 | 8.4.3 |
watershed | 實現分水嶺算法 | 8.5.1 |
inpaint | 進行圖像修補,從掃描的照片中清除灰塵和劃痕, 或者從靜態圖像或視頻中去除不需要的物體 | 8.6.1 |
本章示例程序清單
示例程序序號 | 程序說明 | 對應章節 |
---|---|---|
69 | 輪廓查找 | 8.1.3 |
70 | 查找并繪制輪廓 | 8.1.4 |
71 | 凸包檢測基礎 | 8.2.3 |
72 | 尋找和繪制物體的凸包 | 8.2.4 |
73 | 創建包圍輪廓的矩形邊界 | 8.3.6 |
74 | 創建包圍輪廓的圓形邊界 | 8.3.7 |
75 | 使用多邊形包圍輪廓 | 8.3.8 |
76 | 圖像輪廓矩 | 8.4.4 |
77 | 分水嶺算法的使用 | 8.5.2 |
78 | 實現圖像修補 | 8.6.2 |