whassan's picture
Deploy Sensorflow Studio
e8829d6 verified
Raw History Blame Contribute Delete
21.1 kB
"""Temporal & stereo stability profiling on the acceleration backends.
Pyramidal dense optical flow over BEV rasters of the bevfusion synthetic
scenes gives a MODEL-INDEPENDENT motion baseline: the flow field predicts
where every ground-truth object should be next frame, without consulting
any detection engine. Engines (perception-v1-camera, perception-v3-bevfusion,
imported read-only from sensorflow.bevfusion) are then judged against that
baseline:
* flicker rate — detection present -> absent -> present on a track the
flow says was continuously observable
* bbox jitter — engine frame-to-frame displacement vs the
flow-predicted displacement, normalized
* fragmentation — distinct engine track ids per GT instance over
flow-continuous spans
* ID-switch/flow — fraction of engine ID switches that coincide with a
genuine flow discontinuity (excusable) vs those on
smooth flow (the engine's own fault)
A synthetic stereo pair rendered from the exact scene geometry closes the
loop on depth: block-matching disparity -> metric depth is compared against
ground-truth depth and against each engine's 3D positions.
Everything runs twice — once with reference (float32) flow and once with
vitis_emulated (fixed-point) flow — and the report states whether the
accelerated flow changes any stability verdict (the meta-check that the
accelerated metric itself is trustworthy).
"""
from __future__ import annotations
import time
from typing import Dict, List, Optional, Tuple
import numpy as np
from sensorflow.bevfusion.engines import (
BASELINE_ENGINE, FUSED_ENGINE, run_baseline, run_fused,
)
from sensorflow.bevfusion.scenes import SceneSequence, generate_sequences
from sensorflow.vitis.backend import DeviceConfig, PipelineConfig, get_backend
from sensorflow.vitis.render import (
BEV_M_PER_PX_X, BEV_M_PER_PX_Y, BASELINE_M, FOCAL_PX,
ego_to_bev_px, render_bev_frame, render_stereo_pair,
)
from sensorflow.vitis.store import new_id, save_run
KNOWN_ENGINES = [BASELINE_ENGINE, FUSED_ENGINE]
MATCH_GATE_M = 2.5 # detection <-> GT association gate (meters)
FLOW_RESIDUAL_OK_M = 1.2 # flow residual below this => flow-continuous
STEREO_FRAMES_PER_SEQ = 3
# --------------------------------------------------------------------------
# Flow baseline
# --------------------------------------------------------------------------
def _flow_displacement_m(flow: np.ndarray, r: float, c: float) -> Tuple[float, float]:
"""Sample flow (dx, dy in px) near (r, c); return ego (dx_m, dy_m)."""
h, w = flow.shape[:2]
r0, r1 = int(max(0, r - 2)), int(min(h, r + 3))
c0, c1 = int(max(0, c - 2)), int(min(w, c + 3))
if r1 <= r0 or c1 <= c0:
return 0.0, 0.0
patch = flow[r0:r1, c0:c1]
d_col = float(np.median(patch[..., 0]))
d_row = float(np.median(patch[..., 1]))
# row grows toward ego (-x), col grows toward +y.
return -d_row * BEV_M_PER_PX_X, d_col * BEV_M_PER_PX_Y
def compute_flow_baseline(seq: SceneSequence, backend, seed: int) -> Dict:
"""Per-track flow records: for each GT instance and frame transition,
the flow-predicted displacement vs the true GT displacement."""
imgs = [render_bev_frame(f, seq, seed) for f in seq.frames]
flows = [backend.optical_flow(imgs[i], imgs[i + 1], levels=3,
window=9, iterations=3)
for i in range(len(imgs) - 1)]
gt_pos: Dict[str, Dict[int, Tuple[float, float, bool]]] = {}
for frame in seq.frames:
for g in frame.gt:
gt_pos.setdefault(g.instance_id, {})[frame.index] = (
g.bbox_3d[0], g.bbox_3d[1], g.occluded)
records: Dict[str, Dict[int, Dict]] = {}
for iid, per_frame in gt_pos.items():
for fi, (x, y, occ) in per_frame.items():
nxt = per_frame.get(fi + 1)
if nxt is None or fi >= len(flows):
continue
r, c = ego_to_bev_px(x, y)
fdx, fdy = _flow_displacement_m(flows[fi], r, c)
true_dx, true_dy = nxt[0] - x, nxt[1] - y
residual = float(np.hypot(fdx - true_dx, fdy - true_dy))
records.setdefault(iid, {})[fi] = {
"flow_dx_m": fdx, "flow_dy_m": fdy,
"true_dx_m": true_dx, "true_dy_m": true_dy,
"residual_m": residual,
"continuous": residual <= FLOW_RESIDUAL_OK_M and not occ,
"occluded": occ,
}
return records
# --------------------------------------------------------------------------
# Engine evaluation against the flow baseline
# --------------------------------------------------------------------------
def _run_engine(name: str, seq: SceneSequence, seed: int,
seq_index: int) -> Dict[str, List[Dict]]:
if name == BASELINE_ENGINE:
return run_baseline(seq, seed, seq_index)
if name == FUSED_ENGINE:
out, _ = run_fused(seq, seed, seq_index)
return out
raise ValueError(f"Unknown engine {name!r}; known: {KNOWN_ENGINES}")
def _match_engine_to_gt(seq: SceneSequence,
engine_out: Dict[str, List[Dict]]) -> Dict[str, Dict[int, Dict]]:
"""{instance_id: {frame_index: {x, y, track_id} or absent}} via NN gate."""
assoc: Dict[str, Dict[int, Dict]] = {}
for frame in seq.frames:
dets = list(engine_out.get(frame.frame_id, []))
used = set()
for g in sorted(frame.gt, key=lambda g: g.distance):
best, best_d = None, MATCH_GATE_M
for j, det in enumerate(dets):
if j in used:
continue
d = float(np.hypot(det["bbox_3d"][0] - g.bbox_3d[0],
det["bbox_3d"][1] - g.bbox_3d[1]))
if d < best_d:
best, best_d = j, d
if best is not None:
used.add(best)
det = dets[best]
assoc.setdefault(g.instance_id, {})[frame.index] = {
"x": det["bbox_3d"][0], "y": det["bbox_3d"][1],
"track_id": det.get("track_id"),
}
return assoc
def _engine_track_metrics(seq: SceneSequence, flow_records: Dict,
assoc: Dict[str, Dict[int, Dict]]) -> Dict:
flicker_events = 0
flicker_opportunities = 0
jitter_vals: List[float] = []
fragments = 0
fragment_tracks = 0
id_switches = 0
id_switches_at_flow_break = 0
per_track: List[Dict] = []
gt_frames: Dict[str, List[int]] = {}
for frame in seq.frames:
for g in frame.gt:
gt_frames.setdefault(g.instance_id, []).append(frame.index)
for iid, findices in gt_frames.items():
findices = sorted(findices)
det_frames = assoc.get(iid, {})
flow_rec = flow_records.get(iid, {})
continuous_frames = [fi for fi in findices
if flow_rec.get(fi, {}).get("continuous")]
# Flicker: present -> absent -> present inside flow-continuous span.
presence = [(fi, fi in det_frames) for fi in findices]
detected_indices = [fi for fi, p in presence if p]
events_here = 0
if len(detected_indices) >= 2:
lo, hi = detected_indices[0], detected_indices[-1]
gap_open = False
for fi in findices:
if fi < lo or fi > hi:
continue
if fi not in det_frames:
if flow_rec.get(fi, {}).get("continuous") or \
flow_rec.get(fi - 1, {}).get("continuous"):
gap_open = True
elif gap_open:
events_here += 1
gap_open = False
flicker_events += events_here
flicker_opportunities += max(0, len(continuous_frames) - 1)
# Jitter: engine displacement vs flow-predicted displacement.
for fi in findices:
if fi in det_frames and (fi + 1) in det_frames and fi in flow_rec:
ddx = det_frames[fi + 1]["x"] - det_frames[fi]["x"]
ddy = det_frames[fi + 1]["y"] - det_frames[fi]["y"]
fdx = flow_rec[fi]["flow_dx_m"]
fdy = flow_rec[fi]["flow_dy_m"]
mag = float(np.hypot(fdx, fdy))
dev = float(np.hypot(ddx - fdx, ddy - fdy))
jitter_vals.append(dev / (mag + 0.5))
# Fragmentation over flow-continuous spans + ID-switch/flow overlap.
ids_seen = []
prev_fi = None
for fi in sorted(det_frames):
tid = det_frames[fi]["track_id"]
if ids_seen and tid != ids_seen[-1]:
id_switches += 1
broke = not flow_rec.get(prev_fi, {}).get("continuous", True)
id_switches_at_flow_break += int(broke)
if not ids_seen or tid != ids_seen[-1]:
ids_seen.append(tid)
prev_fi = fi
if continuous_frames:
n_ids_continuous = len({det_frames[fi]["track_id"]
for fi in continuous_frames
if fi in det_frames})
if n_ids_continuous > 0:
fragment_tracks += 1
fragments += n_ids_continuous - 1
per_track.append({
"instance_id": iid,
"gt_frames": len(findices),
"detected_frames": len(det_frames),
"flow_continuous_frames": len(continuous_frames),
"flicker_events": events_here,
"distinct_track_ids": len(set(ids_seen)) if ids_seen else 0,
})
return {
"flicker_events": flicker_events,
"flicker_opportunities": flicker_opportunities,
"jitter_vals": jitter_vals,
"fragments": fragments,
"fragment_tracks": fragment_tracks,
"id_switches": id_switches,
"id_switches_at_flow_break": id_switches_at_flow_break,
"per_track": per_track,
}
def _aggregate_engine(seq_metrics: List[Dict]) -> Dict:
flicker_e = sum(m["flicker_events"] for m in seq_metrics)
flicker_o = max(1, sum(m["flicker_opportunities"] for m in seq_metrics))
jitter = [v for m in seq_metrics for v in m["jitter_vals"]]
frags = sum(m["fragments"] for m in seq_metrics)
frag_tracks = max(1, sum(m["fragment_tracks"] for m in seq_metrics))
switches = sum(m["id_switches"] for m in seq_metrics)
excusable = sum(m["id_switches_at_flow_break"] for m in seq_metrics)
flicker_rate = flicker_e / flicker_o
mean_jitter = float(np.mean(jitter)) if jitter else 0.0
frag_rate = frags / frag_tracks
unexcused = (switches - excusable) / max(1, switches) if switches else 0.0
score = 100.0 * (
0.35 * max(0.0, 1.0 - 4.0 * flicker_rate) +
0.25 * float(np.exp(-1.5 * mean_jitter)) +
0.25 * max(0.0, 1.0 - 0.6 * frag_rate) +
0.15 * (1.0 - unexcused)
)
return {
"flicker_rate": round(flicker_rate, 4),
"mean_jitter": round(mean_jitter, 4),
"fragmentation_per_track": round(frag_rate, 4),
"id_switches": switches,
"id_switches_at_flow_break": excusable,
"unexcused_id_switch_fraction": round(unexcused, 4),
"stability_score": round(score, 2),
}
# --------------------------------------------------------------------------
# Stereo consistency
# --------------------------------------------------------------------------
def _stereo_check(sequences: List[SceneSequence], backend, seed: int,
max_frames_per_seq: int = STEREO_FRAMES_PER_SEQ) -> Dict:
depth_errors: List[float] = []
rel_errors: List[float] = []
near_errors: List[float] = [] # objects closer than 30 m
disparity_errors: List[float] = []
n_objects = 0
for seq in sequences:
step = max(1, len(seq.frames) // max_frames_per_seq)
for frame in seq.frames[::step][:max_frames_per_seq]:
left, right, objs = render_stereo_pair(frame, seq, seed)
if not objs:
continue
disp = backend.stereo_block_match(left, right, max_disparity=48,
block=9)
for o in objs:
u, v = int(round(o["u"])), int(round(o["v"]))
if not (2 <= v < disp.shape[0] - 2 and 2 <= u < disp.shape[1] - 2):
continue
d_meas = float(np.median(disp[v - 2:v + 3, u - 2:u + 3]))
if d_meas < 0.5:
continue
depth = FOCAL_PX * BASELINE_M / d_meas
err = abs(depth - o["depth_m"])
depth_errors.append(err)
rel_errors.append(err / o["depth_m"])
if o["depth_m"] < 30.0:
near_errors.append(err)
disparity_errors.append(abs(d_meas - o["disparity_px"]))
n_objects += 1
return {
"objects_checked": n_objects,
"median_abs_depth_error_m": round(float(np.median(depth_errors)), 3) if depth_errors else None,
"median_rel_depth_error": round(float(np.median(rel_errors)), 4) if rel_errors else None,
"median_abs_depth_error_near_m": round(float(np.median(near_errors)), 3) if near_errors else None,
"p90_abs_depth_error_m": round(float(np.percentile(depth_errors, 90)), 3) if depth_errors else None,
"median_abs_disparity_error_px": round(float(np.median(disparity_errors)), 3) if disparity_errors else None,
"note": ("Stereo depth error grows quadratically with range at fixed "
"disparity error; near-field (<30 m) stats are the "
"geometry-limited figure of merit."),
}
# --------------------------------------------------------------------------
# Full profile
# --------------------------------------------------------------------------
def run_temporal_profile(engines: Optional[List[str]] = None,
n_sequences: int = 3, frames_per_sequence: int = 18,
seed: int = 7, width_bits: int = 12, int_bits: int = 6,
device: str = "versal-ai-edge",
persist: bool = True) -> Dict:
"""Profile engines against reference AND vitis_emulated flow baselines."""
t0 = time.perf_counter()
engines = engines or list(KNOWN_ENGINES)
for e in engines:
if e not in KNOWN_ENGINES:
raise ValueError(f"Unknown engine {e!r}; known: {KNOWN_ENGINES}")
sequences = generate_sequences(n_sequences, frames_per_sequence, seed)
backends = {
"reference": get_backend("reference"),
"vitis_emulated": get_backend(
"vitis_emulated",
PipelineConfig(precision={"default": (width_bits, int_bits)},
device=DeviceConfig(name=device))),
}
# Engine outputs are backend-independent (computed once).
engine_assoc: Dict[str, List[Tuple[SceneSequence, Dict]]] = {e: [] for e in engines}
for si, seq in enumerate(sequences):
for e in engines:
out = _run_engine(e, seq, seed, si)
engine_assoc[e].append((seq, _match_engine_to_gt(seq, out)))
results: Dict[str, Dict] = {}
timeline_sample = None
for be_name, backend in backends.items():
flow_by_seq = [compute_flow_baseline(seq, backend, seed)
for seq in sequences]
engines_report = {}
for e in engines:
seq_metrics, cohort_metrics = [], {}
for (seq, assoc), flow_records in zip(engine_assoc[e], flow_by_seq):
m = _engine_track_metrics(seq, flow_records, assoc)
seq_metrics.append(m)
cohort = f"{seq.time_of_day}/{seq.weather}"
cohort_metrics.setdefault(cohort, []).append(m)
occluded = _occluded_subset(seq, flow_records, assoc)
if occluded is not None:
cohort_metrics.setdefault("occluded", []).append(occluded)
engines_report[e] = _aggregate_engine(seq_metrics)
engines_report[e]["cohorts"] = {
c: _aggregate_engine(ms) for c, ms in sorted(cohort_metrics.items())}
if timeline_sample is None and be_name == "reference":
timeline_sample = _timeline_sample(
engine_assoc[e][0][0], flow_by_seq[0],
engine_assoc[e][0][1], engine=e)
stereo = _stereo_check(sequences, backend, seed)
results[be_name] = {"engines": engines_report, "stereo": stereo}
# Meta-check: does fixed-point flow change any verdict?
ranking = {}
for be_name, res in results.items():
ranking[be_name] = sorted(
engines, key=lambda e: -res["engines"][e]["stability_score"])
score_deltas = {
e: round(results["vitis_emulated"]["engines"][e]["stability_score"] -
results["reference"]["engines"][e]["stability_score"], 2)
for e in engines}
meta = {
"ranking_agrees": ranking["reference"] == ranking["vitis_emulated"],
"ranking_reference": ranking["reference"],
"ranking_vitis_emulated": ranking["vitis_emulated"],
"stability_score_delta_by_engine": score_deltas,
"max_abs_score_delta": max(abs(v) for v in score_deltas.values()),
"note": ("Meta-check that the accelerated (fixed-point) flow metric "
"is itself trustworthy: engine stability verdicts should "
"not change between reference and emulated flow."),
}
run_id = new_id("temprun")
best = ranking["reference"][0]
payload = {
"run_id": run_id,
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
"kind": "temporal",
"params": {"engines": engines, "n_sequences": n_sequences,
"frames_per_sequence": frames_per_sequence, "seed": seed,
"width_bits": width_bits, "int_bits": int_bits,
"device": device},
"results": results,
"backend_agreement": meta,
"timeline_sample": timeline_sample,
"elapsed_s": round(time.perf_counter() - t0, 2),
"summary": {
"best_engine": best,
"best_score": results["reference"]["engines"][best]["stability_score"],
"ranking_agrees": meta["ranking_agrees"],
},
"emulation_note": ("vitis_emulated flow runs on a constraint-faithful "
"CPU emulator; no FPGA hardware was used."),
}
if persist:
save_run("temporal", run_id, payload)
return payload
def _occluded_subset(seq: SceneSequence, flow_records: Dict,
assoc: Dict) -> Optional[Dict]:
"""Metrics restricted to instances that carry a planted occlusion window."""
occluded_iids = {g.instance_id for f in seq.frames for g in f.gt if g.occluded}
if not occluded_iids:
return None
sub_seq = SceneSequence(sequence_id=seq.sequence_id,
time_of_day=seq.time_of_day, weather=seq.weather)
for frame in seq.frames:
sub = frame.model_copy(deep=True)
sub.gt = [g for g in sub.gt if g.instance_id in occluded_iids]
sub_seq.frames.append(sub)
sub_assoc = {k: v for k, v in assoc.items() if k in occluded_iids}
sub_flow = {k: v for k, v in flow_records.items() if k in occluded_iids}
return _engine_track_metrics(sub_seq, sub_flow, sub_assoc)
def _timeline_sample(seq: SceneSequence, flow_records: Dict, assoc: Dict,
engine: str) -> Optional[Dict]:
"""Flow-vs-detection timeline for the most interesting track (the one
with the most detection gaps while flow was continuous)."""
best_iid, best_gaps = None, -1
gt_frames: Dict[str, List[int]] = {}
for frame in seq.frames:
for g in frame.gt:
gt_frames.setdefault(g.instance_id, []).append(frame.index)
for iid, findices in gt_frames.items():
det = assoc.get(iid, {})
gaps = sum(1 for fi in findices
if fi not in det and
flow_records.get(iid, {}).get(fi, {}).get("continuous"))
if gaps > best_gaps and len(findices) >= 6:
best_iid, best_gaps = iid, gaps
if best_iid is None:
return None
det = assoc.get(best_iid, {})
flow_rec = flow_records.get(best_iid, {})
frames = []
for fi in sorted(gt_frames[best_iid]):
frames.append({
"frame_index": fi,
"detected": fi in det,
"track_id": det.get(fi, {}).get("track_id"),
"flow_continuous": bool(flow_rec.get(fi, {}).get("continuous")),
"flow_residual_m": round(flow_rec.get(fi, {}).get("residual_m", 0.0), 3)
if fi in flow_rec else None,
"occluded": bool(flow_rec.get(fi, {}).get("occluded", False)),
})
return {"instance_id": best_iid, "engine": engine,
"sequence_id": seq.sequence_id, "frames": frames}