【OpenCV學習筆記 010】提取直線、輪廓及連通區域
一、Canny運算元檢測輪廓 (http://blog.csdn.net/davebobo/article/details/52583167)
1.概念及原理
(1)之前我們是對梯度大小進行閾值化以得到二值的邊緣影象。但是這樣做有兩個缺點。其一是檢測到的邊緣過粗,難以實現物體的準確定位。其二是很難找到合適的閾值既能足夠低於檢測到所有重要邊緣,又能不至於包含過多次要邊緣,這就是Canny演算法嘗試解決的問題。
(2)Canny運算元通常是基於Sobel運算元,當然也可以使用其他梯度運算元。其思想是使用一個低閾值一個高閾值來確定哪些點屬於輪廓。低閾值的作用主要是包括所有屬於明顯影象輪廓的邊緣畫素。高閾值的作用是定義所有重要輪廓的邊緣。
2.實驗
使用Canny運算元檢測輪廓
原始碼示例(很簡單)
- <pre name="code"class="cpp">#include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- using
- int main(){
- Mat image = imread("tree.jpg", 0);
- namedWindow("image");
- imshow("image", image);
- Mat contours;
- Canny(image, //灰度圖
- contours, //輸出輪廓
- 125, //低閾值
- 350); //高閾值
- //因為正常情況下輪廓是用非零畫素表示 我們反轉黑白值
- Mat contoursInv; //反轉後的影象
- threshold(contours,
- contoursInv,
- 128, //低於該值的畫素
- 255, //將變成255
- THRESH_BINARY_INV);
- namedWindow("contoursInv");
- imshow("contoursInv", contoursInv);
- waitKey(0);
- return 0;
- }
二、霍夫變換檢測直線
1.概念及原理
(1)霍夫變換是檢測直線的經典演算法,最初只用於檢測直線,後被擴充套件能夠檢測其他簡單結構。在霍夫變換中,直線用方程表示為:,p是指直線到影象原點(左上角)的距離,θ則是與直線垂直的角度。
(2)霍夫變換使用二維的累加器以統計特定的直線被識別了多少次。目的是找到二值影象中經過足夠多數量的點的所有直線,它分析每個單獨的畫素點,識別出所有可能經過它的直線,當同一條直線穿過許多點時,說明這條直線明顯的存在。
2.實驗
先用Canny運算元獲取影象輪廓,然後基於霍夫變換檢測直線。
原始碼示例
- #define _USE_MATH_DEFINES
- #include <math.h>
- #include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- usingnamespace cv;
- int main(){
- Mat image = imread("tree.jpg");
- namedWindow("image");
- imshow("image", image);
- Mat result;
- cvtColor(image, result, CV_BGR2GRAY);
- //應用Canny演算法
- Mat contours;
- Canny(result, //灰度圖
- contours, //輸出輪廓
- 125, //低閾值
- 350); //高閾值
- //Hough 變換檢測直線
- vector <Vec2f>lines;
- HoughLines(contours, //一幅邊緣影象
- lines, //代表檢測到的浮點數
- 1,M_PI / 180, // 步進尺寸
- 80); //最小投票數
- //繪製每條線
- vector<Vec2f>::const_iterator it = lines.begin();
- while (it!=lines.end())
- {
- float rho = (*it)[0]; //距離rho
- float theta = (*it)[1]; //角度theta
- if (theta<M_PI / 4. || theta>3.*M_PI / 4.) //垂直線
- {
- //線與第一行的交點
- Point pt1(rho / cos(theta), 0);
- //線與最後一行的交點
- Point pt2((rho - result.rows*sin(theta)) / cos(theta), result.rows);
- //繪製白線
- line(image, pt1, pt2, Scalar(255), 1);
- }
- else//水平線
- {
- //線與第一列的交點
- Point pt1(0, rho / sin(theta));
- //線與最後一列的交點
- Point pt2(result.cols, (rho - contours.cols*cos(theta)) / sin(theta));
- //繪製白線
- line(image, pt1, pt2, Scalar(255), 1);
- }
- ++it;
- }
- cvNamedWindow("hough");
- imshow("hough", image);
- waitKey(0);
- return 0;
- }
函式原型
- //! finds lines in the black-n-white image using the standard or pyramid Hough transform
- CV_EXPORTS_W void HoughLines( InputArray image, OutputArray lines,
- double rho, double theta, int threshold,
- double srn=0, double stn=0 );
第一個引數:一幅包含一組點的二值影象,通常是一幅邊緣影象,比如來自Canny運算元。
第二個引數:輸出Vec2f向量,每個元素元素都是代表檢測到的直線的浮點數(ρ,θ)。
第三、四個引數:直線搜尋時的步進尺寸
第五個引數:能夠被檢測為直線所需要的最小投票數目。
霍夫變換僅僅查詢邊緣的一種排列方式,因為意外的畫素排列或是多條線穿過同一組畫素很有可能帶來錯誤的檢測,所以對其演算法進行改進即概率霍夫變換,我們將其封裝到LineFinder類中進行使用。
程式原始碼
- #define _USE_MATH_DEFINES
- #include <math.h>
- #include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- usingnamespace cv;
- class LineFinder{
- private:
- Mat img; //原圖
- vector<Vec4i>lines; //向量中檢測到的直線的端點
- //累加器的解析度
- double deltaRho;
- double deltaTheta;
- int minVote; //直線被接受時所需的最小投票數
- double minLength; //直線的最小長度
- double maxGap; //沿著直線方向的最大缺口
- public:
- //預設的累加器的解析度為單個畫素即1 不設定缺口及最小長度的值
- LineFinder() :deltaRho(1), deltaTheta(M_PI / 180), minVote(10), minLength(0.), maxGap(0.){};
- //設定累加器的解析度
- void setAccResolution(double dRho, double dTheta){
- deltaRho = dRho;
- deltaTheta = dTheta;
- }
- //設定最小投票數
- void setMinVote(int minv){
- minVote = minv;
- }
- //設定缺口及最小長度
- void setLineLengthAndGap(double length, double gap){
- minLength = length;
- maxGap = gap;
- }
- //使用概率霍夫變換
- vector<Vec4i>findLines(Mat &binary){
- lines.clear();
- HoughLinesP(binary, lines, deltaRho, deltaTheta, minVote, minLength, maxGap);
- return lines;
- }
- //繪製檢測到的直線
- void drawDetectedLines(Mat &image,Scalar color = Scalar(255,255,255)){
- //畫線
- vector<Vec4i>::const_iterator it2 = lines.begin();
- while (it2!=lines.end())
- {
- Point pt1((*it2)[0],(*it2)[1]);
- Point pt2((*it2)[2], (*it2)[3]);
- line(image, pt1, pt2, color);
- ++it2;
- }
- }
- };
- int main(){
- Mat image = imread("tree.jpg");
- namedWindow("image");
- imshow("image", image);
- Mat result;
- cvtColor(image, result, CV_BGR2GRAY);
- //應用Canny演算法
- Mat contours;
- Canny(result, //灰度圖
- contours, //輸出輪廓
- 125, //低閾值
- 350); //高閾值
- //建立LineFinder例項
- LineFinder finder;
- //設定概率Hough引數
- finder.setLineLengthAndGap(100, 20);
- finder.setMinVote(80);
- //檢測並繪製直線
- vector<Vec4i>lines = finder.findLines(contours);
- finder.drawDetectedLines(image);
- cvNamedWindow("Detected Lines with HoughP");
- imshow("Detected Lines with HoughP", image);
- waitKey(0);
- return 0;
- }
三、直線擬合一組點
1.概念及原理
(1)Hough 變換可以提取影象中的直線。但是提取的直線的精度不高。而很多場合下,我們需要精確的估計直線的引數,這時就需要進行直線擬合。直線擬合的方法很多,比如一元線性迴歸就是一種最簡單的直線擬合方法。但是這種方法不適合用於提取影象中的直線。因為這種演算法假設每個資料點的X 座標是準確的,Y 座標是帶有高斯噪聲的。可實際上,影象中的每個資料點的XY 座標都是帶有噪聲的。Opencv通過最小化每個點到直線的距離之和進行求解,有多個距離函式, CV_DIST_L1 、CV_DIST_L2 、CV_DIST_C 、CV_DIST_L12、 CV_DIST_FAIR 、CV_DIST_WELSCH和 CV_DIST_HUBER ,其中最快的是歐式距離即CV_DIST_L2它對應的是標準的二乘法。
2.實驗
原始碼示例
- #define _USE_MATH_DEFINES
- #include <math.h>
- #include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- usingnamespace cv;
- int main(){
- Mat image = imread("tree.jpg");
- namedWindow("image");
- imshow("image", image);
- Mat result;
- cvtColor(image, result, CV_BGR2GRAY);
- //應用Canny演算法
- Mat contours;
- Canny(result, //灰度圖
- contours, //輸出輪廓
- 125, //低閾值
- 350); //高閾值
- //建立LineFinder例項
- LineFinder finder;
- //設定概率Hough引數
- finder.setLineLengthAndGap(100, 20);
- finder.setMinVote(80);
- //檢測並繪製直線
- vector<Vec4i>lines = finder.findLines(contours);
- int n = 0; //選擇line 0
- //黑色影象
- Mat oneline(contours.size(), CV_8U, Scalar(0));
- //白色直線
- line(oneline,
- Point(lines[n][0], lines[n][1]),
- Point(lines[n][2], lines[n][3]),
- Scalar(255),
- 5
- );
- //輪廓與白線進行AND操作
- bitwise_and(contours, oneline, oneline);
- Mat oneLineInv; //白色直線反轉後的影象
- threshold(oneline,
- oneLineInv,
- 128, //低於該值的畫素
- 255, //將變成255
- THRESH_BINARY_INV);
- cvNamedWindow("One line");
- imshow("One line", oneLineInv);
- //將指定直線相關的點置入cv::Points型別的std::vector中
- vector<Point>points;
- //遍歷畫素得到所有點的位置
- for (int y = 0; y < oneline.rows; y++)
- {
- //y行
- uchar *rowPtr = oneline.ptr<uchar>(y);
- for (int x = 0; x < oneline.cols; x++)
- {
- //x列
- //如果位於輪廓上
- if (rowPtr[x])
- {
- points.push_back(Point(x, y));
- }
- }
- }
- Vec4f lineVec;
- fitLine(Mat(points),
- lineVec,
- CV_DIST_L2, //距離型別
- 0, //L2距離不使用該引數
- 0.01,0.01); //精確值
- int x0 = lineVec[2]; //直線上的點
- int y0 = lineVec[3];
- int x1 = x0 - 200 * lineVec[0]; //使用單元向量
- int y1 = y0 - 200 * lineVec[1]; //新增長度為200的向量
- cv::line(result, Point(x0, y0), Point(x1, y1), Scalar(0), 3);
- cvNamedWindow("Estimated line");
- imshow("Estimated line", result);
- waitKey(0);
- return 0;
- }
實驗過程說明
(1)首先識別出可能排列成直線的點,即我們使用霍夫變換檢測到的一條直線。
(2)接著得到僅包含指定直線相關的點即oneline,然後將集合中的點放置在cv::Pointdes的vector中。
(3)呼叫cv::fitLine函式找到最合適的線。
fitLine函式原型及說明
- void fitLine( InputArray points,
- OutputArray line,
- int distType,
- double param,
- double reps,
- double aeps );
distType 指定擬合函式的型別,可以取 CV_DIST_L2、CV_DIST_L1、CV_DIST_L12、CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER。
param 就是 CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER 公式中的C。如果取 0,則程式自動選取合適的值。
reps 表示直線到原點距離的精度,建議取 0.01。
aeps 表示直線角度的精度,建議取 0.01。
四、提取連通區域的輪廓
1.概念及原理
(1)Opencv中提供了一個簡單的函式用於提取連通區域cv::findContours。它是通過系統的掃描影象直到遇到連通區域的一個點,以它為起始點,跟蹤它的輪廓,標記邊界上的元素,當輪廓完整閉合,掃描回到上一個位置,直到再次發現新的成分。
2.實驗
提取下圖的連通區域輪廓。
程式例項
- #include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- usingnamespace cv;
- int main(){
- Mat image = cvLoadImage("group.jpg");
- Mat grayImage;
- cvtColor(image, grayImage, CV_BGR2GRAY);
- //轉換為二值圖
- Mat binaryImage;
- threshold(grayImage, binaryImage, 90, 255, CV_THRESH_BINARY);
- //二值圖 這裡進行了畫素反轉,因為一般我們用255白色表示前景(物體),用0黑色表示背景
- Mat reverseBinaryImage;
- bitwise_not(binaryImage, reverseBinaryImage);
- vector <vector<Point>>contours;
- findContours(reverseBinaryImage,
- contours, //輪廓的陣列
- CV_RETR_EXTERNAL, //獲取外輪廓
- CV_CHAIN_APPROX_NONE); //獲取每個輪廓的每個畫素
- //在白色影象上繪製黑色輪廓
- Mat result(reverseBinaryImage.size(), CV_8U, Scalar(255));
- drawContours(result, contours,
- -1, //繪製所有輪廓
- Scalar(0), //顏色為黑色
- 2); //輪廓線的繪製寬度為2
- namedWindow("contours");
- imshow("contours", result);
- //移除過長或過短的輪廓
- int cmin = 100; //最小輪廓長度
- int cmax = 1000; //最大輪廓
- vector<vector<Point>>::const_iterator itc = contours.begin();
- while (itc!=contours.end())
- {
- if (itc->size() < cmin || itc->size() > cmax)
- itc = contours.erase(itc);
- else
- ++itc;
- }
- //在白色影象上繪製黑色輪廓
- Mat result_erase(binaryImage.size(), CV_8U, Scalar(255));
- drawContours(result_erase, contours,
- -1, //繪製所有輪廓
- Scalar(0), //顏色為黑色
- 2); //輪廓線的繪製寬度為2
- namedWindow("contours_erase");
- imshow("contours_erase", result_erase);
- waitKey(0);
- return 0;
- }
五、計算連通區域的形狀描述符
1.概念及原理
連通區域通常對應於場景中的某個物體,為了識別該物體或者將它與其他影象元素作比較,我們需要進行一些測量儀來獲取它的特徵,這裡我們就利用Opencv中可用的一些形狀描述符。
2.實驗
利用Opencv中的形狀描述符來描述上例提取到的輪廓。
原始碼示例
- #include<iostream>
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include<imgproc/imgproc.hpp>
- usingnamespace std;
- usingnamespace cv;
- int main(){
- Mat image = cvLoadImage("group.jpg");
- Mat grayImage;
- cvtColor(image, grayImage, CV_BGR2GRAY);
- //轉換為二值圖
- Mat binaryImage;
- threshold(grayImage, binaryImage, 90, 255, CV_THRESH_BINARY);
- //二值圖 這裡進行了畫素反轉,因為一般我們用255白色表示前景(物體),用0黑色表示背景
- Mat reverseBinaryImage;
- bitwise_not(binaryImage, reverseBinaryImage);
- vector <vector<Point>>contours;
- findContours(reverseBinaryImage,
- contours, //輪廓的陣列
- CV_RETR_EXTERNAL, //獲取外輪廓
- CV_CHAIN_APPROX_NONE); //獲取每個輪廓的每個畫素
- //在白色影象上繪製黑色輪廓
- Mat result(reverseBinaryImage.size(), CV_8U, Scalar(255));
- drawContours(result, contours,
- -1, //繪製所有輪廓
- Scalar(0), //顏色為黑色
- 2); //輪廓線的繪製寬度為2
- namedWindow("contours");
- imshow("contours", result);
- //移除過長或過短的輪廓
- int cmin = 100; //最小輪廓長度
- int cmax = 1000; //最大輪廓
- vector<vector<Point>>::const_iterator itc = contours.begin();
- while (itc!=contours.end())
- {
- if (itc->size() < cmin || itc->size() > cmax)
- itc = contours.erase(itc);
- else
- ++itc;
- }
- //在白色影象上繪製黑色輪廓
- Mat result_erase(binaryImage.size(), CV_8U, Scalar(255));
- drawContours(result_erase, contours,
- -1, //繪製所有輪廓
- Scalar(0), //顏色為黑色
- 2); //輪廓線的繪製寬度為2
- //namedWindow("contours_erase");
- //imshow("contours_erase", result_erase);
- //測試包圍盒
- Rect r0 = boundingRect(Mat(contours[0]));
- rectangle(result_erase, r0, Scalar(128), 2);
- Rect r1 = boundingRect(Mat(contours[1]));
- rectangle(result_erase, r1, Scalar(128), 2);
- //測試最小包圍圓
- float radius;
- Point2f center;
- minEnclosingCircle(Mat(contours[2]), center, radius);
- circle(result_erase, Point(center), static_cast<int>(radius), Scalar(128), 2);
- //測試多邊形近似
- vector <Point> poly;
- approxPolyDP(Mat(contours[3]),
- poly,
- 5, //近似的精確度
- true); //這是個閉合形狀
- //遍歷每個片段進行繪製
- vector<Point>::const_iterator itp = poly.begin();
- while (itp != (poly.end() - 1))
- {
- line(result_erase, *itp, *(itp + 1), Scalar(128), 2);
- ++itp;
- }
- //首尾用直線相連
- line(result_erase, *(poly.begin()), *(poly.end() - 1), Scalar(128), 2);
- //凸包是另一種多邊形近似,計算凸包
- vector <Point> hull;
- convexHull(Mat(contours[4]), hull);
- vector<Point>::const_iterator ith = hull.begin();
- while (ith != (hull.end() - 1))
- {
- line(result_erase, *ith, *(ith + 1), Scalar(128), 2);
- ++ith;
- }
- line(result_erase, *(hull.begin()), *(hull.end() - 1), Scalar(128), 2);
- //另一種強大的描述符力矩
- //測試力矩
- //遍歷所有輪廓
- itc = contours.begin();
- while (itc!=contours.end())
- {
- //計算所有的力矩
- Moments mom = moments(Mat(*itc++));
- //繪製質心
- circle(result_erase,
- Point(mom.m10 / mom.m00, mom.m01 / mom.m00), //質心座標轉換為整數
- 2,
- Scalar(0),
- 2); //繪製黑點
- }
- namedWindow("contours_erase");
- imshow("contours_erase", result_erase);
- waitKey(0);
- return 0;
- }
形狀描述符描述輪廓實驗結果