简介:双目立体视觉的Matlab实现资源包,面向计算机视觉入门与进阶开发者,适用于学习相机标定、特征检测与匹配、基础矩阵/本质矩阵求解、三角测量、立体匹配及视差图生成等核心环节。包内共10个文件,以4个m脚本为主干,对应完整的双目处理流程;1个mat文件保存标定或匹配所需的参数数据;3个txt提供说明或运行备注;2个png用于展示实验结果与视差效果,压缩包仅346KB,便于对照源码逐段调试。已有1535人学习。这份资源不是孤立代码,而是将双目立体视觉的关键知识点串联成可运行的Matlab工程:从双目标定到特征匹配,再到三维点恢复与深度计算,均有脚本可循。对正在做课程设计、毕业设计或准备三维视觉项目实践的读者,可作为直接参考的基础框架,既能理解理论公式如何落地,也能在此基础上替换数据集或改进算法。
1. 双目立体视觉项目开局:一个Matlab工程如何把深度图算明白
很多做视觉开发的同学第一次听说“双目立体视觉”时,印象里就是左右两个摄像头拍同一场景,算出视差、反推深度。可真到了落地,标定板没拍平、极线没对齐、匹配出来全是雪花点,每一步都在劝退。这个基于Matlab的工程,把双目标定、极线校正、SGM立体匹配到深度图生成的整条链路做成了可跑通的闭环,适合想在Matlab里快速验证算法、或者刚入门立体视觉的开发者。它解决的核心问题很直接:你不用自己拼论文里的算法,打开工程、换一组自己的图像对、跑一遍,就能看到视差图和深度图,判断自己的摄像头配置到底能不能出活。
2. 技术架构拆解:从标定板到深度图的三层流水线
2.1 整体流水线:一个双目工程的数据流向
双目立体视觉的工程实现,本质上是把“物理世界的三维点”映射到“左右两张图像上的像素”再反推回来的过程。这个Matlab工程按顺序分五个阶段:单目标定、立体标定、极线校正、立体匹配、深度反推。前两个阶段是离线完成的,后三个阶段可以在线处理每一帧图像。
标定阶段做的事是求出左右相机各自的内参矩阵、畸变系数,以及两个相机之间的相对位姿关系。这些参数被封装成Matlab的stereoParameters对象,后续校正和匹配都在用它。极线校正这一步很多人容易忽略,它把左右图像按极线约束重新投影,让同名点在两条水平扫描线上,这样匹配从二维搜索降成一维搜索,速度和准确率都上一个台阶。立体匹配是所有环节里最吃参数的部分,Matlab内置的disparitySGM和disparityBM两条路分别对应半全局和局部匹配策略。
数据的流向很清晰:棋盘格图像对进入标定,输出stereoParams;普通场景图像对进入校正,输出对齐后的左右图;再进匹配,输出视差图;最后结合焦距和基线长度反推深度。整个流程里唯一需要人工干预的就是标定环节,后面全部是脚本自动跑。
2.2 工程目录:拿到zip先认这几个文件
打开zip包,先别急着点运行,花两分钟把目录结构看一遍。常见做法是根目录放一个main.m作为入口脚本,calibration和matching各占一个子目录,datasets目录按left和right分开放标定图像和测试图像。
stereo-vision/ ├─ config.m # 全局参数:路径、图像尺寸、视差范围 ├─ main.m # 入口脚本,串联整个流程 ├─ calibration/ │ ├─ detectBoardCorners.m # 棋盘格角点检测 │ └─ calibrateStereo.m # 立体标定,输出 stereoParams ├─ matching/ │ ├─ rectification.m # 极线校正 │ ├─ computeDisparity.m # SGM/BM 匹配 │ └─ depthFromDisparity.m # 视差转深度 ├─ utils/ │ ├─ loadStereoPair.m # 按时间戳读取左右图像对 │ └─ savePointCloud.m # 导出 PLY 点云 └─ datasets/ ├─ calib_left/ calib_right/ # 标定板图像对 └─ test_left/ test_right/ # 待测场景图像对这个结构本身就是在告诉你怎么改:换数据集只动datasets目录,换算法只动matching目录。我一般拿到工程第一步就是把config.m里的路径改成自己的绝对路径,确认图像能读进来,再逐步跑后面的模块。main.m里通常只是顺序调用这几个函数,如果你只想看某一步的中间结果,直接在命令行窗口单步执行对应脚本就行。
2.3 算法选型:SGM和BM怎么选
Matlab的计算机视觉工具箱同时提供BM和SGM两条匹配路线,它们的计算量和效果差异很大。BM是局部块匹配,原理是在左图的某个像素周围取一个窗口,在右图的扫描线上找相似度最高的位置,速度快但是对弱纹理区域非常敏感,容易出现整块整块错配。
SGM半全局匹配则把“每个像素独立找最优”改成“一条扫描线上的能量函数最小化”,引入平滑惩罚项,让视差图在物体边缘清晰、在平坦区域连续。这个工程的默认配置走的是SGM,因为对于室内场景和中等纹理的物体,SGM的鲁棒性明显好过BM。
| 对比项 | BM | SGM |
|---|---|---|
| 计算代价 | 低,适合实时 | 高,但Matlab有C优化路径 |
| 弱纹理区域 | 容易成片错配 | 平滑项约束后更稳 |
| 边缘保留 | 一般 | 较好 |
| 参数数量 | 少(窗口、阈值) | 多(P1/P2、Uniqueness、BlockSize) |
| 适用场景 | 纹理丰富、实时预览 | 室内场景、离线精度验证 |
如果只是快速预览效果,先把DisparityRange设到[0 64],BlockSize设为15,SGM跑一遍看整体形状对不对,再细调。后面章节会逐个参数讲含义。
3. 核心模块拆解:Matlab标定代码如何一步步求出立体参数
3.1 加载标定图像:数据集的目录约定
标定是整个工程的基石,标定参数错一点,后面视差图就全歪。这个工程对标定图像的读取做了一个约定:calib_left和calib_right两个目录下的文件名一一对应,比如同一序号代表同一时刻拍摄的左右帧。读取时用imageDatastore把两个目录包装成数据源,再用循环配对。
% 标定图像路径 calibDir = fullfile('datasets', 'calib'); leftDs = imageDatastore(fullfile(calibDir, 'left')); rightDs = imageDatastore(fullfile(calibDir, 'right')); % 检查图像对数量是否一致 assert(numel(leftDs.Files) == numel(rightDs.Files), ... 'left和right目录下的图像数量不一致,请检查采集过程');这段代码的核心是imageDatastore按目录批量管理图像文件,后面配合detectCheckerboardPoints可以一次性处理所有标定板图像。assert那行是血泪经验,我遇到过左右目录各少一张的情况,Matlab的角点检测函数会直接报维度不匹配,查了半天才发现是文件漏拷了。
3.2 棋盘格角点提取与立体标定
图像加载进来之后,下一步是提取棋盘格角点。Matlab的detectCheckerboardPoints支持同时传入左右图像,自动做立体角点匹配,并剔除那些左右图不是同一帧、或者棋盘格只在一侧出现的图像对。这个函数返回的imagePoints是一个N×1的cell数组,每个cell里是M×2的角点坐标矩阵,M是棋盘格内角点数。
% 检测左右图像的棋盘格角点 boardSize = [9 6]; % 内角点数量:9列 x 6行 [imagePoints, boardSize, pairsUsed] = ... detectCheckerboardPoints(leftDs.Files, rightDs.Files); % 生成对应的世界坐标(假设标定板位于z=0平面) worldPoints = generateCheckerboardPoints(boardSize, 24.5); % 方格边长24.5mm % 执行立体标定 stereoParams = estimateCameraParameters(imagePoints, worldPoints, ... 'NumRadialDistortionCoefficients', 2, ... 'EstimateTangentialDistortion', true, ... 'WorldUnits', 'millimeters');这里的逻辑分两层。第一层是角点提取,detectCheckerboardPoints内部会先做灰度化、再用harris角点加亚像素细化,棋盘格数量越少、图像越模糊,漏检概率越高。第二层是参数估计,estimateCameraParameters本质是最小化重投影误差的非线性优化,它需要世界坐标与图像坐标的对应关系,worldPoints由generateCheckerboardPoints根据格子数量和边长自动生成。
参数里的NumRadialDistortionCoefficients设为2,表示用两个径向畸变系数k1、k2来建模镜头畸变。如果镜头是广角或者鱼眼,系数不够用,可以考虑改成3,但要警惕过拟合。EstimateTangentialDistortion控制是否估计切向畸变,普通工业镜头这一项很小,但开了也比不开稳妥。
3.3 重投影误差:判断标定好坏的标准
标定完不能直接拿去用,先看重投影误差。这个值表示“用算出来的内外参把世界坐标投影回图像”和“实际检测到的角点坐标”之间的平均像素距离。误差小于0.3像素属于状态很好,0.5像素以内可接受,超过1像素就要重拍了。
% 计算重投影误差 [reprojectionErrors, errorsUsed] = ... reprojectionErrors(stereoParams, imagePoints, worldPoints); % 求平均误差 meanError = mean(reprojectionErrors(:)); fprintf('平均重投影误差: %.3f 像素\n', meanError); % 绘制误差分布图 figure; bar(reprojectionErrors); xlabel('图像对序号'); ylabel('重投影误差(像素)'); title('每对标定图像的重投影误差');这段代码里reprojectionErrors返回一个N×1的向量,每个元素对应一对图像的误差。如果某一对图像的误差特别大,极大概率是那张标定板有反光、或者拍照时手抖糊了,直接把这组图像对删掉重新标定即可。我一般会画一张柱状图扫一眼,哪个柱子明显高于其他柱子就干掉哪个,一只标定板进度的“后悔药”就在这里。
整个标定环节输出的stereoParams对象里,除了内参和畸变,还包括CameraParameters1和CameraParameters2两个子对象,以及TranslationOfCamera2这个3×1向量。TranslationOfCamera2的第一分量就是基线长度,后面深度反推时要用。
4. 核心模块拆解:SGM视差计算与深度反推的工程细节
4.1 极线校正:让两幅图变成严格水平对齐
标定完成之后,所有待测图像对都要先过极线校正这一关。校正的数学本质是重投影变换,把左右图像重新投影到一对共面的虚拟平面上,使得同一个三维点在两幅图像上的像素行号完全一致。Matlab里这一步只需要调用rectifyStereoImages,把原始图像和stereoParams传进去。
% 读取一对测试图像 I1 = imread(fullfile('datasets', 'test_left', 'frame_001.png')); I2 = imread(fullfile('datasets', 'test_right', 'frame_001.png')); % 极线校正 [J1, J2] = rectifyStereoImages(I1, I2, stereoParams, ... 'OutputView', 'valid', 'Interpolation', 'linear'); % 可视化校正结果 figure; imshowpair(J1, J2, 'montage'); title('校正后的左右图像(valid模式裁剪了无效区域)');校正结果有两个常见坑。第一个是OutputView参数的取值,'full'会保留原始图像的全部像素,校正后边缘会出现大量黑色变形区域;'valid'则裁剪掉这些无效区域,只保留两图都能对齐的部分。工程里默认用'valid',优点是视差图的黑边少,缺点是图像视野变小,如果你要保留完整的可视范围,得改成'full'。
校正之后我习惯把两幅图拼接成一个红绿叠加图,用imshowpair快速扫一眼:如果物体边缘出现明显红绿错位,说明标定参数有问题,或者左图右图的顺序反了。Interpolation参数一般用'linear'就行,用'cubic'更平滑但慢一倍,对后续匹配结果的提升非常有限。
4.2 SGM立体匹配参数怎么调
校正完成,进入整个工程最核心的模块:立体匹配。工程默认用disparitySGM,它把匹配问题建模成能量函数最小化,每个像素除了数据项还带平滑惩罚项。disparitySGM的输入是两张灰度图或彩色图,内部会自动转灰度,输出是单通道视差图。
% 计算视差图 disparityRange = [0 96]; % 最小和最大视差(像素) blockSize = 15; % 匹配窗口边长,奇数 uniquenessThreshold = 5; % 唯一性阈值 disparityMap = disparitySGM(J1, J2, ... 'DisparityRange', disparityRange, ... 'BlockSize', blockSize, ... 'UniquenessThreshold', uniquenessThreshold, ... 'ContrastThreshold', 0.5, ... 'TextureThreshold', 0.0002); % 中值滤波去除孤立噪点 disparityMap = medfilt2(disparityMap, [5 5], 'symmetric');DisparityRange是视差搜索区间,单位是像素。基线越长或者物体越近,视差越大,区间上限就要设得越高,但计算量也线性增长。设置这个值的通用办法是:先跑到场景里离相机最近的那个物体上,用实测距离反推一个视差上限,再留20%余量。BlockSize是匹配窗口边长,窗口太小纹理不足会匹配错,窗口太大边缘会被拉平,15对1080p图像是折中值。
UniquenessThreshold是SGM里最影响稀疏度的参数,它要求“最佳匹配”和“次佳匹配”之间的代价差至少达到这个百分比。设得过高,很多弱纹理区域直接被判为不可信,视差图越来越稀疏,合理范围是3~10。ContrastThreshold和TextureThreshold是控制匹配代价计算前置条件的两个阈值,场景光照均匀、纹理弱时,TextureThreshold要调小,否则大片区域直接被跳过。
4.3 视差转深度:公式与后处理
视差图和深度图的关系来自三角测量原理。等校正好之后,左右相机光轴近似平行,深度Z与视差d满足:Z = f × B / d。其中f是左相机焦距(像素单位),B是基线长度(毫米单位)。像素焦距f直接取stereoParams.CameraParameters1.FocalLength的第一个分量,基线取TranslationOfCamera2的x分量。
% 提取焦距和基线 focalLength = stereoParams.CameraParameters1.FocalLength(1); baseline = abs(stereoParams.TranslationOfCamera2(1)); fprintf('焦距: %.2f 像素, 基线: %.2f mm\n', focalLength, baseline); % 视差转深度 depthMap = zeros(size(disparityMap)); validIdx = disparityMap > 0; depthMap(validIdx) = focalLength * baseline ./ disparityMap(validIdx); % 归一化可视化 figure; imagesc(depthMap); colorbar; title('深度图(单位:mm)'); axis image;这里有个关键点:视差为0的像素在公式里会产生除以零的Inf,所以先建立validIdx掩膜,把无效像素的深度值保持为0。实际场景中物体被遮挡的区域、纹理缺失的区域都会导致视差为0,这些地方最后在点云里表现为空洞。深度图出来之后,我一般还会做一个距离截断,把超过设定量程的深度过滤掉,因为远距离的视差误差会被放大成很大的深度抖动。
% 截断无效深度 maxDistance = 5000; % 最大关注距离 5 米 depthMap(depthMap > maxDistance) = 0; % 深度图平滑 depthMap = medfilt2(depthMap, [5 5], 'symmetric');深度图平滑要谨慎,中值滤波会抹掉细小物体的边缘。如果做的是人脸等近距离场景,建议把滤波窗口降到3×3。视差到深度这一步没有太多玄学,关键是保证输入视差图的质量,视差图上的一两个坏像素,反推成深度后可能偏移几十毫米。
5. 实战避坑:标定与匹配最常见的五个翻车点
5.1 标定板反光导致角点检测漂移
现象:标定完成后重投影误差高达1.5像素以上,而且误差集中在某几张图上,视差图在标定板附近出现系统性扭曲。
原因:标定板是覆膜亚克力材质,在灯光下产生镜面反光,角点检测的亚像素定位被高光拉偏。另一个隐性原因是标定板没有完全贴在平面上,轻微弯曲也会让角点坐标和世界坐标不一致。
解决:拍摄前用哑光纸打印标定板,或者把光源换成漫射光。每次拍完立即用detectCheckerboardPoints返回值里的pairsUsed确认哪些图对可用,把误差最大的那对删掉重新标定。如果重投影误差分布图里单一柱子异常,优先怀疑那张图的角点定位。
5.2 视差图右侧大面积“黑洞”
现象:SGM跑出来的视差图右边一大片区域全为0,但左图对应位置明明有清晰的物体。
原因:DisparityRange上限设置偏小。视差d = x_left - x_right,对于左图中靠右的像素,如果真实视差超过搜索上限,最优匹配落在搜索区间之外,SGM只能给一个低置信度的0。场景越近、基线越长,这个现象越明显。
解决:测量相机到最近物体的距离,反推理论视差上限,再乘以1.5倍余量。比如标定得到的f = 800像素,基线 = 100mm,最近物体0.5米,视差上限大约是800×100/500 = 160像素,DisparityRange就要设成[0 192]。写代码时把DisparityRange打印出来,再对比最大有效视差,很快能定位。
5.3 匹配结果稀疏:UniquenessThreshold调太高
现象:视差图整体形状正确,但只有物体边缘周围有视差值,平坦区域全是0,看起来像一张线稿。
原因:UniquenessThreshold要求最佳匹配的代价明显低于次佳匹配,平坦区域里所有候选窗口的代价都差不多,自然被判为“唯一性不足”。纹理越弱,这个阈值越要往低调。
解决:先把UniquenessThreshold降到默认值5,如果还是稀疏就继续降到3。代价是弱纹理区域可能出现噪声,需要靠SGM的P1、P2平滑项往回拉,而不是靠UniquenessThreshold硬切。记住这条调参顺序:先放宽唯一性,再加平滑约束,效果比反过来好。
5.4 视差图里的NaN让深度图带洞
现象:depthMap计算完成后显示一串NaN警告,点云导出时文件损坏。
原因:disparityMap在特定情况下会产生Inf或NaN,视差为0产生的Inf已经被掩膜处理了,但NaN来自SGM内部对异常像素的标记。直接用NaN参与除法,结果扩散成整片NaN。
解决:深度反推前先做一次显式数值清理,把非有限值全部置为0,再参与后续计算。
% 清理非有限视差 disparityMap(~isfinite(disparityMap)) = 0;这个小修正在我第二次跑工程时就帮了大忙,第一次导出的PLY文件里全是空洞,排查了半天才发现是一个隐藏的NaN把点云坐标全污染了。养成在每一个矩阵运算前检查isfinite的习惯,能少走很多弯路。
5.5 运动模糊或滚动快门导致匹配失效
现象:静态测试图像对效果很好,一旦拍动态场景,视差图的边缘出现条纹状错位,深度值来回跳动。
原因:双目相机两帧不是严格同步曝光,物体移动时左右图拍到的是不同时刻的位置,极线约束被打破。滚动快门更是加剧了这个问题。
解决:离线阶段,选择无运动物体的静态场景验证算法;在线阶段,尽量使用带硬件同步触发功能的双目模组。纯粹靠算法无法完全消除异步曝光误差,工程里可以用一个简单的办法兜底:把左右图像的采集时间戳对齐,只处理时间差小于1毫秒的图像对。
6. 二次开发:把工程改造成自己的双目采集系统
6.1 参数外置到config脚本
工程跑通之后,第一步改造应该是把所有硬编码参数挪到config.m里集中管理。视差范围、窗口大小、深度截断距离、图像路径这几个值分散在多个脚本里的话,每改一次都要翻三个文件,迟早漏改一个。统一抽出来后,换一个相机场景只需要改这一个文件。我在重构时会把相机相关参数和算法相关参数分成两个struct,一个存基线、焦距、图像尺寸,一个存DisparityRange、BlockSize、UniquenessThreshold,后续做参数扫描实验时方便批量替换。
6.2 把离线图像对改成实时采集
如果想接真实相机,把main.m里的imread部分替换成硬件接口即可。Matlab的webcam或Image Acquisition Toolbox都能直接输出图像帧,关键是实时采集需要给SGM限帧率。我的做法是先用前20帧做标定校正,确认极线对齐后,再进入实时匹配循环,每次循环用tic/toc控制处理节奏,避免画面越积越卡。如果SGM在目标机器上跑到500毫秒一帧,就先降到VGA分辨率测试,别直接上1080p,这会让你误判算法性能。
6.3 输出点云PLY与代码生成注意点
深度图换算成点云坐标是二次开发里最常见需求。Matlab里先用图像坐标系生成网格点,再按深度值映射到相机坐标,最后用pointCloud对象封装并导出PLY文件。
% 由深度图构造三维点云 [rows, cols] = find(depthMap > 0); z = depthMap(sub2ind(size(depthMap), rows, cols)); x = (cols - stereoParams.CameraParameters1.PrincipalPoint(1)) ... .* z / focalLength; y = (rows - stereoParams.CameraParameters1.PrincipalPoint(2)) ... .* z / focalLength; ptCloud = pointCloud([x(:), y(:), z(:)]); pcwrite(ptCloud, 'output.ply', 'PLYFormat', 'binary');生成点云后可以用pcshow检查形状,常见问题是点云整体倾斜,原因是主点坐标标定不准,重新走一遍标定流程即可。如果想把工程导出成C++源码,要用Matlab Coder,但要注意disparitySGM需要额外配置代码生成参数,而且匹配速度并不一定比Matlab快多少,二次开发时优先保留Matlab环境做验证,只有部署阶段才考虑代码生成。从那以后我每次拿到一个双目视觉工程,都强制走一遍“标定→校正→匹配→深度”完整闭环,确认每一级输出的数据形状和量纲都没问题,再谈改造。希望帮到你。
本文还有配套的精品资源,点击获取