HOWTO · OpenCV

OpenCV 形状检测

本文介绍如何使用 OpenCV 的 findContours() 和 approxPolyDP() 函数检测图像中存在的形状。

本页内容

本教程将介绍如何使用 OpenCV 准备图像、查找外部轮廓、将每条轮廓近似为多边形,并把顶点数量作为简单的形状分类启发式依据。

准备用于形状检测的二值图像

findContours() 以二值掩码为输入。在下面的流程中,前景形状为白色(255),背景为黑色(0)。使用 cvtColor() 将 BGR 图像转换为灰度图,然后使用 threshold() 和 Otsu 方法根据图像自动选择阈值。如果浅色背景上的前景较暗,请反转生成的掩码,或使用反向阈值处理,让对象显示为白色。

读取输入文件时,可使用 cv2.imread("shape-detection.webp"),并在处理前检查返回的图像是否为 None。将 BGR 图像转换为灰度图,再通过 cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 创建二值掩码。掩码中的前景对象应为白色,背景应为黑色;如果图像的明暗极性相反,请使用反向阈值处理。

使用 findContours() 和 approxPolyDP() 检测并分类形状

使用 RETR_EXTERNAL 调用 findContours(),提取每个白色对象的外轮廓,然后用 approxPolyDP() 近似轮廓并计算所得顶点数。3 个顶点通常提示这是三角形,4 个顶点提示这是四边形,5 个顶点提示这是五边形。这些判断只是启发式规则:有 4 个顶点并不能证明形状是正方形。超过 12 个顶点时,可以将轮廓标记为近似圆形;这个阈值取决于近似容差、图像分辨率和轮廓质量。

简短示例使用有代表性的顶点数量来展示各类标签。下面的完整图像示例会从脚本所在目录加载提供的 shape-detection.webp 文件,因此即使从其他工作目录启动脚本,也能找到该文件;示例会把标注后的 PNG 保存到当前工作目录。运行图像处理流程前,请安装 OpenCV(opencv-python)和 NumPy。提供的运行时证据仅涵盖标签示例,并不能证明 OpenCV 图像处理流程已执行。

def label_for_vertex_count(count):
    if count == 3:
        return "triangle"
    if count == 4:
        return "quadrilateral"
    if count == 5:
        return "pentagon"
    if count > 12:
        return "circle-like"
    return "polygon"


approximated_vertex_counts = [3, 4, 5, 16]
for count in approximated_vertex_counts:
    print(f"{label_for_vertex_count(count)}: {count} vertices")
triangle: 3 vertices
quadrilateral: 4 vertices
pentagon: 5 vertices
circle-like: 16 vertices

简短的代码片段单独展示了分类规则。下面的完整脚本将同样的规则应用于所提供的图像:检查图像载入、将 BGR 像素转换为灰度图,并构建 Otsu 二值掩码。掩码假设明亮的前景对象位于较暗的背景上;当前景较暗、背景较亮时,请使用 cv2.THRESH_BINARY_INV。所提供的图像包含多个面板,因此外部轮廓除了形状本身,也会包含面板边界。标签仍然只是启发式判断:4 个顶点表示四边形,而不一定是正方形;轮廓顺序和顶点数量取决于图像质量和近似容差。

from pathlib import Path


def label_for_vertex_count(count):
    if count == 3:
        return "triangle"
    if count == 4:
        return "quadrilateral"
    if count == 5:
        return "pentagon"
    if count > 12:
        return "circle-like"
    return "polygon"


def main():
    try:
        import cv2
    except ImportError:
        print("OpenCV is required; install it with python -m pip install opencv-python.")
        raise SystemExit(1)

    image_path = Path(__file__).resolve().with_name("shape-detection.webp")
    image = cv2.imread(str(image_path))
    if image is None:
        print(f"Could not load image: {image_path}")
        raise SystemExit(1)

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, mask = cv2.threshold(
        gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
    )
    result = cv2.findContours(
        mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    contours = result[0] if len(result) == 2 else result[1]

    draw = True
    annotated = image.copy()
    for contour in contours:
        perimeter = cv2.arcLength(contour, True)
        approximation = cv2.approxPolyDP(
            contour, 0.01 * perimeter, True
        )
        label = label_for_vertex_count(len(approximation))
        print(f"{label}: {len(approximation)} vertices")

        if draw:
            cv2.drawContours(
                annotated, [approximation], -1, (0, 0, 255), 2
            )

    if draw:
        output_path = Path.cwd() / "shape-detection-annotated.png"
        if cv2.imwrite(str(output_path), annotated):
            print(f"Saved annotated image: {output_path}")
        else:
            print(f"Could not save annotated image: {output_path}")
            raise SystemExit(1)


if __name__ == "__main__":
    main()

这些图像处理代码供读者参考和改编;提供的运行时输出来自上面的独立标签示例,并不代表这些代码已执行。

OpenCV 颜色元组采用 BGR 顺序,因此 (0, 0, 255) 会绘制红色。粗细设为 2 时会绘制轮廓线;粗细为负数时会填充轮廓。

使用 drawContours() 绘制检测到的形状

在完整脚本中,draw = True 会以红色绘制每条近似轮廓,并将 shape-detection-annotated.png 保存到当前工作目录;cv2.imwrite() 会报告保存是否成功。设置 draw = False 可同时跳过绘制和保存。脚本不会打开 GUI,因此可以在无头环境中运行。下图仍仅作视觉参考。

shape detection

了解 OpenCV 轮廓函数的参数

findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 接收二值掩码、轮廓检索模式和轮廓近似方法。RETR_EXTERNAL 只返回外边界,适用于任务不需要处理内部孔洞的情况。CHAIN_APPROX_SIMPLE 会压缩轮廓点中的直线段,同时保留边界形状。不同版本的 OpenCV 中,findContours() 返回值的数量可能不同;若要兼容这两种形式,可用 result[0] if len(result) == 2 else result[1] 选取轮廓。

approxPolyDP(contour, epsilon, closed) 接收一条轮廓、最大近似距离,以及表示曲线是否闭合的布尔值。epsilon 常用的设置方式是 0.01 * cv2.arcLength(contour, True);提高这一比例通常会减少顶点,降低这一比例则会让近似结果更贴合轮廓。该函数使用 Douglas–Peucker 算法,因此顶点数量同时取决于此容差和图像本身。

对于 drawContours(image, contours, contour_index, color, thickness),函数会原地修改图像;contours 提供边界,contour_index 选择一条轮廓(设为 -1 时选择全部),color 是 BGR 元组。thickness 为负数时会填充所选轮廓。