OpenCV.js 轮廓进阶函数实战:凸包缺陷、点在多边形内测试与形状匹配
本篇基于 OpenCV.js 官方教程 Contours : More Functions 展开,聚焦轮廓分析中的三个进阶函数:cv.convexityDefects(凸包缺陷)、cv.pointPolygonTest(点到轮廓的最短距离)与 cv.matchShapes(形状相似度匹配)。读完本篇,你将掌握如何用这三个函数量化轮廓的凹陷程度、判断点与轮廓的位置关系、比较任意两个轮廓的形状相似性,并能对照 OpenCV 源码理解每个函数的返回格式与边界行为,配合仓库中现成的浏览器端可运行示例(Web 示例页面 + 输入图像)直接复现实验。
一、功能定位:这三个函数解决什么问题
在 OpenCV 的轮廓分析体系中,cv.findContours 提取出轮廓点集之后,还需要一系列“几何度量”函数来刻画轮廓的形态。本篇三个函数分别对应三类典型需求:
| 函数 | 解决的问题 | 输入 | 输出 |
|---|---|---|---|
cv.convexityDefects |
找出轮廓相对其凸包的所有凹陷位置及凹陷深度 | 轮廓 + 凸包索引 | 4 元素缺陷向量(起点、终点、最远点、深度) |
cv.pointPolygonTest |
计算点到轮廓的最短距离,并带内外符号 | 轮廓 + 待测点 | 带符号距离(正/负/零) |
cv.matchShapes |
比较两个轮廓(或灰度图)的形状相似度 | 两个轮廓 | 相似度度量值(越小越相似) |
这三个函数在 C++ 端属于几何算法核心,在当前仓库中实现于 modules/geometry/src/convhull.cpp 等文件,函数声明见 modules/geometry/include/opencv2/geometry/2d.hpp。
二、凸包缺陷 Convexity Defects
2.1 概念:什么是凸包缺陷
回顾轮廓特征章节(js_contour_features)中对凸包(convex hull)的定义:凸包是包裹整个轮廓的最小凸多边形。物体轮廓上任何偏离该凸包的位置,都可以被视为一个“凸包缺陷”(convexity defect),即轮廓的凹陷处。
OpenCV 的可视化方式是:连接凸包边的起点和终点画一条直线,然后在轮廓上离这条直线最远的点处画一个圆。这个最远点就是缺陷的代表点,它到弦的距离就是缺陷深度。
注意:求凸包时必须传
returnPoints = false(JS 中即cv.convexHull(cnt, hull, false, false)),得到的凸包才是轮廓点索引,cv.convexityDefects需要这种索引形式的输入。若返回的是坐标点,缺陷计算无法把索引对应回原始轮廓。
2.2 函数接口与参数
cv.convexityDefects(contour, convexhull, convexityDefect)
| 参数 | 说明 |
|---|---|
contour |
输入轮廓,要求为 CV_32SC2 点集(findContours 的默认输出) |
convexhull |
由 cv.convexHull 求得、且必须包含轮廓点索引的凸包 |
convexityDefect |
输出的凸包缺陷向量。每个缺陷是一个 4 元素数组 (start_index, end_index, farthest_pt_index, fixpt_depth):前三个是缺陷起点、终点、最远点在原始轮廓中的 0 基索引;fixpt_depth 是最远点到凸包边的距离的定点近似(8 位小数位),要得到浮点深度需除以 256,即 depth = fixpt_depth / 256.0 |
2.3 完整示例代码
仓库中附有可直接在浏览器运行的示例页 js_contours_more_functions_convexityDefects.html,其核心代码如下:
let src = cv.imread('canvasInput');
let dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
cv.threshold(src, src, 100, 200, cv.THRESH_BINARY);
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
cv.findContours(src, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
let hull = new cv.Mat();
let defect = new cv.Mat();
let cnt = contours.get(0);
let lineColor = new cv.Scalar(255, 0, 0);
let circleColor = new cv.Scalar(255, 255, 255);
cv.convexHull(cnt, hull, false, false); // 第 4 个参数 returnPoints 必须为 false
cv.convexityDefects(cnt, hull, defect);
for (let i = 0; i < defect.rows; ++i) {
// defect.data32S 中每 4 个 int 描述一个缺陷
let start = new cv.Point(cnt.data32S[defect.data32S[i * 4] * 2],
cnt.data32S[defect.data32S[i * 4] * 2 + 1]);
let end = new cv.Point(cnt.data32S[defect.data32S[i * 4 + 1] * 2],
cnt.data32S[defect.data32S[i * 4 + 1] * 2 + 1]);
let far = new cv.Point(cnt.data32S[defect.data32S[i * 4 + 2] * 2],
cnt.data32S[defect.data32S[i * 4 + 2] * 2 + 1]);
cv.line(dst, start, end, lineColor, 2, cv.LINE_AA, 0);
cv.circle(dst, far, 3, circleColor, -1);
}
cv.imshow('canvasOutput', dst);
src.delete(); dst.delete(); hierarchy.delete(); contours.delete(); hull.delete(); defect.delete();
几个关键点:
defect.data32S[i * 4]取的是轮廓点索引,而cnt.data32S[k * 2] / [k * 2 + 1]才是该点的 x、y 坐标(CV_32SC2每点占 2 个 32 位整数),二者必须区分;- 示例对输入图(shape.jpg)做灰度化 + 二值化(阈值 100/200),用
cv.RETR_CCOMP取轮廓,最终在dst上用红色画“起点—终点”弦线、白色画最远点圆,直观呈现每个凹陷; - 示例末尾统一调用
delete()释放Mat,这是 OpenCV.js 中避免内存泄漏的惯用写法。
2.4 源码级行为解析
阅读 modules/geometry/src/convhull.cpp 中 convexHull 的实现,可以确认“必须传索引”的原因:当 returnPoints 为 false 时,函数把结果写成 CV_32S 的一维索引向量(Mat(nout, 1, CV_32S, hullbuf));为 true 时才拷贝回坐标点。
再看 convexityDefects 的 C++ 实现(自 L353 起),可以得到几条实践边界:
- 点集过少时直接返回空:轮廓点数
npoints <= 3时缺陷向量被置空返回——三角形本身就是凸的,没有缺陷可言; - 凸包退化为 1~2 点时视为恒凸:源码注释明确
if hull consists of one or two points, contour is always convex,同样返回空缺陷; - 凸包索引必须单调:函数会校验凸包索引序列的单调性,若轮廓存在自相交导致索引不单调,会抛出
StsBadArg错误("The convex hull indices are not monotonous..."),这是遇到异常输入时值得排查的方向; - 深度的几何含义:对每一对相邻凸包点
pt0 → pt1,实现沿轮廓逐点计算点到弦线的垂直距离dist = |−dy0*dx + dx0*dy| * scale,其中scale = 1/|pt1−pt0|,记录最大距离点作为farthest_pt_index,深度即该距离的定点量化值。
三、点在多边形内测试 Point Polygon Test
3.1 功能与返回值的符号约定
cv.pointPolygonTest 求图像中一个点与轮廓之间的最短距离,返回带符号距离:
- 点在轮廓内部 → 正值;
- 点在轮廓外部 → 负值;
- 点恰好在轮廓上 → 零。
这意味着一次调用同时完成了“内外判断”与“距离度量”两件事,常用于前景/背景判定、质心校验等场景。
3.2 函数接口
cv.pointPolygonTest(contour, pt, measureDist)
| 参数 | 说明 |
|---|---|
contour |
输入轮廓 |
pt |
待测点,cv.Point 类型 |
measureDist |
为 true 时估计点到最近轮廓边的带符号距离;为 false 时只判断点在轮廓内/外/上(返回 ±1 或 0) |
3.3 使用示例
let dist = cv.pointPolygonTest(cnt, new cv.Point(50, 50), true);
若只想判断内外,把第三个参数改为 false 即可,函数只返回 1(内)、-1(外)或 0(在轮廓上),不计算实际距离。该函数的 C++ 实现位于 modules/geometry/src/geometry.cpp,与前述凸包函数同属 geometry 模块的 2D 几何工具集。
四、形状匹配 Match Shapes
4.1 基于 Hu 矩的相似度度量
cv.matchShapes() 用于比较两个形状(两个轮廓,或两张灰度图),返回一个相似度度量值——结果越小,匹配越好。它基于 Hu 矩(Hu-moment values)计算:Hu 矩具有平移、缩放、旋转不变性,因此两个形状即使经过平移或大小缩放,仍能得到较小的匹配误差,适合做形状级别的识别与归类。不同的比较方法(method)对应不同的不变矩组合,OpenCV 提供了多种 cv::ShapeMatchModes,可选方法可在官方 API 文档中查阅。
4.2 函数接口
cv.matchShapes(contour1, contour2, method, parameter)
| 参数 | 说明 |
|---|---|
contour1 |
第一个轮廓或灰度图像 |
contour2 |
第二个轮廓或灰度图像 |
method |
比较方法,取值见 cv::ShapeMatchModes |
parameter |
方法相关参数(当前版本暂不支持) |
4.3 完整示例代码:在 coins.jpg 上比较两枚硬币轮廓
仓库提供示例页 js_contours_more_functions_shape.html,输入图像为 coins.jpg,核心逻辑如下:
let src = cv.imread('canvasInput');
let dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
cv.threshold(src, src, 177, 200, cv.THRESH_BINARY);
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
cv.findContours(src, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
let contourID0 = 10;
let contourID1 = 5;
let color0 = new cv.Scalar(255, 0, 0);
let color1 = new cv.Scalar(0, 0, 255);
// 可尝试更多不同的参数
let result = cv.matchShapes(contours.get(contourID0), contours.get(contourID1), 1, 0);
matchShapesOutput.innerHTML = result;
cv.drawContours(dst, contours, contourID0, color0, 1, cv.LINE_8, hierarchy, 100);
cv.drawContours(dst, contours, contourID1, color1, 1, cv.LINE_8, hierarchy, 100);
cv.imshow('canvasOutput', dst);
src.delete(); dst.delete(); contours.delete(); hierarchy.delete();
要点说明:
- 示例中
method = 1,对应cv::ShapeMatchModes中的CONTOURS_MATCH_I1;换用其他枚举值(如 2、3)即可对比不同度量方法的结果差异; - 由于输入是二值化后的硬币图,
findContours会得到多个轮廓,示例取索引 10 和 5 两条轮廓分别画成红色与蓝色以便目视对照,并输出二者匹配得分; drawContours的倒数第二个参数传了hierarchy与偏移量 100,是为了在有层级(外轮廓/孔洞)的轮廓集合中正确绘制指定contourID。
五、实践要点小结
结合文档与源码,使用这三个函数时建议注意:
- 凸包索引先行:
cv.convexHull第 4 个参数(returnPoints)必须为false,缺陷函数才拿得到索引;这是文档明确强调的前置条件,源码中CV_32S索引向量的输出形式也印证了这一点。 - 深度换算:
convexityDefects返回的fixpt_depth是定点数,展示或阈值过滤前记得除以 256 得到真实像素距离。 - 符号距离的复用价值:
pointPolygonTest的正负号可直接当作内外掩码使用;measureDist=false时是更廉价的纯判定模式。 - 形状匹配的不变性前提:
matchShapes依赖 Hu 矩的平移/缩放/旋转不变性,适合比较“形状”而非“位置”;若两图形形状本身差异大,得分会明显偏大。 - OpenCV.js 内存管理:所有示例遵循“用完即
delete()”的原则,Mat、MatVector、hull、defect等对象都应显式释放,尤其在循环或连续帧处理场景中。
六、延伸阅读
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 StartedRust0629
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

