OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

时间:2024-05-21 17:56:43
本文所有代码参考OpenCV 300

目录
1.提取轮廓
2.绘制轮廓
3.获取轮廓最小外接矩形
4.获取轮廓最小外接圆
5.填充RotatedRect

1.提取轮廓
void findContours//提取轮廓,用于提取图像的轮廓
(
InputOutputArray image,//输入图像,必须是8位单通道图像,并且应该转化成二值的
OutputArrayOfArrays contours,//检测到的轮廓,每个轮廓被表示成一个point向量
OutputArray hierarchy,//可选的输出向量,包含图像的拓扑信息。其中元素的个数和检测到的轮廓的数量相等
int mode,//说明需要的轮廓类型和希望的返回值方式
int method,//轮廓近似方法
Point offset = Point()
)

1.1 mode取值类型

mode的值决定把找到的轮廓如何挂到轮廓树节点变量(h_prev, h_next, v_prev, v_next)上。下图展示了四种可能的mode值所得到的结果的拓扑结构。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础) OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

                (a)                                                                  (b)

                                    图1 (a)原图示意图,(b)mode不同类型的拓扑结构

mode
描述
轮廓结果图

CV_RETR_EXTERNAL 

只检测出最外轮廓即c0。图1中第一个轮廓指向最外的序列,除此之外没有别的连接。


OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

CV_RETR_LIST

检测出所有的轮廓并将他们保存到表(list)中,图1中描绘了这个表,被找到的9条轮廓相互之间由h_prev和h_next连接。这里并没有表达出纵向的连接关系,没有使用v_prev和v_next.

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

 CV_RETR_CCOMP

检测出所有的轮廓并将他们组织成双层的结构,第一层是外部轮廓边界,第二层边界是孔的边界。从图1可以看到5个轮廓的边界,其中3个包含孔。最外层边界c0有两个孔,c0之间的所有孔相互间由h_prev和h_next指针连接。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

  CV_RETR_TREE

检测出所有轮廓并且重新建立网状的轮廓结构。图1中,根节点是最外层的边界c0,c0之下是孔h00,在同一层中与另一个孔h01相连接。同理,每个孔都有子节点(相对于c000和c010),这些子节点和父节点被垂直连接起来。这个步骤一直持续到图像最内层的轮廓,这些轮廓会成为树叶节点。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)
注意:
(1)测试图像为
OpenCV——findContours函数的使用(基于Mat轮廓处理基础)
(2)从图中可以看出,CV_RETR_EXTERNAL 得到的只有一个最外的轮廓,hierarchy的值为{-1,-1,-1,-1},表示除此之外没有别的连接。其他三种方式都可以得到所有的轮廓,而hierarchy的值是不同的,反映了不同的轮廓连接方法即结果的拓扑机构。
(3)此时 method = CV_CHAIN_APPROX_NONE
(4)method可以取值CV_RETR_FLOODFILL,但是在测试时直接崩溃

测试代码
Mat CreateTestImage();

int main(int argc, _TCHAR* argv[])
{
    Mat src = CreateTestImage();
    Mat gray;
    cvtColor(src, gray, CV_BGR2GRAY);
    Mat bw;
    cv::threshold(gray, bw, 128, 255,THRESH_BINARY);
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    cv::findContours(gray, contours, hierarchy,CV_RETR_EXTERNAL , CV_CHAIN_APPROX_NONE  );
    Mat dst = Mat ::zeros(src.size(),CV_8UC3);
    drawContours(dst, contours, -1, Scalar(255,255, 255));

    cv::namedWindow("src",0);
    cv::imshow("src",src);
    cv::namedWindow("dst",0);
    cv::imshow("dst",dst);
    cv::waitKey();

    system("pause");
    return 0;
}

