34、图像轮廓处理

发布时间 2023-04-11 15:26:51作者: 夏蝉沐雪

1、轮廓的发现与绘制

代码清单7-11 findContours()函数原型1
1.    void cv::findContours(InputArray  image,
2.                              OutputArrayOfArrays  contours,
3.                              OutputArray  hierarchy,
4.                              int  mode,
5.                              int  method,
6.                              Point  offset = Point() 
7.                              )
  • image:输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像。
  • contours:检测到的轮廓,每个轮廓中存放着像素的坐标。
  • hierarchy:轮廓结构关系描述向量。
  • mode:轮廓检测模式标志,可以选择的参数在表7-2给出。
  • method:轮廓逼近方法标志,可以选择的参数在表7-3给出。
  • offset:每个轮廓点移动的可选偏移量。这个参数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。

  该函数主要用于检测图像中的轮廓信息,并输出各个轮廓之间的结构信息。函数的第一个参数是待检测轮廓的输入图像,从理论上讲检测图像轮廓需要是二值化图像,但是该函数会对非0像素视为1,0像素保持不变,因此该参数能够接受非二值化的灰度图像。由于该函数默认二值化操作不能保持图像主要的内容,因此常需要对图像进行预处理,利用threshold()函数或者adaptiveThreshold()函数根据需求进行二值化。第二个参数用于存放检测到的轮廓,数据类型为vector<vector>,每个轮廓中存放着属于该轮廓的像素坐标。函数的第三个参数用于存放各个轮廓之间的结构信息,数据类型为vector,数据的尺寸与检测到的轮廓数目相同,每个轮廓结构信息中第1个数据表示同层下一个轮廓索引、第2个数据表示同层上一个轮廓索引、第3个数据表示下一层第一个子轮廓索引、第4个数据表示上层父轮廓索引。函数第四个参数是轮廓检测模式的标志,可以选择的参数及含义在表7-2给出。函数第五个参数是选择轮廓逼近方法的标志,可以选择的参数及含义在表7-3给出。函数最后一个参数是每个轮廓点移动的可选偏移量。这个函数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。

  有时我们只需要检测图像的轮廓,并不关心轮廓之间的结构关系信息,此时轮廓之间的结构关系变量会造成内存资源的浪费,因此OpenCV 4提供了findContours()函数的另一种函数原型,可以不输出轮廓之间的结构关系信息,该种函数原型在代码清单7-12中给出。

代码清单7-12 findContours()函数原型2
1.    void cv::findContours(InputArray  image,
2.                              OutputArrayOfArrays  contours,
3.                              int  mode,
4.                              int  method,
5.                              Point  offset = Point() 
6.                              )
  • image:输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像。
  • contours:检测到的轮廓,每个轮廓中存放着像素的坐标。
  • mode:轮廓检测模式标志,可以选择的参数在表7-2给出。
  • method:轮廓逼近方法标志,可以选择的参数在表7-3给出。
  • offset:每个轮廓点移动的可选偏移量。这个函数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。

提取了图像轮廓后,为了能够直观的查看轮廓检测的结果,OpenCV 4提供了显示轮廓的drawContours()函数,该函数的函数原型在代码清单7-13中给出。

