ARTICLE DETAIL

资讯详情

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

OpenCV相机标定实战:calibrateCamera参数详解与避坑指南

OpenCV相机标定实战:calibrateCamera参数详解与避坑指南 相机标定是计算机视觉里绕不开的一个环节不管是做三维重建、视觉SLAM、AR还是畸变矫正第一步基本都得过这一关。OpenCV里对应的函数就是calibrateCamera这名字听起来挺唬人其实剥开来看就是把“三维世界坐标”和“二维图像坐标”之间的对应关系算清楚。我最早接触这个函数的时候对着文档看了半天参数一个个认识但真到自己写代码时还是懵尤其是那个objectPoints和imagePoints到底怎么组织官方文档说得云里雾里实际跑起来各种报错。这篇文章就把我这些年用下来的经验一次性讲清楚从原理到代码从参数到避坑尽量让新手少走弯路。1. 先搞懂标定到底在算什么在碰calibrateCamera之前先得明白相机成像的基本模型。可以这么理解相机把三维世界里的一个点通过镜头投射到二维传感器上这个过程可以用针孔模型来近似描述。针孔模型里有几个关键参数内参矩阵决定焦距和光心位置畸变系数描述镜头带来的形变外参描述相机在世界坐标系下的位姿。标定的本质就是通过已知的三维点和对应的二维像素点把这几个参数反算出来。1.1 内参、外参和畸变系数分别是什么内参矩阵一般用cameraMatrix表示是一个3x3的矩阵里面包含焦距fx、fy和光心坐标cx、cy。焦距的单位是像素不是毫米它跟传感器分辨率、镜头焦距都有关系。光心理论上是图像中心但实际上因为装配误差会有几个像素的偏移标定就是为了把这些真实值抠出来。外参用rvecs和tvecs表示分别对应旋转向量和平移向量。对每次拍摄的棋盘格图像都会有一组外参它描述的是棋盘格坐标系和相机坐标系之间的相对关系。如果你要做的是单目标定、只需要矫正畸变外参基本不用细看但如果要做双目立体匹配外参会直接影响深度计算精度。畸变系数distCoeffs是最容易让人困惑的地方它通常是4个或5个参数k1、k2、p1、p2、k3。k1、k2、k3是径向畸变系数也就是画面边缘往外凸或往里凹的那种变形p1、p2是切向畸变系数描述镜头和传感器不平行造成的梯形变形。实际标定中光线穿过镜头中心和边缘的折射程度不同所以径向畸变是最明显的一般在画面边缘会看到明显的桶形或枕形畸变。1.2 为什么用棋盘格而不是别的图案OpenCV的标定例程基本都用棋盘格这是有原因的。棋盘格的角点检测算法非常稳定角点是黑白格子的交点具有旋转不变性无论相机从哪个角度看都能准确检测出来。而且棋盘格的角点数量多一次拍照可以提取几十个特征点标定时数据量充足鲁棒性更好。另外一种常见标定板是圆点阵列OpenCV也支持findCirclesGrid但在实际使用中我发现圆点标定板对光照和遮挡更敏感稍微有点反光或者部分圆点被遮挡就容易失败。所以如果不是特殊场景直接用棋盘格最省心。我记得当时给手里的工业相机做标定用A4纸打印的7x10棋盘格效果就很好不一定要买那种贵价的玻璃标定板。2. calibrateCamera函数参数逐一拆解直接看OpenCV官方文档calibrateCamera的原型有好多重载核心参数其实就那么几个。我这里说的是C和Python都通用的逻辑无论你用什么语言参数的理解是一样的。double calibrateCamera( InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize, InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs, int flags 0, TermCriteria criteria TermCriteria(TermCriteria::COUNT TermCriteria::EPS, 30, DBL_EPSILON) );2.1 objectPoints怎么填才正确objectPoints是三维参考点坐标这是新手最容易踩坑的地方。很多人以为要填世界坐标系下真实的物理坐标比如棋盘格每个角点在毫米单位下的位置这本身没错但有个前提——每个角点的坐标必须是针对同一张图像、同一组检测结果对应的。实际操作中我们通常以棋盘格平面为z0平面来建立坐标系第一个角点设为原点。比如棋盘格是9列7行每个格子边长30mm那么角点的坐标就是(0,0,0), (30,0,0), (60,0,0)...以此类推y方向同理z始终为0。这里有个细节角点数不是格子数比如9x7的棋盘格角点数是8列6行也就是48个角点。我见过有人把世界坐标填成相机坐标系下的坐标这就不对了。calibrateCamera内部会通过外参将世界坐标转换到相机坐标再通过内参投影到图像平面你只需要提供相对于棋盘格的世界坐标就够了棋盘格的位置和姿态由外参去拟合。2.2 imagePoints的获取与组织方式imagePoints是图像上检测到的对应角点的像素坐标。通常用findChessboardCorners函数检测得到然后用cornerSubPix进行亚像素精化。这个imagePoints必须和objectPoints一一对应第i个三维点对应第i个二维点顺序不能乱。imagePoints的数据类型是vectorvectorPoint2f外层vector代表图像数量内层vector代表一张图中所有角点。objectPoints的结构和它完全一样只是Point2f换成了Point3f。这里有个容易混淆的地方——有人以为一张棋盘格只需要一个objectPoints但实际每张图都要提供对应的objectPoints只不过因为棋盘格坐标系不变所以每张图的objectPoints内容完全一样。我当时犯过一个低级错误把不同分辨率下拍的图混在一起标定。不同分辨率下同一张棋盘格的角点位置其实一样但标定必须统一size图片尺寸变了内参含义就变了结果自然不对。所以标定前先统一图像尺寸或者对图像做resize的时候要格外小心。2.3 imageSize、rvecs和tvecs的作用imageSize是图像的宽高以像素为单位它的作用是给畸变矫正提供参考。畸变参数跟图像尺寸有关同一个镜头在不同的分辨率下畸变表现不一样所以这个参数必须和输入图像的实际分辨率一致否则矫正效果会打折扣。rvecs和tvecs是输出参数每张图对应一个旋转向量和一个平移向量。它们是标定的副产物通常不需要过度关注但在某些场景下有用比如你要把棋盘格和相机之间的外参提取出来就得从rvecs和tvecs里拿。注意旋转向量是Rodrigues表示法不是旋转矩阵要用Rodrigues函数转换成3x3旋转矩阵。2.4 flags参数和criteria的调优心得flags参数控制标定过程中哪些参数固定、哪些参数参与优化。默认值为0表示所有参数都参与优化这对于大多数场景已经够用。但有些时候你想固定某些参数比如你已经通过其他方式知道了cx、cy不想让它们参与迭代就可以用CALIB_FIX_PRINCIPAL_POINT。我比较常用的flag组合是CALIB_ZERO_TANGENT_DIST也就是假设切向畸变系数为0只估计径向畸变。如果镜头质量还不错这种假设通常成立可以减少待优化参数数量提升稳定性。当然如果镜头有明显的切向畸变比如镜头有磕碰那还是不要用这个flag。另一个值得关注的是CALIB_FIX_K3和CALIB_FIX_K4等这些用来固定高阶畸变系数。一般标定5参数畸变模型k1、k2、p1、p2、k3就够了少用7参数、8参数模型参数越多越容易过拟合。我自己在标定普通镜头时用5参数就有很好的效果只有在鱼眼镜头这类极端畸变的场景下才会考虑更多参数。criteria是迭代终止条件包括最大迭代次数和误差阈值。默认值在大多数情况下都够用不需要动。但如果你发现标定结果反复横跳也就是每次跑出来的内参差异很大可以考虑把最大迭代次数调大比如TermCriteria(TermCriteria::COUNT TermCriteria::EPS, 100, 1e-6)给优化器更多时间收敛。3. 完整标定流程实操理论讲得再多不实际操作一遍总是不踏实。下面我把从拍照到输出内参的完整流程过一遍包含Python代码示例和每一步的注意点。Python接口在OpenCV中非常成熟代码量也最少适合快速验证。3.1 采集标定图片的正确姿势采集图像是标定流程中最影响结果的一步这一步做不好后面再优化也没用。我曾经偷懒一张棋盘格放在桌子上一动不动相机转来转去拍了十几张结果标定出来的内参跟厂家标称值差了很远。原因就是棋盘格在画面中的姿态和位置太单一不够“多样性”。正确的采集方式是固定相机移动棋盘格让棋盘格在画面中出现在不同位置、不同角度、不同距离。具体来说画面中心、四角、边缘都要覆盖到棋盘格要倾斜跟相机光轴成30度到60度的角度同时保证所有角点都在画面内。采集15到20张左右就足够稳定了我之前做过实验10张以下误差会明显偏大20张以上收益递减。还有一个细节是打印棋盘格的时候背景要平整不要有折痕不要反光。用哑光纸打印效果最好放在硬纸板或者泡沫板上保持平整。如果打印出来格子边缘发虚宁可重新打印也不要勉强用。3.2 角点检测与亚像素精化拿到图像后先调用findChessboardCorners检测角点。这个函数有几个参数值得注意首先是patternSize它表示角点数量比如棋盘格有7列10行个格子那么角点数是6行9列也就是(9, 6)。注意这个顺序先列后行跟图像宽度高度的顺序对应。如果检测失败函数返回False可能是棋盘格曝光过度、变形太厉害或者被遮挡。我可以分享一个技巧把图像转成灰度图适当做直方图均衡化能显著提高检测成功率。另外如果图像比较大可以先缩小再检测再把角点坐标缩放回去速度会快很多。检测到初始角点之后一定要做亚像素精化也就是cornerSubPix。这个函数通过迭代优化角点位置到亚像素精度对于提高标定精度作用极大。我记得第一次做标定的时候跳过了这步重投影误差大了差不多0.3个像素做一个精化就能降到0.1以内。import cv2 import numpy as np import glob # 棋盘格参数内角点数量注意不是格子数量 pattern_size (9, 6) # 格子边长单位mm实际值取决于你的打印设置 square_size 30.0 # 准备objectPoints所有图像共用同一个参考坐标 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size object_points [] image_points [] images glob.glob(calib_images/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(fFailed to find corners in {fname}) continue # 亚像素精化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) object_points.append(objp) image_points.append(corners) # 可视化绘制角点便于确认检测是否正确 cv2.drawChessboardCorners(img, pattern_size, corners, ret) cv2.imshow(Corners, img) cv2.waitKey(50) cv2.destroyAllWindows()3.3 调用calibrateCamera并评估结果有了object_points和image_points就可以调用核心函数了。返回的ret是重投影误差这个值要重点关注它衡量的是三维点投影到图像平面的位置跟检测到的位置之间的平均偏差。一般来说ret小于0.5像素说明标定效果好在0.5到1.0之间说明可以接受但要检查一下采集的图像质量超过1.0就说明标定有问题需要重新检查和采集。ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( object_points, image_points, gray.shape[::-1], # 注意这里是(width, height) None, None ) print(f重投影误差: {ret}) print(内参矩阵:) print(mtx) print(畸变系数:) print(dist)这里有个坑imageSize参数是(width, height)也就是先宽后高的顺序。如果你直接传gray.shape那是(height, width)顺序反了标定出来的内参会直接出错。我当时就在这个细节上卡了一晚上画面当中显示完全正常的棋盘格但内参就是不合理。标定完成后还可以用getOptimalNewCameraMatrix和undistort来验证矫正效果。如果矫正后的图像直线变直了说明标定结果是可信的。# 矫正畸变验证效果 h, w gray.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst cv2.undistort(img, mtx, dist, None, newcameramtx) # 裁剪黑边 x, y, w, h roi dst_cropped dst[y:yh, x:xw]3.4 不同标定板尺寸对结果的影响棋盘格格子大小是标定中的一个容易忽略的变量。理论上square_size只要在objp和实际物理尺寸之间保持一致标定出的内参中焦距的单位像素是物理独立的焦距乘以格子边长才能换算到毫米但内参矩阵本身的数值不依赖格子边长。难理解的例子你用square_size30和square_size15去标定同一组图像得到的fx会是不同的他们的比例恰好等于边长比例。这个不是错误而是因为fx、fy的单位是像素不是物理长度。如果你要做双目深度计算或者需要知道相机到物体的实际距离必须保证square_size是真实的物理尺寸否则算出来的深度会按比例缩放。4. 常见问题与排查技巧实录写代码容易调试难。calibrateCamera的报错信息有时候很隐晦不仔细分析很难发现是哪一步出了问题。这里整理几个我实际踩过的坑和排查方法。4.1 标定结果反复变化不稳定如果你每次标定出来的内参都不一样甚至差异很大往往不是算法问题而是输入数据的问题。常见原因有几个一是采集图像张数太少数据量不足以约束这么多参数二是图像过于相似比如棋盘格总在画面中央姿态变化不大造成病态问题。我自己的一套排查思路是先画角点图确认每一张图的角点都被正确检测和对应。然后再看标定后的重投影误差如果某些图像误差特别大比如超过1个像素说明那张图像有问题可能是模糊、畸变过大或者棋盘格有反光。另外图像分辨率对结果也有影响。分辨率太低的图像角点检测精度天然就差标定出来的内参误差会被放大。建议标定图像分辨率至少不低于500万像素如果用的是300万像素以下的摄像头可以多拍20张以上来补偿。4.2 误用不同分辨率的图像我之前提到过一次但这个问题实在太常见值得再单独拎出来说。有些人标定时用了一部分1280x720和一部分1920x1080的图像或者训练图像是1920x1080但标定用的图像是640x480导致畸变系数与实际画面不匹配。这个问题的本质是畸变系数是归一化的与分辨率相关。同样的镜头在1920x1080下畸变看起来比640x480下更明显因为像素更密集同样的物理位移对应更多像素。所以标定用的图像分辨率必须与实际应用时一致。如果你在应用中需要多个分辨率那就分开标定分别保存对应的内参。4.3 棋盘格角点检测失败的常见原因findChessboardCorners返回False看起来是小问题实际很影响效率。我遇到最多的原因是光照不均棋盘格部分区域过曝或者对比度过低。解决办法是调整光源方向让光照尽量均匀或者用灰度变换增强对比度。另一个原因是图像畸变太严重尤其是广角镜头和鱼眼镜头画面边缘的棋盘格变形过大导致检测算法无法准确识别。这时候可以通过把棋盘格放在画面中心区域来规避同时多拍几个角度。如果还是检测不到可以先用undistort做一次粗略矫正再检测但注意标定数据本身的图像和imageSize要一致。4.4 标定精度不够的排查方向当重投影误差偏大时我一般会按照这个优先级去排查首先检查角点检测是否准确有没有错检或漏检其次检查棋盘格是否平整有没有弯曲变形然后确认标定板是否发生位移标定过程中标定板必须固定不动最后检查有没有使用不同分辨率的图像。还有一个小提示不要用带有白色边框的棋盘格图像去标定因为findChessboardCorners会把白色边框误认为是白色格子干扰角点检测。打印机如果裁切不准确建议把棋盘格图案尽量铺满纸张或者手动裁掉边缘多余部分。5. 几个实用场景的扩展用法calibrateCamera的用途远远不只是输出一组内参和畸变系数配合其他OpenCV函数可以实现很多功能。这里简单讲两个我实际做过的方向给有需求的读者一点启发。5.1 结合solvePnP做位姿估计标定出内参之后你可以用solvePnP来计算棋盘格或者说参考坐标系与相机之间的相对位姿。这在机械臂抓取、无人机降落、AR场景中非常常见。棋盘格提供一个已知尺寸的平面通过检测角点并匹配三维坐标就能得到精准的6自由度位姿。我当时在这个方向上的经验是solvePnP的精度受角点检测精度影响很大角点一定要做亚像素精化。另外如果棋盘格离相机太远角点几乎擠在一起位姿估计会退化这时候尽量让棋盘格离相机近一些或者在镜头视野中占足够大的面积。5.2 双目标定与立体视觉如果你要做双目测距、三维重建calibrateCamera是第一步之后还需要用stereoCalibrate来计算两个相机之间的相对外参再通过stereoRectify得到矫正映射表。整个流程中单目标定的结果质量直接影响双目标定的稳定性。单目标定做得好的标志是内参在不同分辨率、不同采集批次之间能保持一致。所以在做双目前建议先分别对左右相机做单目标定确保重投影误差都小于0.5像素再去做双目标定。如果单目标定都不稳定双目标定结果基本是废的。6. 我对calibrateCamera的一点心得相机标定这东西原理说复杂也复杂说简单也简单。但真正用好了能省掉后面无数调试的功夫。我个人的体会是标定效果的好坏七分靠图像采集三分靠参数调优。与其纠结flags参数怎么配不如老老实实多拍一些质量高、姿态丰富的图像。还有一点标定结果尽量保存下来包括内参矩阵、畸变系数、图像分辨率最好连采集环境的描述也记录下来。这样如果后续镜头松动、换设备可以快速判断是否需要重新标定。我一般会把标定参数写成JSON文件或者YAML文件存到项目配置目录用的时候直接加载不需要每次启动都重新标定。如果你刚开始接触标定不用追求一次做到完美先把流程跑通拿到一组内参和矫正图看效果再反向调整采集策略。这个过程本身就是对相机成像模型最好的理解方式。希望这篇文章能让你少走一些弯路。
返回列表