OpenCV-Python (官方)中文教程(部分一)_Day19

21.4轮廓的其他函数

(1).凸缺陷(Convexity Defects)

前面我们已经学习了轮廓的凸包,对象上的任何凹陷都被成为凸缺陷。OpenCV 中有一个函数 cv2.convexityDefect() 可以帮助我们找到凸缺陷。函数调用如下:

hull = cv2.convexHull(cnt,returnPoints = False)

defects = cv2.convexityDefects(cnt,hull)

注意:如果要查找凸缺陷,在使用函数  cv2.convexHull  找凸包时,参数returnPoints 一定要是 False。

它会返回一个数组,其中每一行包含的值是 [起点,终点,最远的点,到最 远点的近似距离]。我们可以在一张图上显示它。我们将起点和终点用一条绿线 连接,在最远点画一个圆圈,要记住的是返回结果的前三个值是轮廓点的索引。 所以我们还要到轮廓点中去找它们。

import cv2

import numpy as np

# 创建白色背景图像(800x800像素,更大更清晰)

img = np.ones((800, 800, 3), dtype=np.uint8) * 255

# 定义五边形轮廓坐标(包含一个明显凹陷)

# 点顺序:左上 -> 右上 -> 凹陷点 -> 右下 -> 左下

contour_points = np.array([

    [[200, 150]],  # 左上

    [[600, 150]],  # 右上

    [[500, 400]],  # 凹陷点(中间靠左)

    [[600, 650]],  # 右下

    [[200, 650]]   # 左下

], dtype=np.int32)

# 1. 绘制原始五边形轮廓(绿色粗线)

cv2.drawContours(img, [contour_points], -1, (0, 180, 0), 10)

# 2. 计算并绘制凸包(红色粗线)

convex_hull = cv2.convexHull(contour_points)

cv2.drawContours(img, [convex_hull], -1, (0, 0, 255), 5)

# 3. 计算凸性缺陷(凹陷区域)

hull_indices = cv2.convexHull(contour_points, returnPoints=False)

defects = cv2.convexityDefects(contour_points, hull_indices)

# 4. 绘制缺陷特征

if defects is not None:

    for i in range(defects.shape[0]):

        # 提取缺陷数据:起点、终点、最远点、距离

        start_idx, end_idx, far_idx, distance = defects[i, 0]

        

        # 获取坐标点

        start_point = tuple(contour_points[start_idx][0])

        end_point = tuple(contour_points[end_idx][0])

        far_point = tuple(contour_points[far_idx][0])

        

        # 绘制缺陷连接线(蓝色实线)

        cv2.line(img, start_point, end_point, (255, 0, 0), 3)

        

        # 绘制凹陷最深点(黄色大圆点+黑色中心)

        cv2.circle(img, far_point, 12, (0, 220, 220), -1)

        cv2.circle(img, far_point, 3, (0, 0, 0), -1)

# 5. 添加中文标注(需要支持中文的字体)

# 模拟中文标注(实际运行时需要替换为中文字体)

font = cv2.FONT_HERSHEY_SIMPLEX

cv2.putText(img, 'Original (Green)', (50, 70), font, 1.2, (0, 150, 0), 2)

cv2.putText(img, 'Convex Hull (Red)', (50, 110), font, 1.2, (0, 0, 200), 2)

cv2.putText(img, 'Defect (Blue/Yellow)', (450, 70), font, 1.2, (200, 0, 0), 2)

# 显示结果

cv2.imshow('凸包缺陷检测演示', img)

cv2.waitKey(0)

cv2.destroyAllWindows()

# 保存图像

cv2.imwrite('凸包缺陷检测结果.jpg', img)

结果:

(2).多边形点测试(pointPolygonTest)

求解图像中的一个点到一个对象轮廓的最短距离。如果点在轮廓的外部, 返回值为负;如果在轮廓上,返回值为 0; 如果在轮廓内部,返回值为正。

下面我们以点(50,50)为例:

dist = cv2.pointPolygonTest(cnt,(50,50),True)

此函数的第三个参数是 measureDist。如果设置为 True,就会计算最短距离。如果是   False,只会判断这个点与轮廓之间的位置关系(返回值为+1,-1,0)。

注意:如果不需要知道具体距离,建议将第三个参数设为 False,这样速 度会提高 2 到 3 倍.

(3).形状匹配(Match Shapes)

函数 cv2.matchShape() 可以帮我们比较两个形状或轮廓的相似度。如果返回值越小,匹配越好。它是根据 Hu 矩值来计算的。文档中对不同的方法都有解释。

我们试着将下面的图形进行比较:

import cv2

img1 = cv2.imread('star.jpg',0)

img2 = cv2.imread('star2.jpg',0)

ret, thresh = cv2.threshold(img1, 127, 255,0)

ret, thresh2 = cv2.threshold(img2, 127, 255,0)

contours,hierarchy = cv2.findContours(thresh,2,1)

cnt1 = contours[0]

