从零搭建双目测距系统:OpenCV+Python实战指南

1. 双目视觉基础与环境准备

双目视觉技术通过模拟人类双眼的视差原理,利用两个摄像头从不同角度拍摄同一场景,通过计算图像中对应点的位置差异来获取深度信息。这种技术广泛应用于机器人导航、自动驾驶、工业检测等领域。

1.1 硬件选择与配置

推荐硬件配置方案

组件类型 入门级配置 专业级配置 说明
摄像头 普通USB摄像头×2 工业级全局快门相机 建议选择固定焦距镜头
支架 3D打印/自制支架 铝合金可调基线支架 基线距离建议50-200mm
计算设备 树莓派4B NVIDIA Jetson系列 需考虑实时性要求

提示:摄像头安装时需确保光轴平行,可通过机械调整或软件校正实现。

1.2 开发环境搭建

安装必要的Python库:

pip install opencv-contrib-python numpy matplotlib

验证OpenCV安装:

import cv2
print(cv2.__version__)  # 应显示4.x版本

2. 相机标定与立体校正

2.1 棋盘格标定实战

准备标定板(建议打印A4尺寸的7x9棋盘格),采集15-20组不同角度的图像:

import cv2
import numpy as np

# 定义棋盘格参数
pattern_size = (6, 8)  # 内角点数量
square_size = 2.5      # 棋盘格方块实际尺寸(mm)

# 准备对象点
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) * square_size

# 存储对象点和图像点
obj_points = []
img_points_left = []
img_points_right = []

# 标定过程
for i in range(1, 16):
    img_l = cv2.imread(f'left_{i}.jpg')
    img_r = cv2.imread(f'right_{i}.jpg')
    
    gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY)
    gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY)
    
    # 查找角点
    ret_l, corners_l = cv2.findChessboardCorners(gray_l, pattern_size)
    ret_r, corners_r = cv2.findChessboardCorners(gray_r, pattern_size)
    
    if ret_l and ret_r:
        obj_points.append(objp)
        img_points_left.append(corners_l)
        img_points_right.append(corners_r)

2.2 立体相机参数计算

# 单目标定
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)

# 立体标定
flags = cv2.CALIB_FIX_INTRINSIC
ret, _, _, _, _, 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], flags=flags)

2.3 立体校正与极线对齐

# 计算校正变换
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
    mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T)

# 生成校正映射表
left_map1, left_map2 = cv2.initUndistortRectifyMap(
    mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2)
right_map1, right_map2 = cv2.initUndistortRectifyMap(
    mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2)

# 应用校正
img_l_rect = cv2.remap(img_l, left_map1, left_map2, cv2.INTER_LINEAR)
img_r_rect = cv2.remap(img_r, right_map1, right_map2, cv2.INTER_LINEAR)

3. 视差图计算与优化

3.1 SGBM算法参数详解

Semi-Global Block Matching (SGBM)是OpenCV中效果较好的立体匹配算法:

def create_sgbm():
    window_size = 5
    min_disp = 0
    num_disp = 16*5  # 必须是16的整数倍
    
    stereo = cv2.StereoSGBM_create(
        minDisparity=min_disp,
        numDisparities=num_disp,
        blockSize=window_size,
        P1=8*3*window_size**2,
        P2=32*3*window_size**2,
        disp12MaxDiff=1,
        uniquenessRatio=10,
        speckleWindowSize=100,
        speckleRange=32,
        mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY)
    return stereo

stereo = create_sgbm()
disparity = stereo.compute(img_l_rect, img_r_rect).astype(np.float32)/16.0

关键参数调优指南

参数 作用 推荐范围 调整策略
minDisparity 最小视差 0-64 根据最近物体距离调整
numDisparities 视差范围 16-256 越大检测距离越远
blockSize 匹配块大小 3-11 纹理丰富场景用较小值
uniquenessRatio 唯一性比率 5-15 值越大匹配越严格

3.2 视差图后处理

# 视差归一化显示
disp_vis = cv2.normalize(disparity, None, alpha=0, beta=255,
                        norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
disp_vis = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET)

# 空洞填充(可选)
right_matcher = cv2.ximgproc.createRightMatcher(stereo)
right_disp = right_matcher.compute(img_r_rect, img_l_rect)
wls_filter = cv2.ximgproc.createDisparityWLSFilter(stereo)
filtered_disp = wls_filter.filter(disparity, img_l_rect, disparity_map_right=right_disp)

4. 三维坐标转换与测距

4.1 视差转深度计算

# 使用Q矩阵进行三维重建
points_3d = cv2.reprojectImageTo3D(disparity, Q, handleMissingValues=True)

# 计算指定点的距离
def get_distance(disparity, x, y):
    if disparity[y,x] <= 0: 
        return -1  # 无效点
    return (Q[2,3] * Q[3,2]) / (disparity[y,x] * Q[3,2] + Q[2,2])

# 鼠标交互获取距离
def on_mouse(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONDOWN:
        distance = get_distance(disparity, x, y)
        print(f"Position: ({x},{y}), Distance: {distance:.2f} mm")

cv2.namedWindow('disparity')
cv2.setMouseCallback('disparity', on_mouse)

4.2 精度提升技巧

常见问题解决方案

  1. 边缘锯齿问题

    • 使用导向滤波优化视差图
    • 后处理阶段应用双边滤波
  2. 远距离精度不足

    • 增加基线距离(相机间距)
    • 使用更高分辨率摄像头
  3. 弱纹理区域匹配失败

    • 尝试不同的特征匹配算法
    • 添加人工标记点辅助匹配
# 导向滤波示例
guided_filter = cv2.ximgproc.createGuidedFilter(img_l_rect, radius=10, eps=100)
filtered_disp = guided_filter.filter(disparity)

5. 完整系统集成与性能优化

5.1 实时视频处理流程

cap_left = cv2.VideoCapture(0)  # 左摄像头
cap_right = cv2.VideoCapture(1) # 右摄像头

while True:
    ret_l, frame_l = cap_left.read()
    ret_r, frame_r = cap_right.read()
    
    if not (ret_l and ret_r):
        break
    
    # 校正图像
    frame_l_rect = cv2.remap(frame_l, left_map1, left_map2, cv2.INTER_LINEAR)
    frame_r_rect = cv2.remap(frame_r, right_map1, right_map2, cv2.INTER_LINEAR)
    
    # 计算视差
    disparity = stereo.compute(frame_l_rect, frame_r_rect)
    
    # 显示结果
    cv2.imshow('Left', frame_l_rect)
    cv2.imshow('Disparity', cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX))
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

5.2 性能优化策略

多线程处理框架

from threading import Thread
import queue

class CameraThread(Thread):
    def __init__(self, cam_id, output_queue):
        Thread.__init__(self)
        self.cam_id = cam_id
        self.queue = output_queue
        self.running = True
        
    def run(self):
        cap = cv2.VideoCapture(self.cam_id)
        while self.running:
            ret, frame = cap.read()
            if ret:
                self.queue.put(frame)
        cap.release()

# 创建处理线程
left_queue = queue.Queue()
right_queue = queue.Queue()
left_thread = CameraThread(0, left_queue)
right_thread = CameraThread(1, right_queue)
left_thread.start()
right_thread.start()

GPU加速方案

# 使用CUDA加速的SGBM
stereo = cv2.cuda.createStereoBM(numDisparities=64, blockSize=21)
gpu_left = cv2.cuda_GpuMat()
gpu_right = cv2.cuda_GpuMat()

gpu_left.upload(img_l_rect)
gpu_right.upload(img_r_rect)
gpu_disp = stereo.compute(gpu_left, gpu_right)
disp = gpu_disp.download()

在实际项目中,我发现基线距离的选择对系统性能影响最大。经过多次测试,对于室内1-3米的测距范围,12cm的基线距离配合720p摄像头能达到最佳平衡。

更多推荐