第 15 章

轮廓的检测与匹配

本章共 6 个小节 · findContours · drawContours · hierarchy · moments · Hu 矩 · 形状匹配
边缘检测取得的边缘线有时并不连续。轮廓检测则进一步分析影像中可能的形状,定位形状并取得其特征, 例如质心、面积、周长和边界框。这些资料可用于后续的影像识别与形状匹配。
15-1

影像内图形的轮廓

轮廓是影像内图形或外形的边缘线条。使用 findContours() 找轮廓前,通常需要先把彩色影像转为灰阶, 再做阈值处理得到黑白二值影像;也可以理解为在黑色影像中寻找白色物体。

15-1-1 找寻图形轮廓 findContours()

contours, hierarchy = cv2.findContours(image, mode, method)
参数 / 返回值说明
contours影像中找到的所有轮廓。资料类型是列表,列表元素是各轮廓的像素点坐标数组。
hierarchy轮廓之间的层次关系。
image8 位单通道影像。若来源是彩色影像,需要先转灰阶并二值化。
mode轮廓检测模式,例如 RETR_EXTERNALRETR_LISTRETR_CCOMPRETR_TREE
method轮廓近似方法,例如 CHAIN_APPROX_NONECHAIN_APPROX_SIMPLECHAIN_APPROX_TC89_L1

15-1-2 绘制图形的轮廓 drawContours()

image = cv2.drawContours(src_image, contours, contourIdx, color, thickness, lineType, hierarchy, maxLevel, offset)
参数说明
src_image要绘制轮廓的目标影像。函数会直接修改这张影像。
contoursfindContours() 得到的轮廓列表。
contourIdx要绘制的轮廓索引;设为 -1 表示绘制所有轮廓。
colorBGR 格式轮廓颜色。
thickness线条粗细;设为 -1 表示填满轮廓。
hierarchy可选,轮廓层次资料。
maxLevel可选,绘制的轮廓层次深度。
offset可选,绘制轮廓时的偏移量。
15-2

绘制影像内图形轮廓的系列实例

本节从简单二值图开始,逐步演示找轮廓、绘制轮廓、列出轮廓属性、只检测外部轮廓、检测所有轮廓, 以及在一般照片中绘制轮廓或使用遮罩取出目标。

程序实例 ch15_1.py:在 easy.jpg 中找外部轮廓并以绿色绘制

# ch15_1.py import cv2 src = cv2.imread("easy.jpg") cv2.imshow("src", src) src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) dst = cv2.drawContours(src, contours, -1, (0, 255, 0), 5) cv2.imshow("result", dst) cv2.waitKey(0) cv2.destroyAllWindows()
外部轮廓
ch15_1.py findContours 和 drawContours 执行结果
使用 RETR_EXTERNAL 只检测外部轮廓,参考原书第 15-5 页。

程序实例 ch15_1-1.py:再次显示 src,观察 drawContours() 是否影响原始影像

# ch15_1-1.py # 与 ch15_1.py 相同,但在绘制后再显示一次 src。 dst = cv2.drawContours(src, contours, -1, (0, 255, 0), 5) cv2.imshow("result", dst) cv2.imshow("src1", src)
drawContours 会更新来源影像
ch15_1-1.py 再次显示 src 的结果
drawContours() 执行后,等号左侧结果与原 src 内容都会显示轮廓,参考原书第 15-6 页。

程序实例 ch15_2.py:查看 contours 的类型与数量

# ch15_2.py import cv2 src = cv2.imread("easy.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) print(f"资料类型 = {type(contours)}") print(f"轮廓数量 = {len(contours)}")

程序实例 ch15_3.py:依索引逐一绘制轮廓

# ch15_3.py n = len(contours) imgList = [] for i in range(n): img = np.zeros(src.shape, np.uint8) imgList.append(img) imgList[i] = cv2.drawContours(imgList[i], contours, i, (255, 255, 255), 5) cv2.imshow("contours" + str(i), imgList[i])

程序实例 ch15_4.py:列出轮廓属性

