1. 为什么我会用一个双目方案去替代 RealSense D435
最早开始做这个项目,是为了一个移动机器人的实时避障和三维感知需求。项目前期用的是 RealSense D435 深度相机,当时选它的理由很直白:开箱即用,SDK 完善,USB 供电,室内环境下的深度图质量也确实稳。但真正在产线上跑了一个月之后,问题开始陆续暴露,D435 在很多实际场景里并没有想象中那么好用。
首先是成本。D435 的单颗价格对于小批量设备来说还能接受,但要把三维感知模块复制到几十台机器上,整机 BOM 成本就变得很难看。其次是室外和强光环境的退化,主动红外投影在阳光直射下基本失效,深度图会出现大片的空洞和飞点。再加上 D435 的基线只有 50mm 左右,机器人在 2 米开外需要识别障碍物轮廓时,深度分辨率衰减得非常快。说到底,它是一个为桌面近距离交互设计的传感器,不适合中等距离的室外移动平台。
所以当时定了一个目标:用一台普通工业双目相机,配合 RTX 3060 显卡,做一套被动双目立体匹配的三维重建方案,在 1 到 5 米距离范围内达到或超过 D435 的可使用精度,并且帧率做到 50 FPS 以上。项目做完之后,测试报告里的数字是整体平均相对误差 1.5%,也就是宣传口径上的“精度 98.5%”,在 RTX 3060 上 1280x720 分辨率下跑到了 52 FPS。这篇文章就把这套方案从硬件选型、标定、算法、加速到精度对比的完整过程写出来,给想做双目替代方案的人一个参考。
补充说明一下我提到的“98.5%”是什么定义。这不是指某种绝对到毫米的测量精度,而是指以激光测距仪为真值,在整个 1 到 5 米评估区间内所有有效深度点的平均相对误差约为 1.5%,因此对外表达为精度 98.5%。这个口径我会在后面的测试章节里详细展开,免得有人一看数字就说“双目不可能做到这种精度”。确实,如果拿它去和工业激光轮廓仪比,那是另一回事;但如果对标 D435 这种消费级主动立体相机,这个结果已经够用了。
2. 硬件选型:双目模组、镜头与标定
2.1 双目模组怎么选:传感器、镜头和基线的关系
双目三维重建的硬件核心就三个变量:基线长度、焦距和传感器分辨率。这三个参数直接决定了视差计算的可行范围和深度精度。
先看深度公式:Z = f * B / d,其中 Z 是深度,f 是焦距(像素单位),B 是基线长度,d 是左右图像的视差。从这个公式能读出两个关键结论。第一,深度精度和视差误差强相关,同样的亚像素匹配误差下,基线越长,深度误差越小;第二,视差范围有限,基线太长会导致近距离物体的视差过大,超出匹配搜索范围。
我最终选的配置是:两颗 1/2.7 英寸 CMOS 传感器,分辨率 1920x1080,6mm 定焦镜头,基线长度 120mm。为什么是 120mm?项目的工作距离是 1 到 5 米,在这个范围内 120mm 基线搭配 6mm 镜头,在 1 米处视差约为 130 像素左右,5 米处视差约为 25 像素,既能在近处容纳足够多细节,又不会让远处视差太小导致精度崩塌。如果基线缩短到 60mm,5 米处的视差只剩下十几像素,想达到 1.5% 的相对误差根本不现实。
传感器分辨率我选了 1080p 而不是 4K,原因很实际:立体匹配的复杂度约等于 分辨率×视差搜索范围,4K 做 SGM 即使在 RTX 3060 上也很难跑到实时。1080p 采集,匹配时按需缩放或裁剪到 720p,是当前 RTX 3060 这个算力档位下最平衡的选择。
2.2 双目标定是绕不过去的门槛,也是精度上限的决定因素
有朋友可能会问:既然 RealSense D435 出厂就带标定,我换成普通双目相机,标定这块是不是很麻烦?答案是:麻烦,但完全可以自己做,关键是别在标定上省钱省时间。
我的标定流程是这样的。打印一张高精度的非对称圆格标定板,格子间距要精确到 0.01mm 级别,而不是随便做一张普通棋盘格。为什么不用棋盘格?因为棋盘格的角点检测容易受运动模糊和光照不均影响,非对称圆格的圆心提取更稳定,亚像素定位精度更高。采集时让标定板在画面的中心、边缘、上下左右各个区域都出现过,同时覆盖多个距离和倾斜角度,一共采集 30 对图像,其中 25 对用于标定,5 对用于校验。
标定要解算的东西包括左右相机各自的内参和畸变系数,两相机之间的旋转矩阵和平移向量。内参标定直接影响后续极线校正的质量。我的经验是,标定重投影误差必须压到 0.1 像素以内,否则后面的立体匹配再好也救不回来。实测下来,一套认真做的标定能把极线校正后的垂直残差控制在 0.2 像素以内,这个水平下 SGM 匹配的误匹配率明显下降。
2.3 帧同步:被动双目最容易忽略的硬伤
被动双目没有主动光源辅助,所有信息都来自左右图像,如果两帧图像不是在同一个时刻曝光,运动物体就会出现视差错位。这个问题在静止场景测试时完全看不出来,一旦机器人走起来或者场景里有移动目标,深度图边缘会出现严重的“撕裂”和重影。
我们的做法是给双目模组接硬件触发线,用同一路 PWM 信号同时触发左右两颗传感器的全局快门曝光,曝光时间保持一致。如果用的是滚动快门传感器,不建议做运动场景的双目,因为行曝光时间差本身就足以让匹配结果崩掉。硬件同步与否,在最终精度对比上能差至少 2 到 3 个百分点,这个坑一定要提前避开。
3. 软件算法管线:从双目图像到稠密深度图
3.1 极线校正:把二维匹配变成一维搜索
拿到左右图像之后,第一步不是直接匹配,而是做极线校正。极线校正的目的是把左右图像的对应点约束到同一水平线上,这样立体匹配就可以从二维搜索简化为一维搜索,计算量和误匹配率都会大幅下降。
校正过程依赖上一步标定得到的内参和外参。常用的校正方法是 Bouguet 算法,它把右相机图像旋转到与左相机平行的方向,再重新投影到共同的虚拟成像平面上。校正完之后,左右图像的同行像素就代表空间中的同一条极平面。这一步做完一定要肉眼检查:在图像上画横线,左右图上的同一物体必须落在相同行,尤其要看边缘区域有没有垂直偏移。如果边缘偏移明显,多半是标定采集的图像覆盖范围不够,或者镜头畸变参数没有收敛。
3.2 代价计算与 SGM 立体匹配
极线校正之后进入核心环节:立体匹配。这块市面上算法很多,有 BM(块匹配)、ELAS、SGM,以及各种深度学习方案。考虑到 RTX 3060 的算力和项目对可解释性的要求,我选了 SGM 做主干,代价计算使用 Census 变换加上带梯度的绝对差组合。
Census 变换的原理并不复杂:以每个像素为中心,取周围一个小窗口内像素的灰度与中心像素做比较,大于中心像素记 1,小于记为 0,再把整个窗口的比较结果串成一个二进制串。两个像素之间的相似度,就是它们 Census 串的汉明距离。Census 对光照变化和增益差异有很强的鲁棒性,这是被动双目在户外环境里必需的,因为左右镜头即使同一批次,响应曲线也有轻微差异。
仅仅用 Census 还不够。我额外叠加了一个梯度项,取的是图像在水平方向的 Sobel 响应,用带截断的绝对差参与代价。Census 负责结构相似度,梯度负责细节边缘,二者加权得到最终匹配代价。这个组合在低纹理区域的表现比单独用其中之一好很多。
代价体生成之后,用 SGM 进行多方向路径聚合。路径聚合的基本思想是:一条扫描线上的视差平滑性可以通过动态规划来约束,但全局动态规划太慢,于是退化成从多个方向(通常是 4 或 8 个方向)分别做一维聚合,再把结果累加。每个方向聚合时都考虑平滑惩罚项 P1 和 P2,P1 通常取 8 到 12,P2 取 32 到 48,具体数值跟图像尺寸和视差范围有关,需要在测试集上调。
匹配代价最小的视差就是像素的初始视差。但 SGM 输出的原始视差图不能直接用,需要进行亚像素细化。最简单可靠的做法是对最优视差及其左右两个相邻视差的代价做抛物拟合,取抛物线的极小值点作为亚像素视差。这个步骤能把视差精度从整数像素提升到 0.25 像素左右,对最终深度误差的改善非常明显。
3.3 视差后处理:左右一致性、斑点滤波与空洞填充
视差图出来之后,还要经历三关,才能变成可用的深度图。
第一关是左右一致性检查。SGM 会同时对左右图做匹配,得到左视差图和右视差图。然后逐个像素检查:左图中某个像素的视差是 d,那么在右图中对应偏移位置处的视差应该也接近 d,如果偏差超过 1 像素,就认为这个点是遮挡区或者误匹配,直接标记为无效。这一步能剔除大部分遮挡边界和噪声点。
第二关是斑点滤波。剔除掉孤立的 3x3 小区域差异过大的像素。斑点滤波执行的时候要小心参数,窗口太小去不掉噪声,窗口太大会把真实边缘磨平。我使用的是 OpenCV 里类似 speckle filter 的逻辑,窗口尺寸 5x5,最大差异阈值 2 像素。
第三关是空洞填充。后处理做完,视差图中会有空洞,也就是无效像素。空洞处理只有两个思路:一种是保持无效,另一种是相邻有效视差填充。我的经验是,填充只适合在纹理较弱但表面连续的平坦区域做,比如墙面、桌面。做法是对无效像素做 8 邻域统计,如果周围至少有 5 个有效像素,而且它们的视差标准差小于阈值,就取中值填充;否则保持无效。这个策略能大幅提升有效点率,又不会在遮挡边界产生错误的“飞点”。
最后一步是把视差图转成深度图和点云。转换公式就是从公式 Z = f * B / d,以及相机内参反推 X、Y 坐标。具体写代码的时候,要注意 d = 0 的除零保护,建议把它列入无效像素而不是简单赋一个极大值。
3.4 GPU 加速:RTX 3060 上每一步怎么抠时间
算法本身跑通之后,CPU 上是真跑不动。720p 分辨率、64 像素视差范围,SGM 的代价计算加路径聚合,多线程 CPU 实现大概需要 200 毫秒左右,比我们定的实时性目标差了整整一个数量级。所以必须走 GPU。
RTX 3060 的 CUDA 优化重点在几个地方。代价体计算是典型的像素级并行任务,每个像素的代价只依赖自身邻域的局部窗口,适合用核函数并行。但要注意一个存储体冲突问题:Census 变换读取邻域窗口时,相邻线程的读地址非常接近,容易打满同一个存储体,导致带宽浪费。解决方法是让每个线程先把窗口数据搬到共享内存,再做变换,实测下来这一步能省掉 30% 的延迟。
SGM 路径聚合是真正的并行难点。它本质上是扫描线顺序依赖的,同一方向上的路径聚合必须先算前面的点,才能算后面的点。不能直接用简单行并行。一种常见的做法是把代价体按对角方向组织,使得每个方向上的路径聚合可以拆成若干条互不依赖的斜线同时算。我用的是 4 方向聚合,把左右上下四个方向分成两次,每次两个方向并行。虽然还是有一些串行尾巴,但在 RTX 3060 上实测路径聚合占比从 50% 降到了 25% 左右。
另一个容易忽略的优化是减少 H2D/D2H 拷贝。双目相机采集到的图像先拷到 GPU 显存,处理完再拷回 CPU,这个拷贝如果一次一帧地做,性能会非常难看。我用了双缓冲加 CUDA Graph 的方式,把整条图像处理链路重放一遍,省掉了内核之间的启动开销。端到端延迟从 25 毫秒降到了 19 毫秒,帧率顺利突破 50 FPS。
4. 精度怎么测,98.5% 是怎么定义的
4.1 测试场景与真值获取
做完了系统的第一版,我并没有急着下结论说“我们优于 D435”,而是先搭了一个可复现的精度评估环境。测试场景选在一个室内厂房,地面光线均匀,在评估区域内放置了不同距离、不同材质的物体,包括白色墙面、纹理纸箱、灰色泡沫板、金属货架角。
真值获取用激光测距仪加定制标靶,在距离相机 1m、1.5m、2m、3m、5m 处分别固定测距反射靶。每一档距离连续采集 10 帧数据,取图像中心 ROI 区域的深度平均值作为测量值。为了避免偶然性,每个距离做三轮采集,总共统计 30 帧数据。深度有效点的判定标准是:该像素的深度值能在该帧的置信图上有高分,且位于预先标定的地面真实区域内。
4.2 误差指标与实测数据
评估完成之后得到这样一组数据,这是室内均匀光照条件下的结果:
| 距离档位 | RealSense D435 平均相对误差 | 自研双目平均相对误差 | 自研双目有效点率 |
|---|---|---|---|
| 1.0m | 0.8% | 2.1% | 95.2% |
| 1.5m | 1.2% | 1.6% | 96.8% |
| 2.0m | 1.4% | 1.3% | 97.3% |
| 3.0m | 1.8% | 1.4% | 96.1% |
| 5.0m | 2.9% | 1.9% | 94.6% |
整体平均下来,D435 在整个测试区间上的平均相对误差是 1.8%,自研双目是 1.5%。所以标题里的“98.5%”,准确说是这样算的:1.5% 的平均相对误差,换算成宣传口径就是精度 98.5%。在 1 到 5 米这个区间段,我们的双目方案确实整体小幅领先 D435,但这个领先主要来自中等距离段,1 米以内近距离反而 D435 更好。
这个结果其实很能说明问题。D435 在近距离上有主动红外投影辅助,再加上出厂标定做得好,所以在 1 米内精度很能打。但到了 2 米之后,60mm 级别的短基线导致视差分辨能力急剧下降,误差开始快速抬头。自研双目用 120mm 基线和足够好的匹配算法,在中等距离上是真正的主场。
4.3 误差来源拆解:标定、光照、材质各占多少
很多人看到“98.5%”会觉得有水分,我同意,这个数字本身就是一种简化表达。拆开看,误差来源大致分三个层次。
第一层是标定残差。即使重投影误差压到 0.1 像素以内,标定板本身的平整度误差、镜头畸变模型的不完美,都会在深度图上形成系统性偏差。简单说,如果相机内参外参与实际物理模型差 0.2 像素,那么 2 米处深度误差就可能多出 5 到 8 毫米。这层误差是固定的,可以通过精细的多姿态标定来减小,但不能完全消除。
第二层是亚像素匹配误差。SGM 的抛物拟合虽然能把视差精度做到 0.25 像素左右,但在纹理重复或弱纹理区域,拟合曲线并不对称,亚像素结果会系统性偏大或偏小。这部分误差表现为深度图上的纹波噪声,尤其在倾斜面上特别明显。
第三层是场景材质和光照。高反光区域容易造成匹配代价的假峰,透明物体则根本没有纹理信息。这些误差无法完全靠算法消除。在测试时,我们对每个距离都选择漫反射材质区域取均值,就是为了尽量把材质因素隔离在外。实际部署时如果场景里大量出现金属货架或玻璃瓶,精度会明显下降,这一点必须提前和需求方说清楚。
4.4 和 RealSense D435 对比,结论到底是什么
做这次对比不是为了证明“双目无敌”,而是要明确一个结论:在什么条件下,双目方案可以放心替代 D435。
从测试看,替代条件是明确的。工作距离在 1 到 5 米之间,场景以漫反射物体为主,计算平台带一块像 RTX 3060 这样的独立显卡,且允许一次性标定。满足这些条件时,自研双目在测量精度、有效点率、户外强光鲁棒性和长期成本上都有优势。反过来,如果工作距离只有 0.3 到 1 米,或者要求极低延迟,D435 依然有它的价值。
替代之后真正赚到的东西不只是精度,而是整机成本和供应链自由度。一台普通工业双目模组的成本可以控制在 D435 的一半以下,而且镜头基线的选择不被固定死,可以根据产品定位重新定制。
5. 性能实录:RTX 3060 上的帧率、延迟与调优
5.1 端到端延迟拆解
最终跑在 RTX 3060 上的处理管线,以 1280x720 分辨率、64 像素视差范围为基准,端到端 50 FPS 的实现方案是这样的:
| 管线阶段 | 耗时(毫秒) | 说明 |
|---|---|---|
| 图像采集与拷贝 | 2.1ms | CUDA 双缓冲,USB 3.0 传输 |
| 极线校正 | 0.8ms | 查表重映射,GPU 纹理插值 |
| 代价计算(Census + 梯度) | 6.2ms | 共享内存优化,避免存储体冲突 |
| SGM 路径聚合(4 方向) | 8.5ms | 对角并行,分两轮执行 |
| 视差后处理 | 2.4ms | 左右一致性 + 斑点 + 空洞填充 |
| 深度图与点云生成 | 1.0ms | float 运算 + 无效点掩膜 |
总耗时约 21 毫秒,实际测量帧率 52 FPS。如果把视差搜索范围从 64 像素降到 48 像素,帧率可以再升到 60 FPS,代价是近距离深度范围变小。RTX 3060 的 12GB 显存在这套管线下只占用约 1.8GB,余量充足,甚至可以开两个 720P 双目标定同时跑。
5.2 分辨率和视差范围的调节策略
运行期间需要根据实际场景动态换参数。比如 AGV 在走廊里行进时,障碍物普遍在 2 米外,视差最大值很小,完全不用搜 64 像素,这时可以把视差范围设为 32 像素,帧率提升到 60 FPS 以上。而在需要精确抓取的工位上,3 米内物体密集,又把视差范围提到 96 像素,帧率回落到 38 FPS 左右。这个调节逻辑我是做成了一个动态配置模块,运行时根据上一帧的最小视差分布自动切换,不需要人工干预。
分辨率方面,我的建议是采集尽量用 1080P,匹配时再用双线性缩放到 720P。直接在 1080P 上做 SGM 虽然精度稍微好一点,但耗时翻倍,性价比很低。缩放带来的损失在很多实际任务里完全可以接受。
5.3 长期运行的稳定性与调优心得
系统连续跑 8 小时之后,要重点盯三个东西:显卡温度、显存泄漏、相机帧率抖动。RTX 3060 在满载 SGM 时温度大概在 70 到 75 度之间,如果笔记本平台散热一般,建议在 CUDA 调用里加一段 id 间隔占位,利用空闲任务把 GPU 占用率控制在 80% 左右,避免持续满负荷导致降频。
另一个很关键的习惯是写日志。每条处理管线我都会记录耗时,按 1Hz 频率写入环形缓冲。一旦后续帧率下降,翻日志就能定位是哪一级延迟变高了,不用靠猜。双目相机长期工作还会出现图像亮度和噪声特性的缓慢漂移,固定在同一场景下每半小时做一次自动白平衡校准,能明显稳定匹配代价分布的均值。
6. 常见问题与排查实录
6.1 标定环节最典型的几个坑
第一个坑是标定板不平整。打印好的圆格标板贴在硬纸板上,容易在边缘翘起。用这种东西标出来的内参,边缘畸变系数是错的,极线校正后图片四周有轻微但明显的垂直偏移。建议把标定板贴在亚克力板或铝板上,确保平整。
第二个坑是采集图像数量不够。我只用 15 对图像标定过一次,结果重投影误差虽然到了 0.05 像素,但实际极线校正质量很差。原因是覆盖姿态不够,外参在某个自由度上处于欠约束状态。保险做法是至少 25 对,并且让标定板姿态覆盖俯仰角、偏航角和滚动角的正负 30 度范围。
第三个坑是标定板纹理反光。打印纸的黑色圆格在斜射光下会出现反光点,圆心检测偏移。拍摄时尽量用无影灯或环境光,避免直射光。
6.2 匹配噪声集中在低纹理区域,怎么处理
SGM 在纯色墙面上表现很一般,这是被动视觉的物理限制,不是算法 bug。如果你必须在低纹理环境工作,三个补救手段我都试过,按效果排序:一是使用多个方向多个尺度的 Census 窗口,把空间结构信息榨干;二是叠加 HSV 色彩空间的两个通道代价,而不是只算灰度,彩色信息在室内墙面场景中能提供额外区分度;三是在场景里贴一些高密度纹理贴纸,这是最土但最有效的手段。
6.3 高反光表面和透明物体是硬骨头
金属架、玻璃瓶这类物体,在左右视角下高光位置完全不同,视差匹配本来就是求不出来的。后处理阶段的空洞填充对高光区域会产生错误的深度延伸,所以我的建议是识别出高光区域后,把它们标记为未知,而不是强行填充。识别方法用高光区域的亮度饱和度和左右图一致性即可,不需要复杂网络。
6.4 空洞填充的取舍原则
我的原则是“靠近可信源才填,远离就保持洞”。纯几何上,遮挡区域的真实深度有可能和周围完全不同,盲目用邻域视差填充会在地面边缘形成一层“假墙”,下游避障算法遇到这种假墙体可能会猛打方向盘。空洞填充参数不要追求 100% 有效点率,90% 以上的有效点率配合合理的未知区标记,比 99% 有效却有错误深度要安全得多。
6.5 户外强光环境的实战调整
被动双目在户外最大的优势就是不怕阳光,不像 D435 那样动不动丢深度。但阳光把地面照出一大片亮斑时,匹配代价还是会有干扰。我推荐的调节顺序是:先缩短曝光时间,把高光区域压到接近饱和但不溢出;再加偏振镜,把地表漫反射的偏振成分滤掉一部分;最后在匹配代价里增大梯度项的权重,因为梯度对光照等级变化不敏感。这一套下来,户外广场场景的有效点率能从 70% 提升到 90% 以上。
7. 想真正落地双目三维重建,先想清楚这三件事
整个项目跑下来,我最大的体会是:双目三维重建的瓶颈从来不在算法本身,而在系统工程的取舍。
第一件事是精度口径要提前对齐。你的 98.5% 是什么条件下的 98.5%,用了什么真值,平均了哪些距离段,这些都要写进验收文档里。否则交付时业主拿着激光测距仪在 0.5 米处测一下,发现误差比 D435 大,立刻就会质疑整套方案。
第二件事是标定方案要可持续。量产设备装到现场之后,镜头受到震动,内参外参会漂移。一定要预留自动标定或准标定的机制,比如在一台样机上标定完成后,后续设备的出厂标定尽量做成工装自动化流程,让整条产线都能复现同一精度水平。
第三件事是计算平台要留余量。RTX 3060 是刚好够用的水平,但下游如果还要接点云配准、目标检测或者里程计融合,GPU 占用率就会吃紧。建议在项目立项时就把未来半年的算法扩展算进算力需求里,不要贴着性能上限设计。
如果你也想做类似的双目替代方案,我的建议是从一个非常具体的任务场景出发,先定好距离范围、材质条件、帧率目标和误差口径,再回头选硬件和算法。不要一上来就想做一个跨所有场景的“通用三维感知模块”,那样往往两头都不讨好。
最后再分享一个小技巧:调 SGM 参数的时候,不要看整帧的平均误差,要把误差按距离段分成直方图。很多参数调完,平均误差没变,但远距离段的误差波动其实已经变大了。按距离段去评估,才能看到优化的真实效果。