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()函数绘制轮廓和轮廓点,并可视化:
# 创建画布并复制,用于显示不同检测效果
canvas = np.zeros((640, 640, 3), dtype="uint8")
image_contour_points = canvas.copy()
image_contour_outline = canvas.copy()
image_contour_points_outline = canvas.copy()
# 绘制轮轮廓点
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_ji,mu_ji和nu_ji。
m_ji表示空间矩,其计算公式如下:
mu_ji表示中心矩,其计算公式如下:
在:
由定义可知,中心矩具有平移不变性。因此,中心矩适用于描述物体的形状。然而,空间矩和中心矩的缺点是它们依赖于对象的大小并且它们不是尺度不变的。
nu_jl表示归一化中心矩,其计算公式如下:
根据定义,归一化中心矩是平移和缩放不变量。
接下来,计算一些基于矩的对象特征(例如,中心、偏心或轮廓区域)。
4. 1 一些基于矩的对象特征
我们已经知道矩是从轮廓计算出来的特征,虽然它不能直接理解刻画的几何意义,但是一些几何性质是可以从矩计算出来的。
接下来,我们首先计算检测到的轮廓的矩,然后相应地计算一些对象特征:
M = cv2.moments(contours[0])
print("Contour area: '{}'".format(cv2.contourArea(contours[0])))
print("Contour area: '{}'".format(M['m00']))
矩m_00给出了轮廓的区域,这等价于函数cv2.contourArea()。要计算轮廓的质心,需要使用以下方法:
print("center X : '{}'".format(round(M['m10'] / M['m00'])))
print("center Y : '{}'".format(round(M['m01'] / M['m00'])))
圆度是测量轮廓接近完美圆形轮廓的程度。轮廓圆度计算公式如下:
其中,P是轮廓的周长,A是轮廓的区域面积。如果轮廓为圆形,其圆度为1;值越高,它将越不像圆:
def roundness(contour, moments):
"""计算轮廓圆度"""
length = cv2.arcLength(contour, True)
k = (length * length) / (moments['m00'] * 4 * np.pi)
return k
偏心率(也称为伸长率)是一种衡量轮廓伸长的程度。偏心ε可以直接从对象的长半轴a和短半轴b计算得出:
因此,计算轮廓的偏心度的一种方法是首先计算拟合轮廓的椭圆,然后从计算出的椭圆导出a和b;最后,利用上述公式计算ε:
def eccentricity_from_ellipse(contour):
"""利用拟合的椭圆计算偏心率"""
# 拟合椭圆
(x, y), (MA, ma), angle = cv2.fitEllipse(contour)
a = ma / 2
b = MA / 2
ecc = np.sqrt(a ** 2 - b ** 2) / a
return ecc
另一种方法是使用轮廓矩计算偏心率:
接下来,使用轮廓矩计算偏心率:
def eccentricity_from_moments(moments):
"""利用轮廓矩计算偏心率"""
a1 = (moments['mu20'] + moments['mu02']) / 2
a2 = np.sqrt(4 * moments['mu11'] ** 2 + (moments['mu20'] - moments['mu02']) ** 2) / 2
ecc = np.sqrt(1 - (a1 - a2) / (a1 + a2))
return ecc
纵横比是轮廓边界矩形的宽度与高度的比率,可以基于cv2.boundingRect()计算的最小边界矩形的尺寸来计算纵横比:
def aspect_ratio(contour):
"""计算纵横比"""
x, y, w, h = cv2.boundingRect(contour)
res = float(w) / h
return res
在下图中,通过可视化脚本中计算的所有对象属性来显示轮廓分析结果:
需要注意的是,在上面的例子中,只有简单的对象特征是使用二阶矩计算的。为了更准确地描述复杂对象,应该使用更高阶矩或更复杂的矩。对象越复杂,则应计算的矩阶越高,以尽量减少从矩中重构对象的误差。
4.2 Hu 不变矩
Hu 不变矩可以保持平移、缩放和旋转不变,同时,所有的矩(第 7 个矩除外)对于反射都是不变的。第 7 个矩因反射而改变,从而使其能够区分镜像图片。OpenCV提供cv2.HuMoments()来计算 7 个Hu 不变矩,使用方法如下:
cv2.HuMoments(m[, hu]) → hu
这里,m对应于用cv2.moments()计算的矩,输出hu对应于 7 个 Hu 不变矩。
7 个 Hu 不变矩定义如下:
接下来编写程序计算 7 个 Hu 不变矩,为了计算不变矩,必须首先使用cv2.moments()计算矩。计算图像矩时,可以使用矢量形状或图像,如果binaryImage参数为真(仅用于图像),则输入图像中的所有非零像素将被视为1。计算使用矢量形状和图像的图像矩后,根据计算的矩,计算 Hu 不变矩。
# 加载图像并将其转化为灰度图像
image = cv2.imread("example.png")
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 获取二值图像
ret, thresh = cv2.threshold(gray_image, 70, 255, cv2.THRESH_BINARY)
# 计算图像矩,传递参数为图像
M = cv2.moments(thresh, True)
print("moments: '{}'".format(M))
def centroid(moments):
"""根据图像矩计算质心"""
x_centroid = round(moments['m10'] / moments['m00'])
y_centroid = round(moments['m01'] / moments['m00'])
return x_centroid, y_centroid
# 计算质心
x, y = centroid(M)
# 计算 Hu 矩并打印
HuM = cv2.HuMoments(M)
print("Hu moments: '{}'".format(HuM))
# 计算图像矩时传递轮廓,重复以上计算过程
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
M2 = cv2.moments(contours[0])
print("moments: '{}'".format(M2))
x2, y2 = centroid(M2)
# 绘制轮廓
draw_contour_outline(image, contours, (255, 0, 0), 10)
# 绘制质心
cv2.circle(image, (x, y), 25, (255, 255, 0), -1)
cv2.circle(image, (x2, y2), 25, (0, 0, 255), -1)
# 打印质心
print("('x','y'): ('{}','{}')".format(x, y))
print("('x2','y2'): ('{}','{}')".format(x2, y2))
# 可视化,show_img_with_matplotlib()函数与前述示例类似,不再赘述
def show_thresh_with_matplotlib(thresh, title, pos):
ax = plt.subplot(1, 2, pos)
plt.imshow(thresh, cmap='gray')
plt.title(title, fontsize=8)
plt.axis('off')
show_img_with_matplotlib(image, "detected contour and centroid", 1)
show_thresh_with_matplotlib(thresh, 'thresh', 2)
plt.show()
观察所计算的矩,Hu不变矩,以及质心,可以发现使用矢量形状和图像的结果是相似的,但稍有不同,例如,获得的质心:
('x','y'): ('1124','1713')
('x2','y2'): ('1157','1636')
由于光栅化图像的分辨率有限,坐标相差几个像素。为轮廓估计的矩与为光栅化轮廓计算的矩略有不同。该程序的输出可以在上图中看到,其中两个质心以不同的颜色显示,以查看它们之前的不同之处。
接下来,为了对比Hu不变矩,我们使用三个图像。第一个是原始图像,第二个将原始图像旋转180度,第三个将原始图像水平翻转,计算上述图像的Hu不变矩。
程序的第一步是使用cv2.imread()加载图像,并通过使用cv2.cvtColor()将它们转换为灰度图像。第二步是应用cv2.threshold()获取二进制图像。最后,使用cv2.humoments()计算Hu不变矩:
# 加载图像并进行转换
images = [image_1, image_2, image_3]
des = ['original', 'rotation', 'reflection']
for i in range(len(images)):
image = images[i]
# 转换为灰度图像
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 图像二值化
ret, thresh = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
# 计算 Hu 不变矩
HuM_1 = cv2.HuMoments(cv2.moments(thresh, True)).flatten()
# 打印 Hu 不变矩
print("Hu moments ({}): '{}'".format(des[i], HuM_1))
# 可视化
show_img_with_matplotlib(image, "original", 1+i )
plt.show()
查看计算的 Hu 不变矩结果:
Hu moments (original): '[ 3.01270761e-01 2.85277848e-02 6.91011783e-03 3.83970453e-04 -3.46840290e-07 -3.85059443e-05 5.20465006e-07]'
Hu moments (rotation): '[ 3.01270761e-01 2.85277848e-02 6.91011783e-03 3.83970453e-04 -3.46840290e-07 -3.85059443e-05 5.20465006e-07]'
Hu moments (reflection): '[ 3.01270761e-01 2.85277848e-02 6.91011783e-03 3.83970453e-04 -3.46840290e-07 -3.85059443e-05 -5.20465006e-07]'
可以看到,除了第七个矩外,计算的 Hu 不变矩在这三种情况下是相同的。
概括
在本文中,首先介绍了轮廓的相关概念,然后了解利用cv2.findContours()检测轮廓、cv2.drawContours()绘制轮廓,在获取轮廓后,我们可以利用图像矩来计算轮廓的几何特征。
系列链接
OpenCV-Python实战(1)——OpenCV简介与图像处理基础
OpenCV-Python实战(2)——图像与视频文件的处理
OpenCV-Python实战(3)——OpenCV中绘制图形与文本
OpenCV-Python实战(4)——OpenCV常见图像处理技术
OpenCV-Python实战(5)——OpenCV图像运算
OpenCV-Python实战(6)——OpenCV中的色彩空间和色彩映射
OpenCV-Python实战(7)——直方图详解
OpenCV-Python实战(8)——直方图均衡化
OpenCV-Python实战(9)——OpenCV用于图像分割的阈值技术
OpenCV-Python实战(11)——OpenCV轮廓检测相关应用
OpenCV-Python实战(12)——一文详解AR增强现实
OpenCV-Python实战(13)——OpenCV与机器学习的碰撞
OpenCV-Python实战(14)——人脸检测详解
版权声明:本文为博主盼小辉丶原创文章,版权归属原作者,如果侵权,请联系我们删除!
原文链接:https://blog.csdn.net/LOVEmy134611/article/details/120865039