单目深度查表的原理和计算过程梳理:从图像像素到地面物理坐标

在割草机器人、扫地机器人等项目中,如果视觉采用的是单目,经常会遇到这样一个需求:

已知相机图像中的一个像素,如何快速得到它在机器人周围地面上的实际物理位置?

一种常见实现方式,是提前生成一张"像素坐标到地面物理坐标"的查找表。运行时无需重复执行矩阵运算,只需用像素坐标查表,便能得到对应的前向距离和横向距离。(但是这样的使用前提是默认设备是运行在平坦的地面上的,如果遇到陡坡等不平的路况,识别出来的物体的实际物理位置就很不准)

本文以一个实际的 640×640 查找表 camera.bin 为例,梳理这种方法的原理、计算过程、坐标系定义、BEV 倒梯形的来源,以及它与深度图反投影之间的区别。

需要先强调:本文讨论的"单目深度查表"并不是利用神经网络预测任意物体的单目深度,而是利用相机标定参数和"目标点位于地面"的假设,求像素射线与地面的交点。


目录

1.camera.bin 是什么?

camera.bin 是一张预先计算好的像素坐标到地面物理坐标的查找表(Lookup Table,LUT)。

经过实际解析,该文件的结构为:

text 复制代码
图像宽度:640
图像高度:640
每个像素:2 个 little-endian float32
文件大小:640 × 640 × 2 × 4 = 3,276,800 字节

读取后可组织成以下数组:

python 复制代码
table.shape == (640, 640, 2)

每个像素存储一对物理坐标:

python 复制代码
physical_x, physical_y = table[pixel_y, pixel_x]

这里存在三套容易混淆的坐标概念:

概念 坐标 含义
图像像素坐标 (pixel_x, pixel_y) 或 (u,v) 图像中的列和行
数组索引 [pixel_y, pixel_x] 数组按 [行, 列] 访问
机器人物理坐标 (physical_x, physical_y) 前后距离和左右距离

因此:

python 复制代码
# 图像中的像素坐标是 (u, v)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5387f44ef17246e5a1cc2cd04fda79bd.png#pic_center)

u = pixel_x
v = pixel_y

# 数组索引顺序是 [行, 列],即 [v, u]
X_forward, Y_lateral = table[v, u]

table[pixel_y, pixel_x] 并不表示 pixel_y 只负责生成 physical_x,也不表示 pixel_x 只负责生成 physical_y。它只是使用图像的行、列定位查找表中的一个元素,然后一次性取出该像素对应的两个物理坐标分量。

2.一个像素为什么不能直接确定三维点?

相机把三维世界投影成二维图像。在这个过程中,距离信息被压缩掉了。

反过来,仅凭图像中的一个像素,我们只能知道"从相机往哪个方向看",却不知道物体到底位于这条视线上的哪个位置:

text 复制代码
相机 ●────────P1────────P2────────P3
              同一条成像射线

P1、P2、P3 处于同一条射线上,它们可以投影到同一个像素。因此,一个普通单目像素对应的是一条空间射线,而不是唯一的三维点。

要从这条射线上确定一个点,还需要增加一个条件。常见条件有两种:

  1. 深度相机或深度模型直接提供该像素的深度;
  2. 假设该像素对应的点位于一个已知平面,例如地面 Z=0。

camera.bin 所体现的是第二种思路。

3.从像素得到相机射线

设图像像素为:

text 复制代码
(u,v) = (pixel_x,pixel_y)

相机内参通常写为:

K = f x 0 c x 0 f y c y 0 0 1 K= \begin{bmatrix} f_x&0&c_x\\ 0&f_y&c_y\\ 0&0&1 \end{bmatrix} K= fx000fy0cxcy1

其中:

  • f x , f y f_x,f_y fx,fy:以像素为单位的焦距;
  • c x , c y c_x,c_y cx,cy:相机主点,通常接近图像中心。

将像素坐标归一化:

x n = u − c x f x x_n=\frac{u-c_x}{f_x} xn=fxu−cx

y n = v − c y f y y_n=\frac{v-c_y}{f_y} yn=fyv−cy

便可得到相机坐标系中的射线方向:

d c = x n y n 1 \mathbf d_c= \begin{bmatrix} x_n\\ y_n\\ 1 \end{bmatrix} dc= xnyn1

矩阵形式为:

d c = K − 1 u v 1 \mathbf d_c=K^{-1} \begin{bmatrix} u\\v\\1 \end{bmatrix} dc=K−1 uv1

这个矩阵表达式本质上只是前面两个减法和除法的紧凑写法。

例如,假设:

text 复制代码
fx = 500, fy = 500
cx = 320, cy = 320
u  = 500, v  = 450

则:

text 复制代码
xn = (500 - 320) / 500 = 0.36
yn = (450 - 320) / 500 = 0.26

射线方向可写成:

text 复制代码
d_camera = (0.36, 0.26, 1)

它表达的是一个方向比例:沿相机正前方走 1 个单位时,同时向相机右侧偏移 0.36 个单位、向相机下方偏移 0.26 个单位。

如果相机存在明显的径向或切向畸变,应当先对 (u,v) 去畸变,再执行上述计算。

4.相机外参起什么作用?

通过相机内参得到的射线仍然位于相机坐标系中,但机器人通常需要机器人本体坐标系中的位置。

假设机器人坐标系定义为:

text 复制代码
X:机器人前进方向,forward
Y:机器人横向方向,lateral
Z:机器人上方

相机可能安装在机器人中心的上方、前方或侧面,并且通常会向地面俯视。相机相对于机器人的位置和姿态就是相机外参。

若外参采用"相机坐标转换到机器人坐标"的形式:

P r = R r c P c + t r c \mathbf P_r=R_{rc}\mathbf P_c+\mathbf t_{rc} Pr=RrcPc+trc

则:

  • R r c R_{rc} Rrc 表示相机坐标系到机器人坐标系的旋转;
  • t r c \mathbf t_{rc} trc 表示相机原点在机器人坐标系中的位置;
  • 相机在机器人坐标系中的位置为 C r = t r c \mathbf C_r=\mathbf t_{rc} Cr=trc;
  • 射线在机器人坐标系中的方向为:

d r = R r c d c \mathbf d_r=R_{rc}\mathbf d_c dr=Rrcdc

于是,这条射线在机器人坐标系中的任意一点都可以写成:

P r ( λ ) = C r + λ d r \mathbf P_r(\lambda)=\mathbf C_r+\lambda\mathbf d_r Pr(λ)=Cr+λdr

这里的 λ \lambda λ 表示从相机出发,沿射线方向走了多少倍。

展开后:

X = C x + λ d x X=C_x+\lambda d_x X=Cx+λdx

Y = C y + λ d y Y=C_y+\lambda d_y Y=Cy+λdy

Z = C z + λ d z Z=C_z+\lambda d_z Z=Cz+λdz

需要特别注意,工程中也经常使用方向相反的外参,例如"机器人坐标转换到相机坐标"。两种定义不能直接混用。生成查找表前,必须先确认 R R R 和 t t t 的具体方向,必要时对外参求逆。

5.利用地面 Z=0 确定射线交点

如果机器人坐标系把地面定义为:

Z = 0 Z=0 Z=0

那么射线与地面相交时,交点的 Z 坐标必须满足:

0 = C z + λ d z 0=C_z+\lambda d_z 0=Cz+λdz

因此:

λ = − C z d z \lambda=-\frac{C_z}{d_z} λ=−dzCz

得到 λ \lambda λ 后,代回射线方程:

X = C x + λ d x X=C_x+\lambda d_x X=Cx+λdx

Y = C y + λ d y Y=C_y+\lambda d_y Y=Cy+λdy

最终得到该像素对应的地面物理点:

P r = ( X , Y , 0 ) \mathbf P_r=(X,Y,0) Pr=(X,Y,0)

因为所有交点都被假设在地面上,Z 恒为 0,所以查找表只需保存 X 和 Y。

5.1 数值示例

假设相机在机器人坐标系中的位置为:

text 复制代码
C = (0.20, 0.00, 0.50) m

经过内参反投影和外参旋转后,某个像素的射线方向为:

text 复制代码
d = (0.80, 0.20, -0.40)

这表示射线沿机器人前方移动的同时,也向侧面和下方移动。由于机器人坐标系的 Z 轴向上,向下对应负数。

