深度相机这个词这几年被用得很泛,好像只要输出一张带距离信息的图就能叫深度相机。但真正做过项目的人都知道,双目、结构光和 ToF 这三种主流 3D 成像方式,从物理原理到工程实现几乎是三套完全不同的活儿,选错了方案,后面无论怎么调参数都是在跟物理规律较劲。我这几年前后做过物流体积测量、机器人避障、工业件检测和消费类人机交互几个方向,三种方案都完整走过从选型、标定到量产调试的流程,踩过的坑够写一本小册子。这篇就把双目、结构光和 ToF 三条路线拆开讲透:每种方案靠什么物理量换深度、精度公式怎么算、量程边界在哪、什么场景该选谁,以及实际调试中最容易翻车的那些细节。不管你是刚接触深度感知、准备做方案选型,还是已经在调某一家的模组但点云一直不干净,下面这些内容应该都能直接拿去用。
1. 三条路线的分野:先搞清楚问题卡在哪
做 3D 成像,本质上要解决的都是同一件事:把一张二维的像素阵列,变成一组带 (X, Y, Z) 的三维坐标。相机传感器本身只能记录光强,Z 这个维度要么靠几何关系"算"出来,要么靠时间/相位"测"出来。双目和结构光走的是前一条路——用三角测量,靠两台"相机"(其中一台可以是投影仪)之间的基线做几何解算;ToF 走的是后一条路——直接给光打个时间戳,测它跑一趟回来花了多久,或者产生了多少相位延迟。这个根本差异决定了后面所有的性能取舍。
我习惯用一个类比来跟团队新人解释:双目像人用两只眼睛判断远近,靠的是左右眼看到的画面有差异,脑子把这个差异换算成距离;结构光像在黑屋子里拿一支手电筒打出一层细密的光斑,光斑落在不同距离的物体上会变形,看变形就能反推形状;ToF 则像蝙蝠回声定位,发出一束光,等它弹回来,看等了多久。这三句话虽然粗糙,但基本抓住了各自的技术内核,也解释了为什么双目怕无纹理、结构光怕强反光、ToF 怕多径反射。
1.1 从二维像素到三维坐标,缺的都是同一块信息
双目和结构光共享同一套三角测量公式:已知两个光心之间的距离 b(基线)、相机的等效焦距 f,以及同一个物点在两个成像面上的位置差 d(视差),深度就是 Z = f·b/d。这个公式简单到可以在白板上三秒钟写完,但它是整个被动/主动立体视觉的地基。它同时告诉你一件很重要的事:深度不是被"测量"出来的,而是被"除法"算出来的,分母是视差,所以视差只要差一点点,深度就会差一大截——这是所有立体视觉方案精度随距离平方衰减的根源。
ToF 走的是完全另一条数学路径。连续波调制(iToF)测的是发射光和接收光之间的相位差 φ,深度 d = c·φ/(4π·f_mod),其中 c 是光速、f_mod 是调制频率。脉冲飞行时间(dToF)更直接,测的是光子的往返时间 t,深度 d = c·t/2。两条路径的共同点是:深度分辨率直接由时间或相位的测量分辨率决定,与场景纹理无关,这也是 ToF 在无纹理表面(白墙、纯色地面)上表现稳定的原因。
下面这张表是我在做方案预研时用来快速对齐认知的,把三种方式的"物理抓手"和由此衍生的强弱项摆在一起看,很多选型问题当场就有答案了。
| 维度 | 双目立体视觉 | 结构光 | ToF(iToF / dToF) |
|---|---|---|---|
| 深度信息来源 | 视差(几何三角) | 视差(几何三角,投影仪替代一台相机) | 相位差或飞行时间 |
| 是否主动发光 | 被动(可加辅助纹理光) | 主动 | 主动 |
| 精度随距离变化 | 平方衰减,最快 | 平方衰减,但基数小 | 近似线性衰减 |
| 典型工作距离 | 0.3 m ~ 10 m+ | 0.2 m ~ 2 m | 0.1 m ~ 5 m(iToF),数米~上百米(dToF) |
| 对无纹理表面 | 极差 | 好(自带纹理) | 好 |
| 对高反光/镜面 | 一般 | 差 | 一般偏差 |
| 分辨率 | 高(跟随相机) | 高(跟随相机) | 低(受像素与调制限制) |
| 成本结构 | 低,纯相机+算力 | 中,需投影模组 | 中到高,需专用传感器 |
1.2 为什么没有一种方案能通吃
经常有人问我:既然 ToF 不怕无纹理、结构光精度高,为什么还要用双目?答案藏在成本结构和场景约束里。双目只需要两颗普通图像传感器加算力,硬件成本是三兄弟里最低的,而且分辨率随便堆——两颗 500 万像素的模组做出来的点云密度,ToF 在可见的未来都追不上。它的代价是把复杂度全部推给了算法和算力,以及在某些场景下物理上就是拿不到数据。
结构光的处境有点像"精度刺客":在 0.3 到 1 米这个距离段,它能做到亚毫米级的重复精度,工业检测、人脸建模这类应用几乎绕不开它。但它的投影模组需要精密的衍射光学元件来做图案投射,标定一旦漂移整个系统就废了,而且遇到镜面、高反光金属、阳光直射,基本就是当场罢工。ToF 的优势在于"直接"和"鲁棒":帧率高、算法轻、对纹理不敏感、结构紧凑,特别适合需要实时性的移动平台。缺点是分辨率上不去、边缘飞点多、多机同时工作会互相干扰。
所以真实的工程决策从来不是"哪个技术更先进",而是"在我的距离段、我的材质、我的成本、我的算力预算下,哪个方案的失效模式是我能兜住的"。这句话我在每个项目立项会上都会重复一遍,因为它能省掉很多无谓的技术争论。
2. 双目立体视觉:最像人眼,也最考验算法
双目是我接触的第一个深度方案,也是很多团队做深度感知的起点,因为它的硬件门槛实在太低了——两颗相机、一个刚性支架、一块能跑匹配算法的板子,就能出点云。但"能出点云"和"点云能用"之间隔着的距离,往往比想象中大得多。我见过不少项目在实验室的标定板前面效果漂亮,一到现场面对白墙、玻璃、阳光就全线崩盘,问题基本都能追溯到几个固定的地方。
2.1 三角测量的核心公式,以及误差是怎么被放大的
先把公式摆清楚。假设左右相机已经过极线校正,同一个物点在左图的横坐标是 x_L,在右图是 x_R,那么视差 d = x_L - x_R,深度 Z = f·b/d,其中 f 是校正后的等效焦距(单位像素),b 是基线(单位米)。把 X、Y 也算出来:X = (x_L - c_x)·Z/f,Y = (y - c_y)·Z/f,这就是从视差图反投影出点云的完整链路。
关键在于误差传递。对 Z = f·b/d 求导,得到深度误差与视差误差的关系:
ΔZ = (Z² / (f·b)) · Δd
这个公式值得贴在墙上。它说明三件事:第一,深度误差随距离平方增长,2 米处的误差是 1 米处的 4 倍;第二,加大基线 b 或者提高焦距 f 都能降低误差,但两者都有物理上限——基线太大会导致近距离视差超出搜索范围、遮挡区域变多,焦距太大会缩小视场角;第三,提升视差精度 Δd 是最直接的收益来源,而 Δd 取决于匹配算法、图像质量和纹理丰富程度。
拿一组真实参数算一遍。假设 f = 800 像素(对应约 2.8 mm 焦距配 3.5 μm 像元),b = 120 mm,匹配算法能做到 0.25 像素的亚像素精度:
| 测量距离 Z | 视差 d | 深度误差 ΔZ |
|---|---|---|
| 0.5 m | 192 px | 约 0.65 mm |
| 1.0 m | 96 px | 约 2.6 mm |
| 2.0 m | 48 px | 约 10.4 mm |
| 3.0 m | 32 px | 约 23.4 mm |
| 5.0 m | 19.2 px | 约 65 mm |
看完这张表就明白为什么双目在近距离其实很能打,而到了 3 米以外如果没有长基线和大靶面,精度会迅速跌到厘米级。反过来,最小可测距离也受限制:如果搜索视差范围是 128 像素,那么 Z_min = f·b/d_max = 800×0.12/128 ≈ 0.75 m,比这更近的物体视差会超过搜索窗口,直接被算法判定为无效。这个坑我踩过一次——项目要求测 0.5 米,结果模组按 128 视差配的参数,近处一片空洞,排查了半天才反应过来是量程不够,把 numDisparities 提到 256 才解决。
2.2 极线校正:把二维搜索降成一维
原始的双目图像里,同一个物点在左右图中的位置既不同行也不同列,做匹配要在二维邻域里搜索,计算量是灾难级的。极线校正的作用就是通过一次单应变换,把两张图重投影到同一个虚拟成像平面上,使得同一个物点严格落在同一行——之后匹配只需要在水平方向找对应点,搜索空间从 O(W²) 降到 O(W)。
这一步的实现依赖于立体标定。标准流程是:先用棋盘格分别标定左右相机的内参(焦距、主点、畸变系数),再做双目标定,求出两台相机之间的旋转 R 和平移 T;然后调用 stereoRectify 计算两个校正变换矩阵和投影矩阵,其中投影矩阵里的第四行对应的就是那个著名的 Q 矩阵;接着用 initUndistortRectifyMap 生成映射表,remap 把原图重采样到校正后的坐标系。这套流程看起来很标准,但我在实际项目里发现至少一半的"校正后仍有明显垂直视差"的问题,都出在标定阶段,而不是代码写错了。
最常见的两个错误:一是标定图像覆盖范围不足。很多人拍十几张棋盘格全挤在画面中央,四角的畸变根本没被约束住,标出来的畸变系数在边缘区域就是错的,校正之后画面中间很准、四周仍然偏。正确做法是让棋盘格出现在画面的四角、中心、上下左右各个位置,并且要有不同倾斜角度(±30° 左右),一般 20 到 30 组比较稳妥。二是忽略了标定板的平整度。打印在普通 A4 纸上的棋盘格,贴到亚克力板上稍微有点气泡,或者纸张受潮起皱,重投影误差看着不大,但实际的空间精度会受影响。如果精度要求高,直接用陶瓷或玻璃基底的标定板,别省这个钱。
2.3 从标定到点云的完整实现
下面这套代码是我常用的最小可运行骨架,基于 Python 和一个主流开源视觉库。它的价值不在于代码本身有多精妙,而在于把整条链路的顺序和常见参数交代清楚,你可以直接改改参数就跑起来。
import cv2 import numpy as np # ---------- 1. 单目标定 ---------- # 棋盘格内角点 9x6,方格边长 25 mm pattern = (9, 6) square = 0.025 objp = np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) * square objpoints, imgpoints_l, imgpoints_r = [], [], [] for path_l, path_r in pairs: img_l = cv2.cvtColor(cv2.imread(path_l), cv2.COLOR_BGR2GRAY) img_r = cv2.cvtColor(cv2.imread(path_r), cv2.COLOR_BGR2GRAY) ok_l, c_l = cv2.findChessboardCorners(img_l, pattern) ok_r, c_r = cv2.findChessboardCorners(img_r, pattern) if not (ok_l and ok_r): continue crit = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) c_l = cv2.cornerSubPix(img_l, c_l, (11, 11), (-1, -1), crit) c_r = cv2.cornerSubPix(img_r, c_r, (11, 11), (-1, -1), crit) objpoints.append(objp) imgpoints_l.append(c_l) imgpoints_r.append(c_r) ret_l, K_l, D_l, _, _ = cv2.calibrateCamera(objpoints, imgpoints_l, img_l.shape[::-1], None, None) ret_r, K_r, D_r, _, _ = cv2.calibrateCamera(objpoints, imgpoints_r, img_r.shape[::-1], None, None) # 经验阈值:单目重投影误差 < 0.2 px,双目标定后 < 0.3 px 才算可用 # ---------- 2. 双目标定 ---------- ret, K_l, D_l, K_r, D_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, K_l, D_l, K_r, D_r, img_l.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC # 单目标定可靠时固定内参,只解算外参 ) print("baseline(mm) =", np.linalg.norm(T) * 1000, " reproj =", ret) # ---------- 3. 立体校正 ---------- R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify( K_l, D_l, K_r, D_r, img_l.shape[::-1], R, T, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0 ) map1x, map1y = cv2.initUndistortRectifyMap(K_l, D_l, R1, P1, img_l.shape[::-1], cv2.CV_32FC1) map2x, map2y = cv2.initUndistortRectifyMap(K_r, D_r, R2, P2, img_l.shape[::-1], cv2.CV_32FC1) # ---------- 4. 视差计算 ---------- sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=256, # 必须是 16 的整数倍 blockSize=5, # 奇数,纹理弱时调到 7~11 P1=8 * 5 * 5, P2=32 * 5 * 5, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, preFilterCap=63, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) left_rect = cv2.remap(img_l, map1x, map1y, cv2.INTER_LINEAR) right_rect = cv2.remap(img_r, map2x, map2y, cv2.INTER_LINEAR) disp = sgbm.compute(left_rect, right_rect).astype(np.float32) / 16.0 # ---------- 5. 反投影点云 ---------- points = cv2.reprojectImageTo3D(disp, Q) mask = disp > disp.min() + 1.0 cloud = points[mask]这段代码里有几个参数值得单独说。numDisparities 决定了最近可测距离,它和最小可测距离成反比,宁可开大一点,代价只是算力;blockSize 是匹配窗口,纹理弱的时候要加大到 7 或 11,但加大会导致边缘变糊、细小结构丢失;uniquenessRatio 是唯一性检验,对抑制重复纹理误匹配很有效,纹理单一的场景可以提到 15;P1/P2 是平滑惩罚项,官方推荐的初始值是 P1 = 8×通道数×blockSize²,P2 = 32×通道数×blockSize²,遇到噪点多就往上调,遇到边缘糊就往下调。SKIP 一下,这里不展开讲每种模式的区别,实际用起来 SGBM_3WAY 在精度和速度之间平衡得最好。
需要提醒的是,SGBM 输出的视差图是乘以 16 的定点数,必须除以 16 再送进 reprojectImageTo3D,这个细节漏掉的话点云深度会整体放大 16 倍,我第一次用的时候就被这个坑了半小时。另外 reprojectImageTo3D 必须有合法的 Q 矩阵,而 Q 来自 stereoRectify,所以如果你手动改过校正参数,Q 也要同步更新,两者不匹配会导致点云整体扭曲。
2.4 双目最容易翻车的几个场景
无纹理表面是双目的头号天敌。白墙、纯色纸箱、没有印刷图案的塑料外壳,左右图在这些区域长得一模一样,匹配算法找不到唯一对应点,输出就是大片空洞。解决办法有两条:一是加主动纹理投射,用一颗激光散斑投射器往场景里打随机斑点,人为制造纹理——这其实就是把双目往结构光的方向推了一步,市面上很多所谓"主动双目"就是这个思路;二是靠算法补,用 WLS 滤波或者基于深度学习的立体匹配网络把空洞填上,但填出来的值本质是插值,精度不能当真。
光照不均和过曝也会毁掉匹配。左右相机的曝光如果不一致,同一块区域在左图亮、右图暗,基于灰度相似度的代价计算就会失效,表现为整片区域的视差不连续。工程上的做法是让两颗相机共用同一个曝光控制环路,或者干脆锁死曝光时间,靠补光灯保证亮度。还有逆光场景,太阳直射进镜头会产生大面积饱和,那一片的视差全部丢失,加遮光罩是最简单有效的办法。
最后是同步问题。如果两颗相机不是硬件同步触发,拍动态物体时左右图的时刻不一致,物点位置对不上,视差计算直接出错。做静态测量可以不管,但只要是移动平台或者被测物在动,就必须用硬件触发线把两颗相机的曝光时刻对齐,误差控制在微秒级以内。这一点在选模组的时候就要确认,别等做完了才发现两颗相机只能软触发。
3. 结构光:用投影"刻"出纹理
结构光的思路特别巧妙:既然双目怕无纹理,那我就主动往场景里投射一套已知的图案,让每个像素都有独一无二的特征,匹配问题瞬间从"猜"变成"查表"。从几何上看,结构光系统和双目没有本质区别,只是把其中一台相机换成了一台投影仪——投影仪的光心就是第二个"相机光心",投射的图案就是它"看到"的图像。所以前面那套三角测量的公式、误差传递的规律,在结构光这里全部适用,只是基线的两端变成了相机和投影仪。
3.1 把投影仪当成第二只"眼睛"
理解了这一点,很多工程细节就顺理成章了。结构光模组的基线指的是相机光心和投影仪光心之间的距离,一般比双目小得多,常见在 30 到 100 mm 之间——因为投影仪需要和相机共享大部分视场,基线太大重叠区域就没了。基线小意味着同样的视差误差下深度误差更大,但结构光的补偿在于它能拿到极其精确的"视差":投射的图案是已知的,解码出来的对应点精度可以做到亚像素甚至更高,这就把 Δd 压得很小,整体精度反而上去了。
另一个关键点是投影仪也需要标定。很多人只标相机不标投影仪,结果点云整体有系统性偏差。投影仪不是相机,不能直接拍棋盘格,常用做法有两种:一是把投影仪当作逆光路相机,投射棋盘格到白板上,用相机拍下来,通过已知的相机位姿反算投影仪的内外参;二是把投影仪和相机组成一个整体,直接标定"相机-投影仪"系统的相位-深度映射关系,用几个已知深度的平面做拟合。前者通用性强,后者精度更高但换一个模组就要重标。
3.2 编码策略:格雷码、相移与散斑
结构光的"结构"就体现在图案的编码方式上,不同的编码方式直接决定了精度、速度和鲁棒性,这是选型时最该看懂的部分。
格雷码属于二值编码,把投影区域按二进制切成若干块,每次投射一位,接收端根据每个像素在序列中的明暗变化解出它属于哪一块。优点是解码简单、抗噪能力强、对反射率变化不敏感;缺点是需要投射 log₂(N) 张图,N 是条纹数,要做到高分辨率就得投十几张,不适合动态场景。而且格雷码在条纹边界处容易出错,因为边界像素会同时接收到相邻两个码区的光,解出来的码字是非法的。
相移法是目前精度最高的一类。它投射正弦条纹,通过移动相位投射多张(常见三步、四步、五步),然后对每个像素解出相位:
φ = arctan2(I₃ - I₁, I₀ - I₂) # 四步相移,相位步进 90°
这个公式简洁得让人愉悦,但它给出的是包裹相位,范围只有 [-π, π],对应一个条纹周期内的位置。要得到绝对深度,必须做相位展开(unwrapping),把跨越多个周期的相位接起来。展开的方法有多频外差、格雷码辅助、多波长等等,实际项目里最常见的是"格雷码 + 相移"的组合:格雷码负责粗定位到哪一个周期,相移负责周期内的精确定位,两者结合既保证了绝对测量又保证了精度。
散斑投射则是另一条路,它不追求精确的相位,而是投射一张随机斑点图,然后像双目一样做块匹配。优点是单帧就能出深度,适合动态场景,而且模组可以做得很小、成本很低,很多消费级设备用的就是这套。代价是精度不如相移法,而且对斑点图案的质量依赖极大——斑点的大小、密度、随机性都直接影响匹配效果,这类图案通常是用衍射光学元件一次性做出来的,做坏了没法调。
3.3 精度与量程的参数账
结构光的精度同样可以用三角测量的误差公式估算,但要注意这里的"视差"是编码位置差。假设投影图案的一个周期对应 T 个像素的视差变化,相位测量精度为 Δφ(弧度),那么等效视差精度 Δd = T·Δφ/(2π),代入误差公式:
ΔZ = (Z² / (f·b)) · (T · Δφ / (2π))
举个具体数字:f = 1500 像素,b = 60 mm,Z = 0.5 m,条纹周期 T = 32 像素,相位精度 Δφ = 0.05 rad(约 3°)。先算 Z²/(f·b) = 0.25/(1500×0.06) = 0.00278 m/px,再算等效视差误差 32×0.05/6.283 ≈ 0.255 px,乘起来得到 ΔZ ≈ 0.7 mm。这与实际模组在 0.5 米处的表现基本吻合,也解释了为什么结构光在近距离能做到亚毫米级。
量程方面,结构光的最近距离受限于相机和投影仪的视场重叠——太近了投影图案可能压根打不到相机视野里;最远距离受限于投影光功率和图案的对比度,距离一远,投影的条纹在相机里就糊成一团,解码失败。实际可用的工作范围通常是 0.2 到 1.5 米,超过 2 米还能用的模组基本要定制,而且成本会明显上升。
3.4 现场调试的几个硬经验
第一个经验是关于曝光的。结构光对曝光极其敏感,因为解码依赖条纹的对比度。场景里只要有一样东西反光强烈(比如金属件、包装膜),那一片就会过曝,条纹被削平,相位解算直接失败,而且失败区域往往会向外扩散,形成大片错误点云。解决办法是做多曝光融合:同一个图案用不同曝光时间拍几张,取每张里不饱和的像素合成一张 HDR 图再解码。这会牺牲帧率,但在工业检测这类静态场景完全值得。
第二个经验是关于互反射的。当被测物有凹陷、折角或者两个物体靠得很近时,投影的光打到 A 面反射到 B 面,再被相机接收,相机以为这是 B 面直接反射的光,算出来的深度就错了。这种误差在点云上表现为"褶皱"或者虚假的凸起,而且在单视角下几乎无法从数据上分辨。缓解手段有限:改变投射角度、多视角融合、或者用偏振片抑制。真正高精度的检测方案通常是多相机多投影仪从不同角度拍,靠冗余来剔除错误点。
第三个经验是温度漂移。投影仪里的激光光源和衍射元件都会随温度变化产生微小的形变,导致投射图案发生位移,而这个位移在系统标定里是没有被建模的。结果是设备开机半小时后精度慢慢变差,重启又好了。工程上的做法是开机预热一段时间再做标定,或者内置温度传感器做补偿表。这个坑很隐蔽,如果没遇到过,可能会花几周时间去怀疑算法。
4. ToF:直接测量光的飞行时间
ToF 的哲学和前面两种完全不同:它不关心场景长什么样,只关心光跑了多久。这个特性让它在无纹理表面上表现得极其稳定,也让它的误差模型跟双目、结构光几乎不重叠。ToF 分两大流派——连续波调制(iToF)和脉冲直接飞行时间(dToF),两者在原理、器件和适用场景上差别不小,实际选型时经常被混为一谈,这里分开讲。
4.1 iToF 的相位法
iToF 发出的是经过正弦调制的连续光,接收端把回光与本地参考信号做相关,通过多次不同相位的采样解出相位差。最经典的是四相位采样,在 0°、90°、180°、270° 四个时刻分别采集电荷量 I₀ 到 I₃,然后:
φ = arctan2(I₃ - I₁, I₀ - I₂)
深度 d = c·φ/(4π·f_mod)。同时还能算出两个副产品:幅度 A = √((I₃-I₁)² + (I₀-I₂)²)/2,反映回光强度,可以用来判断该像素是否可信;偏置 B = (I₀+I₁+I₂+I₃)/4,反映环境光强度,可以用来做环境光抑制。
相位法的固有问题是模糊距离。因为相位只能测到 2π 的范围内,超过这个范围就绕回来了,最大不模糊距离 d_max = c/(2·f_mod)。调制频率 20 MHz 对应 7.5 米,50 MHz 对应 3 米,100 MHz 对应 1.5 米。想要远距离就得用低频,但低频的相位测量精度差,深度误差大。这个矛盾的标准解法是多频调制:用两个或三个不同频率同时测,靠中国剩余定理的思路解出唯一距离。比如 20 MHz 和 21 MHz 组合,拍频相当于 1 MHz,不模糊距离一下扩展到 150 米,同时保留了高频的精度。代价是采样帧数翻倍,功耗和发热都上去了。
iToF 的深度精度同样可以算:
Δd = c · Δφ / (4π · f_mod)
假设调制频率 100 MHz,相位测量精度 1°(0.01745 rad),则 Δd = 3×10⁸ × 0.01745 / (4π × 10⁸) ≈ 4.2 mm。这个数字和市面上多数 iToF 模组在 1 米左右的实际表现是对得上的。想提高精度就得提高调制频率,但频率上去之后不模糊距离又下来了,这就是 iToF 设计里永恒的两难。
4.2 dToF 的单光子计数
dToF 更接近"雷达"的思路:发射一个极短的光脉冲(纳秒甚至皮秒级),然后用单光子雪崩二极管(SPAD)记录每个光子的到达时间,反复发射几百上千次,把到达时间做成直方图,直方图的峰值位置就是飞行时间。它的核心器件是 SPAD 阵列加上时间数字转换电路,每个像素独立计时,属于典型的"单光子统计"。
dToF 的优势非常明显:直接测时间,不存在相位模糊问题,天生适合远距离;单光子探测灵敏度极高,微弱回光也能捕捉;抗环境光能力强,因为环境光子在时间上是均匀分布的,而信号光子集中在一个窄峰里,做直方图之后信噪比很好。缺点是器件工艺复杂、像素填充率低、分辨率难做高,而且高精度的时间测量电路功耗不低。
工程上 dToF 有几个特有的误差要处理。一是走离误差(walk error),当回光很弱或者脉冲很宽时,直方图的峰值会被"拉偏",导致测距偏大。二是多径干扰,光在场景里反弹多次才回来,直方图上会出现多个峰,选错峰就测错。三是飞点,在物体边缘,一个像素可能同时接收到前景和背景的回光,直方图上两个峰都很强,融合出来的深度落在两者之间,形成一个悬空的点。这些误差在点云上表现为物体边缘的"毛刺",处理办法通常是做邻域一致性滤波,把和周围深度差太大的点剔除掉,但这会牺牲边缘的锐度,又是一个取舍。
4.3 误差源与工程对策
把 ToF 的误差源梳理一遍,会发现它跟双目、结构光的重叠很少,这也是为什么 ToF 在某些场景下能补上另外两者的短板。
| 误差源 | 影响表现 | 对策 |
|---|---|---|
| 多径干扰 | 深度整体偏大,凹陷处异常凸起 | 多频解算、空间一致性滤波、场景预处理 |
| 飞点(混合像素) | 物体边缘出现悬空噪点 | 中值/双边滤波、边缘膨胀后剔除 |
| 环境光饱和 | 强光下幅度下降、深度抖动 | 窄带滤光片、多帧累积、提高调制频率 |
| 温度漂移 | 长时间工作后深度整体偏移 | 内置温度补偿表、定期回零标定 |
| 反射率差异 | iToF 中不同颜色/材质深度不一致 | 幅度归一化、反射率标定 |
| 近距饱和 | 近距离高反光物体深度丢失 | 降低发射功率、多曝光 |
| 多机干扰 | 多台设备同时工作互相串扰 | 错频、编码调制、时分复用 |
窄带滤光片这一项值得单独说。ToF 模组通常会在传感器前加一片中心波长与发射光一致的窄带滤光片(带宽 20 到 50 nm),把环境光滤掉绝大部分。这一片小小的滤光片对性能影响巨大,我做过对比测试,同一颗模组拆掉滤光片之后,在室内日光灯下的深度噪声直接翻倍,在窗边阳光下基本不可用。所以如果你的模组在强光环境下表现异常,先检查滤光片的贴合是否完好、有没有漏光缝隙。
5. 三者怎么选:一张对比表和几条判断
技术细节讲完了,落到选型上,我一般按四步走:先定距离段,再定精度要求,再看材质和环境,最后看成本和算力。这个顺序很重要,因为距离段一旦定了,可选方案基本就锁定一半了。
5.1 关键参数横向对比
| 参数 | 双目 | 结构光 | iToF | dToF |
|---|---|---|---|---|
| 典型精度(近距) | mm 级(0.5 m) | 亚 mm 级(0.3~0.8 m) | mm~cm 级 | cm 级 |
| 精度随距离 | 平方衰减 | 平方衰减 | 近似线性 | 近似线性 |
| 推荐工作距离 | 0.5~5 m | 0.2~1.5 m | 0.3~3 m | 0.5~200 m |
| 输出分辨率 | 高(等于相机分辨率) | 高 | 中低 | 低 |
| 帧率 | 取决于算法,10~30 fps | 低(多帧编码),1~10 fps | 高,30~60 fps | 高 |
| 动态场景适应性 | 好(单帧匹配) | 差(需多帧) | 好 | 好 |
| 无纹理表面 | 差 | 优秀 | 优秀 | 优秀 |
| 高反光/镜面 | 一般 | 差 | 一般 | 一般 |
| 强光环境 | 好(被动) | 差 | 一般 | 好 |
| 多机共存 | 好 | 一般(图案可能互相干扰) | 差(需错频) | 一般 |
| 算法复杂度 | 高 | 中 | 低 | 低 |
| 硬件成本 | 低 | 中 | 中 | 中高 |
| 标定复杂度 | 中 | 高 | 中 | 低 |
表格里最容易被误读的是"精度"这一行。不同厂商、不同模组的实际表现差异极大,同样是 iToF,有的模组在 1 米处能稳定在 5 mm,有的飘到 3 cm。所以表格只能用来做初筛,最终一定要拿样机在自己的真实场景里测。我做选型时有个固定动作:准备一套包含白墙、黑色橡胶、不锈钢件、透明亚克力、逆光窗口的测试场景,每种方案都跑一遍,把失败的样子看清楚,比看任何规格书都有用。
5.2 场景映射与选型判断
静态高精度测量,比如工业件尺寸检测、文物扫描、注塑件缺陷检查,结构光是首选。这类场景物体不动,可以慢慢投十几张图案,精度能压到亚毫米,而且工作距离近,正好在结构光的舒适区。要注意的是必须解决高反光问题,通常用多曝光融合加偏振片,必要时换用蓝光投射——蓝光在金属表面的漫反射比例更高,条纹质量更好,这是很多工业方案里不写在规格书上的小技巧。
中距离动态避障,比如移动机器人、自动导引车,iToF 或 dToF 更合适。这类应用对精度要求不高(厘米级足够),但对实时性和无纹理表面的鲁棒性要求高,ToF 正对口。选 iToF 还是 dToF 主要看距离:室内 3 米以内 iToF 的精度和分辨率更优;需要 5 米以上或者有强光直射,dToF 更稳。
成本敏感的消费类应用,比如体感交互、简单的存在检测,双目加一颗散斑投射器是很划算的组合。硬件便宜、分辨率高、算法可以跑在中低端芯片上,缺点是标定和一致性控制比较费功夫,量产时的良率要靠工艺保证。
远距离、大范围,比如仓储体积测量、料堆盘点,dToF 或者带长焦的双目更合适。这类场景视野大、距离远,精度要求反而是相对的——量一个两米的箱子,误差 2 cm 完全能接受。
还有一个经常被忽略的因素:被测物的运动状态。结构光需要多帧编码,被测物一动,几帧之间的图案对不上,解码就错了,所以它天然不适合测运动物体。双目和 ToF 单帧就能出结果,动态场景更友好。这一点在做产线检测时特别关键,如果工件在传送带上移动,结构光方案必须配合触发和停顿,节拍会被拉长。
6. 常见问题与排查实录
这一节是我这些年攒下来的故障速查表,基本都是"现象—原因—处理"的形式,遇到问题直接对号入座,能省不少时间。
6.1 故障速查表
| 现象 | 可能原因 | 排查与处理 |
|---|---|---|
| 双目点云大片空洞 | 无纹理表面、光照不足 | 加散斑投射器或补光;检查曝光是否过暗 |
| 双目近处完全测不到 | 视差超出搜索范围 | 增大 numDisparities,重算最小可测距离 |
| 双目校正后仍有垂直视差 | 标定覆盖不足、标定板不平 | 重拍覆盖四角的标定图;换硬质标定板 |
| 双目深度整体偏大/偏小 | 基线参数错误或 Q 矩阵不匹配 | 核对 stereoCalibrate 输出的 T 与实际基线 |
| 结构光条纹断裂、解码失败 | 局部过曝或全黑 | 多曝光融合;检查投影功率与环境光 |
| 结构光点云出现虚假凸起 | 互反射、镜面二次反射 | 改变投射角度;多视角融合;喷涂显影剂 |
| 结构光精度随时间变差 | 光源温漂导致图案位移 | 预热后再标定;加温度补偿 |
| ToF 边缘大量飞点 | 混合像素 | 中值或双边滤波;边缘区域降权 |
| ToF 深度整体偏大 | 多径干扰 | 换多频配置;检查场景是否有强反射面 |
| ToF 强光下深度抖动 | 环境光饱和 | 确认窄带滤光片完好;缩短曝光、多帧累积 |
| ToF 多机同时工作互相干扰 | 调制频率相同 | 错频配置;时分复用触发 |
| 三类方案都测不到黑色物体 | 反射率过低 | 提高发射功率;表面处理;改用接触式测量 |
6.2 几条踩过的坑
第一条:标定环境的温度要和实际使用环境接近。我做过一个项目,标定是在空调房里做的,设备装到没有空调的现场后,深度系统性偏了将近 1 厘米。查了很久才发现是支架的热胀冷缩导致基线变化,铝合金支架在 20℃ 温差下长度变化大概在几十微米量级,听起来很小,但配合三角测量的放大效应,反映到深度上就是毫米甚至厘米级。后来换了低膨胀系数的材料,并且把标定流程挪到现场做,问题才彻底解决。所以如果精度要求高,标定的环境温度一定要跟工作环境对齐,别在实验室标完了直接拉到现场用。
第二条:不要相信规格书上的精度数字。规格书上的精度通常是"在特定距离、特定反射率、特定环境光下、中心区域"的测试结果,和你实际场景里的表现可能差一个数量级。我见过标称 1% 精度的模组,在 2 米处对着一块灰板测出来误差 5%,一查是把边缘区域的点也算进去了,而边缘的畸变和飞点根本没被标称值覆盖。正确做法是自己搭一个简易的精度验证台:一块平板装在位移台上,从最近到最远走一遍,每 10 cm 采一组数据,算出每个距离段的误差分布,画成曲线,这才是你这个模组在你这个场景下的真实能力。
第三条:点云的可视化方式会影响你的判断。很多人调深度的时候只看深度图的灰度,觉得挺干净就以为没问题,结果转成点云一看全是褶皱。原因是深度图的灰度范围被自动拉伸了,小误差在视觉上被压掉了,而点云是真实的三维坐标,误差会直接体现为表面的凹凸。我的习惯是每次调参都同时看三样东西:深度伪彩图、点云侧视图、以及一个已知平面的拟合残差。平面拟合残差是最客观的指标,把一个标准平板放进去,用 RANSAC 拟合一个平面,看内点的 RMS 误差,低于 1 mm 才算及格。
第四条:主动光源的人眼安全不要想当然。结构光和 ToF 都要往外打光,功率上去之后就必须考虑安全等级。正规模组都会做相应的等级认证,但如果你自己搭系统、自己换光源,一定要确认峰值功率、脉宽、重复频率都在安全范围内,尤其是需要近距离照射或者可能被人直视的场景。这不是技术问题,是底线问题,别为了性能指标去赌。
第五条:多传感器融合时的外参标定,比单个传感器标定更容易出错。实际系统里深度模组往往还要和 RGB 相机、IMU 一起用,它们之间的外参一旦标错,融合结果会以一种很难排查的方式出问题——比如点云和图像看着对得上,但一测距离就偏。我用的办法是做一个简单的验证:在场景里放几个特征明显的标定球,分别用两个传感器测它们的球心,比较球心距离,如果对不上,就说明外参有问题。这个验证比看重叠效果可靠得多,因为人眼对小幅错位很不敏感。
最后说一个我个人的判断:这三种技术不会互相取代,长期看是各自守住自己的距离段和场景。双目守住低成本高分辨率的盘子,结构光守住近距离高精度,ToF 守住实时性和中远距离。真正做项目的时候,与其纠结"哪个更先进",不如把场景里的约束条件列清楚——距离、精度、材质、动态、成本、算力、体积——然后看哪个方案的失效模式你能接受、能兜住。我在实际项目里最常见的情况是混合使用:近距离用结构光保证精度,中远距离用 ToF 做避障和定位,两套点云在同一个坐标系里融合,各自发挥长处。这种组合往往比死磕单一方案的效果好得多,代价只是多一套标定和同步的工作量,而这部分工作量,远比把某一套方案调到极限要划算。