Browse Source
release: Fox v1.1 "Ares" — Stage-Based Pipeline & Radar Parity
release: Fox v1.1 "Ares" — Stage-Based Pipeline & Radar Parity
This release marks a major architectural shift and achieves full feature parity with the high-fidelity Shenron radar testbench. Architecture: - Implemented PipelineManager and Stage-based execution (Sim, Shenron, MCAP, Video). - Decoupled recorder from physics/augmentation logic into src/processing/physics.py. - Migrated all path resolution to Pathlib for cross-platform robustness. Radar & Metrology: - Integrated FastHeatmapEngine for stateful, high-performance RD/RA/CFAR rendering. - Implemented dual-plot Range-Azimuth (Static Absolute vs Dynamic Peak). - Flattened telemetry schema for direct Foxglove Plot panel compatibility. - Added hardware-accurate 3D FOV frustum projections (LINE_LIST). Dashboard & UX: - Bumped version to v1.1. - Fixed stop.flag detection bug in simulation shutdown. - Optimized GPU headroom by maintaining idle/sync mode during synthesis. Signed-off-by: Antigravity AI <cortex@deepmind.google>main
19 changed files with 1566 additions and 491 deletions
-
84123.md
-
2dashboard/templates/index.html
-
43intel/internal/1.1/V1.1_walkthrough.md
-
297intel/internal/1.1/changelog.html
-
284scripts/data_to_mcap.py
-
10scripts/generate_shenron.py
-
317src/main.py
-
1src/pipeline/__init__.py
-
87src/pipeline/base.py
-
100src/pipeline/manager.py
-
1src/pipeline/stages/__init__.py
-
55src/pipeline/stages/mcap_stage.py
-
57src/pipeline/stages/shenron_stage.py
-
268src/pipeline/stages/sim_stage.py
-
72src/pipeline/stages/video_stage.py
-
1src/processing/__init__.py
-
200src/processing/physics.py
-
170src/recorder.py
-
4tmp/test_import.py
@ -0,0 +1,84 @@ |
|||||
|
# Implementation Plan: Stage-Based Pipeline Refactor |
||||
|
|
||||
|
Refactor the Fox CARLA ADAS simulation pipeline to separate concerns across environment management, data processing, and serialization. This moves the project from a monolithic `main.py` to a modular, stage-based architecture. |
||||
|
|
||||
|
## User Review Required |
||||
|
|
||||
|
> [!IMPORTANT] |
||||
|
> **Dashboard Telemetry Persistence**: The Dashboard backend (`app.py`) parses specific `stdout` patterns like `[SHENRON_STEP]` and `[AUTO-MCAP]`. We must ensure the new `PipelineManager` or the individual stages continue to emit these strings to avoid breaking the UI progress bars. |
||||
|
|
||||
|
> [!WARNING] |
||||
|
> **Resource Management**: During the transition from `SimulationStage` to `ShenronStage`, we must explicitly ensure CARLA's synchronous mode is either released or the process is handled such that GPU memory is freed for the heavy Shenron Torch operations. |
||||
|
|
||||
|
## Proposed Changes |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### [NEW] Pipeline Core (`src/pipeline/`) |
||||
|
|
||||
|
Establish the foundational classes for stage-based execution. |
||||
|
|
||||
|
#### [NEW] [base.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/pipeline/base.py) |
||||
|
- Define `PipelineContext`: Dataclass to carry `session_path`, `scenario_config`, and `args`. |
||||
|
- Define `PipelineStage`: Abstract Base Class with `run(context)` and `cleanup()` methods. |
||||
|
|
||||
|
#### [NEW] [manager.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/pipeline/manager.py) |
||||
|
- Implement `PipelineManager`: Orchestrates the execution of a list of `PipelineStage` objects. |
||||
|
- Handles error propagation and graceful halts. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### [NEW] Processing Layer (`src/processing/`) |
||||
|
|
||||
|
Extract physics and data-augmentation logic to make it reusable and testable. |
||||
|
|
||||
|
#### [NEW] [physics.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/processing/physics.py) |
||||
|
- **`calculate_radial_velocity`**: Extracted from `recorder.py`. Calculates line-of-sight velocity for LiDAR points. |
||||
|
- **`compute_adas_metrics`**: Extracted from `recorder.py`. Calculates range, azimuth, and closing velocity for NPCs. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### [MODIFY] Simulation & Recording |
||||
|
|
||||
|
Clean up existing components to focus on their primary responsibilities. |
||||
|
|
||||
|
#### [MODIFY] [recorder.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/recorder.py) |
||||
|
- Remove physics calculation logic. |
||||
|
- Update `save()` to accept pre-calculated metrics. |
||||
|
- Move `_generate_videos` to a utility function. |
||||
|
|
||||
|
#### [MODIFY] [main.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/main.py) |
||||
|
- Remove monolithic simulation loop. |
||||
|
- Transform into a thin wrapper that initializes `PipelineManager` with `SimStage`, `ShenronStage`, and `McapStage`. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### [NEW] Pipeline Stages (`src/pipeline/stages/`) |
||||
|
|
||||
|
Encapsulate logic into discrete execution units. |
||||
|
|
||||
|
#### [NEW] [sim_stage.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/pipeline/stages/sim_stage.py) |
||||
|
- Port the CARLA simulation loop from `main.py`. |
||||
|
- Manage `SensorManager` and `Recorder` lifecycles. |
||||
|
|
||||
|
#### [NEW] [shenron_stage.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/pipeline/stages/shenron_stage.py) |
||||
|
- Wrap `scripts/generate_shenron.py` logic. |
||||
|
- Ensure `[SHENRON_STEP]` telemetry is emitted. |
||||
|
|
||||
|
#### [NEW] [mcap_stage.py](file:///d:/CARLA/CARLA_0.9.16/PythonAPI/Fox/src/pipeline/stages/mcap_stage.py) |
||||
|
- Wrap `scripts/data_to_mcap.py` logic. |
||||
|
- Register Foxglove schemas and handle final serialization. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Verification Plan |
||||
|
|
||||
|
### Automated Tests |
||||
|
- **Physics Parity Test**: Create a script in `scratch/` that runs the old `recorder.py` logic and the new `physics.py` logic on the same frame to ensure bit-identical results for radial velocity and ADAS metrics. |
||||
|
- **Pipeline Dry-Run**: Run the pipeline with `--skip-sim` on an existing data folder to verify that `ShenronStage` and `McapStage` correctly identify and process the files. |
||||
|
|
||||
|
### Manual Verification |
||||
|
- **Dashboard Validation**: Launch the Dashboard via `dashboard.bat`, trigger a simulation, and verify that: |
||||
|
1. The console log shows the new stage transitions. |
||||
|
2. The progress bars for Shenron and MCAP update correctly. |
||||
|
- **Foxglove Check**: Open the generated `.mcap` in Foxglove and verify that the LiDAR (with radial velocity) and Radar pointclouds are visualized correctly. |
||||
@ -0,0 +1,43 @@ |
|||||
|
# Fox v1.1 Release Walkthrough |
||||
|
|
||||
|
This release marks a major architectural shift from a monolithic script to a modular, stage-based pipeline, while simultaneously achieving full feature parity with the high-fidelity `test_shenron.py` radar testbench. |
||||
|
|
||||
|
## 🚀 Key Achievements |
||||
|
|
||||
|
### 1. Unified Radar Physics & Visualization |
||||
|
- **Single Source of Truth**: All radar plotting and post-processing logic is now centralized in `scripts/ISOLATE/sim_radar_utils/plots.py`. |
||||
|
- **FastHeatmapEngine**: Migrated the production pipeline to the stateful rendering engine, resulting in consistent dB scaling and improved performance. |
||||
|
- **Dual RA Stream**: Range-Azimuth (RA) visualization now produces both a **Static** (absolute magnitude) and **Dynamic** (peak-tracking) stream for Foxglove. |
||||
|
- **3D FOV Frustums**: Added hardware-accurate 3D frustum wireframes for all radar types (AWRL1432 and RadarBook). |
||||
|
|
||||
|
### 2. Modular Stage-Based Pipeline |
||||
|
- **PipelineManager**: Orchestrates the execution of discrete stages: `Simulation` → `Shenron` → `MCAP` → `Video`. |
||||
|
- **Feature Parity**: Ported advanced telemetry flattening and metrology math from the testbench to the production exporter. |
||||
|
- **Selective Execution**: New CLI flags allow running only specific stages (e.g., `--only-mcap` to re-process existing data). |
||||
|
|
||||
|
### 3. Robustness & Portability |
||||
|
- **Pathlib Integration**: Replaced brittle `os.path` joins with robust `Pathlib` logic in all pipeline stages and the orchestrator. |
||||
|
- **Stop Flag Detection**: Fixed a path bug in the stop flag detection to ensure graceful simulation shutdown works reliably across different working directories. |
||||
|
- **Hardware Persistence**: Added `radar_specs.json` generation to save hardware constants (carrier frequency, velocity limits) during synthesis for downstream visualization. |
||||
|
|
||||
|
## 🛠️ Verification Results |
||||
|
|
||||
|
| Component | Status | Verification Detail | |
||||
|
|---|---|---| |
||||
|
| **Dashboard** | ✅ | Version bumped to v1.1; scenario list and status polling verified. | |
||||
|
| **SimStage** | ✅ | Verified ego spawn, weather application, and `stop.flag` path resolution. | |
||||
|
| **ShenronStage** | ✅ | Verified `sys.path` injection and telemetry event emission. | |
||||
|
| **McapStage** | ✅ | Verified `FastHeatmapEngine` initialization and `ISOLATE` path handling. | |
||||
|
| **Telemetry** | ✅ | Verified flattened schema for Foxglove Plot panel compatibility. | |
||||
|
|
||||
|
## 📦 File Audit Summary |
||||
|
|
||||
|
- `src/main.py`: Refactored to thin CLI wrapper for `PipelineManager`. |
||||
|
- `src/pipeline/*`: New core architecture for stage management. |
||||
|
- `src/recorder.py`: Decoupled from physics logic; focused on raw capture. |
||||
|
- `src/processing/physics.py`: Reusable physics layer for radial velocity and ADAS metrics. |
||||
|
- `scripts/data_to_mcap.py`: Upgraded to v1.1 with testbench parity and 3D diagnostics. |
||||
|
- `dashboard/templates/index.html`: UI updated to reflect v1.1. |
||||
|
|
||||
|
--- |
||||
|
*Generated by Antigravity AI | Fox v1.1 "Ares" Release* |
||||
@ -0,0 +1,297 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Fox v1.1 | Changelog</title> |
||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono&display=swap" rel="stylesheet"> |
||||
|
<style> |
||||
|
:root { |
||||
|
--bg: #0d1117; |
||||
|
--surface: #161b22; |
||||
|
--border: rgba(255, 255, 255, 0.1); |
||||
|
--text: #c9d1d9; |
||||
|
--text-muted: #8b949e; |
||||
|
--accent-sim: #3b82f6; |
||||
|
--accent-shenron: #10b981; |
||||
|
--accent-radar: #f59e0b; |
||||
|
--accent-mcap: #8b5cf6; |
||||
|
--glow-sim: rgba(59, 130, 246, 0.5); |
||||
|
--glow-shenron: rgba(16, 185, 129, 0.5); |
||||
|
} |
||||
|
|
||||
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
||||
|
|
||||
|
body { |
||||
|
background-color: var(--bg); |
||||
|
color: var(--text); |
||||
|
font-family: 'Outfit', sans-serif; |
||||
|
line-height: 1.6; |
||||
|
padding-bottom: 5rem; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
max-width: 900px; |
||||
|
margin: 0 auto; |
||||
|
padding: 2rem; |
||||
|
} |
||||
|
|
||||
|
header { |
||||
|
padding: 4rem 0; |
||||
|
text-align: center; |
||||
|
border-bottom: 1px solid var(--border); |
||||
|
margin-bottom: 3rem; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.version-badge { |
||||
|
display: inline-block; |
||||
|
background: linear-gradient(135deg, var(--accent-sim), var(--accent-mcap)); |
||||
|
color: white; |
||||
|
padding: 0.5rem 1.5rem; |
||||
|
border-radius: 100px; |
||||
|
font-weight: 800; |
||||
|
font-size: 0.9rem; |
||||
|
letter-spacing: 2px; |
||||
|
margin-bottom: 1rem; |
||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3); |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 4rem; |
||||
|
font-weight: 800; |
||||
|
letter-spacing: -2px; |
||||
|
background: linear-gradient(to right, #fff, #8b949e); |
||||
|
-webkit-background-clip: text; |
||||
|
-webkit-text-fill-color: transparent; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
.tagline { |
||||
|
font-size: 1.2rem; |
||||
|
color: var(--text-muted); |
||||
|
font-weight: 300; |
||||
|
} |
||||
|
|
||||
|
section { margin-bottom: 4rem; } |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.5rem; |
||||
|
text-transform: uppercase; |
||||
|
letter-spacing: 3px; |
||||
|
margin-bottom: 2rem; |
||||
|
color: #fff; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 1rem; |
||||
|
} |
||||
|
|
||||
|
h2::after { |
||||
|
content: ''; |
||||
|
flex: 1; |
||||
|
height: 1px; |
||||
|
background: var(--border); |
||||
|
} |
||||
|
|
||||
|
.card-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); |
||||
|
gap: 1.5rem; |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
background: var(--surface); |
||||
|
border: 1px solid var(--border); |
||||
|
border-radius: 16px; |
||||
|
padding: 2rem; |
||||
|
transition: transform 0.3s ease, border-color 0.3s ease; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.card:hover { |
||||
|
transform: translateY(-5px); |
||||
|
border-color: rgba(255,255,255,0.2); |
||||
|
} |
||||
|
|
||||
|
.card::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
top: 0; left: 0; width: 4px; height: 100%; |
||||
|
background: var(--accent); |
||||
|
} |
||||
|
|
||||
|
.card.architecture { --accent: var(--accent-sim); } |
||||
|
.card.radar { --accent: var(--accent-radar); } |
||||
|
.card.ux { --accent: var(--accent-shenron); } |
||||
|
|
||||
|
.card-icon { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
background: rgba(255,255,255,0.05); |
||||
|
border-radius: 12px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-bottom: 1.5rem; |
||||
|
color: var(--accent); |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 1.25rem; |
||||
|
margin-bottom: 1rem; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
ul { |
||||
|
list-style: none; |
||||
|
color: var(--text-muted); |
||||
|
font-size: 0.95rem; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
margin-bottom: 0.8rem; |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
gap: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
li::before { |
||||
|
content: '→'; |
||||
|
color: var(--accent); |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.code-block { |
||||
|
background: #010409; |
||||
|
padding: 1rem; |
||||
|
border-radius: 8px; |
||||
|
font-family: 'JetBrains Mono', monospace; |
||||
|
font-size: 0.85rem; |
||||
|
margin-top: 1rem; |
||||
|
border: 1px solid var(--border); |
||||
|
color: #79c0ff; |
||||
|
} |
||||
|
|
||||
|
.footer { |
||||
|
text-align: center; |
||||
|
padding-top: 4rem; |
||||
|
border-top: 1px solid var(--border); |
||||
|
color: var(--text-muted); |
||||
|
font-size: 0.85rem; |
||||
|
} |
||||
|
|
||||
|
.badge { |
||||
|
font-size: 0.7rem; |
||||
|
padding: 0.2rem 0.5rem; |
||||
|
border-radius: 4px; |
||||
|
text-transform: uppercase; |
||||
|
font-weight: 700; |
||||
|
margin-left: 0.5rem; |
||||
|
} |
||||
|
.badge.new { background: #238636; color: #fff; } |
||||
|
.badge.fix { background: #8957e5; color: #fff; } |
||||
|
|
||||
|
@keyframes fadeIn { |
||||
|
from { opacity: 0; transform: translateY(20px); } |
||||
|
to { opacity: 1; transform: translateY(0); } |
||||
|
} |
||||
|
|
||||
|
.pop-in { animation: fadeIn 0.6s ease out forwards; } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
|
||||
|
<div class="container"> |
||||
|
<header class="pop-in"> |
||||
|
<div class="version-badge">VERSION 1.1 "ARES"</div> |
||||
|
<h1>Fox Simulation</h1> |
||||
|
<p class="tagline">The "Ares" Update: Stage-Based Orchestration & Radar Fidelity</p> |
||||
|
</header> |
||||
|
|
||||
|
<section> |
||||
|
<h2>Core Architecture</h2> |
||||
|
<div class="card-grid"> |
||||
|
<div class="card architecture pop-in"> |
||||
|
<div class="card-icon"> |
||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> |
||||
|
</div> |
||||
|
<h3>Stage-Based Pipeline <span class="badge new">New</span></h3> |
||||
|
<ul> |
||||
|
<li>Decoupled monolithic script into discrete, manageable stages.</li> |
||||
|
<li>Introduced <code>PipelineManager</code> for sequential execution control.</li> |
||||
|
<li>Support for selective stage execution (e.g., <code>--only-mcap</code>).</li> |
||||
|
</ul> |
||||
|
<div class="code-block"> |
||||
|
Sim → Shenron → MCAP → Video |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card architecture pop-in" style="animation-delay: 0.1s;"> |
||||
|
<div class="card-icon"> |
||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> |
||||
|
</div> |
||||
|
<h3>Robust Path Resolution <span class="badge fix">Fix</span></h3> |
||||
|
<ul> |
||||
|
<li>Migrated all internal pathing from <code>os.path</code> to <code>Pathlib</code>.</li> |
||||
|
<li>Guaranteed resolution of <code>stop.flag</code> across all directories.</li> |
||||
|
<li>Standardized project root discovery (parents[3] resolution).</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<h2>Radar & Metrology</h2> |
||||
|
<div class="card-grid"> |
||||
|
<div class="card radar pop-in" style="animation-delay: 0.2s;"> |
||||
|
<div class="card-icon"> |
||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg> |
||||
|
</div> |
||||
|
<h3>Testbench Parity <span class="badge new">Update</span></h3> |
||||
|
<ul> |
||||
|
<li>Integrated <code>FastHeatmapEngine</code> for dB-consistent rendering.</li> |
||||
|
<li>Implemented 68.0 dB system gain offset for RD/CFAR math.</li> |
||||
|
<li>Dual RA Plots: Fixed Absolute Bounds vs. Dynamic Peak Tracking.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card radar pop-in" style="animation-delay: 0.3s;"> |
||||
|
<div class="card-icon"> |
||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg> |
||||
|
</div> |
||||
|
<h3>3D Diagnostics <span class="badge new">New</span></h3> |
||||
|
<ul> |
||||
|
<li>Hardware FOV Frustum wireframes in Foxglove 3D view.</li> |
||||
|
<li>RHS conversion for seamless coordinate alignment.</li> |
||||
|
<li>Automated <code>radar_specs.json</code> persistence for downstream scaling.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<h2>Dashboard & UX</h2> |
||||
|
<div class="card-grid"> |
||||
|
<div class="card ux pop-in" style="animation-delay: 0.4s;"> |
||||
|
<div class="card-icon"> |
||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg> |
||||
|
</div> |
||||
|
<h3>Orchestrator v1.1 <span class="badge new">Update</span></h3> |
||||
|
<ul> |
||||
|
<li>Updated Dashboard UI with v1.1 branding.</li> |
||||
|
<li>Refined Telemetry schema (flattened) for real-time graphing.</li> |
||||
|
<li>Improved simulation status polling and graceful exit logic.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<div class="footer"> |
||||
|
<p>Released April 23, 2026 | Built by Antigravity AI for BATL CARLA</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1 @@ |
|||||
|
# src/pipeline — Stage-based pipeline orchestration for the Fox ADAS framework. |
||||
@ -0,0 +1,87 @@ |
|||||
|
""" |
||||
|
src/pipeline/base.py |
||||
|
--------------------- |
||||
|
Core abstractions for the Fox stage-based pipeline. |
||||
|
|
||||
|
Defines: |
||||
|
- PipelineContext: Shared state passed between pipeline stages. |
||||
|
- PipelineStage: Abstract base class that all stages must implement. |
||||
|
""" |
||||
|
|
||||
|
import argparse |
||||
|
from abc import ABC, abstractmethod |
||||
|
from dataclasses import dataclass, field |
||||
|
from pathlib import Path |
||||
|
from typing import Optional |
||||
|
|
||||
|
|
||||
|
@dataclass |
||||
|
class PipelineContext: |
||||
|
""" |
||||
|
Shared state container carried through the pipeline. |
||||
|
|
||||
|
Populated by the CLI parser and enriched by each stage as it runs. |
||||
|
For example, SimulationStage writes `session_path` after recording, |
||||
|
and downstream stages (Shenron, MCAP) read it. |
||||
|
""" |
||||
|
|
||||
|
# --- Set by CLI / entry point --- |
||||
|
scenario_name: str = "braking" |
||||
|
args: Optional[argparse.Namespace] = None |
||||
|
|
||||
|
# --- Enriched by SimulationStage --- |
||||
|
session_path: Optional[Path] = None # e.g. data/braking_20260423_093000 |
||||
|
frame_count: int = 0 # Total frames recorded |
||||
|
|
||||
|
# --- Pipeline control --- |
||||
|
success: bool = True # False halts remaining stages |
||||
|
skip_stages: list = field(default_factory=list) # Stage names to skip |
||||
|
|
||||
|
def should_skip(self, stage_name: str) -> bool: |
||||
|
"""Check if a stage should be skipped based on CLI flags.""" |
||||
|
return stage_name in self.skip_stages |
||||
|
|
||||
|
|
||||
|
class PipelineStage(ABC): |
||||
|
""" |
||||
|
Abstract base class for a single pipeline stage. |
||||
|
|
||||
|
Each stage encapsulates one discrete unit of work: |
||||
|
- SimulationStage → CARLA capture loop |
||||
|
- ShenronStage → Physics-based radar synthesis |
||||
|
- McapStage → Foxglove MCAP serialization |
||||
|
|
||||
|
Lifecycle |
||||
|
--------- |
||||
|
1. manager calls stage.run(context) |
||||
|
2. If run() returns False, the pipeline halts. |
||||
|
3. manager calls stage.cleanup() regardless of success/failure. |
||||
|
""" |
||||
|
|
||||
|
@property |
||||
|
@abstractmethod |
||||
|
def name(self) -> str: |
||||
|
"""Human-readable stage identifier (e.g. 'simulation').""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
def run(self, ctx: PipelineContext) -> bool: |
||||
|
""" |
||||
|
Execute the stage logic. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
ctx : PipelineContext |
||||
|
Shared state — read inputs, write outputs. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
bool |
||||
|
True if the stage succeeded. False halts the pipeline. |
||||
|
""" |
||||
|
|
||||
|
def cleanup(self): |
||||
|
""" |
||||
|
Optional resource teardown (GPU memory, CARLA connections, etc.). |
||||
|
Called by the PipelineManager in a finally block. |
||||
|
""" |
||||
|
pass |
||||
@ -0,0 +1,100 @@ |
|||||
|
""" |
||||
|
src/pipeline/manager.py |
||||
|
------------------------ |
||||
|
PipelineManager — Sequential executor for pipeline stages. |
||||
|
|
||||
|
Usage |
||||
|
----- |
||||
|
from pipeline.manager import PipelineManager |
||||
|
from pipeline.stages.sim_stage import SimulationStage |
||||
|
from pipeline.stages.shenron_stage import ShenronStage |
||||
|
from pipeline.stages.mcap_stage import McapStage |
||||
|
|
||||
|
manager = PipelineManager([ |
||||
|
SimulationStage(), |
||||
|
ShenronStage(), |
||||
|
McapStage(), |
||||
|
]) |
||||
|
manager.execute(context) |
||||
|
""" |
||||
|
|
||||
|
from pipeline.base import PipelineContext, PipelineStage |
||||
|
|
||||
|
|
||||
|
class PipelineManager: |
||||
|
""" |
||||
|
Orchestrates the sequential execution of pipeline stages. |
||||
|
|
||||
|
Responsibilities |
||||
|
---------------- |
||||
|
- Run stages in order, passing a shared PipelineContext. |
||||
|
- Skip stages listed in ctx.skip_stages. |
||||
|
- Halt the pipeline if any stage returns False. |
||||
|
- Ensure cleanup() is called for every stage that was started. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, stages: list[PipelineStage]): |
||||
|
self._stages = stages |
||||
|
|
||||
|
def execute(self, ctx: PipelineContext) -> bool: |
||||
|
""" |
||||
|
Run all stages sequentially. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
ctx : PipelineContext |
||||
|
Shared state for the entire pipeline run. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
bool |
||||
|
True if all stages completed successfully. |
||||
|
""" |
||||
|
started_stages = [] |
||||
|
|
||||
|
try: |
||||
|
for stage in self._stages: |
||||
|
# Check skip list |
||||
|
if ctx.should_skip(stage.name): |
||||
|
print(f"[PIPELINE] Skipping stage: '{stage.name}'") |
||||
|
continue |
||||
|
|
||||
|
# Check if a previous stage failed |
||||
|
if not ctx.success: |
||||
|
print(f"[PIPELINE] Halting — previous stage failed. " |
||||
|
f"Skipping '{stage.name}'.") |
||||
|
break |
||||
|
|
||||
|
print(f"\n{'='*60}") |
||||
|
print(f"[PIPELINE] Starting stage: '{stage.name}'") |
||||
|
print(f"{'='*60}") |
||||
|
|
||||
|
started_stages.append(stage) |
||||
|
|
||||
|
try: |
||||
|
result = stage.run(ctx) |
||||
|
if not result: |
||||
|
ctx.success = False |
||||
|
print(f"[PIPELINE] Stage '{stage.name}' returned " |
||||
|
f"failure. Pipeline will halt.") |
||||
|
except Exception as e: |
||||
|
ctx.success = False |
||||
|
print(f"[PIPELINE] Stage '{stage.name}' raised an " |
||||
|
f"exception: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
|
||||
|
finally: |
||||
|
# Cleanup in reverse order (most recent stage first) |
||||
|
for stage in reversed(started_stages): |
||||
|
try: |
||||
|
stage.cleanup() |
||||
|
except Exception as e: |
||||
|
print(f"[PIPELINE] Cleanup error in '{stage.name}': {e}") |
||||
|
|
||||
|
if ctx.success: |
||||
|
print(f"\n[PIPELINE] All stages completed successfully.") |
||||
|
else: |
||||
|
print(f"\n[PIPELINE] Pipeline finished with errors.") |
||||
|
|
||||
|
return ctx.success |
||||
@ -0,0 +1 @@ |
|||||
|
# src/pipeline/stages — Individual pipeline stage implementations. |
||||
@ -0,0 +1,55 @@ |
|||||
|
""" |
||||
|
src/pipeline/stages/mcap_stage.py |
||||
|
----------------------------------- |
||||
|
McapStage — Foxglove MCAP serialization. |
||||
|
|
||||
|
Wraps the logic from scripts/data_to_mcap.py into a PipelineStage. |
||||
|
Reads the session folder (camera PNGs, radar/lidar NPYs, Shenron outputs) |
||||
|
and produces a unified .mcap file for Foxglove visualization. |
||||
|
|
||||
|
Dashboard Compatibility |
||||
|
----------------------- |
||||
|
This stage preserves the [AUTO-MCAP] stdout pattern that the Dashboard |
||||
|
SSE parser relies on for status updates. |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
from pathlib import Path |
||||
|
|
||||
|
# Add project root to sys.path |
||||
|
sys.path.append(str(Path(__file__).resolve().parents[3])) |
||||
|
|
||||
|
from pipeline.base import PipelineStage, PipelineContext |
||||
|
|
||||
|
|
||||
|
class McapStage(PipelineStage): |
||||
|
""" |
||||
|
Pipeline stage that converts a recorded session into a Foxglove |
||||
|
MCAP file. |
||||
|
""" |
||||
|
|
||||
|
@property |
||||
|
def name(self) -> str: |
||||
|
return "mcap" |
||||
|
|
||||
|
def run(self, ctx: PipelineContext) -> bool: |
||||
|
if ctx.session_path is None or not ctx.session_path.exists(): |
||||
|
print("[MCAP] No session path found. Skipping.") |
||||
|
return True # Not a failure — just nothing to process |
||||
|
|
||||
|
# Import the existing conversion function |
||||
|
from scripts.data_to_mcap import convert_folder |
||||
|
|
||||
|
print(f"[AUTO-MCAP] Triggering seamless conversion for: " |
||||
|
f"{ctx.session_path}") |
||||
|
|
||||
|
try: |
||||
|
convert_folder(str(ctx.session_path)) |
||||
|
except Exception as e: |
||||
|
print(f"[MCAP] Error during MCAP conversion: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
return False # MCAP failure is more serious — flag it |
||||
|
|
||||
|
return True |
||||
@ -0,0 +1,57 @@ |
|||||
|
""" |
||||
|
src/pipeline/stages/shenron_stage.py |
||||
|
-------------------------------------- |
||||
|
ShenronStage — Physics-based radar synthesis from LiDAR data. |
||||
|
|
||||
|
Wraps the logic from scripts/generate_shenron.py into a PipelineStage. |
||||
|
Reads augmented LiDAR .npy files from ctx.session_path and produces |
||||
|
Shenron radar pointclouds + metrology heatmaps. |
||||
|
|
||||
|
Dashboard Compatibility |
||||
|
----------------------- |
||||
|
This stage preserves the [SHENRON_INIT] and [SHENRON_STEP] stdout |
||||
|
patterns that the Dashboard SSE parser relies on for progress bars. |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
from pathlib import Path |
||||
|
|
||||
|
# Add project root to sys.path |
||||
|
sys.path.append(str(Path(__file__).resolve().parents[3])) |
||||
|
|
||||
|
from pipeline.base import PipelineStage, PipelineContext |
||||
|
from pathlib import Path |
||||
|
|
||||
|
|
||||
|
class ShenronStage(PipelineStage): |
||||
|
""" |
||||
|
Pipeline stage that runs the Shenron physics-based radar engine |
||||
|
on a recorded simulation session. |
||||
|
""" |
||||
|
|
||||
|
@property |
||||
|
def name(self) -> str: |
||||
|
return "shenron" |
||||
|
|
||||
|
def run(self, ctx: PipelineContext) -> bool: |
||||
|
if ctx.session_path is None or not ctx.session_path.exists(): |
||||
|
print("[SHENRON] No session path found. Skipping.") |
||||
|
return True # Not a failure — just nothing to process |
||||
|
|
||||
|
# Import the existing processing function |
||||
|
from scripts.generate_shenron import process_session |
||||
|
|
||||
|
print(f"[AUTO-SHENRON] Synthesizing physics-based radar for: " |
||||
|
f"{ctx.session_path}") |
||||
|
|
||||
|
try: |
||||
|
process_session(ctx.session_path) |
||||
|
except Exception as e: |
||||
|
print(f"[SHENRON] Error during radar synthesis: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
# Non-fatal: downstream MCAP can still process camera/lidar |
||||
|
return True |
||||
|
|
||||
|
return True |
||||
@ -0,0 +1,268 @@ |
|||||
|
""" |
||||
|
src/pipeline/stages/sim_stage.py |
||||
|
--------------------------------- |
||||
|
SimulationStage — CARLA environment management and sensor capture loop. |
||||
|
|
||||
|
Extracted from the monolithic main.py. This stage handles: |
||||
|
- CARLA client connection and sync mode setup. |
||||
|
- Ego vehicle spawning and weather application. |
||||
|
- The main world.tick() loop with sensor capture and recording. |
||||
|
- Graceful shutdown via stop.flag detection. |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
import math |
||||
|
from pathlib import Path |
||||
|
|
||||
|
# Add project root to sys.path (src/pipeline/stages/sim_stage.py -> 3 levels to src, 4 to root) |
||||
|
ROOT_DIR = Path(__file__).resolve().parents[3] |
||||
|
sys.path.append(str(ROOT_DIR)) |
||||
|
|
||||
|
import config |
||||
|
from pipeline.base import PipelineStage, PipelineContext |
||||
|
from scenario_loader import load_scenario |
||||
|
from pathlib import Path |
||||
|
|
||||
|
|
||||
|
class SimulationStage(PipelineStage): |
||||
|
""" |
||||
|
Pipeline stage that runs the live CARLA simulation capture loop. |
||||
|
|
||||
|
Writes raw sensor data (camera PNGs, radar/lidar NPYs, frames.jsonl) |
||||
|
to disk and populates ctx.session_path for downstream stages. |
||||
|
""" |
||||
|
|
||||
|
@property |
||||
|
def name(self) -> str: |
||||
|
return "simulation" |
||||
|
|
||||
|
def run(self, ctx: PipelineContext) -> bool: |
||||
|
# Late imports — CARLA is only needed for this stage |
||||
|
import carla |
||||
|
from sensors import SensorManager |
||||
|
from recorder import Recorder |
||||
|
|
||||
|
args = ctx.args |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 1. Load & Parameterize scenario |
||||
|
# ------------------------------------------------------------------ |
||||
|
scenario = load_scenario(args.scenario) |
||||
|
|
||||
|
if args.params: |
||||
|
try: |
||||
|
p_str = args.params.strip().strip('"').strip("'") |
||||
|
p_dict = {} |
||||
|
for item in p_str.split(","): |
||||
|
if "=" in item: |
||||
|
k, v = item.split("=", 1) |
||||
|
p_dict[k.strip().strip('"').strip("'")] = ( |
||||
|
v.strip().strip('"').strip("'") |
||||
|
) |
||||
|
scenario.apply_parameters(p_dict) |
||||
|
except Exception as e: |
||||
|
print(f"[ERROR] Failed to parse --params '{args.params}': {e}") |
||||
|
|
||||
|
# Determine simulation duration (CLI > Scenario > Config) |
||||
|
max_frames = args.frames |
||||
|
if max_frames is None: |
||||
|
max_frames = scenario.max_frames |
||||
|
if max_frames is None: |
||||
|
max_frames = config.MAX_FRAMES |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 2. Connect to CARLA |
||||
|
# ------------------------------------------------------------------ |
||||
|
client = carla.Client("localhost", 2000) |
||||
|
client.set_timeout(10.0) |
||||
|
|
||||
|
world = client.get_world() |
||||
|
|
||||
|
# Apply Weather (CLI > Scenario > Config) |
||||
|
from utils import get_weather_preset |
||||
|
|
||||
|
weather_name = args.weather |
||||
|
if weather_name is None: |
||||
|
weather_name = scenario.weather |
||||
|
if weather_name is None: |
||||
|
weather_name = config.DEFAULT_WEATHER |
||||
|
|
||||
|
world.set_weather(get_weather_preset(weather_name)) |
||||
|
print(f"[INFO] Applied weather: {weather_name}") |
||||
|
|
||||
|
blueprint_library = world.get_blueprint_library() |
||||
|
map_ = world.get_map() |
||||
|
|
||||
|
# Clean slate: remove all existing vehicles and walkers |
||||
|
for actor in world.get_actors().filter("vehicle.*"): |
||||
|
actor.destroy() |
||||
|
for actor in world.get_actors().filter("walker.*"): |
||||
|
actor.destroy() |
||||
|
print("[INFO] Cleared existing actors") |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 3. Traffic Manager + Sync mode |
||||
|
# ------------------------------------------------------------------ |
||||
|
traffic_manager = client.get_trafficmanager(8000) |
||||
|
traffic_manager.set_synchronous_mode(True) |
||||
|
|
||||
|
settings = world.get_settings() |
||||
|
settings.synchronous_mode = True |
||||
|
settings.fixed_delta_seconds = config.DELTA_SECONDS |
||||
|
world.apply_settings(settings) |
||||
|
|
||||
|
print(f"[INFO] Sync mode enabled @ {1/config.DELTA_SECONDS:.1f} FPS") |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 4. Spawn ego vehicle |
||||
|
# ------------------------------------------------------------------ |
||||
|
vehicle_bp = blueprint_library.filter(config.DEFAULT_EGO_MODEL)[0] |
||||
|
spawn_points = map_.get_spawn_points() |
||||
|
|
||||
|
ego_vehicle = None |
||||
|
|
||||
|
# Priority: CLI Argument > Scenario Preference |
||||
|
sp_req = args.spawn_point |
||||
|
if sp_req is None: |
||||
|
sp_req = scenario.ego_spawn_point |
||||
|
|
||||
|
if sp_req is not None: |
||||
|
if isinstance(sp_req, int): |
||||
|
if sp_req < len(spawn_points): |
||||
|
sp = spawn_points[sp_req] |
||||
|
ego_vehicle = world.try_spawn_actor(vehicle_bp, sp) |
||||
|
if ego_vehicle: |
||||
|
print(f"[INFO] Ego spawned at requested point " |
||||
|
f"index {sp_req}") |
||||
|
else: |
||||
|
print(f"[WARN] Spawn index {sp_req} out of range " |
||||
|
f"(max {len(spawn_points)-1})") |
||||
|
elif isinstance(sp_req, carla.Transform): |
||||
|
ego_vehicle = world.try_spawn_actor(vehicle_bp, sp_req) |
||||
|
if ego_vehicle: |
||||
|
print(f"[INFO] Ego spawned at requested absolute " |
||||
|
f"transform: {sp_req.location}") |
||||
|
|
||||
|
# Fallback search if no request was made |
||||
|
if ego_vehicle is None: |
||||
|
for i, sp in enumerate(spawn_points): |
||||
|
ego_vehicle = world.try_spawn_actor(vehicle_bp, sp) |
||||
|
if ego_vehicle: |
||||
|
print(f"[INFO] Ego spawned at fallback spawn point " |
||||
|
f"index {i}") |
||||
|
break |
||||
|
|
||||
|
if ego_vehicle is None: |
||||
|
print("[ERROR] Failed to spawn ego vehicle") |
||||
|
return False |
||||
|
|
||||
|
# If scenario requested a specific absolute transform, move there |
||||
|
if sp_req is not None and isinstance(sp_req, carla.Transform): |
||||
|
ego_vehicle.set_transform(sp_req) |
||||
|
print(f"[INFO] Ego moved to requested absolute transform: " |
||||
|
f"{sp_req.location}") |
||||
|
|
||||
|
ego_vehicle.set_autopilot(True, traffic_manager.get_port()) |
||||
|
|
||||
|
# Notify scenario of ego spawn (optional hook) |
||||
|
scenario.on_ego_spawned(ego_vehicle) |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 5. Attach sensors + recorder |
||||
|
# ------------------------------------------------------------------ |
||||
|
sensor_manager = SensorManager(world, blueprint_library, ego_vehicle) |
||||
|
sensor_manager.spawn_sensors() |
||||
|
|
||||
|
recorder = None |
||||
|
if not args.no_record: |
||||
|
recorder = Recorder(scenario_name=scenario.name) |
||||
|
|
||||
|
spectator = world.get_spectator() |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 6. Scenario setup |
||||
|
# ------------------------------------------------------------------ |
||||
|
# Tick once to settle ego physics before scenario setup |
||||
|
world.tick() |
||||
|
|
||||
|
scenario.setup(world, ego_vehicle, traffic_manager) |
||||
|
print(f"[INFO] Running scenario: '{scenario.name}' for " |
||||
|
f"{max_frames} frames") |
||||
|
|
||||
|
frame_count = 0 |
||||
|
flag_path = ROOT_DIR / "tmp" / "stop.flag" |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 7. Main simulation loop |
||||
|
# ------------------------------------------------------------------ |
||||
|
try: |
||||
|
from tqdm import tqdm |
||||
|
pbar = tqdm(total=max_frames, |
||||
|
desc=f"SIMULATING: {scenario.name}", |
||||
|
unit=" frames", colour='cyan') |
||||
|
|
||||
|
while frame_count < max_frames: |
||||
|
if os.path.exists(flag_path): |
||||
|
print("\n[INFO] Stop flag detected! Breaking simulation " |
||||
|
"loop for graceful shutdown...") |
||||
|
break |
||||
|
|
||||
|
world.tick() |
||||
|
frame_count += 1 |
||||
|
|
||||
|
# Sync progress bar without refreshing |
||||
|
if pbar: |
||||
|
pbar.n = frame_count |
||||
|
|
||||
|
cam, cam_tpp, radar, lidar = sensor_manager.get_data() |
||||
|
|
||||
|
transform = ego_vehicle.get_transform() |
||||
|
# Third-person spectator view |
||||
|
spectator.set_transform( |
||||
|
carla.Transform( |
||||
|
transform.location + |
||||
|
transform.get_forward_vector() * -5.5 + |
||||
|
carla.Location(z=2.8), |
||||
|
carla.Rotation( |
||||
|
pitch=transform.rotation.pitch - 15, |
||||
|
yaw=transform.rotation.yaw, |
||||
|
roll=transform.rotation.roll |
||||
|
) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
# Record frame |
||||
|
if recorder: |
||||
|
extra_meta = scenario.get_scenario_metadata() |
||||
|
recorder.save(cam, cam_tpp, radar, lidar, ego_vehicle, |
||||
|
extra_meta=extra_meta) |
||||
|
|
||||
|
scenario.step(frame_count, ego_vehicle, pbar=pbar) |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# 8. Shutdown (always runs) |
||||
|
# ------------------------------------------------------------------ |
||||
|
finally: |
||||
|
if pbar: |
||||
|
pbar.close() |
||||
|
print("\n\n[INFO] Simulation complete. Cleaning up...") |
||||
|
|
||||
|
scenario.cleanup() |
||||
|
sensor_manager.destroy() |
||||
|
ego_vehicle.destroy() |
||||
|
if recorder: |
||||
|
recorder.close() |
||||
|
# Populate context for downstream stages |
||||
|
ctx.session_path = Path(recorder.base_path) |
||||
|
ctx.frame_count = recorder.frame_id |
||||
|
|
||||
|
ctx.scenario_name = scenario.name |
||||
|
|
||||
|
# ------------------------------------------------------------------ |
||||
|
# EXPLICIT IDLE MODE: Leave the simulator in synchronous mode to |
||||
|
# freeze GPU load ~0%, giving Shenron full GPU headroom. |
||||
|
# ------------------------------------------------------------------ |
||||
|
|
||||
|
print("[INFO] SimulationStage done") |
||||
|
return True |
||||
@ -0,0 +1,72 @@ |
|||||
|
""" |
||||
|
src/pipeline/stages/video_stage.py |
||||
|
---------------------------------- |
||||
|
VideoStage — Post-processing utility for generating MP4 previews. |
||||
|
|
||||
|
Decoupled from the Recorder to prevent blocking the simulation-to-processing |
||||
|
transition. This stage runs at the very end of the pipeline. |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import cv2 |
||||
|
from pathlib import Path |
||||
|
from pipeline.base import PipelineStage, PipelineContext |
||||
|
|
||||
|
# Add project root to sys.path |
||||
|
sys.path.append(str(Path(__file__).resolve().parents[3])) |
||||
|
|
||||
|
|
||||
|
class VideoStage(PipelineStage): |
||||
|
""" |
||||
|
Pipeline stage that stitches captured camera frames into .mp4 videos. |
||||
|
""" |
||||
|
|
||||
|
@property |
||||
|
def name(self) -> str: |
||||
|
return "video_generation" |
||||
|
|
||||
|
def run(self, ctx: PipelineContext) -> bool: |
||||
|
if ctx.session_path is None or not ctx.session_path.exists(): |
||||
|
print("[VIDEO] No session path found. Skipping.") |
||||
|
return True |
||||
|
|
||||
|
camera_path = ctx.session_path / "camera" |
||||
|
camera_tpp_path = ctx.session_path / "camera_tpp" |
||||
|
|
||||
|
print(f"[VIDEO] Generating video previews for: {ctx.session_path}", flush=True) |
||||
|
|
||||
|
# Configs: (folder, output_name) |
||||
|
configs = [ |
||||
|
(camera_path, "maneuver_dash.mp4"), |
||||
|
(camera_tpp_path, "maneuver_tpp.mp4") |
||||
|
] |
||||
|
|
||||
|
for folder, out_name in configs: |
||||
|
if not folder.exists(): |
||||
|
continue |
||||
|
|
||||
|
images = sorted([img for img in os.listdir(folder) if img.endswith(".png")]) |
||||
|
if not images: |
||||
|
continue |
||||
|
|
||||
|
first_frame_path = folder / images[0] |
||||
|
first_frame = cv2.imread(str(first_frame_path)) |
||||
|
if first_frame is None: |
||||
|
continue |
||||
|
|
||||
|
h, w, _ = first_frame.shape |
||||
|
out_path = ctx.session_path / out_name |
||||
|
|
||||
|
# Using mp4v codec for broad compatibility |
||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v') |
||||
|
out = cv2.VideoWriter(str(out_path), fourcc, 20.0, (w, h)) |
||||
|
|
||||
|
for img_name in images: |
||||
|
frame = cv2.imread(str(folder / img_name)) |
||||
|
if frame is not None: |
||||
|
out.write(frame) |
||||
|
|
||||
|
out.release() |
||||
|
print(f" - Video saved: {out_name}", flush=True) |
||||
|
|
||||
|
return True |
||||
@ -0,0 +1 @@ |
|||||
|
# src/processing — Reusable physics and data-augmentation utilities. |
||||
@ -0,0 +1,200 @@ |
|||||
|
""" |
||||
|
src/processing/physics.py |
||||
|
-------------------------- |
||||
|
Reusable physics utilities for sensor data augmentation. |
||||
|
|
||||
|
Extracted from the monolithic recorder.py to enable: |
||||
|
- Standalone re-processing of existing datasets. |
||||
|
- Unit testing without a live CARLA connection. |
||||
|
- Shared use by the Recorder and future analysis tools. |
||||
|
""" |
||||
|
|
||||
|
import math |
||||
|
from types import SimpleNamespace |
||||
|
|
||||
|
import numpy as np |
||||
|
|
||||
|
|
||||
|
# ----------------------------------------------------------------------- |
||||
|
# Radial Velocity Injection (for Shenron Radar) |
||||
|
# ----------------------------------------------------------------------- |
||||
|
|
||||
|
def calculate_radial_velocity(lidar_data, vehicle, world): |
||||
|
""" |
||||
|
Augment raw semantic LiDAR data with per-point radial velocity. |
||||
|
|
||||
|
Takes the 6-column CARLA semantic LiDAR output [x, y, z, cos, obj, tag] |
||||
|
and injects a radial_speed column, producing a 7-column array: |
||||
|
[x, y, z, radial_speed, cos, obj, tag]. |
||||
|
|
||||
|
Radial speed is the projection of relative velocity onto the |
||||
|
line-of-sight (LOS) vector for each point. A positive value means |
||||
|
the target is moving away from the sensor. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
lidar_data : np.ndarray |
||||
|
Raw reshaped LiDAR array with shape (N, 6) from CARLA. |
||||
|
vehicle : carla.Vehicle |
||||
|
The ego vehicle actor (used for velocity and yaw). |
||||
|
world : carla.World |
||||
|
The CARLA world (used to look up actor velocities by ID). |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
np.ndarray |
||||
|
Augmented array with shape (N, 7): |
||||
|
[x, y, z, radial_speed, cos, obj, tag]. |
||||
|
""" |
||||
|
total_points = lidar_data.shape[0] |
||||
|
|
||||
|
if total_points == 0: |
||||
|
return np.empty((0, 7), dtype=np.float32) |
||||
|
|
||||
|
# 1. Ego velocity in local (sensor-aligned) coordinates |
||||
|
ego_vel = vehicle.get_velocity() |
||||
|
yaw_rad = math.radians(vehicle.get_transform().rotation.yaw) |
||||
|
c, s = math.cos(yaw_rad), math.sin(yaw_rad) |
||||
|
ego_vx_local = ego_vel.x * c + ego_vel.y * s |
||||
|
ego_vy_local = -ego_vel.x * s + ego_vel.y * c |
||||
|
ego_vel_local = np.array([ego_vx_local, ego_vy_local, ego_vel.z], |
||||
|
dtype=np.float32) |
||||
|
|
||||
|
# 2. Relative velocity for each point (static world by default) |
||||
|
V_rel_all = np.zeros((total_points, 3), dtype=np.float32) |
||||
|
V_rel_all[:] = -ego_vel_local |
||||
|
|
||||
|
# Extract actor IDs via bitwise reinterpretation |
||||
|
obj_ids = lidar_data[:, 4].astype(np.float32).view(np.uint32) |
||||
|
unique_hit_ids = np.unique(obj_ids) |
||||
|
|
||||
|
for act_id in unique_hit_ids: |
||||
|
if act_id == 0: |
||||
|
continue |
||||
|
act = world.get_actor(int(act_id)) |
||||
|
if act is not None: |
||||
|
act_vel = act.get_velocity() |
||||
|
ax_l = act_vel.x * c + act_vel.y * s |
||||
|
ay_l = -act_vel.x * s + act_vel.y * c |
||||
|
az_l = act_vel.z |
||||
|
V_rel_all[obj_ids == act_id] = ( |
||||
|
np.array([ax_l, ay_l, az_l], dtype=np.float32) - ego_vel_local |
||||
|
) |
||||
|
|
||||
|
# 3. Project onto LOS unit vector |
||||
|
pts = lidar_data[:, 0:3] |
||||
|
ranges = np.linalg.norm(pts, axis=1) |
||||
|
safe_ranges = np.where(ranges < 0.001, 1.0, ranges) |
||||
|
unit_LOS = pts / safe_ranges[:, None] |
||||
|
|
||||
|
# Positive radial_speed ⇒ distance increasing (moving away) |
||||
|
radial_speed = np.sum(V_rel_all * unit_LOS, axis=1, keepdims=True) |
||||
|
|
||||
|
# 4. Assemble output: [x, y, z, radial_speed, cos, obj, tag] |
||||
|
augmented = np.hstack(( |
||||
|
lidar_data[:, 0:3], |
||||
|
radial_speed, |
||||
|
lidar_data[:, 3:6] |
||||
|
)) |
||||
|
|
||||
|
return augmented |
||||
|
|
||||
|
|
||||
|
# ----------------------------------------------------------------------- |
||||
|
# ADAS Relative Metrics |
||||
|
# ----------------------------------------------------------------------- |
||||
|
|
||||
|
def to_local_location(transform, location): |
||||
|
""" |
||||
|
Convert a world-frame location to a transform-local location. |
||||
|
|
||||
|
Applies inverse translation and yaw rotation (pitch/roll ignored |
||||
|
for horizontal ADAS metrics). |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
transform : carla.Transform |
||||
|
The reference frame (typically the ego vehicle). |
||||
|
location : carla.Location |
||||
|
The world-frame location to convert. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
SimpleNamespace |
||||
|
Object with .x, .y, .z in the local frame. |
||||
|
""" |
||||
|
rel_loc = location - transform.location |
||||
|
|
||||
|
yaw_rad = math.radians(transform.rotation.yaw) |
||||
|
c, s = math.cos(yaw_rad), math.sin(yaw_rad) |
||||
|
|
||||
|
lx = rel_loc.x * c + rel_loc.y * s |
||||
|
ly = -rel_loc.x * s + rel_loc.y * c |
||||
|
|
||||
|
return SimpleNamespace(x=lx, y=ly, z=rel_loc.z) |
||||
|
|
||||
|
|
||||
|
def calculate_relative_metrics(ego, npc): |
||||
|
""" |
||||
|
Calculate range, azimuth, and closing velocity between ego and an NPC. |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
ego : carla.Vehicle |
||||
|
The ego vehicle actor. |
||||
|
npc : carla.Actor |
||||
|
The target NPC actor. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
dict |
||||
|
{"range": float, "azimuth": float, "closing_velocity": float} |
||||
|
""" |
||||
|
e_t = ego.get_transform() |
||||
|
n_t = npc.get_transform() |
||||
|
e_v = ego.get_velocity() |
||||
|
n_v = npc.get_velocity() |
||||
|
|
||||
|
# 1. Range (Euclidean distance) |
||||
|
rel_pos_v = n_t.location - e_t.location |
||||
|
rng = rel_pos_v.length() |
||||
|
|
||||
|
if rng < 0.1: # avoid div by zero |
||||
|
return {"range": rng, "azimuth": 0.0, "closing_velocity": 0.0} |
||||
|
|
||||
|
# 2. Azimuth — angle in ego's local frame |
||||
|
rel_pos_local = to_local_location(e_t, n_t.location) |
||||
|
azimuth = math.degrees(math.atan2(rel_pos_local.y, rel_pos_local.x)) |
||||
|
|
||||
|
# 3. Closing Velocity |
||||
|
# V_c = -(V_npc - V_ego) · (P_npc - P_ego) / |P_npc - P_ego| |
||||
|
rel_vel = n_v - e_v |
||||
|
unit_los = rel_pos_v / rng |
||||
|
closing_vel = -(rel_vel.x * unit_los.x + |
||||
|
rel_vel.y * unit_los.y + |
||||
|
rel_vel.z * unit_los.z) |
||||
|
|
||||
|
return { |
||||
|
"range": round(rng, 3), |
||||
|
"azimuth": round(azimuth, 3), |
||||
|
"closing_velocity": round(closing_vel, 3) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def get_actor_class(actor) -> str: |
||||
|
""" |
||||
|
Categorise a CARLA actor into broad ADAS classes. |
||||
|
|
||||
|
Returns |
||||
|
------- |
||||
|
str |
||||
|
One of: "vehicle", "vru", "pedestrian", "unknown". |
||||
|
""" |
||||
|
type_id = actor.type_id |
||||
|
if "walker" in type_id: |
||||
|
return "pedestrian" |
||||
|
if "vehicle" in type_id: |
||||
|
if "bicycle" in type_id or "motorcycle" in type_id: |
||||
|
return "vru" # Vulnerable Road User |
||||
|
return "vehicle" |
||||
|
return "unknown" |
||||
@ -0,0 +1,4 @@ |
|||||
|
import sys |
||||
|
sys.path.append('scripts/ISOLATE') |
||||
|
from sim_radar_utils.plots import FastHeatmapEngine, postprocess_ra, scan_convert_ra, render_heatmap |
||||
|
print("All imports OK") |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue