dToF 到深度图:分清 range、Z、FOV 与裁剪后的内参

“dToF 转深度图”听起来像一个格式转换,实际上至少有三个问题:传感器给的是什么距离?每个测量对应哪条光线?最终要生成哪个相机坐标系下的图?

本文接着 小孔成像与相机内参 往下讲。没有绑定某一款硬件;具体设备的单位、有效状态、光线方向和 SDK 输出定义,必须以该设备文档为准,不能根据 depthdistance_mm 这样的变量名猜。

1. dToF 测的是飞行时间,但 SDK 不一定输出原始斜距

dToF 是 direct Time of Flight,即直接飞行时间测距。在发射与接收中心近似重合、往返路径相同,并已扣除电子延迟等偏置的简化模型下:

rcΔt2r\approx\frac{c\,\Delta t}{2}

其中 cc 为光速,Δt\Delta t 为往返传播时间,rr 为到反射点的距离。例如往返约 10 ns 对应约 1.5 m。发射与接收中心明显分离时,传播时间对应两段路径之和,应依赖设备的标定模型,而不是无条件把一半路径当作接收中心到点的距离。ST 官方 dToF 介绍第 11 页 给出了基本往返时间关系。

具体实现可能对光子到达时间建立直方图、估计峰值并输出置信度。测量还受到反射率、环境光、多径、串扰与混合目标等影响,并不是每个输出数值都天然可靠。多区域 dToF 的一个实例是 ams OSRAM TMF8821

这里要把三个维度分开:

维度 可能的情况 为什么重要
测量原理 dToF、间接 ToF、双目视差 决定原始观测与误差来源
输出布局 多区域阵列、扫描点、二维图像、XYZ 点云 决定如何找到每个测量的方向
数值语义 径向 range、光轴 Z、已标定 XYZ 决定能否直接反投影

ToF 是原理,LiDAR 常指主动光学探测系统,不能简单把两者当成互斥类别。反过来,名字叫“深度相机”也不代表它是 ToF;例如 RealSense D435 的官方技术类型为立体视觉,本站 D435i 笔记中的双目原理不能直接搬来解释 dToF。

一个真实反例:distance_mm 已经可能是 Z

ST 员工对 VL53L5CX 的说明指出,该器件内部已经做了 radial-to-perpendicular 转换:正对一个垂直于光轴的平墙时,各区域输出应近似对应相同的垂直距离,而非边缘更长的原始斜距。见 ST 关于 VL53L5CX 输出含义的说明

因此,对这类已转换输出再乘一次 cosθ\cos\theta,反而会把正确平墙“压弯”。这是具体型号的反例,不是所有 dToF 都输出 Z 的保证。

2. range 与 Z:同一个点的两种距离

仍使用相机 optical 坐标:XX 右、YY 下、ZZ 前。空间点为 P=(X,Y,Z)P=(X,Y,Z),则:

r=X2+Y2+Z2,Z=rcosθr=\sqrt{X^2+Y^2+Z^2},\qquad Z=r\cos\theta

θ\theta 是光线与正光轴的夹角。光轴上二者相同,边缘则一般有 r>Zr>Z

考虑一面位于 Z=2Z=2 m 的正对平墙:

  • 光轴上的点:r=2r=2 m,Z=2Z=2 m。
  • 水平离轴 45° 的点:X=2,Y=0X=2,Y=0r=222.828r=2\sqrt{2}\approx2.828 m,但 ZZ 仍是 2 m。

如果把第二个点的 r=2.828r=2.828 当成 Z 反投影,会得到 (2.828,0,2.828)(2.828,0,2.828),而不是 (2,0,2)(2,0,2)。越偏离光轴,几何越被拉长。

反过来,若 SDK 已经输出 Z=2 m,却误当成 range 再乘余弦,就得到 Z≈1.414 m。没有转换、重复转换,都会错。

正对平墙是一个很有用的排错实验,但要避开墙边、无效状态、多径和低信噪比区域;它是验证文档理解的辅助证据,不是替代型号文档的万能判据。

