从标定到测距:双目相机深度计算的完整流程解析
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, alpha=0 ) # 计算校正映射 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)× 焦距(像素)/ 最近物体距离(mm)
blockSize(匹配块大小):
- 必须是奇数
- 值越大结果越平滑但边缘越模糊
- 通常设置在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%以内。
