Sensors in Depth: Camera Intrinsics to LiDAR Point Clouds, Part 2
lidar_bp = bpl.find("sensor.lidar.ray_cast")
self.agent_lidar = self.world.spawn_actor(
lidar_bp,
carla.Transform(carla.Location(z=2.5), carla.Rotation(yaw=-90.0)),
attach_to=self.ego)
两个数字:z = 2.5(车顶,比相机的 2.0 还高半米,避免机盖遮挡下扫的激光线)和 yaw = -90(绕竖直轴转 −90 度)。
激光雷达 360° 旋转,按说绕 z 轴转任何角度都”对称”——但点云输出的坐标系原点方向不对称:每条激光线的方位角是从传感器自身的 x 轴起算的。换一个 yaw,同一个世界点在点云里的 坐标就完全不同。
yaw = -90 的作用是对齐训练坐标系:AutoMoT 沿用 CARLA 0.9.15 时代 leaderboard 的标准挂法,其训练点云的坐标约定就建立在这个 yaw 上。UE5 侧复刻这个角度,模型看到的点云坐标系才与训练时一致。
跨版本迁移仿真器时,注意力容易全放在 channels、range 这类 blueprint 属性上——它们有”默认值会变”的坑。位姿走的是另一条路:它是用户代码显式给的,不会静默变,但也因此在重构挂载代码时最容易被”顺手优化”。对学习类模型,传感器位姿和数值参数同等重要:它决定了模型看到的坐标系,错 90° 等于把整个点云转了 90°,BEV 特征全废,且不报任何错。
专项探测(experiments/EXP-T3.4-automot-ue5/lidar_probe.md,2026-09-02 UE5 实测)确认:CARLA 0.10 的 sensor.lidar.ray_cast 全部关键属性可设,但默认值与 0.9.15 差异巨大:
| 参数 | 0.10 默认 | 0.9.15 默认(=训练口径) | 倍数 |
|---|---|---|---|
channels | 64 | 32 | 2× |
range | 50 m | 10 m | 5× |
points_per_second | 600000 | 56000 | ~10.7× |
rotation_frequency | 60 Hz | 10 Hz | 6× |
upper_fov / lower_fov | 10° / −30° | 10° / −30° | 相同 |
训练口径全取右列——AutoMoT 在 0.9.15 上跑时 agent 侧没有任何显式 set,全靠 blueprint 默认值,所以右列就是它”从小看到大”的点云规格。
upper_fov / lower_fov = 10° / −30° 两版相同:垂直视场 40°,上仰 10°、下扫 30°。下扫角度大是车载 LiDAR 的典型设计——路面和近处障碍物都在地平线以下。但”相同”不代表可以不设:跨版本迁移的铁律是每一个参数都显式 set,包括碰巧没变的——不设就是把正确性寄托在”默认值永远不变”上。
默认值变了,仿真器不 crash、不 warning:0.10 收到一台”裸” LiDAR 蓝图,就按自己的默认值吐 64 线、50 米、60 万 pps 的点云——格式合法、数据流畅,只有密度和形状悄悄换了规格。模型侧的表象是”性能莫名变差”,排查时最先怀疑的永远是模型和数据管线,最后才轮到传感器默认值。这就是为什么不显式 set 参数就一定静默掉坑。
机械旋转式 LiDAR 的 channels 是垂直方向的激光线数:32 线就是 32 束激光,按垂直视场(这里是上 10° 到下 −30°)排开,随旋转头一起 360° 扫描。线数决定点云在垂直方向的分辨率——线越多,垂直采样越密,车辆、行人轮廓的”层数”越多。
0.10 默认 64 线,0.9.15 默认 32 线。同样一辆车:
对人是”更清晰了”,对模型是灾难:AutoMoT 训练时吃的就是 32 线点云,它的 BEV 特征提取器是在这个密度和垂直分布上学出来的。换成 64 线点云,等于输入落在训练分布之外——BEV 特征直接失配,而且输入格式完全合法,没有任何报错。
lidar_bp.set_attribute("channels", "32")
成本一行代码;不设的代价是整条 UE5 数据链在错误的点云规格上跑完,下游所有指标作废。
range 是 LiDAR 的最远探测距离(米):超过这个距离的命中点不返回。0.10 默认 50 米,0.9.15 默认只有 10 米。
直觉上量程越大越好——看得远总是好事。但学习类模型不吃这套逻辑:
这就是”训练分布优先于传感器性能”的又一例:和 jpg 重注入、锁曝光同一逻辑——传感器配置的准则是复刻训练分布,不是追求指标好看。
lidar_bp.set_attribute("range", "10.0")
10 米对驾驶决策确实短(高速场景制动距离都不止),但这不是本实验要修的问题——模型能力上限由训练数据决定,仿真侧的职责是忠实复现训练条件,而不是替模型”升级”传感器。
points_per_second(pps)、rotation_frequency、channels 和仿真帧率共同决定每帧点云有多稠密。验算训练口径(56000 pps、10 Hz、32 线、20 FPS 仿真)是不是自洽:
每帧 2800 点正是 0.9.15 文档口径的预期密度(56000 pps ÷ 20 fps)。
CARLA 的 LiDAR 回调给的不是点云对象,是一块连续内存: 个 float32 紧凑排列。解码两行:
pts = np.frombuffer(ev.raw_data, dtype=np.float32).reshape(-1, 4)
frombuffer:零拷贝解释字节流;reshape(-1, 4):每 4 个浮点成一行,行数自动推——就是 个点。四列的含义:
| 列 | 含义 | 单位 / 坐标系 |
|---|---|---|
| 0–2 | x, y, z | 米,自车(ego)坐标系 |
| 3 | intensity | 反射强度 |
点直接在 ego 系下给出,不需要再做传感器-车辆的外参变换(挂载位姿已在 spawn 时生效)。intensity 反映目标表面的回波强度,与材质和入射角相关。
0.10 与 0.9.15 的点云内存格式完全同构——同一段 frombuffer + reshape 代码两侧通吃。格式兼容是好消息,但也是一个陷阱的引子:格式一样不等于内容一样,0.10 会在这块内存里填进 0.9.15 从不产生的东西——未命中射线的 sentinel 巨值(约 量级),必须清洗后才能进下游。
激光射出去没打到任何东西(超出量程、射向天空)时:
接近 float32 上限(约 ),这是典型的**哨兵值(sentinel value)**设计:用一个不可能出现在真实数据里的特殊值标记”此处无效”。本意不坏——保留了”这条射线存在但没命中”的信息;但它把”过滤无效点”的责任悄悄转移给了消费方。
AutoMoT 的点云下游是 lidar_to_histogram:把点撒进 BEV 网格做高度直方图。这个过程对坐标范围敏感:
而最阴的是它不报错:数据类型合法、形状合法、流程跑通,只有模型的 BEV 特征悄悄变成垃圾。表象是”UE5 侧模型性能崩了”,根因在一行没写的清洗代码上。
跨版本迁移时,“格式兼容”是最危险的兼容——字段定义一致让人跳过数据内容审计。正确姿势是探测期就做数值范围检查:对每类传感器的原始输出跑一遍 min/max/isfinite 统计,新旧版本对表,sentinel 这类差异在第一时间现形。
sentinel 巨值(约 )的清洗只要一行(route_executor.py 的 on_lidar 回调):
def on_lidar(ev):
pts = np.frombuffer(ev.raw_data, dtype=np.float32).reshape(-1, 4)
pts = pts[np.isfinite(pts).all(axis=1)] # 0.10 未命中 sentinel 清洗
self.agent_data["lidar"] = (ev.frame, pts)
np.isfinite(pts):逐元素检查”是有限值”——NaN 与 ±inf 判 False,得到与 pts 同形状的布尔数组。0.10 的未命中哨兵在 float32 流里表现为边界量级的非有限值,正好被这一步揪出。.all(axis=1):按行求逻辑与——一个点四个分量全是有限值才算好点。只要 x/y/z/intensity 任一分量是哨兵,整行判死。pts[掩码]:布尔索引,巨值点整行剔除,只留真实命中点。用 isfinite 而不是”和某个阈值比较”是对的做法:哨兵的确切值是实现细节(探测报告只说 量级),依赖具体数值比较脆弱;而”有限性”是语义判断,版本再怎么改哨兵值都稳。一个值得知道的边界情况:如果某代实现的哨兵恰好是 float32 上限以内仍然有限的巨值,isfinite 会放行——对数值范围有先验的场景(比如车载 LiDAR 的坐标不可能超过量程),再加一道 np.abs(pts[:, :3]) < range 的量程裁剪是双保险。
清洗必须在服务器侧、进 lidar_to_histogram 之前完成。点云一旦带着脏点进了 BEV 直方图分箱,污染就扩散到整帧特征,事后无法分离。这也是数据管线的一般原则:无效数据在离源头最近的地方杀掉,每往后传一站,定位成本翻一倍。
LiDAR 的 channels/range/pps/转速全部按训练口径显式对齐之后,实测揭示一个无法靠参数消除的差距(lidar_probe.md):
差了四倍多。参数没有设错——验算链(56000/10 转 → 每线每转 175 点 → 一帧半转 → 2800 点)在数学上是闭合的。缺的点丢在了射线命中率上:UE5 侧 raycast 实际打中物体返回的点更少,未命中的射线(清洗掉的那些)占比更高。几何场景、网格精度、射线求交实现的差异,都会体现在命中率上。
人为补点(插值、复制、降采样另一侧)在工程上轻而易举,但在这个项目里是篡改实验变量:
正确做法是如实报告、声明口径:这个密度差进入 CP3 报告的 LiDAR 稀释声明——AutoMoT 的结论带 LiDAR 通道,其 UE4/UE5 差异里混有几何域差,不能与纯渲染效应混为一谈。这也是项目选纯视觉的 SimLingo 当主模型的原因:渲染变量不被几何传感器稀释。
清洗后的点云进 AutoMoT 的 lidar_to_histogram:把点撒进 ±32 米的 BEV 网格(4 像素/米,即 256×256),按高度以 0.2 米为界分成上下两箱,输出 2×256×256 的直方图——这就是 AutoMoT 动作分支的鸟瞰输入。635 vs 2800 的密度差,最终表现为这张直方图的整体稀疏化。