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,因此可以在无头环境中运行。下图仍仅作视觉参考。
了解 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 为负数时会填充所选轮廓。