射线高度为:

text 复制代码
Z = 0.50 + λ × (-0.40)

令 Z=0:

text 复制代码
λ = 1.25

于是:

text 复制代码
X = 0.20 + 1.25 × 0.80 = 1.20 m
Y = 0.00 + 1.25 × 0.20 = 0.25 m
Z = 0

该像素最终对应机器人前方 1.20 m、横向 0.25 m 的地面位置。

5.2 地面不一定必须写成 Z=0

Z=0 只是坐标系定义后的便利表达。如果机器人坐标系原点位于车体内部,而不是地面,那么地面可能是:

text 复制代码
Z = Z_ground

此时:

λ = Z g r o u n d − C z d z \lambda=\frac{Z_{ground}-C_z}{d_z} λ=dzZground−Cz

更一般的地面平面可以写成 n T P + d = 0 \mathbf n^T\mathbf P+d=0 nTP+d=0,其中 n \mathbf n n 是平面法向量。相应的射线交点参数为:

λ = − n T C + d n T d \lambda=-\frac{\mathbf n^T\mathbf C+d} {\mathbf n^T\mathbf d} λ=−nTdnTC+d

所以,算法真正依赖的是"机器人坐标系中的地面平面方程",而不是固定依赖字面意义上的 Z=0。

6.完整的查找表生成过程

整个计算链路可以概括为:

text 复制代码
图像像素 (u,v)
      │
      │ 去畸变、使用相机内参 K⁻¹
      ▼
相机坐标系射线 d_camera
      │
      │ 使用相机外参 R、t
      ▼
机器人坐标系中的相机位置 C 和射线 d_robot
      │
      │ 与地面平面 Z=0 求交
      ▼
地面物理坐标 (X forward, Y lateral)
      │
      ▼
table[pixel_y, pixel_x] = (X,Y)

对应的简化伪代码如下:

python 复制代码
import numpy as np

table = np.empty((image_height, image_width, 2), dtype=np.float32)

for pixel_y in range(image_height):
    for pixel_x in range(image_width):
        # 实际工程中应先做镜头去畸变
        x_normalized = (pixel_x - cx) / fx
        y_normalized = (pixel_y - cy) / fy

        ray_camera = np.array([
            x_normalized,
            y_normalized,
            1.0,
        ])

        ray_robot = R_camera_to_robot @ ray_camera

        scale = -camera_position_robot[2] / ray_robot[2]
        point_robot = camera_position_robot + scale * ray_robot

        table[pixel_y, pixel_x, 0] = point_robot[0]  # X forward
        table[pixel_y, pixel_x, 1] = point_robot[1]  # Y lateral

table.astype("<f4").tofile("camera.bin")

实际工程还应增加有效性判断,例如:

python 复制代码
valid = (
    np.isfinite(point_robot).all()
    and scale > 0
    and min_forward <= point_robot[0] <= max_forward
    and min_lateral <= point_robot[1] <= max_lateral
)

scale > 0 表示交点位于相机射线的正方向,而不是相机后方。

7.为什么 BEV 图会呈现倒梯形?

在实际输出图中,左侧为 640×640 的相机图像和 YOLO 实例分割结果,右侧则是将分割像素转换到地面物理坐标后绘制的 BEV 图。

右图不是直接显示 camera.bin 中哪些位置"有数值",而是经过了以下过程:

text 复制代码
YOLO 实例分割 mask
          ↓
选择需要转换的 mask 像素
          ↓
逐像素查询 camera.bin
          ↓
获得 (X forward, Y lateral)
          ↓
在物理坐标系中重新绘制

概念代码为:

python 复制代码
pixel_ys, pixel_xs = np.nonzero(mask)

physical_xy = table[pixel_ys, pixel_xs]
physical_x = physical_xy[:, 0]  # forward
physical_y = physical_xy[:, 1]  # lateral

plt.scatter(physical_y, physical_x)

倒梯形来自相机透视关系:

  • 图像底部对应机器人附近的地面;
  • 图像靠上的地面区域通常对应更远的位置;
  • 同样的图像宽度,在近处覆盖的实际横向范围较窄;
  • 同样的图像宽度,在远处覆盖的实际横向范围较宽。

从 camera.bin 中可以得到以下实际数据:

像素行 pixel_y 前向距离 X 整行覆盖的横向 Y
639 0.574 m -0.260~+0.261 m
600 0.600 m -0.305~+0.305 m
495 0.749 m -0.564~+0.564 m
441 0.999 m -1.000~+1.001 m
409 1.487 m -1.847~+1.849 m
397 1.981 m -2.708~+2.711 m

转换到物理坐标后,外轮廓自然表现为:

text 复制代码
远处,X 较大       ─────────────────  横向范围宽
                      \           /
                       \         /
                        \       /
近处,X 较小              ───────     横向范围窄

因此,倒梯形表达的是相机视场在地面平面上的覆盖范围。它并不表示 .bin 文件中只有倒梯形区域才有数据。

右图中一条条水平线也很好解释:同一像素行对应几乎相同的前向距离 X,而同一行的不同像素列对应不同的横向距离 Y。因此,一行 mask 像素投影到 BEV 后会形成一条水平线,多行叠加后便形成倒梯形区域。

8.为什么 BEV 横轴是 Y lateral,纵轴是 X forward?

这是机器人坐标系和 BEV 显示方式的约定,不是数学定律。

当前项目采用的物理坐标定义为:

text 复制代码
X = forward,机器人前进方向
Y = lateral,机器人左右方向
Z = up,机器人上方

BEV 通常希望机器人位于图像底部,并朝屏幕上方行驶:

text 复制代码
                    X forward
                        ↑
                        |
Y lateral(左) ← 机器人 → Y lateral(右)

因此屏幕显示坐标为:

python 复制代码
screen_horizontal = physical_y
screen_vertical = physical_x

对应:

python 复制代码
plt.scatter(physical_y, physical_x)

这并不表示 X、Y 被弄反了,而是将物理 X 轴画在屏幕竖直方向,将物理 Y 轴画在屏幕水平方向。

当然,也可以采用另一套定义,例如 X 表示左右、Y 表示前后。但相机外参、查找表、定位、规划和显示程序必须统一使用同一套坐标约定。

9.它与深度图的 2D 转 3D 有什么不同?

深度图反投影通常使用:

Z c = d e p t h ( u , v ) Z_c=depth(u,v) Zc=depth(u,v)

X c = ( u − c x ) Z c f x X_c=\frac{(u-c_x)Z_c}{f_x} Xc=fx(u−cx)Zc

Y c = ( v − c y ) Z c f y Y_c=\frac{(v-c_y)Z_c}{f_y} Yc=fy(v−cy)Zc

这里的 Z c Z_c Zc 表示像素对应物体点在相机坐标系中沿光轴方向的深度。它不是机器人坐标系中的地面高度。

上述公式要求 depth(u,v) 表示沿相机光轴的 Z 深度。如果传感器输出的是从相机光心到目标点的欧氏距离(range),则不能不加转换地直接代入同一公式。

而地面求交使用的:

text 复制代码
Zr = 0

表示机器人坐标系中的地面高度。

为了避免混淆,可以明确写成:

符号 含义
Z c Z_c Zc 相机坐标系中的光轴深度
Z r Z_r Zr 机器人坐标系中的垂直高度

同一个地面点完全可能同时满足:

text 复制代码
相机坐标系:Zc = 1.5 m
机器人坐标系:Zr = 0

因为两者属于不同坐标系,也表示不同方向上的量。

两种方法的对比如下:

方法 如何确定射线上的位置 能恢复什么
深度反投影 由 depth(u,v) 直接提供 Z c Z_c Zc 地面、墙壁、障碍物等任意表面点
地面求交 用 Z r = 0 Z_r=0 Zr=0 的平面约束反求交点 假设位于地面上的点

二者的数学本质都是:

text 复制代码
三维点 = 射线起点 + 缩放量 × 射线方向

区别只是缩放量从哪里获得:

text 复制代码
深度方法:由深度传感器或深度模型直接给出
地面求交:由"交点必须位于地面"反推得到

如果 mask 像素实际属于一个立起来的障碍物,地面查表仍会给出一组 (X,Y),但它表示该像素射线继续延伸后与假想地面的交点,不一定是障碍物表面的真实三维坐标。

10.为什么地平线附近会出现巨大数值?

地面求交使用:

λ = − C z d z \lambda=-\frac{C_z}{d_z} λ=−dzCz

当某条射线几乎平行于地面时:

d z → 0 d_z\rightarrow0 dz→0

此时:

λ → ± ∞ \lambda\rightarrow\pm\infty λ→±∞

直观上,射线需要延伸非常远才能与地面相交;如果完全平行,则永远不会相交。

对 camera.bin 的解析显示,其投影奇异线约位于:

text 复制代码
pixel_y ≈ 371.247

附近的实际数据为:

text 复制代码
pixel_y=370:X≈ -31.716
pixel_y=371:X≈-161.767
pixel_y=372:X≈  53.666
pixel_y=373:X≈  23.292

坐标在相邻两行之间快速增大并发生符号翻转,这是射线跨过地平线时的典型现象,也说明这张表很符合平面投影模型。

地平线以上的射线通常指向天空,不会与相机前方的地面相交。公式仍可能返回负缩放量或相机后方的交点,所以"表中存在有限浮点数"不等于"该点在物理上有效"。

实际应用一般还应结合以下条件过滤:

  • 射线交点位于相机前方,即 scale > 0;
  • 前向距离处于机器人业务范围内;
  • 横向距离处于合理范围内;
  • 像素位于有效 ROI;
  • 像素类别确实允许按地面模型解释。

11.所有像素都有坐标,是否代表都真实有效?

不是。

对 camera.bin 的检查结果为:

text 复制代码
总像素数:409,600
有限坐标对:409,600
NaN/Inf:0
(0,0) 占位:0

这说明生成程序很可能对整张图的每个像素都执行了投影公式,但公式产生数值不代表该像素一定对应实际可观测的地面位置。

应区分:

text 复制代码
文件中有数学计算结果
          ≠
像素对应真实、可用的地面位置

例如:

  • 地平线以上的像素可能对应天空;
  • 障碍物像素并不真的位于地面;
  • 极远位置可能超过机器人感知和规划范围;
  • 射线反方向的交点没有实际意义;
  • 图像边缘的畸变和标定误差可能更大。

因此,一个可靠系统通常应同时维护或动态计算有效 mask,而不能只以 NaN 或 (0,0) 判断坐标是否有效。

12.单应矩阵与逐射线求交

因为所有目标点都被假设位于同一个平面上,像素到地面坐标的映射也可以合并成一个单应矩阵:

s X = h 11 u + h 12 v + h 13 sX=h_{11}u+h_{12}v+h_{13} sX=h11u+h12v+h13

s Y = h 21 u + h 22 v + h 23 sY=h_{21}u+h_{22}v+h_{23} sY=h21u+h22v+h23

s = h 31 u + h 32 v + h 33 s=h_{31}u+h_{32}v+h_{33} s=h31u+h32v+h33

其中, H H H 是单应矩阵, s s s 是齐次坐标的缩放因子。用前两个方程分别除以第三个方程,完成归一化后可得:

X = h 11 u + h 12 v + h 13 h 31 u + h 32 v + h 33 X=\frac{h_{11}u+h_{12}v+h_{13}}{h_{31}u+h_{32}v+h_{33}} X=h31u+h32v+h33h11u+h12v+h13

Y = h 21 u + h 22 v + h 23 h 31 u + h 32 v + h 33 Y=\frac{h_{21}u+h_{22}v+h_{23}}{h_{31}u+h_{32}v+h_{33}} Y=h31u+h32v+h33h21u+h22v+h23

单应矩阵可以通过两种方式得到:

  1. 根据相机内参、外参和地面平面计算;
  2. 使用多组图像点与地面测量点直接拟合。

逐射线求交更容易理解每一步的物理意义;单应矩阵更紧凑,也适合批量计算。将结果预计算为查找表后,两者在运行阶段都变成一次简单的数组访问。

根据 camera.bin 的数值规律,可以判断它非常符合地面平面投影模型;但如果没有原始生成代码或标定记录,仅凭二进制表无法百分之百确定它是由内外参直接计算,还是由实测控制点拟合单应矩阵后生成。

13.工程实现中最容易出错的地方

13.1 外参方向使用错误

必须确认外参表达的是:

text 复制代码
camera → robot

还是:

text 复制代码
robot → camera

两者需要互相求逆,不能直接混用。

13.2 图像预处理没有同步到标定模型

YOLO 的 640×640 输入可能由原始图像经过以下操作得到:

  • 等比例缩放;
  • 非等比例缩放;
  • 中心裁剪;
  • letterbox 填充;
  • 旋转或镜像。

查表必须针对最终参与索引的图像坐标生成。如果相机标定针对原始分辨率,而查询使用模型输入坐标,就必须把 resize、crop、padding 等变换计入内参或坐标换算。

13.3 忽略镜头畸变

广角相机的图像边缘畸变可能非常明显。查表生成时应明确输入图像是原始畸变图,还是已经去畸变后的图像,并使用匹配的相机模型。

13.4 将障碍物像素误认为地面点

地面查表只能给出射线与地面的交点。对于具有高度的障碍物,这个位置不是物体表面的真实三维点。若需要恢复障碍物三维形状,应使用深度传感器、双目、结构光、激光雷达或其他深度估计方法。

13.5 只判断数值是否有限

投影公式即使在地平线以上也可能输出有限数值,因此有效性判断还应包含射线方向、距离范围、ROI 和业务类别等条件。

14. 总结

所谓"像素到地面物理坐标查表",可以概括为以下过程:

text 复制代码
像素 (u,v)
   ↓ 相机内参
相机射线方向
   ↓ 相机外参
机器人坐标系中的射线
   ↓ 假设目标位于地面 Z=0
射线与地面的交点 (X,Y,0)
   ↓ 提前保存
像素到物理坐标查找表

其中:

  • 相机内参决定一个像素对应的观察方向;
  • 相机外参决定相机在机器人上的位置和朝向;
  • 地面 Z=0 提供了从一条射线确定唯一交点所需的额外约束;
  • 查找表将复杂投影计算提前完成,使运行时只需一次数组查询;
  • BEV 的倒梯形来自相机视场投影到地面后"近处窄、远处宽"的透视结构;
  • BEV 横轴使用 Y lateral、纵轴使用 X forward,是为了让机器人前进方向朝向屏幕上方;
  • 地面求交中的 Z=0 与深度反投影中的 Z=depth(u,v) 含义不同:前者是机器人坐标系中的平面高度,后者是相机坐标系中的光轴深度。

最后应牢记:查表返回的是"在地面假设下的投影交点"。它非常适合地面分割、可通行区域、割草区域和近地面目标的快速坐标转换,但它不能替代真正的三维深度感知。

相关推荐
boss-dog1 个月前
关于智能割草机器人调研
调研·割草机器人
szxinmai主板定制专家2 个月前
基于 RK3576 的智能割草机器人解决方案,从样机到量产
人工智能·嵌入式硬件·计算机视觉·fpga开发·机器人·rk3588+fpga·割草机器人
打马诗人8 个月前
【单目跟踪测距】使用YOLOv11和OCSort进行多类别目标跟踪,并集成单目相机测距功能,适用于交通场景目标检测、跟踪与距离估计
yolo·目标检测·目标跟踪·单目测距
Hi202402171 年前
基于ROS2进行相机标定,并通过测试相机到棋盘格之间的距离进行验证
数码相机·docker·ros·相机·机器视觉·ros2·单目测距
从懒虫到爬虫2 年前
yolov5 +gui界面+单目测距 实现对图片视频摄像头的测距
yolov5·单目测距·qq767172261
从懒虫到爬虫2 年前
YOLO目标检测的单目(多目标测距),使用相机光学模型,支持目标检测模型训练,可输出目标位置和距离信息并可视化
数码相机·yolo·目标检测·单目测距
渡伴3 年前
鱼眼相机的测距流程及误差分析[像素坐标系到空间一点以及测距和误差分析]
单目测距·鱼眼相机·坐标系转换
渡伴3 年前
基于yolov5的单目测距实现与总结+相机模型+标定
yolo·目标检测·单目测距·针孔相机模型
AutoDriver3 年前
单目测距那些事儿(上) | 从MobileEye谈起
目标检测·自动驾驶·单目测距·mobileye·视觉测距·视觉测速