3. 先选输入分支,再恢复三维

已确认的径向 range 转同相机 Z,与 XYZ 经外参投到目标相机的两条流程
A 仅适用于已确认的径向 r;SDK 已输出 Z 时不要重复转换。跨相机还需 B 的外参投影。手机端可横向滑动查看。

3.1 已知单位光线方向时,公式最通用

设厂家标定得到某个区域或测量点的单位方向:

d^=(dx,dy,dz)T,d^=1\hat d=(d_x,d_y,d_z)^T,\qquad\|\hat d\|=1

则输入不同,公式也不同:

已确认的输入 恢复的源坐标系三维点
径向距离 rr PS=rd^P_S=r\hat d
沿源坐标系正 Z 轴的深度 ZSZ_S PS=(ZS/dz)d^P_S=(Z_S/d_z)\hat d,要求 dz>0d_z>0
已标定 XYZ 直接使用,不再做一次深度反投影

注意这里 dzd_zZSZ_S 必须针对同一条轴、同一个坐标系定义。如果设备坐标不是本文 optical 约定,要先明确它的坐标映射。

3.2 无畸变针孔图像是一个特殊情况

若输出确实对应已知 KK 的无畸变针孔像素网格:

q=[xnyn1]=[(ucx)/fx(vcy)/fy1],d^=q1+xn2+yn2q=\begin{bmatrix}x_n\\y_n\\1\end{bmatrix} =\begin{bmatrix}(u-c_x)/f_x\\(v-c_y)/f_y\\1\end{bmatrix},\qquad \hat d=\frac{q}{\sqrt{1+x_n^2+y_n^2}}

于是:

P=Zq,P=rqq,Z=r1+xn2+yn2\boxed{P=Zq},\qquad \boxed{P=r\frac{q}{\|q\|}},\qquad \boxed{Z=\frac{r}{\sqrt{1+x_n^2+y_n^2}}}

千万不要把 qq 当单位向量直接写成 P=rqP=rq。此外,原始畸变像素必须先经过适合该设备的去畸变/反投影模型。

3.3 8×8 阵列不等于一张普通相机的小图

多区域 dToF 的一个格子通常覆盖一束有面积的角度范围,而不是无限细的一根理想光线。区域内存在前后两个物体时,输出还受峰值选择、回波和固件处理影响;把数值放在区域中心射线上,只是一种代表点近似。

不要仅凭“8×8、总 FOV 多少度”就断言每格等角度,或反推出一套准确的针孔 K。优先使用厂家的每区方向表、畸变模型或 SDK 反投影。

同理,按方位角/仰角排列的球面 range image 与按 X/Z,Y/ZX/Z,Y/Z 排列的透视图不同。若某系统将方位角 α\alpha 从正 Z 轴向右计,仰角 β\beta 向上计,那么在本文右、下、前坐标中,一种明确约定的单位射线为:

d^=[cosβsinαsinβcosβcosα]\hat d=\begin{bmatrix}\cos\beta\sin\alpha\\-\sin\beta\\\cos\beta\cos\alpha\end{bmatrix}

实际设备的零角、正方向和通道角度可能不同,不能直接套用。更不能把球面图 resize 一下就称作透视深度图;应恢复三维后重新投影。PCL 的 RangeImagePlanar 文档 也区分了平面投影与球面投影。

4. XYZ 投到目标深度图:不只是改一个矩阵尺寸

假设已有源坐标系点 PSP_S,目标是某个相机 C 的深度图。正确顺序为:

  1. 根据设备的状态、置信度、量程和单位筛掉无效观测。
  2. 通过外参变换到目标相机:PC=RCSPS+tCSP_C=R_{CS}P_S+t_{CS}
  3. 丢弃非有限值及 ZCzminZ_C\le z_{\min} 的点,避免投影到身后或除以接近零的数。
  4. 使用与目标图像模式匹配的 K 和畸变模型,得到像素 (u,v)(u,v)
  5. 做目标图像边界判断,再离散化成像素索引。
  6. 同一像素有多个候选点时,保留最近的正 ZCZ_C,同时保留有效性掩码。

