OpenCV-Python实战(10)——详解 OpenCV 轮廓检测

Posted 盼小辉丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV-Python实战(10)——详解 OpenCV 轮廓检测相关的知识,希望对你有一定的参考价值。

0. 前言

在计算机视觉领域,轮廓通常指图像中对象边界的一系列点。因此,轮廓通常描述了对象边界的关键信息,包含了有关对象形状的主要信息,该信息可用于形状分析与对象检测和识别。在本文中,将首先通过简单示例了解轮廓的基本概念,然后通过实际示例来了解如何检测和压缩轮廓,最后介绍如何利用图像矩描述检测到的轮廓属性。

1. 轮廓介绍

轮廓视为对象边界曲线包含的所有点,通过对这些点的分析可以进行形状判断以及对象检测和识别等计算机视觉过程。OpenCV 提供了许多函数来检测和处理轮廓,在深入了解这些函数之前,我们首先通过函数模拟观察轮廓的基本结构:

def get_test_contour():
    cnts = [np.array(
        [[[600, 320]], [[460, 562]], [[180, 563]], [[40, 320]], 
         [[179, 78]], [[459, 77]]], dtype=np.int32)]
    return cnts

如上所示,轮廓是由 np.int32 类型的多个点组成的数组,调用此函数可以获取此阵列表示的轮廓,此阵列只有包含一个轮廓:

contours = get_test_contour()
print("contour shape: '{}'".format(contours[0].shape))
print("'detected' contours: '{}' ".format(len(contours)))

获得轮廓后,我们可以应用 OpenCV 提供与轮廓相关的所有函数。请注意,get_one_contour() 函数中仅包含简单轮廓,而在实际场景中,检测到的真实轮廓通常有数百个点,因此调试代码将十分耗时,此时设置一个简单轮廓(例如此处的 get_one_contour() 函数)以调试和测试与轮廓相关的函数将非常有用。
OpenCV 提供了cv2.drawContours() 用于在图像中绘制轮廓,我们可以调用此函数来查看轮廓外观:

def draw_contour_outline(img, cnts, color, thickness=1):
    for cnt in cnts:
        cv2.drawContours(img, [cnt], 0, color, thickness)

此外,我们可能还想要绘制图像中的轮廓点:

def draw_contour_points(img, cnts, color):
    for cnt in cnts:
        # 维度压缩
        squeeze = np.squeeze(cnt)
        # 遍历轮廓阵列的所有点
        for p in squeeze:
            # 为了绘制圆点,需要将列表转换为圆心元组
            p = array_to_tuple(p)
            # 绘制轮廓点
            cv2.circle(img, p, 10, color, -1)

    return img
    
def array_to_tuple(arr):
    """将列表转换为元组"""
    return tuple(arr.reshape(1, -1)[0])

最后,调用 draw_contour_outline()draw_contour_points() 函数绘制轮廓和轮廓点,并可视化:

# 绘制轮轮廓点
draw_contour_points(image_contour_points, contours, (255, 0, 255))

# 绘制轮廓
draw_contour_outline(image_contour_outline, contours, (0, 255, 255), 3)

# 同时绘制轮廓和轮廓点
draw_contour_outline(image_contour_points_outline, contours, (255, 0, 0), 3)
draw_contour_points(image_contour_points_outline, contours, (0, 0, 255))

# 可视化函数
def show_img_with_matplotlib(color_img, title, pos):
    img_RGB = color_img[:, :, ::-1]
    ax = plt.subplot(1, 3, pos)
    plt.imshow(img_RGB)
    plt.title(title, fontsize=8)
    plt.axis('off')
    
# 绘制图像
show_img_with_matplotlib(image_contour_points, "contour points", 1)
show_img_with_matplotlib(image_contour_outline, "contour outline", 2)
show_img_with_matplotlib(image_contour_points_outline, "contour outline and points", 3)

# 可视化
plt.show()

2. 轮廓检测

我们已经介绍了轮廓的相关概念,并通过实例了解了轮廓的绘制,接下来我们将介绍如何在 OpenCV 中检测轮廓。为此我们首先绘制一些预定义的形状,然后使用绘制的形状讲解如何进行轮廓检测:

def build_sample_image():
    """绘制一些基本形状"""
    img = np.ones((500, 500, 3), dtype="uint8") * 70
    cv2.rectangle(img, (50, 50), (250, 250), (255, 0, 255), -1)
    cv2.rectangle(img, (100, 100), (200, 200), (70, 70, 70), -1)
    cv2.circle(img, (350, 350), 100, (255, 255, 0), -1)
    cv2.circle(img, (350, 350), 50, (70, 70, 70), -1)
    return img
    
