Town10 and the Aligned Route Set, Part 1
对比实验的第一个前提是两侧用同一个考场,而两代仿真器的可用地图差距悬殊(均为启动 server 后 API 逐条探测的实测值,不是抄文档):
| 侧 | 仿真器 | 可用地图 |
|---|---|---|
| UE4 | CARLA 0.9.15 | 20 张:Town01–07(各带 _Opt 优化版)、Town10HD(+_Opt)、Town11/12/13/15 |
| UE5 | CARLA 0.10 | 2 张:Town10HD_Opt(重制版)+ Mine_01(矿场) |
交集只剩 Town10——没得选,这是 0.10 技术预览期带来的硬约束。
截至 2026-08,CARLA 0.10(UE5.5)仍是技术预览版,缺的不只是地图:
Mine_01 矿场图没有城市道路网和信号灯,做不了城市驾驶评测,所以真正的候选只有 Town10 重制版一张。
单图约束后面一路传导:路线集只能在 Town10 上出(下半集)、结论只限”受控对齐条件下的存在性证据”、域随机化(domain randomization)维度被锁死——这些都在 CP5 报告的 limitation 里如实登记。
CARLA 地图的道路网络由 OpenDRIVE(开放道路格式)语义描述,而不是渲染网格。评测逻辑关心的三要素:
这一层是”语义”,与渲染无关:同一张 Town10 在 UE4/UE5 两侧的车道几何、路口拓扑、信号灯周期是同一份逻辑——这是对比实验公平性的地基。若两侧信号灯周期或路口几何不一致,DS 差异就无法归因。
驾驶分 DS(Driving Score)是路线完成分 × 违章罚分连乘:
其中 RC(Route Completion)是路线完成百分比(0–100), 是第 次违章的罚分系数。本项目的评分核心 tools/ue5harness/scoring_core.py 与 Bench2Drive 的 leaderboard/utils/statistics_manager.py 系数集完全一致(跨侧唯一事实源):
| 违章 | 系数 |
|---|---|
| 撞行人 | 0.5 |
| 撞车 | 0.6 |
| 撞静态物 | 0.65 |
| 闯红灯 | 0.7 |
| 未停 stop 标志 | 0.8 |
| 场景超时 / 未让行急救车 | 0.7 |
闯一次红灯 DS 直接乘 0.7——罚掉三成;闯两次就是 ,腰斩。乘法结构意味着违章叠加是指数级的,这也是为什么”路口 + 信号灯”区域对最终分数的杀伤力远大于直行路段。
地图作者在每张地图里预埋一批”合法车位”——spawn 点(出生点),每个是一个完整的位姿(transform):三维位置 + 朝向(yaw),保证车辆出生在路上而不是楼里。API 一行拿到全部:
spawn_points = tmap.get_spawn_points() # list[carla.Transform],UE5 侧 Town10HD_Opt 实测 155 个
路线的起点、终点都从这个列表里取。路线档案里”28 号 → 115 号”这样的表述,用的就是列表下标。
get_spawn_points() 返回的是一个数组,编号只是数组索引。UE4 和 UE5 两侧的 Town10 各自有独立的一套 spawn 点列表——虽然都来自 Town10 这张地图,但:
因此跨侧对齐时绝不能抄下标,更不能抄坐标。本项目路线生成器(tools/t23_generate_aligned_routes.py)两侧各跑一遍,各自在本侧 spawn 列表里采样,跨侧对齐只靠 route_id 语义对应(同 id 即同题),坐标各侧独立实测——这正是”对齐为度”在数据层面的落法。
随机撒坐标可能落在楼顶、河里、护栏外;spawn 点是地图作者验证过的可行驶位姿。用它当起点/终点,还能顺带保证英雄车(ego vehicle)出生朝向与车道方向一致,省掉”出生即压线”的脏数据。
KID(Kernel Inception Distance)度量两组图像在 Inception 特征空间里的分布差异。做法:把每张图过一遍 InceptionV3 网络取倒数第二层特征向量,然后比较两组特征分布的最大均值差异(MMD, Maximum Mean Discrepancy)的平方:
其中 是 Inception 特征提取器,MMD 用多项式核 ( 为特征维度)。直觉理解:MMD 比较两个分布的”核均值嵌入”,两分布越像值越小,0 表示不可区分;无需人工标注,相当于给两组照片的风格差距自动打分。
与更常见的 FID(Fréchet Inception Distance)不同,KID 不假设特征服从高斯分布,且用无偏估计量——小样本下更稳,代价是估计值可以为负(无偏估计量的正常性质,不代表”距离为负”)。
实验设置:成对采图(UE4 12636 张 / UE5 21056 张 PNG),相机规格复刻 SimLingo 训练相机(1024×512, fov 110°),曝光属性锁定, bootstrap 求 95% 置信区间。结果(experiments/EXP-T3.2-kid/metrics.json):
| 对比 | KID |
|---|---|
| 跨引擎 UE4 vs UE5 | 0.0743,CI95 [0.0712, 0.0775] |
| 域内基线 UE4 vs UE4(自己重跑) | 0.0001 |
| 域内基线 UE5 vs UE5 | −0.0000 |
跨引擎与域内相差约三个数量级(≈700×),且置信区间与零完全不重叠——“语义对齐、渲染不对齐”从此不是形容词,是有数字背书的结论。域内基线 ≈0 也同时标定了度量本身的噪声底:两侧引擎确定性重渲染几乎不产生 KID。
采图时给每张图打上场景组标签(直行/左转/右转,随图落盘),KID 分组计算(experiments/EXP-T3.2-kid/metrics.json):
| 场景组 | KID | 样本量(UE4 / UE5) |
|---|---|---|
| 直行 straight | 0.094 | 4284 / 6980 |
| 左转 left | 0.122 | 5803 / 8723 |
| 右转 right | 0.196 | 2549 / 1731 |
右转组接近直行组的两倍。
KID 度量的是整幅图像的特征分布差异,所以某类画面元素占比越大,它的渲染差异对 KID 的贡献就越大。转弯时相机正对路口:
所以 right > left > straight 的排序,本质是”画面里城市结构占比”的排序。右转样本量最小(2549/1731),标准差也最大(0.0014 vs 直行 0.0008),但 2 倍量级的组间差远超这个噪声。
这个分组结果反过来约束出题:路线集必须覆盖转弯,不能只考直行——否则恰好把渲染差异最大的场景从考卷里漏掉,研究”渲染保真度影响”就变成了在差异最小的切片上做实验。这也是下半集路线生成器专门加”路口定向生成”一路候选的直接动机。
leaderboard 系路线 XML 的根元素是 <routes>,每条路线一个 <route> 标签,三个属性:
<route id="10000" road_id="28" town="Town10HD_Opt">
id:路线编号。本项目从 10000 连续编到 10019,共 20 条。它是跨引擎对齐的唯一钥匙——两侧 XML 里 id 相同的两条路线就是”同一道考题”,所有报告里的跨侧对照都按 id 配对,而不是按坐标(坐标两侧独立生成,完全不同)。road_id:起点 spawn 点的索引,顺带回答了英雄车出生在哪个车位。注意它只是本侧 get_spawn_points() 列表的下标,跨侧不可比。town:声明在哪张图上考。这里有个容易看错的细节:UE5 侧写 Town10HD_Opt,UE4 侧写 Town10HD(t23_generate_aligned_routes.py 里 town = "Town10HD_Opt" if side == "ue5" else "Town10HD")。0.10 的重制版地图就叫 Town10HD_Opt 这个名字,两侧 XML 的 town 字段因此并不逐字相同——对齐靠 id,不靠 town 字符串。leaderboard 的路线 XML 是社区事实标准:Bench2Drive、SimLingo、carla_garage 的评测器都消费它。本项目保持 schema 完全兼容,换来一个关键性质——UE4 侧官方评测管线零改动即可消费同一套文件,UE5 侧自建 executor 也按同一 schema 解析,两侧分数口径直接可比,评测器本身不引入新的变量。
<waypoints> 里是沿路线每隔 2 m 一个的位置点。以 10000 号路线的真实坐标为例(UE5 侧):x 基本不动在 −41 附近,y 从 88.9 一路减到 −12.8——一条沿南北向直行约 102 m 的路线,五十多个路点。与 meta 档案里的 distance: 101.76 吻合。
2 m 的分辨率来自生成时的规划器参数:
grp = GlobalRoutePlanner(tmap, 2.0) # 第二个参数 = 路点采样分辨率(米)
route = grp.trace_route(start, end) # 返回 [(waypoint, RoadOption), ...]
leaderboard 原生格式其实允许稀疏关键点(执行器内部再规划补全)。本项目刻意存全量密集路点,生成器源码里的注释说得很直白:
# 保存完整路线 waypoints(而非稀疏 keypoints),避免执行器重新规划时路径漂移
full_waypoints = [r[0].transform.location for r in route]
稀疏关键点意味着执行器侧要重新寻路:不同执行器、不同地图版本、甚至不同参数的全局规划器,补出来的路径可能走不同车道、绕不同弯——这在单侧评测里无伤大雅,但在跨引擎对比里是致命的:两侧走的路径不一样,DS 差异就混进了”路径漂移”这个干扰变量。全量路点把这个自由度彻底钉死:执行器照点走,不重新规划。
meta 档案里的 distance 是路点逐段欧氏距离累加:
dist = sum(route[k][0].transform.location.distance(route[k+1][0].transform.location)
for k in range(len(route) - 1))
所以 10000 号路线两侧档案分别是 101.76 m(UE5)与 101.66 m(UE4)——10 cm 的差异不是 bug,而是两侧各自独立规划、独立累加的实测结果,也是”对齐为度”(同量级即可,不逐点相等)的直接体现。
每条路线的 <scenarios> 块是空的,只保留一个 DummyScenario 占位,给出出生触发点(trigger_point 与首路点同位):
<scenarios>
<!-- 无真实场景;仅占位,供评测器定位出生触发 -->
</scenarios>
trigger_point 记录位置和朝向,告诉评测器”从这里开始跟踪”。除此之外没有任何场景触发器。
Bench2Drive 的 44 种交互场景(前车急刹、行人鬼探头、无保护左转等)全部由 ScenarioRunner 框架驱动:在 trigger_point 处检测英雄车进入触发区,然后注入动态事件。而 ScenarioRunner 完全不兼容 CARLA 0.10(官方 issue #1164,无发布计划)——想注入也注入不了。
所以”留空”不是省事,是双重决定:
空场景不等于没难度——路口几何、信号灯、背景交通本身已经足够让模型掉分(SimLingo 在对齐集上 UE4 基线 DS 只有 49.59,路口卡死是主要失分模式)。
leaderboard 系的天气不是全局单值,而是沿路线进度插值:每个 <weather> 块带一个 route_percentage 属性,表示”路线走到百分之几时的天气”,执行器在两个锚点之间对各项参数做线性插值。要表达”全程天气不变”,标准手法就是写两个参数完全相同的锚点:
<weathers>
<weather cloudiness="5.0" fog_density="10.0" precipitation="0.0"
precipitation_deposits="0.0" route_percentage="0"
sun_altitude_angle="45.0" sun_azimuth_angle="-1.0"
wetness="0.0" wind_intensity="10.0"/>
<weather cloudiness="5.0" fog_density="10.0" precipitation="0.0"
precipitation_deposits="0.0" route_percentage="100"
sun_altitude_angle="45.0" sun_azimuth_angle="-1.0"
wetness="0.0" wind_intensity="10.0"/>
</weathers>
起终点锚点参数一样,插值结果自然是全程恒定:无雨、无积水、太阳仰角固定 45°,任何进度处天气都是同一个值。
| 参数 | 含义 |
|---|---|
cloudiness | 云量(本项目 5.0,近无云) |
fog_density / wetness | 雾浓度 / 路面湿度 |
precipitation / precipitation_deposits | 降雨强度 / 路面积水 |
sun_altitude_angle / sun_azimuth_angle | 太阳仰角 / 方位角(固定 45° / −1°) |
wind_intensity | 风力 |
两侧 XML 的 weather 块逐字段一致,天气这个维度被彻底钉死成常量。这一步与路线对齐、全量路点同构:每钉死一个变量,跨侧 DS 差异的可归因范围就收窄一分。天气锁死后,画面里剩下的唯一系统性差异就是渲染引擎本身——这正是本研究要隔离的变量。代价也如实登记:锁白天意味着域随机化受限,结论不外推到夜间/雨天(CP5 limitation)。