File size: 22,551 Bytes
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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())