强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenCV 计算机视觉完全教程 / 第 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_L1Teh-Chin L1链码近似
CHAIN_APPROX_TC89_KCOSTeh-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 扩展阅读

资源链接说明
OpenCV 轮廓教程docs.opencv.org/4.x/d4/d73/tutorial_py_contours_begin轮廓入门
轮廓属性docs.opencv.org/4.x/d1/d32/tutorial_py_contour_properties属性详解
下一章第 09 章 — 几何变换仿射/透视/旋转

本章小结: 掌握了轮廓的查找、绘制、属性计算、近似、凸包、层级结构和形状匹配,能够完成形状检测、物体计数等实际任务。