ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenCV三维视觉实战:solvePnP与intersectConvexConvex实现工业缺陷定位

OpenCV三维视觉实战:solvePnP与intersectConvexConvex实现工业缺陷定位 1. 从一个“抓缺陷”的需求说起工业质检这行干久了你会发现一个规律越是看起来简单的需求背后藏的坑越多。前阵子接了个小项目需求方给的原话是“帮我把产品上那三个缺陷抓出来”。听起来像是图像处理入门题实际上手才知道这三个缺陷分布在不同的空间位置上有平面内的也有涉及深度信息的单靠传统的阈值分割和轮廓检测根本搞不定。这个项目的核心思路其实就藏在几个关键词里select3dobj、solvePnP、intersectConvexConvex、image2plane。这几个函数名一摆出来做过视觉定位和三维重建的朋友应该就有感觉了——这不是单纯的二维图像处理而是一套“二维检测加三维定位”的组合拳。简单说就是先在图像平面上把疑似缺陷的区域找出来再通过相机标定和位姿估计把这些区域映射到真实的三维空间里最后用几何求交的方式判断缺陷是否落在目标物体的有效表面上。这套方案能解决什么问题最典型的场景就是产品在传送带上姿态不固定相机拍到的图像里缺陷可能出现在任何角度和位置你没法用固定的ROI去框它。这时候就需要先识别出物体的三维姿态再把缺陷位置转换到物体自身的坐标系里才能准确判断“这个缺陷到底在不在产品上、在产品的哪个部位”。适合谁来参考如果你已经写过一些OpenCV的基础代码做过轮廓检测、模板匹配这类二维操作现在想往三维视觉方向迈一步那这个案例会是一个很好的练手项目。它不需要昂贵的深度相机用普通的工业相机加一个标定板就能跑起来成本可控效果也够用。2. 整体方案设计与思路拆解2.1 为什么不能只用二维方法先说说为什么不能偷懒只用二维方法。假设产品是一个长方体零件相机从斜上方拍摄。在图像里这个零件的上表面是一个平行四边形侧面是另一个平行四边形。如果缺陷出现在上表面靠近边缘的位置在二维图像里它可能和侧面的某个区域重叠。你用二维的轮廓分析根本分不清这个缺陷到底属于上表面还是侧面。更麻烦的是如果产品在传送带上旋转了一个角度同一个缺陷在图像里的位置和形状都会变。你没法用“缺陷距离左边缘多少像素”这种规则来判断因为左边缘本身在图像里的位置就在变。所以必须引入三维信息。核心逻辑是先把物体在三维空间中的姿态算出来然后把图像上的缺陷点反投影到物体自身的坐标系里在物体坐标系里做判断。这样不管物体怎么转缺陷在物体上的相对位置是不变的判断规则也就稳定了。2.2 四个核心函数的角色分工这套方案里四个关键词对应的函数各司其职我画个表把它们的职责说清楚函数/概念角色输入输出select3dobj三维物体选择与建模物体的三维模型或特征点物体坐标系下的关键点集合solvePnP位姿估计三维点对应二维点相机内参旋转向量和平移向量intersectConvexConvex凸多边形求交两个凸多边形的顶点交集面积和交集多边形image2plane图像点到平面映射图像点平面方程相机参数平面上的三维点select3dobj这个函数在OpenCV的官方文档里其实不太常见它更多是出现在一些三维物体识别和抓取的示例中。它的作用是定义一个三维物体的关键点集合比如一个长方体的八个顶点或者一个零件的几个特征角点。这些点构成了物体自身的坐标系后续所有计算都围绕这个坐标系展开。solvePnP是OpenCV里非常成熟的一个函数全称是Perspective-n-Point。它的作用是已知一组三维点和它们在图像上的二维投影点以及相机的内参矩阵和畸变系数求解出相机相对于物体的旋转和平移。说白了就是回答“相机在物体的哪个方位、朝哪个方向看”这个问题。intersectConvexConvex是一个几何工具函数用来计算两个凸多边形的交集。在这个项目里它的作用是把缺陷区域投影到物体表面后和物体表面的有效区域做求交判断缺陷是否真的落在物体上以及落在哪个面上。image2plane是一个概念性的操作OpenCV里没有直接叫这个名字的函数但可以通过cv::projectPoints的逆运算或者射线与平面求交来实现。它的作用是把图像上的一个像素点沿着相机光心的射线方向投射到三维空间中的一个平面上得到该平面上的三维坐标。2.3 方案的整体流程整个方案的流程可以分成两大阶段离线标定阶段和在线检测阶段。离线阶段要做的事情相机标定拿到内参矩阵和畸变系数定义物体的三维模型确定关键点的三维坐标确定物体所在平面的方程这个平面通常是传送带表面或者工装夹具的定位面。在线阶段采集图像预处理检测缺陷候选区域提取物体上的特征点和三维模型点做匹配用solvePnP求解位姿把缺陷区域反投影到物体表面用intersectConvexConvex判断缺陷归属输出结果。这个流程的好处是离线阶段的工作一次做完后续每次检测只需要跑在线部分的计算速度很快。而且位姿估计用的是特征点匹配对光照变化和部分遮挡有一定的鲁棒性。3. 核心细节解析与实操要点3.1 相机标定一切精度的基础相机标定这一步很多人觉得麻烦就想跳过或者随便用个默认参数。我踩过的坑告诉你标定不准后面全白搭。solvePnP对相机内参非常敏感焦距差个几十像素位姿估计的误差可能就有几毫米。标定板的选择上棋盘格是最常用的。我一般用9x6的棋盘格方格边长根据视野大小来定通常选20mm到50mm。标定的时候标定板要覆盖整个视野而且要出现在不同的角度和位置至少拍15到20张。注意标定板不要只在一个平面上平移要有倾斜和旋转这样标定出来的畸变系数才准。标定完成后你会得到内参矩阵K和畸变系数D。K的形式是K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fx和fy是焦距的像素表示cx和cy是主点坐标。这些值在后续的solvePnP和反投影中都要用到。注意标定完成后建议用标定板上的点做一次重投影验证看看重投影误差是否在0.5像素以内。如果误差太大说明标定过程有问题需要重新拍图。3.2 三维物体建模与关键点选择select3dobj的核心是定义物体的三维关键点。以长方体零件为例我通常选八个顶点作为关键点。但实际操作中八个顶点不一定都能在图像里看到所以需要选那些在大多数姿态下都可见的点。我的经验是选物体的角点或者边缘交点这些点在图像里容易通过角点检测或者直线求交找到。关键点的三维坐标要精确测量可以用游标卡尺量也可以从CAD模型里导出。坐标系的定义要统一一般把物体底面中心设为原点Z轴垂直于底面向上。关键点的数量上solvePnP至少需要4个点才能求解但实际用6到8个点更稳。点太少噪声影响大点太多匹配错误的概率增加。我一般用6个点分布在物体的不同面上保证在常见姿态下至少能看到4个。3.3 缺陷检测与候选区域提取缺陷检测这部分取决于缺陷的类型。常见的缺陷有划痕、凹坑、污点、裂纹等。对于划痕和裂纹可以用Canny边缘检测加形态学操作对于凹坑和污点可以用局部阈值分割或者背景差分。我一般先用高斯滤波平滑图像然后用自适应阈值或者Otsu阈值分割得到二值图再用形态学开闭运算去掉噪点最后用findContours提取轮廓。每个轮廓就是一个缺陷候选区域。这里有个细节缺陷候选区域的轮廓要转成凸多边形因为intersectConvexConvex只处理凸多边形。如果轮廓是凹的可以用convexHull求凸包。虽然这样会稍微扩大缺陷区域但在判断归属时影响不大因为后续还有面积比例的判断。提示缺陷检测的阈值不要设得太死建议用自适应阈值或者基于统计的方法。我试过固定阈值换一批产品就得重新调很麻烦。3.4 位姿估计的实操细节solvePnP的调用看起来简单但参数设置有很多讲究。函数原型是bool solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess false, int flags SOLVEPNP_ITERATIVE);objectPoints是三维点imagePoints是对应的二维点。flags的选择很关键SOLVEPNP_ITERATIVE适合点数较少且初始值已知的情况SOLVEPNP_EPNP适合点数较多的情况SOLVEPNP_IPPE适合平面物体。对于我们的长方体零件如果关键点都在同一个平面上用SOLVEPNP_IPPE效果最好。求解出来的rvec是旋转向量需要用Rodrigues变换转成旋转矩阵R。tvec是平移向量。有了R和t就可以把物体坐标系下的点变换到相机坐标系P_cam R * P_obj t再通过相机内参投影到图像p_img K * P_cam这个变换链是后续反投影的基础。3.5 图像点到平面的反投影image2plane这个操作本质上是射线与平面求交。给定图像上的一个像素点(u, v)先通过内参矩阵的逆变换得到归一化平面上的点x_norm (u - cx) / fx y_norm (v - cy) / fy然后构造相机坐标系下的射线方向d_cam (x_norm, y_norm, 1)如果平面在相机坐标系下的方程是n·P d那么射线与平面的交点就是t d / (n · d_cam) P_cam t * d_cam再通过R和t的逆变换把P_cam转回物体坐标系P_obj R^T * (P_cam - t)这样就得到了缺陷点在物体坐标系下的三维坐标。注意平面方程n和d的确定可以通过已知的平面上的三个点来求。这三个点可以是物体底面的三个角点在物体坐标系下坐标已知通过位姿变换到相机坐标系后用叉积求法向量。4. 实操过程与核心环节实现4.1 环境搭建与依赖安装这个项目我是在Ubuntu 20.04上做的OpenCV用的是4.5.4版本。安装OpenCV的方式有很多我推荐从源码编译虽然慢一点但可控性强而且能确保contrib模块可用。编译OpenCV的基本步骤sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev git clone https://github.com/opencv/opencv.git git clone https://github.com/opencv/opencv_contrib.git cd opencv mkdir build cd build cmake -D CMAKE_BUILD_TYPERELEASE \ -D CMAKE_INSTALL_PREFIX/usr/local \ -D OPENCV_EXTRA_MODULES_PATH../../opencv_contrib/modules \ -D WITH_CUDAOFF \ -D BUILD_EXAMPLESON .. make -j$(nproc) sudo make install编译完成后用pkg-config --modversion opencv4验证版本。如果是在Windows上用VS2022建议下载预编译的库注意选择和你VS版本匹配的vc16或vc17。提示如果你在Python环境下遇到ModuleNotFoundError: No module named opencv说明Python绑定没装好。可以用pip install opencv-python opencv-contrib-python快速安装但注意这种方式装的OpenCV可能缺少某些contrib模块。4.2 标定数据的采集与处理标定这一步我写了一个小工具用摄像头实时采集棋盘格图像按空格键保存。采集的时候注意标定板要平整不能有弯曲光照要均匀避免反光和阴影标定板在图像中的大小要适中太小了角点检测不准至少采集15张覆盖不同的角度和位置采集完成后用cv::findChessboardCorners检测角点用cv::cornerSubPix做亚像素优化然后用cv::calibrateCamera计算内参和畸变系数。std::vectorstd::vectorcv::Point3f objectPoints; std::vectorstd::vectorcv::Point2f imagePoints; // 填充objectPoints和imagePoints cv::Mat K, D; std::vectorcv::Mat rvecs, tvecs; cv::calibrateCamera(objectPoints, imagePoints, imageSize, K, D, rvecs, tvecs);标定完成后把K和D保存到文件后续直接读取使用。4.3 物体关键点的三维坐标定义以我做的这个零件为例它是一个100mm x 60mm x 30mm的长方体。我选了6个关键点关键点编号物体坐标系下的坐标 (mm)说明P0(0, 0, 0)底面左前角P1(100, 0, 0)底面右前角P2(100, 60, 0)底面右后角P3(0, 60, 0)底面左后角P4(0, 0, 30)顶面左前角P5(100, 0, 30)顶面右前角这6个点在图像里比较容易通过角点检测找到。实际使用时根据零件的姿态可能只能看到其中4个solvePnP用4个点也能求解但精度会差一些。4.4 图像采集与缺陷检测图像采集用工业相机分辨率1280x960帧率30fps。采集到的图像先做灰度化然后用高斯滤波去噪窗口大小5x5。缺陷检测的代码框架cv::Mat gray, binary; cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0); cv::adaptiveThreshold(gray, binary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 11, 2); cv::Mat kernel cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3)); cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel); cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, kernel); std::vectorstd::vectorcv::Point contours; cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);对于每个轮廓计算面积过滤掉太小的噪点。然后求凸包得到凸多边形。4.5 位姿求解与缺陷反投影位姿求解的关键是找到图像上的关键点和三维模型点的对应关系。我用的方法是先用模板匹配或者角点检测找到物体的大致位置然后在预期位置附近搜索角点。匹配的时候用最近邻加RANSAC剔除误匹配。cv::Mat rvec, tvec; cv::solvePnPRansac(objectPoints, imagePoints, K, D, rvec, tvec, false, 100, 8.0, 0.99, inliers, cv::SOLVEPNP_ITERATIVE);得到位姿后把缺陷轮廓的每个点反投影到物体表面。这里要注意缺陷可能出现在物体的不同面上所以需要先判断缺陷在哪个面上。我的做法是把缺陷轮廓的中心点反投影到物体坐标系看它的Z坐标接近0还是30接近0就在底面接近30就在顶面。反投影的代码cv::Mat R; cv::Rodrigues(rvec, R); cv::Mat R_inv R.t(); cv::Mat t_inv -R_inv * tvec; // 对于图像上的点(u, v) double x_norm (u - K.atdouble(0,2)) / K.atdouble(0,0); double y_norm (v - K.atdouble(1,2)) / K.atdouble(1,1); cv::Mat d_cam (cv::Mat_double(3,1) x_norm, y_norm, 1.0); // 平面方程 n·P d这里以顶面为例n(0,0,1)d30 // 需要把平面方程转到相机坐标系 cv::Mat n_obj (cv::Mat_double(3,1) 0, 0, 1); cv::Mat n_cam R * n_obj; double d_cam_plane 30 - n_cam.dot(tvec); double t_scale d_cam_plane / n_cam.dot(d_cam); cv::Mat P_cam t_scale * d_cam; cv::Mat P_obj R_inv * (P_cam - tvec);4.6 缺陷归属判断与intersectConvexConvex的应用得到缺陷在物体坐标系下的多边形后用intersectConvexConvex和物体表面的有效区域求交。有效区域就是物体表面的边界比如顶面的有效区域是(0,0,30)到(100,60,30)的矩形。std::vectorcv::Point2f defectPoly, surfacePoly, intersection; // 填充defectPoly和surfacePoly double area cv::intersectConvexConvex(defectPoly, surfacePoly, intersection); double ratio area / cv::contourArea(defectPoly); if (ratio 0.8) { // 缺陷主要落在物体表面上 }ratio大于0.8说明缺陷的80%以上面积在物体表面上可以判定为有效缺陷。如果ratio小于0.5可能是误检或者缺陷在背景上。5. 常见问题与排查技巧实录5.1 位姿估计不准的排查思路位姿估计不准是最常见的问题表现是反投影后的缺陷位置偏移很大。排查的时候按以下顺序检查问题现象可能原因排查方法位姿抖动大关键点匹配不稳定检查角点检测的重复性增加RANSAC迭代次数反投影偏移相机内参不准重新标定检查重投影误差特定角度误差大关键点共面增加非共面的关键点整体偏移物体坐标系定义错误检查三维点的坐标定义我遇到过一次位姿估计在物体正对相机时很准但倾斜角度大了就偏。后来发现是标定的时候标定板没有倾斜畸变系数没标出来。重新标定后问题解决。5.2 缺陷检测的误检和漏检误检和漏检是缺陷检测的永恒话题。我的经验是误检多降低阈值分割的灵敏度增加形态学操作的强度或者用面积过滤漏检多提高阈值分割的灵敏度减小形态学操作的强度或者用多尺度检测边缘缺陷漏检边缘处的缺陷容易被形态学操作腐蚀掉可以先用边缘检测单独处理边缘区域提示不要追求100%的检出率工业场景下95%以上的检出率加上人工复检通常是可以接受的。追求过高的检出率会导致误检率飙升反而增加人工负担。5.3 intersectConvexConvex的坑intersectConvexConvex这个函数有个坑它要求输入的两个多边形都是凸的而且顶点顺序要一致都是顺时针或都是逆时针。如果顶点顺序不一致返回的交集可能是空的或者错误的。我的做法是在调用之前先用cv::convexHull求凸包然后用cv::contourArea的符号判断顶点顺序如果是逆时针就反转。另外这个函数返回的是交集面积但交集多边形的顶点顺序不保证。如果需要后续处理交集多边形要重新排序。5.4 性能优化的一点经验这套流程如果全部用C实现在1280x960的图像上单帧处理时间大约在30到50毫秒可以做到实时。如果是在树莓派上跑建议降低图像分辨率到640x480并且把缺陷检测和位姿估计分开位姿估计不需要每帧都做可以每隔几帧做一次。Python环境下OpenCV的Python绑定性能比C差不少但用numpy做向量化操作可以弥补。关键是要避免在Python循环里逐像素操作尽量用OpenCV的函数。6. 几个容易被忽略的细节6.1 镜头畸变对反投影的影响前面说的反投影公式是假设没有畸变的。实际上镜头畸变会导致图像上的点偏移尤其是边缘区域。如果缺陷出现在图像边缘不校正畸变的话反投影误差可能有几个像素。校正的方法是先用cv::undistortPoints把图像点去畸变然后再做反投影。这个步骤很多人会忽略但在高精度场景下必须做。6.2 平面方程的精确确定image2plane里的平面方程如果直接用物体底面的理论方程可能会有误差。因为物体放在传送带上传送带本身可能不平或者物体和传送带之间有间隙。我的做法是用物体底面的四个角点通过位姿变换到相机坐标系然后用最小二乘拟合平面方程。这样得到的平面方程更准确。6.3 多缺陷的排序和输出如果一帧图像里检测到多个缺陷需要按一定的规则排序输出。我一般按缺陷面积从大到小排序或者按缺陷在物体坐标系下的位置排序。输出的时候除了缺陷的位置和面积还会输出缺陷所属的面顶面、底面、侧面方便后续处理。6.4 标定和检测的坐标系一致性标定的时候棋盘格所在的坐标系和检测时物体所在的坐标系要统一。我一般把棋盘格放在传送带上标定完成后传送带表面就是Z0平面。这样物体放在传送带上底面的Z坐标就是0省去了坐标变换的麻烦。7. 从二维到三维的思维转变这个项目做下来最大的收获不是某个函数的用法而是思维方式的转变。二维图像处理你看到的是像素和轮廓三维视觉你看到的是空间中的点和面。同一个缺陷在二维图像里可能只是一个不规则的斑块但在三维空间里它是一个有位置、有方向、有面积的曲面片。select3dobj、solvePnP、intersectConvexConvex、image2plane这几个函数单独看都不复杂但组合起来就能解决实际问题。关键是要理解每个函数在流程中的位置和作用知道什么时候该用哪个函数参数怎么调误差怎么控制。如果你也在做类似的项目我的建议是先把标定做扎实这是所有精度的基础然后从简单的平面物体开始跑通整个流程最后再处理复杂的三维物体和多面缺陷。不要一上来就追求完美先让流程跑起来再逐步优化。最后分享一个小技巧调试的时候把反投影后的三维点再投影回图像和原始缺陷轮廓叠加显示可以直观地看到误差有多大。这个可视化方法帮我省了很多调试时间。
返回列表