93-OpenCVSharp —- Cv2.FindContours()函数功能(轮廓检测,轮廓的层次结构)详解

专栏地址:

《 OpenCV功能使用详解200篇 》

《 OpenCV算子使用详解300篇 》

《 Halcon算子使用详解300篇 》

内容持续更新 ,欢迎点击订阅


一 、 FindContours函数详解

1. 核心原理

FindContours 是 OpenCV 中用于从二值图像中提取轮廓的核心函数。轮廓(Contour)是指图像中具有相同颜色或灰度的连续像素点组成的曲线,是图像中物体形状的重要表征。

其核心原理基于连通区域分析

  • 输入为二值图像(通常通过阈值分割或边缘检测得到),其中目标区域为非零值(如255),背景为0;
  • 函数通过扫描图像,追踪所有连续的非零像素点,将其连接成闭合或开放的曲线,形成轮廓;
  • 同时记录轮廓之间的层次关系(如父轮廓包含子轮廓),用于复杂形状的嵌套结构分析。

2. 算子功能及应用场景

  • 功能:从二值图像中提取所有轮廓,并记录轮廓的像素坐标及层次关系,为后续的形状分析、目标识别等提供基础数据。
  • 应用场景
    • 目标检测与计数(如工业质检中零件数量统计);
    • 形状分析(如判断物体是圆形、矩形还是多边形);
    • 图像分割(提取感兴趣区域ROI);
    • 目标跟踪(通过轮廓匹配定位物体);
    • 手势识别、字符识别等计算机视觉任务。

3. 算子函数参数详解(OpenCVSharp)

void Cv2.FindContours(
    InputArray image,               // 输入二值图像
    OutputArrayOfArrays contours,   // 输出轮廓集合
    OutputArray hierarchy,          // 输出轮廓层次结构
    RetrievalModes mode,            // 轮廓检索模式
    ContourApproximationModes method, // 轮廓逼近方法
    Point offset = null             // 轮廓点坐标偏移量
)
  • image
    输入图像,必须是单通道二值图像(8位)。通常需先通过 Cv2.ThresholdCv2.Canny 处理得到(非零值为目标,0为背景)。若输入为彩色或灰度图,需先转为二值图。

  • contours
    输出参数,类型为 VectorOfVectorPoint,存储所有提取的轮廓。每个轮廓是一个 VectorPoint(像素坐标集合),例如 contours[0] 表示第1个轮廓的所有点。

  • hierarchy
    输出参数,类型为 Mat(1×N×4的整数矩阵),存储轮廓的层次关系。每个轮廓对应4个值 [next, prev, child, parent]

    • next:同一层级下的下一个轮廓索引(-1表示无);
    • prev:同一层级下的上一个轮廓索引(-1表示无);
    • child:当前轮廓的第一个子轮廓索引(-1表示无);
    • parent:当前轮廓的父轮廓索引(-1表示无,即最外层轮廓)。
  • mode:轮廓检索模式(关键参数),决定是否记录层次关系及如何组织轮廓:

    • RetrievalModes.External:仅提取最外层轮廓(忽略嵌套轮廓),适用于只需外轮廓的场景(如计数独立物体);
    • RetrievalModes.List:提取所有轮廓,但不记录层次关系(轮廓无序),适用于无需层次的简单场景;
    • RetrievalModes.CComp:提取所有轮廓,按两层结构组织(外层为顶层,内层为第二层),适用于嵌套层次简单的场景(如物体内部有孔洞);
    • RetrievalModes.Tree:提取所有轮廓,记录完整的层次树结构(推荐用于需要嵌套关系的场景,如复杂形状分析)。
  • method:轮廓逼近方法,决定如何简化轮廓点(减少冗余点):

    • ContourApproximationModes.None:存储所有轮廓点(精确但数据量大);
    • ContourApproximationModes.Simple:仅保留轮廓的拐角点(如矩形只保留4个顶点),大幅减少数据量,适用于形状识别;
    • ContourApproximationModes.L1 / TC89L1 / TC89KCOS:基于Teh-Chin链逼近算法的优化模式,平衡精度和效率。
  • offset
    可选参数,为所有轮廓点添加坐标偏移量(如 new Point(10, 20) 表示所有点的x加10,y加20)。常用于图像ROI提取后,将轮廓坐标映射回原图。

