本文将学习轮廓的层次结构,即轮廓中的父子关系。
理论基础
在最近几篇关于轮廓的文章中,我们使用了OpenCV提供的与轮廓相关的几个函数。
是当使用cv2.findContours()
函数在图像中找到轮廓时,传递了一个参数,轮廓检索模式。
通常传递cv2.RETR_LIST
或cv2.RETR_ TREE
。但这实际上意味着什么呢?
此外,在输出中得到了三个数组,第一个是图像,第二个是轮廓,还有一个输出,称之为层次结构(请查看前面文章中的代码)。 但我们从未在任何地方使用过这种等级制度。那么这个层次结构、目的和函数参数有什么关系呢?
这就是我们将在本文中讨论的内容。
层级结构
通常使用cv2.findContours()
函数来检测图像中的对象,有时物体位于不同的位置。
但在某些情况下,某些形状位于其他形状的内部。
就像嵌套的数字一样。在这种情况下称外部为父,内部为子。
这样,图像中的轮廓彼此之间就有一定的关系。
我们可以指定一个轮廓是如何相互连接的,比如,它是其他轮廓的子轮廓,还是父轮廓等。
这种关系的表示称为层次结构。
如下面的示例图像:
在该图像中,存在若干形状(我已将其编号为 0-5)。 其中,2 和 2a 表示最外层方框的外部轮廓和内部轮廓。
在此,轮廓0、1、2属于外部轮廓/最外层轮廓。 可以说它们位于层级0(hierarchy-0),或者说它们处于同一层级。
接下来分析轮廓-2a。其性质可定义为: 层级归属:作为轮廓-2的子轮廓(或反言之,轮廓-2是其父轮廓), 属于第一级子层级(hierarchy-1)。 嵌套关系:轮廓-3是轮廓-2的子轮廓,属于第二级子层级, 轮廓-4和轮廓-5是轮廓-3a的子轮廓,处于最深层级。 根据当前编号规则,轮廓-4可视为轮廓-3a的首个子轮廓(但轮廓-5也可能具有该属性)
解释这些概念是为了帮助理解以下术语:相同层级(same hierarchy level)、外部轮廓(external contour)、 子轮廓(child contour)、父轮廓(parent contour) 和首个子轮廓(first child) 等。 现在,让我们进入OpenCV的具体实现。
OpenCV中的层级结构表示
每个轮廓都包含其所属层级、子轮廓和父轮廓等信息。
OpenCV 使用一个包含四个值的数组来表示这些关系:[Next, Previous, First_Child, Parent]
。
Next
表示同一层级的下一个轮廓,以图中的轮廓-0 为例:
- 与其同级的下一个轮廓是轮廓-1,因此 Next = 1
- 轮廓-1 的同级下一个轮廓是轮廓-2,故 Next = 2
- 轮廓-2 没有同级后续轮廓,因此 Next = -1
- 轮廓-4 与轮廓-5 同级,其 Next = 5
Previous
表示同级中的上一个轮廓:
与Next
参数同理,但方向相反:
- 轮廓1的上一个是轮廓0(Previous=0)
- 轮廓2的上一个是轮廓1(Previous=1)
- 轮廓0作为起始轮廓无上一个(Previous=-1)
First_Child
表示首个直接子轮廓:
子轮廓索引规则:
- 轮廓2的子轮廓是2a → First_Child=2a的索引
- 轮廓3a有两个子轮廓(4和5),但仅记录第一个 →
First_Child
=4
Parent
表示父轮廓索引:
与First_Child
互为逆关系:
- 轮廓4和5的
Parent
都指向3a - 轮廓3a的
Parent
指向3 - 顶层轮廓
Parent
=-1
注意: 当轮廓无子节点或父节点时,对应字段值为-1。
至此,我们已经理解了OpenCV的层级结构表示方式,接下来可以基于前文图示案例, 深入探讨OpenCV中的轮廓检索模式(Contour Retrieval Modes)。 具体解析以下标志位的含义:
cv2.RETR_LIST
cv2.RETR_TREE
cv2.RETR_CCOMP
cv2.RETR_EXTERNAL
轮廓检索模式详解
RETR_LIST
这是四种检索模式中最简单的一种。 该模式仅提取所有轮廓,不建立任何父子关系。 在此模式下:
- 所有轮廓都处于同一层级
- 父轮廓和子轮廓被平等对待
- 层级数组中的第3、4位(
First_Child
和Parent
)恒为-1 - 仅
Next
和Previous
字段保持有效值
hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[ 3, 1, -1, -1],
[ 4, 2, -1, -1],
[ 5, 3, -1, -1],
[ 6, 4, -1, -1],
[ 7, 5, -1, -1],
[-1, 6, -1, -1]]])
hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[-1, 1, -1, -1]]])
在只需要提取外部轮廓的情况下,可以使用这个标志。 某些场景下会很有用。
RETR\_CCOMP
RETR_CCOMP
(检索所有轮廓并组织为两层层级结构),
这个标志会检索所有轮廓,并将其组织成一个两层级的层次结构:
- 层级-1:物体的外部轮廓(即边界)
- 层级-2:物体内部的孔洞轮廓(如有) 如果孔洞内部还嵌套了其他物体,那么该物体的轮廓又会回到层级-1,而它的孔洞则属于层级-2,以此类推。
举个简单的例子:想象一个黑色背景上的“大白圈”(数字0的形状)。
- 外层圆圈属于 层级-1
- 内层圆圈(孔洞部分)属于层级-2
可以用一张简单的示意图来说明。 图中用红色数字标注了轮廓的检测顺序(与OpenCV的检测顺序一致), 并用绿色数字(1或2)表示它们所属的层级。
contour-0:
- 同一层级的下一个轮廓是 contour-3(Next=3)
- 没有前一个轮廓(Previous=-1)
- 第一个子轮廓是层级-2 的 contour-1(
First_Child=1
) - 没有父轮廓(Parent=-1,因为它是顶层轮廓)→ 层级数组表示为
[3, -1, 1, -1]
contour-1(层级-2):
- 同一层级的下一个轮廓(同属 contour-0 的子轮廓)是 contour-2(Next=2)
- 没有前一个轮廓(Previous=-1)
- 没有子轮廓(
First_Child=-1
) - 父轮廓是 contour-0(Parent=0)→ 数组为
[2, -1, -1, 0]
contour-2(层级-2):
- 没有下一个同级轮廓(Next=-1,contour-0 的子轮廓到此结束)
- 前一个同级轮廓是 contour-1(Previous=1)
- 没有子轮廓(
First_Child
=-1) - 父轮廓是 contour-0(Parent=0)→ 数组为
[-1, 1, -1, 0]
contour-3(层级-1):
- 同一层级的下一个轮廓是 contour-5(Next=5)
- 前一个轮廓是 contour-0(Previous=0)
- 子轮廓是层级-2 的 contour-4(
First_Child
=4) - 没有父轮廓(Parent=-1)→ 数组为
[5, 0, 4, -1]
contour-4(层级-2):
- 没有同级轮廓(Next=-1,它是 contour-3 的唯一子轮廓)
- 没有前一个同级轮廓(Previous=-1)
- 没有子轮廓(
First_Child
=-1) - 父轮廓是 contour-3(Parent=3)→ 数组为
[-1, -1, -1, 3]
剩下的轮廓可以依此类推。最终得到的层级关系如下:
hierarchy
array([[[ 3, -1, 1, -1],
[ 2, -1, -1, 0],
[-1, 1, -1, 0],
[ 5, 0, 4, -1],
[-1, -1, -1, 3],
[ 7, 3, 6, -1],
[-1, -1, -1, 5],
[ 8, 5, -1, -1],
[-1, 7, -1, -1]]])
RETR\_TREE
RETR_TREE(完整层级树结构) 这是最强大的模式,堪称“完美先生”!它会检索所有轮廓,并构建一个完整的家族层级树,明确标注谁是谁的祖父、父亲、儿子、孙子……甚至更远的辈分关系。
示例分析:
以之前的图像为例,改用 cv2.RETR_TREE
重新检测轮廓,并按 OpenCV 的结果重新排序:
- 红色数字:轮廓的检测顺序(如 contour-0、contour-1…)
- 绿色数字:轮廓在层级树中的深度(层级顺序)。
Contour-0(层级-0,最外层轮廓)
- 同级下一个轮廓:Contour-7(Next=7)
- 同级前一个轮廓:无(Previous=-1)
- 第一个子轮廓:Contour-1(
First_Child
=1) - 父轮廓:无(Parent=-1,因为它是顶层轮廓)→ 层级数组:
[7, -1, 1, -1]
Contour-1(层级-1,Contour-0 的子轮廓)
- 同级下一个轮廓:无(Next=-1,它是 Contour-0 的唯一直接子轮廓)
- 同级前一个轮廓:无(Previous=-1)
- 第一个子轮廓:Contour-2(
First_Child
=2) - 父轮廓:Contour-0(Parent=0)→ 层级数组:
[-1, -1, 2, 0]
剩下的可以自己试试。
hierarchy
array([[[ 7, -1, 1, -1],
[-1, -1, 2, 0],
[-1, -1, 3, 1],
[-1, -1, 4, 2],
[-1, -1, 5, 3],
[ 6, -1, -1, 4],
[-1, 5, -1, 4],
[ 8, 0, -1, -1],
[-1, 7, -1, -1]]])