OpenCV 轮廓特征详解:moments、contourArea、approxPolyDP、凸包与拟合函数实战指南
本篇指南基于 OpenCV 官方 Python 教程中的 "Contour Features" 文档展开,系统讲解如何从 findContours 得到的轮廓中提取各类几何特征——包括矩(moments)、面积、周长、多边形近似、凸包、凸性判断、包围矩形、最小外接圆、椭圆与直线拟合。读完本文,你将掌握全部 12 个轮廓特征函数的用途、参数含义与调用方式,并能结合当前仓库源码(modules/geometry 模块)理解这些函数在底层是如何实现的。
1. 目标与背景
本教程系列回答的核心问题是:拿到轮廓之后,还能对它们做什么? 本篇聚焦"不同轮廓特征"的提取,涵盖面积(area)、周长(perimeter)、质心(centroid)、包围框(bounding box)等常见需求。你将看到大量与轮廓相关的函数,这些函数在 C++ 中声明于 geometry 模块公共头文件,Python 绑定后统一挂在 cv2(即文档中的 cv)命名空间下。
所有示例都以同一张 star.jpg(五角星图像)作为输入,首先二值化并提取轮廓:
import numpy as np
import cv2 as cv
img = cv.imread('star.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
ret, thresh = cv.threshold(img, 127, 255, 0)
contours, hierarchy = cv.findContours(thresh, 1, 2)
兼容性说明:原文使用旧式整数写法
1, 2,其含义分别是cv.RETR_EXTERNAL(只取最外层轮廓)与cv.CHAIN_APPROX_SIMPLE(压缩存储轮廓点)。在新版本 OpenCV 中建议使用具名常量,可读性更好:contours, hierarchy = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
下文均以 cnt = contours[0] 作为待分析轮廓。
2. 矩(Moments)与质心
图像矩(image moments)是一类用于计算物体质心、面积等特征的数学量。OpenCV 提供 cv.moments() 函数,它返回一个包含所有矩值的字典:
cnt = contours[0]
M = cv.moments(cnt)
print(M)
M 是一个形如 {'m10': ..., 'm01': ..., 'm00': ..., 'nu11': ..., ...} 的字典,同时包含原始矩(m**)与中心矩/归一化矩(mu**、nu**)。从矩中可以提取大量有用数据,其中质心(centroid)由如下关系给出:
- Cx = M10 / M00
- Cy = M01 / M00
对应代码:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
源码佐证:
cv.moments的 C++ 实现位于 moments.cpp,函数签名为cv::Moments cv::moments(InputArray _src, bool binary),即对输入区域(可以是轮廓、掩膜或二值图像)逐像素累加 x、y 的幂次积来得到各阶矩。这也解释了为什么M['m00'](0 阶矩)在数值上等于区域面积。
3. 轮廓面积(Contour Area)
轮廓面积可直接用 cv.contourArea() 计算,也可以从矩中读取 M['m00']:
area = cv.contourArea(cnt)
C++ 侧的完整声明见 2d.hpp:
double contourArea( InputArray contour, bool oriented = false );
可以看到除了轮廓外还有一个 oriented 参数(默认 false):当轮廓为顺时针(或按有向定义)时,传入 oriented=True 会得到带符号的面积(负值),这在需要区分轮廓缠绕方向的场景中有用。
4. 轮廓周长(Contour Perimeter)
周长又称弧长(arc length),由 cv.arcLength() 求得。第二个参数指定该形状是否为闭合轮廓(传 True 表示闭合,否则视为一般曲线):
perimeter = cv.arcLength(cnt, True)
该值后面还会被反复用作其他函数的基准(例如近似精度 epsilon 常取周长的一个百分比),所以先把它算出来是常见做法。
5. 轮廓近似(Contour Approximation)
cv.approxPolyDP() 将一个轮廓用更少的顶点近似为另一个形状,精度由你指定的 epsilon 控制。它是 Douglas-Peucker 算法(Ramer-Douglas-Peucker)的实现。
一个典型场景:你想在图中找一个正方形,但由于成像问题得到的是一个"不规则坏形状"。此时可以用该函数把形状近似成规整多边形。其中第二个参数 epsilon 是"轮廓上任意点到近似轮廓的最大允许距离",即精度参数;epsilon 的选择需要经验,常见做法是取周长的一定比例:
epsilon = 0.1 * cv.arcLength(cnt, True)
approx = cv.approxPolyDP(cnt, epsilon, True)
上节配图展示了两个典型结果:绿色曲线分别是 epsilon = 10% 周长与 epsilon = 1% 周长时的近似效果——epsilon 越小,近似曲线越贴合原始轮廓,顶点数也越多;第三个参数指定曲线是否闭合。
源码佐证:实现位于 approx.cpp。可以看到源码对 epsilon 做了显式合法性检查(
epsilon < 0.0或非有限值会抛出StsOutOfRange错误),随后将输入校验为CV_32S/CV_32F的二维点向量,再调用内部 Douglas-Peucker 例程approxPolyDP_完成压缩。函数声明见 2d.hpp,头文件注释与本文一致:epsilon 即"原始曲线与其近似之间的最大距离",closed控制首尾顶点是否相连。实践提示:判断多边形类型常依赖近似后的顶点数,例如
len(approx) == 4视为四边形、len(approx) == 3视为三角形——这正是"近似 + 顶点计数"组合的典型用法。
6. 凸包(Convex Hull)
凸包在结果形态上看似与轮廓近似类似,但算法本质不同(某些情况下二者可能给出相同结果)。cv.convexHull() 会对曲线做凸性缺陷(convexity defects)检查并加以"修正"。直观理解:凸曲线总是向外凸出、或至少是平的;一旦曲线向内凹陷,这些凹陷处就是凸性缺陷。下图中红线是手形轮廓的凸包,双向箭头标出的位置即为凸性缺陷——凸包与轮廓之间局部偏离最大的地方。
函数语法与参数细节:
hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]])
- points:传入的轮廓点集;
- hull:输出结果,通常省略;
- clockwise:方向标志。为
True时输出的凸包按顺时针方向排列,否则为逆时针; - returnPoints:默认
True,此时返回凸包点的坐标;若为False,则返回凸包点对应轮廓点的索引。
因此只取凸包本身时:
hull = cv.convexHull(cnt)
但若要查找凸性缺陷,必须传 returnPoints=False。原文用矩形做了演示:先求得其轮廓 cnt,再以 returnPoints=True 求凸包,得到四个角点:
[[[234 202]], [[ 51 202]], [[ 51 79]], [[234 79]]]
再以 returnPoints=False 求同一直,得到 [[129], [67], [0], [142]]——这些是轮廓中对应点的索引。例如第一个值:cnt[129] = [[234, 202]],与上一个结果的第一个点完全一致(其余点同理)。理解"索引形式"的凸包是后续讨论凸性缺陷(convexity defects)的关键,教程后文还会再次用到它。
源码佐证:C++ 声明见 2d.hpp:
void convexHull(InputArray points, OutputArray hull, int clockwise=false, bool returnPoints=true),参数默认值与上表一致。
7. 凸性判断(Checking Convexity)
判断一条曲线是否整体为凸,直接用 cv.isContourConvex(),它只返回布尔值:
k = cv.isContourConvex(cnt)
源码佐证:声明见 2d.hpp:
bool isContourConvex(InputArray contour)。
8. 包围矩形(Bounding Rectangle)
包围矩形分两种:轴对齐直矩形与最小面积旋转矩形。
8.a. 直包围矩形(Straight Bounding Rectangle)
直矩形不考虑物体旋转,因此其面积不一定是所有包围矩形中最小的。由 cv.boundingRect() 求得。设左上角坐标为 (x, y),宽高为 (w, h):
x, y, w, h = cv.boundingRect(cnt)
cv.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
它返回一个 cv.Rect(可解包为 x, y, w, h 四个整型分量),绘制时以左上角 (x, y) 与右下角 (x+w, y+h) 两个对角点定义矩形,(0,255,0) 为 BGR 绿色,线宽 2。
8.b. 旋转包围矩形(Rotated Rectangle)
旋转矩形以最小面积为约束,因此会考虑物体的旋转角度。使用 cv.minAreaRect(),它返回一个 Box2D 结构,包含:中心 (x, y)、宽高 (width, height) 以及旋转角度。但绘制该矩形需要它的 4 个角点,由 cv.boxPoints() 获得:
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img, [box], 0, (0, 0, 255), 2)
兼容性说明:
np.int0在新版 NumPy 中已移除,可改为box = box.astype(np.int32),效果相同(boxPoints返回浮点角点,绘制前需转为整型坐标)。
两种矩形在同一张图中的对比:绿色为直包围矩形,红色为旋转包围矩形。
源码佐证:
minAreaRect的声明见 2d.hpp,头文件文档特别说明:旋转角定义为"以顺时针顺序取 y 坐标最大的角点为起点,连接起点与终点的直线"与水平轴的夹角,取值范围恒在[-90, 0)区间内(当物体旋转超过直角时改用下一条边测量角度)。另外boxPoints返回的 4 个顶点按顺时针顺序排列,起点为 y 坐标最大(y 相同时取最右)的点,见 2d.hpp——这一点在需要按顺序处理角点(如目标检测框排序)时非常关键。
9. 最小外接圆(Minimum Enclosing Circle)
用 cv.minEnclosingCircle() 求完全覆盖物体且面积最小的圆:
(x, y), radius = cv.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
cv.circle(img, center, radius, (0, 255, 0), 2)
函数返回圆心 (x, y) 与半径 radius(均为浮点,绘制前转整型),cv.circle 以绿色 BGR 值 (0,255,0)、线宽 2 画出该外接圆。
源码佐证:声明见 2d.hpp,头文件注明该函数"使用迭代算法"求 2D 点集的最小包围圆,输出参数为
Point2f& center与float& radius,与 Python 端的解包形式一致。
10. 椭圆拟合(Fitting an Ellipse)
cv.fitEllipse() 对物体拟合一个椭圆,返回的是该椭圆的旋转包围矩形(椭圆内切于其中):
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img, ellipse, (0, 255, 0), 2)
返回的 ellipse 元组形如 ((center_x, center_y), (major_axis, minor_axis), angle),可直接传入 cv.ellipse 绘制,无需再手工换算角点。
源码佐证:声明见 2d.hpp:
RotatedRect fitEllipse(InputArray points),即 C++ 侧返回RotatedRect,Python 端呈现为三元组。
11. 直线拟合(Fitting a Line)
同理,可以对一组点拟合直线。假设图像中有一组白色点(对应 cnt),可近似出一条直线:
rows, cols = img.shape[:2]
[vx, vy, x, y] = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01)
lefty = int((-x * vy / vx) + y)
righty = int(((cols - x) * vy / vx) + y)
cv.line(img, (cols - 1, righty), (0, lefty), (0, 255, 0), 2)
这里 cv.fitLine 采用参数化表示,返回 (vx, vy, x, y):直线过点 (x, y),方向向量为 (vx, vy)。因此当 x = 0 时纵坐标为 lefty,当 x = cols-1(图像右边界)时纵坐标为 righty,两点即确定整条绿色拟合线。
参数说明(对照 C++ 声明 2d.hpp void fitLine(InputArray points, OutputArray line, int distType, int param, double ransacReprojThreshold, double confidence)):
- 第二参数
cv.DIST_L2:距离度量类型,此处为最小二乘(L2 范数); - 第三参数
0:param,DIST_L1/DIST_FAIR等稳健度量下的分位数相关参数,L2 下不使用; - 第四、五参数
0.01, 0.01:RANSAC 重投影阈值与置信度,本例按默认精度传入。
12. 函数速查表与仓库入口
| 功能 | Python 函数 | C++ 声明位置 |
|---|---|---|
| 矩 / 质心 | cv.moments(cnt) |
moments.cpp |
| 轮廓面积 | cv.contourArea(cnt) |
2d.hpp |
| 周长 | cv.arcLength(cnt, closed) |
2d.hpp |
| 多边形近似 | cv.approxPolyDP(cnt, eps, closed) |
approx.cpp |
| 凸包 | cv.convexHull(cnt) |
2d.hpp |
| 凸性判断 | cv.isContourConvex(cnt) |
2d.hpp |
| 直包围矩形 | cv.boundingRect(cnt) |
2d.hpp |
| 旋转包围矩形 | cv.minAreaRect(cnt) |
2d.hpp |
| 旋转矩形角点 | cv.boxPoints(rect) |
2d.hpp |
| 最小外接圆 | cv.minEnclosingCircle(cnt) |
2d.hpp |
| 椭圆拟合 | cv.fitEllipse(cnt) |
2d.hpp |
| 直线拟合 | cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01) |
2d.hpp |
几点实践小结:
- epsilon 的选取是近似类函数的核心经验:
approxPolyDP的 epsilon 常用k * cv.arcLength(cnt, True)(k 取 1%~10%)起步,再按顶点数结果微调; returnPoints=False的索引形式是凸包类接口的重要特性,凸性缺陷检测等进阶功能依赖它;- 从源码结构看,本文涉及的几何特征函数在仓库中统一声明于 geometry 模块 的头文件中,Python 端则全部映射为
cv2命名空间函数,教程代码与 API 文档可相互印证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