4. 使用注意事项

  1. 输入图像预处理

    • 必须是二值图像(非0即255),建议先用 Cv2.GaussianBlur 降噪,再用 Cv2.ThresholdCv2.Canny 生成二值图(Canny边缘检测可减少噪声干扰);
    • 若目标是白色(255),背景是黑色(0),轮廓提取更稳定;反之可先用 Cv2.BitwiseNot 反转。
  2. 轮廓层级的理解

    • 嵌套轮廓(如圆环的外圆和内圆)中,外轮廓是父轮廓,内轮廓是子轮廓,hierarchyparentchild 会关联两者;
    • 选择 RetrievalModes.Tree 可完整保留层级,适合分析复杂嵌套结构。
  3. 轮廓点的存储

    • 轮廓点按顺时针或逆时针顺序存储,可通过 Cv2.ContourArea 计算面积,Cv2.Arclength 计算周长;
    • ContourApproximationModes.Simple 可显著减少计算量(如矩形从数百点简化为4点)。
  4. 性能优化

    • 对大图像,可先缩放尺寸再提取轮廓;
    • 非必要时避免用 RetrievalModes.Tree(计算量较大),优先用 ExternalList

5. 相关算子对比

算子功能差异优点缺点
FindContours从二值图提取轮廓及层次提供完整轮廓数据和层级关系依赖高质量二值图,对噪声敏感
Canny边缘检测(输出边缘像素)抗噪声能力强,边缘连续仅输出边缘,不形成闭合轮廓
drawContours绘制提取的轮廓可自定义颜色、厚度,支持填充依赖 FindContours 的输出
findContours(Python)与C#版本功能一致,参数略有差异语法更简洁性能略低于C#版本

6. 可运行的完整案例

以下案例实现从图像中提取轮廓,绘制轮廓并计算基本属性(面积、周长、形状判断)。

using System;
using OpenCvSharp;

class ContourDetectionDemo
{
    static void Main(string[] args)
    {
        // 1. 读取图像并预处理
        Mat src = Cv2.ImRead("shapes.jpg"); // 替换为你的图像路径
        if (src.Empty())
        {
            Console.WriteLine("无法读取图像!");
            return;
        }
        Mat gray = new Mat();
        Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 转为灰度图

        // 降噪并生成二值图(Canny边缘检测)
        Mat blurred = new Mat();
        Cv2.GaussianBlur(gray, blurred, new Size(5, 5), 0);
        Mat edges = new Mat();
        Cv2.Canny(blurred, edges, 50, 150); // 边缘检测,阈值可调整

        // 2. 提取轮廓
        var contours = new VectorOfVectorPoint();
        Mat hierarchy = new Mat();
        Cv2.FindContours(
            image: edges,
            contours: contours,
            hierarchy: hierarchy,
            mode: RetrievalModes.Tree, // 提取所有轮廓并保留层级
            method: ContourApproximationModes.Simple // 简化轮廓点
        );

        // 3. 分析并绘制轮廓
        for (int i = 0; i < contours.Count; i++)
        {
            // 计算轮廓属性
            double area = Cv2.ContourArea(contours[i]);
            double perimeter = Cv2.Arclength(contours[i], closed: true);

            // 过滤小面积轮廓(去除噪声)
            if (area < 100) continue;

            // 形状判断(基于轮廓逼近)
            double epsilon = 0.04 * perimeter; // 逼近精度(周长的4%)
            var approx = new VectorPoint();
            Cv2.ApproxPolyDP(contours[i], approx, epsilon, closed: true);
            string shape = approx.Count switch
            {
                3 => "三角形",
                4 => "矩形",
                _ => "圆形" // 大于4边近似为圆形
            };

            // 绘制轮廓(随机颜色)
            Scalar color = new Scalar(
                Cv2.TheRNG().Uniform(0, 256),
                Cv2.TheRNG().Uniform(0, 256),
                Cv2.TheRNG().Uniform(0, 256)
            );
            Cv2.DrawContours(src, contours, i, color, 2, LineTypes.Link8, hierarchy);

            // 绘制形状标签和属性
            Moments moments = Cv2.Moments(contours[i]);
            Point2f center = new Point2f(
                (float)(moments.M10 / moments.M00),
                (float)(moments.M01 / moments.M00)
            );
            Cv2.PutText(src, $"{shape} (面积: {area:F1})", 
                new Point((int)center.X - 50, (int)center.Y),
                HersheyFonts.HersheySimplex, 0.5, Scalar.White, 2);
        }

        // 4. 显示结果
        Cv2.ImShow("轮廓检测结果", src);
        Cv2.ImShow("边缘图像", edges);
        Cv2.WaitKey(0);
        Cv2.DestroyAllWindows();
    }
}
    