contours,hierarchy = cv2.findContours(thresh2,2,1)

cnt2 = contours[0]

ret = cv2.matchShapes(cnt1,cnt2,1,0.0)

print (ret)

得到的结果是:

• A 与自己匹配 0.0

• A 与 B 匹配 0.001946

• A 与 C 匹配 0.326911

看见了吗,及时发生了旋转对匹配的结果影响也不是非常大。

注意:Hu  矩是归一化中心矩的线性组合,之所以这样做是为了能够获取代表图像的某个特征的矩函数,这些矩函数对某些变化如缩放,旋转,镜像映射(除了 h1)具有不变形。此段摘自《学习 OpenCV》中文版。

21.5轮廓的层次结构

在前面的内容中我们使用函数 cv2.findContours 来查找轮廓,我们需 要传入一个参数:轮廓提取模式(Contour_Retrieval_Mode)。我们总是把它设置为 cv2.RETR_LIST 或者 cv2.RETR_TREE,效果还可以。同时,我们得到的结果包含 3 个数组:第一个是图像,第二个是轮廓,第三 个是层次结构。

(1).层次结构的概念

通常我们使用函数 cv2.findContours 在图片中查找一个对象。有时对 象可能位于不同的位置。还有些情况,一个形状在另外一个形状的内部。这种情况下我们称外部的形状为父,内部的形状为子。按照这种方式分类,一幅图 像中的所有轮廓之间就建立父子关系。这样我们就可以确定一个轮廓与其他轮 廓是怎样连接的,比如它是不是某个轮廓的子轮廓,或者是父轮廓。这种关系就成为组织结构

下图就是一个简单的例子:

在这幅图像中,我给这几个形状编号为 0-5。2 和 2a 分别代表最外边矩形的外轮廓和内轮廓。在这里边轮廓 0,1,2  在外部或最外边。我们可以称他们为(组织结构)0 级,简单来说就是他们属于同一级。我们把2a当成轮廓 2  的子轮廓,它就成为(组织结构)第1 级。同样轮廓 3 是轮廓 2 的子轮廓,成为(组织结构)第 3 级。最后轮廓4,5 是轮廓 3a 的子轮廓,成为(组织结构)4 级(最后一级)。按照这种方式 给这些形状编号,我们可以说轮廓 4  是轮廓 3a  的子轮廓(当然轮廓 5也是)。

为什么2a和3不是同一级?

结构位置不同:

2a是轮廓2的"内轮廓"(子轮廓)

3是轮廓2的另一个独立子轮廓

它们虽然都是轮廓2的子级,但属于不同的分支

(2).OpenCV中的层次结构

不管层次结构是什么样的,每一个轮廓都包含自己的信息:谁是父,谁 是子等。OpenCV 使用一个含有四个元素的数组表示。[Next,Previous, First_Child,Parent]

Next   表示同一级组织结构中的下一个轮廓。

以上图中的轮廓 0 为例,轮廓 1 就是他的 Next。同样,轮廓 1 的 Next是 2,Next=2。

那轮廓 2 呢?在同一级没有 Next。这时 Next=-1。而轮廓 4 的 Next为 5,所以它的 Next=5。

Previous    表示同一级结构中的前一个轮廓。

与前面一样,轮廓 1 的 Previous 为轮廓 0,轮廓 2 的 Previous 为轮 廓 1。轮廓 0 没有 Previous,所以 Previous=-1。

First_Child   表示它的第一个子轮廓。

没有必要再解释了,轮廓 2 的子轮廓为 2a。所以它的 First_Child 为 2a。那轮廓 3a 呢?它有两个子轮廓。但是我们只要第一个子轮廓,所以是轮 廓 4(按照从上往下,从左往右的顺序排序)。

Parent 表示它的父轮廓。

与 First_Child 刚好相反。轮廓 4 和 5 的父轮廓是 3a。而轮廓 3a的父轮廓是3。

注意:如果没有父或子,就为 -1。

现在我么了解了 OpenCV 中的轮廓组织结构。我们还是根据上边的图片 再学习一下 OpenCV 中的轮廓检索模式。

cv2.RETR_LIST,cv2.RETR_TREE,cv2.RETR_CCOMP,cv2.RETR_EXTERNAL

到底代表什么意思?

(3).轮廓检索模式

RETR_LIST 从解释的角度来看,这中应是最简单的。它只是提取所有的轮 廓,而不去创建任何父子关系。换句话说就是“人人平等”,它们属于同一级组织轮廓。

所以在这种情况下,组织结构数组的第三和第四个数都是 -1。但是,很明显,Next 和 Previous 要有对应的值,你可以自己试着看看。下面就是得到的结果,每一行是对应轮廓的组织结构细节。例如,第一 行对应的是轮廓 0。下一个轮廓为 1,所以  Next=1。前面没有其他轮廓,所 以   Previous=0。接下来的两个参数就是-1,与刚才我们说的一样。

如果你不关心轮廓之间的关系,这是一个非常好的选择。

