opencv——輪廓發現與輪廓(二值影像)分析

唯有自己強大發表於2021-05-22

引言

二值影像分析最常見的一個主要方式就是輪廓發現輪廓分析,其中輪廓發現的目的是為輪廓分析做準備,經過輪廓分析我們可以得到輪廓各種有用的屬性資訊。

這裡順帶提下邊緣檢測,和輪廓提取的區別:

邊緣檢測主要是通過一些手段檢測數字影像中明暗變化劇烈(即梯度變化比較大)畫素點,偏向於影像中畫素點的變化。如canny邊緣檢測,結果通常儲存在和源圖片一樣尺寸和型別的邊緣圖中。 
輪廓檢測指檢測影像中的物件邊界,更偏向於關注上層語義物件。如OpenCV中的findContours()函式, 它會得到每一個輪廓並以點向量方式儲存,除此也得到一個影像的拓撲資訊,即一個輪廓的後一個輪廓、前一個輪廓、父輪廓和內嵌輪廓的索引編號。 


一,輪廓的發現與繪製


 在OpenCV裡面利用findContours()函式和drawContours()函式實現這一功能。

  • findContours()函式
void findContours(
InputArray          image,
OutputArrayOfArrays contours,
OutputArray         hierarchy,
int                 mode,
int                 method,
Point               offset = Point()
                  )

引數一: image,輸入影像、八位單通道的,背景為黑色的二值影像。(一般是經過Canny、拉普拉斯等邊緣檢測運算元處理過的二值影像)

引數二:contours,輸出輪廓影像。是一個向量,向量的每個元素都是一個輪廓。因此,這個向量的每個元素仍是一個向量。即:

           vector<vector<Point> > contours;

引數三:hierarchy,輸出各個輪廓的繼承關係。hierarchy也是一個向量,長度和contours相等,每個元素和contours的元素對應。hierarchy的每個元素是一個包含四個整型數的向量。即:

           vector<Vec4i> hierarchy;

引數四:mode,檢測輪廓的方法。有四種方法:

  1. RETR_EXTERNAL:只檢測外輪廓。忽略輪廓內部的洞。
  2. RETR_LIST:檢測所有輪廓,但不建立繼承(包含)關係。
  3. RETR_TREE:檢測所有輪廓,並且建立所有的繼承(包含)關係。
  4. RETR_CCOMP:檢測所有輪廓,但是僅僅建立兩層包含關係。

引數五:method,每個輪廓的編碼資訊。也有四種(常用前兩種)

  1. CHAIN_APPROX_NONE:把輪廓上所有的點儲存。
  2. CHAIN_APPROX_SIMPLE:只儲存輪廓上的拐點。
  3. CHAIN_APPROX_TC89_L1,CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似演算法

引數六: Point,偏移量。預設為0

注意該函式將白色區域當作前景物體。所以findContours()函式是黑色背景下找白色輪廓。(重要!!!)

  • drawContours()函式
