File size: 25,479 Bytes
77e3c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#!/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 <results>/<dataset>/<manifest>.json):
    bin/devrun -t 1800 -- python code/scripts/run_public_tt.py <manifests> --out-dir <results>
    # 2. renders (research venv: OpenCV, PIL, PyYAML):
    tools/research-venv/bin/python code/scripts/make_demo.py --results <results> --sample-tt <synthetic tt 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, 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())