# ch15_4.py import cv2 src = cv2.imread("easy.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) for i in range(len(contours)): print(f"contours[{i}].shape = {contours[i].shape}")

程序实例 ch15_5.py:列出编号 1 的轮廓点内容

# ch15_5.py print(f"轮廓数量 = {len(contours)}") print("contours[1] =") print(contours[1])
轮廓列表与索引

这组实例的重点是观察 contours 的资料类型、轮廓数量,并用索引逐一绘制轮廓。

若只需要表现程序输出,使用文字和代码块即可;这里不使用截图,避免把 hierarchy 章节的截图误放到本节。

程序实例 ch15_6.py:只检测外部轮廓

# ch15_6.py src = cv2.imread("easy1.jpg") contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )

程序实例 ch15_7.py:检测所有轮廓

# ch15_7.py # 若要检测所有轮廓,将检测模式改为 RETR_LIST。 contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE )

程序实例 ch15_8.py:绘制一般影像轮廓

# ch15_8.py src = cv2.imread("lake.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 150, 255, cv2.THRESH_BINARY) cv2.imshow("binary", dst_binary) contours, hierarchy = cv2.findContours(dst_binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) dst = cv2.drawContours(src, contours, -1, (0, 255, 0), 2) cv2.imshow("result", dst)

程序实例 ch15_9.py:以实心轮廓清除背景

# ch15_9.py # 使用白色实心轮廓建立遮罩,可视为取出图案。 mask = np.zeros(src.shape[:2], np.uint8) dst = cv2.drawContours(mask, contours, -1, 255, -1) dst_result = cv2.bitwise_and(src, src, mask=dst)

程序实例 ch15_10.py:保留天空背景并隐藏灯塔

# ch15_10.py # 保留天空背景,但将灯塔以黑色显示。 mask = np.ones(src.shape[:2], np.uint8) * 255 dst = cv2.drawContours(mask, contours, -1, 0, -1) dst_result = cv2.bitwise_and(src, src, mask=dst)
一般影像轮廓
ch15_8 到 ch15_10 湖边建筑轮廓处理结果
这张截图对应 ch15_8.py:显示原始影像、二值影像与绿色轮廓结果;后续实例再用遮罩清除背景或保留背景,参考原书第 15-1015-13 页。
15-3

认识轮廓层级 hierarchy

hierarchy 用来定义轮廓之间的层级关系。每个轮廓对应一个含 4 个元素的数组: [Next, Previous, First_Child, Parent],分别代表同层下一个轮廓、同层前一个轮廓、第一个子轮廓、父轮廓。

检测模式层级行为
RETR_EXTERNAL只检测外部轮廓。
RETR_LIST检测所有轮廓,但不建立层级关系。
RETR_CCOMP检测所有轮廓,同时建立两个层级关系。
RETR_TREE检测所有轮廓,同时建立完整树状层级关系。

程序实例 ch15_11.py:检测模式 RETR_EXTERNAL

# ch15_11.py src = cv2.imread("easy2.jpg") contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) print(f"hierarchy 资料类型:{type(hierarchy)}") print(f"列印层级 \n {hierarchy}")

程序实例 ch15_12.py:检测模式 RETR_LIST

# ch15_12.py import cv2 src = cv2.imread("easy2.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE ) print(f"hierarchy 资料类型:{type(hierarchy)}") print(f"列印层级 \n {hierarchy}")

程序实例 ch15_13.py:检测模式 RETR_CCOMP

# ch15_13.py import cv2 src = cv2.imread("easy3.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE ) print(f"hierarchy 资料类型:{type(hierarchy)}") print(f"列印层级 \n {hierarchy}")

程序实例 ch15_14.py:检测模式 RETR_TREE

