Add visualization.py
Browse files- fsd_model/visualization.py +163 -0
fsd_model/visualization.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Visualization utilities for FSD model outputs.
|
| 3 |
+
Generates visual representations of:
|
| 4 |
+
- Sensor placement on vehicle
|
| 5 |
+
- BEV perception outputs
|
| 6 |
+
- Planned trajectory
|
| 7 |
+
- Control commands
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import numpy as np
|
| 12 |
+
from typing import Dict, Optional, Tuple, List
|
| 13 |
+
import math
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def visualize_sensor_layout_ascii(vehicle_config) -> str:
|
| 17 |
+
"""Generate ASCII art of sensor placement on vehicle."""
|
| 18 |
+
sc = vehicle_config.sensor_config
|
| 19 |
+
half_l = vehicle_config.length / 2
|
| 20 |
+
half_w = vehicle_config.width / 2
|
| 21 |
+
|
| 22 |
+
# Create grid
|
| 23 |
+
grid_h, grid_w = 30, 50
|
| 24 |
+
grid = [[' '] * grid_w for _ in range(grid_h)]
|
| 25 |
+
|
| 26 |
+
# Scale factors
|
| 27 |
+
scale_x = (grid_w - 10) / (vehicle_config.length + 2)
|
| 28 |
+
scale_y = (grid_h - 6) / (vehicle_config.width + 2)
|
| 29 |
+
|
| 30 |
+
cx, cy = grid_w // 2, grid_h // 2
|
| 31 |
+
|
| 32 |
+
# Draw vehicle outline
|
| 33 |
+
vw = int(vehicle_config.length * scale_x / 2)
|
| 34 |
+
vh = int(vehicle_config.width * scale_y / 2)
|
| 35 |
+
|
| 36 |
+
for x in range(cx - vw, cx + vw + 1):
|
| 37 |
+
if 0 <= x < grid_w:
|
| 38 |
+
if 0 <= cy - vh < grid_h:
|
| 39 |
+
grid[cy - vh][x] = 'β'
|
| 40 |
+
if 0 <= cy + vh < grid_h:
|
| 41 |
+
grid[cy + vh][x] = 'β'
|
| 42 |
+
|
| 43 |
+
for y in range(cy - vh, cy + vh + 1):
|
| 44 |
+
if 0 <= y < grid_h:
|
| 45 |
+
if 0 <= cx - vw < grid_w:
|
| 46 |
+
grid[y][cx - vw] = 'β'
|
| 47 |
+
if 0 <= cx + vw < grid_w:
|
| 48 |
+
grid[y][cx + vw] = 'β'
|
| 49 |
+
|
| 50 |
+
# Corners
|
| 51 |
+
for (dy, dx), ch in [
|
| 52 |
+
((-vh, -vw), 'β'), ((-vh, vw), 'β'),
|
| 53 |
+
((vh, -vw), 'β'), ((vh, vw), 'β')
|
| 54 |
+
]:
|
| 55 |
+
gy, gx = cy + dy, cx + dx
|
| 56 |
+
if 0 <= gy < grid_h and 0 <= gx < grid_w:
|
| 57 |
+
grid[gy][gx] = ch
|
| 58 |
+
|
| 59 |
+
# Direction arrow
|
| 60 |
+
if 0 <= cy < grid_h and cx + vw + 1 < grid_w:
|
| 61 |
+
grid[cy][cx + vw + 1] = 'βΊ'
|
| 62 |
+
grid[cy][cx] = '+' # center
|
| 63 |
+
|
| 64 |
+
# Place cameras
|
| 65 |
+
for i, cam in enumerate(sc.cameras):
|
| 66 |
+
gx = cx + int(cam.placement.x * scale_x)
|
| 67 |
+
gy = cy - int(cam.placement.y * scale_y)
|
| 68 |
+
if 0 <= gy < grid_h and 0 <= gx < grid_w:
|
| 69 |
+
grid[gy][gx] = 'C'
|
| 70 |
+
|
| 71 |
+
# Place ultrasonics
|
| 72 |
+
for i, us in enumerate(sc.ultrasonics):
|
| 73 |
+
gx = cx + int(us.placement.x * scale_x)
|
| 74 |
+
gy = cy - int(us.placement.y * scale_y)
|
| 75 |
+
if 0 <= gy < grid_h and 0 <= gx < grid_w:
|
| 76 |
+
if grid[gy][gx] == ' ' or grid[gy][gx] in 'ββ':
|
| 77 |
+
grid[gy][gx] = 'U'
|
| 78 |
+
|
| 79 |
+
# Add labels
|
| 80 |
+
result = "Vehicle Sensor Layout (Top View)\n"
|
| 81 |
+
result += "C = Camera, U = Ultrasonic, + = Center, βΊ = Forward\n"
|
| 82 |
+
result += "=" * grid_w + "\n"
|
| 83 |
+
for row in grid:
|
| 84 |
+
result += ''.join(row) + "\n"
|
| 85 |
+
result += "=" * grid_w + "\n"
|
| 86 |
+
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def format_model_output(output: Dict[str, torch.Tensor]) -> str:
|
| 91 |
+
"""Format model output as human-readable text."""
|
| 92 |
+
lines = []
|
| 93 |
+
lines.append("ββββββββββββββββββββββββββββββββββββββββββββ")
|
| 94 |
+
lines.append("β FSD Model Output Summary β")
|
| 95 |
+
lines.append("β βββββββββββββββββββββββββββββββββββββββββββ£")
|
| 96 |
+
|
| 97 |
+
# Control outputs
|
| 98 |
+
if "control/steering_deg" in output:
|
| 99 |
+
steer = output["control/steering_deg"].mean().item()
|
| 100 |
+
throttle = output["control/throttle"].mean().item()
|
| 101 |
+
brake = output["control/brake"].mean().item()
|
| 102 |
+
lines.append(f"β Steering: {steer:+.2f}Β°")
|
| 103 |
+
lines.append(f"β Throttle: {throttle:.3f}")
|
| 104 |
+
lines.append(f"β Brake: {brake:.3f}")
|
| 105 |
+
|
| 106 |
+
# Safety
|
| 107 |
+
if "planning/collision_risk" in output:
|
| 108 |
+
risk = output["planning/collision_risk"].mean().item()
|
| 109 |
+
emergency = output["planning/emergency_brake"].mean().item()
|
| 110 |
+
lines.append(f"β Collision Risk: {risk:.3f}")
|
| 111 |
+
lines.append(f"β Emergency Brake: {emergency:.3f}")
|
| 112 |
+
|
| 113 |
+
# Behavior
|
| 114 |
+
if "planning/behavior_logits" in output:
|
| 115 |
+
behaviors = [
|
| 116 |
+
"keep_lane", "turn_left", "turn_right", "change_left",
|
| 117 |
+
"change_right", "stop", "yield", "park", "reverse", "emergency"
|
| 118 |
+
]
|
| 119 |
+
probs = torch.softmax(output["planning/behavior_logits"], dim=-1)
|
| 120 |
+
top_prob, top_idx = probs.mean(0).topk(3)
|
| 121 |
+
lines.append("β Top behaviors:")
|
| 122 |
+
for p, i in zip(top_prob, top_idx):
|
| 123 |
+
lines.append(f"β {behaviors[i.item()]}: {p.item():.3f}")
|
| 124 |
+
|
| 125 |
+
# Waypoints
|
| 126 |
+
if "planning/safe_waypoints" in output:
|
| 127 |
+
wp = output["planning/safe_waypoints"]
|
| 128 |
+
lines.append(f"β Planned waypoints: {wp.shape[1]}")
|
| 129 |
+
lines.append(f"β First: ({wp[0,0,0]:.2f}, {wp[0,0,1]:.2f})")
|
| 130 |
+
lines.append(f"β Last: ({wp[0,-1,0]:.2f}, {wp[0,-1,1]:.2f})")
|
| 131 |
+
|
| 132 |
+
# Controller weights
|
| 133 |
+
if "control/controller_weights" in output:
|
| 134 |
+
w = output["control/controller_weights"].mean(0)
|
| 135 |
+
lines.append(f"β Controller mix: Neural={w[0]:.2f} Stanley={w[1]:.2f} PID={w[2]:.2f}")
|
| 136 |
+
|
| 137 |
+
lines.append("ββββββββββββββββββββββββββββββββββββββββββββ")
|
| 138 |
+
|
| 139 |
+
return "\n".join(lines)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def format_parameter_count(counts: Dict[str, int]) -> str:
|
| 143 |
+
"""Format parameter counts as a nice table."""
|
| 144 |
+
lines = []
|
| 145 |
+
lines.append("βββββββββββββββββββ¬βββββββββββββββ¬ββββββββββββ")
|
| 146 |
+
lines.append("β Module β Parameters β Size (MB) β")
|
| 147 |
+
lines.append("βββββββββββββββββββΌβββββββββββββββΌββββββββββββ€")
|
| 148 |
+
|
| 149 |
+
for name, count in counts.items():
|
| 150 |
+
if name in ["total", "total_trainable"]:
|
| 151 |
+
continue
|
| 152 |
+
size_mb = count * 4 / (1024 * 1024) # float32
|
| 153 |
+
lines.append(f"β {name:<15} β {count:>12,} β {size_mb:>8.2f} β")
|
| 154 |
+
|
| 155 |
+
lines.append("βββββββββββββββββββΌβββββββββββββββΌββββββββββββ€")
|
| 156 |
+
total = counts.get("total", 0)
|
| 157 |
+
trainable = counts.get("total_trainable", 0)
|
| 158 |
+
total_mb = total * 4 / (1024 * 1024)
|
| 159 |
+
lines.append(f"β {'TOTAL':<15} β {total:>12,} β {total_mb:>8.2f} β")
|
| 160 |
+
lines.append(f"β {'Trainable':<15} β {trainable:>12,} β β")
|
| 161 |
+
lines.append("βββββββββββββββββββ΄βββββββββββββββ΄ββββββββββββ")
|
| 162 |
+
|
| 163 |
+
return "\n".join(lines)
|