File size: 13,965 Bytes
ac1609a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Render the model card's demo media (``media/``) from the TT (p150) outputs. CPU only; research venv (numpy, PIL,
matplotlib, OpenCV); the TT outputs come from a device run (``run_public_tt.py``, ``tests/test_e2e_device.py``).

    PY=/home/ubuntu/experiments/tt-models/tools/research-venv/bin/python
    $PY code/scripts/make_demo.py datasets --ps019 <tt>/ps019/detections.json --ns0103 <tt>/ns0103/detections.json
    $PY code/scripts/make_demo.py tt-vs-cpu --ps019 <tt>/ps019/detections.json --f1 <tt>/ps019/f1.json
    $PY code/scripts/make_demo.py synthetic --tt-body <logs>/e2e_shipped_synthetic_5cam.json

- ``datasets``: camera mosaics (the five network crops with the projected TT boxes + a BEV tile), large BEV plots and a
  sequence GIF of PandaSet 019 and nuScenes scene-0103 through ``research/streampetr/public_data/scripts/
  render_media.py`` (its privacy blur of heads and plates, its attribution footer); the nuScenes files get the
  ``_NC`` (non-commercial) suffix. Never shipped as data: only the renders are copied into ``media/``.
- ``tt-vs-cpu``: PandaSet 019 frame 40 in a bird's-eye view, TT boxes against the CPU fp32 reference (ONNX Runtime)
  boxes, and the per-frame detection F1 of the 80-frame free-running sequence (no camera pixels).
- ``synthetic``: the shipped synthetic sample: its five camera images with the TT boxes projected, and a BEV with the
  TT boxes, the stored CPU reference and the objects the pattern was made for (generated data, Apache-2.0).
