OpenCV 轮廓特征详解:moments、contourArea、approxPolyDP、凸包与拟合函数实战指南
2026/9/7 7:26:26 网站建设 项目流程

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

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

本篇指南基于 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& 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.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.hppvoid 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 文档可相互印证。

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询