从标定到测距:双目相机深度计算的完整流程解析
1. 双目相机测距的基本原理双目相机测距的核心思想其实很简单——模仿人类双眼的立体视觉。想象一下当你闭上一只眼睛时判断物体距离会变得困难而睁开双眼后大脑会自动根据左右眼看到的微小差异来计算距离。双目相机正是基于这个原理工作的。具体来说当两个相机左相机和右相机同时拍摄同一场景时空间中的物体在左右图像中的位置会有细微差别这个差别称为视差Disparity。视差越大说明物体距离相机越近视差越小则距离越远。通过几何关系可以推导出深度计算公式深度Z (焦距f × 基线距离b) / 视差d这里有几个关键参数需要注意基线距离b两个相机光心之间的水平距离相当于人眼间距焦距f相机的光学焦距决定了成像的放大倍数视差d同一物体在左右图像中的水平坐标差单位像素在实际应用中这个公式看似简单但要获得准确的深度信息需要经过一系列复杂的预处理步骤。我刚开始接触时常常因为忽略这些步骤导致测距结果偏差很大。2. 相机标定消除镜头畸变2.1 为什么需要标定刚拿到双目相机时我直接拍摄了几组测试图像计算深度结果发现测距误差非常大。后来才明白所有镜头都存在畸变就像哈哈镜会扭曲图像一样。常见的畸变包括径向畸变图像边缘的直线会弯曲鱼眼效果切向畸变由于镜头安装不平行导致的图像扭曲如果不进行标定矫正这些畸变会严重影响后续的立体匹配精度。我曾经做过对比测试未标定的系统测距误差可能高达20%而标定后可以控制在5%以内。2.2 标定实操步骤使用OpenCV进行标定的完整流程如下准备标定板推荐使用棋盘格标定板比如9x6的格子打印在硬质材料上采集多角度图像左右相机同时拍摄15-20张不同角度的标定板图像角点检测使用cv2.findChessboardCorners检测棋盘格角点亚像素优化用cv2.cornerSubPix提高角点定位精度计算参数调用cv2.calibrateCamera得到相机内参和畸变系数这里有个实用技巧拍摄标定图像时要让标定板尽量充满整个画面并覆盖画面的四个角落和中心区域。我通常会采用米字形移动路径来确保各个区域都有覆盖。# 示例单目相机标定代码 import numpy as np import cv2 # 准备对象点棋盘格实际坐标 objp np.zeros((6*9,3), np.float32) objp[:,:2] np.mgrid[0:9,0:6].T.reshape(-1,2) # 存储对象点和图像点 objpoints [] # 实际空间中的3D点 imgpoints [] # 图像中的2D点 # 读取标定图像 images glob.glob(calibration/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找角点 ret, corners cv2.findChessboardCorners(gray, (9,6), None) if ret: objpoints.append(objp) # 亚像素精确化 corners2 cv2.cornerSubPix(gray,corners, (11,11), (-1,-1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) imgpoints.append(corners2) # 相机标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)3. 立体校正让图像行对齐3.1 校正的重要性标定完成后还需要进行立体校正。这一步的目的是让左右相机的图像平面完全平行确保同一物体在两幅图像中只存在水平方向的位移即行对齐。如果不做校正物体在垂直方向也会有位移这会极大增加立体匹配的难度。在实际项目中我遇到过因为校正不准确导致深度图出现大量垂直条纹的情况。后来发现是因为标定板拍摄角度不够多样导致外参估计不准确。3.2 校正实现方法OpenCV提供了stereoRectify函数来计算校正参数# 立体校正 flags cv2.CALIB_ZERO_DISPARITY R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, image_size, R, T, flags, alpha0 ) # 计算校正映射 map1x, map1y cv2.initUndistortRectifyMap( cameraMatrix1, distCoeffs1, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap( cameraMatrix2, distCoeffs2, R2, P2, image_size, cv2.CV_32FC1) # 应用校正 img1_rectified cv2.remap(img1, map1x, map1y, cv2.INTER_LINEAR) img2_rectified cv2.remap(img2, map2x, map2y, cv2.INTER_LINEAR)校正效果可以通过绘制水平线来验证在校正后的图像上同一物体在左右图中的y坐标应该相同只有x坐标有差异。4. 立体匹配寻找对应点4.1 匹配算法选择立体匹配是双目测距中最关键的环节也是计算量最大的部分。OpenCV主要提供两种算法BM算法Block Matching速度快但精度较低SGBM算法Semi-Global Block Matching速度较慢但精度更高经过多次测试我发现SGBM在大多数场景下表现更好特别是在纹理丰富的区域。但在实时性要求高的场合BM算法仍然是首选。4.2 参数调优经验立体匹配的效果高度依赖参数设置以下是我总结的关键参数调优经验numDisparities视差范围必须是16的整数倍设置过大会增加计算量过小会导致远处物体无法匹配经验值基线距离mm× 焦距像素/ 最近物体距离mmblockSize匹配块大小必须是奇数值越大结果越平滑但边缘越模糊通常设置在3-15之间uniquenessRatio唯一性比率消除模糊匹配值越大匹配条件越严格通常设置在5-15之间# SGBM参数设置示例 window_size 3 min_disp 0 num_disp 16*5 stereo cv2.StereoSGBM_create( minDisparity min_disp, numDisparities num_disp, blockSize window_size, uniquenessRatio 10, speckleWindowSize 100, speckleRange 32, disp12MaxDiff 1, P1 8*3*window_size**2, P2 32*3*window_size**2 ) # 计算视差图 disp stereo.compute(imgL, imgR).astype(np.float32) / 16.05. 深度计算与优化5.1 从视差到深度得到视差图后使用reprojectImageTo3D函数可以将视差转换为三维坐标# 计算三维坐标 points_3d cv2.reprojectImageTo3D(disp, Q) # 提取深度信息Z坐标 depth_map points_3d[:,:,2]需要注意的是视差图中的视差值实际上是放大了16倍的OpenCV的设计所以在计算前需要除以16。5.2 深度图优化原始深度图通常存在以下问题噪声特别是在低纹理区域空洞匹配失败的像素点边缘锯齿由于块匹配的特性导致我常用的优化方法包括中值滤波cv2.medianBlur双边滤波保留边缘的同时平滑区域空洞填充使用邻域均值或最邻近值填充# 深度图优化示例 # 中值滤波 depth_map cv2.medianBlur(depth_map, 5) # 双边滤波 depth_map cv2.bilateralFilter(depth_map, 9, 75, 75) # 空洞填充简单版 mask (depth_map 0).astype(np.uint8) depth_map cv2.inpaint(depth_map, mask, 3, cv2.INPAINT_TELEA)6. 实际应用中的挑战与解决方案在多个实际项目中我发现双目测距系统面临的主要挑战包括光照变化问题不同光照条件下测距结果不一致解决方案使用主动红外补光或增加曝光补偿低纹理区域问题白墙等区域难以匹配解决方案投射随机图案或使用结构光辅助实时性要求问题高分辨率图像处理速度慢解决方案使用GPU加速或降低分辨率基线选择短基线近距离精度高但远距离能力差长基线远距离效果好但近距离视差过大折中方案根据应用场景选择合适基线通常为5-15cm一个实用的建议是在室内场景中保持环境光照稳定并在相机周围增加纹理如贴纸或图案可以显著提高测距精度。我在开发智能扫地机器人项目时就通过这种方式将测距误差从8%降低到了3%以内。