第 08 章 — 轮廓分析
第 08 章 — 轮廓分析
8.1 轮廓基础
轮廓(Contour)是图像中具有相同颜色或强度的连续点组成的曲线,是形状分析和物体检测的基础。
轮廓 vs 边缘
| 概念 | 说明 | 数据结构 |
|---|
| 边缘(Edge) | 像素级的梯度变化 | 二值图像 |
| 轮廓(Contour) | 连续的点序列 | 点坐标列表 |
注意: 查找轮廓前必须先进行二值化或边缘检测处理。
8.2 查找轮廓
import cv2
import numpy as np
# 读取并预处理
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 查找轮廓
contours, hierarchy = cv2.findContours(
binary,
cv2.RETR_TREE, # 检索模式
cv2.CHAIN_APPROX_SIMPLE # 近似方法
)
print(f"找到 {len(contours)} 个轮廓")
for i, cnt in enumerate(contours):
print(f" 轮廓 #{i}: {cnt.shape[0]} 个点, 面积={cv2.contourArea(cnt):.0f}")
// C++ 查找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(binary, contours, hierarchy,
cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
检索模式(Retrieval Modes)
| 模式 | 常量 | 说明 |
|---|
RETR_EXTERNAL | 只取外层 | 只检测最外层轮廓 |
RETR_LIST | 列表 | 检测所有轮廓,不建立层级 |
RETR_CCOMP | 两层 | 只有两层:外层和内层 |
RETR_TREE | 树 | 检测所有轮廓,建立完整层级树 |
RETR_FLOODFILL | 洪水填充 | 未压缩的轮廓 |
近似方法(Approximation Methods)
| 方法 | 常量 | 说明 |
|---|
CHAIN_APPROX_NONE | 存储所有点 | 精确但数据量大 |
CHAIN_APPROX_SIMPLE | 压缩 | 只存储端点(推荐) |
CHAIN_APPROX_TC89_L1 | Teh-Chin L1 | 链码近似 |
CHAIN_APPROX_TC89_KCOS | Teh-Chin KCOS | 链码近似 |
8.3 绘制轮廓
import cv2
img = cv2.imread("shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
# 绘制所有轮廓(绿色,线宽 2)
result_all = img.copy()
cv2.drawContours(result_all, contours, -1, (0, 255, 0), 2)
# 绘制单个轮廓(第 3 个)
result_single = img.copy()
cv2.drawContours(result_single, contours, 2, (0, 0, 255), 2)
# 填充轮廓
result_filled = img.copy()
cv2.drawContours(result_filled, contours, -1, (255, 0, 0), cv2.FILLED)
# 用不同颜色绘制每个轮廓
result_colorful = img.copy()
for i, cnt in enumerate(contours):
color = tuple(np.random.randint(0, 255, 3).tolist())
cv2.drawContours(result_colorful, [cnt], -1, color, 2)
8.4 轮廓属性
8.4.1 基本几何属性
import cv2
import numpy as np
# 假设已获取轮廓 cnt
for i, cnt in enumerate(contours):
# 面积
area = cv2.contourArea(cnt)
# 周长(闭合轮廓)
perimeter = cv2.arcLength(cnt, closed=True)
# 边界矩形
x, y, w, h = cv2.boundingRect(cnt) # 正矩形
rect = cv2.minAreaRect(cnt) # 最小面积旋转矩形
box = cv2.boxPoints(rect) # 旋转矩形的四个顶点
box = np.int32(box)
# 最小外接圆
(cx, cy), radius = cv2.minEnclosingCircle(cnt)
# 最小外接椭圆(至少需要 5 个点)
if len(cnt) >= 5:
ellipse = cv2.fitEllipse(cnt)
# 拟合直线
if len(cnt) >= 5:
line = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01)
# 矩(Moments)
M = cv2.moments(cnt)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00']) # 质心 x
cy = int(M['m01'] / M['m00']) # 质心 y
else:
cx, cy = 0, 0
# 纵横比
aspect_ratio = float(w) / h if h > 0 else 0
# 占空比(Extent)— 面积 / 边界矩形面积
extent = float(area) / (w * h) if w * h > 0 else 0
# 充实度(Solidity)— 面积 / 凸包面积
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area) / hull_area if hull_area > 0 else 0
# 等效直径
equi_diameter = np.sqrt(4 * area / np.pi)
print(f"轮廓 #{i}:")
print(f" 面积={area:.0f}, 周长={perimeter:.1f}")
print(f" 边界框=({x},{y},{w}×{h})")
print(f" 质心=({cx},{cy})")
print(f" 纵横比={aspect_ratio:.2f}, 占空比={extent:.2f}, "
f"充实度={solidity:.2f}")
8.4.2 矩(Moments)详解
M = cv2.moments(cnt)
# 空间矩
m00 = M['m00'] # 面积
m10 = M['m10'] # x 方向一阶矩
m01 = M['m01'] # y 方向一阶矩
# 中心矩(平移不变)
mu20 = M['mu20']
mu11 = M['mu11']
mu02 = M['mu02']
# 归一化中心矩(平移 + 缩放不变)
nu20 = M['nu20']
nu11 = M['nu11']
nu02 = M['nu02']
# Hu 矩(平移 + 缩放 + 旋转不变)
hu = cv2.HuMoments(M).flatten()
# 轮廓匹配(Hu 矩)
match_score = cv2.matchShapes(cnt1, cnt2, cv2.CONTOURS_MATCH_I2, 0)
# 值越小越相似
8.5 轮廓近似
import cv2
import numpy as np
# 多边形近似(Douglas-Peucker 算法)
epsilon = 0.02 * cv2.arcLength(cnt, True) # 精度 = 周长的 2%
approx = cv2.approxPolyDP(cnt, epsilon, True)
print(f"原始点数: {len(cnt)}, 近似点数: {len(approx)}")
# 根据近似点数判断形状
def identify_shape(approx_points):
n = len(approx_points)
if n == 3:
return "三角形"
elif n == 4:
# 检查是否为正方形
rect = cv2.minAreaRect(approx_points)
w, h = rect[1]
ratio = min(w, h) / max(w, h) if max(w, h) > 0 else 0
return "正方形" if ratio > 0.9 else "矩形"
elif n == 5:
return "五边形"
elif n > 8:
return "圆形"
else:
return f"{n}边形"
shape = identify_shape(approx)
print(f"检测到形状: {shape}")
8.6 凸包
import cv2
# 凸包 — 包含所有点的最小凸多边形
hull = cv2.convexHull(cnt)
# 凸缺陷(物体凹陷部分)
hull_indices = cv2.convexHull(cnt, returnPoints=False)
defects = cv2.convexityDefects(cnt, hull_indices)
# 凸性判断
is_convex = cv2.isContourConvex(cnt)
print(f"是否凸: {is_convex}")
# 绘制凸包
result = img.copy()
cv2.drawContours(result, [hull], -1, (0, 255, 0), 2)
# 绘制凸缺陷
if defects is not None:
for i in range(defects.shape[0]):
s, e, f, d = defects[i, 0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv2.circle(result, far, 5, (0, 0, 255), -1)
8.7 轮廓层级
层级结构表示轮廓之间的包含关系(父子关系)。
import cv2
contours, hierarchy = cv2.findContours(
binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
# hierarchy 结构: [next, previous, first_child, parent]
# hierarchy[0] 为第一层
print(f"hierarchy shape: {hierarchy.shape}")
print(f"hierarchy:\n{hierarchy}")
# 遍历层级关系
for i, cnt in enumerate(contours):
next_c = hierarchy[0][i][0]
prev_c = hierarchy[0][i][1]
child = hierarchy[0][i][2]
parent = hierarchy[0][i][3]
depth = 0
p = parent
while p != -1:
depth += 1
p = hierarchy[0][p][3]
print(f"轮廓 #{i}: 深度={depth}, 父={parent}, "
f"子={child}, 面积={cv2.contourArea(cnt):.0f}")
# 只获取特定层级
# 外层轮廓
outer = [contours[i] for i in range(len(contours))
if hierarchy[0][i][3] == -1]
层级模式对比
| 模式 | 效果 |
|---|
RETR_EXTERNAL | 只返回最外层(父=-1 的) |
RETR_LIST | 所有轮廓,层级全为-1 |
RETR_CCOMP | 两层结构(外层 + 内层) |
RETR_TREE | 完整树结构 |
8.8 轮廓匹配
import cv2
# 形状匹配(Hu 矩)
# method: CONTOURS_MATCH_I1, I2, I3
score = cv2.matchShapes(cnt1, cnt2, cv2.CONTOURS_MATCH_I2, 0)
# 值越小越相似,0 = 完全相同
# 距离匹配(点到轮廓距离)
dist = cv2.pointPolygonTest(cnt, (100, 200), True)
# > 0: 点在轮廓内部
# < 0: 点在轮廓外部
# = 0: 点在轮廓上
8.9 实战:形状检测器
"""
shape_detector.py — 自动检测并标注几何形状
"""
import cv2
import numpy as np
def detect_shapes(image_path):
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255,
cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(binary, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
result = img.copy()
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 500:
continue
# 多边形近似
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
n = len(approx)
# 形状识别
shape = "未知"
if n == 3:
shape = "三角形"
elif n == 4:
x, y, w, h = cv2.boundingRect(approx)
ratio = float(w) / h
shape = "正方形" if 0.9 <= ratio <= 1.1 else "矩形"
elif n == 5:
shape = "五边形"
elif n == 6:
shape = "六边形"
elif n > 6:
shape = "圆形"
# 绘制结果
cv2.drawContours(result, [approx], -1, (0, 255, 0), 2)
M = cv2.moments(cnt)
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
cv2.putText(result, shape, (cx - 30, cy),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
return result
# 使用
# result = detect_shapes("shapes.png")
# cv2.imwrite("detected.png", result)
8.10 实战:车牌区域检测
"""
plate_region.py — 简易车牌区域定位
"""
import cv2
import numpy as np
def find_plate_region(image_path):
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Sobel 边缘
sobel_x = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize=3)
_, binary = cv2.threshold(sobel_x, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 形态学操作
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 5))
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# 查找轮廓
contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
result = img.copy()
candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
ratio = float(w) / h
area = cv2.contourArea(cnt)
# 车牌特征:宽高比约 3:1,面积适中
if 2.0 < ratio < 6.0 and 1000 < area < 50000:
candidates.append((x, y, w, h))
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
print(f"找到 {len(candidates)} 个候选区域")
return result
# 使用
# result = find_plate_region("car.jpg")
8.11 轮廓操作速查表
| 操作 | 函数 | 说明 |
|---|
| 查找轮廓 | findContours() | 二值图 → 轮廓列表 |
| 绘制轮廓 | drawContours() | 在图像上绘制 |
| 面积 | contourArea() | 轮廓面积(像素²) |
| 周长 | arcLength() | 轮廓周长 |
| 边界矩形 | boundingRect() | 正外接矩形 |
| 最小矩形 | minAreaRect() | 旋转最小矩形 |
| 最小圆 | minEnclosingCircle() | 最小外接圆 |
| 凸包 | convexHull() | 最小凸多边形 |
| 凸性 | isContourConvex() | 是否凸多边形 |
| 多边形近似 | approxPolyDP() | 顶点简化 |
| 矩 | moments() | 空间矩/中心矩 |
| 形状匹配 | matchShapes() | Hu 矩相似度 |
| 点测试 | pointPolygonTest() | 点与轮廓关系 |
8.12 扩展阅读
本章小结: 掌握了轮廓的查找、绘制、属性计算、近似、凸包、层级结构和形状匹配,能够完成形状检测、物体计数等实际任务。