drawContours(
      InputOutputArray  binImg, // 輸出影像
      OutputArrayOfArrays  contours,//  全部發現的輪廓物件
      Int contourIdx// 輪廓索引號,-1表示繪製所有輪廓
      const Scalar & color,// 繪製時候顏色
      int  thickness,// 繪製線寬,-1表示填充輪廓內部
      int  lineType,// 線的型別LINE_8
      InputArray hierarchy,// 拓撲結構圖
      int maxlevel,// 最大層數, 0只繪製當前的,1表示繪製繪製當前及其內嵌的輪廓
      Point offset = Point()// 輪廓位移,可選

二,輪廓分析(二值影像分析)


 在得到影像的輪廓以後,我們就可以進行輪廓分析。經過輪廓分析我們可以得到輪廓各種有用的屬性資訊、常見的如下: 

  • ?計算輪廓面積 : 

contourArea(contour, oriented = False)  //計算輪廓的面積

引數說明:contour為輸入的單個輪廓值;oriented:輪廓方向,預設值false。
如果為true,該函式返回一個帶符號的面積,其正負取決於輪廓的方向(順時針還是逆時針)。
如果是預設值false,則面積以絕對值的形式返回.
根據這個特性可以根據面積的符號來確定輪廓的位置。
  • ?計算輪廓周長:
arcLength(contour, closed)   //  計算輪廓的周長

引數說明:contour為輸入的單個輪廓值,closed表示輪廓是否封閉(true為封閉,false為不封閉)
  • ?計算幾何矩與中心距: moments()
  Moments m = moments(contours[t]); //獲取輪廓的距
//計算輪廓質心
    double cx = m.m10 / m.m00;
    double cy = m.m01 / m.m00;
  • ?輪廓的外接矩形:

輪廓的外接矩形有兩種,如下圖,綠色的叫外接矩形boundingRect(),表示不考慮旋轉並且能包含整個輪廓的矩形。藍色的叫最小外接矩形minAreaRect(),考慮了旋轉

1️⃣外接矩形Rect boundingRect(InputArray points)

輸入引數points可以一系列點的集合,對輪廓來說就是該輪廓的點集 返回結果是一個正矩形,包含以下資訊:

  • 矩形左上角的座標(rect.x,rect.y)
  • 矩形的寬和高(rect.width,rect.height)
Rect  rect = boundingRect(Mat(contours[i]));//獲取輪廓外接正矩形
rectangle(src, rect, (0, 0, 255), 2, 8, 0);

2️⃣最小外接矩形minAreaRect()

輸入引數points可以一系列點的集合,對輪廓來說就是該輪廓的點集 返回結果是一個旋轉矩形,包含下面的資訊:

  • 旋轉矩形的中心座標(rect.center)
  • 旋轉矩形的寬和高(rect.size.width,rect.size.height)
  • 旋轉矩形的角度(rect.angle)
RotatedRect rect = minAreaRect(contours[i]);//獲取輪廓最小外接矩形
Point2f P[4];
rect.points(P);//獲取四頂點座標
for (int j = 0; j <= 3; j++)
{
    line(src, P[j], P[(j + 1) % 4], Scalar(0,0,255), 1);//依次連線
}
  • ?最小外接圓/擬合圓:minEnclosingCircle()
void minEnclosingCircle(InputArray points, Point2f& center, float& radius);
points,輸入的二維點集,可以是 vector 或 Mat 型別。
center,圓的輸出圓心。
radius,圓的輸出半徑。
 
例如:
    findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
    //尋找包裹輪廓的最小圓
    vector<Point2f>centers(contours.size());//圓心個數
    vector<float>radius(contours.size());//半徑個數
    for (int i = 0; i < contours.size(); i++) 
{
//尋找並繪製最小圓 minEnclosingCircle(contours[i], centers[i], radius[i]); circle(src, centers[i], radius[i], scalar(0,0,255), 2); }
  • ?擬合橢圓:fitEllipse() 
RotatedRect fitEllipse(InputArray points);
//唯一一個引數是輸入的二維點集,可以是 vector 或 Mat 型別。
例如:
// 輪廓發現與繪製
vector<vector<Point>> contours;
findContours(binary, contours,  RETR_EXTERNAL, CHAIN_APPROX_NONE, Point());
for (size_t t = 0; t < contours.size(); t++)
 {
// 擬合橢圓
RotatedRect rrt = fitEllipse(contours[t]);
ellipse(src, rrt, Scalar(0, 0, 255), 2, 8);
}
imshow("contours", src);
  • ?擬合直線:fitLine()

OpenCV中直線擬合正是基於最小二乘法實現的。其函式將計算出的直線資訊存放在 line 中,(為Vec4f 型別)。line[0]、line[1] 存放的是直線的方向向量,float cosθ = oneline[0]; float sinθ = oneline[1]。line[2]、line[3] 存放的是直線上一個點的座標。

實現直線擬合的API如下:

void   fitLine(
    InputArray points,  //輸入待擬合的二維點的陣列或vector
    OutputArray line,   //輸出直線,Vec4f (2d)或Vec6f (3d)的vector
    int distType,          //距離型別
    double param,      //距離引數(一般設為0)
    double reps,        //徑向的精度引數(一般設為0.01)
    double aeps        //角度精度引數(一般設為0.01)
              )
distType(距離型別)有六種引數:(DIST_L2就是最小二乘法)

opencv實現:

Mat src = imread("D:/opencv練習圖片/直線擬合.png");
    imshow("原圖片", src);
    // 去噪聲與二值化
    Mat dst, gray, binary;
    Canny(src, binary, 80, 160, 3, false);
    imshow("canny二值化", binary);
    Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    dilate(binary, binary, k);
    // 輪廓發現與繪製
    vector<vector<Point>> contours;
    findContours(binary, contours,  RETR_EXTERNAL, CHAIN_APPROX_NONE, Point());
    for (size_t t = 0; t < contours.size(); t++) {
        // 最大外接輪廓
        Rect rect = boundingRect(contours[t]);
        int m = max(rect.width, rect.height);
        if (m < 30) continue;
        // 直線擬合
        Vec4f oneline;
        fitLine(contours[t], oneline, DIST_L1, 0, 0.01, 0.01);
        float cosθ = oneline[0];
        float sinθ = oneline[1];
        float x0 = oneline[2];
        float y0 = oneline[3];

        // 直線引數斜率k與截矩b
        float k = sinθ / cosθ; //求tanθ,也就是斜率
        float b = y0 - k * x0;

        float x = 0;
        float y = k * x + b;
        line(src, Point(x0, y0), Point(x, y), Scalar(0, 0, 255), 2, 8, 0);
    }
    imshow("結果", src);

  • ?輪廓的凸包:convexHull()

凸包(Convex Hull)是一個計算幾何(圖形學)中常見的概念。簡單來說,給定二維平面上的點集,凸包就是將最外層的點連線起來構成的凸多邊形,它是能包含點集中所有點的。

理解物體形狀或輪廓的一種比較有用的方法便是計算一個物體的凸包,然後計算其凸缺陷(convexity defects)。

convexHull (   InputArray  points,         /輸入的二維點集,Mat型別資料即可
               OutputArray     hull,       //輸出引數,用於輸出找到的凸包
               bool   clockwise = false,   //操作方向,為True時,輸出的凸包為順時針方向,否則為逆時針方向
               bool  returnPoints = true   //凸包的返回形式,預設值為true,此時返回點座標的形式,否則返回對應點的索引值
            )

凸包檢測原理:

 opencv實現:

    Mat src = imread("D:/opencv練習圖片/凸包檢測.jpg");
    imshow("原圖片", src);
    // 二值化
    Mat dst, gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
    // 形態學去除干擾
    Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(binary, binary, MORPH_OPEN, k);
    imshow("binary", binary);
    // 輪廓發現與繪製
    vector<vector<Point>> contours;
    findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point());
    for (size_t t = 0; t < contours.size(); t++) {
        vector<Point> hull;
        convexHull(contours[t], hull);//凸包檢測
        bool isHull = isContourConvex(contours[t]);//判斷輪廓是否為凸包
        printf("test convex of the contours %s", isHull ? "Y" : "N");
        int len = hull.size();
        //繪製凸包
        for (int i = 0; i < hull.size(); i++) {
            circle(src, hull[i], 4, Scalar(255, 0, 0), 2, 8, 0);//
            line(src, hull[i%len], hull[(i + 1) % len], Scalar(0, 0, 255), 2, 8, 0);//
        }
    }
    imshow("凸包檢測", src);

  • ?多邊形逼近-逼近真實形狀:approxPolyDP()

輪廓的多邊形逼近指的是:使用多邊形來近似表示一個輪廓。 多邊形逼近的目的是為了減少輪廓的頂點數目。 多邊形逼近的結果依然是一個輪廓,只是這個輪廓相對要粗曠一些。 

void approxPolyDP(        InputArray curve,        //輸入曲線,一般是由影像的輪廓點組成的點集
                          OutputArray approxCurve, //表示輸出的逼近後多邊形的點集(型別與輸入曲線的型別相同)
                          double epsilon,          //輪廓逼近的頂點距離真實輪廓曲線的最大距離,該值越小表示越逼近真實輪廓
                          bool closed              //表示輸出的多邊形是否封閉
                  )

 opencv實現:

    Mat src = imread("D:/opencv練習圖片/多邊形逼近.png");
    Mat dstImage_3(src.size(), CV_8UC3, Scalar(0));
    Mat dstImage_6(src.size(), CV_8UC3, Scalar(0));
    Mat dstImage_10(src.size(), CV_8UC3, Scalar(0));
    imshow("原圖片", src);
    // 二值化
    Mat dst, gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
    // 形態學去除干擾
    Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(binary, binary, MORPH_OPEN, k);
    imshow("binary", binary);
    // 輪廓發現與繪製
    vector<vector<Point>> contours;
    findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point());
    vector<vector<Point>> contours_poly(contours.size());
    for (int i = 0; i < contours.size(); i++)
    {
        //epsilon==3
        approxPolyDP(Mat(contours[i]), contours_poly[i], 3, true);
        drawContours(dstImage_3, contours_poly, i, Scalar(230,130,255), 1, LINE_AA);
        //epsilon==6
        approxPolyDP(Mat(contours[i]), contours_poly[i], 6, true);
        drawContours(dstImage_6, contours_poly, i, Scalar(255,255,160), 1, LINE_AA);
        //epsilon==10
        approxPolyDP(Mat(contours[i]), contours_poly[i], 10, true);
        drawContours(dstImage_10, contours_poly, i, Scalar(175, 255, 255), 1, LINE_AA);

    }
    imshow("epsilon=3", dstImage_3);
    imshow("epsilon=6", dstImage_6);
    imshow("epsilon=10", dstImage_10);

 從以上結果可以看出,設定的精度epsilon越小,多邊形越擬合。

  • ?檢測點是否在輪廓內pointPolygonTest()

