OPENCV——查找图形轮廓

图像形状查找在OPENCV里面是非常常见的功能,它常用于视觉任务、目标检测、图像分割等等。在OPENCV中通常使用Canny函数、findContours函数、drawContours函数结合在一起去做轮廓的形检测。

一、重要函数讲解

1.1 findContours 函数的简介以及定义

在OPENCV中通常使用findContours函数去寻找图片的轮廓,也是OPENCV中处理轮廓最重要的函数之一,它常用于找到二值图像中所有物体的轮廓。它的实现原理是通过扫描一张二值图像,然后找到所有的轮廓,并把所有的数据存储在向量里面。下面我们来看看findContours的函数定义

复制代码
void findContours(
    InputArray image,
    OutputArrayOfArrays contours,
    OutputArray hierarchy,
    int mode,
    int method,
    Point offset = Point()
);

**第一个参数:**image输入的二值图像,这个图像通常是用在边缘检测、阈值处理等等

第二个参数: contours输出的轮廓集合,每一个轮廓都是由点组成,通常用**vector<vector<Point>>**来表示

第三个参数: hierarchy输出的轮廓层次结构,这通常表示轮廓之间的父子关系,这个是可选参数,通常用vector<Vec4i> hierarchy来表示。比方说,第i个轮廓,hierarchyi0、hierarchyi1、hierarchyi2、hierarchyi3, 依次为第i个轮廓Next、Pervious、First_Child,Parent, 这表示的是相同等级下种下一轮廓、前一轮廓,第一个子轮廓和父轮廓的索引号。若轮廓i没有下一个,前一个或者父级轮廓,则层次相应的元素是负数。如下图:

Next **:**表示同一级别的下一个轮廓索引,若我们图片中取出轮廓0,同一水平的下一个是轮廓1。所以说当轮廓 == 0的时候,NEXT就是轮廓1。

Previous **:**表示同一级别的上一个轮廓索引,如轮廓1的同一级别的上一个是轮廓0。以此类推,轮廓2的上一个轮廓是轮廓1。

First_Child **:**表示的是当前轮廓的第一个子轮廓的索引。比方说,对于轮廓2,子轮廓是2a,所以轮廓2的First_Child是轮廓2a相对应的索引值。而对于3a来说,它有两个轮廓分别是6,7, 但这里只能取第一个轮廓,所以这里是6。

Parent **:**表示的是当前轮廓的父轮廓索引,比方说对于轮廓6和轮廓7来说,它们的父轮廓都是3a。

**第四个参数:**mode轮廓检索模式,通常有以下选项

枚举值 作用 适用场景
RETR_EXTERNAL 只检测最外层轮廓,忽略所有内部孔洞轮廓 最常用,嵌入式优先推荐;只需要物体外轮廓、不需要内部细节时使用,计算量最小
RETR_LIST 检测所有轮廓,但不建立层级关系,所有轮廓同级 需要全部轮廓但不需要父子关系时使用
RETR_CCOMP 检测所有轮廓,建立两层层级(外层 + 内层孔洞) 有孔洞的物体(比如圆环、带孔零件)检测
RETR_TREE 检测所有轮廓,建立完整的树形层级关系 需要完整轮廓嵌套结构的复杂场景

**第五个参数:**method轮廓近似方法,通常有以下的几种方法

枚举值 作用 适用场景
CHAIN_APPROX_SIMPLE 压缩水平、垂直、对角线方向的冗余点,只保留线段端点;比如矩形轮廓只存 4 个角点 强烈推荐,大幅减少轮廓点数量,节省内存和计算量,嵌入式必用
CHAIN_APPROX_NONE 保存轮廓上所有像素点,点数量极多 需要精确轮廓轨迹的高精度场景,一般不用

**第六个参数:**offset轮廓点偏移量,默认(0,0)

输出数据结构说明

  • contours 是二维向量:contours[0] 是第一条轮廓,contours[0][0] 是第一条轮廓的第一个点坐标;
  • 每条轮廓都是一组连续的 Point(x,y) 坐标,围成闭合的边缘。

1.2 ​​​​​​​drawContours 函数的简介以及定义

在OPENCV中drawContours常用于绘制图像的轮廓,如上图,我们来看看这个函数的API定义:

复制代码
void 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、轮廓索引数组,指定要绘制哪些轮廓

第四个参数: contourColor **,**轮廓颜色,使用Scalar类型表示

第五个参数: thickness **,**轮廓线宽,默认1

第六个参数: lineType **,**轮廓线类型,默认为LINE_8

第七个参数: hierarchy **,**轮廓层次结构,用于绘制轮廓的父子关系。默认为noArray()

第八个参数: maxLevel 表示绘制轮廓的最大层级数量**。**若maxLevel 为0,则只绘制指定的轮廓;若maxLevel 为1,则绘制轮廓极其所有嵌套轮廓;若maxLevel 为2,则绘制轮廓、所有嵌套轮廓、所有嵌套到嵌套的轮廓。

**第九个参数:**轮廓点的偏移量,默认为(0,0)

1.3 ​​​​​​​Canny 函数的简介以及定义