Mat CreateTestImage()
{
    //第一层,绘制一个黑色背景的图像
    int nWidth = 557;
    int nHeight = 291;
    Mat dst = Mat::zeros(nHeight,nWidth,CV_8UC3);

    //第二层,绘制一个白色矩形,上下左右各有等距边框
    int nBorder = 20;
    int nX2 = nBorder;
    int nY2 = nBorder;
    int nWidth2 = nWidth -nBorder*2 ;
    int nHeight2 = nHeight - nBorder*2;
    cv::rectangle(dst,cv::Rect(nX2,nY2,nWidth2,nHeight2),cv::Scalar(255,255,255),-1);

    //第三层,在第二层中间绘制两个黑色矩形,上下左右各有等距边框,两个黑色矩形间距一个边框距离
    int nX3 = nX2 + nBorder;
    int nY3 = nY2 + nBorder;
    int nWidth3 = (nWidth2-nBorder*3)*0.5 ;
    int nHeight3 =(nHeight2-nBorder*2) ;
    int nX3_1 = nX3+nWidth3+nBorder;
    cv::rectangle(dst,cv::Rect(nX3,nY3,nWidth3,nHeight3),cv::Scalar(0,0,0),-1);
    cv::rectangle(dst,cv::Rect(nX3_1,nY3,nWidth3,nHeight3),cv::Scalar(0,0,0),-1);

    //第四层,在第三层两个黑色矩形中再分别绘制两个白色矩形,上下左右各有等距边框
    int nX4 = nX3 + nBorder;
    int nY4 = nY3 + nBorder;
    int nWidth4 = (nWidth3-nBorder*2);
    int nHeight4 =(nHeight3-nBorder*2) ;
    int nX4_1 = nX3_1+nBorder;
    cv::rectangle(dst,cv::Rect(nX4,nY4,nWidth4,nHeight4),cv::Scalar(255,255,255),-1);
    cv::rectangle(dst,cv::Rect(nX4_1,nY4,nWidth4,nHeight4),cv::Scalar(255,255,255),-1);

    //第五层,在第四层两个白色矩形中再分别绘制两个黑色矩形,上下左右各有等距边框
    int nX5 = nX4 + nBorder;
    int nY5 = nY4 + nBorder;
    int nWidth5 = (nWidth4-nBorder*2);
    int nHeight5 =(nHeight4-nBorder*2) ;
    int nX5_1 = nX4_1+nBorder;
    cv::rectangle(dst,cv::Rect(nX5,nY5,nWidth5,nHeight5),cv::Scalar(0,0,0),-1);
    cv::rectangle(dst,cv::Rect(nX5_1,nY5,nWidth5,nHeight5),cv::Scalar(0,0,0),-1);


    //第六层,在第⑤层第二个矩形绘制四个白色矩形,上下左右各有等距边框,四个白色矩形间距一个边框距离,只绘制下方两个矩形
    int nWidth6 = (nWidth5-nBorder*3)*0.5 ;
    int nHeight6 =(nHeight5-nBorder*3)*0.5 ;
    int nX6 = nX5_1 + nBorder;
    int nY6 = nY5 + nBorder + nHeight6 + nBorder;
    int nX6_1 = nX6 + nWidth6 + nBorder;
    cv::rectangle(dst,cv::Rect(nX6,nY6,nWidth6,nHeight6),cv::Scalar(255,255,255),-1);
    cv::rectangle(dst,cv::Rect(nX6_1,nY6,nWidth6,nHeight6),cv::Scalar(255,255,255),-1);

    return dst;
}

1.2 method取值类型
method这个参数决定了轮廓的表达方式,这要根据自己提取轮廓后的应用选择合适的轮廓描述方法来决定用哪一种。
method
描述
结果

CV_CHAIN_CODE 

用freeman链码输出轮廓,其他方法输出多边形(顶点的序列)。通过实验证明是无法绘制出轮廓的。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

CV_CHAIN_APPROX_NONE

将链码编码中的所有点转换为点。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

CV_CHAIN_APPROX_SIMPLE

压缩水平,垂直或斜的部分,只保存最后一个点。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS


使用Teh-Chin链逼近算法中的一个。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)

CV_LINK_RUNS


与上述的算法完全不同,连接所有的水平层次的轮廓。

OpenCV——findContours函数的使用(基于Mat轮廓处理基础)
注意:
(1)CV_CHAIN_APPROX_NONE由于像素之间没有间隔已经练成线了。
(2)CV_CHAIN_APPROX_SIMPLE 和 CV_CHAIN_APPROX_TC89_L1的结果都是保存的轮廓的顶点,但是仔细看可以看出CV_CHAIN_APPROX_TC89_L1轮廓线是有断的感觉不是连贯的,这可能是轮廓逼近过程中的误差,具体情况就不清楚了,也有可能这种方法对具有某种特征的图逼近效果比较好。
(3)CV_LINK_RUNS是和其他算法完全不同,连接所有水平层次的轮廓,但结果画出来的是一条条垂直的线。另外,在mode=CV_RETR_EXTERNAL 的前提下,选用这种方式也是可以画出所有轮廓的,而其他的方式只能画出最外层的轮廓。理论上CV_LINK_RUNSCV_RETR_LIST搭配使用。

测试代码
Mat CreateTestImage();

int main(int argc, _TCHAR* argv[])
{
    Mat src = CreateTestImage();
    Mat gray;
    cvtColor(src, gray, CV_BGR2GRAY);
    Mat bw;
    cv::threshold(gray, bw, 128, 255,THRESH_BINARY);
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    cv::findContours(gray, contours, hierarchy, CV_RETR_LIST, CV_LINK_RUNS);
    Mat dst = Mat ::zeros(src.size(),CV_8UC3);
    drawContours(dst, contours, -1, Scalar(255,255, 255));

    for each(vector<Point> contour in contours )
    {
        for each(Point pt in contour)
        {
            cv::circle(dst,pt,1,Scalar(0,0, 255),-1);
        }

    }

    cv::namedWindow("src",0);
    cv::imshow("src",src);
    cv::namedWindow("dst",0);
    cv::imshow("dst",dst);
    cv::waitKey();

    system("pause");
    return 0;
}

