在割草机器人、扫地机器人等项目中,如果视觉采用的是单目,经常会遇到这样一个需求:
已知相机图像中的一个像素,如何快速得到它在机器人周围地面上的实际物理位置?
一种常见实现方式,是提前生成一张"像素坐标到地面物理坐标"的查找表。运行时无需重复执行矩阵运算,只需用像素坐标查表,便能得到对应的前向距离和横向距离。(但是这样的使用前提是默认设备是运行在平坦的地面上的,如果遇到陡坡等不平的路况,识别出来的物体的实际物理位置就很不准)
本文以一个实际的 640×640 查找表 camera.bin 为例,梳理这种方法的原理、计算过程、坐标系定义、BEV 倒梯形的来源,以及它与深度图反投影之间的区别。
需要先强调:本文讨论的"单目深度查表"并不是利用神经网络预测任意物体的单目深度,而是利用相机标定参数和"目标点位于地面"的假设,求像素射线与地面的交点。
目录
- [1.camera.bin 是什么?](#1.camera.bin 是什么?)
- 2.一个像素为什么不能直接确定三维点?
- 3.从像素得到相机射线
- 4.相机外参起什么作用?
- [5.利用地面 Z=0 确定射线交点](#5.利用地面 Z=0 确定射线交点)
-
- [5.1 数值示例](#5.1 数值示例)
- [5.2 地面不一定必须写成 Z=0](#5.2 地面不一定必须写成 Z=0)
- 6.完整的查找表生成过程
- [7.为什么 BEV 图会呈现倒梯形?](#7.为什么 BEV 图会呈现倒梯形?)
- [8.为什么 BEV 横轴是 Y lateral,纵轴是 X forward?](#8.为什么 BEV 横轴是 Y lateral,纵轴是 X forward?)
- [9.它与深度图的 2D 转 3D 有什么不同?](#9.它与深度图的 2D 转 3D 有什么不同?)
- 10.为什么地平线附近会出现巨大数值?
- 11.所有像素都有坐标,是否代表都真实有效?
- 12.单应矩阵与逐射线求交
- 13.工程实现中最容易出错的地方
-
- [13.1 外参方向使用错误](#13.1 外参方向使用错误)
- [13.2 图像预处理没有同步到标定模型](#13.2 图像预处理没有同步到标定模型)
- [13.3 忽略镜头畸变](#13.3 忽略镜头畸变)
- [13.4 将障碍物像素误认为地面点](#13.4 将障碍物像素误认为地面点)
- [13.5 只判断数值是否有限](#13.5 只判断数值是否有限)
- [14. 总结](#14. 总结)
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)
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 处于同一条射线上,它们可以投影到同一个像素。因此,一个普通单目像素对应的是一条空间射线,而不是唯一的三维点。
要从这条射线上确定一个点,还需要增加一个条件。常见条件有两种:
- 深度相机或深度模型直接提供该像素的深度;
- 假设该像素对应的点位于一个已知平面,例如地面
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
单应矩阵可以通过两种方式得到:
- 根据相机内参、外参和地面平面计算;
- 使用多组图像点与地面测量点直接拟合。
逐射线求交更容易理解每一步的物理意义;单应矩阵更紧凑,也适合批量计算。将结果预计算为查找表后,两者在运行阶段都变成一次简单的数组访问。
根据 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)含义不同:前者是机器人坐标系中的平面高度,后者是相机坐标系中的光轴深度。
最后应牢记:查表返回的是"在地面假设下的投影交点"。它非常适合地面分割、可通行区域、割草区域和近地面目标的快速坐标转换,但它不能替代真正的三维深度感知。