1. 从"抓出三个缺陷"说起:这个案例到底在做什么
第一次看到"抓出三个缺陷"这个标题,很多人会以为是某种质检流水线的项目,其实它讲的是 OpenCV 里一个非常经典、但平时很少有人系统讲清楚的能力组合:用单目相机、已知几何关系的物体,反推出物体在三维空间中的位姿,再结合轮廓相交判断,把画面里"不该出现的东西"标记出来。关键词里那几个函数——select3dobj、solvePnP、intersectConvexConvex、image2plane——基本就把这条技术链路串起来了。
我先把这条链路用大白话讲一遍,方便不同基础的读者都能跟上。假设你面前有一块平板或者一个规则零件,相机拍了一张图。你知道这个物体真实的物理尺寸(比如边长、孔位坐标),也知道相机的大致内参。那么只要在图像里找到物体上几个已知点的像素坐标,就能用solvePnP反推出物体相对于相机的位置和朝向,也就是所谓的位姿(pose)。有了位姿,就能把物体三维模型上的点投影回图像平面,这个过程在示例里对应image2plane这类投影换算。投影之后,你就能拿"理论上应该出现的轮廓"和"图像里实际检测到的轮廓"做比对,用intersectConvexConvex算它们的相交面积,交并比一低,就说明这块区域有异常——可能是缺件、多件、错位、破损,这就是"缺陷"。
select3dobj在这个流程里扮演的是"选点/选目标"的角色,它负责从三维对象上挑出用于求解位姿的对应点,或者从候选里筛出符合几何约束的那一组。很多人卡住的地方不是不会调solvePnP,而是不知道该喂给它哪些点、这些点怎么和图像点一一对应,select3dobj解决的正是这个"配对"问题。
所以这个案例的定位很明确:它不是教你训练一个深度学习缺陷检测模型,而是教你用几何 + 投影 + 轮廓运算这套传统视觉方法,在结构已知、光照可控的场景下,快速搭出一个能定位异常区域的检测流程。适合谁看?适合已经会读写图像、会找轮廓,但一碰到"三维位姿""投影比对"就发怵的工程师;也适合做工业质检、尺寸测量、装配校验方向,想用轻量方案替代重型模型的朋友。它的最大好处是可解释、可复现、对样本量要求低,缺点是依赖标定精度和几何先验,场景一变就得重新配参数。
下面我不按"函数手册"的方式讲,而是按我实际搭这套流程的顺序,把每一步为什么这么做、哪里最容易翻车,掰开揉碎说清楚。
2. 位姿求解这条主线:solvePnP 到底在算什么
2.1 为什么单目相机也能算出三维位置
先破一个常见误解:单张图没有深度信息,凭什么能算出物体离相机多远?答案是靠已知尺寸做尺度约束。如果你知道物体上两个点的真实距离是 50mm,又在图像里量出它们相隔 200 像素,那么结合相机焦距,就能反推出这段距离对应的深度。solvePnP的本质就是解这样一个方程组:给定若干组"三维世界点 ↔ 二维图像点"的对应关系,求出一个旋转向量和一个平移向量,使得三维点经过这个变换再投影后,尽量贴合观测到的二维点。
数学上它解的是透视投影模型下的最小化重投影误差问题。你不需要手推公式,但必须理解它的输入输出:
- 输入:
objectPoints(物体坐标系下的三维点,单位通常是毫米)、imagePoints(对应的像素坐标)、相机内参矩阵cameraMatrix、畸变系数distCoeffs。 - 输出:
rvec(旋转向量,可用Rodrigues转成旋转矩阵)、tvec(平移向量,即物体原点在相机坐标系下的位置)。
这里有个关键点很多人忽略:objectPoints的坐标系是你自己定义的。你可以把物体某个角点定为原点,也可以把中心定为原点,只要所有三维点和图像点严格对应同一套定义就行。定义混乱是后面投影对不上的头号原因。
2.2 点数够不够、分布好不好,直接决定成败
solvePnP最少需要几个点?理论上 4 个不共面的点就能解,但实际工程里我强烈建议至少 6 个,且尽量分散在物体不同区域。原因很简单:如果所有点都挤在一个小角落,方程对旋转的约束很弱,解出来的位姿在另一个方向上会剧烈抖动,投影自然就飘。
我踩过的一个典型坑:拿一块矩形板,只用了四个角点。结果板子稍微倾斜,rvec就跳得厉害,投影轮廓和实际轮廓能差十几个像素。后来改成"四角 + 中心孔位 + 一条边中点"共 7 个点,稳定性立刻上来了。这就是select3dobj存在的意义——它不是随便选点,而是按几何分布去挑一组良态的点。你可以把它理解成一个"选点策略器":优先选那些在物体上跨度大、不共线、不共面的特征点。
提示:判断点分布好不好,有个土办法——把这些三维点投影到图像上,看它们在画面里是不是铺得够开。如果全挤在一小块,果断换点。
2.3 内参和畸变:最容易被"差不多"害死的地方
solvePnP的精度对内参极其敏感。我见过太多人直接拿一个"大概"的焦距就上,结果位姿怎么调都不对,最后怀疑是算法问题,其实是内参错了。相机内参矩阵长这样:
fx 0 cx 0 fy cy 0 0 1fx、fy是焦距(像素单位),cx、cy是主点。如果你没有标定板,至少要做一次棋盘格标定,把fx、fy、cx、cy和畸变系数k1, k2, p1, p2, k3都标出来。畸变系数尤其重要,广角镜头边缘的桶形畸变会让图像点整体偏移,solvePnP会把这个偏移误当成位姿误差去"补偿",结果就是解出来的姿态是歪的。
实操建议:标定完把重投影误差看一眼,正常应该在 0.5 像素以内。如果超过 1 像素,要么标定板拍得不够多角度,要么图像本身模糊,别急着往下做。
3. 投影回图像:image2plane 与轮廓比对的衔接
3.1 从三维位姿到二维轮廓的完整换算
拿到rvec、tvec之后,把物体模型上的点投影回图像,走的是标准流程:先用旋转矩阵和平移向量把三维点变换到相机坐标系,再用内参矩阵投影到归一化平面,最后乘焦距加主点得到像素坐标。OpenCV 里可以直接用projectPoints一步到位,示例里image2plane这类函数做的就是这件事的封装或变体。
这一步的产出是理论轮廓:如果物体在位姿正确、没有缺陷,那么它应该出现在图像里的边界。接下来你要拿它和实际轮廓比对。实际轮廓怎么来?常见做法是先做灰度化、滤波、二值化(threshold或adaptiveThreshold),再findContours提取。这里有个顺序问题:先投影还是先提轮廓?我的经验是先把实际轮廓提出来,再投影理论轮廓,因为投影依赖位姿,而位姿依赖图像点,图像点又来自实际特征,逻辑上是"先看现实,再算理论"。
3.2 轮廓比对为什么用 intersectConvexConvex
intersectConvexConvex算的是两个凸多边形的交集面积。为什么强调凸?因为凸多边形的相交计算有成熟且高效的算法,结果稳定。如果你的实际轮廓是凹的(比如带缺口的零件),直接丢进去会出错或者结果不可信。解决办法是先做凸包(convexHull),或者把凹轮廓拆成若干凸块分别比对。
比对的核心指标是交并比(IoU):
IoU = 交集面积 / (理论面积 + 实际面积 - 交集面积)IoU 接近 1,说明理论轮廓和实际轮廓高度重合,这块区域正常;IoU 明显偏低,说明实际轮廓偏离了理论位置,或者面积对不上——缺了一块、多了一块、整体移位,都属于"缺陷"。
这里要提醒一个坑:轮廓的坐标系必须统一。findContours出来的点是图像坐标,projectPoints出来的也是图像坐标,看似一致,但如果中间做过 ROI 裁剪、缩放、翻转,坐标系就错位了。我建议在比对前把两套点都打印一下包围盒,肉眼确认范围对得上,再往下算。
3.3 三个缺陷是怎么被"抓"出来的
回到标题的"三个缺陷"。在典型实现里,流程会维护一个"应该正常"的基准,然后对画面里的多个候选区域逐一比对,凡是 IoU 低于阈值的就记为异常。三个缺陷通常对应三类偏差:
| 缺陷类型 | 表现 | 判定依据 |
|---|---|---|
| 位置偏移 | 实际轮廓整体平移 | 交集面积骤降但形状相似 |
| 尺寸异常 | 实际轮廓偏大或偏小 | 面积比偏离 1 较多 |
| 形状缺失 | 局部轮廓凹陷或断裂 | 凸包与原始轮廓差异大 |
我实际调的时候发现,阈值不能一刀切。位置偏移对 IoU 很敏感,稍微移几个像素 IoU 就掉;而尺寸异常可能 IoU 还很高但面积比已经不对。所以我一般同时看两个量:IoU 和面积比,两个都正常才算通过。这样能有效降低误报。
4. 参数调优与常见翻车现场
4.1 二值化阈值:缺陷检测的隐形开关
整套流程里,最影响结果的往往不是solvePnP,而是二值化阈值。阈值高一点,轮廓瘦一圈;低一点,轮廓胖一圈。轮廓一胖一瘦,IoU 就跟着变,缺陷判定就跟着抖。我的做法是:先用 Otsu 自动阈值跑一遍看整体效果,再针对具体场景手动微调,并且把阈值和 IoU 阈值绑定着调——因为两者是耦合的,单独调一个没有意义。
光照不均的场景,全局阈值基本没戏,得上adaptiveThreshold。但自适应阈值会产生很多噪点小轮廓,记得用面积过滤(contourArea)把小于某个像素数的轮廓扔掉,否则一堆碎点会干扰比对。
4.2 位姿抖动:为什么同一物体两次结果不一样
如果你发现同一物体、同一位置,连续两帧解出来的tvec差了好几毫米,别急着怀疑相机。常见原因有三个:
- 图像点检测不稳定。角点检测受噪声影响,像素级抖动会放大到位姿上。
- 点太少或分布太集中。前面说过,约束不足。
- 用了
SOLVEPNP_ITERATIVE但初值不好。可以换SOLVEPNP_EPNP或SOLVEPNP_SQPNP试试,后者对平面物体更稳。
我的经验是:对平面物体优先用SOLVEPNP_IPPE系列,它专门针对平面场景做了优化,比通用迭代法稳得多。这个细节很多教程不讲,但实测差别很明显。
4.3 轮廓相交的边界情况
intersectConvexConvex在两个多边形完全不相交时返回的相交面积为 0,这没问题;但如果两个多边形几乎重合,浮点误差可能让结果出现极小负值或超出理论面积的值。稳妥做法是对结果做一次 clamp,把面积限制在[0, min(areaA, areaB)]区间内。另外,输入点最好是CV_32F类型且顺序一致(都顺时针或都逆时针),顺序乱了交集算出来是错的。
注意:如果你的轮廓点数特别多(几千个点),先做
approxPolyDP简化,既提速又降噪,对 IoU 影响很小。
5. 把这套流程落到工程里的几点心得
5.1 标定一次不够,要定期复核
相机摔过、镜头拧过、对焦变过,内参就可能变。我一般会在项目里留一个"标定自检"入口:拿一块已知尺寸的标定板,跑一遍solvePnP,看重投影误差是否还在阈值内。超过就重新标定。这个习惯帮我避免过好几次"算法突然不准"的假故障。
5.2 把"正常样本"的 IoU 分布先统计出来
不要拍脑袋定 IoU 阈值。正确做法是:先拿一批确认正常的样本跑一遍,统计 IoU 的均值和标准差,阈值定在均值减 3 倍标准差附近。这样阈值是有数据支撑的,而不是"我觉得 0.8 差不多"。缺陷样本本来就少,用正常样本的分布来定边界,是最省事也最靠谱的办法。
5.3 缺陷定位要能"指出来",而不只是"报个数"
工程上,光告诉用户"有缺陷"没用,得把缺陷区域框出来。我的做法是把理论轮廓和实际轮廓的差集区域单独提取出来,画成高亮掩膜叠加在原图上。这样操作员一眼就能看到"哪里不对",而不是对着一串 IoU 数字发呆。这个可视化步骤在调试阶段也极其有用,能帮你快速判断是位姿错了还是轮廓提取错了。
5.4 什么时候该放弃传统方法
说句实在话,这套几何方法有明确的适用边界:物体结构已知、刚性好、光照可控、背景干净,它又快又准。但如果是柔性物体、表面纹理复杂、缺陷形态千变万化,硬套这套流程会调到你怀疑人生。这时候该上学习类方法就上,别跟几何死磕。我个人的判断标准是:如果正常样本的 IoU 分布都收不紧,说明几何先验不成立,趁早换路线。
最后分享一个我调试时常用的小技巧:把rvec、tvec、IoU、面积比这几个量实时打印在画面上,边移动物体边观察数值变化。你会对"哪个参数在什么情况下敏感"建立起非常直观的感觉,这比看任何文档都快。等这套感觉有了,再回头调阈值,基本就是几分钟的事。