ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

双目视觉深度感知与三维重建:从相机标定到点云生成的完整工程实战

双目视觉深度感知与三维重建:从相机标定到点云生成的完整工程实战 简介这套基于双目摄像头的立体视觉深度感知与三维重建算法系统面向计算机视觉学习者、机器人导航与自动驾驶领域开发者完整覆盖从相机标定、立体匹配、视差计算到深度图生成、点云重建再到目标检测与跟踪的算法链路。资源共132个文件其中包含50个JPG图像样本、20个Python算法脚本另有HTML/CSS/JS等前端展示页面、TXT说明文档以及少量字体与动态库文件整体压缩包仅8.12MB便于快速部署与代码阅读。已有229人学习下载。内容以立体匹配和视差计算为核心结合多视角几何完成相机标定与校正通过深度图与点云重建直观展示三维场景同时提供目标检测与跟踪、实时视频处理的参考实现并配套示例图像和可运行脚本帮助读者快速理解算法原理并验证效果。资源适合用于课程设计、毕业设计或工程预研可在此基础上针对机器人导航、自动驾驶等场景做进一步定制。1. 从测距到三维重建这套双目视觉系统包解决了什么在机器人导航和自动驾驶环境里单目测距最让人头疼的地方是尺度不确定同一辆车换个视角测出来的距离可能差出好几米。双目摄像头不需要深度学习来猜尺度它靠两幅图像之间的几何关系直接算出每个像素的深度。这套基于双目摄像头的立体视觉深度感知与三维重建算法系统把立体匹配、视差计算、深度图生成、点云重建、目标检测与跟踪、实时视频处理、多视角几何、相机标定与校正串成了一条能跑通的完整链路。它不是只贴一个SGBM函数而是从标定板拍摄到输出点云和3D目标位置的工程包。适合做机器人导航、自动驾驶感知预研或者想用一份代码把传统双目三维重建走通的人。下面按我实际拆项目的顺序来写先立住几何原理再给标定、匹配、深度、点云、跟踪每个环节的落地参数和坑。2. 把两幅图像变成一副深度图立体匹配、视差计算与深度公式2.1 极线约束与视差搜索范围双目视觉的最底层依据是多视角几何中的极线几何。左右两个摄像头光心之间的距离叫基线B空间里的任意一点P在左右图像上投影点的横坐标差就是视差d。如果两幅图像没有做校正找同名点需要在二维平面里搜计算量很大也容易匹配到错误纹理。所以这套系统里第一步并不是匹配而是用一种叫极线校正的变换把左右图像重投影到一个共同的理想平面上。极线校正之后同一个三维点的左右投影位于同一水平线上匹配搜索从二维变成一维速度直接提升一个数量级。立体匹配要做的事情就是在左图某个像素的同一行向右搜索找到右图最相似的位置。这个搜索范围正是OpenCV里numDisparities参数设定的视差值。搜索范围不仅决定计算量还直接决定最近可测距离。根据深度公式Z f * B / d当视差达到最大值d_max时深度达到最小值。举个例子一台相机fx约700像素基线0.12m如果numDisparities设成96那么最近深度约为700 * 0.12 / 96 0.875m如果设成192最近深度可以到0.4375m但匹配计算量翻倍近距离低纹理区域还会更容易出现错误视差。我做室内机器人导航时一般把numDisparities控制在96到128之间因为底盘前方盲区本来就有安全距离太近的值对停障没意义反而增加噪声。如果是自动驾驶前视基线更长、工作距离更远numDisparities可以小一些但需要配合更高的分辨率和更大的基线。这是一组需要根据相机参数和场景反复推的参数不存在一个万能数字。下面这个表格是我常用的初始选择场景基线工作距离初始numDisparities室内机器人0.06~0.12 m0.5~5 m96园区无人车0.12~0.3 m1~20 m128自动驾驶前视0.3~0.6 m2~100 m64~96注意numDisparities在OpenCV里必须是16的整数倍这是SGBM实现里的一个硬约束调参前先确认这个数能整除16否则函数不会报错但结果会不对。2.2 从BM到SGBM匹配代价与平滑惩罚参数立体匹配算法很多但这套系统里最常用也最容易落地的是半全局块匹配SGBM以及在低算力设备上的BM。BM是朴素块匹配它对每个像素取一个固定窗口在搜索范围内比较像素块灰度差异取最小代价对应的视差。速度快但每个像素独立决策没有考虑邻域视差连续性出图的颗粒感很重低纹理区域容易出现成片错误。SGBM则在块匹配的基础上引入了代价聚合沿着多个方向传播代价对相邻像素之间视差变化施加惩罚把“平滑性”这个先验加进求解过程。OpenCV里的实现是StereoSGBM。下面这段代码是创建matcher的函数参数都做了注释import cv2 def create_sgbm(max_disp96, block_size11): # max_disp 需要能被16整除实际搜索范围是0到max_disp-1 sgbm cv2.StereoSGBM_create( minDisparity0, numDisparitiesmax_disp, # 视差搜索范围 blockSizeblock_size, # 匹配块大小必须是奇数 P18 * 3 * block_size ** 2, # 相邻视差变化1时的惩罚 P232 * 3 * block_size ** 2, # 相邻视差变化1时的惩罚 disp12MaxDiff1, # 左右一致性检查最大偏差 preFilterCap63, # 预处理截断值防止梯度太大 uniquenessRatio15, # 最小判别度越大越保守 speckleWindowSize100, # 连通域噪声过滤窗口 speckleRange1, # 连通域内允许的视差波动 modecv2.STEREO_SGBM_MODE_SGBM ) return sgbm这里最关键的是P1和P2。P1惩罚相邻像素视差差1的情况P2惩罚视差跳变更大的情况。P2设得越大深度图越平滑但会把细小的物体边缘吞掉P2设得太小低纹理区域又会出现大量横向裂缝。经验上P2取P1的4到8倍并且P1/P2都和blockSize的平方相关所以改了blockSize之后这两项要跟着改。uniquenessRatio代表最小代价和次小代价的差值比例它控制“这个匹配是否足够唯一”设成15能在多数场景下抑制错误匹配但低纹理区域会更容易变成无效像素这一点在后面避坑章节展开。BM模式参数类似但计算更快。在树莓派或Jetson Nano这类设备上如果640x480分辨率需要实时可以先跑BM验证管线再用SGBM追求质量。不要一上来就上SGBM那样性能瓶颈会掩盖标定和相机本身的很多问题。2.3 视差转深度fx、基线与DOFFS拿到视差图后深度可以用一个简单公式换算Z f * B / d。这里f是校正后图像的焦距fx以像素为单位B是双目基线单位取决于你想要输出的深度单位d是浮点视差。需要特别注意的是OpenCV中的SGBM输出是定点数内部计算精度是16位实际视差要除以16也就是用disparity.astype(np.float32) / 16.0。如果忘了除以16所有深度都会是真实值的1/16这个错误非常隐蔽因为距离看起来“好像”是近了一些但曲线关系不对。还有一点是DOFFS也就是两个相机主点在x方向上的偏移。在使用stereoRectify生成的Q矩阵时DOFFS会被包含在Q矩阵里。如果自己用fx和baseline写代码通常可以近似忽略DOFFS但在近距、大基线的情况下它会带来几个像素级的视差偏移深度误差会被放大。更稳妥的做法是用Q矩阵做三维反投影而不是自己拼公式。下面是常用的深度图生成代码import numpy as np def disparity_to_depth(disp_raw, fx, baseline, min_depth0.2, max_depth50.0): # disp_raw 是SGBM的原始输出int16格式 disp disp_raw.astype(np.float32) / 16.0 depth np.zeros_like(disp, dtypenp.float32) valid disp 0 # 深度单位由baseline单位决定如果baseline是米depth就是米 depth[valid] (fx * baseline) / disp[valid] # 加上物理约束过滤掉无穷远和过近的错误点 depth[depth min_depth] 0 depth[depth max_depth] 0 return depth这段代码后面直接接可视化或点云生成。min_depth通常设为0.2米因为双目在非常近的距离上视差大、匹配窗口容易覆盖不同深度结果不可信max_depth则根据场景设室内设10米自动驾驶设80米已经足够超过的部分视差接近亚像素测不准不如直接置零。代码里用了np.zeros_like而不是直接令无效区域为nan是为了避免后面的中值滤波和点云生成受到NaN干扰。如果你想保留无效区域做分析可以另外保存mask不要把NaN混进计算链路。3. 相机标定与校正重投影误差、棋盘格规格与立体校正检查3.1 棋盘格标定板的规格与拍摄方案这套系统能输出多准的点云很大程度不取决于算法而取决于相机标定与校正这一环节做得多细。很多人把OpenCV自带的示例图片打出来就标定结果内外参误差大后续立体匹配再努力也白搭。建议按下面的规格准备标定板内角点选9x6也就是棋盘格实际打印10x7格子边长至少30mm并且用卡尺量一下打印出来的实际尺寸不要把设计尺寸直接填进代码。标定板要用胶水贴在平整的硬板或玻璃上有折角的标定板会让角点坐标产生系统误差。拍摄数量不是越多越好至少20对以上才稳定。我一般拍30对并遵循一个“九宫格”拍摄原则标定板出现在画面的中心、左上、右上、左下、右下、上下边缘和左右边缘在每个位置都让标定板左右旋转30度、上下俯仰20到30度。为什么必须这样拍单目标定求解的是内参和畸变如果标定板永远正面朝向相机旋转矩阵的可观测性就不足解出来的fx和fy高度相关畸变系数也会漂移重投影误差看着不高但外参很不稳定。拍摄时还需要注意左右摄像头要尽量同时采集并且标定板是同一个位置。如果左右相机没有硬件同步至少用同一个型号、同一曝光参数并在拍完一轮后筛选左右图都清晰、都没有运动模糊的图像对。下表是我建议的参数项目推荐值内角点数9 x 6格子边长实测30~35 mm图像对数25~30覆盖位置九宫格 旋转/俯仰画面占比15%~40%左右图曝光固定关闭自动曝光这个表格看起来简单但每一条都对应一个具体的标定坑。自动曝光不关左右图亮度差很大角点亚像素提取会不一致画面占比始终很大畸变参数就拟合不好。把这里做好了后面立体校正的精度才有讨论意义。3.2 双目内外参标定与立体校正脚本标定流程一般是先分别做左右单目标定再做双目标定最后stereoRectify生成校正映射表。OpenCV里的典型实现如下import cv2 import numpy as np # pattern_size是内角点数square_size是实测格子边长(mm) def calibrate_stereo(left_imgs, right_imgs, pattern_size(9, 6), square_size30.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 objpoints [] imgpointsL [] imgpointsR [] for imgL, imgR in zip(left_imgs, right_imgs): grayL cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY) grayR cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY) retL, cornersL cv2.findChessboardCorners(grayL, pattern_size, None) retR, cornersR cv2.findChessboardCorners(grayR, pattern_size, None) if retL and retR: # 亚像素精细化提高角点定位精度 criteria (cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 30, 0.001) cornersL cv2.cornerSubPix(grayL, cornersL, (11, 11), (-1, -1), criteria) cornersR cv2.cornerSubPix(grayR, cornersR, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpointsL.append(cornersL) imgpointsR.append(cornersR) retL, mtxL, distL, _, _ cv2.calibrateCamera(objpoints, imgpointsL, grayL.shape[::-1], None, None) retR, mtxR, distR, _, _ cv2.calibrateCamera(objpoints, imgpointsR, grayR.shape[::-1], None, None) # 双目标定使用已求出的内参作为初值 retS, mtxL, distL, mtxR, distR, R, T, E, F cv2.stereoCalibrate( objpoints, imgpointsL, imgpointsR, mtxL, distL, mtxR, distR, grayL.shape[::-1], criteria(cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-6), flags0 ) return retL, retR, retS, mtxL, distL, mtxR, distR, R, T这段代码里单目标定返回的retL和retR是重投影误差的RMS值单位是像素。双目标定返回的retS同样评估立体重投影误差。如果retS超过0.8像素我不会继续往下做而是先检查是否有标定板的图像对没找全、标定板是否有折痕、左右图曝光差异是否过大。cornerSubPix的窗口大小取(11,11)比默认的(5,5)更稳窗口太大又容易跨过角点边缘这个值配合30次迭代对9x6棋盘格是稳妥的。拿到R和T后还要做立体校正。立体校正的关键参数是alpha它控制校正后图像允许裁剪多少。alpha0表示尽可能裁剪掉不规则边缘结果是最干净的无畸变图像alpha1表示保留原图所有像素只做矫正但边缘会出现拉伸变形。点云重建我一般用alpha0因为后续SGBM匹配和深度计算都基于校正图不需要保留边缘的冗余视野。R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtxL, distL, mtxR, distR, grayL.shape[::-1], R, T, alpha0 ) map1L, map2L cv2.initUndistortRectifyMap(mtxL, distL, R1, P1, grayL.shape[::-1], cv2.CV_16SC2) map1R, map2R cv2.initUndistortRectifyMap(mtxR, distR, R2, P2, grayR.shape[::-1], cv2.CV_16SC2)initUndistortRectifyMap生成的映射表类型CV_16SC2是OpenCV针对remap优化过的定点格式比CV_32FC1更快实时视频处理里一定用这个类型。3.3 校正后必须检查的四项指标标定脚本跑完不代表校正就合格。我每次都会做下面四项检查前两项直接决定立体匹配的成败。第一项是单靶和双靶的重投影误差。单目retL/retR应该小于0.3像素双目的retS小于0.5像素。如果单目小于0.3但双目大于0.8问题往往出在左右相机外参的一致性上比如左右标定板图像对不是同一时间拍的或者标定板在两边图像中位置差异过大。第二项是极线对齐误差。校正后左右图像的同一个角点应该在同一行垂直方向的像素差越小越好。可以用OpenCV再检测一次棋盘格角点来算def check_epipolar(left_rect, right_rect, pattern_size(9, 6)): retL, cornersL cv2.findChessboardCorners(left_rect, pattern_size, None) retR, cornersR cv2.findChessboardCorners(right_rect, pattern_size, None) if retL and retR: err_y np.mean(np.abs(cornersL - cornersR)[:, 0, 1]) print(fvertical epipolar error: {err_y:.3f} px)这个误差在0.5像素以内SGBM才拿得到稳定视差。如果误差超过1像素之前拍的标定板角度不够或者stereoCalibrate时flags设置得太随意建议重新标定而不是手动平移图像来“补偿”。第三项是Q矩阵里的平移向量。Q是由stereoRectify生成的4x4矩阵它的第三行第四列是-1/Tx所以Q[3][2]的倒数就是基线Tx单位与标定板尺寸单位一致。如果我把标定板尺寸填成毫米Tx就是毫米之后生成的点云坐标就是毫米。很多项目点云x坐标变小或深度变成负值都是这里出问题。第四项是已知距离验证把标定板放在1米、2米、3米等几个固定位置用深度图取标定板中心深度误差应控制在1%到3%以内。如果远处误差越来越大通常是fx标定偏了或者基线不准如果近处误差大优先怀疑视差匹配参数而不是标定。4. 深度图生成与三维重建实时视频流、点云输出与目标跟踪4.1 实时视频流映射表只算一次双线程跑满30fps很多初学者把标定和立体匹配写在同一个循环里每帧都调initUndistortRectifyMap和stereoRectify这是实时视频处理里最不应该犯的错误。相机标定与校正只做一次校正映射表map1L、map2L等是固定不变的只有在前视摄像头被重新安装或者镜头聚焦改变时才需要重做。在实时循环里每帧只需要执行remap和SGBM compute。我的典型实时管线是这样组织的采集线程从双目摄像头读取左右原始图做灰度转换和remap处理线程拿校正后的左右图做SGBM计算、后处理和点云生成两个线程之间通过一个只保存最近一帧的共享队列传递数据避免处理卡顿时旧帧堆积导致延迟越来越大。# 实时循环核心映射表事先已经生成好 capL cv2.VideoCapture(0) capR cv2.VideoCapture(1) # 固定曝光避免左右图亮度不一致 capL.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) capR.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) while True: retL, frameL capL.read() retR, frameR capR.read() if not retL or not retR: continue gL cv2.cvtColor(frameL, cv2.COLOR_BGR2GRAY) gR cv2.cvtColor(frameR, cv2.COLOR_BGR2GRAY) rL cv2.remap(gL, map1L, map2L, cv2.INTER_LINEAR) rR cv2.remap(gR, map1R, map2R, cv2.INTER_LINEAR) disp_raw sgbm.compute(rL, rR) disp disp_raw.astype(np.float32) / 16.0 # 后续深度/点云处理这里remap的插值选INTER_LINEAR不要用INTER_CUBIC因为立体匹配对边缘敏感三次卷积反而会在棋盘格边缘产生过冲。SGBM的compute对输入灰度图要求是8位或16位校正图保持8位即可。如果程序跑不满30fps先看瓶颈在哪。640x480分辨率下SGBM默认参数大约要20到40毫秒1080p直接跑会掉到个位数帧率。常见做法是把分辨率限制在640x480或者先降采样到一半尺寸做匹配再把视差图放大回去。放大视差图时不要用双线性插值而是用最近邻因为视差是离散值双线性插值会生成不存在的亚像素视差。4.2 深度图后处理空洞、椒盐噪声与置信度截断SGBM直接输出的视差图不会很干净典型问题有三种匹配失败产生的黑色空洞、低纹理区域的椒盐状错误、物体边缘的横向拖尾。空洞一般在遮挡区域和反光区域这些地方左右视图确实看不到同一个点属于物理意义上的无效深度。椒盐噪声则是匹配代价不唯一造成的深度看起来在做无规则跳变。我处理顺序是这样的先用uniquenessRatio和disp12MaxDiff生成的掩膜把不可靠像素标成0再做中值滤波最后补空洞。因为如果先填空洞噪声会被当成有效深度扩散出去。# 中值滤波前先做有效性掩膜 disp disp_raw.astype(np.float32) / 16.0 mask disp 0 # 中值滤波窗口5能去掉孤立椒盐点又不至于磨掉边缘 disp_smooth cv2.medianBlur(disp.astype(np.float32), 5) disp_smooth[~mask] 0 # 空洞填充对每个无效像素用闭运算把小块空洞填上 filled cv2.morphologyEx(disp_smooth, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8))medianBlur对CV_32F是支持的但要注意它会改变NaN值所以我先把无效像素置0再滤波。morphologyEx的闭运算可以把小的空洞填上窗口不要超过3x3否则靠近前景边缘的空洞会错误地填成背景深度。如果你使用的是嵌入式设备可以把这两步换成OpenCV的快速滤波或者直接用专用深度传感器上的后处理模块。空洞填充之后还有一个重要操作是置信度截断。对于深度值大于某个阈值或者视差值小于2像素的像素直接置0。因为这时候视差只相差1个像素对应的深度误差可以达到数米这些“看起来有值”的深度比空洞更危险放进点云后会形成一大片很远的杂点。我一般把视差小于3的像素全部设为无效宁少勿错。4.3 点云重建Q矩阵、坐标系与降采样有了干净的视差图接下来就进入三维重建环节。OpenCV提供了现成的reprojectImageTo3D函数它用Q矩阵把每个有效像素反投影为三维空间坐标。Q矩阵来自stereoRectify包含主点偏移、焦距和基线信息。下面这段代码可以把深度图转成Open3D的点云对象import cv2 import numpy as np import open3d as o3d points_3d cv2.reprojectImageTo3D(disp, Q) # disp是浮点视差 mask disp 0 points points_3d[mask] colors cv2.cvtColor(left_rect, cv2.COLOR_GRAY2RGB)[mask] pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points.reshape(-1, 3)) pcd.colors o3d.utility.Vector3dVector(colors.reshape(-1, 3) / 255.0) # 体素降采样保留每0.01m立方体内的一个点 pcd_down pcd.voxel_down_sample(voxel_size0.01)reprojectImageTo3D返回的三通道数组通道顺序是X、Y、Z。坐标系定义是原点在左相机光心Z轴指向前方Y轴向下X轴向右这里“前方”由Q矩阵和stereoRectify的旋转决定。如果深度图是校正后的那么点云坐标直接对应校正前左相机的物理坐标体系。voxel_down_sample的voxel_size参数很关键。室内机器人导航0.01m足够细室外自动驾驶或园区场景0.03~0.05m更好能明显减少点数又不丢失结构。如果直接把原始深度图所有有效点都输出一个640x480的深度图可能产生超过十万个点后续处理会非常吃力。如果你不知道自己的坐标单位是什么可以用标定板实测距离来验证。放一个已知尺寸的物体在相机前方从点云里量它的宽度如果和真实尺寸一致单位就对了。我遇到过标定板尺寸填了像素值的情况结果点云宽度比真实物体大了一百倍这就是填错单位。Q矩阵中的Tx单位与标定板尺寸单位一致这是最常见的坑。如果你后续要转向3D高斯三维重建这类更高质量重建这份点云可以作为初始点云输入但务必先确认坐标系是左相机坐标系且尺度是米。4.4 目标检测与跟踪2D检测加深度反投影比纯点云聚类更省心在机器人导航和自动驾驶环境里三维重建不是最终目的最终目的是感知目标的位置和运动趋势。这套系统把目标检测与跟踪放在点云生成之后但并不意味着一定要在三维空间里做检测。我实际更常用的路线是左图用YOLO或更轻量检测器输出2D框取框内深度可靠像素的中值作为目标深度再由相机内参反投影得到目标的3D坐标。这个方案的设计理由是纯点云聚类在低纹理墙面上会把墙面和人物粘在一起检测框则天然带了语义信息。而且2D检测框的垂直中心对应目标底部比随机取点云质心更稳定。# 假设detect_objects返回一个列表每项是(x1,y1,x2,y2,class) for box in detect_objects(left_rect): x1, y1, x2, y2 box[:4] roi_depth depth[y1:y2, x1:x2] valid_depth roi_depth[roi_depth 0] if len(valid_depth) 10: continue # 用中位数而不是均值避免框内背景杂点把距离拉偏 z np.median(valid_depth) u (x1 x2) / 2.0 v (y1 y2) / 2.0 X (u - cx) * z / fx Y (v - cy) * z / fy # 得到目标在左相机坐标系下的位置(X,Y,Z)这段反投影公式用到的cx、cy、fx、fy来自校正后的P1矩阵不是原始相机内参。如果用原始内参坐标会有一点点偏差近距离目标尤其明显。取了中值深度之后跟踪层可以简单用卡尔曼滤波状态量设为(X,Y,Z,VX,VY,VZ)假设目标匀速运动。当目标被遮挡时深度中值会跳到遮挡物上我一般会给卡尔曼预测设置一个较大的过程噪声用平滑后的轨迹做避障决策而不是直接用单帧结果。5. 避坑双目系统从开发到现场部署最常踩的五个坑5.1 标定重投影误差很漂亮但深度图全是黑洞现象单目标定重投影误差0.25像素双目标定0.4像素看起来都不错可一跑SGBM深度图大面积黑色明明有纹理的地方也匹配不出来。原因这种情况往往不是匹配参数的问题而是标定板拍摄方案太“正”了。如果所有标定板图像都正对摄像头旋转外参的可观测性不足标定出来的R虽然数值正常极线校正却存在看不见的误差另一个常见原因是左右相机的自动曝光没有关左右图亮度差异太大SGBM在一个大的搜索窗口里找不到相似匹配。解决重新拍一轮标定板强制加入30度以上的左右旋转和俯仰同时把左右相机的自动曝光、自动白平衡全部关闭用固定曝光值。如果没时间重标可以先把uniquenessRatio从15降到5speckleWindowSize从100提到200黑色空洞会明显减少但这只是临时缓解不解决几何层面的问题。5.2 远处深度整体偏小误差随着距离线性变大现象实测3米处深度显示2.5米5米处显示4.2米误差比例几乎恒定近处1米内又基本准确。原因这是典型的fx或基线参数与真实物理值不符。fx偏小会让深度Z f*B/d整体偏小基线B填错同理。另一个容易被忽略的是标定板格子边长填了设计值而不是实测值导致标定出来的fx带有系统误差。或者Q矩阵里的Tx符号不对深度会变成负值不会呈现这种线性偏小。解决先用棋盘格在1米、2米、3米各放一次记录深度值与真实值的比值。如果比值恒定直接调整标定时的square_size参数或者在后处理里把基线值乘上一个校正因子。但最靠谱的是重新实测标定板边长重新标定而不是在代码里硬凑一个比例因子。硬凑的因子只在当前距离有效换个场景就翻车。5.3 低纹理墙面上深度噪声像雪花目标检测框跟着抖现象在白色墙壁或光滑地板上SGBM输出大量跳跃的视差值2D检测框叠加在深度图上后目标框中心距离每一帧差出半米跟踪轨迹像锯齿。原因低纹理区域对任何基于局部窗口的立体匹配算法都不友好SGBM即使有平滑惩罚也救不了大面积重复纹理。另一个原因是uniquenessRatio设得太小像素点即使有两个代价接近的候选视差也照样被赋予了深度值而这些候选里至少有一个是错的。解决先提高uniquenessRatio到15以上并开启disp12MaxDiff1增加左右一致性约束错误视差会变成黑色空洞。然后对检测框内的深度值做5%到95%分位数截断再取中位数即使框内混入少量噪声点也不会把距离拉偏。坚决不要用平均值平均值对个别极端深度点太敏感。5.4 程序跑半个小时内存飙升最后卡死现象实时视频流开始时帧率正常运行一段时间后占用内存越来越大最后系统无响应。原因最常见的是在循环里重新创建映射表或大数组。比如每帧调用initUndistortRectifyMap虽然OpenCV可能内部缓存了一些资源但多次调用会造成大量中间对象。另一个问题是线程模型问题采集线程比处理线程快共享队列如果无限增长旧帧占用的深度图、点云数组全堆在内存里。解决映射表在初始化时生成一次循环里只做remap。共享队列用固定长度比如只保留最近一帧新帧到来时直接替换。Python里还要注意不要偷偷把disp和点云数组的引用保存在全局列表里比如做可视化时习惯性地把每一帧的点云append到list这个操作会让内存只涨不降改成原地更新或者直接复用同一块缓冲区。5.5 室外太阳光下深度值突然全部变成近距离现象从室内移到室外图像不调任何参数SGBM输出的深度图在路面、反光车身上出现大块近距伪深度有时候甚至整帧都变成1米以内。原因自动曝光在强光下把画面调得很亮白色高光区域左右图像都过曝像素值全部饱和在同一数值立体匹配找不到正确视差。而全局快门或曝光时间不当运动模糊也会造成同一行的图像错位。解决室外场景必须用全局快门相机关闭自动曝光和自动白平衡后手动设置曝光时间。高光区域可以在预处理阶段做gamma校正或者限制输入像素值范围preFilterCap也可以调低。更深一层部署时要设置一个图像亮度置信度掩膜当过曝像素占比超过阈值时不要相信该区域的深度宁可变成空洞也不要给自动驾驶控制层一个错误距离。这个掩膜在机器人导航里比后处理滤波更管用。6. 留一道标定场自检重投影误差、深度精度与点云置信度一起验证6.1 开机自检脚本的落地思路这套系统虽然在开发时已经做好相机标定与校正但现场设备经过运输、振动、温度变化后内外参很容易悄悄漂移。我的习惯是在部署目录里放一个自检脚本每次上电后先花三分钟跑一遍再决定要不要用当前标定参数继续工作。自检脚本做的事情很简单沿用之前的9x6棋盘格固定放在设备前方1.5米处左右相机各采集一张图调用自检函数获取三组数据——重投影误差、极线对齐误差、棋盘格中心深度误差。如果重投影误差大于0.5像素、极线误差大于1像素、深度误差超过3%就提示重新标定并拒绝进入感知主流程。python system_check.py \ --pattern 9x6 \ --square 30.0 \ --target-distance 1.5 \ --calib-left left.yaml \ --calib-right right.yaml \ --calib-stereo stereo.yaml脚本内部会读取左右标定文件生成映射表对固定距离处的棋盘格做角点提取然后用SGBM深度图取中心区域深度中位数和真实距离比较。输出以下三条指标检查项阈值失败动作单目重投影误差 0.3 px建议重标定极线对齐误差 1.0 px禁止启用感知1.5m深度误差 3%禁止启用感知这个自检的价值不只是发现故障它还会把每次的误差值写进日志。当点云重建或目标跟踪出现问题需要定位是算法问题还是硬件问题时日志能直接告诉你相机有没有松动。以前我偷懒以为标定是一次性的后来有一次在园区无人车上跑上午还好好的下午深度全偏排查了两个小时才意识到是固定相机的支架被石子颠松了。从那以后我每次上电都强制走一遍标定场自检把重投影误差、极线误差和深度误差一起存档。如果今天发现误差比昨天大了0.1像素我会直接拧一遍相机支架而不是继续调SGBM参数。这个习惯帮我省掉了大量“为什么算法突然变蠢”的排查时间希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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