# ch15_14.py import cv2 src = cv2.imread("easy3.jpg") src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) ret, dst_binary = cv2.threshold(src_gray, 127, 255, cv2.THRESH_BINARY) contours, hierarchy = cv2.findContours( dst_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE ) print(f"hierarchy 资料类型:{type(hierarchy)}") print(f"列印层级 \n {hierarchy}")
RETR_EXTERNAL 输出示例
hierarchy 资料类型:<class 'numpy.ndarray'> 列印层级 [[[ 1 -1 -1 -1] [-1 0 -1 -1]]]
RETR_EXTERNAL 层级截图
ch15_11.py RETR_EXTERNAL hierarchy 执行结果
ch15_11.py 使用 RETR_EXTERNAL,两个外部轮廓都没有父轮廓或子轮廓。
RETR_CCOMP 层级
ch15_13.py RETR_CCOMP hierarchy 执行结果
ch15_13.pyRETR_CCOMP 输出呈现外层与内层轮廓的两层关系,参考原书第 15-1815-22 页。
15-4

轮廓的特征:影像矩(Image moments)

轮廓的特征包括质心、面积、周长、边界框等。影像矩是一组统计参数,可记录像素所在位置与强度分布, 常用于比较轮廓与模式识别。

m = cv2.moments(array, binaryImage)

基础影像矩可推导轮廓质心。若 m00 是零阶矩,则质心坐标为:

cx = m10 / m00 cy = m01 / m00

程序实例 ch15_15.py:列印影像矩

# ch15_15.py for i in range(len(contours)): M = cv2.moments(contours[i]) print(f"轮廓面积 str({i}) = {M['m00']}") print(f"影像矩 str({i}) = \n{M}")

程序实例 ch15_16.py:绘制轮廓质心

# ch15_16.py dst = cv2.drawContours(src, contours, -1, (0, 255, 0), 5) for c in contours: M = cv2.moments(c) cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) cv2.circle(dst, (cx, cy), 5, (255, 0, 0), -1) cv2.imshow("result", dst)
影像矩与质心
ch15_15.py 列印轮廓面积和影像矩
ch15_15.py 逐一列印轮廓面积与影像矩,输出内容较长,保留截图的换行结构。
ch15_16.py 为每个轮廓绘制中心点
ch15_16.py 使用 m10 / m00m01 / m00 计算质心,并在每个轮廓中心绘制蓝点。

OpenCV 也提供 contourArea()arcLength(),分别用于计算轮廓面积和轮廓周长。

area = cv2.contourArea(contour, oriented) length = cv2.arcLength(contour, closed)

程序实例 ch15_17.py:计算轮廓面积

# ch15_17.py for i in range(len(contours)): area = cv2.contourArea(contours[i]) print(f"轮廓({i})面积 = {area}")

程序实例 ch15_18.py:计算轮廓周长

# ch15_18.py for i in range(len(contours)): length = cv2.arcLength(contours[i], True) print(f"轮廓({i})周长 = {length}")
15-5

轮廓外形的匹配:Hu 矩

中心矩对轮廓平移具有不变性,但对外形匹配仍不够。Hu 矩是归一化中心矩的组合, 在平移、缩放和旋转下保持相对稳定,因此常用于轮廓外形匹配。

hu = cv2.HuMoments(m)

程序实例 ch15_19.py:计算 heart.jpg 的 Hu 矩

# ch15_19.py src = cv2.imread("heart.jpg") M = cv2.moments(contours[0]) hu = cv2.HuMoments(M) print(f"Hu Moments = \n{hu}")

程序实例 ch15_20.py:列出 3heart.jpg 内部 3 个轮廓的 Hu 矩

# ch15_20.py src = cv2.imread("3heart.jpg") for i in range(len(contours)): M = cv2.moments(contours[i]) hu = cv2.HuMoments(M) print(f"轮廓({i}) Hu 矩 = \n{hu}")

程序实例 ch15_21.py:列出 3shapes.jpg 内部 3 个轮廓的 Hu 矩

