#!/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 the research renderer of ``research/bevdet/public_data/scripts/render_media.py``, whose drawing primitives it reuses: 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 of the demo frames (device; writes //.json): bin/devrun -t 1800 -- python code/scripts/run_public_tt.py --out-dir # 2. renders (research venv: OpenCV, PIL, PyYAML): tools/research-venv/bin/python code/scripts/make_demo.py --results --sample-tt \\ --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, ego-pose mode (``port2hz``); the temporal modes (no history / Autoware node / ego-pose mode, TT, next to the CPU reference); the Autoware K quirk (scaled vs raw K, TT); the TT-vs-CPU agreement (BEV side by side + matched scores); - 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 GIF; - 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: score >= 0.4 on the public frames (the Autoware node publishes >= 0.2; the denser set is unreadable at this size), >= 0.2 on the synthetic sample. """ 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, Optional, Sequence, Tuple import numpy as np ROOT = Path(os.environ.get("TT_MODELS_ROOT", "/home/ubuntu/experiments/tt-models")) PUB = ROOT / "research" / "bevdet" / "public_data" CODE = Path(__file__).resolve().parents[1] BUNDLE = CODE.parent THR = 0.4 TITLE = "BEVDet (Autoware tensorrt_bevdet) on one Tenstorrent Blackhole p150" MODE_DOC = { "port2hz": "ego-pose mode (stream.T_world_from_ego): 8 key-frames of history at 2 Hz, aligned with the ego motion", "autoware": "Autoware node semantics: camera rate (10 Hz), history never aligned (identity ego pose)", "single": "one frame, no history (a reset frame: every history slot = the current map)", } def load_renderer(): """research/bevdet/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("bevdet_render_media", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod R = None # the renderer module (set in main) def results(path: Path) -> Dict[str, Any]: return json.loads(path.read_text()) def frame_of(res: Dict[str, Any], name: str) -> Dict[str, Any]: return next(f for f in res["frames"] if f["name"] == name) def tt_footer(extra: str = "") -> Tuple[str, int, Tuple[int, int, int], bool]: return (f"TT = output of the port on one p150 (ETH dispatch, 12x10 grid, 1 CQ; fp32 weights, bf16 activations).{extra}", 11, R.INK2, False) # ----------------------------------------------------------------------------------------------- PandaSet def pandaset_mosaic(res_dir: Path, out: Path) -> str: res = results(res_dir / "PandaSet" / "port2hz__ps019_2hz.json") if (res_dir / "PandaSet").is_dir() else \ results(res_dir / "pandaset" / "port2hz__ps019_2hz.json") fr = frame_of(res, "f40") cams, pts, gt = R.ps_frame_data("019", 40, fr["ego2global"]) imgs = {c: R.P.load_bgr(cams[c]["path"]) for c in R.P.CAMS} boxes = fr["boxes"] n = len([b for b in boxes if b["score"] >= THR]) header = [(f"{TITLE}: PandaSet sequence 019, frame 40 (San Francisco)", 16, R.INK, True), (f"Tenstorrent p150 output, {MODE_DOC['port2hz']}. Cross-rig input: 1920x1080 cameras resized to " "1600x900 like the node, K scaled.", 12, R.INK2, False)] info = [("What is drawn", 14, R.INK, True), (f"TT detections with score >= {THR:g} ({n} of {len(boxes)} decoded; the node publishes >= 0.2). The " "weights were trained on nuScenes only: this rig (camera heights 1.8-2.1 m, 52 deg front / 92 deg side " "lenses) is out of domain, so ranges are biased (front ~15 % short).", 12, R.INK2, False)] footer = [(R.P.PS_ATTRIBUTION.format(changes="images downscaled to 320x180, 3D boxes, annotation outlines, LiDAR " "points and a BEV plot drawn on top"), 10, R.MUTED, False)] R.compose(cams, imgs, boxes, pts, gt, header, info, footer, THR).save(out, quality=88) return out.name def pandaset_dir(res_dir: Path) -> Path: for name in ("PandaSet", "pandaset"): if (res_dir / name).is_dir(): return res_dir / name raise FileNotFoundError(f"no PandaSet results under {res_dir}") def bev_with_caption(title: str, sub: str, boxes, pts, gt, size=380, rng=51.2): cap = R.text_strip(size, [(title, 12, R.INK, True), (sub, 11, R.INK2, False)]) return R.stack([cap, R.bev_panel(size, rng, pts, gt, boxes)], size) def grid(panels, cols: int, gap: int = 8): from PIL import Image w = max(p.width for p in panels) h = max(p.height for p in panels) rows = math.ceil(len(panels) / cols) out = Image.new("RGB", (cols * w + (cols - 1) * gap, rows * h + (rows - 1) * gap), R.PLANE) for i, p in enumerate(panels): out.paste(p, ((i % cols) * (w + gap), (i // cols) * (h + gap))) return out def pandaset_modes(res_dir: Path, out: Path) -> str: psd = pandaset_dir(res_dir) sets = [("single__ps019_f40", "f40", "TT: no history (reset frame)"), ("autoware__ps019_f30-40", "f40", "TT: Autoware node, 10 Hz, no ego motion"), ("port2hz__ps019_2hz", "f40", "TT: ego-pose mode, 2 Hz history"), (None, "f40", "CPU fp32 reference: ego-pose mode")] panels = [] for man, name, title in sets: res = results(psd / f"{man}.json") if man else results(PUB / "pandaset" / "results" / "port2hz__ps019_2hz.json") fr = frame_of(res, name) cams, pts, gt = R.ps_frame_data("019", 40, fr["ego2global"]) shown = [b for b in fr["boxes"] if b["score"] >= THR] panels.append(bev_with_caption(title, f"{len(shown)} boxes with score >= {THR:g}", shown, pts, gt)) body = grid(panels, 2) hdr = R.text_strip(body.width, [(f"Temporal modes on PandaSet 019 frame 40 (ego at 11 m/s), {TITLE}", 13, R.INK, True), ("Grey outlines: annotations. Blue vehicles, orange pedestrians, aqua two-wheelers, " "grey dots cones / barriers.", 11, R.INK2, False)]) ftr = R.text_strip(body.width, [tt_footer(" The ego pose is what makes the history useful at speed."), (R.P.PS_ATTRIBUTION.format(changes="BEV plots of model outputs, annotations and " "LiDAR points"), 10, R.MUTED, False)]) R.stack([hdr, body, ftr], body.width).save(out, optimize=True) return out.name def pandaset_kquirk(res_dir: Path, out: Path) -> str: psd = pandaset_dir(res_dir) panels = [] for man, title in (("single__ps019_f40", "K scaled to 1600x900 (the port's default)"), ("single_quirk__ps019_f40", "raw 1920x1080 K (the node's quirk)")): fr = results(psd / f"{man}.json")["frames"][0] cams, pts, gt = R.ps_frame_data("019", 40, fr["ego2global"]) shown = [b for b in fr["boxes"] if b["score"] >= THR] n02 = sum(b["score"] >= 0.2 for b in fr["boxes"]) panels.append(bev_with_caption(f"TT: {title}", f"{len(shown)} boxes with score >= {THR:g} ({n02} >= 0.2)", shown, pts, gt, size=400)) body = grid(panels, 2, gap=16) hdr = R.text_strip(body.width, [("The Autoware K quirk on a 1920x1080 rig: PandaSet 019 frame 40, one frame, " "Tenstorrent p150 output", 13, R.INK, True)]) ftr = R.text_strip(body.width, [tt_footer(" The node resizes the image to 1600x900 but keeps the camera's K; " "the port scales K by default (calibration.scale_intrinsics=false " "reproduces the node)."), (R.P.PS_ATTRIBUTION.format(changes="BEV plots of model outputs and annotations"), 10, R.MUTED, False)]) R.stack([hdr, body, ftr], body.width).save(out, optimize=True) return out.name def match_pairs(a: List[Dict[str, Any]], b: List[Dict[str, Any]], max_dist: float = 0.5): """Greedy same-label BEV-centre matching (the e2e gates' rule): a (descending score) -> nearest unused b.""" used = [False] * len(b) pairs = [] for i in sorted(range(len(a)), key=lambda k: -a[k]["score"]): best, bd = -1, max_dist for j, o in enumerate(b): if used[j] or o["label"] != a[i]["label"]: continue d = math.hypot(o["x"] - a[i]["x"], o["y"] - a[i]["y"]) if d <= bd: best, bd = j, d if best >= 0: used[best] = True pairs.append((i, best)) return pairs def score_scatter(pairs_scores: Sequence[Tuple[float, float]], size: int, caption: str): """TT score vs CPU score of matched boxes: one series (blue), the y = x line, thresholds 0.2 dashed.""" from PIL import Image, ImageDraw im = Image.new("RGB", (size, size), R.SURF) d = ImageDraw.Draw(im) pad_l, pad_b, pad_t, pad_r = 44, 40, 34, 14 x0, y0, x1, y1 = pad_l, pad_t, size - pad_r, size - pad_b lo, hi = 0.1, 1.0 def px(v, w): return (x0 + (v - lo) / (hi - lo) * (x1 - x0), y1 - (w - lo) / (hi - lo) * (y1 - y0)) f = R.font(11) for t in (0.2, 0.4, 0.6, 0.8, 1.0): gx, _ = px(t, lo) _, gy = px(lo, t) d.line([(gx, y0), (gx, y1)], fill=R.GRID, width=1) d.line([(x0, gy), (x1, gy)], fill=R.GRID, width=1) d.text((gx - 9, y1 + 5), f"{t:.1f}", font=f, fill=R.MUTED) d.text((8, gy - 7), f"{t:.1f}", font=f, fill=R.MUTED) d.line([px(lo, lo), px(hi, hi)], fill=R.AXIS, width=2) for a, b in pairs_scores: cx, cy = px(b, a) d.ellipse([cx - 3, cy - 3, cx + 3, cy + 3], fill=R.SERIES["vehicle"], outline=R.SURF) d.text((x0, 8), caption, font=R.font(12, True), fill=R.INK) d.text((x1 - 120, y1 + 22), "CPU fp32 score", font=f, fill=R.INK2) d.text((x0 + 4, y0 + 4), "TT score", font=f, fill=R.INK2) return im def pandaset_tt_vs_cpu(res_dir: Path, out: Path) -> Dict[str, Any]: """BEV of the TT boxes and of the CPU reference (ego-pose mode, frame 40), and TT vs CPU scores of the matched boxes (every frame of the 2 Hz run).""" psd = pandaset_dir(res_dir) tt = results(psd / "port2hz__ps019_2hz.json") cpu = results(PUB / "pandaset" / "results" / "port2hz__ps019_2hz.json") panels = [] for res, title in ((tt, "TT (Tenstorrent p150)"), (cpu, "CPU fp32 reference")): fr = frame_of(res, "f40") cams, pts, gt = R.ps_frame_data("019", 40, fr["ego2global"]) shown = [b for b in fr["boxes"] if b["score"] >= 0.2] panels.append(bev_with_caption(title, f"{len(shown)} boxes with score >= 0.2 (what the node publishes)", shown, pts, gt, size=300)) pairs, n_ref, n_hit = [], 0, 0 cpu_frames = {f["name"]: f for f in cpu["frames"]} for f in tt["frames"]: c = cpu_frames.get(f["name"]) if not c or "boxes" not in c: continue ref_pub = [b for b in c["boxes"] if b["score"] >= 0.2] n_ref += len(ref_pub) m = match_pairs(ref_pub, f["boxes"]) n_hit += len(m) pairs += [(f["boxes"][j]["score"], ref_pub[i]["score"]) for i, j in m] diffs = sorted(abs(a - b) for a, b in pairs) err = diffs[-1] if diffs else 0.0 p99 = float(np.percentile(diffs, 99)) if diffs else 0.0 scatter = score_scatter(pairs, 300, f"Matched boxes, all {len(tt['frames'])} frames") body = grid(panels + [scatter], 3, gap=8) hdr = R.text_strip(body.width, [("TT vs the CPU fp32 reference: PandaSet 019, ego-pose mode at 2 Hz", 13, R.INK, True), (f"{n_hit} of {n_ref} CPU boxes with score >= 0.2 have a same-class TT box within " f"0.5 m (recall {n_hit / max(n_ref, 1):.3f}); |score difference| of the matched " f"pairs: p99 {p99:.4f}, max {err:.3f}. Left / centre: frame 40.", 11, R.INK2, False)]) ftr = R.text_strip(body.width, [tt_footer(), (R.P.PS_ATTRIBUTION.format( changes="BEV plots of model outputs, annotations and LiDAR points; a score scatter"), 10, R.MUTED, False)]) R.stack([hdr, body, ftr], body.width).save(out, optimize=True) return {"file": out.name, "matched": n_hit, "reference": n_ref, "max_score_err": err, "p99_score_err": p99} # ----------------------------------------------------------------------------------------------- nuScenes def nuscenes_dir(res_dir: Path) -> Path: for name in ("nuScenes", "nuscenes"): if (res_dir / name).is_dir(): return res_dir / name raise FileNotFoundError(f"no nuScenes results under {res_dir}") def nuscenes_mosaic(res_dir: Path, scene: str, kf: int, out: Path) -> str: res = results(nuscenes_dir(res_dir) / f"port2hz__{scene}.json") fr = [f for f in res["frames"] if f.get("key_frame", True)][kf] rec, cams, pts, gt = R.ns_frame_data(scene, kf) imgs = {c: R.P.load_bgr(cams[c]["path"]) for c in R.P.CAMS} boxes = fr["boxes"] n = len([b for b in boxes if b["score"] >= THR]) place = {"scene-0103": "Boston Seaport", "scene-0916": "Singapore Queenstown"}[scene] header = [(f"{TITLE}: nuScenes {scene} ({place}, mini_val), key-frame {kf}", 16, R.INK, True), (f"Tenstorrent p150 output, {MODE_DOC['port2hz']}. In-domain data (the weights were trained on " "nuScenes). CC BY-NC-SA 4.0: non-commercial.", 12, R.INK2, False)] info = [("What is drawn", 14, R.INK, True), (f"TT detections with score >= {THR:g} ({n} of {len(boxes)} decoded; the Autoware node publishes " ">= 0.2). Boxes are in the ego frame at the CAM_FRONT stamp, projected into each camera with its own " "pose.", 12, R.INK2, False)] footer = [(R.P.NS_ATTRIBUTION + ". Changes: images downscaled to 320x180, 3D boxes, annotation outlines, LiDAR " "points and a BEV plot drawn on top. Label: CC BY-NC-SA 4.0, non-commercial.", 10, R.MUTED, False)] R.compose(cams, imgs, boxes, pts, gt, header, info, footer, THR).save(out, quality=88) return out.name def nuscenes_gif(res_dir: Path, scene: str, out: Path) -> str: from PIL import Image res = results(nuscenes_dir(res_dir) / f"port2hz__{scene}.json") frames = [f for f in res["frames"] if f.get("key_frame", True)] ims = [] for kf, fr in enumerate(frames): rec, cams, pts, gt = R.ns_frame_data(scene, kf) shown = [b for b in fr["boxes"] if b["score"] >= THR] img = R.P.load_bgr(cams["CAM_FRONT"]["path"]) tile = R.camera_tile(img, cams["CAM_FRONT"]["K"], cams["CAM_FRONT"]["T_cam_ego"], shown, 448, 252, "front") bev = R.bev_panel(252, 51.2, pts, gt, shown) row = Image.new("RGB", (700, 252), R.PLANE) row.paste(tile, (0, 0)) row.paste(bev, (448, 0)) hdr = R.text_strip(700, [(f"BEVDet on a Tenstorrent p150: nuScenes {scene}, key-frame {kf:02d} (2 Hz), ego-pose " f"mode, TT score >= {THR:g}. CC BY-NC-SA 4.0 (c) Motional, non-commercial; see " "media/ATTRIBUTION.md", 10, R.INK2, False)], pad=4) ims.append(R.stack([hdr, row], 700).quantize(colors=96, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)) ims[0].save(out, save_all=True, append_images=ims[1:], duration=500, loop=0, optimize=True) return out.name # ------------------------------------------------------------------------------------------ synthetic sample def to_vendor(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """/predict rows (gravity-centre z) -> vendor box dicts (bottom-centre z) for the renderer.""" out = [] for d in rows: (x, y, z), (l, w, h) = d["center"], d["size"] out.append({"x": x, "y": y, "z": z - h / 2, "l": l, "w": w, "h": h, "r": d["yaw"], "score": d["score"], "label": d["label_id"]}) return out def synthetic(tt_body: Path, out: Path) -> Dict[str, Any]: from PIL import Image sample = CODE / "tt_bevdet" / "samples" / "synthetic_6cam.json" man = json.loads(sample.read_text()) calib = json.loads((CODE / "tt_bevdet" / "calib" / "synthetic_6cam.json").read_text()) ref = json.loads((sample.parent / "synthetic_6cam.reference.json").read_text()) tt = json.loads(tt_body.read_text()) tt_boxes = [b for b in to_vendor(tt["detections"]) if b["score"] >= 0.2] ref_boxes = [b for b in to_vendor(ref["detections"]) if b["score"] >= 0.2] w = 960 tw, th = w // 3, (w // 3) * 9 // 16 names = {"CAM_FRONT_LEFT": "front left", "CAM_FRONT": "front", "CAM_FRONT_RIGHT": "front right", "CAM_BACK_LEFT": "back left", "CAM_BACK": "back", "CAM_BACK_RIGHT": "back right"} mos = Image.new("RGB", (w, 2 * th), R.PLANE) for i, c in enumerate(R.P.CAMS): bgr = np.asarray(Image.open(sample.parent / man["images"][c]).convert("RGB"))[:, :, ::-1] K = np.asarray(calib["cameras"][c]["intrinsics"], np.float64) T = np.linalg.inv(np.asarray(calib["cameras"][c]["T_ref_from_camera"], np.float64)) mos.paste(R.camera_tile(np.ascontiguousarray(bgr), K, T, tt_boxes, tw, th, names[c]), ((i % 3) * tw, (i // 3) * th)) targets = [(t["center"][0], t["center"][1], t["size"][0], t["size"][1], t["yaw"]) for t in man["targets"]] p_tt = bev_with_caption("TT (Tenstorrent p150)", f"{len(tt_boxes)} boxes with score >= 0.2", tt_boxes, None, targets, size=470, rng=30.0) p_ref = bev_with_caption("CPU fp32 reference (stored)", f"{len(ref_boxes)} boxes with score >= 0.2", ref_boxes, None, targets, size=470, rng=30.0) mid = grid([p_tt, p_ref], 2, gap=20) pairs = match_pairs(ref_boxes, to_vendor(tt["detections"])) errs = [abs(ref_boxes[i]["score"] - to_vendor(tt["detections"])[j]["score"]) for i, j in pairs] hdr = R.text_strip(w, [("The shipped sample: a synthetic six-camera test pattern (code/tt_bevdet/samples/" "synthetic_6cam.json)", 15, R.INK, True), ("Rendered road views whose network-visible pixels were optimised against the CPU reference " "so that the network reports eight objects (grey outlines: where they were placed). No real " f"scene. TT boxes drawn. {len(pairs)} of {len(ref_boxes)} reference boxes matched within " f"0.5 m, max |score difference| {max(errs, default=0.0):.4f}.", 11, R.INK2, False)]) ftr = R.text_strip(w, [tt_footer(" Generated data (code/scripts/make_sample.py), Apache-2.0."), ("Blue vehicles, orange pedestrians, aqua two-wheelers, grey dots cones / barriers.", 10, R.MUTED, False)]) R.stack([hdr, mos, mid, ftr], w).save(out, optimize=True) return {"file": out.name, "matched": len(pairs), "reference": len(ref_boxes), "max_score_err": max(errs, default=0.0)} ATTRIBUTION = """# Media attribution (bevdet-p150) Every image here shows outputs of the BEVDet network Autoware deploys (`bevdet_one_lt_d.onnx`, AutowareFoundation/tensorrt_bevdet v1.0) 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_bevdet/reference`). Rendered by `code/scripts/make_demo.py` from the TT runs of `code/scripts/run_public_tt.py` on the public-data manifests of the tt-models workspace (`research/bevdet/public_data`). Colours: vehicles blue, pedestrians orange, bicycles / motorcycles aqua, traffic cones / barriers grey dots (dataviz reference palette, dark steps); grey hairline outlines are the dataset annotations (the synthetic sample: where its objects were placed); LiDAR points are context only (BEVDet 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:** {ps} **nuScenes:** {ns}. The rendered images are adaptations and are shared under CC BY-NC-SA 4.0 (non-commercial use only). **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. """ def main() -> int: global R ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("--results", type=Path, required=True, help="run_public_tt.py --out-dir") ap.add_argument("--sample-tt", type=Path, required=True, help="TT /predict body of the shipped sample " "(smoke protocol)") ap.add_argument("--out", type=Path, default=BUNDLE / "media") a = ap.parse_args() R = load_renderer() a.out.mkdir(parents=True, exist_ok=True) rows, report = [], {} ps_lic, ns_lic = "CC BY 4.0 + PandaSet Dataset Terms", "**CC BY-NC-SA 4.0 + nuScenes Terms of Use: non-commercial**" f = pandaset_mosaic(a.results, a.out / "bevdet_pandaset_019_f40.jpg") rows.append(f"| `{f}` | PandaSet (HF mirror georghess/pandaset@e2e123a) | sequence 019 (San Francisco), frame 40 | " f"{ps_lic} | images downscaled to 320x180 tiles; TT boxes, annotation outlines, LiDAR points and a BEV " "plot drawn |") f = pandaset_modes(a.results, a.out / "bevdet_pandaset_019_f40_modes.png") rows.append(f"| `{f}` | PandaSet | sequence 019, frame 40 (BEV only, no camera pixels) | {ps_lic} | BEV plots of " "TT and CPU outputs, annotations, LiDAR points |") f = pandaset_kquirk(a.results, a.out / "bevdet_pandaset_019_f40_kquirk.png") rows.append(f"| `{f}` | PandaSet | sequence 019, frame 40 (BEV only) | {ps_lic} | BEV plots of TT outputs and " "annotations |") report["pandaset_tt_vs_cpu"] = r = pandaset_tt_vs_cpu(a.results, a.out / "bevdet_pandaset_019_tt_vs_cpu.png") rows.append(f"| `{r['file']}` | PandaSet | sequence 019, frames 0-40 at 2 Hz (BEV of frame 40 + scores) | {ps_lic} | " "BEV plots of TT and CPU outputs, annotations, LiDAR points; a score scatter |") for scene in ("scene-0103", "scene-0916"): f = nuscenes_mosaic(a.results, scene, 20, a.out / f"bevdet_nuscenes_{scene}_kf20_NC.jpg") rows.append(f"| `{f}` | nuScenes v1.0-mini | {scene} (mini_val), key-frame 20 | {ns_lic} | images downscaled " "to 320x180 tiles; TT boxes, annotation outlines, LiDAR points and a BEV plot drawn |") f = nuscenes_gif(a.results, "scene-0103", a.out / "bevdet_nuscenes_scene-0103_NC.gif") rows.append(f"| `{f}` | nuScenes v1.0-mini | scene-0103, all 40 key-frames (2 Hz) | {ns_lic} | front camera " "downscaled to 448x252 + BEV, TT boxes, 96 colours |") report["synthetic"] = r = synthetic(a.sample_tt, a.out / "bevdet_synthetic_6cam.png") rows.append(f"| `{r['file']}` | 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 |") ps_text = R.P.PS_ATTRIBUTION.format(changes="camera images downscaled; predicted 3D boxes, annotation outlines and " "LiDAR points drawn on top; BEV plots") ps_text += (" 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.") (a.out / "ATTRIBUTION.md").write_text(ATTRIBUTION.format(rows="\n".join(rows), ps=ps_text, ns=R.P.NS_ATTRIBUTION)) sizes = {p.name: p.stat().st_size for p in sorted(a.out.iterdir()) if p.is_file()} print(json.dumps({"report": report, "sizes": sizes}, indent=1)) return 0 if __name__ == "__main__": sys.exit(main())