简介:这份资源是面向计算机、人工智能、自动化、通信工程等专业学生与科研人员的双目摄像头立体视觉系统完整项目包,围绕相机标定、立体匹配与深度图生成三大核心环节展开,可直接用于毕业设计、课程设计或项目立项演示。压缩包共190个文件,约2.58MB,以34个Python脚本和38张jpg实验图像为主体,辅以C/C++源码、JavaScript前端页面、JSON配置及多种工程文件,覆盖算法实现、界面展示与数据处理等模块,并附有项目报告与设计文档。已有40人学习关注。读者可从中获得一套可复现的立体视觉实现流程,包括标定参数求解、匹配算法调试与深度图可视化思路,同时能借鉴其目录组织与工程配置方式,便于在此基础上修改扩展或快速搭建自己的视觉实验环境。
1. 双目摄像头立体视觉系统:从标定到深度图,一套能跑通的工程路径
双目摄像头立体视觉系统,核心就三件事:标定、匹配、深度图生成。很多人第一次接触这个方向,以为把两个摄像头往板子上一焊,跑个开源算法就能出深度,结果标定误差一大,极线校正后左右图对不齐,匹配出来的视差图全是噪点,深度值飘得没法看。这个标题对应的是一套完整的工程链路:先用张正友标定法把内外参和畸变系数标出来,再做立体校正让左右图像行对齐,接着用 SGBM 或 BM 做稠密匹配得到视差图,最后根据基线、焦距和视差反算深度。适合做毕业设计的学生、刚转视觉的嵌入式工程师,以及想把双目方案落到机器人避障、体积测量、三维重建场景的从业者。整套流程不依赖昂贵设备,两个同型号 USB 摄像头加一块棋盘格就能起步,但每一步的参数和边界条件都得抠清楚,否则后面全是玄学。
2. 标定:双目相机内参、外参与畸变系数的获取路径
2.1 为什么双目标定不能只做单目标定再拼起来
单目相机标定解决的是像素坐标到归一化平面坐标的映射,输出内参矩阵和畸变系数。双目标定在此基础上多了一层:左右相机之间的旋转矩阵 R 和平移向量 T。常见做法是分别对左右相机做单目标定,再用同一组棋盘格图像计算两相机之间的位姿关系。但这里有个容易翻车的地方:如果左右相机不是硬件同步触发,拍摄同一棋盘格时两幅图的时刻有偏差,标定出来的 R 和 T 会引入运动误差。我一般会先用软触发同步,或者把曝光时间调短、棋盘格静止放置,尽量消除时间差。
张正友标定法的核心思想是:棋盘格平面在多个不同位姿下,利用平面单应性矩阵求解相机参数。OpenCV 里对应calibrateCamera和stereoCalibrate两个函数。单目标定负责内参和畸变,双目标定负责外参。如果只做单目标定再手动拼外参,平移向量的尺度无法确定,深度图就没有真实物理单位。
2.2 采集标定图像的具体操作与参数设置
标定图像的质量直接决定标定精度。棋盘格建议用 9x6 或 11x8 的内角点,打印在平整硬板上,避免纸张弯曲。采集时覆盖相机视野的各个区域:中心、四角、远近、左右倾斜。一般 15 到 25 组有效图像就够,太少会导致参数过拟合,太多则引入重复位姿反而降低标定稳定性。
下面是一段采集和单目标定的 Python 代码,基于 OpenCV:
import cv2 import numpy as np import glob # 棋盘格内角点数量,注意是内角点,不是格子数 chessboard_size = (9, 6) # 棋盘格方格实际物理尺寸,单位毫米 square_size = 25.0 # 生成棋盘格三维坐标 objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp *= square_size objpoints = [] # 三维点 imgpoints_left = [] # 左图二维点 imgpoints_right = [] # 右图二维点 left_images = sorted(glob.glob('left/*.png')) right_images = sorted(glob.glob('right/*.png')) for lpath, rpath in zip(left_images, right_images): img_l = cv2.imread(lpath) img_r = cv2.imread(rpath) 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, chessboard_size, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: # 亚像素角点精化,窗口大小和迭代终止条件 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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) objpoints.append(objp) imgpoints_left.append(corners_l) imgpoints_right.append(corners_r) # 单目标定 ret_l, mtx_l, dist_l, _, _ = cv2.calibrateCamera(objpoints, imgpoints_left, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ = cv2.calibrateCamera(objpoints, imgpoints_right, gray_r.shape[::-1], None, None) print("左相机内参:\n", mtx_l) print("左相机畸变:\n", dist_l) print("右相机内参:\n", mtx_r) print("右相机畸变:\n", dist_r)这段代码的逻辑是:先定义棋盘格的三维坐标,然后对每一对左右图提取角点并做亚像素精化,最后分别调用calibrateCamera得到左右内参和畸变。cornerSubPix的窗口大小(11,11)是经验值,图像分辨率高时可以适当加大。criteria控制迭代精度,0.001 像素的终止阈值在多数场景下够用。
2.3 双目标定与极线校正的参数含义
单目标定完成后,用stereoCalibrate计算左右相机之间的 R、T、E、F:
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5), flags=cv2.CALIB_FIX_INTRINSIC ) print("旋转矩阵 R:\n", R) print("平移向量 T:\n", T)CALIB_FIX_INTRINSIC表示固定单目标定得到的内参,只优化外参。如果图像质量好、标定板覆盖充分,也可以不加这个标志,让内外参一起优化,但计算量更大,且可能因为初值不好而发散。T 的模长就是双目基线,单位与棋盘格物理尺寸一致。基线越大,深度分辨率越高,但视差搜索范围也越大,匹配难度上升。
极线校正用stereoRectify和initUndistortRectifyMap:
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha=0 # alpha=0 表示校正后图像只保留有效像素 ) map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2)alpha=0会裁剪掉校正后黑边,alpha=1保留全部像素但边缘有无效区域。Q是视差到深度的重投影矩阵,后面生成深度图要用。校正后的左右图,同一物点在同一行上,这是稠密匹配的前提。
3. 立体匹配:SGBM 参数怎么调才能出干净视差图
3.1 BM 与 SGBM 的选型差异
OpenCV 提供两种经典立体匹配算法:StereoBM和StereoSGBM。BM 是块匹配,速度快,适合嵌入式实时场景,但视差图噪声大、边缘模糊。SGBM 是半全局块匹配,引入多方向代价聚合,视差图更平滑、边缘更准,但计算量大约是 BM 的 5 到 10 倍。毕业设计或离线处理优先选 SGBM,实时性要求高的嵌入式场景可以先用 BM 跑通再考虑优化。
常见做法是:先用 SGBM 得到质量较好的视差图作为基准,再根据帧率要求决定是否降级到 BM 或做分辨率缩放。
3.2 SGBM 关键参数逐项说明
SGBM 的参数多,调起来容易懵。下面是一组可复现的配置:
import cv2 import numpy as np # 读取校正后的左右图 img_l = cv2.imread('rectified_left.png', cv2.IMREAD_GRAYSCALE) img_r = cv2.imread('rectified_right.png', cv2.IMREAD_GRAYSCALE) # SGBM 参数 min_disp = 0 num_disp = 16 * 5 # 必须是 16 的整数倍 block_size = 5 stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, blockSize=block_size, P1=8 * 1 * block_size * block_size, P2=32 * 1 * block_size * block_size, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, preFilterCap=63, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) disparity = stereo.compute(img_l, img_r).astype(np.float32) / 16.0numDisparities决定视差搜索范围,必须覆盖场景中最近物点的视差。如果深度图近处出现大片空洞,说明这个值太小。blockSize是匹配块大小,奇数,3 到 11 之间。块越大,弱纹理区域越稳定,但边缘越糊。P1和P2是平滑惩罚项,控制视差图平滑程度,经验公式是P1 = 8 * channels * blockSize^2,P2 = 32 * channels * blockSize^2。uniquenessRatio过滤掉匹配歧义大的像素,值越大过滤越狠,一般 5 到 15。speckleWindowSize和speckleRange用来剔除小连通域的噪点,窗口 50 到 200,范围 1 到 2。
3.3 视差图后处理与空洞填充
原始 SGBM 视差图在弱纹理、遮挡区域会有空洞。常见后处理是左右一致性检查加空洞填充:
# 左右一致性检查 stereo_right = cv2.StereoSGBM_create( minDisparity=-num_disp, numDisparities=num_disp, blockSize=block_size, P1=8 * 1 * block_size * block_size, P2=32 * 1 * block_size * block_size, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, preFilterCap=63, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) disparity_right = stereo_right.compute(img_r, img_l).astype(np.float32) / 16.0 # 一致性检查 h, w = img_l.shape mask = np.zeros_like(disparity, dtype=np.uint8) for y in range(h): for x in range(w): d = int(disparity[y, x]) if d > 0 and d < num_disp: xr = x - d if 0 <= xr < w and abs(disparity[y, x] - disparity_right[y, xr]) < 1.0: mask[y, x] = 255 # 用中值滤波填充小空洞 disparity_filtered = cv2.medianBlur(disparity, 5) disparity_filtered[mask == 0] = 0左右一致性检查的原理是:左图某像素的视差 d,对应右图 x-d 位置的视差应该接近 d。不满足的像素标记为无效。中值滤波可以填掉小空洞,但大空洞需要更复杂的插值或后续帧融合。这一步做完,视差图基本能看了。
4. 深度图生成:从视差到三维点云的换算与验证
4.1 重投影矩阵 Q 的物理含义
stereoRectify输出的 Q 矩阵是 4x4,作用是把视差图上的像素坐标 (x, y, d) 映射到三维空间:
[X, Y, Z, W]^T = Q * [x, y, d, 1]^T实际三维坐标是 (X/W, Y/W, Z/W)。Q 矩阵里包含基线、焦距、主点等信息。深度 Z 的近似公式是Z = f * B / d,其中 f 是校正后焦距,B 是基线,d 是视差。视差越小,深度越大,误差也越大。这就是为什么双目在远距离精度差,近距离精度好。
4.2 生成深度图并过滤无效值
# 用 Q 矩阵重投影 points_3d = cv2.reprojectImageTo3D(disparity_filtered, Q) # 深度图就是 Z 通道 depth_map = points_3d[:, :, 2] # 过滤无效深度 depth_map[disparity_filtered == 0] = 0 depth_map[depth_map > 10000] = 0 # 超过 10 米视为无效 depth_map[depth_map < 0] = 0 # 归一化显示 depth_vis = cv2.normalize(depth_map, None, 0, 255, cv2.NORM_MINMAX) depth_vis = np.uint8(depth_vis) cv2.applyColorMap(depth_vis, cv2.COLORMAP_JET)reprojectImageTo3D输出的三维点单位与标定时棋盘格物理尺寸一致。如果标定用毫米,深度就是毫米。过滤条件根据实际场景调整,室内 0.3 到 5 米,室外可能到几十米。深度图可视化用伪彩色更直观,但注意归一化会丢失绝对尺度信息,只用于观察。
4.3 用已知距离验证深度精度
标定和匹配做完,必须验证。最简单的方法:把棋盘格放在已知距离,比如 1 米、2 米、3 米,看深度图对应区域的 Z 值是否接近。误差在 1% 到 3% 以内算正常。如果误差大,检查基线 T 的模长是否与实测一致,棋盘格物理尺寸是否准确,以及校正后图像是否真的行对齐。
另一个验证方法是重投影误差:把三维点重新投影回图像,看与原始角点的偏差。stereoCalibrate返回的 ret 值就是重投影误差,一般小于 0.5 像素算好,大于 1 像素说明标定图像质量有问题。
5. 避坑与排查:双目系统从标定到深度图的 5 个血泪教训
5.1 标定重投影误差大,深度图整体偏移
现象:stereoCalibrate返回的 ret 大于 1.5 像素,深度图整体偏近或偏远。 原因:棋盘格打印尺寸与代码里square_size不一致,或者标定图像太少、位姿太集中。 解决:用卡尺量实际方格尺寸,重新采集 20 组以上覆盖不同位姿的图像,确保棋盘格平整。
5.2 极线校正后左右图不对齐
现象:校正后左右图同一物点不在同一行,视差图出现横向条纹。 原因:单目标定内参不准,或者stereoRectify的alpha设置导致裁剪区域不一致。 解决:先检查单目标定重投影误差,再确认initUndistortRectifyMap左右使用对应的 R1/R2 和 P1/P2。alpha左右要一致。
5.3 SGBM 视差图大片空洞
现象:弱纹理墙面、白墙区域视差全为 0。 原因:numDisparities太小,或者blockSize太小导致匹配歧义。 解决:增大numDisparities到覆盖最近物点,增大blockSize到 7 或 9,适当降低uniquenessRatio。
5.4 深度图近处准远处飘
现象:1 米处误差几厘米,3 米处误差几十厘米。 原因:双目深度误差与距离平方成正比,这是原理性限制。 解决:增大基线 B 可以提高远距离精度,但会加剧遮挡和匹配难度。根据应用场景权衡,室内 1 到 3 米用 6 到 10 厘米基线够用。
5.5 左右相机不同步导致运动物体视差错误
现象:静态场景深度图正常,有运动物体时视差图出现拖影或错误深度。 原因:两个 USB 摄像头独立曝光,运动物体在两幅图中位置不一致。 解决:用硬件同步触发,或者降低曝光时间、提高帧率,减少时间差。软触发同步在 OpenCV 里可以用VideoCapture的set和grab/retrieve配合,但效果有限。
6. 进阶技巧:用视差图做点云拼接与体积测量
深度图生成后,reprojectImageTo3D输出的三维点可以直接保存为 PLY 点云,用 MeshLab 或 CloudCompare 查看。如果要做体积测量,比如测一个箱子,思路是:先分割出箱子区域,取点云的上表面和地面,计算高度差,再乘以底面积。这里的关键是坐标系对齐——双目相机的坐标系原点在左相机光心,地面平面需要提前标定或者用 RANSAC 拟合。
我一般会先用cv2.solvePnP把相机坐标系转到世界坐标系,让 Z 轴垂直地面。然后对点云做直通滤波,保留箱子高度范围内的点,再用cv2.convexHull求底面积。体积精度受深度图噪声影响,近距离小物体误差可以控制在 5% 以内,远距离大物体误差会明显上升。
另一个实用技巧是视差图转点云后做统计滤波,去掉离群点:
from scipy.spatial import cKDTree points = points_3d.reshape(-1, 3) valid = points[:, 2] > 0 points = points[valid] # 统计滤波:每个点找 20 个邻居,计算平均距离 tree = cKDTree(points) distances, _ = tree.query(points, k=20) mean_dist = distances.mean(axis=1) # 去掉平均距离超过阈值 2 倍标准差的点 threshold = mean_dist.mean() + 2 * mean_dist.std() filtered_points = points[mean_dist < threshold]这段代码用 KDTree 做邻域查询,去掉稀疏离群点。k=20是邻域大小,点云密度高时可以减小。阈值用均值和标准差自适应,比固定值更稳。做完这一步,点云干净很多,后续拟合平面或测量体积都更可靠。
最后说个习惯:每次改完标定参数或 SGBM 参数,别只看深度图好不好看,一定回到重投影误差和已知距离验证上。视觉项目里,肉眼看着顺的图,数值可能错得离谱。希望帮到你。
本文还有配套的精品资源,点击获取