代码清单7-13 drawContours()函数原型
  void cv::drawContours(InputOutputArray  image,
                            InputArrayOfArrays  contours,
                            int   contourIdx,
                            const Scalar &  color,
                            int  thickness = 1,
                            int  lineType = LINE_8,
                            InputArray  hierarchy = noArray(),
                            int  maxLevel = INT_MAX,
                            Point  offset = Point() 
                            )
  • image:绘制轮廓的目标图像。
  • contours:所有将要绘制的轮廓
  • contourIdx:要绘制的轮廓的数目,如果是负数,则绘制所有的轮廓。
  • color:绘制轮廓的颜色。
  • thickness:绘制轮廓的线条粗细,如果参数为负数,则绘制轮廓的内部,默认参数值为1.
  • lineType:边界线连接的类型,可以选择参数在表7-4给出,默认参数值为LINE_8。
  • hierarchy:可选的结构关系信息,默认值为noArray()。
  • maxLevel:表示用于绘制轮廓的最大等级,默认值为INT_MAX。
  • offset:可选的轮廓偏移参数,按指定的移动距离绘制所有的轮廓。

  该函数用于绘制findContours()函数检测到的图像轮廓。函数的第一个参数为绘制轮廓的图像,根据需求该参数可以是单通道的灰度图像或者三通道的彩色图像。第二个参数是所有将要绘制的轮廓,数据类型为vector<vector>。第三个参数是要绘制的轮廓数目,该参数的数值与第二个参数相对应,应小于所有轮廓的数目,如果该参数值为负数,则绘制所有的轮廓。第四个参数是绘制轮廓的颜色,对于单通道的灰度图像用Scalar(x)赋值,对于三通道的彩色图像用Scalar(x,y,z)赋值。第五个参数是边界线的连接类型,可以选择的参数在表7-4给出,默认参数值为LINE_8。第六个参数是可选的结构关系信息,默认值为noArray()。第七个参数表示绘制轮廓的最大等级,参数值如果为0,则仅绘制指定的轮廓;如果为1,则该函数绘制轮廓和所有嵌套轮廓;如果为2,则该函数绘制轮廓以及所有嵌套轮廓和所有嵌套到嵌套轮廓的轮廓,以此类推,默认值为INT_MAX。函数最后一个参数是可选的轮廓偏移参数,按指定的移动距离绘制所有的轮廓。

代码清单7-14 myContours.cpp轮廓检测与绘制
  #include <opencv2\opencv.hpp>
  #include <iostream>
  #include <vector>
  
  using namespace cv;
  using namespace std;
  
  int main()
  {
    system("color F0");  //更改输出界面颜色
    Mat img = imread("coins.jpg");
    if (img.empty())
    {
      cout << "请确认图像文件名称是否正确" << endl;
      return -1;
    }
    imshow("原图", img);
    Mat gray, binary;
    cvtColor(img, gray, COLOR_BGR2GRAY);  //转化成灰度图
    GaussianBlur(gray, gray, Size(9, 9), 2, 2);  //平滑滤波
    threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);  //自适应二值化
  
    // 轮廓发现与绘制
    vector<vector<Point>> contours;  //轮廓
    vector<Vec4i> hierarchy;  //存放轮廓结构变量
    findContours(binary, contours, hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE, Point());
    //绘制轮廓
    for (int t = 0; t < contours.size(); t++)
    {
      drawContours(img, contours, t, Scalar(0, 0, 255), 2, 8);
    }
    //输出轮廓结构描述子
    for (int i = 0; i < hierarchy.size(); i++)
    {
      cout << hierarchy[i] << endl;
    }
  
    //显示结果
    imshow("轮廓检测结果", img);
    waitKey(0);
    return 0;
  }

2、轮廓面积与周长

代码清单7-15 contourArea()函数原型
    double cv::contourArea(InputArray  contour,
                                bool  oriented = false 
                                )
  • contour:轮廓的像素点
  • oriented:区域面积是否具有方向的标志,true表示面积具有方向性,false表示不具有方向性,默认值为不具有方向性的false。

  该函数用于统计轮廓像素点围成区域的面积,函数的返回值是统计轮廓面积的结果,数据类型为double。函数第一个参数表示轮廓的像素点,数据类型为vector或者Mat,相邻的两个像素点之间逐一相连构成的多边形区域即为轮廓面积的统计区域。连续的三个像素点之间的连线有可能在同一条直线上,因此为了减少输入轮廓像素点的数目,可以只输入轮廓的顶点像素点,例如一个三角形的轮廓,轮廓中可能具有每一条边上的所有像素点,但是在统计面积时可以只输入三角形的三个顶点。函数第二个参数是区域面积是否具有方向的标志,参数为true时表示统计的面积具有方向性,轮廓顶点顺时针给出和逆时针给出时统计的面积互为相反数;参数为false时表示统计的面积不具有方向性,输出轮廓面积的绝对值。

