ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

双目视觉立体标定与校正:从原理到OpenCV实操详解

双目视觉立体标定与校正:从原理到OpenCV实操详解 简介一套基于VS2013与OpenCV3.0实现的双目视觉立体标定与校正完整工程面向需要开展立体匹配、三维重建预研的视觉开发者及学习OpenCV双目模块的初学者。压缩包共251个文件总大小约29.64MB主要包含OpenCV头文件与库文件hpp/h、lib、dll、cmake配置文件、棋盘格标定png图像、标定结果yml、构建日志以及可直接运行的EXE和工程源代码目录结构清晰便于直接打开调试或移植核心函数。资源内置标定结果和日志可辅助检查标定精度与校正效果。该资源已有1735人学习下载具有较高参考价值。通过学习可掌握立体标定的完整流程、理解校正参数含义并以此为基础快速进入立体匹配与三维重建开发。1. 从“双眼看世界”说起双目视觉立体标定与校正到底在解决什么问题做双目视觉的人大概都经历过这种场面双目模组固定得好好的左右相机型号一致采集回来的图像肉眼看没什么毛病可一跑立体匹配视差图直接花了。问题通常不在算法而在最前端的立体标定与校正没有做扎实。很多人觉得标定就是拿棋盘格拍几张图跑一遍OpenCV里的calibrateCamera就完事其实里面的门道比想象中多。这里先理清三个概念标定、校正、匹配。标定是求解相机内外参数和畸变系数的过程校正是利用这些参数对图像做几何变换把带畸变的图像修正为标准针孔模型下的理想图像并完成左右视图的行对齐立体匹配才是真正逐像素计算视差的下游任务。标定和校正的精度直接决定了匹配结轨可用性。为什么不能跳过这一步双目测距的核心原理是三角定位同一物点在左右图像中的水平坐标差视差和深度成反比。但真实相机存在镜头畸变左右目安装也不可能绝对平行如果不先做校正左右图像的同名点根本不在同一水平线上匹配算法只能在大范围内暴力搜索既慢又容易出错。哪怕你用深度学习匹配网络输入分布被破坏后精度一样崩。顺带说一句“校正”这个词在不同领域含义完全不同。红外图像两点校正针对的是探测器响应不均匀性做的是增益和偏移补偿keystone校正用在投影仪梯形畸变修正功率因数校正属于电气领域处理的是电压电流相位不同步问题。这些跟视觉标定里的畸变校正、立体校正不是一回事搜索资料时别被各种科普带偏方向。1.1 标定、校正、匹配三者的边界写代码之前先把整个流程在脑子里过一遍。一个完整的双目视觉工程通常包含五步单目标定、双目标定、立体校正、立体匹配、三维重建或测距。每一步都有明确的输入输出单目标定分别求解左右相机的内参焦距fx、fy主点cx、cy和畸变系数k1、k2、p1、p2、k3。双目标定求解左右相机坐标系之间的旋转矩阵R和平移向量T也就是双目外参。立体校正基于内外参对图像重投影使左右图像极线平行、行对齐。立体匹配在行对齐图像上沿极线搜索同名点输出视差图。三维重建通过视差和重投影矩阵Q把像素坐标换算成三维坐标或深度。不少人把第2步和第3步合起来叫“双目校正”把第4步叫“双目匹配”。我见过不少项目是前面步骤草草跑完在立体匹配调了一个月最后回头检查才发现是校正没做对。很多人一上来就想调SGM参数、调视差平滑权重其实先花半天时间把标定和校正做扎实后面匹配的返工量能少一大半。1.2 不标定直接匹配的代价有人问现在深度学习的匹配网络都这么强了能不能跳过标定校正答案是可以绕但代价很大。基于深度学习的立体匹配虽然端到端出视差图但仍然假设输入图像对是经过极线校正的。模型在训练时的数据都做了行对齐处理如果你的输入没有校正相当于人为改变了输入数据分布网络泛化能力直接被打折扣。更关键的是深度学习解决的是匹配问题它并不知道两个相机的真实内外参数无法输出尺度正确的深度。你要做的是测距就必须有标定环节提供的几何信息。所以在工程项目里双目视觉立体标定与校正这一套基本功是绕不过去的早学会早受益。2. 标定背后的数学原理从像素坐标到三维坐标我刚开始接触标定时总觉得公式吓人后来想通了标定本质上就是在解“三维世界的点映射到二维像素坐标”的数学模型。搞懂了这个映射关系你就知道标定到底在求哪些量。2.1 针孔模型与内外参数理想情况下相机成像可以抽象成针孔模型。空间中一点PX、Y、Z经过相机光心投影到成像平面上再用像素坐标系表示。这个过程的数学表达是[ z_c \begin{bmatrix} u \ v \ 1 \end{bmatrix} K \begin{bmatrix} R t \end{bmatrix} \begin{bmatrix} X \ Y \ Z \ 1 \end{bmatrix} ]其中K是内参矩阵包含fx、fy焦距单位是像素和cx、cy主点坐标R和t是外参表示相机在世界坐标系里的姿态和位置。单目标定就是求K和畸变系数双目标定就是求左右相机坐标系之间的R和t。这里有个容易忽略的细节fx和fy在像素坐标系里并不完全相等。因为感光芯片的像元不一定是正方形再加上后期裁剪两个方向的比例会有细微差别。标定出来的fx和fy如果差了一两个像素很正常但如果差了几十个像素就要怀疑是不是左右图像分辨率不一致或者标定板图像被resize过。2.2 畸变模型让图像偏离针孔模型的元凶任何物理镜头都不可能完美符合针孔模型。光线经过透镜会发生折射产生两类畸变径向畸变光线离光轴越远弯曲越厉害表现为画面边缘的“桶形”或“枕形”变形。靠近画面边缘的直线会明显弯曲。切向畸变镜片和成像传感器不完全平行导致图像边缘出现轻微的拉伸扭曲。OpenCV里用5个系数来描述畸变k1、k2、k3是径向系数p1、p2是切向系数。畸变模型本质上是一组多项式标定的过程就是拟合这组多项式的最优系数。工程上有个经验值普通镜头用4个系数就够了鱼眼或广角镜头才需要k3。如果你用普通镜头强制估计k3反而容易引入额外的数值噪声导致边缘区域校正失真。打个比方畸变校正就像给一张被揉皱的纸做熨烫。标定就是先搞清楚这张纸的褶皱规律畸变系数校正是根据这个规律把褶皱烫平remap重映射。褶皱规律没摸清烫出来的纸反而更皱。2.3 像差校正与“各种校正”的边界这里专门说一下“校正”这个词的多义性因为网上经常有人搜“像差校正”“误差校正”“keystone校正”“功率因数校正”等关键词最后发现跟自己的问题根本不是一回事。视觉标定里的像差校正就是上面说的径向畸变和切向畸变补偿属于光学成像系统的几何误差修正。红外图像两点校正是红外探测器工程里的术语因为每个像元的响应增益和偏置不同需要用高温和低温两个黑体辐射源做两点定标求出每个像元的补偿系数。keystone校正是投影显示领域的东西解决的是投影仪斜着投射时画面变成梯形的问题本质是数字图像几何变换。功率因数校正则是电力电子领域的概念跟图像完全不沾边。所以当你说“我要做双目校正”时一定要明确是在做视觉几何层面的畸变校正和极线校正而不是其他领域的“校正”。做工程最怕的就是概念混淆方向错了后面全是白费功夫。3. 实操流程标定板选择、图像采集与工具选型理论讲完进入实战。这一节我把整个标定流程的操作细节拆开讲包括标定板怎么选、图怎么拍、代码怎么写、结果怎么看。3.1 标定板怎么选、怎么拍标定板有两种主流选择棋盘格和圆点阵列板。棋盘格用cv2.findChessboardCorners检测圆点板用cv2.findCirclesGrid检测。我的经验是棋盘格普及率高资料多室内固定光源下检测稳定。缺点是纯色纹理弱运动模糊或强反光下角点容易丢。圆点阵列板光照变化大、画面纹理复杂的环境下鲁棒性更好圆心定位精度高。缺点是制作要求高圆点几何中心偏移会影响标定结果。棋盘格打印时尽量用激光打印机贴在刚性平板上。软纸贴在弯曲的显示屏幕上角点坐标本身就有误差标定出来内参再准也白搭。格的边长要量准确一般用游标卡尺多测几个格子取平均。边长单位建议统一用毫米后面三维重建时才不会出量纲错误。我习惯用边长30mm、内角点9x6的棋盘格这是OpenCV示例里的经典配置资料多、坑少。图像采集是标定质量的关键环节至少20到30对图像是底线。我的实际项目里一般拍35到40对。摆放姿态要有层次倾斜、旋转、远近都要有让标定板出现在画面的九个区域左上、中上、右上、左中、中心、右中、左下、中下、右下。如果只放在画面中间拍30张标定结果的外延性很差画面边缘区域的畸变校正效果会明显变差。拍摄时注意三点一是标定板不能有运动模糊手持拍摄时快门速度要足够高二是板面光照要均匀避免强反光把角点区域的灰度对比度破坏三是左右相机要同时采集时间戳对齐不能左图拍的是这个姿势、右图拍的是另一个姿势否则双目标定直接废掉。3.2 标定代码与关键参数选择下面给出一个基于OpenCV的Python标定流程代码不是完整工程而是把关键步骤串起来方便你对照自己的项目修改。import numpy as np import cv2 import glob # 棋盘格内角点数例如 9x6 表示横向9个、纵向6个内角点 pattern_size (9, 6) square_size 30.0 # 每格边长单位毫米 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 obj_points [] # 世界坐标 img_points_l [] # 左图像素坐标 img_points_r [] # 右图像素坐标 images_l sorted(glob.glob(left/*.jpg)) images_r sorted(glob.glob(right/*.jpg))这里有个高频踩坑点pattern_size传的是内角点数量不是棋盘格的总格子数。比如一张9x6的棋盘格图片交叉点内角点是9-1x6-1 8x540个。很多人把9和6当成格子数导致角点检测永远失败。接下去是单目标定ret_l, mtx_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera( obj_points, img_points_l, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, rvecs_r, tvecs_r cv2.calibrateCamera( obj_points, img_points_r, gray_r.shape[::-1], None, None)单目标定完成之后做双目联合标定criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6) ret, M1, d1, M2, d2, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteriacriteria, flagscv2.CALIB_FIX_INTRINSIC)CALIB_FIX_INTRINSIC这个标志很关键它告诉stereoCalibrate不重新估计左右目的内参而是固定在上面单目标定的结果上只优化双目外参R和T。这样做的好处是优化变量变少数值稳定性高不容易收敛到局部极小值。如果你对单目结果不够自信可以去掉这个标志让双目联合优化全部参数但前提是采集的角点对数量要足够多、视角分布要足够均匀否则联合优化反而会发散。有一个点要提醒stereoCalibrate对左右图像点顺序极其敏感。img_points_l和img_points_r必须是同一帧图像上检测到的同一组角点一一对应。如果采集时左右图时间戳没对齐对应点不一致标定出的R和T就是错的而且重投影误差看不出来。3.3 重投影误差怎么判断标定质量标定完不能直接走人必须用重投影误差来验证。所谓重投影误差就是把世界坐标点通过求得的参数重新投到像素平面和检测到的角点坐标比一比算平均距离。这个值能直观反映标定结果的精度。mean_error_l 0 for i in range(len(obj_points)): imgpoints2, _ cv2.projectPoints( obj_points[i], rvecs_l[i], tvecs_l[i], mtx_l, dist_l) error cv2.norm(img_points_l[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error_l error print(left re-projection error: %.4f % (mean_error_l / len(obj_points)))我的经验是重投影误差在0.1到0.3像素之间算合格0.5像素以上就要查数据了。如果总体误差大优先检查是不是有个别帧的图像运动模糊严重或者标定板没放平。把这些异常帧单独找出来剔除掉往往误差就下来了。还有一个小技巧把每帧的误差都打印出来不用只看平均值。平均值被大量好帧拉低了局部烂帧的问题就被掩盖了。4. 立体校正让左右图像严格行对齐的关键一步标定完成内外参数都有了接下来就是立体校正。这一步的目标是把左右两张图像通过重投影变换使它们满足极线约束也就是左右图像的同名点落在同一水平线上。做完这一步之后立体匹配才能在一维方向水平方向搜索对应点效率和数据可靠性都会大幅提升。4.1 为什么要追求行对齐我们先理解原图的情况两个相机安装时很难做到绝对平行总会有微小的旋转和平移偏差。这导致同一个三维点在左右图像上的投影位置不仅水平方向有差异垂直方向也有差异。立体匹配如果要在二维区域内搜索同名点计算量爆炸错误匹配率也会显著上升。校正之后两个相机被“虚拟旋转”到完全平行的位置左右图像同一行正好对应空间中同一个极平面同名点只存在水平偏移。这样匹配就变成了纯一维搜索速度和精度都上来了。我在实际项目中见过不少案例校正前用SGM算法跑一对1024x768的图要一秒钟校正后同样参数只用一百多毫秒效果还好得多。4.2 校正算法的两种思路OpenCV里stereoRectify完成的是经典的Bouguet算法它的核心思想是把右相机相对左相机的旋转矩阵R“劈成两半”一半给左相机、一半给右相机让两个相机各转过一半角度这样公共视场最大重投影畸变最小。它同时保证左右图像的重投影畸变尽量小是用得最多的一种方式。另一种是Hartley算法不依赖准确的相机内外参数而是通过图像本身的基础矩阵F推导校正变换。这种方法适合相机内参未知或者标定数据不理想的情况但校正后的图像会有轻微的非线性形变深度精度会受影响。工程上还是优先用Bouguet因为它能保持度量意义。只有当标定数据反复出问题或者用了很特殊的镜头时我才会考虑Hartley作为备选。stereoRectify里那个alpha参数也值得说道说道。alpha0表示裁剪掉所有黑边输出图像最小化无效区域alpha1表示保留所有原始像素图像会带黑边但信息不丢失。做深度估计时我习惯用alpha0反正黑边区域没有视差如果是给后续算法保留更多上下文就用alpha1。4.3 校正映射与remap落地校正不是直接修改图像矩阵而是先计算映射表再通过重映射完成像素搬运R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( M1, d1, M2, d2, gray_l.shape[::-1], R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0) map1_l, map2_l cv2.initUndistortRectifyMap( M1, d1, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( M2, d2, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1) rect_l cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)映射表只用算一次之后的每一帧图像直接调用remap就行不用重复计算。在实时系统里可以把映射表提前算好放内存里降低每帧延迟。重点说一下CALIB_ZERO_DISPARITY标志。设置了它OpenCV会让两个相机的校正后主点严格对齐也就是校正后相机的主射线相互平行。这样视差和深度对应的是无穷远点的视差为零计算重投影矩阵Q时会更清晰。如果不设这个标志Q矩阵最后一行的数值要靠stereoRectify输出来反推容易出符号错误。校正完成后一定要做行对齐验证。最简单的办法是把左右校正图上下拼接或者并排显示后画水平线看同名特征点是否落在同一条水平线上。我是用标定板再拍一帧检测角点后计算左右图对应角点的平均行差要求小于0.5像素。如果大于这个数检查是不是R、T传反了或者左右图的顺序弄反了。4.4 重投影矩阵与后续立体匹配的衔接stereoRectify输出里的Q矩阵是三维重建的关键它是一个4x4矩阵形如Q [[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 0, f], [0, 0, -1/Tx, (cx - cx)/Tx]]其中Tx是双目基线在水平方向的分量f是校正后焦距。已知视差d和像素坐标(u, v)通过Q矩阵就能算出一组齐次坐标再归一化得到三维坐标。这里要注意Tx的符号和基线方向如果左右相机在水平方向排列Tx为正如果相机的物理位置是右目在左目的左边安装反了Tx为负直接会导致深度值全是负数。遇到这类问题先检查相机安装方向再去翻标定代码。校正之后就可以衔接立体匹配算法了。SGM或基于深度学习的匹配网络都是在行对齐的图像上操作。匹配得到的视差图经过Q矩阵重投影就能生成深度图或点云。整个双目视觉立体标定与校正流程到这里才算闭环。5. 常见问题与排查技巧实录标定和校正的坑我在项目里踩过不少也帮别人排查过不少。下面整理成一张速查表每一条背后都是真实踩坑记录。现象可能原因解决方法findChessboardCorners检测不到角点内角点数量参数错误、光照反光、运动模糊确认pattern_size是内角点数补光并避免玻璃反光调高快门速度重投影误差大于0.5像素个别帧运动模糊、标定板不平整、内参初始值不对逐帧打印误差并剔除异常帧换刚性标定板重新做单目标定校正后行不对齐R和T传反、左右图顺序反了、stereoRectify参数错误检查双目标定输入点序交换左右图再跑一遍对比视差图深度全为负或花屏Tx符号错误、Q矩阵计算异常检查相机安装方向确认Tx正负打印Q矩阵最后一行数值左右焦距相差过大镜头型号不同、图像分辨率不一致、标定板被resize过检查图像尺寸和镜头型号统一分辨率重新标定标定结果每次都不稳定采集姿态单一、图像数量不够、板子离镜头太近增加图像数量到30对以上覆盖画面九个区域保持标定板在视场中占比适中重点讲几个我没在表格里写全的细节。第一标定板在画面中的占比。板上角点区域如果占整个画面不到十分之一角点检测精度会下降标定出的畸变系数偏差大。反过来如果板子离镜头太近导致畸变过大角点检测也会失败。我习惯让标定板在画面中占比在四分之一到二分之一之间同时不停变换倾角和远近。第二左右图采集时间戳必须严格对齐。手持标定板晃动时左图和右图不是同一时刻的姿态双目标定就会把这种不一致当成两相机间的位姿关系导致外参偏差。严谨的做法是用硬件触发同步采集或者至少保证标定板静止几秒钟再触发。第三标定完成后务必做一次端到端验证。不要只看重投影误差那只能说明角点拟合得好不能说明深度准。拿一个已知尺寸的物体放在不同的距离上用标定结果测距对比确认误差在可接受范围内。我在项目里通常拿一个边长100mm的标定块分别在0.5米、1米、2米位置测记录误差曲线。如果远端误差突然膨胀多半是基线太短或图像分辨率不够单纯调标定参数已经救不了。第四图纸上的畸变系数负数不要慌。k1、k2等系数可正可负负值代表桶形畸变正值代表枕形畸变这都很正常。只要重投影误差小、校正后图像直线变直标定就是合格的。不要因为看到负系数就怀疑代码写错了。6. 标定之外的几点小建议最后分享几个实际操作中的小经验不一定在书上能看到。我一般在标定完成后会把左右相机的映射表、内参、外参全部序列化保存下来文件名带日期和镜头型号。因为镜头拧动过、相机被碰过参数就会变这时候需要一个干净的初始化参数作为参考。重新标定时我会先用旧参数暴力初始化再用新采集的数据精调这样收敛速度快很多尤其是双目外参R和T。另外stereoCalibrate返回的旋转向量和平移向量单位要分清。旋转向量通过cv2.Rodrigues转为旋转矩阵平移向量的单位取决于标定板边长输入的单位。如果棋盘格边长输入的是毫米那T就是毫米Q矩阵和深度输出也是毫米。颜色和尺度单位不一致是三维重建项目里最常见的低级错误之一。还有一个小技巧判断标定质量不要太依赖单一指标。除了重投影误差我还会检查左右校正图的水平边缘是否平行再用实际场景的地面、墙面直线做一次直线度验证。真正的工业项目里标定板上的角点误差只是整个系统误差的一部分镜头温漂、结构形变、安装震动都会带来额外误差。做完一套标定流程后心里要清楚这套系统的精度极限在哪里而不是纸面数据漂亮就觉得万事大吉。双目视觉立体标定与校正说难不难说简单也不简单关键在于把每一步的原理吃透再通过足够多样化的采集数据去验证自己的参数。把这套流程反复走几遍后面再做立体匹配和测距你会明显感觉到整个pipeline稳了很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进