这里输出的是目标相机的 Z,不一定等于源设备的 range,也不一定等于源坐标系的 Z。ROS 标准浮点深度采用沿相机 Z 轴的米制深度,参见 REP 118。具体 SDK 的其他编码仍要单独确认。

4.1 带坐标轴变换的完整例题

源坐标为“前、左、上”,目标相机坐标为“右、下、前”,暂设两者原点重合、安装朝向没有额外旋转:

RCS=[010001100],tCS=0R_{CS}=\begin{bmatrix}0&-1&0\\0&0&-1\\1&0&0\end{bmatrix}, \qquad t_{CS}=0

这个 R 只是例题的轴约定变换,不是可用于真实硬件的通用外参

对源点 PS=(5,1,0.5)P_S=(5,-1,0.5) m:

PC=(1,0.5,5)mP_C=(1,-0.5,5)\,\mathrm{m}

使用 fx=fy=600,cx=320,cy=240f_x=f_y=600,c_x=320,c_y=240,得到:

u=60015+320=440,v=6000.55+240=180u=600\frac{1}{5}+320=440,\qquad v=600\frac{-0.5}{5}+240=180

该像素应写入 Z=5 m,而非 r=26.255.12348r=\sqrt{26.25}\approx5.12348 m。

若另一点为 (2,1,10)(2,-1,10)(目标相机系),它也投到 (440,180)(440,180)。两者发生碰撞时留下 Z=5,而不是平均成 7.5。较小深度更新也是 Open3D 点云投影内核 中采用的基本处理方式。

4.2 像素离散化与无效值必须有明确约定

本文规定像素中心为整数,采用最近像素投影:

0.5u<W0.5,0.5v<H0.5-0.5\le u<W-0.5,\quad -0.5\le v<H-0.5

i=u+0.5,j=v+0.5i=\lfloor u+0.5\rfloor,\qquad j=\lfloor v+0.5\rfloor

这样先检查边界,再取整,输出索引一定合法。右、下边界不包含在内。若某库使用 floor(u)、不同的采样中心或边界规则,应整体匹配该库,而不是混用本文的边界与另一种取整方式。

没有点落入的像素用 NaN 或显式 invalid mask 表示。不能把“无观测”当成“距离 0 米”;展示时可以着色为黑色,但数值语义仍要区分。浮点 NaN 与某些 16 位格式中的 0 无效值,转换时也要显式处理。

4.3 Z-buffer 解决不了所有遮挡

取最小 Z 只解决已采到的点之间的投影碰撞。如果前景没有采到点,背景仍可能穿过空隙;不同传感器存在基线时,还有源视角看得到、目标相机却被遮挡的区域。

因此“已投影”“真实观测”“插值补洞”“遮挡推断”应分开记录。把前景 1 m 与背景 5 m 双线性平均成 3 m,会制造一个本不存在的表面。最近邻不会混合数值,但也可能复制错误表面;没有任何简单缩放方法能自动解决深度边界问题。

5. FOV 到底由什么决定

FOV 是视场角,不是分辨率。对于一个已经明确的无畸变针孔图像,它由像素范围和 K 一起决定

在本文像素中心约定下,横向外边缘对应的方向角为:

θL=arctan0.5cxfx,θR=arctanW0.5cxfx\theta_L=\arctan\frac{-0.5-c_x}{f_x},\qquad \theta_R=\arctan\frac{W-0.5-c_x}{f_x}

HFOV=θRθL\boxed{\mathrm{HFOV}=\theta_R-\theta_L}

同理,将 W,cx,fxW,c_x,f_x 换成 H,cy,fyH,c_y,f_y 得到垂直 FOV。这里的角度来自像平面横/纵截面;目标点的矩形透视范围由 X/ZX/ZY/ZY/Z 判断,不能用球面仰角 atan2(Y,X2+Z2)\operatorname{atan2}(Y,\sqrt{X^2+Z^2}) 的固定上下界直接替代。

若主点严格位于几何中心 cx=(W1)/2c_x=(W-1)/2,左右对称,才简化为:

HFOV=2arctanW2fx,fx=W2tan(HFOV/2)\mathrm{HFOV}=2\arctan\frac{W}{2f_x},\qquad f_x=\frac{W}{2\tan(\mathrm{HFOV}/2)}

FOV 输入三角函数前要从度转成弧度。仅有一个对角 FOV,通常不足以确定 fx,fy,cx,cyf_x,f_y,c_x,c_y;还需要像素尺度、主点、投影模型等假设。真实畸变镜头、多区域 dToF 或扫描传感器的覆盖范围,也不能只靠这组针孔公式描述。

同一视场将 640 列重采样成 320 列,K 也相应更新,视场可以不变;从 640 列只保留中间 320 列,视场则缩小。两者虽然输出尺寸相同,几何含义完全不同。

6. ROI 裁剪:主点平移,焦距不变

从原图取整数 ROI:横向索引 [x0,x0+w)[x_0,x_0+w),纵向索引 [y0,y0+h)[y_0,y_0+h)。例如 NumPy 中:

cropped = image[y0:y0 + h, x0:x0 + w]

原像素中心在新图的坐标为 u=ux0,v=vy0u'=u-x_0,v'=v-y_0。代入投影式可得:

fx=fx,fy=fy,cx=cxx0,cy=cyy0f'_x=f_x,\quad f'_y=f_y,\quad c'_x=c_x-x_0,\quad c'_y=c_y-y_0

裁剪没有改镜头,只是换了像素原点与保留范围。主点可能偏到一侧,甚至落在裁剪图外;这本身并不意味着内参错误。

完整图、近中心 ROI、偏右 ROI 的主点与 FOV 对比,裁剪不改变焦距
橙色为 ROI 中心方向,白色为光轴方向;两者不一定重合。手机端可横向滑动查看。

对 640×480 图像,取 fx=600,cx=320f_x=600,c_x=320,只裁左右、保持高度:

图像 x0 宽度 新 cx 左边缘角 右边缘角 水平 FOV
完整图 0 640 320 −28.11° 28.04° 56.14°
近中心 ROI 160 320 160 −14.98° 14.89° 29.86°
偏右 ROI 240 320 80 −7.64° 21.76° 29.40°

微小不对称来自例题主点 320 与图像几何中心 319.5 的差别。这里不是用视场的像素比例硬乘角度,而是对裁剪后的左右边缘分别取反正切。

为什么“裁一半宽度”不是“FOV 减半”?对严格居中模型,宽度保留比例为 α\alpha 时:

HFOV=2arctan(αtanHFOV2)\mathrm{HFOV}'=2\arctan\left(\alpha\tan\frac{\mathrm{HFOV}}{2}\right)

它是正切与反正切关系,而非角度线性关系。越广角,线性近似越容易产生明显偏差。

还有两种名字相近但含义不同的裁剪:

  • 图像 ROI:从已有图像里去掉部分像素,按上式更新主点。
  • 三维点云按目标相机视场筛选:先将点变到目标相机系,再投影并判断边界。若 dToF 与目标相机有平移,不能只裁源传感器的角度来代替,近距离视差会使结果不同。

7. Resize 与 letterbox:除了比例,还有半像素约定

7.1 先写坐标映射,再更新 K

设先裁剪,再按比例 sx,sys_x,s_y 缩放,最后左右/上下补边,其中左侧 padding 为 pxp_x、顶部为 pyp_y。如果使用简单坐标映射:

u=sx(ux0)+px,v=sy(vy0)+pyu'=s_x(u-x_0)+p_x,\qquad v'=s_y(v-y_0)+p_y

则:

fx=sxfx,fy=syfy,cx=sx(cxx0)+px,cy=sy(cyy0)+pyf'_x=s_xf_x,\quad f'_y=s_yf_y, \quad c'_x=s_x(c_x-x_0)+p_x, \quad c'_y=s_y(c_y-y_0)+p_y

