1. ScenarioRunner初探:为什么这个工具值得关注
第一次接触ScenarioRunner是在去年的一次自动驾驶仿真测试项目中。当时团队正在寻找一个能够批量执行复杂场景的工具,而ScenarioRunner恰好出现在CARLA官方推荐的生态工具列表中。经过0.9.16版本的深度使用,我发现这确实是一个被低估的利器。
ScenarioRunner本质上是一个场景执行引擎,它允许你定义、管理和自动化执行各种驾驶场景。与直接使用CARLA原生API相比,0.9.16版本最大的改进在于提供了更灵活的场景描述方式和更稳定的执行控制。举个例子,在测试AEB(自动紧急制动)系统时,我们可以用简单的Python脚本定义行人突然出现的场景,然后批量运行上百次来收集统计数据。
注意:虽然ScenarioRunner常与CARLA配合使用,但它实际上是一个独立工具,理论上可以适配任何支持Python接口的仿真环境。
2. 0.9.16版本核心功能解析
2.1 场景描述语言升级
这个版本最让我惊喜的是对OpenSCENARIO 1.0标准的支持。以前需要写几十行Python代码才能描述的场景,现在用XML格式的osc文件几行就能搞定。比如下面这个跟车场景:
<Storyboard> <Init> <Actions> <Private entity="ego_vehicle"> <TeleportAction> <Position x="10" y="0" z="0"/> </TeleportAction> </Private> </Actions> </Init> <Story name="Following" owner="ego_vehicle"> <Act name="MainAct"> <ManeuverGroup> <Maneuver name="FollowLeadVehicle"> <Event priority="overwrite" name="StartFollowing"> <Action> <FollowTrajectoryAction> <Trajectory ref="lead_vehicle_trajectory" offset="5"/> </FollowTrajectoryAction> </Action> </Event> </Maneuver> </ManeuverGroup> </Act> </Story> </Storyboard>2.2 执行控制增强
在实际项目中,我发现0.9.16版本新增的--timeout参数特别实用。以前遇到仿真卡死的情况只能手动终止进程,现在可以设置超时自动终止:
python scenario_runner.py --scenario FollowLeadingVehicle --timeout 60另一个重要改进是--randomize参数,它可以自动对场景中的初始位置、速度等参数进行随机化,这对于生成测试数据集非常有用。
3. 实战:构建自定义场景测试套件
3.1 环境准备
建议使用Python 3.7+和CARLA 0.9.13+的组合。安装依赖时有个小技巧:
pip install -r requirements.txt --extra-index-url https://pypi.org/simple我在Ubuntu 20.04和Windows 10上都成功运行过,但Linux下的性能明显更好。如果遇到pygame报错,试试:
sudo apt-get install python3-dev libsdl2-dev3.2 编写第一个自定义场景
假设我们要测试车辆在十字路口左转时的决策能力。在scenarios文件夹下新建left_turn.py:
import py_trees from scenario_runner.scenario import Scenario class LeftTurnScenario(Scenario): def __init__(self, world, ego_vehicle, config): super().__init__("LeftTurnScenario", ego_vehicle, config) def _create_behavior(self): # 构建行为树 sequence = py_trees.composites.Sequence("LeftTurnSequence") sequence.add_child(WaitForTrafficLight()) # 自定义条件 sequence.add_child(ExecuteLeftTurn()) # 自定义动作 return sequence3.3 批量执行与结果收集
使用--outputDir参数可以自动保存每次运行的日志和截图:
for i in {1..100}; do python scenario_runner.py --scenario LeftTurnScenario --outputDir ./results/run_$i done然后可以用这个Python脚本快速统计成功率:
import glob import json success_count = 0 for log_file in glob.glob("./results/run_*/report.json"): with open(log_file) as f: if json.load(f)["score"]["score"] == 100: success_count += 1 print(f"成功率: {success_count/100:.1%}")4. 性能优化与疑难排解
4.1 提升执行效率的技巧
在长期使用中,我总结了几个关键优化点:
- 同步模式设置:在config.py中调整:
SyncMode = True # 必须为True才能保证场景精确执行 DeltaSeconds = 0.05 # 值越小仿真越精确,但性能开销越大- 资源预加载:在场景开始前添加:
world.tick() # 强制进行一次世界更新 time.sleep(1) # 给物理引擎初始化留出时间- 交通流控制:复杂场景下建议限制NPC数量:
traffic_manager = client.get_trafficmanager() traffic_manager.set_global_distance_to_leading_vehicle(2.5)4.2 常见错误解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 场景卡在初始化阶段 | CARLA服务未就绪 | 在场景脚本开头添加world.wait_for_tick() |
| NPC行为异常 | 交通管理器端口冲突 | 设置traffic_manager.set_random_device_seed(0) |
| 物理引擎抖动 | 时间步长不合理 | 调整DeltaSeconds在0.02-0.1之间 |
| 内存泄漏 | 未正确销毁actor | 在场景结束时调用actor.destroy() |
5. 进阶应用:与ROS/Autoware集成
在实际的自动驾驶开发中,我们通常需要将ScenarioRunner与ROS系统集成。0.9.16版本改进了外部接口,这里分享我的集成方案:
- 首先建立桥接节点:
import rospy from carla_msgs.msg import CarlaScenarioRunnerStatus class ScenarioRunnerBridge: def __init__(self): self.pub = rospy.Publisher('/scenario_runner/status', CarlaScenarioRunnerStatus, queue_size=10) def publish_status(self, scenario_name, status): msg = CarlaScenarioRunnerStatus() msg.scenario = scenario_name msg.status = status self.pub.publish(msg)- 然后在场景脚本的关键节点添加回调:
def _setup_scenario_trigger(self): def on_scenario_start(): self.bridge.publish_status(self.name, "RUNNING") def on_scenario_end(): self.bridge.publish_status(self.name, "COMPLETED") self.scenario_tree.add_handler( py_trees.common.Status.RUNNING, on_scenario_start) self.scenario_tree.add_handler( py_trees.common.Status.SUCCESS, on_scenario_end)- 最后用roslaunch启动整个系统:
<launch> <node pkg="carla_ros_scenario_runner" type="scenario_runner_bridge.py" name="scenario_runner_bridge"/> <node pkg="scenario_runner" type="scenario_runner.py" name="scenario_runner" args="--scenario CutIn --reloadWorld"/> </launch>6. 版本对比与升级建议
从0.9.15升级到0.9.16时,有几个破坏性变更需要注意:
- 坐标系变更:现在统一使用右手坐标系,旧场景可能需要调整位置参数
- API废弃:Scenario类必须实现_create_behavior()而不是_create_actors()
- 新依赖项:新增了对xmlschema包的要求
我建议的升级步骤:
- 备份现有场景和配置
- 创建新的Python虚拟环境
- 安装新版后先用--list参数检查场景兼容性
- 逐步迁移关键场景
对于还在使用旧版的项目,可以通过这个兼容层过渡:
class LegacyScenarioAdapter(Scenario): def __init__(self, legacy_scenario): self.legacy = legacy_scenario def _create_behavior(self): # 将旧式的actor创建逻辑转换为行为树 root = py_trees.composites.Sequence() for actor in self.legacy.actors: root.add_child(SpawnActorAction(actor)) root.add_child(self.legacy._create_behavior()) return root在持续集成环境中,我通常会维护一个这样的测试矩阵:
| 场景类型 | 0.9.15通过率 | 0.9.16通过率 | 差异分析 |
|---|---|---|---|
| 跟车场景 | 98% | 99% | 新版的跟车距离控制更稳定 |
| 变道场景 | 95% | 93% | 新版对转向角速度限制更严格 |
| 行人避让 | 90% | 96% | 行人运动模型改进 |
7. 场景设计模式与最佳实践
经过多个项目的积累,我总结了几种高效的场景设计模式:
7.1 参数化场景模板
class ParameterizedScenario(Scenario): @classmethod def from_config(cls, config_file): with open(config_file) as f: params = json.load(f) return lambda world, ego: cls(world, ego, params) def __init__(self, world, ego_vehicle, parameters): self.params = parameters super().__init__( f"ParamScenario_{parameters['id']}", ego_vehicle, config )7.2 组合式场景
class CompositeScenario(Scenario): def __init__(self, scenarios): self.sub_scenarios = scenarios super().__init__( "CompositeScenario", scenarios[0].ego_vehicle, scenarios[0].config ) def _create_behavior(self): root = py_trees.composites.Parallel( policy=py_trees.common.ParallelPolicy.SUCCESS_ON_ONE) for scenario in self.sub_scenarios: root.add_child(scenario._create_behavior()) return root7.3 动态难度调整
class AdaptiveScenario(Scenario): def __init__(self, base_scenario): self.base = base_scenario self.difficulty = 1.0 super().__init__( f"Adaptive_{base_scenario.name}", base_scenario.ego_vehicle, base_scenario.config ) def update_difficulty(self, performance_metric): # 根据表现动态调整难度系数 if performance_metric > 0.9: self.difficulty *= 1.1 else: self.difficulty *= 0.9 def _create_behavior(self): behavior = self.base._create_behavior() # 修改行为树参数 for node in behavior.iterate(): if hasattr(node, 'speed'): node.speed *= self.difficulty return behavior在实际项目中,这些模式的组合使用可以大幅提升场景开发效率。比如我们可以先定义基础的跟车、变道等原子场景,然后通过参数化和组合快速生成数百种测试用例。