代码清单7-17 arcLength()函数原型
1.    double cv::arcLength(InputArray  curve,
2.                             bool  closed 
3.                             )
  • curve:轮廓或者曲线的2D像素点。
  • closed:轮廓或者曲线是否闭合标志,true表示闭合。

  该函数能够统计轮廓或者曲线的长度,函数返回值为统计长度,单位为像素,数据类型为double。函数的第一个参数是轮廓或者曲线的2D像素点,数据类型为vector或者Mat。函数的第二个参数是轮廓或者曲线是否闭合的标志,true表示闭合。函数统计的长度是轮廓或者曲线相邻两个像素点之间连线的距离,例如计算三角形三个顶点A、B和C构成的轮廓长度时,并且函数第二个参数为true时,统计的长度是三角形三个边AB、BC和CA的长度之和;当参数为false时,统计的长度是由A到C三个点之间依次连线的距离长度之和,即AB和BC的长度之和。

3、轮廓外接多边形

  矩形是常见的几何形状,矩形的处理和分析方法也较为简单,OpenCV 4提供了两个函数求取轮廓外接矩形,分别是求取轮廓最大外接矩形的boundingRect()函数和求取轮廓最小外接矩形的minAreaRect()函数。寻找轮廓外接最大矩形就是寻找轮廓X方向和Y方向两端的像素,该矩形长和宽分别与图像的两个轴平行。boundingRect()函数可以实现这个功能,该函数的函数原型在代码清单7-19中给出。

代码清单7-19 boundingRect()函数原型
Rect cv::boundingRect(InputArray  array)    
  • array:输入的灰度图像或者2D点集,数据类型为vector或者Mat。

  该函数可以求取包含输入图像中物体轮廓或者2D点集的最大外接矩形,函数只有一个参数,可以是灰度图像或者2D点集,灰度图像的参数类型为Mat,2D点集的参数类型为vector或者Mat。该函数的返回值是一个Rect类型的变量,该变量可以直接用rectangle()函数绘制矩形。返回值共有四个参数,前两个参数是最大外接矩形左上角第一个像素的坐标,后两个参数分别表示最大外接矩形的宽和高。

  最小外接矩形的四个边都与轮廓相交,该矩形的旋转角度与轮廓的形状有关,多数情况下矩形的四个边不与图像的两个轴平行。minAreaRect()函数可以求取轮廓的最小外接矩形,该函数的函数原型在代码清单7-20中给出。

代码清单7-20 minAreaRect()函数原型
RotatedRect cv::minAreaRect(InputArray  points)
  • points:输入的2D点集合

  该函数可以根据输入的2D点集合计算最小的外接矩形,函数的返回值是RotatedRect类型的变量,含有矩形的中心位置、矩形的宽和高和矩形旋转的角度。RotatedRect类具有两个重要的方法和属性,可以输出矩形的四个顶点和中心坐标。输出四个顶点坐标的方法是points(),假设RotatedRect类的变量为rrect,可以通过rrect.points(points)命令进行读取,其中坐标存放的变量是Point2f类型的数组。输出矩形中心坐标的属性是center,假设RotatedRect类的变量为rrect,可以通过opt=rrect.center命令进行读取,其中坐标存放的变量是Point2f类型的变量。

代码清单7-21 myRect.cpp计算轮廓外接矩形
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    Mat img = imread("stuff.jpg");
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Mat img1, img2;
    img.copyTo(img1);  //深拷贝用来绘制最大外接矩形
    img.copyTo(img2);  //深拷贝迎来绘制最小外接矩形
    imshow("img", img);

    // 去噪声与二值化
    Mat canny;
    Canny(img, canny, 80, 160, 3, false);
    imshow("", canny);

    //膨胀运算,将细小缝隙填补上
    Mat kernel = getStructuringElement(0, Size(3, 3));
    dilate(canny, canny, kernel);

    // 轮廓发现与绘制
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(canny, contours, hierarchy, 0, 2, Point());

    //寻找轮廓的外接矩形
    for (int n = 0; n < contours.size(); n++)
    {
        // 最大外接矩形
        Rect rect = boundingRect(contours[n]);
        rectangle(img1, rect, Scalar(0, 0, 255), 2, 8, 0);

        // 最小外接矩形
        RotatedRect rrect = minAreaRect(contours[n]);
        Point2f points[4];
        rrect.points(points);  //读取最小外接矩形的四个顶点
        Point2f cpt = rrect.center;  //最小外接矩形的中心

                                     // 绘制旋转矩形与中心位置
        for (int i = 0; i < 4; i++)
        {
            if (i == 3)
            {
                line(img2, points[i], points[0], Scalar(0, 255, 0), 2, 8, 0);
                break;
            }
            line(img2, points[i], points[i + 1], Scalar(0, 255, 0), 2, 8, 0);
        }
        //绘制矩形的中心
        circle(img, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
    }

    //输出绘制外接矩形的结果
    imshow("max", img1);
    imshow("min", img2);
    waitKey(0);
    return 0;
}

