news 2026/10/13 15:36:47

OpenCV findContours 详解:mode 与 hierarchy 参数实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV findContours 详解:mode 与 hierarchy 参数实战指南

只要碰过 OpenCV 图像处理,十有八九绕不开cv2.findContours。这个函数表面看着简单,但很多人会在mode和hierarchy上翻车,尤其是hierarchy里那一串四个数字,第一次看到基本等于读天书。这篇文章我就把这两个参数彻底讲透:mode的四种取值到底改变了什么,hierarchy的每个数该怎么读、怎么用,全程用同一张测试图跑四种模式,把输出和层级关系直接摆出来对比。不管你是刚入门的小白,还是写过一阵子的老手,都能从这里拿到能直接抄走的写法。

1. 先搞清楚 findContours 返回的东西到底长什么样

1.1 函数签名和不同版本的“坑”

cv2.findContours()的完整签名长这样:

contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

核心参数就三个:输入二值图image、轮廓检索模式mode、轮廓近似方法method。返回值有两个:contours是轮廓列表,每个轮廓是一个 N×1×2 的numpy.ndarray,存的是多边形顶点坐标;hierarchy则是和轮廓一一对应的层级信息数组。

这里必须提醒一个能让老代码直接崩溃的差异:OpenCV 2.x 版本里这个函数返回三个值,常见写法是:

_, contours, hierarchy = cv2.findContours(...) # OpenCV 2.x

到了 OpenCV 3.x 和 4.x,返回值变成了两个:

contours, hierarchy = cv2.findContours(...) # OpenCV 3.x / 4.x

如果你在跑别人老代码时看到 “not enough values to unpack” 的报错,十有八九就是这个版本差异导致的,把左边的_删掉就行。这个坑太常见了,我后面还会单独拿出来说。

第二个要注意的点是输入图必须是二值图。findContours不会去区分灰度层级,它把所有非零像素当成前景,零像素当背景。如果你只做了灰度化没做阈值化,检测出来的轮廓会多到怀疑人生,因为纹理、噪声这些边缘全会被当成轮廓。所以标准流程一定是灰度图 -> 阈值化/边缘提取 -> findContours。

第三个坑是findContours会修改你传入的二值图。这个行为很多文档写得含蓄,但实际测试下来,调用完之后原图确实会被改掉。如果你后面还要再用这张二值图,记得先bin_img.copy()一份再传进去。

1.2 一张能复现的测试图,后面全靠它说话

空讲不好懂,我先搭一张包含“独立物体、嵌套结构、孔洞、岛屿”的测试图,把典型情况集齐。

import cv2 import numpy as np img = np.zeros((480, 520, 3), dtype=np.uint8) # 独立矩形 A cv2.rectangle(img, (30, 30), (140, 140), (255, 255, 255), -1) # 独立矩形 B cv2.rectangle(img, (330, 30), (480, 160), (255, 255, 255), -1) # 大矩形(白色区域),中间挖一个黑色圆孔,孔里再放一个小白色方块 cv2.rectangle(img, (60, 200), (460, 450), (255, 255, 255), -1) cv2.circle(img, (260, 320), 70, (0, 0, 0), -1) cv2.rectangle(img, (235, 295), (285, 345), (255, 255, 255), -1) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, bin_img = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) bin_img = bin_img.copy() # findContours 会改原图,备份一份

这张图里有五个“边界”:矩形 A 的外边界、矩形 B 的外边界、大矩形的外边界、圆孔的内边界、小矩形的外边界。其中圆孔被包在大矩形内部,小矩形又被包在圆孔内部,天然形成了三层嵌套关系,非常适合观察mode对层级的影响。

有一点需要提前说明:findContours的实际检索顺序和图像扫描方式有关,不同环境下轮廓的索引顺序可能不完全一样。本文下面列出的索引顺序是在我的测试环境里跑出来的典型结果,你本地跑大概率一致,但如果索引对不上,不要慌,这并不影响层级逻辑本身。

2. mode 四种模式逐个实测

mode参数有四个常数值:RETR_EXTERNAL、RETR_LIST、RETR_CCOMP、RETR_TREE。它们决定了轮廓的“家族关系”怎么组织。为了看清差异,我把同一张二值图分别用四种模式跑一遍。

2.1 RETR_EXTERNAL:只要最外层,孔洞和岛屿一概不管

这个模式的字面意思就是“只检索最外圈的轮廓”。凡是嵌套在别的白色区域内部的边界,不管它是孔洞边界还是内部岛屿的外边界,统统不返回。

contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print("轮廓数量:", len(contours)) for i, c in enumerate(contours): print(f"contour[{i}]: area={cv2.contourArea(c):.1f}, hierarchy={hierarchy[0][i]}")

典型输出是这样:

轮廓数量: 3 contour[0]: area=12100.0, hierarchy=[ 1 -1 -1 -1] contour[1]: area=20150.0, hierarchy=[ 2 0 -1 -1] contour[2]: area=160000.0, hierarchy=[-1 1 -1 -1]

三个轮廓分别是矩形 A、矩形 B、大矩形外边界。圆孔没有被检测到,因为它是内部边界;圆孔里的小矩形也没被检测到,因为它不是最外层。

注意hierarchy里的四个数,前两个在说“同级的下一个/上一个轮廓”,后两个是“第一个子轮廓/父轮廓”,这里全是-1,也就是说RETR_EXTERNAL根本不建立任何父子关系,所有返回的轮廓都是独立顶层。这个模式适合什么场景?很简单:你只关心物体最外面那一圈边界,比如分离叠放的卡片、统计桌面上有几个硬币,不需要管物体内部有没有孔。

2.2 RETR_LIST:所有轮廓都要,但不建立任何父子关系

RETR_LIST会把图里所有边界都找出来,包括内部孔洞和更里面的岛屿。但是它“一视同仁”,不做父子层级分配,所有轮廓平级。

contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) print("轮廓数量:", len(contours))

典型输出:

轮廓数量: 5 contour[0]: hierarchy=[ 1 -1 -1 -1] contour[1]: hierarchy=[ 2 0 -1 -1] contour[2]: hierarchy=[ 3 1 -1 -1] contour[3]: hierarchy=[ 4 2 -1 -1] contour[4]: hierarchy=[-1 3 -1 -1]

五个轮廓都在,但每个轮廓的first_child和parent都是-1。next和previous只是把五个轮廓按照检索顺序串成一个单向链表。所以在RETR_LIST下,hierarchy能告诉你的只有“轮廓之间大概谁先被找到”,完全没有包含关系。

这个模式适合那些“只要所有轮廓数量、不关心谁在谁里面”的业务。比如统计画面中有多少条封闭边缘,或者第一步先把所有轮廓捞出来,用面积过滤掉小的,再逐个处理。

2.3 RETR_CCOMP:两级分层,把嵌套关系压扁

RETR_CCOMP是这里最容易被误解的一个。它的设计目标是只保留两层结构:顶层是外部边界,第二层是孔洞边界。对于更深层的嵌套,它会做“压缩”,把里面的轮廓重新放到顶层,而不是继续向下挂。

contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) print("轮廓数量:", len(contours))

典型输出:

轮廓数量: 5 contour[0]: hierarchy=[ 1 -1 -1 -1] contour[1]: hierarchy=[ 2 0 -1 -1] contour[2]: hierarchy=[ 4 1 3 -1] contour[3]: hierarchy=[-1 -1 -1 2] contour[4]: hierarchy=[-1 2 -1 -1]

我逐条解读一下。矩形 A、矩形 B 是独立物体,parent=-1。大矩形的外边界是contour[2],它的第一个子轮廓是圆孔contour[3],所以contour[2]的first_child=3。圆孔的parent=2,一眼就能看出它是被大矩形包着的。

重点来了:圆孔里的小矩形岛,在RETR_TREE里应该挂在圆孔下面,但在RETR_CCOMP里它被提升到了顶层,parent=-1。所以大矩形contour[2]的next会指向contour[4],两个原本有嵌套关系的轮廓在层级上变成了“平级”。这就是前面说的“压扁”:它保证整个结构最多只有两级,外部边界是一级,孔洞边界是二级。

哪些场景适合RETR_CCOMP?如果你的业务里只需要区分“这是外框”和“这是孔洞”这两种角色,比如工业检测里判断一个零件有没有通孔,用它会非常顺手。但如果你的图里嵌套超过两层,并且你要精确区分每一层,那RETR_CCOMP这种压缩行为反而会误导你,这时候应该换RETR_TREE。

2.4 RETR_TREE:完整的家族树,每一层都给你记清楚

RETR_TREE会检索所有轮廓,并且把完整的嵌套关系建成一个树形结构。每个轮廓都能找到自己的父轮廓、子轮廓、同级兄弟,层级深度不会丢失。

contours, hierarchy = cv2.findContours(bin_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) print("轮廓数量:", len(contours))

典型输出:

轮廓数量: 5 contour[0]: hierarchy=[ 1 -1 -1 -1] contour[1]: hierarchy=[ 2 0 -1 -1] contour[2]: hierarchy=[-1 1 3 -1] contour[3]: hierarchy=[-1 -1 4 2] contour[4]: hierarchy=[-1 -1 -1 3]

对照看就是:矩形 A、矩形 B、大矩形在顶层;圆孔是大矩形的子轮廓;小矩形岛是圆孔的子轮廓。hierarchy完整记录了这条链:contour[2] -> contour[3] -> contour[4]。以后我想知道“哪个轮廓属于哪个轮廓”,直接用parent字段就能查。

这就是RETR_TREE最大的价值:它给你一份可以递归遍历的拓扑关系。字符识别、图形结构分析、自然场景里找嵌套图形这类需求,基本都要靠它。

2.5 四种模式横向对比

为了看得更明白,我把五种“边界”在四种模式下的返回情况整理成一个表:

边界RETR_EXTERNALRETR_LISTRETR_CCOMPRETR_TREE
矩形 A 外边界返回返回返回返回
矩形 B 外边界返回返回返回返回
大矩形外边界返回返回返回返回
圆孔内边界不返回返回返回,挂在大矩形下返回,挂在大矩形下
小矩形岛外边界不返回返回返回,但被提升到顶层返回,挂在圆孔下
父子关系无无最多两层完整树

一句话记忆法:EXTERNAL只管最大的壳;LIST全收但不管关系;CCOMP只区分外框和孔洞两层;TREE把嵌套关系完整保留成树。

3. hierarchy 不是乱来的:四个数字逐个拆解

3.1 Next、Previous、First_Child、Parent 四个字段

hierarchy的形状是(1, N, 4),其中 N 是轮廓总数。我见过太多人写代码时踩这个形状的坑,hierarchy[i]直接取出来的是一个一维数组的形状是(4,)吗?不对,实际上你要取的是hierarchy[0][i],因为第一维是固定的 1。如果你觉得绕,可以自己 reshape:

h = hierarchy.reshape(-1, 4)

之后h[i]就是第 i 个轮廓的四个关系值。

这四个值的含义分别是:

位置字段含义
0Next同一父轮廓下,下一个兄弟轮廓的索引;没有就是 -1
1Previous同一父轮廓下,上一个兄弟轮廓的索引;没有就是 -1
2First_Child第一个子轮廓的索引;没有就是 -1
3Parent父轮廓的索引;顶层轮廓就是 -1

可以把它理解成一棵家族树:Next/Previous管的是“亲兄弟”之间的左右关系,First_Child管的是“第一个孩子”指向谁,Parent管的是“我爸是谁”。孩子之间会用Next/Previous串成链表,沿着Next走就能遍历某个父轮廓的所有孩子。

3.2 拿 RETR_TREE 的结果手算一遍

还是用前面那张测试图的RETR_TREE输出,我把层级树画出来:

顶层(parent=-1) ├── contour[0] 矩形 A ├── contour[1] 矩形 B └── contour[2] 大矩形外边界 └── contour[3] 圆孔内边界 └── contour[4] 小矩形岛外边界

我一条一条看:

  • contour[0]矩形 A:[1, -1, -1, -1]。Next=1表示它的下一个兄弟是矩形 B;没有上一个兄弟,没有孩子,没有父亲。
  • contour[1]矩形 B:[2, 0, -1, -1]。Next=2指向大矩形,Previous=0指向矩形 A。
  • contour[2]大矩形:[-1, 1, 3, -1]。它是顶层最后一个,Next=-1;First_Child=3表示它的第一个孩子是圆孔。
  • contour[3]圆孔:[-1, -1, 4, 2]。它没有兄弟,Next=-1、Previous=-1;First_Child=4表示它内部还有一个小矩形;Parent=2表示它是大矩形的孩子。
  • contour[4]小矩形岛:[-1, -1, -1, 3]。没有孩子,Parent=3挂在圆孔下面。

通过这张表你可以看到,只要搞懂了四个字段,任何轮廓的层级角色都能直接查出来。

3.3 根据 hierarchy 筛选轮廓的实用写法

光会读没用,要把hierarchy变成能跑的代码才算落地。我直接给三种高频筛法:

只画顶层轮廓(外部物体):

result = np.zeros_like(img) for i in range(len(contours)): if hierarchy[0][i][3] == -1: # parent == -1 cv2.drawContours(result, contours, i, (255, 255, 255), 2)

只画孔洞轮廓(有父轮廓的):