Canny函数主要用在OPENCV的边缘检测计算,边缘检测是OPENCV图像中非常重要的功能,它的功能如(上图一)。它能够高效地提取图像中的边缘信息,而Canny边缘检测是OPENCV里面最优秀和最精准的边缘检测方法。Canny的工作原理可以分为以下比较重要的步骤进行处理,分别是高斯滤波 ( 将图像转换为灰度图像,高斯滤波作用是平滑图像,让 Canny 检测的时候准确率更高 ) 、梯度强度和方向的计算 ( 计算图像中每个像素的强度和方向、强度表示像素点的边缘强度、梯度表示的是边缘方向这里的梯度需要用到 sobel 因子 ) 、非极大抑制 ( 经过 NMS 操作后,会除去一些不是边缘的像素点 ) 、双阈值处理 ( 给出一个阈值,若超过这个阈值的边缘则会被保留 ) 、边缘链接 ( 经过双阈值处理过后,强边缘则会留下来,弱边缘则会被抑制,并会把所有的强边缘全部连接起来 ) ,步骤如下图 2

下面是双阈值的处理的图解:当梯度值大于maxVal则认为是强边界;当minVal < 梯度值 < maxVal跟边界有连接的部分则保留,否则废弃;梯度值< minVal则废弃。另外需要注意的是高阈值与低阈值的比例最好是2:13:1之间。

下面我们来看看Canny的函数定义:

复制代码
void Canny(
    InputArray image,
    OutputArray edges,
    double threshold1,
    double threshold2,
    int apertureSize = 3,
    bool L2gradient = false
);

第一个参数:image 输入的图像,这个图像一定要单通道灰度图

第二个参数:edges 输出的边缘图像,这个图像也必须是单通道黑白图

第三个参数: threshold1第一个滞后性阈值,低阈值,小于低阈值则认为是弱边缘,就是需要抛弃的边缘。

第四个参数: threshold2第二个滞后性阈值,高阈值,大于高阈值被认为强边缘,需要保留的边缘

第五个参数:apertureSize指的是Sobel算子大小,这个值默认为3,代表的是3*3的矩阵大小。

第六个参数:L2gradient是计算图像梯度幅度值的情况,这个值默认为False;若选择True,则使用更精确的L2范数进行计算

二、查找图形轮廓并画框

经过上一章节的讲解,我们对整个OPENCV提取轮廓的API有了一个大致的了解。本章节主要是讲解如何通过代码来实现OPENCV的轮廓检测提取然后进行画框。具体的如下图:

完成轮廓检测,需要做以上步骤。分别是imread读取图片(这个图片默认是3通道)、利用cvtColor 把8VU3的三通道图片转换成灰度图(8VU1)、调用Canny对灰度图像进行边缘检测、调用findContours去查找轮廓、循环轮廓数量然后调用drawContours进行画框操作。

代码

复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat img = imread("shape.png");

    Mat imgGray;
    cvtColor(img, imgGray, COLOR_RGB2GRAY);

    Mat imgCanny;
    Canny(imgGray, imgCanny, 25, 75);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(imgCanny, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

    Mat drawing = Mat::zeros(imgCanny.size(), CV_8UC3);
    for(int i = 0; i < contours.size(); i++)
    {
        Scalar color = Scalar(255, 255, 0);
        drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
    }

    imwrite("contour,jpg",drawing);
    
    return 0;
}

2.1. 读取原图

复制代码
Mat img = imread("shape.png");
  • 读取当前目录下的shape.png,默认以BGR 三通道彩色格式加载。

2.2. 灰度化转换

复制代码
Mat imgGray;
cvtColor(img, imgGray, COLOR_RGB2GRAY);
  • 功能:将彩色图转为单通道灰度图,Canny 只支持单通道输入。

2.3. Canny 边缘检测

复制代码
Mat imgCanny;
Canny(imgGray, imgCanny, 25, 75);
  • 功能:对灰度图做边缘检测,输出单通道二值边缘图(白色为边缘,黑色为背景)。
  • 参数说明:低阈值 25,高阈值 75,高低阈值比例为 3:1,大于75表示的是强边缘需要保存下来的,小于25是弱边缘需要忽略的,25-75之间的边缘则会用算法进行候选;默认使用 3×3 Sobel 算子。

2.4. 轮廓检测

复制代码
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(imgCanny, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

处理完边缘后,就findContours 可以通过findContours 去查找所有图像的轮廓了,由于我们检测的图像没有内嵌的形状,所以我们选择RETR_EXTERNAL 的模式只检测外轮廓,Method 的方法则选择CHAIN_APPROX_SIMPLE存储所有的轮廓点。注意:contours一般是用vector<vector<point>>表示,hierarchy通常用vector<Vec4i>表示.

2.5. 轮廓绘制与保存

1. 创建绘制画布

复制代码
Mat drawing = Mat::zeros(imgCanny.size(), CV_8UC3);
  • 创建一张和原图尺寸一致的纯黑三通道图像,作为绘制轮廓的背景板,方便突出显示轮廓。

2. 循环绘制所有轮廓

复制代码
for(int i = 0; i < contours.size(); i++)
{
    Scalar color = Scalar(255, 255, 0);
    drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
}
  • 遍历每一条检测到的轮廓,逐个绘制到黑色画布上。
  • 参数说明:
    • 颜色Scalar(255,255,0):BGR 格式下为青色(蓝 + 绿满值,红为 0)。
    • 线宽2:轮廓线条粗细为 2 像素。
    • 线型8:8 连通线型,线条更平滑。
    • maxLevel=0:只绘制当前层级轮廓,配合RETR_EXTERNAL使用时无额外效果。

3. 保存结果图

复制代码
imwrite("contour,jpg",drawing);