Browse Source
feat: Implement Deterministic Showcase Suite with Asynchronous Pipeline and Auto-MCAP
feat: Implement Deterministic Showcase Suite with Asynchronous Pipeline and Auto-MCAP
This commit represents a major architectural upgrade to the CARLA ADAS simulation project, transitioning from stochastic Traffic Manager behavior to a fully deterministic, high-impact demo environment. 1. Deterministic Scenario Architecture New Showcase Scenario: Implemented scenarios/showcase.py for a manual "Left-Turn Across Path" (LTAP) maneuver. Physics Velocity Injection: Overrode engine/friction variance by directly setting set_target_velocity for NPC actors at 35 km/h. Multi-Stage Waypoint Guidance: Integrated EGO_MID and P1_MID transition points to ensure realistic, cinematic turning radii and reliable collision-avoidance timing. Manual Steering Controller: Developed a proportional steering-to-target helper for all actors. 2. Sensor & Data Pipeline Dual-Camera Support: Integrated a second Third-Person Perspective (TPP) camera. Radar Serialization Fix: Implemented trigonometric conversion of raw spherical coordinates (Depth, Azimuth, Altitude) into Cartesian XYZ + Velocity 16-byte PointClouds for Foxglove Studio. Synchronization: Aligned all sensor data (Dash Cam, TPP Cam, Radar, Lidar) at the orchestrator level. 3. Performance & Asynchronous I/O Asynchronous Recorder: Refactored src/recorder.py to use ThreadPoolExecutor, offloading cv2.imwrite operations to background threads to eliminate simulation stutter. Rapid Iteration: Added --no-record global flag to main.py for sub-second iteration during coordinate tuning. 4. Automation & Seamless UX Auto-MCAP conversion: Integrated data_to_mcap.py as a post-simulation callback in main.py . Auto-Video Stitching: Implement automatic .mp4 generation for both camera views using OpenCV's VideoWriter. Weather/Time CLI: Added --weather flag with presets (Clear, Rain, Sunset, Night). Console Sync: Integrated tqdm for professional progress tracking and refactored scenario logging to use pbar.write() to prevent terminal flickering/staircasing. 5. Dependency Updates Added tqdm as a core dependency for enhanced console visualization.1843_integration
14 changed files with 475 additions and 35 deletions
-
2config.py
-
39data_to_mcap.py
-
79intel/showcase.md
-
1scenarios/__init__.py
-
2scenarios/base.py
-
2scenarios/braking.py
-
2scenarios/cutin.py
-
2scenarios/obstacle.py
-
132scenarios/showcase.py
-
22scripts/get_pos.py
-
29scripts/spawn_at_spectator.py
-
87src/main.py
-
63src/recorder.py
-
28src/sensors.py
@ -0,0 +1,79 @@ |
|||||
|
# CARLA Custom Scenario Design: Deterministic Showcase Suite (Intel) |
||||
|
|
||||
|
This document serves as a "Deep Dive" into the design decisions, technical implementation, and lessons learned during the development of the **Deterministic Showcase Suite**. Use this as a reference for creating future high-impact ADAS demo scenarios. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🏗️ 1. Architecture: Manual vs. Traffic Manager |
||||
|
|
||||
|
**Standard Approach**: Using `ego_vehicle.set_autopilot(True)` and relying on Traffic Manager (TM). |
||||
|
- **Pros**: Easy to set up, handles traffic rules automatically. |
||||
|
- **Cons**: Unpredictable. Random lane changes, braking for minor obstacles, and "softening" of turns makes for poor demo repeatability. |
||||
|
|
||||
|
**Deterministic Approach (Showcase)**: Manual coordinate-based control. |
||||
|
- **Core Strategy**: Define precise spawn points (X, Y, Z) and use a custom steering-to-target loop. |
||||
|
- **Why**: Ensures that the "Left-Turn Across Path" (LTAP) maneuver happens at the *exact same frame* every time, regardless of noise or simulation variance. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🏎️ 2. The "Secret Sauce": Physics Velocity Injection |
||||
|
|
||||
|
One of the biggest challenges in CARLA is consistent timing. Rolling friction, gear shifts, and engine torque curves cause variability in speed. |
||||
|
|
||||
|
**The Fix**: Direct `set_target_velocity` override. |
||||
|
- **Implementation**: Instead of applying `throttle=1.0` and hoping for the best, we calculate the forward vector and force the velocity to exactly **9.72 m/s** (35 km/h). |
||||
|
- **Result**: The NPC reaches the intersection at the *exact same frame* every run, allowing us to tune the EGO's arrival time down to the millisecond. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🗺️ 3. Multi-Stage Waypoint Guidance |
||||
|
|
||||
|
**The Issue**: Standard "Steer toward target" logic often leads to curb-clipping or unrealistic, jagged turns at intersections. CARLA's pathfinding is optimized for driving, not cinematic maneuvers. |
||||
|
|
||||
|
**The Solution**: Intermediate `MID` points. |
||||
|
- **Strategy**: Instead of pointing the NPC directly at the final target, we created a `P1_MID` waypoint. |
||||
|
- **Outcome**: This forces the vehicle to stay in its lane longer and take a wider, "swept" turn, which looks significantly more professional and avoids clipping the junction geometry. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📡 4. Radar Calibration & Serialization |
||||
|
|
||||
|
**The Caveat**: CARLA's Radar sensor exports data in **Spherical Coordinates** (Depth, Azimuth, Altitude, Velocity). Most visualization tools (like Foxglove PointClouds) expect **Cartesian (XYZ)**. |
||||
|
|
||||
|
**The Math**: |
||||
|
- `x = depth * cos(alt) * cos(az)` |
||||
|
- `y = depth * cos(alt) * sin(az)` |
||||
|
- `z = depth * sin(alt)` |
||||
|
- **Note**: The `velocity` field must be included separately and requires a 16-byte point stride (XYZ + V) in the MCAP schema. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⚡ 5. Performance & Caveats |
||||
|
|
||||
|
### Asynchronous I/O |
||||
|
- Always use `ThreadPoolExecutor` for image saving. Saving 720p PNGs synchronously will drop your simulation from 30 FPS to ~5 FPS, ruining the physics time-step. |
||||
|
|
||||
|
### The "Progress Bar Clash" |
||||
|
- When using `tqdm`, standard `print()` statements will "staircase" the bar and ruin the UI. |
||||
|
- **Fix**: Refactor scenarios to accept a `pbar` object and use `pbar.write(msg)` instead of `print(msg)`. |
||||
|
|
||||
|
### CARLA API Gotchas |
||||
|
- **Weather API**: The parameter for sun height is `sun_altitude_angle`, NOT `pitch`. Using the wrong name (or passing an `int` when a `float` is expected) will trigger a `Boost.Python` type error. |
||||
|
- **Sensor Sync**: Always fetch sensor data *after* `world.tick()` to ensure the frames match the metadata. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 6. The Automation Pipeline |
||||
|
|
||||
|
We turned a 2-step manual process into a single-step autonomous pipeline: |
||||
|
1. `main.py` runs the simulation. |
||||
|
2. `recorder.py` captures frames (Async) + generates `.mp4` previews. |
||||
|
3. `main.py` cleanup callback triggers `data_to_mcap.py` automatically. |
||||
|
|
||||
|
**Run Command**: |
||||
|
```powershell |
||||
|
python src/main.py --scenario showcase --frames 250 --weather ClearNoon |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
*Created: March 2026 | Intel Repository — ADAS Development* |
||||
@ -1 +1,2 @@ |
|||||
# scenarios package |
# scenarios package |
||||
|
from .showcase import ShowcaseScenario |
||||
@ -0,0 +1,132 @@ |
|||||
|
""" |
||||
|
scenarios/showcase.py |
||||
|
--------------------- |
||||
|
Custom high-impact demo scene: Left Turn Across Path (LTAP). |
||||
|
Built step-by-step for full control and learning. |
||||
|
|
||||
|
Phase 1: Ego Foundation |
||||
|
""" |
||||
|
|
||||
|
import carla |
||||
|
import math |
||||
|
from scenarios.base import ScenarioBase |
||||
|
|
||||
|
class ShowcaseScenario(ScenarioBase): |
||||
|
@property |
||||
|
def name(self): |
||||
|
return "showcase" |
||||
|
|
||||
|
def setup(self, world, ego_vehicle, traffic_manager) -> None: |
||||
|
""" |
||||
|
Step 11: Deep Intersection Guidance for Ego. |
||||
|
""" |
||||
|
self._world = world |
||||
|
self._ego = ego_vehicle |
||||
|
self._tm = traffic_manager |
||||
|
|
||||
|
# User Coordinates |
||||
|
self.P1 = carla.Transform(carla.Location(x=101.573, y=-8.183, z=0.5), carla.Rotation(yaw=98.4)) |
||||
|
self.P2 = carla.Transform(carla.Location(x=107.412, y=45.309, z=0.5), carla.Rotation(yaw=-87.7)) |
||||
|
self.P3 = carla.Location(x=82.312, y=24.801, z=0.5) |
||||
|
|
||||
|
# NPC Waypoint Guidance |
||||
|
self.P1_MID = carla.Location(x=101.573, y=18.0, z=0.5) |
||||
|
self._npc_reached_mid = False |
||||
|
|
||||
|
# NEW: EGO Mid-point to force a larger turn radius |
||||
|
# Aligned with P2, but 10m further south into intersection |
||||
|
self.EGO_MID = carla.Location(x=107.412, y=32.0, z=0.5) |
||||
|
self._ego_reached_mid = False |
||||
|
|
||||
|
# 1. Setup Ego |
||||
|
self._ego.set_transform(self.P2) |
||||
|
self._ego.set_autopilot(False) |
||||
|
|
||||
|
# 2. Spawn NPC |
||||
|
bp_lib = world.get_blueprint_library() |
||||
|
npc_bp = bp_lib.filter("vehicle.nissan.micra")[0] |
||||
|
self._npc = world.try_spawn_actor(npc_bp, self.P1) |
||||
|
|
||||
|
if self._npc: |
||||
|
self._actors.append(self._npc) |
||||
|
self._npc.set_autopilot(False) |
||||
|
print(f"[SUCCESS] Actors ready. Multi-stage pathing ENABLED.") |
||||
|
|
||||
|
# 3. Visual Debug: Dimmed Boxes (Commented out for final presentation) |
||||
|
# world.debug.draw_box( |
||||
|
# carla.BoundingBox(self.P3 + carla.Location(z=1), carla.Vector3D(0.5, 0.5, 1.0)), |
||||
|
# carla.Rotation(), thickness=0.1, color=carla.Color(178, 0, 0), life_time=20.0 |
||||
|
# ) |
||||
|
# world.debug.draw_box( |
||||
|
# carla.BoundingBox(self.P1_MID + carla.Location(z=1), carla.Vector3D(0.5, 0.5, 1.0)), |
||||
|
# carla.Rotation(), thickness=0.1, color=carla.Color(0, 178, 0), life_time=20.0 |
||||
|
# ) |
||||
|
# world.debug.draw_box( |
||||
|
# carla.BoundingBox(self.EGO_MID + carla.Location(z=1), carla.Vector3D(0.5, 0.5, 1.0)), |
||||
|
# carla.Rotation(), thickness=0.1, color=carla.Color(0, 0, 178), life_time=20.0 |
||||
|
# ) |
||||
|
|
||||
|
def _get_steering_to_target(self, actor, target_loc): |
||||
|
t = actor.get_transform() |
||||
|
v = target_loc - t.location |
||||
|
target_yaw = math.degrees(math.atan2(v.y, v.x)) |
||||
|
delta_yaw = target_yaw - t.rotation.yaw |
||||
|
while delta_yaw > 180: delta_yaw -= 360 |
||||
|
while delta_yaw < -180: delta_yaw += 360 |
||||
|
return max(-1.0, min(1.0, delta_yaw / 60.0)) |
||||
|
|
||||
|
def step(self, frame: int, ego_vehicle, pbar=None) -> None: |
||||
|
""" |
||||
|
Step 11: Multi-stage steering logic. |
||||
|
""" |
||||
|
# --- Ego Control --- |
||||
|
e_loc = ego_vehicle.get_location() |
||||
|
if not self._ego_reached_mid: |
||||
|
e_target = self.EGO_MID |
||||
|
if e_loc.distance(self.EGO_MID) < 2.5: |
||||
|
self._ego_reached_mid = True |
||||
|
msg = "[DEBUG] Ego reached MID point. Swinging into turn." |
||||
|
if pbar: pbar.write(msg) |
||||
|
else: print(msg) |
||||
|
else: |
||||
|
e_target = self.P3 |
||||
|
|
||||
|
e_dist = e_loc.distance(e_target) |
||||
|
ego_steer = self._get_steering_to_target(ego_vehicle, e_target) if e_dist > 1.0 else 0.0 |
||||
|
ego_vehicle.apply_control(carla.VehicleControl(throttle=0.4, steer=ego_steer)) |
||||
|
|
||||
|
# --- NPC Control --- |
||||
|
n_steer, n_dist = 0.0, 0.0 |
||||
|
if hasattr(self, "_npc") and self._npc and self._npc.is_alive: |
||||
|
n_loc = self._npc.get_location() |
||||
|
|
||||
|
if not self._npc_reached_mid: |
||||
|
n_target = self.P1_MID |
||||
|
if n_loc.distance(self.P1_MID) < 2.5: |
||||
|
self._npc_reached_mid = True |
||||
|
msg = "[DEBUG] NPC reached MID point. Switching to P3." |
||||
|
if pbar: pbar.write(msg) |
||||
|
else: print(msg) |
||||
|
else: |
||||
|
n_target = self.P3 |
||||
|
|
||||
|
n_dist = n_loc.distance(n_target) |
||||
|
n_steer = self._get_steering_to_target(self._npc, n_target) if n_dist > 1.0 else 0.0 |
||||
|
|
||||
|
# Physics Injection (35 km/h) |
||||
|
fwd = self._npc.get_transform().get_forward_vector() |
||||
|
self._npc.set_target_velocity(fwd * 9.72) |
||||
|
self._npc.apply_control(carla.VehicleControl(throttle=1.0, steer=n_steer)) |
||||
|
|
||||
|
# Verbose Logging |
||||
|
if frame % 10 == 0: |
||||
|
e_vel = ego_vehicle.get_velocity() |
||||
|
e_speed = 3.6 * math.sqrt(e_vel.x**2 + e_vel.y**2 + e_vel.z**2) |
||||
|
msg = f"[FRAME {frame:03d}] EGO (spd={e_speed:.1f}kph) target={'MID' if not self._ego_reached_mid else 'P3'} | NPC target={'MID' if not self._npc_reached_mid else 'P3'}" |
||||
|
if pbar: |
||||
|
pbar.write(msg) |
||||
|
else: |
||||
|
print(msg) |
||||
|
|
||||
|
def cleanup(self): |
||||
|
self._destroy_actors() |
||||
@ -0,0 +1,22 @@ |
|||||
|
import carla |
||||
|
|
||||
|
def get_pos(): |
||||
|
client = carla.Client("localhost", 2000) |
||||
|
client.set_timeout(10.0) |
||||
|
world = client.get_world() |
||||
|
spectator = world.get_spectator() |
||||
|
t = spectator.get_transform() |
||||
|
loc = t.location |
||||
|
rot = t.rotation |
||||
|
|
||||
|
print("\n" + "="*40) |
||||
|
print("CURRENT SPECTATOR POSITION") |
||||
|
print("="*40) |
||||
|
print(f"Location: x={loc.x:.3f}, y={loc.y:.3f}, z={loc.z:.3f}") |
||||
|
print(f"Rotation: pitch={rot.pitch:.3f}, yaw={rot.yaw:.3f}, roll={rot.roll:.3f}") |
||||
|
print("="*40) |
||||
|
print("\nCopy-paste this into your script:") |
||||
|
print(f"transform = carla.Transform(carla.Location(x={loc.x:.3f}, y={loc.y:.3f}, z={loc.z:.3f}), carla.Rotation(pitch={rot.pitch:.3f}, yaw={rot.yaw:.3f}, roll={rot.roll:.3f}))") |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
get_pos() |
||||
@ -0,0 +1,29 @@ |
|||||
|
import carla |
||||
|
import argparse |
||||
|
|
||||
|
def spawn_here(): |
||||
|
parser = argparse.ArgumentParser() |
||||
|
parser.add_argument("--model", default="vehicle.nissan.micra") |
||||
|
args = parser.parse_args() |
||||
|
|
||||
|
client = carla.Client("localhost", 2000) |
||||
|
client.set_timeout(10.0) |
||||
|
world = client.get_world() |
||||
|
|
||||
|
spectator = world.get_spectator() |
||||
|
t = spectator.get_transform() |
||||
|
|
||||
|
# Lower it slightly so it doesn't drop from the sky, |
||||
|
# but stays above ground |
||||
|
t.location.z -= 1.0 |
||||
|
|
||||
|
bp = world.get_blueprint_library().filter(args.model)[0] |
||||
|
actor = world.try_spawn_actor(bp, t) |
||||
|
|
||||
|
if actor: |
||||
|
print(f"[SUCCESS] Spawned {args.model} at spectator location.") |
||||
|
else: |
||||
|
print("[ERROR] Spawn failed. Collision or invalid location.") |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
spawn_here() |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue