ARTICLE DETAIL

资讯详情

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

基于OpenCV的单目视觉测量:从相机标定到毫米级尺寸

基于OpenCV的单目视觉测量:从相机标定到毫米级尺寸 简介这是一份基于OpenCV的单目视觉测量系统研究的学术论文文档源自《计算机测量与控制》2015年刊作者来自华中科技大学武汉光电国家实验室。内容面向机器视觉、工业检测方向的科研人员与工程师系统阐述了如何利用开源库OpenCV搭配CCD工业相机、650nm红光一字线激光器与滤光片等硬件实现工件宽度的动态实时非接触测量。\n\n资源为1个docx格式的文献全文压缩包大小仅21KB便于阅读与打印。其中详细描述了系统软件设计流程包括Gige工业相机IP配置与初始化、MVImage至Iplimage图像类型转换、像素当量标定以及二值化、感兴趣区域截取、轮廓提取等关键图像处理步骤并给出实验数据单次测量约100ms最大相对误差不超过1.5%。\n\n目前已有152人学习下载。该文档不仅展示了OpenCV在激光加工、焊缝跟踪等场景下的工程落地方法也提供了完整的单目结构光视觉测量系统设计思路与实验验证过程对从事机器视觉课题研究或相关项目开发的读者具有直接参考价值。1. 单目测量不是“猜尺寸”一个摄像头也能测出毫米级尺寸基于OpenCV的单目视觉测量系统核心思路是用一个普通摄像头在没有深度信息的条件下把像素尺寸还原成毫米级物理尺寸。第一次接触这个方向的人都会想单目没有深度凭什么测绝对值答案是靠已知几何约束比如被测物贴着一个固定平面或者物体上有已知尺寸的特征点。这套方案在产线零件尺寸检测、机器人抓取定位、高校视觉课题里都很常见成本低、部署快。但它不是拿OpenCV读一张图、量几个像素就完事的图像处理项目真正决定测量精度的是标定、平面假设和边缘提取三步。下面按我做这类系统的顺序把参数、代码和容易翻车的地方讲清楚。2. 测量系统第一步相机标定与畸变校正参数怎么设单目测量的第一个环节永远不是测量算法本身而是先把镜头和感光芯片的投影关系定下来。普通镜头存在径向畸变和切向畸变画面边缘越靠近直线会弯、会拉伸如果不管畸变直接量像素同一把卡尺放在画面中心和放在画面边缘量出来的毫米数会不一样。这就是为什么任何单目视觉测量系统都要先做相机标定。常见做法是张正友棋盘格标定法OpenCV里对应的接口是cv2.calibrateCamera整个流程看三步准备标定板、拍图、算内参。2.1 用 cv2.calibrateCamera 跑通棋盘格标定的最小流程标定板可以买玻璃基板的也可以自己打印后贴到玻璃或铝板上方格边长用游标卡尺量准。下面是最小可用流程不需要额外依赖。import cv2 import numpy as np import glob # 棋盘格内角点数7列 x 6行注意不是方格数量 CHECKERBOARD (7, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 角点的物理坐标单位先按1格处理 square_size 20.0 # 实测方格边长单位mm objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp[:, :2] * square_size # 乘上实际边长标定结果才有mm单位 obj_points [] # 每张图对应的3D坐标 img_points [] # 每张图对应的像素坐标 # calib_imgs/ 下放15~20张不同角度的标定板照片 for fname in sorted(glob.glob(calib_imgs/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) np.savez(calib_params.npz, mtxmtx, distdist) print(重投影误差:, ret) print(相机矩阵:\n, mtx) print(畸变系数:, dist.ravel())这段代码里最容易写错的是gray.shape[::-1]。gray.shape返回(高, 宽)而calibrateCamera要求的是(宽, 高)反了之后内参会静默出错后续所有像素坐标换算都会偏。另外cornerSubPix用的是(11, 11)搜索窗口角点定位能到亚像素级别比直接拿findChessboardCorners的原始输出做标定稳定得多。dist输出是5个畸变系数顺序是k1, k2, p1, p2, k3。如果用到了鱼眼镜头那是另一套fisheye模型普通测量场景用上面的针孔模型就够了。标定板每格的物理单位这里乘了square_size如果后面要做solvePnP算位姿这一步必须乘否则输出的平移向量是“格子”单位而不是毫米。2.2 标定质量的验收指标重投影误差、焦距与主点标定不是跑通就完了要会看结果。calibrateCamera返回的ret就是重投影误差单位是像素。我的习惯是小于0.1px说明标定板角点检测和相机模型吻合得很好0.1到0.3px在产线场景可以接受超过0.5px就要回头查原因而不是继续往下做。# 用标定结果回投所有标定板角点逐图算平均误差 mean_error 0 for i in range(len(obj_points)): imgpoints2, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(img_points[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error error print(mean reprojection error:, mean_error / len(obj_points))除了重投影误差还要看相机矩阵mtx里的fx和fy。绝大多数工业相机的像素是正方形的fx和fy应该非常接近如果差很多要么是CMOS像素不是正方形要么是标定板照片方向覆盖不够。主点cx, cy正常情况应该在图像中心附近比如1280x720分辨率下cx应该接近640。如果cx偏到图像边缘多半是标定板照片全是居中摆放、没有覆盖画面四角导致的。标定完成后测量前要对图像做畸变校正。最省事的写法是cv2.undistort但注意alpha参数h, w gray.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst cv2.undistort(img, mtx, dist, None, newcameramtx)alpha1时保留全部像素画面边缘会带黑边alpha0会自动裁剪掉畸变拉伸出来的空白区域。测量场景我一般用alpha1因为裁剪后的图像尺寸和内参不再直接对应容易引入新的换算错误。2.3 标定现场的三个物理条件平整度、光照、拍摄张数标定算法本身是成熟的黑匣子真正的坑在物理条件上。第一个是标定板平整度。打印纸直接贴墙上或者桌面纸面本身不平标定出来的畸变参数会把弯曲当成镜头畸变一起拟合进去重投影误差可能依然很低但实际测量就是偏。常见做法是夹在两块玻璃板之间或者贴到铝板上。第二个是光照。标定板不能有高光反射棋盘格的黑格会反光角点检测的精度会明显下降。第三个是拍摄张数和角度分布。15到25张照片每张之间旋转和平移角度差10到20度画面四角都必须有棋盘格经过。如果只在一个位置前后移动拍20张内参里畸变参数会极度不稳定。还有一个容易被忽略的点标定完成后镜头焦距必须锁死一旦动了变焦环或者换了镜头整个内参作废必须重新标定。这也是HALCON和OpenCV的一个明显区别HALCON的标定和亚像素测量算子更省心但商业授权费用高OpenCV免费、流程完全透明适合自己掌控测量链路。3. 从像素到毫米单应矩阵与比例系数法的边界标定解决的是“把图像校正到理想针孔模型下”的问题之后要把像素转成毫米必须先问清楚被测物体是不是在一个固定的已知平面上答案不同做法完全不同。如果物体贴着一个固定平面用单应矩阵或比例系数法如果物体是三维结构或者相机要测位姿就得走到solvePnP那一步。3.1 光轴垂直于测量平面时的比例系数法最简单的方案是比例系数法。当相机光轴基本垂直于测量平面、被测物完全贴着这个平面时可以先在测量平面上放一个已知尺寸的参考物比如标准量块或标定板计算出每个像素对应的物理尺寸physical_mm 20.0 # 参考物的实际尺寸单位mm pixel_count 300.0 # 参考物在畸变校正后图像里的像素尺寸 scale physical_mm / pixel_count # 单位mm/pixel print(scale:, scale)scale的含义是每个像素代表多少毫米。之后测任意物体只需把像素尺寸乘以scale。这里有两个前提图像必须经过畸变校正否则画面边缘的像素尺寸和中心不一致参考物必须和被测物在同一个平面上高度差几毫米比例系数就不成立。比例系数法的优点是快不需要计算矩阵缺点是只适用于“相机正对平面”的场景。一旦相机倾斜画面不同位置的比例关系不一样同一个物体放在画面左边和右边量出来的结果可能差几个毫米。这种情况我一般不会用比例系数而是直接用单应矩阵。3.2 cv2.findHomography 建立像素到物理坐标的映射单应矩阵解决的是相机任意角度下一个平面上的点映射到图像平面的变换关系。常见做法是把标定板放在测量平面上提取角点像素坐标角点的物理坐标已知用cv2.findHomography求一个3x3的H矩阵之后这个平面上任意点的像素坐标都能换算成毫米坐标。import cv2 import numpy as np square_size 20.0 # 标定板方格边长单位mm # 标定板所有角点的物理坐标平面假设 z0 objp np.zeros((6 * 7, 2), np.float32) objp[:, :2] np.mgrid[0:7, 0:6].T.reshape(-1, 2) * square_size # 测量平面上拍摄的标定板图像提取角点像素坐标 ret, corners cv2.findChessboardCorners(gray, (7, 6), None) pix_pts corners.reshape(-1, 1, 2).astype(np.float32) obj_pts objp.reshape(-1, 1, 2).astype(np.float32) # H 将像素坐标映射到物理坐标注意 src/dst 顺序 H, _ cv2.findHomography(pix_pts, obj_pts) # 任意像素点换算成物理坐标单位mm test_pix np.array([[[320, 240]]], dtypenp.float32) test_mm cv2.perspectiveTransform(test_pix, H) print(物理坐标(mm):, test_mm.reshape(-1, 2))findHomography最少4个点就能算但这里用了42个角点冗余点多且默认会用最小二乘拟合个别角点检测偏了也不会太过影响结果。如果想要更强鲁棒性可以加上cv2.RANSAC参数它会自动剔除误匹配点。关键的坑是findHomography的参数顺序。srcPoints是图像上的点dstPoints是物理坐标点顺序反了H就变成了从毫米到像素的矩阵后面perspectiveTransform输出单位就乱了。perspectiveTransform要求输入是N×1×2或N×2的浮点数组输出结构和输入一致。这里objp的物理坐标也得是float32整数坐标会报错。如果你用的不是标定板而是直接在测量平面上放几个已知间距的标记点也可以用cv2.getPerspectiveTransform。它和findHomography的区别是前者恰好需要4组对应点、不做优化后者支持更多点并且能做RANSAC实际工程里优先用findHomography。3.3 什么时候不能用单应矩阵离面旋转与透视单应矩阵的适用条件是严格共面。被测物体必须贴在求解H时标定板所在的那个平面上物体稍微翘起来、垫高了几毫米像素坐标映射出来的物理坐标就会带系统误差。高度差越大误差越大这种误差无法通过后续算法补偿只能在硬件上保证被测平面固定。另一个边界是相机角度。单应矩阵本身可以处理相机倾斜拍摄但倾斜角度越大远处的像素分辨率越低同样的边缘提取误差放大的倍数越高。比如相机正对平面时1像素对应0.1mm倾斜40度后远处1像素可能对应0.3mm。所以我一般建议相机光轴和测量平面法线的夹角控制在20度以内另外搭配亮度均匀的光源避免倾斜造成反光和阴影。4. 位姿测量与 solvePnP不在同一平面时怎么办当被测物体是三维结构或者不仅要测尺寸、还要知道物体相对于相机的旋转和平移时前面两种方法就不够用了。这时需要用到cv2.solvePnP给定物体上一组已知3D坐标的特征点以及它们在当前图像中的2D像素坐标求解物体坐标系到相机坐标系的旋转和平移。4.1 solvePnP 的输入、输出和求解方法选择solvePnP的输入有四样3D特征点坐标、2D像素坐标、相机内参矩阵、畸变系数。前两个需要一一对应3D坐标的单位就是输出平移向量的单位所以标定时乘了square_size这里就非常关键。import cv2 import numpy as np # 被测量物体上已知的4个特征点单位mm object_points np.array([ [0, 0, 0], # 特征点0 [100, 0, 0], # 特征点1 [100, 80, 0], # 特征点2 [0, 80, 0], # 特征点3 ], dtypenp.float32) # 这4个点在当前图像中的像素坐标顺序必须和上面一一对应 image_points np.array([ [528, 352], [620, 360], [635, 460], [548, 448], ], dtypenp.float32) # 相机内参和畸变系数来自第2章标定结果 camera_matrix np.load(calib_params.npz)[mtx] dist_coeffs np.load(calib_params.npz)[dist] found, rvec, tvec cv2.solvePnP( object_points, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE) if found: print(旋转向量:, rvec.ravel()) print(平移向量:, tvec.ravel()) # 相机到目标的距离单位mm print(距离:, np.linalg.norm(tvec)) # 旋转向量转旋转矩阵后续算欧拉角要用 R, _ cv2.Rodrigues(rvec)rvec是3x1的旋转向量不能直接当欧拉角用习惯上先用cv2.Rodrigues转成3x3旋转矩阵再根据你的坐标约定解算出俯仰、偏航、横滚。tvec是被测物体坐标系原点在相机坐标系下的位置也就是“物体在相机前方多远处”。flags参数有几个常见选择flags适用场景说明SOLVEPNP_ITERATIVE任意3D构型基于Levenberg-Marquardt优化最通用点数越多越稳SOLVEPNP_P3P只有4个点追求速度速度快但对噪声敏感建议只用做初值SOLVEPNP_IPPE所有特征点共面平面目标专用速度和稳定性都好OpenCV 4.x可用如果特征点都在一个平面上比如测量一块平板上的二维码角点SOLVEPNP_IPPE是更好的选择。它专门针对平面目标优化比用ITERATIVE在噪声下的表现更稳。如果目标不是平面的就用ITERATIVE。P3P我只在点数刚好4个且实时性要求高的场合用结果容易跳不建议直接用于测量。4.2 特征点提取与匹配ORB 和 SIFT 的工程选型image_points不会凭空出现。工业上最可靠的做法是直接用人工标记标定板棋盘格角点、AprilTag二维码角点、或者圆点阵列这些特征点提取稳定误匹配率低。最省事的是棋盘格角点第2章已经会提取了直接复用。如果要测量的是没有人工标记的自然物体就需要特征点提取和匹配。常见选型是ORB和SIFT的对比。ORB免费、快、适合实时场景但尺度不变性差物体离相机很远或很近时特征描述子匹配率下降。SIFT稳定性更好但在老版本OpenCV里需要自己编译extra模块折腾成本高新版本可以直接调用。测量场景中相机和物体的距离通常是固定的尺度变化不大我一般直接用ORB加汉明距离匹配速度优势明显。这也是HALCON和OpenCV在实际使用中的一个区别HALCON自带成熟的亚像素边缘和测量算子交付效率高OpenCV则胜在免费、流程透明可控。4.3 solvePnP 结果乱跳的三种误用方式很多新手第一次跑通solvePnP发现旋转向量来回跳第一反应是换算法实际上最常见的坑不在算法。第一个坑是2D和3D点顺序没对齐。3D坐标的第0个点必须对应图像上第0个角点一旦顺序错一位解出来的位姿就是错的而且这种错没有明显规律看起来像随机跳动。先做可视化把每个特征点的索引号画在图上确认。第二个坑是单位不一致。object_points里写的是毫米tvec输出就是毫米如果有的地方用了格子数、有的地方用了毫米结果偏差几个数量级。另外图像一旦cv2.resize过所有2D坐标都要除以对应缩放系数否则内参和像素坐标对不上。第三个坑是点数太少。4个点是solvePnP能工作的下限任何一个特征点提取偏了1个像素解出来的位姿可能就偏几毫米。我一般会提取10个以上的点参与求解让优化算法有足够的冗余去分摊误差。5. 单目测量系统常见问题与误差排查做单目测量踩坑是常态。这里列几个我实际遇到过、也帮别人排查过的高频问题每条按现象、原因、解决三个层面写可以直接对照自检。5.1 现象一重投影误差很低实测尺寸却偏了1.5毫米标定报告显示重投影误差只有0.08px按说相机模型没毛病但拿标准量块一测尺寸偏了1.5mm。这种情况问题基本不在标定而在边缘提取。标定只保证角点检测准确测量时用的是物体边缘如果边缘有反光、阴影、或者物体本身颜色和背景对比度不够边缘检测结果偏移两三个像素很常见乘以scale就成了毫米级误差。先检查图像是否被缩放再检查物体是否贴合测量平面最后加光源。产线上解决边缘稳定的首选是背光把被测物放在背光板上物体轮廓是纯黑背景是纯白边缘对比度拉满边缘提取精度能明显提升。如果必须是正光就固定光源角度不要用环境光。5.2 现象二ORB 匹配乱配位姿随机跳动ORB描述子是二进制向量距离度量必须用汉明距离。如果用了默认的L2欧氏距离匹配结果基本是乱的。另外ORB特征点数量默认只有500纹理少的物体上提取不到足够的特征。解决方法是把匹配器换成cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue)增加nfeatures到2000左右。匹配完之后还要过一遍外点筛选最常见是用cv2.findHomography加RANSAC或者用ratio test保留最近邻和次近邻距离比值小于0.75的匹配对。过滤之后如果匹配对少于15个宁可放弃这一帧重新采集也不要拿脏数据去解算位姿。5.3 现象三Python 环境里 OpenCV 装不上、运行报错ModuleNotFoundError: No module named cv2这个报错最常见的原因是当前Python环境和安装环境不一致。我习惯在项目目录下先建虚拟环境再pip install opencv-python。在Ubuntu或树莓派上如果系统里已经有python3-opencvconda环境里再装一份两个库会互相抢先彻底卸载系统版本再装。报错信息里路径出现pip-req-build这样的临时目录通常是pip在尝试源码编译而不是下载wheel包常见于Python版本过老或pip版本过旧的Windows环境。解决方法是升级pip或者用python -m pip install opencv-python --only-binary :all:强制使用预编译包。Windows下用VS2022编译C工程链接OpenCV时也容易报错多数是预编译库的MSVC版本和编译器不匹配。新版OpenCV 4.x的预编译包对应特定MSVC版本装错版本在链接阶段会出现大量LNK2038之类的错误。如果不想折腾版本匹配用vcpkg安装最省心。5.4 现象四测量速度只有 2 FPS跟不上产线节拍速度瓶颈往往不在算法本身而在图像采集。USB摄像头默认输出YUYV格式CPU做格式转换开销大先用cv2.VideoCapture的CAP_PROP_FOURCC设置成MJPG帧率能明显提升。其次是把处理分辨率降到满足测量精度需求的最小值1280x720能到0.1mm精度就没必要用1920x1080。算法侧优先做ROI裁剪上一帧检测到物体之后下一帧只在周边区域搜索比全图提取特征快得多。如果还是在树莓派这类嵌入式平台上跑同一套代码用C重写通常能快2到3倍OpenCV的C API和Python高度一致迁移成本可控。定位瓶颈的办法是逐段打印耗时不要凭感觉优化很多时候cv2.imread的耗时比特征提取还高。6. 验证闭环与进阶技巧用棋盘格做测量系统自检测量系统上线前一定要有一个可重复的验证流程否则你永远不确定今天测出来的数据是准的还是碰巧准的。最实用的自检工具就是标定板把标定板放到测量平面上位置和求H时不一样然后提取角点像素坐标用之前求好的单应矩阵换算成毫米坐标和标定板已知的物理坐标对比。# H 是第3章求好的单应矩阵用不同位置的照片做验证 ret, corners cv2.findChessboardCorners(gray, (7, 6), None) pix corners.reshape(-1, 1, 2).astype(np.float32) mm cv2.perspectiveTransform(pix, H).reshape(-1, 2) known objp.reshape(-1, 2) # 标定板角点的已知物理坐标mm err np.sqrt(np.sum((mm - known) ** 2, axis1)) print(角点数量:, len(err)) print(平均误差(mm):, err.mean()) print(最大误差(mm):, err.max())注意验证用的照片不能是求H用的那张图否则是拿同一个变换验证同一个变换结果必然漂亮没有意义。正确做法是求完H后把标定板换个位置、换个角度再拍一张再算误差。这套自检我习惯每次开机跑一遍耗时不到一分钟。如果平均误差超过0.3mm优先检查相机是否被碰过、测量平面上是否有异物、镜头焦距是否被改动过。自检通过之后再做正式测量数据才敢报给产线。进阶方面想要把边缘测量精度从1个像素提升到0.1到0.2像素就得做亚像素处理。棋盘格角点用cv2.cornerSubPix直线边缘用Canny找到边缘点集然后cv2.fitLine拟合直线再计算两条拟合直线之间的距离比直接数像素稳定得多。最后说一个经验教训。我做第一个测量项目时标定重投影误差一直在0.08px以下但实测总是偏快2毫米。我反复调阈值、换算法折腾了一周才发现是标定板打印纸贴在桌上纸面本身不平整标定过程中板子弯曲导致畸变参数被拟合错了。那之后我形成了一个习惯先验证物理条件再谈算法调优。单目测量系统本身不复杂难的是把“像素到毫米”之间的每个假设都坐实——平面是否共面、标定是否有效、边缘是否提取准确。做得越久越觉得这套系统里最玄学的环节反而不是算法而是光照和物理约束。希望帮到你。本文还有配套的精品资源点击获取
返回列表