whassan's picture
Deploy Sensorflow Studio (part 2)
5d10875 verified
Raw History Blame Contribute Delete
6.66 kB
"""FastAPI router for studio-UX support (/api/studio-ux/*).
Endpoints:
GET /layouts/{key} fetch a persisted UI layout/preset (JSON blob)
PUT /layouts/{key} persist a UI layout/preset
GET /bev/replay deterministically re-generate one BEV-fusion scene
sequence (same generators/engines as /api/bevfusion,
read-only) and return per-frame GT boxes, camera and
LiDAR detections with covariances, fused+tracked
boxes (masklet-propagated flagged) and the camera
baseline boxes — the data behind the interactive
top-down BEV canvas.
This module only *reads* from other subsystems (pure functions on synthetic
scenes); the only writes are its own layout files under runs/studio_ux/.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/api/studio-ux", tags=["studio-ux"])
LAYOUTS_DIR = Path("runs/studio_ux/layouts")
_KEY_RE = re.compile(r"^[A-Za-z0-9._-]{1,120}$")
def _layout_path(key: str) -> Path:
if not _KEY_RE.match(key):
raise HTTPException(422, "layout key must match [A-Za-z0-9._-]{1,120}")
return LAYOUTS_DIR / f"{key}.json"
@router.get("/layouts/{key}")
def get_layout(key: str) -> Dict[str, Any]:
path = _layout_path(key)
if not path.exists():
return {"key": key, "value": None}
try:
with open(path) as f:
return {"key": key, "value": json.load(f)}
except Exception:
return {"key": key, "value": None}
class LayoutBody(BaseModel):
value: Any
@router.put("/layouts/{key}")
def put_layout(key: str, body: LayoutBody) -> Dict[str, Any]:
path = _layout_path(key)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(body.value, f)
return {"key": key, "saved": True}
# ------------------------------------------------------------------ BEV replay
def _det_dict(det) -> Dict[str, Any]:
return {
"modality": det.modality,
"x": round(det.x, 3),
"y": round(det.y, 3),
"cov": [[round(v, 5) for v in row] for row in det.cov],
"dims": [round(v, 3) for v in det.dims],
"yaw": round(det.yaw, 4),
"class_name": det.class_name,
"confidence": round(det.confidence, 3),
}
@router.get("/bev/replay")
def bev_replay(seed: int = Query(7),
n_sequences: int = Query(6, ge=1, le=24),
frames_per_sequence: int = Query(24, ge=8, le=60),
sequence: int = Query(0, ge=0)) -> Dict[str, Any]:
"""Deterministic replay of one scene sequence through both engines."""
from sensorflow.bevfusion.engines import FrameToFrameTracker
from sensorflow.bevfusion.fusion import build_modality_map, decode_bev, fuse_maps
from sensorflow.bevfusion.geometry import BEVGrid
from sensorflow.bevfusion.masklet import BEVMaskletTracker
from sensorflow.bevfusion.scenes import generate_sequences
from sensorflow.bevfusion.sensors import camera_rng, lidar_rng, simulate_camera, simulate_lidar
from sensorflow.schemas.unified_frame import Object3D
if sequence >= n_sequences:
raise HTTPException(422, f"sequence must be < n_sequences ({n_sequences})")
sequences = generate_sequences(n_sequences=n_sequences,
frames_per_sequence=frames_per_sequence,
seed=seed)
seq = sequences[sequence]
grid = BEVGrid()
cam_gen = camera_rng(seed, sequence)
lid_gen = lidar_rng(seed, sequence)
base_gen = camera_rng(seed, sequence) # baseline uses the same camera stream
masklet = BEVMaskletTracker(bounds=(grid.x_min, grid.x_max, grid.y_min, grid.y_max))
base_tracker = FrameToFrameTracker(gate=3.0)
frames: List[Dict[str, Any]] = []
for frame in seq.frames:
cam_dets = simulate_camera(frame, seq, cam_gen)
lid_dets = simulate_lidar(frame, seq, lid_gen)
# fused engine (mirrors engines.run_fused, but keeps the raw detections)
cam_map = build_modality_map(cam_dets, grid, "camera")
lid_map = build_modality_map(lid_dets, grid, "lidar")
decoded = decode_bev(fuse_maps(cam_map, lid_map))
proposals = [Object3D(bbox_3d=b["bbox_3d"], class_name=b["class_name"],
confidence=b["confidence"]) for b in decoded]
tracked = masklet.update(frame.frame_id, proposals)
propagated_ids = getattr(masklet, "last_propagated", set())
fused = [{
"bbox_3d": [round(v, 3) for v in t.bbox_3d],
"class_name": t.class_name,
"confidence": round(float(t.confidence or 0.0), 3),
"track_id": t.track_id,
"propagated": t.track_id in propagated_ids,
} for t in tracked]
# camera-only baseline (mirrors engines.run_baseline; identical rng stream)
base_boxes = [{
"bbox_3d": [d.x, d.y, d.z, d.dims[0], d.dims[1], d.dims[2], d.yaw],
"class_name": d.class_name,
"confidence": d.confidence,
"propagated": False,
} for d in simulate_camera(frame, seq, base_gen)]
base_boxes = base_tracker.update(base_boxes)
baseline = [{
"bbox_3d": [round(float(v), 3) for v in b["bbox_3d"]],
"class_name": b["class_name"],
"confidence": round(float(b["confidence"]), 3),
"track_id": b["track_id"],
"propagated": False,
} for b in base_boxes]
frames.append({
"frame_id": frame.frame_id,
"index": frame.index,
"gt": [{
"instance_id": g.instance_id,
"class_name": g.class_name,
"bbox_3d": g.bbox_3d,
"occluded": g.occluded,
"distance": g.distance,
} for g in frame.gt],
"camera": [_det_dict(d) for d in cam_dets],
"lidar": [_det_dict(d) for d in lid_dets],
"fused": fused,
"baseline": baseline,
})
return {
"sequence_id": seq.sequence_id,
"sequence_index": sequence,
"n_sequences": n_sequences,
"time_of_day": seq.time_of_day,
"weather": seq.weather,
"params": {"n_sequences": n_sequences,
"frames_per_sequence": frames_per_sequence, "seed": seed},
"frames": frames,
}