RETR_EXTERNAL 如果你选择这种模式的话,只会返回最外边的的轮廓, 所有的子轮廓都会被忽略掉。

所以在上图中使用这种模式的话只会返回最外边的轮廓(第 0  级):轮廓0,1,2。下面是我选择这种模式得到的结果:

当你只想得到最外边的轮廓时,你可以选择这种模式。这在有些情况下很 有用。

RETR_CCOMP 在这种模式下会返回所有的轮廓并将轮廓分为两级组织结 构。例如,一个对象的外轮廓为第 1  级组织结构。而对象内部中空洞的轮廓为第 2 级组织结构,空洞中的任何对象的轮廓又是第 1  级组织结构。空洞的组织结构为第 2 级。想象一下一副黑底白字的图像,图像中是数字 0。0 的外边界属于第一级 组织结构,0  的内部属于第 2 级组织结构。

我们可以以下图为例简单介绍一下。我们已经用红色数字为这些轮廓编号, 并用绿色数字代表它们的组织结构。顺序与  OpenCV   检测轮廓的顺序一直。

现在考虑轮廓 0,它的组织结构为第 1 级。其中有两个空洞 1 和 2, 它们属于第 2 级组织结构。所以对于轮廓 0 来说跟他属于同一级组织结构的 下一个(Next)是轮廓 3,并且没有 Previous。它的 Fist_Child 为轮廓 1, 组织结构为 2。由于它是第 1 级,所以没有父轮廓。因此它的组织结构数组为 [3,-1,1,-1]。

现在是轮廓 1,它是第 2 级。处于同一级的下一个轮廓为 2。没有 Previ- ous,也没有 Child,(因为是第 2 级所以有父轮廓)父轮廓是 0。所以数组是 [2,-1,-1,0]。

轮廓 2:它是第 2 级。在同一级的组织结构中没有  Next。Previous  为轮 廓  1。没有子,父轮廓为  0,所以数组是 [-1,1,-1,0]

轮廓 3:它是第 1 级。在同一级的组织结构中 Next 为 5。Previous 为 轮廓  0。子为  4,没有父轮廓,所以数组是 [5,0,4,-1]

轮廓 4:它是第 2 级。在同一级的组织结构中没有 Next。没有 Previous, 没有子,父轮廓为  3,所以数组是 [-1,-1,-1,3]

下面是我得到的答案:

import cv2

import numpy as np

# 创建一个带空洞的测试图像

img = np.zeros((400, 600, 3), dtype=np.uint8)

cv2.rectangle(img, (50, 50), (300, 300), (255, 255, 255), -1)  # 外层矩形(级别0)

cv2.rectangle(img, (100, 100), (250, 250), (0, 0, 0), -1)       # 内层矩形(级别1)

cv2.circle(img, (400, 200), 80, (255, 255, 255), -1)            # 外层圆形(级别0)

cv2.circle(img, (400, 200), 40, (0, 0, 0), -1)                  # 内层圆形(级别1)

# 转为灰度图并二值化

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 使用RETR_CCOMP检测轮廓

contours, hierarchy = cv2.findContours(

    thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE

)

# 打印层级关系

print("层级关系数组:\n", hierarchy)

# 输出格式: [Next, Previous, First_Child, Parent]

# 可视化轮廓层级

for i, cnt in enumerate(contours):

    # 根据层级设置颜色和线宽

    level = hierarchy[0][i][3]  # 父轮廓索引

    color = (0, 255, 0) if level == -1 else (0, 0, 255)  # 级别0绿色,级别1红色

    thickness = 2 if level == -1 else 1

    

    # 绘制轮廓

    cv2.drawContours(img, [cnt], -1, color, thickness)

    

    # 标记轮廓编号和级别

    M = cv2.moments(cnt)

    if M["m00"] > 0:

        cx = int(M["m10"] / M["m00"])

        cy = int(M["m01"] / M["m00"])

        cv2.putText(img, f"{i}(L{0 if level == -1 else 1})",

                   (cx-20, cy), cv2.FONT_HERSHEY_SIMPLEX,

                   0.5, (255, 255, 255), 1)

cv2.imshow("RETR_CCOMP Demo", img)

cv2.waitKey(0)

cv2.destroyAllWindows()

RETR_TREE 是最完美的一个。这种模式下会返回所有轮廓,并且创建一个完整的组织结构列表。它甚至会告诉你谁是爷爷,爸 爸,儿子,孙子等。

还是以上图为例,使用这种模式,对 OpenCV 返回的结果重新排序并分 析它,红色数字是边界的序号,绿色是组织结构。

轮廓 0 的组织结构为 0,同一级中 Next 为 7,没有 Previous。子轮廓 是  1,没有父轮廓。所以数组是 [7,-1,1,-1]。

轮廓 1 的组织结构为 1,同一级中没有其他,没有 Previous。子轮廓是 2,父轮廓为  0。所以数组是  [-1,-1,2,0]。

剩下的自己试试计算一下吧。下面是结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值