自动驾驶-nuScenes 数据集与 3D 框投影深入分析

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

file

理解数据结构

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

file

四层坐标系

这一节请务必读懂。课程后面每次投影出错,十有八九是这里没理清。
file

读取与探索

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 对齐工作的地基。错一次就会导致后续所有几何计算全部偏移。

为什么必须做这个验证?

  1. 自动驾驶多传感器数据最容易踩的坑:NuScenes 的标注在全局坐标系,相机图像在相机坐标系,中间隔了 ego_pose(车身位姿)和 calibrated_sensor(相机外参)。很多人直接乘旋转矩阵却忘记取逆,结果框飞出画面或错位几十米。
  2. BEV 工作的绝对前提:BEV 感知要把多路相机特征投影到统一的车身/地面平面。投影矩阵、深度估计、跨视角对齐全部依赖这条变换链。一旦这里错了,后面的 LSS、BEVFormer、PETR 等全部白搭。
  3. 肉眼可验证:代码把结果画到真实相机图像上,直接看「框是否贴合物体」。比打印矩阵数值直观得多。

完整变换链(代码注释里写得最清楚):

全局坐标 --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(...) —— 完整渲染流程

对每一帧、每一路相机:

  1. 读图像、标定、位姿
  2. 遍历所有 3D 标注 → 算 8 角点(全局)
  3. global_to_camera
  4. 过滤(非常重要):
    • 所有角点深度 < min_depth(在相机后方)→ 跳过
    • 最近角点 > max_depth → 太远跳过
    • 有任何一个角点在后方 → 简单起见整框跳过(避免投影畸变)
  5. 投影到像素
  6. 再过滤画面外的框
  7. 画 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% 的问题都出在这里。

图表示

以下用真实生成示意图把整条投影验证的关键概念讲清楚。每张图都对应代码里最容易混淆的部分。

  1. 完整坐标变换链(代码最核心的那句注释)全局 → 车身 → 相机 → 像素
    file

  2. 为什么必须取逆?(最容易错的地方)
    ego_pose 和 calib 描述的是「从谁到谁」的方向,和我们需要的变换方向正好相反。
    file

  1. 3D 框 8 个角点是怎么算出来的
    file

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

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

什么是全局坐标(车在别人眼中的全局画面吗,比如有车,有行人,有卡车等),为什么会有全局坐标?什么是车身坐标, 全局坐标到车身坐标为什么要取逆?请用实图画出来解释

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

file

简单理解:
全局坐标就像给整座城市画了一张固定的坐标纸。不管你的车开到哪里,这张纸的原点永远不动。所有标注(annotation)都是写在这张纸上的。

  1. 为什么需要全局坐标?
    因为车是在运动的!如果没有全局坐标,只用车身坐标,会出现以下问题:
    • 静止的行人/建筑物,在车身坐标系里位置会不断变化(因为车在动)。
    • 不同时间戳的数据无法对齐。
    • 多帧融合、建图、长期跟踪全部做不了。

有了全局坐标后:

  • 静态物体位置永远固定。
  • 不同时刻的 ego 车位姿都能统一到同一张地图上。
  • 方便多传感器、多帧、多车协同。

file

  1. 什么是车身坐标(Ego / Vehicle Body Coordinate)?
    车身坐标(Ego Frame)是以自车为中心的坐标系。
    • 原点通常在车辆后轴中心(或 IMU 位置)
    • X 轴:车头方向(前)
    • Y 轴:左侧
    • Z 轴:向上

所有传感器(相机、激光雷达)的外参都是相对于这个车身坐标系标定的。

file

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

file

再用一个更直观的「人在房间里」比喻图:
file

总结一句话

坐标系 本质含义 原点是否移动 用途
全局坐标 整个场景的固定世界坐标系 永远固定 统一描述所有物体绝对位置
车身坐标 以自车为中心的坐标系 随车移动 描述传感器与自车关系

官方给的 ego_pose 是「车在全局里的位置」,我们要把「全局里的点」变到「车身上」,方向相反 → 必须取逆

这就是为什么代码里写 .T 和减法的原因。

全局坐标原点如何设定的?

全局坐标的原点是怎么设定的?人为设定的还是有讲究?为什么不能用gps定位的位置,这样就不用原点了?用画实图解释

下面用真实示意图彻底讲清楚你问的三个问题:
-1、全局坐标的原点是怎么设定的?

  • 2、是人为设定还是有讲究?
    -3、为什么不能直接用 GPS 位置(这样就“不用原点”了)?
  1. 全局坐标的原点是怎么设定的?
    在 NuScenes(以及绝大多数自动驾驶数据集)里,全局坐标的原点是人为选定的一个固定点,通常选在数据采集区域附近一个方便的位置。它不是地球的某个绝对特殊点,而是为了让这一整个 log / scene 的坐标数字比较小、计算方便而人为定的。

file

设定原则(有讲究的人为):

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

file

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

file

原因二:局部精度与数值问题

  • GPS 绝对精度通常只有几米到十几米(城市峡谷更差),而自动驾驶标注需要厘米级。
  • 经纬度直接做减法算距离会非常不直观,而且在高纬度地区变形严重。
  • 数字巨大(经度 116.xxx),浮点运算容易出精度问题。

为者常成,行者常至