案例说明

  1. 环境准备:需安装 OpenCvSharp4 及运行时(NuGet),并准备一张包含简单形状(如三角形、矩形、圆形)的图像(命名为 shapes.jpg)。
  2. 流程解析
    • 预处理:将彩色图转为灰度图,高斯模糊降噪,Canny边缘检测生成二值边缘图;
    • 提取轮廓:用 RetrievalModes.Tree 保留层级,Simple 模式简化轮廓点;
    • 分析与绘制:计算每个轮廓的面积、周长,通过多边形逼近判断形状,绘制轮廓及标签。
  3. 关键技巧
    • ContourArea 过滤小轮廓(噪声);
    • ApproxPolyDP 是形状识别的核心(通过边数判断形状);
    • moments 计算轮廓中心,用于放置标签。

运行后将显示原图中所有轮廓及其属性(形状、面积),直观展示轮廓提取的效果。


二、 FindContours函数的hierarchy 参数详解


在OpenCvSharp中,Cv2.FindContours()hierarchy 参数用于描述检测到的轮廓之间的层次关系。对于复杂图像中的嵌套轮廓(如包含孔洞的物体),这一参数能帮助理清轮廓的父子结构。以下是对 hierarchy 的详细说明和使用示例:


1. HierarchyIndex 结构

每个 HierarchyIndex 实例表示一个轮廓的层次信息,包含四个关键属性:

public struct HierarchyIndex
{
    public int Parent;      // 父轮廓的索引(若无父轮廓则为-1)
    public int Next;        // 同一层级中的下一个轮廓索引
    public int Previous;    // 同一层级中的上一个轮廓索引
    public int FirstChild;  // 第一个子轮廓的索引(若无子轮廓则为-1)
}
关键值的意义
  • Parent: 当前轮廓的上层(父)轮廓的索引。

    • 若为 -1,表示该轮廓是顶级轮廓(没有父轮廓)。
    • 例如,外矩形包含内孔洞时,孔洞的 Parent 是外矩形的索引。
  • Next: 同一层级的下一个兄弟轮廓的索引。

    • 若为 -1,表示没有后续兄弟轮廓。
  • Previous: 同一层级的上一个兄弟轮廓的索引。

    • 若为 -1,表示没有前导兄弟轮廓。
  • FirstChild: 第一个直接子轮廓的索引。

    • 若为 -1,表示该轮廓没有子轮廓。

2. 不同检索模式下的层次结构

hierarchy 的结构高度依赖 RetrievalModes 参数:

  • RetrievalModes.External

    • 仅检测最外层轮廓,所有轮廓的 Parent-1。层次结构退化为单层级,无父子、兄弟关系。
  • RetrievalModes.List

    • 检测所有轮廓,但不建立层次关系。所有轮廓的 ParentFirstChild 均为 -1
  • RetrievalModes.CComp

    • 分层轮廓结构,组织为两级:外层轮廓(Parent=-1)和其直接子孔洞(Parent=外层索引)。
    • 更深的层次(如孔洞中的轮廓)会被合并到父孔洞中的同一层。
  • RetrievalModes.Tree

    • 建立完整的层次树。可表示任意层级的轮廓嵌套,如:A包含B,B包含C,C包含D…

