ARTICLE DETAIL

资讯详情

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

双目相机标定从入门到实践:棋盘格到点云全流程指南

双目相机标定从入门到实践:棋盘格到点云全流程指南 棋盘格、角点检测、极线校正、视差图、点云这几个词放在一起很多人第一反应就是“又要面对一大堆数学公式”。但实际做过一次双目相机标定后你会发现这个流程完全可以玩着学——只要搞懂每一步在解决什么问题代码基本就是套OpenCV现成接口真正消耗时间的反而是采集照片和调参数这些看似不起眼的环节。这篇博文的目标很明确用最短的路径把从棋盘格准备到3D点云生成的完整流程走通每一步都附带可以直接运行的Python代码。视频、3D测量、机器人抓取只要你想使用双目相机获取三维数据这套东西大概率都能帮到你。我会把自己踩过的坑、试出来的参数、以及那些文档里很少写清楚的经验都放进来尽量让你读完就能照着复现。1. 双目相机标定到底在标什么先搞懂这几个坐标系标定这个词听起来高大上本质其实很朴素相机是把三维世界压缩成二维像素的设备压缩过程丢失了深度信息双目视觉的使命就是把深度信息“找回来”。而标定就是为这一步搭建好数学基础。1.1 相机成像的数学模型针孔模型与像素坐标先看单个相机。三维空间里的一个点投影到图像平面上常用针孔模型描述它会经历世界坐标→相机坐标→图像坐标→像素坐标的变化。实际使用中我们一般不关心世界坐标到相机坐标的变换那是外参的活更关心的是像素坐标和内参的关系。内参矩阵长这样K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]这里的fx、fy是焦距在像素单位下的值cx、cy是主点坐标。用“度数”来类比的话fx、fy决定了画面中物体被“放大”了多少倍cx、cy则告诉你画面的正中心落在哪个像素上——大多数情况下它会接近图像中心但镜头装配误差会导致偏移所以必须标定出来。真实镜头还会引入畸变主要是径向畸变和切向畸变。径向畸变就是大家熟悉的“桶形/枕形失真”越靠近图像边缘越明显用k1、k2、k3描述切向畸变是镜头与传感器平面不平行导致的用p1、p2描述。标定单目相机本质就是把这些参数估计出来之后就能把畸变的图像恢复成“针孔模型应该输出的样子”。1.2 双目标定的核心目标左相机到右相机的变换单目标定解决的是“每只眼睛的度数”问题但双目系统还必须解决“两只眼睛的相对位置”问题。给定左相机坐标系下的一个点它对应在右相机坐标系里是什么位置这个关系由旋转矩阵R和平移向量T描述也就是从左相机到右相机的刚体变换P_right R * P_left T。你可能听过本质矩阵E、基础矩阵F这些概念stereoCalibrate也会把它们一并算出来。但对最终重建点云的任务来说最关键的是R和T因为后续的立体校正极线校正要靠它们把左右图像的极线拉成水平直线让同名点只在水平方向偏移这样才可以用一维搜索高效匹配像素。评价一个双目标定结果好不好除了看单目重投影误差还要看标定出的R和T是否符合物理直觉双目相机通常是水平放置的所以T应该以x方向位移为主、y方向接近0R应该非常接近单位矩阵。如果你标定出的T_y大得离谱先别急着分析代码检查一下是不是相机本身装配就歪了。2. 棋盘格标定实操从打印一张纸到采集合格的图像对很多人把标定当成“在代码里随便调一下”其实最影响标定质量的环节根本不在代码里而在采集图片这一步。这一节讲透怎么准备标定板、怎么拍出足够“有信息量”的图像以及怎么把不合格的数据剔除干净。2.1 标定板的选择与打印这些细节直接影响最终精度棋盘格是最常见的标定板OpenCV对它的支持最成熟。打印前先确认两件事内角点数量和实际边长。比如“9x6”指的是9列6行的内角点对应的棋盘格实际是10x7个格子。代码里findChessboardCorners传入的是内角点数。边长必须用卡尺精确测量单位毫米然后准确填进代码。别随便估个大概值因为边长直接进入尺度因子假设你标定板的格子实际是30mm你却填了29mm重建出来的物体尺寸会整体偏小约3%在1米距离上就是3厘米的误差工业场景根本没法接受。打印的注意事项尽量用相纸、哑光纸避免普通A4纸受潮变形贴在完全平整的硬板上比如亚克力板或者铝合金板如果条件允许覆一层膜防反光。如果是自己在实验室用我建议标定板面积稍微大一点让它在图像里能占据足够大的区域这样角点亚像素定位的稳定性会好很多。2.2 采集图像的“黄金法则”每张照片都要有“信息量”很多初学者拿着标定板对着相机随便拍二三十张结果标定出来的内外参一塌糊涂。问题往往出在图像“太像了”——棋盘格永远在画面正中间永远正对着相机永远在同一距离。合格的标定图像集要覆盖足够的视角变化。我的做法是把视野水平分成左、中、右三个区域每个区域再覆盖近、中、远三个距离总共9组组合每组拍3-5张共30对左右。每张照片里棋盘格的俯仰角、偏航角都要有变化最好让棋盘格相对于相机有明显倾斜30度级别这样能有效约束内外参数的解空间。还有个关键原则相机固定移动标定板不要拿着相机绕标定板转圈。双目相机两个镜头之间一旦因为晃动发生位移标定外参就会失真。另外确保左右相机标定板上没有反光点、边缘没有被裁切棋盘格至少占图像面积的1/3以上。2.3 角点检测与剔除不合格角点的实战技巧角点检测用cv2.findChessboardCorners就能搞定但实操中经常检测失败或检出错误顺序的角点。我常用的检测标志组合是cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE这两个标志能显著提升光照不均、边缘阴影下的检测成功率。检测到整数像素角点后还要用cornerSubPix做亚像素细化搜索窗口一般设为(11,11)终止条件设为迭代30次、精度0.001。剔除不合格角点是标定流程里容易被忽略但极其重要的一步。我一年前第一次做完整流程时就因为没清理坏数据重投影误差一直下不来后来才发现是某几对图像的角点顺序不一致。推荐按下面的顺序过滤数量过滤左右图像任意一张没有检测到全部角点直接丢弃该图对顺序一致性过滤检测出的角点必须按从左到右、从上到下的行列顺序排列否则后续标定会把对应关系搞错重投影误差过滤用所有图像先跑一次单目标定计算每张图的RMS误差剔除超过1.5倍中位数的图像对再重新标定几何合理性过滤检查棋盘格四个角点围成的四边形面积面积过小说明标定板太远或角度过偏保留价值不大这套过滤逻辑不仅能提升标定精度还能让stereoCalibrate的迭代收敛更稳定。下面这段代码就是检测加过滤的骨架import cv2 import numpy as np import glob CHESS_COL 9 CHESS_ROW 6 SQUARE_SIZE 23.0 objp np.zeros((CHESS_COL * CHESS_ROW, 3), np.float32) objp[:, :2] np.mgrid[0:CHESS_COL, 0:CHESS_ROW].T.reshape(-1, 2) * SQUARE_SIZE obj_points [] img_points_left [] img_points_right [] images_left sorted(glob.glob(images/left/*.jpg)) images_right sorted(glob.glob(images/right/*.jpg)) assert len(images_left) len(images_right) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for img_l, img_r in zip(images_left, images_right): gray_l cv2.imread(img_l, 0) gray_r cv2.imread(img_r, 0) flags cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE ret_l, corners_l cv2.findChessboardCorners(gray_l, (CHESS_COL, CHESS_ROW), None, flags) ret_r, corners_r cv2.findChessboardCorners(gray_r, (CHESS_COL, CHESS_ROW), None, flags) if not (ret_l and ret_r): print(f跳过 {img_l} 或 {img_r}角点检测失败) continue corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points_left.append(corners_l) img_points_right.append(corners_r)3. 完整Python代码从单目标定到立体校正代码部分我会按实际项目习惯从环境准备讲起然后给出单目、双目标定和立体校正的完整实现。建议你先把环境配好再跟着跑。3.1 环境准备与依赖安装基础依赖是OpenCV、NumPy、Matplotlib点云可视化用Open3D。安装命令pip install opencv-contrib-python numpy open3d matplotlib注意opencv-python和opencv-contrib-python不能同时安装否则会有符号冲突。推荐只装opencv-contrib-python因为它包含了SGBM等扩展模块功能更全。Open3D比较大如果只想跑通核心流程可以先不装等生成点云可视化时再补。3.2 单目标定先让每只眼睛“度数”准确单目标定可以复用上一节的角点检测结果核心就一行cv2.calibrateCamera。注意传给它的图像尺寸参数是(宽, 高)即gray.shape[::-1]很多人在这里传错导致报错或者结果异常。ret_l, mtx_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera( obj_points, img_points_left, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, rvecs_r, tvecs_r cv2.calibrateCamera( obj_points, img_points_right, gray_r.shape[::-1], None, None) print(左相机重投影误差:, ret_l) print(左相机内参:\n, mtx_l) print(左相机畸变系数:, dist_l.ravel()) print(右相机重投影误差:, ret_r) print(右相机内参:\n, mtx_r) print(右相机畸变系数:, dist_r.ravel())calibrateCamera返回的第一个值ret是RMS重投影误差单位是像素我一般要求小于0.3。如果误差在0.5以上基本可以断定数据里有不合格图像回到第2.3节做剔除而不要指望靠算法硬吃。3.3 双目标定计算左右相机之间的R和T双目标定是在单目标定基础上用同一组角点对估计左右相机的外参关系。代码flags cv2.CALIB_FIX_INTRINSIC criteria_stereo (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) ret_stereo, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_left, img_points_right, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteriacriteria_stereo, flagsflags) print(双目标定误差:, ret_stereo) print(R:\n, R) print(T:\n, T)CALIB_FIX_INTRINSIC的含义是固定单目标定得到的内参只优化R和T。这个flag在单目结果比较可信时能减少优化变量、提高稳定性。如果采集图像覆盖不充分放开内参让它们一起优化反而容易收敛到离谱的解。R和T的物理意义一个三维点在左相机坐标系下坐标是X_left映射到右相机坐标系为X_right R * X_left T。之后stereoRectify会根据R和T计算出左右相机各自新的旋转矩阵R1、R2以及新的投影矩阵P1、P2还有把视差转成三维坐标的Q矩阵R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha1) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1)alpha参数决定校正后图像的保留范围alpha0时图像会裁掉非公共区域alpha1时保留全部像素代价是边缘会有明显畸变残留和拉伸感。如果只关心中央区域alpha1很直观如果追求高质量视差图alpha0可以减少无用计算。校正后的图像可以直接用remap生成left_rect cv2.remap(gray_l, map1_l, map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(gray_r, map1_r, map2_r, cv2.INTER_LINEAR)想验证极线是否对齐最简单的方法是把左右校正图拼在一起每隔几十像素画一条水平线观察棋盘格和场景边缘是否在同一条水平线上。如果明显错位说明标定或矫正参数出了问题。3.4 把中间结果保存好别让标定成为一次性工程标定一次要采集几十张图还可能要反复调整非常费时间。把关键参数保存到npz文件里之后无论做实验还是换机器直接加载即可np.savez(stereo_params.npz, mtx_lmtx_l, dist_ldist_l, mtx_rmtx_r, dist_rdist_r, RR, TT, R1R1, R2R2, P1P1, P2P2, QQ)下次使用时with np.load(stereo_params.npz) as data: mtx_l data[mtx_l] dist_l data[dist_l] # 按需取出其他参数这里有个重要提醒标定结果只对“标定时的那台相机、那个焦距、那个分辨率”有效。如果你调整了镜头焦距、更换了相机、或者改变了采集分辨率必须重新标定旧的npz文件直接作废。4. 从视差图到3D点云深度信息如何真正“算”出来极线校正完成之后左右图像已经严格水平对齐。此时求深度就变成了一维问题左图上某个像素在右图上只可能出现在同一行里找到它的水平位移视差就能利用三角测量原理算出距离。4.1 视差计算原理一个公式看懂深度双目测距的核心公式是Z f * B / dZ是深度f是焦距像素单位B是基线毫米d是视差像素。可以看出深度和视差成反比离相机越近视差越大越远视差越小且趋近于0。这也解释了为什么双目相机对远处目标的深度精度会越来越差。OpenCV里最常用的立体匹配算法是SGBM半全局块匹配。它本质是在能量最小化框架下做像素匹配比简单的块匹配更平滑对弱纹理区域也有一定鲁棒性。基本用法sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities128, blockSize11, P18 * 3 * 11 ** 2, P232 * 3 * 11 ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, ) disparity sgbm.compute(left_rect, right_rect).astype(np.float32) / 16.0注意compute返回的是CV_16S类型也就是short单位是1/16像素。必须先转成float32再除以16才是真实视差值。忘记除以16是新手最容易犯的错误。关于参数我的经验值是numDisparities取64或128如果目标物体距离很近可以增大blockSize取7到11之间太小噪声大太大容易丢失物体边缘uniquenessRatio取10左右它控制匹配唯一性的置信度P1和P2是平滑惩罚项通常按8 * 3 * blockSize^2和32 * 3 * blockSize^2来设。4.2 用Q矩阵重投影到三维点云的诞生立体校正阶段的Q矩阵就是干这个的。reprojectImageTo3D函数把视差图直接转换成三维坐标数组points_3d cv2.reprojectImageTo3D(disparity, Q, handleMissingValuesTrue)points_3d的shape是(H, W, 3)最后一维分别是X、Y、Z坐标。Z的单位和标定时棋盘格边长的单位一致如果你SQUARE_SIZE填的是毫米Z单位就是毫米填米就是米。记得统一好单位否则后面做测量或者机械臂抓取时会出现数量级错误。视差图里会有无效像素匹配失败、被遮挡的区域一般表现为视差非正或极大需要过滤mask (disparity 0) (disparity 128) valid_xyz points_3d[mask] valid_color right_rect[mask] # 或者用左图取决于你想把哪个作为纹理来源再按距离过滤一些太远或太近的噪点dist np.linalg.norm(valid_xyz, axis1) valid_xyz valid_xyz[(dist 200) (dist 5000)]具体距离范围根据你的相机和场景设置。4.3 点云可视化Open3D快速展示有了三维坐标和对应的RGB颜色点云其实已经“存在”了。用Open3D展示非常直接import open3d as o3d pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(valid_xyz.reshape(-1, 3)) pcd.colors o3d.utility.Vector3dVector(valid_color.reshape(-1, 3) / 255.0) # 可选体素降采样减少点数提高渲染流畅度 pcd_down pcd.voxel_down_sample(voxel_size3.0) o3d.visualization.draw_geometries([pcd_down])voxel_down_sample里的voxel_size单位和你点云坐标单位一致3.0毫米的体素对中距离场景来说既能降噪又能保持轮廓。如果点云出现了“纸张褶皱”一样的效果即同一个平面上有大量周期性起伏优先检查两个地方SGBM的blockSize是不是太小以及标定板的边长是否填错。褶皱往往是视差噪声和标定误差叠加的体现不是点云本身的bug。5. 常见问题与排查记录标定踩坑实录这块内容是我觉得整篇博客最值钱的部分。以下问题基本是我在不同项目里真实踩过的坑按出现频率从高到低排列。5.1 畸变校正后图像边缘被拉得很难看现象remap之后边缘区域有强烈拉伸、模糊甚至出现“旋转”感。原因基本是畸变系数估计不准或者标定板图像没有覆盖到画面边缘。解法是回到采集环节专门补拍几组标定板靠近画面四角、且明显倾斜的图像重新标定。另外alpha参数从1调到0能显著减少边缘裁剪和拉伸但会损失边缘像素适合对边缘区域要求不高的场景。5.2 重投影误差很小但视差图质量差这个现象最容易让人困惑。重投影误差小说明棋盘格标定自洽度好但视差图差可能另有原因。常见因素左右图像亮度不一致SGBM对光照差异敏感。解决先做直方图匹配或者用同一曝光参数采集场景纹理太少。SGBM依赖纹理信息白墙、纯色地板都会导致大片空洞或错误匹配基线太短深度分辨率不足。基线决定了深度误差随距离放大倍数物理限制无法靠算法救回来SGBM参数不合理比如blockSize太大导致边缘模糊排查顺序建议先看校正图像是否对齐再看视差图上是否有大面积黑色空洞最后调参。5.3 角点检测失败怎么办角点检测失败的典型原因有三个棋盘格太小、光照太强导致反光、标定板太斜。可以尝试ret, corners cv2.findChessboardCorners( gray, (CHESS_COL, CHESS_ROW), None, cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE)如果还不行换cv2.findChessboardCornersSB这个接口基于Blob检测对模糊、低对比度、暗光的鲁棒性更好。真不行就重新采集图像别在代码上死磕。5.4 双目相机的硬件注意事项最后提一个很多人忽略的点双目相机在标定过程中千万不要碰镜头、调整焦距、或者改变两个镜头之间的距离。如果是带自动对焦的相机一定要先锁焦再标定如果是变焦镜头锁定在固定焦距上。任何结构变化都会让标定结果失效这也是为什么工业场景普遍使用固定基线、固定焦距的专用双目模组。我个人在实际操作中的体会是标定不是一次性的“打怪通关”而是一个反复迭代的过程。第一次标定结果可能不尽如人意别急着怀疑代码回到图像采集环节补拍、剔除、重新跑往往比调参数更能解决问题。养成把每轮标定的图像集、参数、误差都记录下来的习惯会让你后续排查问题省掉大量时间。这套流程跑顺之后再去看双目视觉的其他环节比如立体匹配优化、深度滤波、SLAM建图都会轻松很多。
返回列表