OpenCV中實現這個功能的API叫做點多邊形測試,它可以準確的得到一個點距離多邊形的距離,如果點是輪廓點或者屬於輪廓多邊形上的點,距離是零,如果是多邊形內部的點是是正數,如果是負數返回表示點是外部。

利用這個特性,我們可以巧妙的獲取輪廓最大內接圓的半徑:

當這個點在輪廓內部(與輪廓距離為正數),其返回的距離是最大值的時候,這個距離就是輪廓的最大內接圓的半徑,該點就是最大內接圓的圓心。這樣我們就巧妙的獲得了圓心的位置與半徑,剩下的工作就很容易了完成,繪製一個圓而已,一行程式碼就可以搞定。

double pointPolygonTest(
    InputArray contour,         //輸入輪廓點集合
    Point2f pt,                 //輸入影像上任一點
    bool measureDist MeasureDist//如果是True,則返回每個點到輪廓的距離,如果是False則返回+1,0,-1三個值,其中+1表示點在輪廓內部,0表示點在輪廓上,-1表示點在輪廓外

 opencv實現:(繪製輪廓的最大內接圓和最小外接圓)

Mat src = imread("D:/opencv練習圖片/影像最大內接圓.png");
    imshow("原圖片", src);
    // 二值化
    Mat dst, gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
    imshow("binary", binary);
    // 輪廓發現與繪製
    vector<vector<Point>> contours;
    findContours(binary, contours, RETR_LIST, CHAIN_APPROX_NONE);
    Mat  dist =Mat::zeros(src.size(), CV_32F);//定義一個Mat物件,存放原圖中每個點到該輪廓的距離,為浮點型資料
    int dist1 = 0;
    int maxdist = 0;
    Point center;
    //尋找最大內接圓引數
         //遍歷每個點,計算該點到輪廓距離
        for (int row = 0; row < dist.rows; row++)
        {
            for (int col = 0; col < dist.cols; col++)
            {
                //通過點多邊形檢測計算獲得點到輪廓距離,並存放至dist中
                dist1 = pointPolygonTest(contours[0], Point(col, row), true);

                if (dist1 > maxdist)
                {
                    maxdist = dist1;//找出dist1中的最大值
                    center = Point(col, row);//獲取最大值的座標
                }
            }
        }    

    //尋找最小外接圓引數
    vector<Point2f>centers(contours.size());//圓心個數
    vector<float>radius(contours.size());//半徑個數
    minEnclosingCircle(contours[1], centers[1], radius[1]);
    //繪製最小外接圓
    circle(src, centers[1], radius[1], Scalar(0, 0, 255), 2);    
    //繪製最大的內接圓
    circle(src, center, maxdist, Scalar(0, 255, 0), 1, LINE_8, 0);    
    imshow("src", src);

需要注意的是:

 1️⃣尋找輪廓的findContours的RETR_LIST引數是從裡向外找輪廓,因此內部輪廓的索引為contours[0],外輪廓索引為contours[1]。而RETR_TREE正好與之相反,從外向內找輪廓

 2️⃣實戰發現,在找尋dist1物件的最大值時,用minMaxLoc函式找到好多值。(目前未理解其中緣由)

相關文章