3. hierarchy数组与contours数组的关系

  • contourshierarchy 数组的长度相同,即每个轮廓对应一个 HierarchyIndex
  • 索引对应hierarchy[i] 描述的是 contours[i] 的层次关系。

4. 遍历轮廓层次的示例代码

示例1:遍历顶级父轮廓及其子轮廓
for (int i = 0; i < contours.Length; i++)
{
    // 仅处理父轮廓(Parent=-1)
    if (hierarchy[i].Parent == -1)
    {
        // 绘制父轮廓(例如红色)
        Cv2.DrawContours(result, contours, i, new Scalar(0, 0, 255), 2);

        // 遍历其所有子轮廓(FirstChild链)
        int childIdx = hierarchy[i].FirstChild;
        while (childIdx != -1)
        {
            // 绘制子轮廓(例如绿色,可能是孔洞)
            Cv2.DrawContours(result, contours, childIdx, new Scalar(0, 255, 0), 2);
          
            // 移动到下一个同级子轮廓
            childIdx = hierarchy[childIdx].Next;
        }
    }
}
示例2:统计孔洞数量
int holeCount = 0;
for (int i = 0; i < hierarchy.Length; i++)
{
    // 若当前轮廓是某个父轮廓的孔洞(Parent != -1)
    if (hierarchy[i].Parent != -1)
    {
        holeCount++;
    }
}
Console.WriteLine($"孔洞数量: {holeCount}");

5. 高级应用场景

嵌套轮廓的递归遍历(Tree模式)
void DrawContourWithChildren(Mat image, Point[][] contours, HierarchyIndex[] hierarchy, int contourIdx, Scalar color)
{
    // 绘制当前轮廓
    Cv2.DrawContours(image, contours, contourIdx, color, 2);
  
    // 递归绘制所有子轮廓
    int childIdx = hierarchy[contourIdx].FirstChild;
    while (childIdx != -1)
    {
        DrawContourWithChildren(image, contours, hierarchy, childIdx, color);
        childIdx = hierarchy[childIdx].Next;
    }
}

// 使用方式:从顶级轮廓开始递归绘制所有子轮廓
for (int i = 0; i < contours.Length; i++)
{
    if (hierarchy[i].Parent == -1)
    {
        DrawContourWithChildren(result, contours, hierarchy, i, new Scalar(255, 0, 0));
    }
}

6. 常见问题及解决

  • 问题1:为什么所有轮廓的Parent均为-1?

    • 原因:使用了 RetrievalModes.ExternalList 模式。
    • 解决:改用 CCompTree 模式以获取层次信息。
  • 问题2:无法获取子轮廓的Next索引?

    • 检查点:确认在遍历时沿着 hierarchy[childIdx].Next 移动,而非递增索引。
  • 问题3:如何忽略孔洞?

    • 在绘制或处理时,跳过 Parent != -1 的轮廓。例如:
      if (hierarchy[i].Parent == -1)
      {
          // 仅处理父轮廓(外轮廓)
      }
      

7. 示意图:轮廓层次结构

想象一个矩形包含一个圆形孔洞,而这个孔洞内部又包含一个三角形:

  • contours数组结构

    • contours[0]: 矩形的点集(父轮廓)
    • contours[1]: 圆形孔洞的点集(子轮廓)
    • contours[2]: 三角形的点集(圆形孔洞的子轮廓)
  • hierarchy数组内容

    • hierarchy[0](矩形): Parent=-1, FirstChild=1, Next=-1, Previous=-1
    • hierarchy[1](孔洞): Parent=0, FirstChild=2, Next=-1, Previous=-1
    • hierarchy[2](三角形): Parent=1, FirstChild=-1, Next=-1, Previous=-1

此时遍历过程可能是:先访问矩形轮廓(Parent=-1),然后根据 FirstChild=1 进入孔洞轮廓,接着孔洞的 FirstChild=2 进入三角形轮廓。


通过合理利用 hierarchy 参数,可以精准控制轮廓的处理逻辑,例如区分外边界与内部孔洞,或在多层级结构中实现递归算法。


三、 绘制圆环


在OpenCvSharp中,填充圆环区域(即外圆和内圆之间的环形区域)的关键在于分离外圆和内圆轮廓,并通过轮廓操作生成掩膜。以下是具体实现步骤:


方法1:基于轮廓层次关系(推荐)

核心思路
  • 使用 RetrievalModes.CCompTree 模式检测轮廓,让外圆为父轮廓,内孔为子轮廓。
  • 绘制父轮廓(外圆)并填充,再扣除子轮廓(内圆)区域。
完整代码
using OpenCvSharp;

Mat src = Cv2.ImRead("circle_ring.png", ImreadModes.Color);
Mat gray = new Mat();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);

// 二值化检测边缘清晰的圆环
Mat binary = new Mat();
Cv2.Threshold(gray, binary, 127, 255, ThresholdTypes.BinaryInv); // 反转以便正确识别层次

Point[][] contours;
HierarchyIndex[] hierarchy;
Cv2.FindContours(
    binary,
    out contours,
    out hierarchy,
    mode: RetrievalModes.CComp, // 分层结构
    method: ContourApproximationModes.ApproxSimple
);

// 创建掩膜
Mat mask = Mat.Zeros(binary.Size(), MatType.CV_8UC1);

for (int i = 0; i < contours.Length; i++)
{
    // 仅处理父轮廓(外圆)
    if (hierarchy[i].Parent == -1)
    {
        // 填充外圆至掩膜(白色)
        Cv2.DrawContours(
            mask,
            contours,
            contourIdx: i,
            color: Scalar.White,
            thickness: -1 // -1表示填充
        );

        // 查找其子轮廓(内孔)
        int childIdx = hierarchy[i].FirstChild;
        while (childIdx != -1)
        {
            // 扣除内孔区域(填充黑色)
            Cv2.DrawContours(
                mask,
                contours,
                contourIdx: childIdx,
                color: Scalar.Black,
                thickness: -1
            );
            childIdx = hierarchy[childIdx].Next;
        }
    }
}

// 应用掩膜到原图(例如将环形区域设为红色)
Mat result = new Mat();
src.CopyTo(result, mask); // 仅保留掩膜中白色的区域
Cv2.Add(result, new Scalar(0, 0, 255), result, mask); // 混合红色

Cv2.ImShow("Result", result);
Cv2.WaitKey(0);

方法2:直接绘制环形(已知内外圆半径)

适用场景

如果已知内外圆圆心和半径,可直接用绘图函数生成环形掩膜。

Mat mask = Mat.Zeros(src.Size(), MatType.CV_8UC1);

// 假设圆心和半径已知
Point center = new Point(200, 200);
int outerRadius = 100;
int innerRadius = 60;

// 绘制外圆(填充白色)
Cv2.Circle(mask, center, outerRadius, Scalar.White, -1);
// 绘制内圆(填充黑色,扣除内部)
Cv2.Circle(mask, center, innerRadius, Scalar.Black, -1);

// 应用颜色(示例:绿色环形)
src.SetTo(new Scalar(0, 255, 0), mask);

关键说明

方法1的层次检测要点
  • 二值化反转:使用 ThresholdTypes.BinaryInv 反转颜色,确保外圆为父轮廓,内孔为子轮廓。
  • 掩膜操作
    • 父轮廓填充 Scalar.White(环形外侧)。
    • 子轮廓填充 Scalar.Black(扣除内侧)。
    • 最终掩膜中白色区域为环形。
方法2的适用性
  • 已知几何参数时更高效,如程序生成的图形。无需轮廓检测。

效果对比

方法优点缺点
基于轮廓层次自适应检测任意形状的环形依赖准确的二值化和轮廓层次关系
直接绘制圆快速且精确需已知圆心和半径

通过选择合适的策略,可灵活应对不同场景下的圆环填充需求。

专栏地址:

《 OpenCV功能使用详解200篇 》

《 OpenCV算子使用详解300篇 》

《 Halcon算子使用详解300篇 》

内容持续更新 ,欢迎点击订阅


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-Vision

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值