2.绘制轮廓
void drawContours//绘制轮廓,用于绘制找到的图像轮廓
(
 InputOutputArray image,//要绘制轮廓的图像
 InputArrayOfArrays contours,//所有输入的轮廓,每个轮廓被保存成一个point向量
 int contourIdx,//指定要绘制轮廓的编号,如果是负数,则绘制所有的轮廓
 const Scalar& color,//绘制轮廓所用的颜色
 int thickness = 1, //绘制轮廓的线的粗细,如果是负数,则轮廓内部被填充
 int lineType = 8, /绘制轮廓的线的连通性
 InputArray hierarchy = noArray(),//关于层级的可选参数,只有绘制部分轮廓时才会用到
 int maxLevel = INT_MAX,//绘制轮廓的*别,这个参数只有hierarchy有效的时候才有效
                                          //maxLevel=0,绘制与输入轮廓属于同一等级的所有轮廓即输入轮廓和与其相邻的轮廓
                                          //maxLevel=1, 绘制与输入轮廓同一等级的所有轮廓与其子节点。
                                          //maxLevel=2,绘制与输入轮廓同一等级的所有轮廓与其子节点以及子节点的子节点
 Point offset = Point()
)

注意:
(1)findContours()运行的时候,这个图像会被直接涂改,因此如果是将来还有用的图像,应该复制之后再传给findContours()。
(2)drawContours()函数中的参数thinkness=CV_FILLED可以填充轮廓,opencv官网对这个参数的解释原文是“If it is negative (for example,thickness=CV_FILLED ), the contour interiors are drawn”

3.获取轮廓外接形状
3.1 获取轮廓最小外接矩形
RotatedRect minAreaRect//求点集的最小外结矩使用方法
InputArray points//输入参数points是所要求最小外结矩的点集数组或向量
);  

3.2 获取轮廓最小外接圆
void minEnclosingCircle//求点集的最小外接圆
InputArray points,//输入参数points是所要求最小外结圆的点集数组或向量;
CV_OUT Point2f& center,//Point2f类型的center是求得的最小外接圆的中心坐标;
CV_OUT float& radius//float类型的radius是求得的最小外接圆的半径;
);  

测试代码
vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(bgs, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    //drawContours(dst, contours, -1, Scalar(255, 0, 255));
    int nAreaTh = img.cols * img.rows * 0.05;
    for(int i=0;i<contours.size();i++)
    {        
        //绘制轮廓的最小外结矩形
        RotatedRect rect=minAreaRect(contours[i]);
        if(rect.size.area() >= nAreaTh)
        {
            Point2f P[4];
            rect.points(P);
            for(int j=0;j<=3;j++)
            {
                line(dst,P[j]/fRadio,P[(j+1)%4]/fRadio,Scalar(255,0,0),2);
            }
        }

        //绘制轮廓的最小外结圆
        Point2f center; float radius;
        minEnclosingCircle(contours[i],center,radius);
        circle(imageContours1,center,radius,Scalar(255),2);

    }

5.填充RotatedRect
void full_rotated_rect(Mat &image, const RotatedRect &rect, const Scalar &color)  
{  
    CvPoint2D32f point[4];  
    Point pt[4];  
    vector<Point> center1, center2;  

    /*画出外框*/  
    cvBoxPoints(rect, point);  
    for (int i = 0; i<4; i++)  
    {  
        pt[i].x = (int)point[i].x;  
        pt[i].y = (int)point[i].y;  
    }  
    line(image, pt[0], pt[1], color, 1);  
    line(image, pt[1], pt[2], color, 1);  
    line(image, pt[2], pt[3], color, 1);  
    line(image, pt[3], pt[0], color, 1);  

    /*填充内部*/  
    find_all_point(pt[0], pt[1], center1);  /*找出两点间直线上的所有点*/  
    find_all_point(pt[3], pt[2], center2);  
    vector<Point>::iterator itor1 = center1.begin(), itor2 = center2.begin();  
    while (itor1 != center1.end() && itor2 != center2.end())  
    {  
        line(image, *itor1, *itor2, color, 1);  /*连接对应点*/  
        itor1++;  
        itor2++;  
    }  

    vector<Point>().swap(center1);  
    vector<Point>().swap(center2);  
}

void find_all_point(Point start, Point end, vector<Point> &save)  
{  
    if (abs(start.x - end.x) <= 1 && abs(start.y - end.y) <= 1)  
    {  
        save.push_back(start);  
        return; /*点重复时返回*/  
    }  

    Point point_center;  
    point_center.x = (start.x + end.x) / 2;  
    point_center.y = (start.y + end.y) / 2;  
    save.push_back(point_center);   /*储存中点*/  
    find_all_point(start, point_center, save);  /*递归*/  
    find_all_point(point_center, end, save);  
}


参考资料: