双目立体视觉测距从原理到OpenCV实战
2026/9/2 19:17:11 网站建设 项目流程

简介:一个基于C#实现的双目立体视觉测距工程,面向计算机视觉初学者及需要实现空间距离测量的开发者。项目借鉴人眼视差机制,通过双相机获取左右图像,完成特征匹配、立体匹配、视差图计算与三角测量,可输出指定点的三维坐标和实际距离,适用于机器人导航、自动驾驶避障、工业检测等场景的入门与二次开发。压缩包共49个文件,整体约2.79MB,以13个CS源码文件为核心,搭配resx/resources界面资源、sln/csproj工程配置、exe/pdb编译产物、jpg左右相机示例图片及config设置文件,目录结构清晰。已有670人学习。代码覆盖图像预处理、特征提取、匹配算法、视差计算与距离测量等关键模块,借助Form1、SettingForm、PointListForm等窗体及设计器代码,可快速梳理界面参数与算法逻辑的关联,理解从相机标定到距离解算的完整流程,是一份兼顾C#编程与视觉算法实践的优质参考。 整理硬盘时翻出一个“双目立体视觉测距.zip”的项目包,解压后里面是标定图片、两路视频流脚本和一份测距实验记录。这个压缩包让我想起第一次把双目测距跑通那天的感受——原来让机器“看到”距离,并没有想象中那么玄乎。

所谓双目立体视觉测距,本质就是模拟人眼的双眼视差机制:同一个物体在左右相机画面里会落在不同的像素位置,这两个位置的差就是视差。只要知道相机的焦距、两个相机之间的距离(基线)和这个视差,就能通过三角公式算出物体的深度距离。

这套技术能解决什么问题?机器人避障、AR眼镜的深度感知、工业场景的尺寸测量、物流分拣都会用到。市面上还有超声波测距、蓝牙测距等非视觉方案,但双目视觉的优势在于非接触、被动式,而且能一次性获得全画面的深度信息。适合什么人看?如果你在学OpenCV、刚接触计算机视觉,或者想从单目检测往深度感知方向走,这篇文章能帮你把“双目测距”这一整条链路完整跑通。

1.1 人眼就是现成的样板

我们闭上一只眼睛,再伸手去拿水杯,会发现动作明显变笨拙。这不是心理作用,是因为单只眼睛丢失了主要的深度线索。人的双眼在头骨上存在约6-7厘米的水平间距,看同一个物体时会形成两个有细微差别的画面,大脑的视觉皮层会计算这两个画面的差异,从而还原出物体的远近。

双目立体视觉测距干的事,就是把这套生物机制工程化。左右两个相机相当于两只眼睛,两个相机镜头中心的间距叫基线。处理系统要做的,就是在两幅图上找到同一个空间点的两个投影,计算它们在水平方向上的像素偏移,这个偏移就是视差。视差越大,物体离相机越近;视差越小,物体越远。用两只普通USB摄像头就能复现这套原理,并不一定需要昂贵的设备。

1.2 先吃透三个关键名词

基线(Baseline):左右相机光心之间的物理距离,一般用毫米表示。它是整个系统的“尺子”,基线一旦标定错,后面所有测距结果都会系统性偏差。

视差(Disparity):同一个三维点在左图和右图中像素坐标的差值。像素视差可以是整数或亚像素值,深度图的每个像素存储的就是这个值。

深度(Depth):也叫深度值,即物体表面某一点到相机平面的距离,单位通常是毫米或米。我们最终要输出的是深度图,深度图里每个像素的亮度代表该点的远近。

这三个词可以类比成“用两只手指比划距离”——基线是你两只手指之间的距离,视差是两只手指指向同一物体时转过的角度差,深度就是物体离你手指的距离。角度差越大,物体越近,这跟视差越大深度越小的规律一模一样。

1.3 核心公式:Z = fB/d 是怎么来的

这个公式是双目测距的地基。推导并不复杂:左右相机平行放置,光心分别为Ol和Or,距离为B,焦距为f。空间点P(深度为Z)在左图成像于xl,在右图成像于xr。根据相似三角形,可以得到:

Z / B = f / (xl - xr)

其中 xl - xr 就是视差d,所以:

Z = f * B / d

这里有个容易栽跟头的细节:如果f的单位是毫米,d的单位是像素,单位不统一,计算必错。实际代码里用的是OpenCV内参矩阵中的fx和fy,单位已经是像素了,直接代入即可。

举个具体数字。假设镜头焦距16mm,传感器像素尺寸3.45um,那么像素焦距 f_pixel ≈ 4638。基线B=120mm,某个物体视差为139像素,那么 Z = 4638 × 120 / 139 ≈ 4003mm,也就是距离约4米。

这里也暴露出双目测距的一个本质矛盾:Z与d是反比关系,视差越小(远处),同样的1像素视差误差会引起越大的距离误差。所以测距精度天然是“近高远低”的。

那为什么不直接用单目测距?单目在没有先验信息时,无法从一张图里恢复绝对尺度——不知道物体真实大小,就推不出它离你多远。双目是几何方法,不需要物体尺寸先验,这是它最大的优势。单目测距通常要配合已知目标尺寸或地面平面假设,适用场景受限得多。

2.1 相机怎么选:分辨率、镜头、同步性

首先是分辨率。720p够做原型验证,但视差精度有限;1080p是实用性和计算量的平衡点;再往上分辨率翻倍,匹配计算量大幅上涨,实时性会成问题。别一上来就上4K,很多场景下瓶颈根本不在分辨率,而在标定和同步。

镜头视角也影响精度。广角镜头能覆盖更大范围,但畸变更大,边缘区域经过校正后有效像素会损失;长焦镜头视场窄,但相同距离下视差更大,测距精度相对更高。验证阶段用普通镜头就好,不需要纠结。

最容易忽视的是同步性。如果拍运动的物体,左右相机没有硬件同步,目标在左右图里的位置会对不上,视差计算直接出错。硬件同步方案常见的有外触发线、同一帧信号源;静态场景可以接受软件异步采集。自己做实验时,如果只测静态物体,软件同步就能对付。

2.2 基线多长合适

基线是双目的“尺子”,直接决定了可测距离范围。基线太短,近距离物体的视差会很大,容易超出匹配搜索范围;基线太长,近距离出现盲区,远处的视差变化又太小,精度也上不去。

经验值参考:室内桌面级测距,基线8-15cm;机器人导航,基线20-30cm;室外远距离测量,基线可能拉到1m以上。一个常用经验是,目标测距距离Z,基线大致取Z的1/10到1/20。举个例子,你想测5米内的物体,基线可以取25-50cm;只想测1米内的桌面物体,基线8-10cm就够了。基线变长的代价是结构体积变大、近距离盲区增加,跟相机镜头的视场角也要匹配,否则远处物体根本不出现在两个画面的共同视野里。

2.3 精度随距离衰减,别被宣传数据骗了

对 Z = fB/d 求导,可以得到 dZ/dd = -Z²/(fB)。这个式子说明:距离误差正比于距离的平方,反比于焦距和基线的乘积。同一套系统,测10米和测2米,误差差距可达25倍。

算个直观的例子。假设f=4638像素,B=120mm,在2米处,1像素视差误差引起约0.36米的距离偏差;在5米处,同样的1像素误差会放大到约2.24米。所以工程上常说“双目测距适合近距离高精度,远距离看个大概”。真要测远的物体,就得加大基线或用更高分辨率的相机,但代价是系统体积和算力都上去了。

这也是为什么很多产品宣传里的“测距100米”需要仔细甄别——标定参数、基线长度、图像分辨率、匹配算法都影响实际精度,理论最大距离和可用精度是两个概念。

3.1 相机标定:精度从这一步决定

标定是所有后续步骤的地基。标定要拿到两个东西:单目内参(焦距fx/fy、主点cx/cy、畸变系数k1/k2/p1/p2/k3)和双目相对外参(左右相机之间的旋转矩阵R和平移向量T)。

最常用的方法是用棋盘格标定板,采集15-20对左右相机同步拍摄的图片,要求棋盘格在画面中覆盖中心、四角和倾斜角度。注意标定板一定要平整,打印的纸板贴在不平整的快递盒上,角点提取就会不稳,后期重投影误差大得离谱。

OpenCV里标定的核心流程是这样的:

# 查找角点 ret, corners = cv2.findChessboardCorners(gray, (cols, rows), None) # 亚像素精确化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) # 单目标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) # 双目标定 ret, mtx1, dist1, mtx2, dist2, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, mtx1, dist1, mtx2, dist2, gray.shape[::-1], criteria=criteria)

一个经验参考:重投影误差小于0.3像素算不错。如果误差偏大,优先检查标定板是否平整、角点有没有提取错、图片数量和角度覆盖是否足够,而不是急着换算法。

3.2 立体校正与极线约束

立体校正(Stereo Rectification)的目标是让左右图像行对齐,把二维搜索降成一维搜索。校正之后,同一个空间点在左右图中的投影位于同一水平线上,这就是极线约束。匹配时只需要沿水平线寻找对应点,计算量大幅下降,误匹配也少很多。

OpenCV里,用 stereoRectify + initUndistortRectifyMap + remap 三步完成:

R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify( mtx1, dist1, mtx2, dist2, img_size, R, T, alpha=0) map1x, map1y = cv2.initUndistortRectifyMap(mtx1, dist1, R1, P1, img_size, cv2.CV_32FC1) map2x, map2y = cv2.initUndistortRectifyMap(mtx2, dist2, R2, P2, img_size, cv2.CV_32FC1) rect_l = cv2.remap(img_l, map1x, map1y, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map2x, map2y, cv2.INTER_LINEAR)

校正完可以做一个验证:在左右图上画同一高度的水平线,观察物体是否落在两条水平线的同一位置。如果你能看到明显的视差偏移但行位置一致,说明校正成功。这一步做不好,后面的匹配再强也白搭。

3.3 立体匹配:BM与SGBM的取舍

立体匹配是算法核心,目标就是给每个像素找到左右图之间的视差。OpenCV主流的两个方法是BM(Block Matching)和SGBM(Semi-Global Block Matching)。

BM速度快,计算量小,适合实时预览,但噪声大,弱纹理区域容易匹配错。SGBM是半全局匹配,在BM的基础上加入了多方向的代价聚合,通过惩罚路径上的视差变化来平滑结果,精度明显高一个档次,但计算量也大不少。桌面级PC上跑1080p图像,SGBM一帧可能几十到几百毫秒,实时性要优化。

我常用的SGBM初始参数:

stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=128, # 必须能被16整除 blockSize=11, # 奇数,常用5-15 P1=8 * 3 * blockSize**2, P2=32 * 3 * blockSize**2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM )

这几个参数里,numDisparities决定了可搜索的最大视差范围,它和最小测距距离相关;blockSize越大,视差图越平滑,但边缘细节会丢;P1/P2是平滑惩罚项,P2越大,对边缘变化的惩罚越大,图像会偏平滑。

3.4 深度计算与可视化

拿到视差图后,深度计算非常简单:

disp = stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0 depth = fx * baseline / (disp + 1e-6)

这里有个坑:SGBM输出的视差是定点数,需要除以16得到真实的浮点视差。另外要处理无效像素(比如图像边界和遮挡区域),通常设为一个大的无效值或NaN。也可以用 cv2.reprojectImageTo3D 配合Q矩阵直接得到三维坐标点云。

可视化方面,视差图适合用伪彩色映射,深度值则做截断映射——把0.5米到10米的范围映射到0-255灰度,超出范围置0。你会发现近处物体很亮,远处很暗,视觉上非常直观。

如果只想测某个物体的距离,别全图算深度。先用目标检测框出物体,再对框内视差取中值,比直接读取单点深度稳健得多——单点容易受噪声影响,中值能把异常值滤掉。

4.1 手机方案为什么值得试

网上流传一个很有意思的实验:利用两台智能手机实现双目立体测距实验。我试过,效果还真超出预期。手机的分辨率和传感器素质比很多廉价USB摄像头好得多,而且两台相同型号的手机基本一致性好,不需要额外购置硬件。

手机方案的短板也明显:没有硬件同步,所以只能测静态场景,运动物体一帧内左右图对不齐,深度全是错的。另外手机有自动对焦,对焦位置一变,内参就变了。解决办法是测距前把对焦锁死,长按屏幕AE/AF锁定,或者用支持手动对焦的App。这些限制让它更适合教学和原理验证,不适合做真正的在线测距系统。

4.2 具体操作步骤

第一步,把两台手机固定在同一水平线上,间距固定。可以用三脚架加长板,或者两块角铝夹住。基线实测值用尺子量两三遍,取平均值,作为标定初值。

第二步,同时拍摄标定板视频。让棋盘格在画面里缓慢移动,覆盖不同角度和位置,录制30-60秒。帧率保持30fps,后期逐帧抽帧做标定。

第三步,同时拍摄被测物体场景。静态场景下,用蓝牙遥控器或同时按下快门,尽量保证两路帧在时间上对齐。

第四步,将视频抽帧,用与第3章相同的OpenCV流程做标定、校正、SGBM匹配、深度计算。核心流程代码可以复用,只需要把输入从相机改为视频帧:

left_frames = extract_frames('left.mp4', interval=5) right_frames = extract_frames('right.mp4', interval=5) # 选一对时间最接近的帧做标定/测距

我实测的一组数据:两台同型号手机,基线14cm,在1米、2米、3米三个位置放置目标物体,测距结果误差大约在2%-5%之间。1米处误差最小,3米处误差明显增大,这跟前面分析的精度衰减规律完全吻合。

4.3 手机方案的误差来源

手机和专用双目相机最大的差距不是分辨率,而是同步和标定稳定性。滚动快门就是个大问题,两台手机即使同时按下快门,行曝光时间也可能错开,目标稍微一动,左右图就会“歪”,视差直接出错。

还有基线测量误差。你手工量到的两台手机镜头中心距离可能偏差个几毫米,这个误差会线性传导到深度值里。比如基线14cm,量成13.5cm,差3.6%,所有测距结果都系统性偏差这么多。要减小这个误差,可以在标定后反投影验证,或者用多组已知距离的物体反推基线值。

另外,手机App的图像处理管线不可控。自动白平衡、自动曝光、HDR都可能改变左右图亮度一致性,导致匹配失效。实验前把这些全关掉,或者用第三方的相机App锁定参数。

5.1 常见问题速查表

现象可能原因解决思路
重投影误差大标定板不平、角点提取错、图片数量少换硬板标定板,拍15-20对不同角度,重跑标定
视差图噪声大blockSize太小、左右图亮度不一致调大blockSize,做直方图均衡
远距离测距跳动1像素视差误差被放大多帧平均、区域中值滤波,或用更长的基线
近距离视差超范围numDisparities不够大增大numDisparities,或缩短基线
运动目标重影缺少硬件同步换静态场景,或改用单目方案+目标先验
左右图亮度差异大自动曝光/白平衡干扰锁定相机参数,或做灰度归一化
深度图出现大量黑点遮挡区域或匹配失败做空洞填充或speckle滤波

5.2 独家调试经验

标定素材不要用单张照片,直接录一段棋盘格在画面里缓慢移动的视频。后期抽帧,一小时内能采到几百帧,比手工拍几十张效率高得多,而且角度覆盖更全面。

左右相机曝光时间尽量一致。哪怕是两个型号相同的摄像头,自动增益的响应也可能不同。手动固定曝光和ISO,能显著提升匹配成功率。

调试视差图时,先看灰度图的匹配效果,再调参数。如果灰度图匹配都一塌糊涂,说明标定或校正环节有问题,别在SGBM那层死磕。

还有个容易被忽略的点:图像要先去畸变再做匹配。很多人直接用原图跑SGBM,边缘区域的匹配精度惨不忍睹。校正后的图虽然边缘有裁切,但深度值可靠得多。

顺带回答一个被问过很多次的问题:ansys mechanical里有测距功能吗?那是仿真软件里的几何测量工具,和图像视觉测距是两个完全不同的路线。如果只是要量CAD模型里的距离,直接在软件的测量命令里操作即可,不需要碰三维视觉。超声波测距、蓝牙测距这些方案则属于另一种思路——一个靠声波反射时间,一个靠信号强度衰减,它们跟双目视觉解决的问题相似,但适用场景和精度特性大不相同。

最后再分享一个我自己的实操体会:双目立体视觉测距的入门门槛没有想象中高,真正卡人的往往不是算法,而是标定认不认真、同步有没有做好、参数有没有耐心打磨。拿两台手机完成第一次实验,看到深度图里物体轮廓逐渐清晰的感觉,比单纯看教程爽得多。如果你也想动手,我的建议很简单——先别追求完美精度,把 Z = fB/d 这条链路完整跑通一次,再回来看哪里可以优化,你会对这套系统理解得更透。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询