# ch15_21.py src = cv2.imread("3shapes.jpg") for i in range(len(contours)): M = cv2.moments(contours[i]) hu = cv2.HuMoments(M) print(f"轮廓({i}) Hu 矩 = \n{hu}")
Hu 矩比较
ch15_20.py 3heart Hu Moments 执行结果
三颗心形轮廓的 Hu 矩接近,参考原书第 15-31 页。
ch15_21.py 3shapes Hu Moments 执行结果
不同形状的 Hu 矩差异更明显,参考原书第 15-32 页。
15-6

再谈轮廓外形匹配

除了直接观察 Hu 矩,也可以使用 matchShapes() 直接比较两个轮廓,返回值越接近 0,外形越相似。

retval = cv2.matchShapes(contour1, contour2, method, parameter)
method说明
cv2.CONTOURS_MATCH_I1使用 Hu 矩计算差异。
cv2.CONTOURS_MATCH_I2使用另一种 Hu 矩距离计算方式。
cv2.CONTOURS_MATCH_I3使用最大相对差异。

程序实例 ch15_22.py:使用 matchShapes() 比较轮廓

# ch15_22.py src = cv2.imread("myheart.jpg") contours, hierarchy = cv2.findContours(dst_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) match0 = cv2.matchShapes(contours[0], contours[0], 1, 0) print(f"轮廓0和0比较 = {match0}") match1 = cv2.matchShapes(contours[0], contours[1], 1, 0) print(f"轮廓0和1比较 = {match1}") match2 = cv2.matchShapes(contours[0], contours[2], 1, 0) print(f"轮廓0和2比较 = {match2}")
Python Shell 输出
轮廓0和0比较 = 0.0 轮廓0和1比较 = 0.00046299603915214704 轮廓0和2比较 = 0.29307592277155203
matchShapes 结果
ch15_22.py matchShapes 执行结果
轮廓 0 与自己比较为 0;轮廓 1 只是旋转,结果接近 0;轮廓 2 差异较大,参考原书第 15-35 页。

OpenCV 还提供形状场景距离与 Hausdorff 距离。两者都属于形状比较方法,可用于进一步衡量轮廓相似度。

sd = cv2.createShapeContextDistanceExtractor() distance = sd.computeDistance(contour1, contour2) hd = cv2.createHausdorffDistanceExtractor() distance = hd.computeDistance(contour1, contour2)

程序实例 ch15_23.py:Shape Context 距离

# ch15_23.py sc = cv2.createShapeContextDistanceExtractor() distance = sc.computeDistance(contours[0], contours[1]) print(f"Shape Context distance = {distance}")

程序实例 ch15_24.py:Hausdorff 距离

# ch15_24.py hd = cv2.createHausdorffDistanceExtractor() distance = hd.computeDistance(contours[0], contours[1]) print(f"Hausdorff distance = {distance}")
Hausdorff 距离
影像1和1比较 = 0.0 影像0和1比较 = 12.206555366516113 影像0和2比较 = 8.5440034866333
ch15_24.py Hausdorff Distance 执行结果
ch15_24.py 使用 Hausdorff 距离比较轮廓,相同影像距离为 0,形状越不同距离越大。
习题

1. 参考 ch15_3.py,使用 hw15_1.jpg 影像,输出轮廓顺序改为输出黄色实心轮廓;并输出所有轮廓,同时中心标记蓝色点。

第 15 章习题 1 参考结果
习题 1 参考图,参考原书第 15-37 页。

2. 使用 hw15_2.jpg,将轮廓颜色改为红色。

3. 使用 hw15_2.jpg,将轮廓色反转。

第 15 章习题 2 至 3 参考结果
习题 2 至 3 参考图,参考原书第 15-38 页。

4. 使用 hw15_2.jpg,列出所有轮廓面积,并将最小面积轮廓用绿色外框标记。

5. 使用 template.jpg 找出 hw15_2.jpg 中外形最相似的轮廓,并以绿色实心填满。

第 15 章习题 4 至 5 参考结果
习题 4 至 5 参考图,参考原书第 15-39 页。

6. 使用 myhand.jpg,建立此影像的轮廓。

第 15 章习题 5 至 6 参考结果
习题 5 至 6 参考图,参考原书第 15-40 页。