contribute上次修改:2026-09-29

3.3 轮廓层级

总目录:1.1 OpenCV介绍与学习路线 | 第三章:边缘、轮廓与几何 | 编号:3.3

← 上一节:3.2 查找与绘制轮廓 | 下一节:3.4 轮廓几何特征 →


3.3 轮廓层级

一张白纸上画一个黑色圆环,圆环外边界是轮廓,里面的孔也是轮廓。谁包着谁,就是轮廓层级要表达的关系。

一、为什么轮廓会有父子关系

外框
└─ 内孔
   └─ 孔里的小岛

只找最外面的物体时不需要层级;要区分边框、孔洞和内部图案时,层级非常有用。

二、hierarchy 的四个数字

findContours 返回的层级常见形状:

print(hierarchy.shape)  # (1, N, 4)
  • 1:外层套了一个多余的大括号(OpenCV 历史遗留)
  • N:一共找到 N 个轮廓
  • 4:每 1 个轮廓,配套 4 个层级信息:[Next, Previous, First_Child, Parent]

hierarchy[0]:把最外层那个没用的1维度剥掉,得到形状 (N,4),也就是 N 个轮廓,每个轮廓 4 个信息。

第 i 个轮廓:

next_i, prev_i, child_i, parent_i = hierarchy[0, i]
位置 含义 -1 表示
[0] next 同层的下一个轮廓 没有
[1] prev 同层的上一个轮廓 没有
[2] first_child 第一个子轮廓 没有孩子
[3] parent 父轮廓 自己是最外层

它不是“深度数组”,而是用索引串起的一棵树。 3.3-file-20260909152046372

图里一共7 条轮廓:

  1. 绿色外框:轮廓 0(顶层)
  2. 红色大框:轮廓 1(绿色的子)
  3. 红色中框:轮廓 2(红色大框的子)
  4. 红色小框:轮廓 3(红色中框的子)
  5. 蓝色大框:轮廓 4(绿色的子,和红色大框轮廓1是同一层级)
  6. 蓝色中框:轮廓 5(蓝色大框的子)
  7. 蓝色小框:轮廓 6(蓝色中框的子)
轮廓0【绿色外框】: [Next=-1, Prev=-1, FirstChild=1, Parent=-1]
轮廓1【红大】:     [Next=4,  Prev=-1, FirstChild=2, Parent=0]
轮廓2【红中】:     [Next=-1, Prev=-1, FirstChild=3, Parent=1]
轮廓3【红小】:     [Next=-1, Prev=-1, FirstChild=-1, Parent=2]
轮廓4【蓝大】:     [Next=-1, Prev=1,  FirstChild=5, Parent=0]
轮廓5【蓝中】:     [Next=-1, Prev=-1, FirstChild=6, Parent=4]
轮廓6【蓝小】:     [Next=-1, Prev=-1, FirstChild=-1, Parent=5]

关于顺序 用 RETR TREE,会给所有检测到的轮廓分配编号 编号顺序 = findContours 算法沿着图像扫描、第一次碰到轮廓边界的顺序。 这个遍历策略叫深度优先遍历 DFS(RETR_TREE 底层就是 DFS):

碰到一个轮廓 A → 立刻深入 A 里面,把 A 所有子、孙子全部找完,分配完编号; 全部挖干净之后,才回到 A 的父节点,继续找 A 的 Next 兄弟。

三、四种检索模式

RETR=Retrieve检索

RETR_EXTERNAL

只保留最外层轮廓:

contours, hierarchy = cv2.findContours(
    binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)

目标互不嵌套、只关心外轮廓时最省心。

RETR_LIST

找出全部轮廓,但不建立父子关系。所有轮廓在 hierarchy 里相当于同层。

RETR_CCOMP

组织为两级:外部边界与孔洞。若孔洞里又有物体,该物体会回到顶层,而不是继续形成任意深度的树。

RETR_TREE

保留完整嵌套关系:父、子、孙都能追踪。

只要最外层        → EXTERNAL
全要但不关心关系  → LIST
只需外层/孔洞两级 → CCOMP
要完整包含关系    → TREE

四、怎样筛父轮廓和子轮廓

只处理最外层

for i, contour in enumerate(contours):
    parent = hierarchy[0, i, 3]
    if parent != -1:
        continue
    # contour 是最外层轮廓

只处理某个父轮廓的直接孩子

border_index = 0

for i, contour in enumerate(contours):
    if hierarchy[0, i, 3] == border_index:
        # contour 是 0 号轮廓的直接子轮廓
        pass

遍历一条子链

child = hierarchy[0, border_index, 2]

while child != -1:
    contour = contours[child]
    child = hierarchy[0, child, 0]  # 同层下一个

五、不要把“内部”简单等同于“目标”

原笔记里有一种写法:父轮廓是 -1 就当边框跳过,其余全当内部目标。这只在二值图拓扑刚好符合预期时成立。

现实中还会出现:

  • 字母 O 自己带孔洞;
  • 反光把一个目标切出子轮廓;
  • 前景背景反转后,父子含义跟着翻转;
  • 多层嵌套时,孙轮廓不一定是你要的目标。

所以层级只提供“谁包含谁”,最终还要结合:

  • 面积范围;
  • 位置是否落在边框 ROI 内;
  • 颜色或灰度;
  • 形状特征;
  • 深度是奇数还是偶数。

六、参考


← 上一节:3.2 查找与绘制轮廓 | 下一节:3.4 轮廓几何特征 →

留言区

不收集任何个人信息;昵称只绑定浏览器里的随机 Cookie。