"""
from __future__ import annotations

import argparse
import json
import math
import os
import shutil
import subprocess
import sys
from pathlib import Path

import numpy as np

CODE = Path(__file__).resolve().parents[1]
BUNDLE = CODE.parent
MEDIA = BUNDLE / "media"
ROOT = Path(os.environ.get("TT_MODELS_ROOT", "/home/ubuntu/experiments/tt-models"))
PUBLIC = ROOT / "research" / "streampetr" / "public_data"
RENDER = PUBLIC / "scripts" / "render_media.py"
CLASSES = ["CAR", "TRUCK", "BUS", "BICYCLE", "PEDESTRIAN"]
# the reference categorical palette, dark-surface steps, fixed class order (as render_media.py)
CLS_HEX = ["#3987E5", "#D95926", "#199E70", "#C98500", "#D55181"]
SURFACE, INK, INK2, GRID = "#1A1A19", "#FFFFFF", "#C3C2B7", "#45453F"
TT_LABEL = "TT p150 (ETH 12x10, 1 CQ)"


def _run(args):
    print("+", " ".join(str(a) for a in args), flush=True)
    subprocess.run([str(a) for a in args], check=True)


# ------------------------------------------------------------------------------------------------ datasets

def datasets(a) -> int:
    py = sys.executable
    tmp = Path(a.work)
    tmp.mkdir(parents=True, exist_ok=True)
    MEDIA.mkdir(exist_ok=True)
    lab = f"{TT_LABEL}"
    if a.ps019:
        _run([py, "-I", RENDER, "ps019", "--frames", "40", "--bev", "--det", a.ps019, "--source-label", lab,
              "--tag", "tt", "--out", tmp])
        _run([py, "-I", RENDER, "ps019", "--gif", "0:80:2", "--gif-tile", "240", "--anim-formats", "gif",
              "--det", a.ps019, "--source-label", lab, "--tag", "tt", "--out", tmp])
        shutil.copy(tmp / "streampetr_ps019_f0040_mosaic_tt.jpg", MEDIA / "streampetr_pandaset_019_f40_tt.jpg")
        shutil.copy(tmp / "streampetr_ps019_f0040_bev_tt.png", MEDIA / "streampetr_pandaset_019_f40_bev_tt.png")
        shutil.copy(tmp / "streampetr_ps019_seq_tt.gif", MEDIA / "streampetr_pandaset_019_seq_tt.gif")
    if a.ns0103:
        # sweep 117 of the CAM_FRONT sequence = key-frame 20 of scene-0103
        _run([py, "-I", RENDER, "ns0103", "--frames", "117", "--bev", "--det", a.ns0103, "--source-label", lab,
              "--tag", "tt", "--out", tmp])
        shutil.copy(tmp / "streampetr_ns0103_f0117_mosaic_tt.jpg",
                    MEDIA / "streampetr_nuscenes_scene-0103_kf20_tt_NC.jpg")
        shutil.copy(tmp / "streampetr_ns0103_f0117_bev_tt.png",
                    MEDIA / "streampetr_nuscenes_scene-0103_kf20_bev_tt_NC.png")
    for p in sorted(MEDIA.iterdir()):
        print(p.name, p.stat().st_size)
    return 0


# ------------------------------------------------------------------------------------------------ helpers

def _corners(x, y, yaw, l, w):
    c, s = math.cos(yaw), math.sin(yaw)
    pts = np.array([[0.5, 0.5], [0.5, -0.5], [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]]) * [l, w]
    return pts @ np.array([[c, s], [-s, c]]) + [x, y]


def _research_rows(dets):
    """research-format dets -> (label, x, y, yaw, l, w, score)."""
    return [(int(d["label"]), d["x"], d["y"], d["yaw"], d["dim_x"], d["dim_y"], d["score"]) for d in dets]


def _body_rows(body):
    return [(int(d["label_id"]), d["center"][0], d["center"][1], d["yaw"], d["size"][0], d["size"][1], d["score"])
            for d in body["detections"]]


def _bev_axes(ax, lim, title):
    ax.set_facecolor(SURFACE)
    ax.set_xlim(lim, -lim)                      # base_link y (left) on a reversed horizontal axis: left = +y
    ax.set_ylim(-lim * 0.45, lim)
    ax.set_aspect("equal")
    for r in range(10, int(lim * 1.5), 10):
        t = np.linspace(0, 2 * np.pi, 200)
        ax.plot(r * np.sin(t), r * np.cos(t), color=GRID, lw=0.6, zorder=0)
        ax.text(-0.3, r + 0.3, f"{r} m", color=INK2, fontsize=7, zorder=1, clip_on=True)
    ax.plot([0], [0], marker=(3, 0, 0), color=INK, ms=9, zorder=5)
    ax.tick_params(colors=INK2, labelsize=7)
    for s in ax.spines.values():
        s.set_color(GRID)
    ax.set_xlabel("base_link y [m] (left)", color=INK2, fontsize=8)
    ax.set_ylabel("base_link x [m] (forward)", color=INK2, fontsize=8)
    ax.set_title(title, color=INK, fontsize=10, loc="left")


def _draw_boxes(ax, rows, solid, lw=1.6, zorder=3):
    for lab, x, y, yaw, l, w, _ in rows:
        c = _corners(x, y, yaw, l, w)
        ax.plot(c[:, 1], c[:, 0], color=CLS_HEX[lab] if solid else INK2, lw=lw if solid else 1.0,
                ls="-" if solid else (0, (3, 2)), zorder=zorder)


def _legend(ax, extra):
    from matplotlib.lines import Line2D

    h = [Line2D([], [], color=CLS_HEX[i], lw=2, label=n) for i, n in enumerate(CLASSES)] + extra
    lg = ax.legend(handles=h, loc="lower left", fontsize=7, facecolor=SURFACE, edgecolor=GRID, labelcolor=INK2,
                   ncol=2)
    lg.set_zorder(10)


# ------------------------------------------------------------------------------------------------ tt vs cpu

def tt_vs_cpu(a) -> int:
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from matplotlib.lines import Line2D

    tt = {int(r["frame"]): r["dets"] for r in json.loads(Path(a.ps019).read_text())["results"]}
    cpu = {int(r["frame"]): r["dets"] for r in json.loads((PUBLIC / "golden" / "ps019" / "detections.json")
                                                             .read_text())["results"]}
    f1 = json.loads(Path(a.f1).read_text())
    frames = f1["frames"]
    k = 40
    fig = plt.figure(figsize=(10.4, 6.6), facecolor=SURFACE)
    gs = fig.add_gridspec(2, 2, width_ratios=[1.15, 1], height_ratios=[1, 1], wspace=0.22, hspace=0.42)
    ax = fig.add_subplot(gs[:, 0])
    _bev_axes(ax, 45, f"PandaSet 019 frame 40: TT {len(tt[k])} vs CPU {len(cpu[k])} boxes")
    ax.set_ylim(-25, 50)
    _draw_boxes(ax, _research_rows(cpu[k]), solid=False, zorder=2)
    _draw_boxes(ax, _research_rows(tt[k]), solid=True)
    _legend(ax, [Line2D([], [], color=INK2, lw=1, ls=(0, (3, 2)), label="CPU fp32 reference")])
    fr = [x["frame"] for x in frames]
    for row, (key, name, ref) in enumerate((("f1", "detection F1 vs CPU (1 m, same class)", None),
                                            ("score_err_max", "max |score difference| of matched boxes", None))):
        b = fig.add_subplot(gs[row, 1])
        b.set_facecolor(SURFACE)
        v = [x.get(key, np.nan) for x in frames]
        b.plot(fr, v, color=CLS_HEX[0], lw=2)
        b.set_title(name, color=INK, fontsize=10, loc="left")
        b.tick_params(colors=INK2, labelsize=7)
        for s in b.spines.values():
            s.set_color(GRID)
        b.grid(color=GRID, lw=0.5)
        b.set_xlabel("frame (10 Hz, one stream from frame 0)", color=INK2, fontsize=8)
        if key == "f1":
            b.set_ylim(0.5, 1.02)
            m = float(np.mean(v))
            b.axhline(m, color=INK2, lw=1, ls=(0, (3, 2)))
            b.text(fr[-1], m - 0.05, f"mean {m:.3f}", color=INK2, fontsize=8, ha="right")
    fig.text(0.01, 0.015, "TT = this port on one p150 (ETH 12x10, 1 CQ), free-running (its own memory); CPU = the "
             "fp32 reference of the same network (ONNX Runtime), free-running. Boxes only, no camera pixels.\n"
             "Contains data from PandaSet (Scale AI and Hesai), https://pandaset.org, CC BY 4.0 + PandaSet Dataset "
             "Terms. Changes: model outputs drawn in a bird's-eye view. Not endorsed by Scale AI or Hesai.",
             color=INK2, fontsize=7)
    out = MEDIA / "streampetr_pandaset_019_tt_vs_cpu.png"
    fig.savefig(out, dpi=110, facecolor=SURFACE)
    print("wrote", out, out.stat().st_size)
    return 0


# ------------------------------------------------------------------------------------------------ synthetic

def synthetic(a) -> int:
    import cv2
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from matplotlib.lines import Line2D
    from PIL import Image

    sys.path.insert(0, str(CODE))
    from tt_streampetr.server.smoke_test import REFERENCE_GATES  # noqa: F401  (same rule as the smoke)

    samples = CODE / "tt_streampetr" / "samples"
    man = json.loads((samples / "synthetic_5cam.json").read_text())
    cal = json.loads((CODE / "tt_streampetr" / "calib" / "synthetic_5cam.json").read_text())
    ref = json.loads((samples / "synthetic_5cam.reference.json").read_text())
    rec = json.loads(Path(a.tt_body).read_text())
    tt = rec.get("body", rec)
    rows_tt, rows_ref = _body_rows(tt), _body_rows(ref)
    order = ["CAM_FRONT_LEFT", "CAM_FRONT", "CAM_FRONT_RIGHT", "CAM_BACK_LEFT", "CAM_BACK_RIGHT"]
    tiles = {}
    for cam in order:
        img = np.asarray(Image.open(samples / man["images"][cam]).convert("RGB")).copy()
        K = np.asarray(cal["cameras"][cam]["intrinsics"], np.float64)
        Tinv = np.linalg.inv(np.asarray(cal["cameras"][cam]["T_ref_from_camera"], np.float64))
        for d in tt["detections"]:
            (x, y, z), (l, w, h), yaw = d["center"], d["size"], d["yaw"]
            c2 = _corners(x, y, yaw, l, w)[:4]
            pts = np.array([[px, py, z + dz] for dz in (-h / 2, h / 2) for px, py in c2])
            pc = pts @ Tinv[:3, :3].T + Tinv[:3, 3]
            if (pc[:, 2] < 0.5).any():
                continue
            uv = pc @ K.T
            uv = uv[:, :2] / uv[:, 2:3]
            col = tuple(int(CLS_HEX[d["label_id"]][i:i + 2], 16) for i in (1, 3, 5))
            for i, j in [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6),
                         (3, 7)]:
                p, q = np.round(uv[i]).astype(int), np.round(uv[j]).astype(int)
                cv2.line(img, tuple(p), tuple(q), (0, 0, 0), 4, cv2.LINE_AA)
                cv2.line(img, tuple(p), tuple(q), col, 2, cv2.LINE_AA)
        tiles[cam] = img
    fig = plt.figure(figsize=(12, 6.4), facecolor=SURFACE)
    gs = fig.add_gridspec(2, 3, wspace=0.04, hspace=0.12)
    pos = {"CAM_FRONT_LEFT": (0, 0), "CAM_FRONT": (0, 1), "CAM_FRONT_RIGHT": (0, 2), "CAM_BACK_LEFT": (1, 0),
           "CAM_BACK_RIGHT": (1, 2)}
    for cam, (r, c) in pos.items():
        ax = fig.add_subplot(gs[r, c])
        ax.imshow(tiles[cam])
        ax.set_axis_off()
        ax.set_title(cam, color=INK2, fontsize=8, loc="left")
    ax = fig.add_subplot(gs[1, 1])
    _bev_axes(ax, 22, "")
    ax.set_xlabel("")
    ax.set_ylabel("")
    for t in man["targets"]:
        ax.plot(t["center"][1], t["center"][0], marker="x", color=INK2, ms=7, mew=1.2, zorder=4)
    _draw_boxes(ax, rows_ref, solid=False, zorder=2)
    _draw_boxes(ax, rows_tt, solid=True)
    ax.set_ylim(-20, 18)
    ax.set_xlim(20, -20)
    _legend(ax, [Line2D([], [], color=INK2, lw=1, ls=(0, (3, 2)), label="stored CPU ref"),
                 Line2D([], [], color=INK2, marker="x", lw=0, label="pattern target")])
    ax.get_legend().remove()
    fig.legend(handles=[Line2D([], [], color=CLS_HEX[i], lw=2, label=n) for i, n in enumerate(CLASSES)]
               + [Line2D([], [], color=INK2, lw=1, ls=(0, (3, 2)), label="stored CPU reference"),
                  Line2D([], [], color=INK2, marker="x", lw=0, label="target of the pattern")],
               loc="lower center", ncol=7, fontsize=8, facecolor=SURFACE, edgecolor=GRID, labelcolor=INK2)
    fig.suptitle(f"streampetr-p150 shipped synthetic sample: {TT_LABEL}, {tt['num_detections']} boxes "
                 f"(stored CPU reference: {ref['num_detections']})", color=INK, fontsize=11, x=0.01, ha="left")
    fig.text(0.01, 0.925, "generated test pattern (code/scripts/make_sample.py), Apache-2.0; base_link, x forward",
             color=INK2, fontsize=8, ha="left")
    out = MEDIA / "streampetr_synthetic_5cam_tt.png"
    fig.savefig(out, dpi=100, facecolor=SURFACE)
    print("wrote", out, out.stat().st_size)
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    sub = ap.add_subparsers(dest="cmd", required=True)
    d = sub.add_parser("datasets")
    d.add_argument("--ps019")
    d.add_argument("--ns0103")
    d.add_argument("--work", default=str(ROOT / "logs" / "streampetr" / "release" / "render"))
    t = sub.add_parser("tt-vs-cpu")
    t.add_argument("--ps019", required=True)
    t.add_argument("--f1", required=True)
    s = sub.add_parser("synthetic")
    s.add_argument("--tt-body", required=True, help="a JSON with the TT /predict body (or {'body': ...})")
    a = ap.parse_args()
    return {"datasets": datasets, "tt-vs-cpu": tt_vs_cpu, "synthetic": synthetic}[a.cmd](a)


if __name__ == "__main__":
    sys.exit(main())