# 加载图像并转换为灰度图像
image = build_sample_image()
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 应用 cv2.threshold() 函数获取二值图像
ret, thresh = cv2.threshold(gray_image, 70, 255, cv2.THRESH_BINARY)

上述函数绘制了两个填充的矩形和两个填充的圆圈,此函数创建的图像具有两个外部轮廓和两个内部轮廓,并在加载图像后,将其转换为灰度图形,并获取二值图像,此二值图像将用于使用 cv2.findContours() 函数查找轮廓。
接下来,就可以调用 cv2.findContours() 检测到利用 build_sample_image() 函数创建的图形的轮廓,cv2.findContours() 函数用法如下:

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

其中,method 参数设置检索与每个检测到的轮廓相关的点时使用的近似方法,cv2.findContours() 返回检测到的二值图像中的轮廓(例如,经过阈值处理之后得到的图像),每个轮廓包含定义边界的所有轮廓点,检索到的轮廓可以以不同的模式( mode )输出:

输出模式说明
cv2.RETR_EXTERNAL仅输出外部轮廓
cv2.RETR_LIST输出没有分层关系的所有轮廓
cv2.RETR_TREE通过建立分层关系输出所有轮廓

输出矢量 hierarchy 包含有关分层关系的信息,为每个检测到的轮廓提供一个索引。对于第 i 个轮廓 contours[i]hierarchy[i][j] ( j 的取值范围为 [0,3] )包含以下内容:

索引说明
hierarchy[i][0]位于相同的层次级别的下一个轮廓的索引,当其为负值时,表示没有下一轮廓
hierarchy[i][1]位于相同的层次级别的前一个轮廓的索引,当其为负值时,表示没没有前一轮廓
hierarchy[i][2]第一个孩子轮廓的索引,当其为负值时,表示没有父轮廓
hierarchy[i][3]父轮廓的索引,当其为负值时,表示没有下一轮廓

调用 cv2.findContours() 函数查找测试图像中轮廓:

# 轮廓检测
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours2, hierarchy2 = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
contours3, hierarchy3 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

# 打印使用不同 mode 参数获得的轮廓数
print("detected contours (RETR_EXTERNAL): '{}' ".format(len(contours)))
print("detected contours (RETR_LIST): '{}' ".format(len(contours2)))
print("detected contours (RETR_TREE): '{}' ".format(len(contours3)))

image_contours = image.copy()
image_contours_2 = image.copy()

# 绘制检测到的轮廓
draw_contour_outline(image_contours, contours, (0, 0, 255), 5)
draw_contour_outline(image_contours_2, contours2, (255, 0, 0), 5)

# 可视化
show_img_with_matplotlib(image, "image", 1)
show_img_with_matplotlib(cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), "threshold = 100", 2)
show_img_with_matplotlib(image_contours, "contours (RETR EXTERNAL)", 3)
show_img_with_matplotlib(image_contours_2, "contours (RETR LIST)", 4)

3. 轮廓压缩

当检测到的轮廓包含大量点时,可以使用轮廓压缩算法来减少轮廓点的数量,OpenCV 提供了减少轮廓点数量的方法,这就是 cv2.findContours() 函数中 method 参数的用武之地了:

可选值解释
cv2.CHAIN_APPROX_NONE禁用压缩,其存储所有边界点,不进行压缩
cv2.CHAIN_APPROX_SIMPLE压缩轮廓的水平,垂直和对角线,仅保留端点,例如,如果将其用于压缩矩形的轮廓,压缩后的结果仅由四个顶点组成
cv2.CHAIN_APPROX_TC89_L1基于非参数方法的Teh-Chin算法压缩轮廓
cv2.CHAIN_APPROX_TC89_KCOS基于非参数方法的Teh-Chin算法压缩轮廓

最后两个压缩算法均是基于非参数方法的Teh-Chin算法压缩轮廓,该算法的第一步根据每个点的局部属性确定其支持区域( region of support, ROS );接下来,该算法计算每个点的相对重要性度量。最后,通过非最大抑制检测优势点。区别在于它们使用不同的显著性度量,对应于离散曲率度量的不同精度。
接下来,我们使用不同的压缩算法来比较它们之间的区别:

# 阈值处理
ret, thresh = cv2.threshold(gray_image, 70, 255, cv2.THRESH_BINARY)

