首页
/ OpenCV 轮廓特征详解:moments、contourArea、approxPolyDP、凸包与拟合函数实战指南

OpenCV 轮廓特征详解:moments、contourArea、approxPolyDP、凸包与拟合函数实战指南

2026-09-06 12:56:03作者:裴锟轩Denise

本篇指南基于 OpenCV 官方 Python 教程中的 "Contour Features" 文档展开,系统讲解如何从 findContours 得到的轮廓中提取各类几何特征——包括矩(moments)、面积、周长、多边形近似、凸包、凸性判断、包围矩形、最小外接圆、椭圆与直线拟合。读完本文,你将掌握全部 12 个轮廓特征函数的用途、参数含义与调用方式,并能结合当前仓库源码(modules/geometry 模块)理解这些函数在底层是如何实现的。

approxPolyDP 不同 epsilon 下的轮廓近似效果,绿色曲线为近似结果

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.hppvoid convexHull(InputArray points, OutputArray hull, int clockwise=false, bool returnPoints=true),参数默认值与上表一致。

7. 凸性判断(Checking Convexity)

判断一条曲线是否整体为凸,直接用 cv.isContourConvex(),它只返回布尔值:

k = cv.isContourConvex(cnt)

源码佐证:声明见 2d.hppbool 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& centerfloat& 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.hppRotatedRect 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 范数);
  • 第三参数 0paramDIST_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

几点实践小结:

  1. epsilon 的选取是近似类函数的核心经验approxPolyDP 的 epsilon 常用 k * cv.arcLength(cnt, True)(k 取 1%~10%)起步,再按顶点数结果微调;
  2. returnPoints=False 的索引形式是凸包类接口的重要特性,凸性缺陷检测等进阶功能依赖它;
  3. 从源码结构看,本文涉及的几何特征函数在仓库中统一声明于 geometry 模块 的头文件中,Python 端则全部映射为 cv2 命名空间函数,教程代码与 API 文档可相互印证。
登录后查看全文
热门项目推荐
相关项目推荐