恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
OpenCV findContours 详解:mode 与 hierarchy 参数实战指南
首页
资讯中心
/
OpenCV findContours 详解:mode 与 hierarchy 参数实战指南
OpenCV findContours 详解:mode 与 hierarchy 参数实战指南
发布时间:2026/10/13 15:36:50
只要碰过 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), dtypenp.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(fcontour[{i}]: area{cv2.contourArea(c):.1f}, hierarchy{hierarchy[0][i]})典型输出是这样轮廓数量: 3 contour[0]: area12100.0, hierarchy[ 1 -1 -1 -1] contour[1]: area20150.0, hierarchy[ 2 0 -1 -1] contour[2]: area160000.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_child3。圆孔的parent2一眼就能看出它是被大矩形包着的。重点来了圆孔里的小矩形岛在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。如果你觉得绕可以自己 reshapeh hierarchy.reshape(-1, 4)之后h[i]就是第 i 个轮廓的四个关系值。这四个值的含义分别是位置字段含义0Next同一父轮廓下下一个兄弟轮廓的索引没有就是 -11Previous同一父轮廓下上一个兄弟轮廓的索引没有就是 -12First_Child第一个子轮廓的索引没有就是 -13Parent父轮廓的索引顶层轮廓就是 -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]。Next1表示它的下一个兄弟是矩形 B没有上一个兄弟没有孩子没有父亲。contour[1]矩形 B[2, 0, -1, -1]。Next2指向大矩形Previous0指向矩形 A。contour[2]大矩形[-1, 1, 3, -1]。它是顶层最后一个Next-1First_Child3表示它的第一个孩子是圆孔。contour[3]圆孔[-1, -1, 4, 2]。它没有兄弟Next-1、Previous-1First_Child4表示它内部还有一个小矩形Parent2表示它是大矩形的孩子。contour[4]小矩形岛[-1, -1, -1, 3]。没有孩子Parent3挂在圆孔下面。通过这张表你可以看到只要搞懂了四个字段任何轮廓的层级角色都能直接查出来。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. 实战场景什么需求选什么 mode4.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 和 255findContours调用后原图变了函数会原地修改传入的图像调用前执行bin_img bin_img.copy()hierarchy[i]取数报维度错误hierarchy形状是(1, N, 4)写hierarchy[0][i]或先.reshape(-1, 4)用RETR_EXTERNAL检不出内孔这是预期行为不是 bug需要孔洞请换RETR_CCOMP或RETR_TREEdrawContours画出乱七八糟的线可能是轮廓索引越界或传入了非二值图打印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, keycv2.contourArea, reverseTrue)再用排序后的索引去对应hierarchy否则层级关系很容易对不上。最后说说我自己的体会。findContours的mode和hierarchy属于那种“看起来简单、用起来总差一口气”的知识点。我最初做图像处理时也犯过傻拿着RETR_EXTERNAL去找孔洞找了半天一无所获还以为是图像预处理出了问题。后来把四种模式在同样一张测试图上的输出全打印出来才彻底想明白它的设计逻辑。所以如果你现在也被这个参数绕得头疼不用急照着文章里的测试图自己跑一遍打印出hierarchy慢慢看很快就能建立起直觉。真正复杂的需求比如嵌套字符、图纸层级、缺陷孔洞判断靠的也就是今天这几行代码的组合。