methods = [cv2.CHAIN_APPROX_NONE, cv2.CHAIN_APPROX_SIMPLE, cv2.CHAIN_APPROX_TC89_L1, cv2.CHAIN_APPROX_TC89_KCOS]
# 循环使用每一压缩算法来比较它们之间的区别
for index in range(len(methods)):
    method = methods[index]
    image_approx = image.copy()
    contours ,hierarchy = cv2.findContours(thresh, cv2.RETR_LIST, method)
    # 可视化
    draw_contour_points(image_approx, contours, (255, 255, 255))
    show_img_with_matplotlib(image_approx, "contours ({})".format(method), 3 + index)

show_img_with_matplotlib(image, "image", 1)
show_img_with_matplotlib(cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), "threshold = 100", 2)

plt.show()

4. 图像矩

在数学中,矩表示函数形状的特定定量测量。而在计算机数据领域,图像矩可以视为图像像素强度的加权平均值,其编码了图像的一些特性。因此,图像矩可用于描述检测到的轮廓的一些性质(例如,物体的质心,或物体的区域等)。
OpenCV 中 提供了 cv2.moments() 函数用于计算向量形状或栅格化形状的三阶矩,函数用法如下:

retval = cv.moments(array[, binaryImage])

因此,可以使用以下方式计算检测到的轮廓(例如,检测到的第一个轮廓)的矩:

M = cv2.moments(contours[0])

我们打印 M,以查看图像矩的信息:

{'m00': 203700.5, 'm10': 65138893.0, 'm01': 65184079.166666664, 'm20': 24157077178.583332, 'm11': 20844151188.958332, 'm02': 24186367618.25, 'm30': 9853254039349.8, 'm21': 7730082994775.5, 'm12': 7733632427205.399, 'm03': 9869218925404.75, 
'mu20': 3327106799.2006187, 'mu11': -268722.3380508423, 'mu02': 3327488151.551258, 'mu30': 487977833.58203125, 'mu21': -253389.3426513672, 'mu12': -458453806.3643799, 'mu03': 1109170.4453125, 
'nu20': 0.08018304628713532, 'nu11': -6.476189966458202e-06, 'nu02': 0.08019223685270128, 'nu30': 2.605672665422043e-05, 'nu21': -1.3530321224005687e-08, 'nu12': -2.448022162878717e-05, 'nu03': 5.9226770393023014e-08}

如上所示,有三种不同类型的矩,包括 m_jimu_jinu_ji
m_ji 表示空间矩,其计算公式如下:
m j i = ∑ x , y ( a r r a y ( x , y ) ⋅ x j ⋅ y i ) m_{ji}=\\sum_{x,y}(array(x,y)\\cdot x^j \\cdot y^i) mji=x,y(array(x,y)xjyi)
mu_ji 表示中心矩,其计算公式如下:
m u j i = ∑ x , y ( a r r a y ( x , y ) ⋅ ( x − x ˉ ) j ⋅ ( y − y ˉ ) i ) mu_{ji}=\\sum_{x,y}(array(x,y)\\cdot (x-\\bar x)^j \\cdot (y-\\bar y)^i) muji=x,y(array(x,y)(xxˉ)j(yyˉ)i)
其中:
x ˉ = m 10 m 00 , y ˉ = m 01 m 00 \\bar x= \\frac {m_{10}}{m_{00}}, \\bar y =\\frac {m_{01}}{m_{00}} xˉ=m00m10,yˉ=m00m01
通过定义,可知中心矩是具有平移不变性。因此,中心矩适合描述物体的形状。然而,空间矩和中心矩的缺点是它们依赖于对象的大小,它们不具尺度不变性。
nu_jl 表示归一化中心矩,其计算公式如下:
n u j i = m u j i m 00 ( i + j ) 2 + 1 nu_{ji}=\\frac{mu_{ji}}{m_{00}^{\\frac{(i+j)}2+1}} nuji=OpenCV-Python实战(16)——人脸追踪详解

OpenCV-Python实战(12)——一文详解AR增强现实

OpenCV-Python实战——直方图详解(❤️含大量实例,建议收藏❤️)

OpenCV-Python实战——图像与视频文件的处理(两万字详解,️建议收藏️)

OpenCV-Python实战(14)——人脸检测详解(仅需6行代码学会4种人脸检测方法)

OpenCV-Python实战(15)——面部特征点检测详解(仅需5行代码学会3种面部特征点检测方法)