bevformer-p150 / code /scripts /make_demo.py
changh95's picture
tt-model push bevformer-p150 (container)
ddaad99 verified
Raw History Blame Contribute Delete
22.6 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Render the model card's demo media (``media/``) from Tenstorrent p150 outputs. CPU only; tt-models workspace only
(it reads the public datasets and reuses the drawing primitives of the research renderer
``research/bevformer/public_data/scripts/render_media.py``: 6-camera mosaics with projected 3D boxes, BEV plots, the
dataviz reference palette's dark steps, renders <= 960 px wide, nothing cropped onto people or plates).
# 1. TT outputs (device): the public sequences through the research runner (results/<tag>/<sequence>.json) and the
# shipped sample through the API (examples/quickstart.py writes quickstart.json):
bin/devrun -t 1800 -- python code/scripts/run_public_tt.py --tag tt_release pandaset:ps019_f30-40 pandaset:ps019_2hz
bin/devrun -t 900 -- python examples/quickstart.py --out-dir <dir>
# (nuScenes mini_val: tests/test_e2e_device.py::test_nuscenes_mini_val writes results/tt_e2e)
# 2. renders (research venv: OpenCV, PIL):
tools/research-venv/bin/python code/scripts/make_demo.py --sample-tt <dir>/quickstart.json --out media
What it draws (every file is listed with its source, licence and changes in ``media/ATTRIBUTION.md``, written too):
- PandaSet 019 frame 40 (CC BY 4.0 + PandaSet Dataset Terms): 6-camera mosaic + BEV of the TT boxes (history
f30 -> f35 -> f40, 0.5 s steps); the TT-vs-CPU agreement (BEV of frame 40 side by side + the matched scores of
the 16 frames of the 2 Hz sequence); an animation of the 2 Hz sequence (front camera + BEV);
- nuScenes v1.0-mini scene-0103 / scene-0916 key-frame 20 (CC BY-NC-SA 4.0: labelled non-commercial in the image and
in ATTRIBUTION.md) and the scene-0103 key-frame animation;
- the shipped synthetic sample (generated by ``code/scripts/make_sample.py``, Apache-2.0): its six cameras with the TT
boxes, and the BEV of the TT boxes next to the stored CPU reference.
Boxes drawn: every detection the Autoware node publishes (score > 0.2), in the physical convention (length along the
heading). Grey dashed outlines: dataset annotations (the synthetic sample: where its objects were placed).
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import math
import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
import numpy as np
ROOT = Path(os.environ.get("TT_MODELS_ROOT", "/home/ubuntu/experiments/tt-models"))
PUB = ROOT / "research" / "bevformer" / "public_data"
CODE = Path(__file__).resolve().parents[1]
BUNDLE = CODE.parent
sys.path.insert(0, str(CODE))
THR = 0.2
TT_SUB = ("Tenstorrent Blackhole p150 output (tt-nn; ETH dispatch, 12x10 grid; fp32 weights, bf16 activations). "
"Boxes: the Autoware node's detections (score > 0.2).")
NC_NOTE = "nuScenes render: CC BY-NC-SA 4.0, non-commercial use only. (c) Motional AD Inc.; not endorsed."
def load_renderer():
"""research/bevformer/public_data/scripts/render_media.py as a module (trusted workspace code, not data)."""
path = PUB / "scripts" / "render_media.py"
spec = importlib.util.spec_from_file_location("bevformer_render_media", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
R = load_renderer()
P = R.P
def results(dataset: str, seq: str, tag: str) -> Dict[str, Any]:
return {f["frame_id"]: f for f in P.read_json(str(PUB / dataset / "results" / tag / f"{seq}.json"))["frames"]}
def match(a: List[Dict], b: List[Dict], dist: float = 0.5) -> List[Tuple[Dict, Dict]]:
"""Greedy same-class matching within ``dist`` m (BEV centres), b in descending score order."""
used, out = set(), []
for rb in sorted(b, key=lambda d: -d["score"]):
best, bd = None, dist
for i, ta in enumerate(a):
if i in used or ta["cls"] != rb["cls"]:
continue
dd = math.hypot(ta["x"] - rb["x"], ta["y"] - rb["y"])
if dd <= bd:
best, bd = i, dd
if best is not None:
used.add(best)
out.append((a[best], rb))
return out
# ------------------------------------------------------------------------------------------------ public frames
def card(dataset, seq, fid, tag, name, title, rows, attribution, out_dir: Path, cpu_tag="cpu"):
z, fr, dets, gts, cams, pts = R.frame_bundle(dataset, seq, fid, tag=tag)
bev = R.draw_bev(pts, dets, gts)
cpu = [R.det_phys(d) for d in results(dataset, seq, cpu_tag)[fid]["dets_autoware"] if d["score"] > THR]
m = match(dets, cpu)
n_gt50 = sum(1 for g in gts if math.hypot(g["x"], g["y"]) < 50)
mg = P.match_dets_to_gt([dict(d) for d in dets], [g for g in gts if math.hypot(g["x"], g["y"]) < 50], dist_thr=2.0)
counts: Dict[str, int] = {}
for d in dets:
counts[R.CLS_GROUP[d["cls"]]] = counts.get(R.CLS_GROUP[d["cls"]], 0) + 1
err = max((abs(a["score"] - b["score"]) for a, b in m), default=0.0)
stats = [f"TT detections (score > 0.2): {len(dets)}",
" " + ", ".join(f"{k} {v}" for k, v in counts.items()),
f"fp32 CPU reference: {len(cpu)}; matched {len(m)}",
f" (same class, < 0.5 m; max |score diff| {err:.3f})",
f"annotations within 50 m: {n_gt50}; matched {len(mg)} (< 2 m)",
f"history: {'on' if fr['use_prev_bev'] else 'off (first frame)'}"]
img = R.compose(cams, bev, title, TT_SUB, list(rows), stats, attribution)
out = out_dir / name
img.save(out, quality=88, optimize=True)
print("wrote", out, img.size, out.stat().st_size, "B", flush=True)
return {"file": name, "dataset": dataset, "sequence": seq, "frame": fid, "tt": len(dets), "cpu": len(cpu),
"matched": len(m), "max_score_diff": round(err, 4)}
def gif(dataset, seq, tag, name, out_dir: Path, size=300, cam_w=534, step=1):
"""Animated front camera + BEV of a sequence (TT outputs), drawn natively at GIF size."""
import cv2
from PIL import Image, ImageDraw
man = P.read_json(str(PUB / dataset / "manifests" / f"{seq}.json"))
ddir = PUB / dataset
res = results(dataset, seq, tag)
gt = {f["frame_id"]: f for f in P.read_json(str(ddir / "gt" / f"{seq}.json"))["frames"]}
frames = []
for fr in man["frames"][::step]:
fid = fr["frame_id"]
if fid not in res:
break
z = np.load(ddir / "inputs" / seq / f"{fid}.npz")
dets = [R.det_phys(d) for d in res[fid]["dets_autoware"] if d["score"] > THR]
gts = [g for g in gt[fid]["boxes"] if g["cls"] is not None and abs(g["x"]) < 51.2 and abs(g["y"]) < 51.2
and (dataset != "nuscenes" or g["num_lidar_pts"] + g["num_radar_pts"] > 0)]
i = P.CAMS.index("CAM_FRONT")
cam = cv2.resize(P.to_node_size(P.decode_jpeg(z["jpeg_CAM_FRONT"])), (cam_w, size), interpolation=cv2.INTER_AREA)
R.draw_boxes_on_camera(cam, dets, z["T_lidar_cam"][i], z["K"][i], cam_w / P.NET_W)
bev = R.draw_bev(R.lidar_points(dataset, z), dets, gts, S=size)
body = np.concatenate([cam, bev], 1)[:, :, ::-1]
im = Image.new("RGB", (body.shape[1], body.shape[0] + 16), R.SURF)
im.paste(Image.fromarray(body), (0, 0))
dr = ImageDraw.Draw(im)
short = ("nuScenes (c) Motional AD Inc., CC BY-NC-SA 4.0, NON-COMMERCIAL; Motional does not endorse this work"
if dataset == "nuscenes" else
"Contains data from PandaSet (Scale AI and Hesai), CC BY 4.0 + PandaSet Dataset Terms; not endorsed")
dr.text((6, body.shape[0] + 2), short, font=R.font(10), fill=R.INK2)
dr.rectangle([0, 0, im.width, 17], fill=(0, 0, 0))
dr.text((6, 2), f"{seq} {fid} Tenstorrent p150 output detections > 0.2: {len(dets)}", font=R.font(11),
fill=R.INK)
frames.append(im)
pal = frames[len(frames) // 2].quantize(colors=256, method=Image.Quantize.MEDIANCUT)
q = [f.quantize(palette=pal, dither=Image.Dither.NONE) for f in frames]
out = out_dir / name
q[0].save(out, save_all=True, append_images=q[1:], duration=500, loop=0, optimize=True)
print("wrote", out, len(q), "frames", out.stat().st_size, "B", flush=True)
return {"file": name, "dataset": dataset, "sequence": seq, "frames": len(q)}
def tt_vs_cpu(dataset, seq, fid, tag, name, out_dir: Path, all_seq: str, all_tag: str):
"""BEV of the CPU reference and of the TT output side by side (frame ``fid`` of ``seq``), plus a scatter of the
matched boxes' TT vs CPU scores over every frame of ``all_seq``. No camera pixels."""
from PIL import Image, ImageDraw
S, sz = 300, 250
z, fr, dets, gts, cams, pts = R.frame_bundle(dataset, seq, fid, tag=tag)
cpu = [R.det_phys(d) for d in results(dataset, seq, "cpu")[fid]["dets_autoware"] if d["score"] > THR]
left, right = R.draw_bev(pts, cpu, gts, S=S), R.draw_bev(pts, dets, gts, S=S)
m = match(dets, cpu)
pairs, n_cpu, n_tt, n_m = [], 0, 0, 0
tt_all, cpu_all = results(dataset, all_seq, all_tag), results(dataset, all_seq, "cpu")
for k in cpu_all:
if k not in tt_all:
continue
# the published boxes (score > 0.2) of both sides
a = [R.det_phys(d) for d in tt_all[k]["dets_autoware"]]
b = [R.det_phys(d) for d in cpu_all[k]["dets_autoware"]]
mm = match(a, b)
n_cpu += sum(1 for d in b if d["score"] > THR)
n_tt += sum(1 for d in a if d["score"] > THR)
n_m += sum(1 for x, y in mm if y["score"] > THR)
pairs += [(y["score"], x["score"]) for x, y in mm]
W_, H_ = 2 * S + 360, 56 + max(S, sz + 80) + 40
im = Image.new("RGB", (W_, H_), R.SURF)
d = ImageDraw.Draw(im)
d.text((12, 8), f"TT vs the fp32 CPU reference: PandaSet 019 frame {fid[1:]} (BEV) and the {len(tt_all)} frames "
f"of the 2 Hz sequence", font=R.font(15, True), fill=R.INK)
d.text((12, 32), f"left: CPU reference ({len(cpu)} boxes) middle: Tenstorrent p150 ({len(dets)} boxes, "
f"{len(m)} matched: same class, < 0.5 m) right: scores", font=R.font(12), fill=R.INK2)
im.paste(Image.fromarray(left[:, :, ::-1]), (0, 56))
im.paste(Image.fromarray(right[:, :, ::-1]), (S + 8, 56))
# scatter
x0, y0 = 2 * S + 60, 76
d.rectangle([x0, y0, x0 + sz, y0 + sz], outline=R.GRID, fill=(0x20, 0x20, 0x1f))
for t in (0.2, 0.4, 0.6, 0.8):
p = int(t * sz)
d.line([(x0 + p, y0), (x0 + p, y0 + sz)], fill=R.GRID)
d.line([(x0, y0 + sz - p), (x0 + sz, y0 + sz - p)], fill=R.GRID)
d.line([(x0, y0 + sz), (x0 + sz, y0)], fill=R.MUTED)
for c_, t_ in pairs:
u, v = x0 + c_ * sz, y0 + sz - t_ * sz
d.ellipse([u - 2, v - 2, u + 2, v + 2], fill=(0x39, 0x87, 0xe5))
d.text((x0, y0 + sz + 6), "CPU score -> (0 .. 1, grid 0.2)", font=R.font(11), fill=R.INK2)
d.text((x0 - 40, y0 - 18), "TT score", font=R.font(11), fill=R.INK2)
errs = [abs(a - b) for a, b in pairs]
d.text((x0, y0 + sz + 22), f"{len(pairs)} matched boxes (published by both)", font=R.font(11), fill=R.INK2)
d.text((x0, y0 + sz + 38), f"mean diff {np.mean([t - c for c, t in pairs]):+.4f}, max |diff| {max(errs):.3f}",
font=R.font(11), fill=R.INK2)
d.text((12, H_ - 30), "Contains data from PandaSet (Scale AI and Hesai), CC BY 4.0 + PandaSet Dataset Terms; LiDAR "
"points (grey) for context only; not endorsed by Scale AI or Hesai.", font=R.font(10),
fill=R.MUTED)
d.text((12, H_ - 16), f"over all frames: CPU {n_cpu} boxes > 0.2, TT {n_tt}, matched {n_m} "
f"(recall {n_m / max(n_cpu, 1):.3f}, precision {n_m / max(n_tt, 1):.3f})",
font=R.font(10), fill=R.MUTED)
out = out_dir / name
im.save(out, optimize=True)
print("wrote", out, im.size, out.stat().st_size, "B", flush=True)
return {"file": name, "pairs": len(pairs), "recall": n_m / max(n_cpu, 1), "precision": n_m / max(n_tt, 1),
"max_abs": max(errs), "mean": float(np.mean([t - c for c, t in pairs]))}
# ------------------------------------------------------------------------------------------- the synthetic sample
def _body_dets(body: Dict[str, Any]) -> List[Dict[str, Any]]:
names, rot = body["meta"]["class_names"], body["meta"]["rot"]
out = []
for i, d in enumerate(body["detections"]):
x, y, z = d["center"]
w, l, h = d["size"]
out.append(R.det_phys({"x": x, "y": y, "z": z, "w": w, "l": l, "h": h, "r": rot[i], "score": d["score"],
"cls": names[d["label_id"]]}))
return out
def synthetic(tt_json: Path, name: str, out_dir: Path):
import cv2
from PIL import Image, ImageDraw
from tt_bevformer.host.calib import T_BASE_FROM_VIRTUAL_LIDAR
from tt_bevformer.host.inputs import resolve_calibration
samples = CODE / "tt_bevformer" / "samples"
man = json.loads((samples / "synthetic_6cam.json").read_text())
cal = resolve_calibration(man["calibration"])
tt = _body_dets(json.loads(Path(tt_json).read_text()))
ref = _body_dets(json.loads((samples / "synthetic_6cam.reference.json").read_text()))
inv = np.linalg.inv(T_BASE_FROM_VIRTUAL_LIDAR)
gts = []
for t in man["targets"]:
p = inv @ np.array(t["center"] + [1.0])
l, w, h = t["size"]
gts.append({"x": p[0], "y": p[1], "z": p[2], "l": l, "w": w, "h": h, "yaw": t["yaw"] + math.pi / 2,
"cls": t["label"]})
cams = {}
for c in P.CAMS:
im = np.asarray(Image.open(samples / man["images"][c]).convert("RGB"))[:, :, ::-1].copy()
im = cv2.resize(im, (R.CAM_W, R.CAM_H), interpolation=cv2.INTER_AREA)
T = inv @ np.asarray(cal["cameras"][c]["T_ref_from_camera"], np.float64)
R.draw_boxes_on_camera(im, tt, T, np.asarray(cal["cameras"][c]["intrinsics"], np.float64), R.CAM_W / 1600.0)
cams[c] = im
S = 300
b_tt, b_ref = R.draw_bev(None, tt, gts, S=S), R.draw_bev(None, ref, gts, S=S)
m = match(tt, ref)
err = max((abs(a["score"] - b["score"]) for a, b in m), default=0.0)
head = 58
W_, H_ = R.W, head + 2 * R.CAM_H + S + 40
canvas = Image.new("RGB", (W_, H_), R.SURF)
d = ImageDraw.Draw(canvas)
d.text((12, 8), "The shipped sample: a synthetic six-camera test pattern (generated; no real scene)",
font=R.font(16, True), fill=R.INK)
d.text((12, 33), "Rendered road views; the pixels around six placed objects (front and back cameras) were optimised "
"against the fp32 CPU reference. Boxes: TT output.", font=R.font(12), fill=R.INK2)
for r_, row in enumerate(R.LAYOUT):
for c_, cam in enumerate(row):
x0, y0 = c_ * R.CAM_W, head + r_ * R.CAM_H
canvas.paste(Image.fromarray(cams[cam][:, :, ::-1]), (x0, y0))
lab = cam.replace("CAM_", "")
tw = d.textlength(lab, font=R.font(11, True))
d.rectangle([x0 + 4, y0 + 4, x0 + 10 + tw, y0 + 20], fill=(0, 0, 0))
d.text((x0 + 7, y0 + 5), lab, font=R.font(11, True), fill=R.INK)
yb = head + 2 * R.CAM_H
canvas.paste(Image.fromarray(b_tt[:, :, ::-1]), (0, yb))
canvas.paste(Image.fromarray(b_ref[:, :, ::-1]), (S + 10, yb))
d.text((6, yb + 4), "TT (p150)", font=R.font(11, True), fill=R.INK)
d.text((S + 16, yb + 4), "CPU reference (stored)", font=R.font(11, True), fill=R.INK)
x = 2 * S + 28
y = yb + 8
lines = [f"TT boxes: {len(tt)}; CPU reference: {len(ref)}",
f"matched {len(m)} (same class, < 0.5 m)", f"max |score diff| {err:.4f}", ""]
for a, b in sorted(m, key=lambda p: -p[1]["score"]):
lines.append(f"{b['cls']:<13s} CPU {b['score']:.3f} TT {a['score']:.3f}")
lines += ["", "grey dashed: where the objects were placed", "(Apache-2.0, code/scripts/make_sample.py)"]
for ln in lines:
d.text((x, y), ln, font=R.font(12), fill=R.INK2)
y += 18
out = out_dir / name
canvas.save(out, optimize=True)
print("wrote", out, canvas.size, out.stat().st_size, "B", flush=True)
return {"file": name, "tt": len(tt), "ref": len(ref), "matched": len(m), "max_score_diff": round(err, 4)}
# ------------------------------------------------------------------------------------------------ attribution
ATTRIBUTION = """# Media attribution (bevformer-p150)
Every image here shows outputs of BEVFormer-small (the official `bevformer_small_epoch_24` checkpoint through the
graph of Autoware's TensorRT export, weights `changh95/bevformer-small-weights` @ `0e6d9d9c7b8`) computed by this
port on one Tenstorrent Blackhole p150 (ETH dispatch, 12x10 grid, 1 command queue; fp32 weights, bf16 activations),
and, where a panel says so, by the port's fp32 CPU reference (`code/tt_bevformer/reference`). Rendered by
`code/scripts/make_demo.py` from the TT runs of `code/scripts/run_public_tt.py` (the research workflow's runner on the
public-data inputs of the tt-models workspace, `research/bevformer/public_data`) and from `examples/quickstart.py` on
the shipped sample. Boxes: the detections the Autoware node publishes (score > 0.2), drawn with the length along the
heading. Colours: vehicles blue, pedestrians orange, bicycles / motorcycles aqua, traffic cones / barriers grey (dataviz
reference palette, dark steps); grey dashed outlines are the dataset annotations (the synthetic sample: where its
objects were placed); grey dots are LiDAR points, context only (BEVFormer is camera-only). Renders are at most 960 px
wide and do not crop or zoom onto people or plates.
| file | source | scene / frame | licence (label next to the image) | changes |
|---|---|---|---|---|
{rows}
**PandaSet:** Contains data from PandaSet (Scale AI and Hesai), https://pandaset.org, licensed under CC BY 4.0 and the PandaSet Dataset Terms. Changes: camera images downscaled; predicted 3D boxes, annotation outlines and LiDAR points drawn on top; BEV plots. Scale AI and Hesai do not endorse this work. Cite: P. Xiao et al., PandaSet: Advanced Sensor Suite Dataset for Autonomous Driving, ITSC 2021. The PandaSet Dataset Terms forbid using the data to identify people; the renders are small and do not crop or zoom onto faces or plates.
**nuScenes:** Rendered from the nuScenes dataset, (c) Motional AD Inc., CC BY-NC-SA 4.0 and the nuScenes Terms of Use (https://www.nuscenes.org/terms-of-use). Non-commercial use only; adaptations under the same license. Motional does not endorse this work. Cite: H. Caesar et al., nuScenes: A Multimodal Dataset for Autonomous Driving, CVPR 2020. The rendered images are adaptations and are shared under CC BY-NC-SA 4.0 (non-commercial use only); their file names end in `_NC`.
**Synthetic sample:** generated by `code/scripts/make_sample.py` of this repository (rendered road views and an
optimised test pattern; no third-party data), Apache-2.0.
"""
PS_SRC = "PandaSet (HF mirror georghess/pandaset@e2e123a)"
NS_SRC = "nuScenes v1.0-mini"
PS_LIC = "CC BY 4.0 + PandaSet Dataset Terms"
NS_LIC = "**CC BY-NC-SA 4.0 + nuScenes Terms of Use: non-commercial**"
MOSAIC = "images downscaled to 320x180 tiles; TT boxes, annotation outlines, LiDAR points and a BEV plot drawn"
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--out", type=Path, default=BUNDLE / "media")
ap.add_argument("--ps-tag", default="tt_release", help="results tag of the PandaSet TT run")
ap.add_argument("--ns-tag", default="tt_e2e", help="results tag of the nuScenes TT run")
ap.add_argument("--sample-tt", type=Path, help="the TT /predict body of the shipped sample")
a = ap.parse_args()
a.out.mkdir(parents=True, exist_ok=True)
rows, index = [], []
ps_attr, ns_attr = R.PS_ATTR, R.NS_ATTR
index.append(card("pandaset", "ps019_f30-40", "f40", a.ps_tag, "bevformer_pandaset_019_f40.jpg",
"BEVFormer-small on a Tenstorrent p150: PandaSet 019 frame 40 (San Francisco)",
["Another rig than the training data (nuScenes):", "52 deg front camera, 92 deg side cameras,",
"mounted 0.3-0.5 m higher. History: frames 30 -> 35 -> 40."], ps_attr, a.out))
rows.append(f"| `bevformer_pandaset_019_f40.jpg` | {PS_SRC} | sequence 019 (San Francisco), frame 40 (history 30, 35) | {PS_LIC} | {MOSAIC} |")
index.append(tt_vs_cpu("pandaset", "ps019_f30-40", "f40", a.ps_tag, "bevformer_pandaset_019_tt_vs_cpu.png", a.out,
"ps019_2hz", a.ps_tag))
rows.append(f"| `bevformer_pandaset_019_tt_vs_cpu.png` | {PS_SRC} | sequence 019, frame 40 (BEV only, no camera pixels) and the 16 frames of the 2 Hz sequence (scores) | {PS_LIC} | BEV plots of TT and CPU outputs, annotations, LiDAR points; a score scatter |")
index.append(gif("pandaset", "ps019_2hz", a.ps_tag, "bevformer_pandaset_019_2hz.gif", a.out))
rows.append(f"| `bevformer_pandaset_019_2hz.gif` | {PS_SRC} | sequence 019, frames 0-75 at 2 Hz (16 frames) | {PS_LIC} | front camera downscaled to 534x300 + BEV, TT boxes, 256 colours |")
for sc, city in (("scene-0103", "Boston"), ("scene-0916", "Singapore")):
nm = f"bevformer_nuscenes_{sc}_kf20_NC.jpg"
index.append(card("nuscenes", sc, "kf20", a.ns_tag, nm,
f"BEVFormer-small on a Tenstorrent p150: nuScenes {sc} key-frame 20 ({city})",
["In-domain (trained on nuScenes train; mini_val", "is held out). History: key-frames 0-19.",
NC_NOTE[:47], NC_NOTE[47:]], ns_attr, a.out))
rows.append(f"| `{nm}` | {NS_SRC} | {sc} (mini_val, {city}), key-frame 20 | {NS_LIC} | {MOSAIC} |")
index.append(gif("nuscenes", "scene-0103", a.ns_tag, "bevformer_nuscenes_scene-0103_NC.gif", a.out, step=2))
rows.append(f"| `bevformer_nuscenes_scene-0103_NC.gif` | {NS_SRC} | scene-0103, key-frames 0-38 (every second, 20 frames) | {NS_LIC} | front camera downscaled to 534x300 + BEV, TT boxes, 256 colours |")
if a.sample_tt:
index.append(synthetic(a.sample_tt, "bevformer_synthetic_6cam.png", a.out))
rows.append("| `bevformer_synthetic_6cam.png` | generated (`code/scripts/make_sample.py`) | the shipped sample | Apache-2.0 | TT boxes on the six cameras; BEV of TT and of the stored CPU reference |")
(a.out / "ATTRIBUTION.md").write_text(ATTRIBUTION.replace("{rows}", "\n".join(rows)))
(a.out / "README.md").unlink(missing_ok=True)
print(json.dumps(index, indent=1, default=float))
return 0
if __name__ == "__main__":
sys.exit(main())