result = np.zeros_like(img) for i in range(len(contours)): if hierarchy[0][i][3] != -1: cv2.drawContours(result, contours, i, (0, 0, 255), 2)

找出带有孔洞的物体轮廓(有子轮廓的):

outer_with_hole = [] for i in range(len(contours)): if hierarchy[0][i][2] != -1: # first_child != -1 outer_with_hole.append(i)

遍历某个轮廓的所有子轮廓,常见于要分析一个物体内部的所有“部件”:

def draw_all_children(idx, img): child = hierarchy[0][idx][2] while child != -1: cv2.drawContours(img, contours, child, (0, 255, 0), 2) child = hierarchy[0][child][0] # 同父的下一个孩子

这段代码里的while循环逻辑要特别记住:父轮廓的First_Child指向第一个孩子,之后每个孩子通过Next指向下一个兄弟,直到Next=-1表示没有兄弟了。这个遍历模式在以后处理字符、图表、工程图纸时非常常用。

4. 实战场景:什么需求选什么 mode

4.1 物体计数:用 RETR_EXTERNAL 避免“孔洞冒充物体”

我有个图像处理 Demo 是统计传送带上的零件数量。零件是一个个实心金属垫片,但部分零件中间有螺钉孔。如果直接用RETR_TREE或者RETR_LIST,一个带孔的垫片会被检出外边界和孔洞边界两条轮廓,统计个数时就把一个零件算成了两个。这时候最简单的解法就是切到RETR_EXTERNAL,只取每个连通区域最外圈的轮廓,孔洞边界直接被忽略,数量立刻变正确。

有同学会担心:“如果两个零件位置重叠怎么办?”重叠其实是另一个问题,和mode无关,要靠形态学处理或者更高级的分割方案。单论“只要外轮廓”这一点,RETR_EXTERNAL是最省心的。

4.2 判断物体是否有孔:CCOMP 的“两级结构”最好用

再举个例子,检测筛网网格是否完整。筛网上的网孔在二值图里就是一个一个黑色的封闭区域,我们关心的是“这张网上到底有多少个通孔”。这种场景用RETR_CCOMP特别合适,因为它天然把外部轮廓放在顶层、把孔洞放在二级。我只需要找那些parent != -1的轮廓,数量就是孔的个数。

如果用RETR_TREE其实也能做,但当筛网层级复杂时,你可能会不小心把更深的岛屿轮廓也数进去。RETR_CCOMP的“压扁”行为在这里反而是优点,因为业务逻辑根本不关心孔里面还有没有东西。

4.3 复杂嵌套结构:TREE 是唯一靠谱的选择

如果你做的是工程图纸识别、电路板元件分析这类图像,里面的结构经常是“外框里有文字,文字里有封闭笔画,笔画内部又有细线”这种多层级嵌套。这个时候RETR_CCOMP的两级结构会把你的拓扑信息打乱,RETR_LIST又没有父子关系,只有RETR_TREE能把每一层都留在原位。

我在某个跨平台图像分析模块里处理灰度化后的 PCB 板图片时,用的就是RETR_TREE。通过hierarchy的父子链,我可以从板子外框出发,逐级下钻到每个焊盘、每个过孔,完全不需要额外维护一份“谁属于谁”的映射表,代码结构清爽很多。

提醒一句:RETR_TREE并不是所有场景的万能解。它需要额外维护树结构,轮廓数量巨大时内存和耗时会稍高;如果你的业务根本不需要层级关系,用RETR_LIST就好,别为了“功能多”选 TREE 然后背性能包袱。

5. 常见坑和排查速查表

我在代码评审和日常排障里反复见到这些问题,直接汇总成一个表,方便你遇到问题时对照。

症状原因解决办法
findContours返回值解包报错OpenCV 2.x 返回 3 个值,3.x/4.x 返回 2 个值去掉代码左端的_和第一个变量
检测出的轮廓数量爆炸输入图不是真正的二值图,灰度纹理被当成轮廓先threshold或Canny,确保输入只有 0 和 255
findContours调用后原图变了函数会原地修改传入的图像调用前执行bin_img = bin_img.copy()
hierarchy[i]取数报维度错误hierarchy形状是(1, N, 4)写hierarchy[0][i],或先.reshape(-1, 4)
用RETR_EXTERNAL检不出内孔这是预期行为,不是 bug需要孔洞请换RETR_CCOMP或RETR_TREE
drawContours画出乱七八糟的线可能是轮廓索引越界,或传入了非二值图打印len(contours)和contour的 dtype 检查
轮廓顺序和预期不一致轮廓按图像扫描顺序返回,不保证面积/层级序用cv2.contourArea和hierarchy自己重新排序
想用轮廓做掩码但尺寸不对画轮廓时的图像尺寸和原图不一致确认绘制目标图像和原图宽高一致

除了这些,再补充两个很少被写进文档但实际很关键的心得:

第一,method参数和mode是两码事。mode管“轮廓之间什么关系”,method管“轮廓存储多少点”。最常用的CHAIN_APPROX_SIMPLE会把直线段压缩成端点,只保留“必要的拐点”,轮廓点数少、内存小;CHAIN_APPROX_NONE保留所有边界像素点,点数爆多。默认能用SIMPLE就用SIMPLE,除非你后续需要精确到每个像素的轮廓坐标。

第二,hierarchy的索引是定位轮廓的关键,但很多人会把“轮廓列表里的索引”和“业务对象编号”混淆。比如你以为contours[0]一定是最大的轮廓,其实不是,它只是被检索到的第一个轮廓。如果你要按面积筛选,先跑一遍:

contours = sorted(contours, key=cv2.contourArea, reverse=True)

再用排序后的索引去对应hierarchy,否则层级关系很容易对不上。

最后说说我自己的体会。findContours的mode和hierarchy属于那种“看起来简单、用起来总差一口气”的知识点。我最初做图像处理时也犯过傻,拿着RETR_EXTERNAL去找孔洞,找了半天一无所获,还以为是图像预处理出了问题。后来把四种模式在同样一张测试图上的输出全打印出来,才彻底想明白它的设计逻辑。所以如果你现在也被这个参数绕得头疼,不用急,照着文章里的测试图自己跑一遍,打印出hierarchy慢慢看,很快就能建立起直觉。真正复杂的需求,比如嵌套字符、图纸层级、缺陷孔洞判断,靠的也就是今天这几行代码的组合。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 15:34:39

项目为什么不用VO?从分层架构到工程取舍的全面解析

“为什么我们项目不用VO?”午饭时间,实习生端着饭盒凑到我工位,问了一句让我愣住的话:“我看项目里 Controller 直接返回了 Entity,为什么不用 VO 啊?”这个问题不是那种能靠背八股文答出来的题。它背后牵涉…

作者头像 李华
网站建设 2026/10/13 15:32:35

上下文管理实战:从滑动窗口到检索增强的工程化方案

1. 上下文管理到底在管什么 第一次听到“上下文管理”这个词,很多人会下意识觉得它是个很虚的概念,像是某种管理学的黑话。但如果你写过稍微复杂一点的对话系统、智能体应用,或者做过长文档问答,你就会发现这四个字背后全是实打实…

作者头像 李华
网站建设 2026/10/13 15:32:08

MSCNN人群密度估计数据集制作全流程指南

1. 项目概述:为什么人群密度估计需要专门制作数据集,而不是直接用现成图像?“基于MSCNN的人群密度估计之数据集制作”——这个标题里藏着三个关键动作:“MSCNN”是模型架构,“人群密度估计”是任务目标,“数…

作者头像 李华
网站建设 2026/10/13 15:31:06

137B编码模型PC本地落地:端侧推理与Agent权限边界实战

1. 这套 137B 编码模型的 PC 落地,到底解决了什么问题先别被“137B”这个大数字唬住。我刚开始听到“把 137B 编码模型搬到 PC”这个说法时,第一反应是:这怕不是拿服务器显存硬塞进台式机里跑的噱头?后来仔细跟了一圈方案细节&…

作者头像 李华
网站建设 2026/10/13 15:30:07

帝国CMS采集伪原创发布推送全链路插件拆解

简介:这是一款面向帝国CMS网站管理员与SEO从业者的免费插件资源,核心解决网站内容更新与搜索引擎优化流程繁琐的问题。插件整合关键词采集、文章采集、伪原创改写、自动发布及自动推送搜索引擎等环节,帮助用户以较低人力成本维持站点内容更新…

作者头像 李华
网站建设 2026/10/13 15:29:56

EarlGrey 贡献指南:从 CLA 签署到合并的完整协作流程解析

测试 【免费下载链接】EarlGrey :tea: iOS UI Automation Test Framework 项目地址: https://gitcode.com/gh_mirrors/ea/EarlGrey 点击查看 免费下载 EarlGrey 是 Google 开源的 iOS UI 自动化测试框架,本指南以仓库根目录的 CONTRIBUTING.md 为核心&a…

作者头像 李华