自动驾驶-nuScenes 数据集与 3D 框投影深入分析
CARLA 造的是"你想要的数据",nuScenes 提供的是"真实世界长什么样"。 本节把它的数据结构彻底拆开——看懂了这个结构,你才知道自己该采什么。彻底搞清 4 层坐标系(传感器→车身→全局→BEV),这是整个课程最容易出错的地方,写出能把 3D 框投影到图像上的验证脚本——肉眼确认标定和坐标变换是对的。

理解数据结构
nuScenes 不是"一个文件夹装图片",而是一套关系型数据库,用 token 互相引用。 这个设计一开始很绕,但理解后你会发现它极其严谨。

四层坐标系
这一节请务必读懂。课程后面每次投影出错,十有八九是这里没理清。
读取与探索
explore_nuscenes.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""nuScenes 数据结构探索 —— 把关系型结构走一遍"""
import argparse
from nuscenes.nuscenes import NuScenes
def explore(dataroot, version="v1.0-mini"):
# verbose=True 会打印加载统计,第一次跑建议开着看看
nusc = NuScenes(version=version, dataroot=dataroot, verbose=True)
print("\n" + "="*60)
print(" 数据集概览")
print("="*60)
print(f"场景数 : {len(nusc.scene)}")
print(f"关键帧数 : {len(nusc.sample)}")
print(f"传感器采样数 : {len(nusc.sample_data)}")
print(f"标注框数 : {len(nusc.sample_annotation)}")
print(f"物体实例数 : {len(nusc.instance)}")
# ---------- 走一个 scene ----------
scene = nusc.scene[0]
print("\n" + "="*60)
print(" 第一个场景")
print("="*60)
print(f"名称 : {scene['name']}")
print(f"描述 : {scene['description']}")
print(f"帧数 : {scene['nbr_samples']}")
# ---------- 走一个 sample ----------
sample = nusc.get("sample", scene["first_sample_token"])
print("\n" + "="*60)
print(" 第一个关键帧的传感器")
print("="*60)
for sensor_name, sd_token in sample["data"].items():
sd = nusc.get("sample_data", sd_token)
print(f" {sensor_name:20} {sd['fileformat']:5} "
f"{sd['width']}x{sd['height']}" if sd['width']
else f" {sensor_name:20} {sd['fileformat']}")
# ---------- 看一路相机的标定 ----------
cam_token = sample["data"]["CAM_FRONT"]
cam_data = nusc.get("sample_data", cam_token)
calib = nusc.get("calibrated_sensor", cam_data["calibrated_sensor_token"])
pose = nusc.get("ego_pose", cam_data["ego_pose_token"])
print("\n" + "="*60)
print(" CAM_FRONT 标定参数")
print("="*60)
print(f"图像文件 : {cam_data['filename']}")
print(f"外参平移 (相机在车身系) : {calib['translation']}")
print(f"外参旋转 (四元数 wxyz) : {calib['rotation']}")
print(f"内参矩阵 K:")
for row in calib["camera_intrinsic"]:
print(f" [{row[0]:9.3f} {row[1]:9.3f} {row[2]:9.3f}]")
print(f"\n车身在全局系的位置 : {pose['translation']}")
# ---------- 看标注 ----------
print("\n" + "="*60)
print(f" 本帧标注框(共 {len(sample['anns'])} 个,显示前 5 个)")
print("="*60)
for ann_token in sample["anns"][:5]:
ann = nusc.get("sample_annotation", ann_token)
print(f" 类别 : {ann['category_name']}")
print(f" 全局位置 : [{ann['translation'][0]:.1f}, "
f"{ann['translation'][1]:.1f}, {ann['translation'][2]:.1f}]")
print(f" 尺寸 wlh : {ann['size']}")
print(f" 可见度 : {ann['visibility_token']}")
# ---------- 类别分布 ----------
from collections import Counter
cats = Counter(a["category_name"] for a in nusc.sample_annotation)
print("\n" + "="*60)
print(" 类别分布(前 10)")
print("="*60)
for name, cnt in cats.most_common(10):
print(f" {name:42} {cnt:6d}")
print("\n提示: 注意长尾分布 —— 少数类别占了绝大多数样本")
print(" 这正是后面 M4/M5 要处理的困难样本问题\n")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--dataroot", default="~/data/nuscenes")
ap.add_argument("--version", default="v1.0-mini")
args = ap.parse_args()
import os
explore(os.path.expanduser(args.dataroot), args.version)
探索结果:
(ad) lunking@lianxiang-desktop:~/carla/work/nuscenes$ python explore_nuscenes.py --dataroot ~/data/nuscenes
======
Loading NuScenes tables for version v1.0-mini...
23 category,
8 attribute,
4 visibility,
911 instance,
12 sensor,
120 calibrated_sensor,
31206 ego_pose,
8 log,
10 scene,
404 sample,
31206 sample_data,
18538 sample_annotation,
4 map,
Done loading in 0.497 seconds.
======
Reverse indexing ...
Done reverse indexing in 0.1 seconds.
======
============================================================
数据集概览
============================================================
场景数 : 10
关键帧数 : 404
传感器采样数 : 31206
标注框数 : 18538
物体实例数 : 911
============================================================
第一个场景
============================================================
名称 : scene-0061
描述 : Parked truck, construction, intersection, turn left, following a van
帧数 : 39
============================================================
第一个关键帧的传感器
============================================================
RADAR_FRONT pcd
RADAR_FRONT_LEFT pcd
RADAR_FRONT_RIGHT pcd
RADAR_BACK_LEFT pcd
RADAR_BACK_RIGHT pcd
LIDAR_TOP pcd
CAM_FRONT jpg 1600x900
CAM_FRONT_RIGHT jpg 1600x900
CAM_BACK_RIGHT jpg 1600x900
CAM_BACK jpg 1600x900
CAM_BACK_LEFT jpg 1600x900
CAM_FRONT_LEFT jpg 1600x900
============================================================
CAM_FRONT 标定参数
============================================================
图像文件 : samples/CAM_FRONT/n015-2018-07-24-11-22-45+0800__CAM_FRONT__1532402927612460.jpg
外参平移 (相机在车身系) : [1.70079118954, 0.0159456324149, 1.51095763913]
外参旋转 (四元数 wxyz) : [0.4998015430569128, -0.5030316162024876, 0.4997798114386805, -0.49737083824542755]
内参矩阵 K:
[ 1266.417 0.000 816.267]
[ 0.000 1266.417 491.507]
[ 0.000 0.000 1.000]
车身在全局系的位置 : [411.4199861830012, 1181.197175631848, 0.0]
============================================================
本帧标注框(共 69 个,显示前 5 个)
============================================================
类别 : human.pedestrian.adult
全局位置 : [373.3, 1130.4, 0.8]
尺寸 wlh : [0.621, 0.669, 1.642]
可见度 : 1
类别 : human.pedestrian.adult
全局位置 : [378.9, 1153.3, 0.9]
尺寸 wlh : [0.775, 0.769, 1.711]
可见度 : 2
类别 : vehicle.car
全局位置 : [353.8, 1132.4, 0.6]
尺寸 wlh : [2.011, 4.633, 1.573]
可见度 : 3
类别 : human.pedestrian.adult
全局位置 : [376.1, 1158.5, 0.9]
尺寸 wlh : [0.752, 0.819, 1.637]
可见度 : 4
类别 : movable_object.trafficcone
全局位置 : [410.1, 1196.8, 0.7]
尺寸 wlh : [0.427, 0.359, 0.794]
可见度 : 4
============================================================
类别分布(前 10)
============================================================
vehicle.car 7619
human.pedestrian.adult 4765
movable_object.barrier 2323
movable_object.trafficcone 1378
vehicle.truck 649
vehicle.motorcycle 471
vehicle.bus.rigid 353
vehicle.bicycle 243
vehicle.construction 196
human.pedestrian.construction_worker 193
提示: 注意长尾分布 —— 少数类别占了绝大多数样本
这正是后面 M4/M5 要处理的困难样本问题
(ad) leoking@leoking-desktop:~/carla/work/nuscenes$
投影验证(本节核心产出)
这个脚本把 3D 标注框投影到相机图像上。如果框能准确套住物体,说明你的坐标变换理解正确—— 这是后面所有 BEV 工作的地基。
project_boxes.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
把 nuScenes 的 3D 标注框投影到相机图像 —— 验证坐标变换是否正确
完整变换链(本节最重要的知识点):
全局 --ego_pose⁻¹--> 车身 --calib⁻¹--> 相机 --K--> 像素
注意前两步都是「逆变换」,因为:
ego_pose 描述「车身在全局系的位姿」,我们要把全局点转到车身系 → 取逆
calib 描述「相机在车身系的位姿」,我们要把车身点转到相机系 → 取逆
"""
import argparse
import os
import numpy as np
import cv2
from nuscenes.nuscenes import NuScenes
from pyquaternion import Quaternion
# 3D 框的 12 条边(顶点索引对)
BOX_EDGES = [
(0, 1), (1, 2), (2, 3), (3, 0), # 顶面
(4, 5), (5, 6), (6, 7), (7, 4), # 底面
(0, 4), (1, 5), (2, 6), (3, 7), # 立柱
]
COLORS = {
"vehicle": (80, 220, 120),
"human": (100, 160, 255),
"movable": (80, 200, 255),
"default": (200, 200, 200),
}
def get_color(category):
for key, c in COLORS.items():
if category.startswith(key):
return c
return COLORS["default"]
def box_corners(center, size, rotation):
"""
计算 3D 框的 8 个角点(全局坐标系)
参数:
center : [x, y, z] 框中心
size : [w, l, h] 宽、长、高(注意 nuScenes 的顺序)
rotation : 四元数 [w, x, y, z]
返回:
(3, 8) 数组
"""
w, l, h = size
# 在物体自身坐标系下的 8 个角点
# x 沿车长方向, y 沿车宽, z 向上
x_c = l / 2 * np.array([1, 1, 1, 1, -1, -1, -1, -1])
y_c = w / 2 * np.array([1, -1, -1, 1, 1, -1, -1, 1])
z_c = h / 2 * np.array([1, 1, -1, -1, 1, 1, -1, -1])
corners = np.vstack((x_c, y_c, z_c)) # (3, 8)
# 旋转 + 平移到全局系
q = Quaternion(rotation)
corners = np.dot(q.rotation_matrix, corners)
corners = corners + np.array(center).reshape(3, 1)
return corners
def global_to_camera(points, ego_pose, calib):
"""
全局坐标 → 相机坐标(本函数是整节课的核心)
points : (3, N)
"""
# ---- 第 1 步: 全局 → 车身 ----
# ego_pose 给的是「车身在全局的位姿」,所以要取逆
points = points - np.array(ego_pose["translation"]).reshape(3, 1)
points = np.dot(
Quaternion(ego_pose["rotation"]).rotation_matrix.T, # .T 即取逆(旋转阵正交)
points)
# ---- 第 2 步: 车身 → 相机 ----
# calib 给的是「相机在车身的位姿」,同样取逆
points = points - np.array(calib["translation"]).reshape(3, 1)
points = np.dot(
Quaternion(calib["rotation"]).rotation_matrix.T,
points)
return points
def camera_to_pixel(points, K):
"""
相机坐标 → 像素坐标(针孔投影)
points : (3, N),相机系下,z 为深度
返回 : (2, N) 像素坐标, (N,) 深度
"""
depths = points[2, :].copy()
# 齐次投影: p = K @ P, 然后除以 z
projected = np.dot(np.array(K), points)
# 防止除零
safe_z = np.where(np.abs(projected[2, :]) < 1e-6, 1e-6, projected[2, :])
projected = projected[:2, :] / safe_z
return projected, depths
def render_sample(nusc, sample_token, cam_name, out_path,
min_depth=1.0, max_depth=60.0):
sample = nusc.get("sample", sample_token)
cam_token = sample["data"][cam_name]
cam_data = nusc.get("sample_data", cam_token)
# 读图
img_path = os.path.join(nusc.dataroot, cam_data["filename"])
img = cv2.imread(img_path)
if img is None:
raise FileNotFoundError(f"读不到图像: {img_path}")
# 取标定与位姿
calib = nusc.get("calibrated_sensor", cam_data["calibrated_sensor_token"])
pose = nusc.get("ego_pose", cam_data["ego_pose_token"])
K = calib["camera_intrinsic"]
n_drawn = 0
for ann_token in sample["anns"]:
ann = nusc.get("sample_annotation", ann_token)
# 1) 算 8 个角点(全局系)
corners = box_corners(ann["translation"], ann["size"], ann["rotation"])
# 2) 全局 → 相机
corners_cam = global_to_camera(corners, pose, calib)
# 3) 过滤:所有角点都在相机后方 or 太远 → 跳过
if np.all(corners_cam[2, :] < min_depth):
continue
if np.min(corners_cam[2, :]) > max_depth:
continue
# 有角点在后方的框投影会畸变,简单起见要求全部在前方
if np.any(corners_cam[2, :] < min_depth):
continue
# 4) 相机 → 像素
pts, depths = camera_to_pixel(corners_cam, K)
# 5) 过滤画面外的
h, w = img.shape[:2]
if np.all(pts[0, :] < 0) or np.all(pts[0, :] > w):
continue
if np.all(pts[1, :] < 0) or np.all(pts[1, :] > h):
continue
# 6) 画 12 条边
color = get_color(ann["category_name"])
for i, j in BOX_EDGES:
p1 = (int(pts[0, i]), int(pts[1, i]))
p2 = (int(pts[0, j]), int(pts[1, j]))
cv2.line(img, p1, p2, color, 2, cv2.LINE_AA)
# 7) 标注类别与距离
label = ann["category_name"].split(".")[-1]
dist = np.mean(depths)
text = f"{label} {dist:.0f}m"
tx, ty = int(np.min(pts[0, :])), int(np.min(pts[1, :])) - 6
cv2.putText(img, text, (tx, max(ty, 14)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1, cv2.LINE_AA)
n_drawn += 1
# 左上角信息条
cv2.rectangle(img, (0, 0), (330, 30), (0, 0, 0), -1)
cv2.putText(img, f"{cam_name} | drawn {n_drawn} boxes", (8, 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
cv2.imwrite(out_path, img)
print(f"[{cam_name}] 画了 {n_drawn} 个框 → {out_path}")
return n_drawn
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dataroot", default="~/data/nuscenes")
ap.add_argument("--version", default="v1.0-mini")
ap.add_argument("--out", default="./out_proj")
args = ap.parse_args()
nusc = NuScenes(version=args.version,
dataroot=os.path.expanduser(args.dataroot),
verbose=False)
# 取第一个场景的第一帧,6 路相机全画一遍
scene = nusc.scene[0]
sample_token = scene["first_sample_token"]
cams = ["CAM_FRONT", "CAM_FRONT_LEFT", "CAM_FRONT_RIGHT",
"CAM_BACK", "CAM_BACK_LEFT", "CAM_BACK_RIGHT"]
total = 0
for cam in cams:
out = os.path.join(args.out, f"{cam}.jpg")
total += render_sample(nusc, sample_token, cam, out)
print(f"\n共画出 {total} 个框(同一物体可能出现在多路相机中)")
print(f"请打开 {args.out}/ 目录肉眼检查:")
print(" ✓ 框能准确套住车辆行人 → 坐标变换正确")
print(" ✗ 框错位/跑飞 → 检查是否漏了逆变换")
if __name__ == "__main__":
main()上边代码解读
这段代码的核心目的:验证 NuScenes 中「全局 → 车身 → 相机 → 像素」整条坐标变换链是否正确。
如果投影出来的 3D 框能准确套住图像中的车辆/行人,说明你对位姿逆变换、相机内外参理解无误——这正是后续所有 BEV(鸟瞰图)融合、多视角检测、LiDAR-Camera 对齐工作的地基。错一次就会导致后续所有几何计算全部偏移。
为什么必须做这个验证?
- 自动驾驶多传感器数据最容易踩的坑:NuScenes 的标注在全局坐标系,相机图像在相机坐标系,中间隔了 ego_pose(车身位姿)和 calibrated_sensor(相机外参)。很多人直接乘旋转矩阵却忘记取逆,结果框飞出画面或错位几十米。
- BEV 工作的绝对前提:BEV 感知要把多路相机特征投影到统一的车身/地面平面。投影矩阵、深度估计、跨视角对齐全部依赖这条变换链。一旦这里错了,后面的 LSS、BEVFormer、PETR 等全部白搭。
- 肉眼可验证:代码把结果画到真实相机图像上,直接看「框是否贴合物体」。比打印矩阵数值直观得多。
完整变换链(代码注释里写得最清楚):
全局坐标 --ego_pose⁻¹--> 车身坐标 --calib⁻¹--> 相机坐标 --K--> 像素坐标
代码结构与逐函数分析
1. 辅助定义
BOX_EDGES:3D 框的 12 条边(顶面 4 + 底面 4 + 立柱 4),用来画线。COLORS+get_color:按类别(vehicle/human/movable)上色,方便区分。
2. box_corners(center, size, rotation) —— 生成全局系下的 8 个角点
w, l, h = size # NuScenes 顺序是 [宽, 长, 高]
# 物体自身坐标系下的 8 个角点
# x 沿车长方向, y 沿车宽, z 向上
x_c = l/2 * [1,1,1,1,-1,-1,-1,-1]
y_c = w/2 * [1,-1,-1,1,1,-1,-1,1]
z_c = h/2 * [1,1,-1,-1,1,1,-1,-1]
然后用四元数旋转 + 平移到全局坐标系。
图解(物体局部坐标系):
z↑
|
| 顶面 (z = +h/2)
y←---+---→x (车头朝 +x)
|
底面 (z = -h/2)
8 个角点编号示意(常见顺序):
0───1
/| /|
3─┼─2 |
| 4─┼─5
|/ |/
7───6
旋转矩阵把局部点变成全局朝向,再加中心点平移。
3. global_to_camera(points, ego_pose, calib) —— 整节课最核心函数
# 第1步:全局 → 车身(取逆)
points = points - ego_pose["translation"]
points = R_ego.T @ points # R.T 因为旋转矩阵正交,逆 = 转置
# 第2步:车身 → 相机(取逆)
points = points - calib["translation"]
points = R_cam.T @ points
为什么必须取逆?(关键概念图)
全局坐标系 (Global)
↑
| ego_pose = T_global←ego (描述「车身在全局里的位置和朝向」)
|
车身坐标系 (Ego / Vehicle)
↑
| calib = T_ego←camera (描述「相机在车身上的位置和朝向」)
|
相机坐标系 (Camera)
4. camera_to_pixel(points, K) —— 针孔相机投影
projected = K @ points # 得到 [u*z, v*z, z]
u = (u*z) / z
v = (v*z) / z
标准针孔模型:
同时返回深度 ( Z )(相机系下 z 坐标),用于后面过滤和标注距离。
5. render_sample(...) —— 完整渲染流程
对每一帧、每一路相机:
- 读图像、标定、位姿
- 遍历所有 3D 标注 → 算 8 角点(全局)
global_to_camera- 过滤(非常重要):
- 所有角点深度 < min_depth(在相机后方)→ 跳过
- 最近角点 > max_depth → 太远跳过
- 有任何一个角点在后方 → 简单起见整框跳过(避免投影畸变)
- 投影到像素
- 再过滤画面外的框
- 画 12 条边 + 类别+距离文字
最后在左上角写「画了几个框」。
6. main
取第一个场景的第一帧,把 6 路相机(前/后/左/右)全部投影一遍,保存到 ./out_proj/。
复杂概念可视化总结
完整数据流:
3D 标注 (全局)
│
│ box_corners()
▼
8 个角点 (全局)
│
│ global_to_camera() ← 两次逆变换
▼
8 个角点 (相机)
│
│ camera_to_pixel() + K
▼
8 个像素点
│
│ 画线 (BOX_EDGES)
▼
相机图像上的 3D 框
坐标系关系侧视图(简化):
全局系原点
\
\ ego_pose
\
车身原点 ──── calib ──── 相机原点
(外参) │
│ 光轴 (Z)
▼
图像平面 (像素)
运行后如何判断对错
- 正确:框紧紧套住车辆/行人,距离标注合理(例如 10–40 m),不同相机看到同一物体的框一致。
- 错误:框整体平移、旋转、飞到天上/地下 → 大概率漏了某一处的逆变换(
.T或减法顺序)。
这个验证通过后,你就可以放心去做:
- 多相机到 BEV 的投影矩阵
- 深度估计监督
- 跨视角特征对齐
- LiDAR 点云投影到图像
等等所有需要几何一致性的工作。
如果运行后框有偏差,优先检查 global_to_camera 里的两处 .T 和减法顺序——90% 的问题都出在这里。
图表示
以下用真实生成示意图把整条投影验证的关键概念讲清楚。每张图都对应代码里最容易混淆的部分。
-
完整坐标变换链(代码最核心的那句注释)全局 → 车身 → 相机 → 像素

-
为什么必须取逆?(最容易错的地方)
ego_pose 和 calib 描述的是「从谁到谁」的方向,和我们需要的变换方向正好相反。
-
3D 框 8 个角点是怎么算出来的

-
相机针孔投影(camera_to_pixel)相机坐标系下的 3D 点如何变成图像上的 2D 像素。

-
整条流程一览(从标注到最终画在图像上)把所有步骤串起来看。

-
什么是全局坐标(车在别人眼中的全局画面吗,比如有车,有行人,有卡车等),为什么会有全局坐标?什么是车身坐标, 全局坐标到车身坐标为什么要取逆?请用实图画出来解释
- 下面用真实示意图把你问的三个核心问题讲透:什么是全局坐标?
- 为什么需要全局坐标?
- 什么是车身坐标?全局 → 车身为什么必须取逆?
- 什么是全局坐标(Global Coordinate)?
全局坐标就是整个场景的固定世界坐标系(World Frame)。
它不是「别人眼里的画面」,而是一张绝对固定的地图坐标系。
所有物体(你的车、对面的车、行人、卡车、红绿灯……)的位置和朝向,都用这个共同的固定原点来描述。

简单理解:
全局坐标就像给整座城市画了一张固定的坐标纸。不管你的车开到哪里,这张纸的原点永远不动。所有标注(annotation)都是写在这张纸上的。
- 为什么需要全局坐标?
因为车是在运动的!如果没有全局坐标,只用车身坐标,会出现以下问题:- 静止的行人/建筑物,在车身坐标系里位置会不断变化(因为车在动)。
- 不同时间戳的数据无法对齐。
- 多帧融合、建图、长期跟踪全部做不了。
有了全局坐标后:
- 静态物体位置永远固定。
- 不同时刻的 ego 车位姿都能统一到同一张地图上。
- 方便多传感器、多帧、多车协同。

- 什么是车身坐标(Ego / Vehicle Body Coordinate)?
车身坐标(Ego Frame)是以自车为中心的坐标系。- 原点通常在车辆后轴中心(或 IMU 位置)
- X 轴:车头方向(前)
- Y 轴:左侧
- Z 轴:向上
所有传感器(相机、激光雷达)的外参都是相对于这个车身坐标系标定的。

- 全局坐标 → 车身坐标 为什么必须取逆?(最关键)
NuScenes 里的 ego_pose 记录的是:「车身在全局坐标系中的位姿」

再用一个更直观的「人在房间里」比喻图:
总结一句话
| 坐标系 | 本质含义 | 原点是否移动 | 用途 |
|---|---|---|---|
| 全局坐标 | 整个场景的固定世界坐标系 | 永远固定 | 统一描述所有物体绝对位置 |
| 车身坐标 | 以自车为中心的坐标系 | 随车移动 | 描述传感器与自车关系 |
官方给的 ego_pose 是「车在全局里的位置」,我们要把「全局里的点」变到「车身上」,方向相反 → 必须取逆。
这就是为什么代码里写 .T 和减法的原因。
全局坐标原点如何设定的?
全局坐标的原点是怎么设定的?人为设定的还是有讲究?为什么不能用gps定位的位置,这样就不用原点了?用画实图解释
下面用真实示意图彻底讲清楚你问的三个问题:
-1、全局坐标的原点是怎么设定的?
- 2、是人为设定还是有讲究?
-3、为什么不能直接用 GPS 位置(这样就“不用原点”了)?
- 全局坐标的原点是怎么设定的?
在 NuScenes(以及绝大多数自动驾驶数据集)里,全局坐标的原点是人为选定的一个固定点,通常选在数据采集区域附近一个方便的位置。它不是地球的某个绝对特殊点,而是为了让这一整个 log / scene 的坐标数字比较小、计算方便而人为定的。

设定原则(有讲究的人为):
- 原点尽量靠近数据采集区域(让坐标值在几十到几百米范围内,而不是几万米)。
- 坐标轴方向固定为 东-北-上(ENU):X 向东、Y 向北、Z 向上。
- 一旦选定,整个 scene / log 的所有时间戳、所有标注都使用这个永远不动的原点。
- 为什么是人为设定,而不是随便乱定?
虽然是人为的,但有明确工程讲究:- 数值稳定性:坐标太大会导致浮点精度问题(尤其是旋转矩阵计算)。
- 方便可视化与调试:数字小,人眼容易检查。
- 与高精地图对齐:很多高精地图本身也是用本地局部坐标系。

- 为什么不能直接用 GPS 位置?这样就“不用原点”了?
这是很多人一开始都会有的疑问。答案是:GPS 不能直接当全局笛卡尔坐标用。
原因一:GPS 是地球椭球坐标,不是平直的米制坐标
GPS 给的是经度、纬度、高度(WGS84),这是球面/椭球坐标系,不是我们需要的欧几里得直角坐标(X、Y、Z 以米为单位)。

原因二:局部精度与数值问题
- GPS 绝对精度通常只有几米到十几米(城市峡谷更差),而自动驾驶标注需要厘米级。
- 经纬度直接做减法算距离会非常不直观,而且在高纬度地区变形严重。
- 数字巨大(经度 116.xxx),浮点运算容易出精度问题。
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)