其本质是 K=AKK'=AK,其中 A 是同一个像素坐标仿射变换。不能用完整 padded 宽度除原宽度来误算缩放倍率;缩放比例与补边是两步。

7.2 常见的中心对齐重采样需要额外 0.5

很多缩放实现以像素中心对齐采样。例如 OpenCV resize 实现 中常见的采样位置关系,对应如下前向坐标变换:

u=sx(ux0+0.5)0.5+pxu'=s_x(u-x_0+0.5)-0.5+p_x

因此应使用:

cx=sx(cxx0+0.5)0.5+px\boxed{c'_x=s_x(c_x-x_0+0.5)-0.5+p_x}

纵向同理,焦距仍为 sxfx,syfys_xf_x,s_yf_y。若使用 align_corners、不同采样模式或厂家图像管线,映射可能不同;这不是必须一律手工加减 0.5 的命令,而是要匹配真实的像素映射。

这里改用相机基础篇的点 PC=(0.2,0.1,2)P_C=(0.2,0.1,2) m,其原图投影为 (380,270)(380,270),不是本篇第 4 节的点。原图先裁 (160,120,320,240)(160,120,320,240),再缩小到 160×120,不补边:

阶段 fx, fy cx, cy 该点的投影
原图 600, 600 320, 240 380, 270
裁剪后 600, 600 160, 120 220, 150
缩放:简单坐标比例 300, 300 80, 60 110, 75
缩放:中心对齐 300, 300 79.75, 59.75 109.75, 74.75

两种结果的差别来自明确的坐标约定,不是空间点变了。对中心对齐缩放,用本文像素外边缘计算的 FOV 保持不变。

再举 letterbox:640×480 等比例缩到 320×240,放入 320×320 画布,上下各补 40 像素。中心对齐时 fx=fy=300,cx=159.75,cy=159.75f'_x=f'_y=300,c'_x=159.75,c'_y=159.75。上下 padding 没有真实测量,必须保留 invalid mask,不能因为 K 能描述那些方向就认为传感器看到了更多区域。

7.3 图像尺寸缩放,不等于深度数值缩放

像素从 640×480 变成 320×240,某个点的真实深度 2 m 仍然是 2 m。要变化的是该点的位置和内参,不是米制深度乘 0.5。

还需注意:

  • 对深度图做普通双线性插值可能混合前后景;应根据任务选择带有效性判断、边界意识的处理,并标记估计值。
  • 从低分辨率 dToF 放大到高分辨率不会创造真实测量或提升原始角分辨率。
  • undistort、rectify、align 到 RGB 可能涉及非线性重映射或不同相机视点,不能只用一个 crop/resize 仿射 A 代替。
  • 在 ROS 中,原图 K、矫正图 P、ROI、binning 的约定应与驱动和 image_geometry 的处理一致,避免重复扣 ROI 或重复缩放。参见 CameraInfo 定义

8. 一份不用硬件也能运行的参考实现

下载 camera_geometry_demo.py。它只依赖 Python 3.9+ 标准库,包含 range/Z 反投影、FOV、crop/resize 内参更新、坐标变换和最近 Z 投影。

python3 camera_geometry_demo.py --self-test

文件中的投影核心是:

# 已经先检查设备状态、置信度和单位;目标是无畸变针孔图。
point_c = transform_point(point_s, rotation, translation)
# 丢弃非有限值和 Z <= z_min 后:
u, v = camera.project(point_c)
if -0.5 <= u < camera.width - 0.5 and -0.5 <= v < camera.height - 0.5:
# 先通过连续坐标边界检查,再防范浮点加法将末端合法值舍入到 W/H。
col = min(camera.width - 1, floor(u + 0.5))
row = min(camera.height - 1, floor(v + 0.5))
z = point_c[2]
if isnan(depth[row][col]) or z < depth[row][col]:
depth[row][col] = z

以下例子可以在下载文件所在目录运行:

from camera_geometry_demo import CameraModel, rasterize_z

camera = CameraModel(640, 480, 600, 600, 320, 240)
rotation = ((0, -1, 0), (0, 0, -1), (1, 0, 0))
points_source = [(5, -1, 0.5), (10, -2, 1)]
depth = rasterize_z(points_source, camera, rotation)
print(depth[180][440]) # 5.0,两个点落到同一像素,保留更近的 Z

cropped = camera.crop(160, 120, 320, 240)
resized = cropped.resize(160, 120, center_aligned=True)
print(resized.fx, resized.cx) # 300.0 79.75
print(cropped.fov_deg())

这是教学实现,不是某款 dToF 驱动:没有替你估计外参、读取置信度、处理镜头畸变或做运动补偿,也没有进行深度补洞。输入点必须先完成相应设备的数据校验。大规模实时点云可换成向量化或并行实现,但应保留这里可验证的坐标、单位和边界约定。

9. 真正接硬件时的排错顺序

现象 优先检查
正对平墙却向边缘鼓起或凹下 range/Z 混淆、重复余弦修正、畸变与光线方向表
整体左右镜像、上下颠倒 optical 轴与机体轴、区域索引顺序、图像是否翻转
裁剪后整体偏移 是否更新主点、是否重复扣 ROI
缩放后偏一点,边缘更明显 fx/fy 比例、半像素约定、实际工作分辨率
RGB 与深度在近处错位尤其明显 外参平移、目标相机 K、跨视角遮挡
静止正常,运动时错位 采集时间戳、设备时钟偏差、扫描点时间与运动补偿
远近差三个数量级 毫米/米、raw depth scale、整数编码解释
物体边缘出现不存在的中间深度 插值跨前后景、区域混合回波、无效值参与平均

时间同步值得单独强调:匹配相近时间戳不等于修正了设备时钟偏差,也不等于消除了曝光或扫描期间的运动。若设备是扫描式输出,需要确认点级采集时刻;非扫描多区域器件则按其实际采集方式处理,不应无条件套上扫描雷达的 deskew 流程。

接入前建议把以下信息写进配置或数据说明:型号与 SDK 版本、输出是 range 还是 Z、单位与无效状态、区域/像素到射线的标定、坐标轴、外参方向、目标图像模式、时间戳来源、crop/resize/padding 顺序。这样后续换设备、改分辨率时,不用重新猜一遍。

10. 用四个问题检查是否真的理解

  1. 某 dToF SDK 已输出垂直深度,边缘像素还要乘 cosθ\cos\theta 吗?
  2. 将宽度从 640 缩到 320,深度 5 m 应变成 2.5 m 吗?
  3. 偏心 ROI 宽度相同,FOV 一定与中心 ROI 相同吗?
  4. 一张深度图的黑色区域能直接认定为近距离障碍物吗?

答案依次是:不应重复转换;米制深度不跟图像尺寸缩放;不一定,应该从左右边缘方向重新算;不能,先区分无效值、无观测和显示配色。

把这篇压缩成一条实践主线就是:确认距离语义 → 用正确光线恢复三维 → 变换到目标坐标系 → 投影与有效性处理 → 图像预处理时同步更新内参。

回到 计算机视觉基础分类,或阅读 相机内参与小孔成像RealSense D435i 工程入门,对照不同测量原理下相同的投影几何。

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention axis-angle bang-bang belief encoder blender bode c++ cadquery calibration camera calibration camera-intrinsics chrome cmake cmakelists cnn colcon computer-vision conan control controller_manager cpp cpu d435i dagger data_struct db depth camera depth-camera design-pattern direct collocation dots dtof economics eigen elevation map executor factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb geometry git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel mac math matlab matrix memory mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pinhole-camera pinocchio pixhawk pixhawk 6c point-cloud policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robot robot parkour robotics ros ros2 ros2_control rsl_rl rtb security sensor-fusion shell signal-processing sim-to-real simulation socket soft dynamics constraints spot stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution terrain reconstruction thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 动力学 四旋翼 四足机器人 实验诊断 强化学习 接触动力学 数值计算 机器人 机器人控制 机器人视觉 构建系统 浮动基 深度学习 深度相机 点云 版本控制
知识共享许可协议