OpenCV 4提供了approxPolyDP()函数用于寻找逼近轮廓的多边形,该函数的函数原型在代码清单7-22中给出。

代码清单7-22 approxPolyDP()函数原型
void cv::approxPolyDP(InputArray  curve,
                      OutputArray  approxCurve,
                      double  epsilon,
                      bool  closed 
                      )
  • curve:输入轮廓像素点。
  • approxCurve:多边形逼近结果,以多边形顶点坐标的形式给出。
  • epsilon:逼近的精度,即原始曲线和逼近曲线之间的最大距离。
  • closed:逼近曲线是否为封闭曲线的标志, true表示曲线封闭,即最后一个顶点与第一个顶点相连。

  该函数根据输入的轮廓得到最佳的逼近多边形。函数的第一个参数是输入的轮廓2D像素点,数据类型是vector或者Mat。第二个参数是多边形的逼近结果,以多边形顶点坐标的形式输出,是CV_32SC2类型的N×1的Mat类矩阵,可以通过输出结果的顶点数目初步判断轮廓的几何形状。第三个参数是多边形逼近时的精度,即原始曲线和逼近曲线之间的最大距离。第四个参数是逼近曲线是否为封闭曲线的标志, true表示曲线封闭,即最后一个顶点与第一个顶点相连。

代码清单7-23 myApproxPolyDP.cpp多边形轮廓拟合
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

//绘制轮廓函数
void drawapp(Mat result, Mat img2)
{
    for (int i = 0; i < result.rows; i++)
    {
        //最后一个坐标点与第一个坐标点连接
        if (i == result.rows - 1)
        {
            Vec2i point1 = result.at<Vec2i>(i);
            Vec2i point2 = result.at<Vec2i>(0);
            line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
            break;
        }
        Vec2i point1 = result.at<Vec2i>(i);
        Vec2i point2 = result.at<Vec2i>(i + 1);
        line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
    }
}

int main(int argc, const char *argv[])
{
    Mat img = imread("approx.png");
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    // 边缘检测
    Mat canny;
    Canny(img, canny, 80, 160, 3, false);
    //膨胀运算
    Mat kernel = getStructuringElement(0, Size(3, 3));
    dilate(canny, canny, kernel);

    // 轮廓发现与绘制
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(canny, contours, hierarchy, 0, 2, Point());

    //绘制多边形
    for (int t = 0; t < contours.size(); t++)
    {
        //用最小外接矩形求取轮廓中心
        RotatedRect rrect = minAreaRect(contours[t]);
        Point2f center = rrect.center;
        circle(img, center, 2, Scalar(0, 255, 0), 2, 8, 0);

        Mat result;
        approxPolyDP(contours[t], result, 4, true);  //多边形拟合
        drawapp(result, img);
        cout << "corners : " << result.rows << endl;

        //判断形状和绘制轮廓
        if (result.rows == 3)
        {
            putText(img, "triangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
        }
        if (result.rows == 4)
        {
            putText(img, "rectangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
        }
        if (result.rows == 8)
        {
            putText(img, "poly-8", center, 0, 1, Scalar(0, 255, 0), 1, 8);
        }
        if (result.rows > 12)
        {
            putText(img, "circle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
        }
    }
    imshow("result", img);
    waitKey(0);
    return 0;
}