第 16 章

轮廓拟合与凸包的相关应用

本章共 3 个小节 · 轮廓拟合 · 凸包 · 凸缺陷 · 几何测试
本章是轮廓知识的延伸。前一章取得轮廓后,可以进一步用矩形、圆形、椭圆、三角形、多边形或直线描述轮廓外形; 也可以计算凸包、凸缺陷,并测试轮廓是否为凸形或某点到轮廓的距离。
共用 本章多数程序都先读取影像、转灰阶、二值化,再用 findContours() 找轮廓,后续才进行拟合或测试。
src = cv2.imread("image.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 )
16-1

轮廓的拟合

轮廓拟合是将凹凸不平整的轮廓用几何图形或多边形框起来。本节依序介绍矩形包围、最小包围矩形、 最小包围圆形、最佳拟合椭圆、最小包围三角形、近似多边形与最佳拟合直线。

16-1-1 矩形包围

boundingRect() 可取得包围轮廓的矩形。此矩形不旋转,边缘与 x、y 轴平行。

retval = cv2.boundingRect(array)
参数 / 返回值说明
retval返回 (x, y, w, h),分别是左上角 x、y 坐标,以及矩形宽度与高度。
array灰阶影像或轮廓。

程序实例 ch16_1.py:列出包围轮廓的矩形框坐标、宽度和高度

# ch16_1.py import cv2 src = cv2.imread("explode1.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) rect = cv2.boundingRect(contours[0]) print(f"矩形 rect = {rect}") x, y, w, h = cv2.boundingRect(contours[0]) print(f"左上角 x = {x}, 左上角 y = {y}") print(f"矩形宽度 = {w}") print(f"矩形高度 = {h}") cv2.waitKey(0) cv2.destroyAllWindows()
矩形参数输出
ch16_1.py boundingRect 输出与原图
读取 explode1.jpg 后列出矩形包围坐标,参考原书第 16-2 页。

程序实例 ch16_2.py / ch16_3.py:用 boundingRect() 的返回值绘制矩形框

# ch16_2.py x, y, w, h = cv2.boundingRect(contours[0]) dst = cv2.rectangle(src, (x, y), (x + w, y + h), (0, 255, 255), 2) cv2.imshow("dst", dst) # ch16_3.py # 将 ch16_2.py 的来源影像改为 explode2.jpg,观察矩形面积变大的情况。 src = cv2.imread("explode2.jpg")
矩形包围结果
ch16_2.py 和 ch16_3.py 矩形包围结果
轴对齐矩形可以包住轮廓,但不一定是面积最小的包围矩形,参考原书第 16-316-4 页。

16-1-2 最小包围矩形

minAreaRect() 会返回可旋转的最小包围矩形。由于返回的是中心、宽高与旋转角度, 还需要用 boxPoints() 取得四个顶点坐标,再用 drawContours() 画出来。

retval = cv2.minAreaRect(points) points = cv2.boxPoints(box)

程序实例 ch16_4.py:使用最小矩形框包围 explode2.jpg 的轮廓

# ch16_4.py import cv2 import numpy as np src = cv2.imread("explode2.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) box = cv2.minAreaRect(contours[0]) print(f"转换前的矩形顶角 = \n{box}") points = cv2.boxPoints(box) points = np.int0(points) print(f"转换后的矩形顶角 = \n{points}") dst = cv2.drawContours(src, [points], 0, (0, 255, 0), 2) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
最小包围矩形
ch16_4.py minAreaRect 最小包围矩形结果
旋转后的矩形更贴合轮廓,参考原书第 16-5 页。

16-1-3 最小包围圆形

minEnclosingCircle() 会取得包住轮廓的最小圆形,返回圆心坐标与半径。

center, radius = cv2.minEnclosingCircle(points)

程序实例 ch16_5.py / ch16_6.py:用最小圆包围轮廓

# ch16_5.py import cv2 import numpy as np src = cv2.imread("explode3.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) (x, y), radius = cv2.minEnclosingCircle(contours[0]) center = (int(x), int(y)) radius = int(radius) dst = cv2.circle(src, center, radius, (0, 255, 255), 2) cv2.imshow("dst", dst) # ch16_6.py # 将来源影像改为 explode1.jpg,观察不同轮廓的圆形包围结果。
最小包围圆形
ch16_5.py 和 ch16_6.py minEnclosingCircle 执行结果
圆心与半径取整后,可用 circle() 绘制包围圆,参考原书第 16-616-7 页。

16-1-4 最佳拟合椭圆

fitEllipse() 会以轮廓点拟合椭圆,返回值可直接传入 ellipse() 绘制。

retval = cv2.fitEllipse(points)

程序实例 ch16_7.py:使用 fitEllipse() 包围云朵轮廓

# ch16_7.py import cv2 src = cv2.imread("cloud.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) ellipse = cv2.fitEllipse(contours[0]) print(f"资料类型 = {type(ellipse)}") print(f"椭圆中心 = {ellipse[0]}") print(f"椭圆轴长 = {ellipse[1]}") print(f"旋转角度 = {ellipse[2]}") dst = cv2.ellipse(src, ellipse, (0, 255, 0), 2) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
最佳拟合椭圆
ch16_7.py fitEllipse 执行结果
fitEllipse() 返回的资料可直接用于绘制椭圆,参考原书第 16-8 页。

16-1-5 最小包围三角形

minEnclosingTriangle() 会返回三角形面积与顶点坐标。书中范例直接用三次 line() 连接三角形三个顶点,习题要求改用 for 循环完成。

area, triangle = cv2.minEnclosingTriangle(points)

程序实例 ch16_8.py:用最小三角形包围 heart.jpg

# ch16_8.py import cv2 import numpy as np src = cv2.imread("heart.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) area, triangle = cv2.minEnclosingTriangle(contours[0]) triangle = np.int0(triangle) print(f"三角形面积 = {area}") print(f"三角形顶点坐标资料类型 = {type(triangle)}") print(f"三角形顶点坐标 = \n{triangle}") dst = cv2.line(src, tuple(triangle[0][0]), tuple(triangle[1][0]), (0, 255, 0), 2) dst = cv2.line(src, tuple(triangle[1][0]), tuple(triangle[2][0]), (0, 255, 0), 2) dst = cv2.line(src, tuple(triangle[0][0]), tuple(triangle[2][0]), (0, 255, 0), 2) cv2.imshow("dst", dst)
最小包围三角形
ch16_8.py minEnclosingTriangle 执行结果
三角形三个顶点可由返回数组取得,参考原书第 16-10 页。

16-1-6 近似多边形

approxPolyDP() 可使用多边形近似轮廓。epsilon 是原始轮廓与近似多边形之间的最大距离, 数值越大,近似越粗略,顶点通常越少。

approx = cv2.approxPolyDP(curve, epsilon, closed)

程序实例 ch16_9.py:比较 epsilon=3 与 epsilon=15

# ch16_9.py import cv2 src = cv2.imread("multiple.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) src1 = src.copy() src2 = src.copy() n = len(contours) for i in range(n): approx = cv2.approxPolyDP(contours[i], 3, True) dst1 = cv2.polylines(src1, [approx], True, (0, 255, 0), 2) approx = cv2.approxPolyDP(contours[i], 15, True) dst2 = cv2.polylines(src2, [approx], True, (0, 255, 0), 2) cv2.imshow("dst1 - epsilon=3", dst1) cv2.imshow("dst2 - epsilon=15", dst2) cv2.waitKey(0) cv2.destroyAllWindows()
近似多边形
ch16_9.py approxPolyDP epsilon 3 与 15 执行结果
下列图分别是原始影像、epsilon=3epsilon=15 的近似多边形,参考原书第 16-11 页。

16-1-7.1 最佳拟合直线

fitLine() 会使用轮廓点拟合直线,并返回正则化向量与直线经过的点。书中范例使用 DIST_L2 距离类型,再计算左边点与右边点来绘制直线。

line = cv2.fitLine(points, distType, param, reps, aeps)

程序实例 ch16_10.py:使用 fitLine() 表达不规则轮廓

# ch16_10.py import cv2 src = cv2.imread("unregular.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) rows, cols = src.shape[:2] vx, vy, x, y = cv2.fitLine(contours[0], cv2.DIST_L2, 0, 0.01, 0.01) print(f"直线正规化向量 = ({vx}, {vy})") print(f"直线经过的点 = ({x}, {y})") lefty = int((-x * vy / vx) + y) righty = int(((cols - x) * vy / vx) + y) dst = cv2.line(src, (0, lefty), (cols - 1, righty), (0, 255, 0), 2) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
最佳拟合直线
ch16_10.py fitLine 执行结果
用轮廓点拟合出代表直线,参考原书第 16-14 页。
16-2

凸包

16-2-1 获得凸包

凸包是包住轮廓的最小凸多边形。convexHull() 可返回凸包顶点坐标,也可返回轮廓中对应点的索引; 后者在计算凸缺陷时会用到。

hull = cv2.convexHull(points, clockwise, returnPoints)
参数说明
points输入轮廓。
clockwise可选参数,决定凸包点顺时针或逆时针排列。
returnPoints默认 True,返回凸包点坐标;设为 False 时返回轮廓点索引。

程序实例 ch16_11.py:使用 heart1.jpg 建立凸包

# ch16_11.py import cv2 src = cv2.imread("heart1.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) hull = cv2.convexHull(contours[0]) dst = cv2.polylines(src, [hull], True, (0, 255, 0), 2) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
心形凸包
ch16_11.py heart1 凸包执行结果
凸包连线会补齐心形上方的凹陷区域,参考原书第 16-16 页。

程序实例 ch16_12.py / ch16_12_1.py / ch16_13.py:手势凸包与面积

# ch16_12.py src = cv2.imread("hand1.jpg") hull = cv2.convexHull(contours[0]) dst = cv2.polylines(src, [hull], True, (0, 255, 0), 2) # ch16_12_1.py convex_area = cv2.contourArea(hull) print(f"凸包面积 = {convex_area}") # ch16_13.py src = cv2.imread("hand2.jpg") n = len(contours) for i in range(n): hull = cv2.convexHull(contours[i]) dst = cv2.polylines(src, [hull], True, (0, 255, 0), 2)
手势凸包
ch16_12.py hand1 凸包执行结果
将凸包应用在单一手势轮廓,参考原书第 16-17 页。
ch16_12_1.py hand1 凸包面积执行结果
contourArea() 可计算凸包面积,参考原书第 16-18 页。
ch16_13.py hand2 多个凸包执行结果
多轮廓影像可用循环逐一计算凸包,参考原书第 16-19 页。

16-2-2 凸缺陷

凸缺陷是轮廓与凸包之间的凹陷区域。使用 convexityDefects() 前,必须让 convexHull() 回传凸包索引,也就是设定 returnPoints=False

convexityDefects = cv2.convexityDefects(contour, convexhull)
凸缺陷概念
凸缺陷 start end far point 说明图
凸缺陷会记录起点、终点、最远点与距离,参考原书第 16-20 页。

程序实例 ch16_14.py:使用 star.jpg 建立凸包和凸缺陷最远点

# ch16_14.py import cv2 src = cv2.imread("star.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) contour = contours[0] hull = cv2.convexHull(contour, returnPoints=False) defects = cv2.convexityDefects(contour, hull) n = defects.shape[0] for i in range(n): s, e, f, d = defects[i, 0] start = tuple(contour[s][0]) end = tuple(contour[e][0]) far = tuple(contour[f][0]) dst = cv2.line(src, start, end, (0, 255, 0), 2) dst = cv2.circle(src, far, 3, (0, 0, 255), -1) cv2.imshow("dst", dst) cv2.waitKey(0) cv2.destroyAllWindows()
星形凸缺陷
ch16_14.py convexityDefects 星形执行结果
红点标示凸缺陷的 far point,参考原书第 16-21 页。
16-3

轮廓的几何测试

16-3-1 测试轮廓包围线是否凸形

isContourConvex() 可测试轮廓是否为凸形,返回 TrueFalse。 书中范例分别测试凸包与近似多边形。

retval = cv2.isContourConvex(contour)

程序实例 ch16_15.py:测试凸包与近似多边形是否凸形

# ch16_15.py import cv2 src = cv2.imread("heart1.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) src1 = src.copy() hull = cv2.convexHull(contours[0]) dst1 = cv2.polylines(src1, [hull], True, (0, 255, 0), 2) cv2.imshow("dst1", dst1) isConvex = cv2.isContourConvex(hull) print(f"凸包是凸形 = {isConvex}") src2 = src.copy() approx = cv2.approxPolyDP(contours[0], 10, True) dst2 = cv2.polylines(src2, [approx], True, (0, 255, 0), 2) cv2.imshow("dst2 - epsilon=10", dst2) isConvex = cv2.isContourConvex(approx) print(f"近似多边形是凸形 = {isConvex}")
Python Shell 输出
凸包是凸形 = True 近似多边形是凸形 = False
凸性测试
ch16_15.py isContourConvex 执行结果
凸包是凸形,近似多边形不一定是凸形,参考原书第 16-22 页。

16-3-2 计算任意坐标点与轮廓包围线的最短距离

pointPolygonTest() 可测试任一点与轮廓的关系。若 measureDist=True, 返回实际带正负号的距离;点在轮廓外为负,在轮廓上为 0,在轮廓内为正。 若 measureDist=False,只返回 -101

retval = cv2.pointPolygonTest(contour, pt, measureDist)
点测试概念
pointPolygonTest 点与轮廓距离说明图
A 在轮廓边界,B 在轮廓内,C 在轮廓外,参考原书第 16-23 页。

程序实例 ch16_16.py:measureDist=True,计算点到凸包边界的距离

# ch16_16.py import cv2 src = cv2.imread("heart1.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_LIST, cv2.CHAIN_APPROX_SIMPLE ) hull = cv2.convexHull(contours[0]) dst = cv2.polylines(src, [hull], True, (0, 255, 0), 2) point_a = (231, 85) dist_a = cv2.pointPolygonTest(hull, point_a, True) dst = cv2.circle(src, point_a, 3, (0, 0, 255), -1) cv2.putText(dst, "A", (236, 95), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) print(f"dist_a = {dist_a}") point_b = (150, 100) dist_b = cv2.pointPolygonTest(hull, point_b, True) dst = cv2.circle(src, point_b, 3, (0, 0, 255), -1) cv2.putText(dst, "B", (160, 110), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2) print(f"dist_b = {dist_b}") point_c = (80, 85) dist_c = cv2.pointPolygonTest(hull, point_c, True) dst = cv2.circle(src, point_c, 3, (0, 0, 255), -1) cv2.putText(dst, "C", (50, 95), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) print(f"dist_c = {dist_c}") cv2.imshow("dst", dst)
Python Shell 输出
dist_a = 0.0 dist_b = 35.65165808180456 dist_c = -16.829141392230833
measureDist=True
ch16_16.py pointPolygonTest measureDist True 执行结果
返回实际距离,正负号表示点在轮廓内外,参考原书第 16-23 页。

程序实例 ch16_17.py:measureDist=False,只判断点的位置关系

# ch16_17.py # 与 ch16_16.py 相同,只将 measureDist 改为 False。 dist_a = cv2.pointPolygonTest(hull, point_a, False) dist_b = cv2.pointPolygonTest(hull, point_b, False) dist_c = cv2.pointPolygonTest(hull, point_c, False) print(f"dist_a = {dist_a}") print(f"dist_b = {dist_b}") print(f"dist_c = {dist_c}")
Python Shell 输出
dist_a = 0.0 dist_b = 1.0 dist_c = -1.0
measureDist=False
ch16_17.py pointPolygonTest measureDist False 执行结果
影像画面与 ch16_16.py 相同,输出改为位置关系,参考原书第 16-24 页。
习题

1. 更改 ch16_2.py,不使用 rectangle() 函数建立最小矩形框,改为使用 Numpy 模块自行建立影像图,然后绘制最小矩形框。

2. 使用 explode4.jpg,参考 ch16_8.py 绘制最小三角形包围,但必须使用 for 循环将三角形用红色线条连接。

3. 使用 hand3.jpg,绘制凸包,同时列出所有轮廓的缺陷数量和凸缺陷。

4. 使用 mutstars.jpg,绘制凸包,同时列出所有轮廓的缺陷数量和凸缺陷。

第 16 章习题 3 hand3 凸缺陷参考结果
习题 3 的手势凸缺陷参考结果,参考原书第 16-24 页。
第 16 章习题 4 mutstars 凸缺陷参考结果
习题 4 的多星形凸缺陷参考结果,参考原书第 16-24 页。