File size: 6,451 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
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Run public-data manifests (research/bevdet/public_data, schema ``bevdet_public_manifest/1``) through the TT model's
Python API and write the results in the CPU reference's format, for ``eval_nuscenes.py`` (paper-style mAP / NDS,
indicative only) and for agreement with the CPU results of the same manifests.

    bin/devrun -t 1800 -- python code/scripts/run_public_tt.py \\
        research/bevdet/public_data/nuscenes/manifests/port2hz__scene-0103.json ... --tag tt
    tools/research-venv/bin/python -I research/bevdet/public_data/scripts/eval_nuscenes.py --modes port2hz \\
        --results-dir research/bevdet/public_data/nuscenes/results/tt --backend-tag tt --skip-gt

Every frame of a manifest goes through ``BEVDet.__call__`` exactly like a client request: the six JPEGs of the frame
(``image_root`` + relative paths), its calibration (K of the image as sent, camera -> ego R / t), and the stream
(one fresh stream per manifest; ``reset`` on a flag-0 frame; the ego pose ``ego2global`` for the ego-motion mode
``port2hz``, none for the Autoware modes, whose AlignBEV is the identity; ``single`` / ``single_quirk`` are one reset
frame, i.e. no history, with the scaled K or the node's raw-K quirk). ``score_threshold=0`` returns every vendor box
(score > 0.1), as the CPU results hold. Output: ``<dataset>/results/<tag>/<manifest>.json`` (or ``--out-dir``; boxes
in the vendor convention: bottom-centre z) and a per-manifest agreement summary against ``<dataset>/results/
<manifest>.json`` (the CPU run) at score >= 0.2 (``tests/test_e2e_device.py`` definition).

nuScenes data are CC BY-NC-SA 4.0: local validation only, never shipped (PLAN.md D17 / D18).
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
from pathlib import Path

import numpy as np

HERE = Path(__file__).resolve()
sys.path.insert(0, str(HERE.parents[1]))

EGO_MOTION_MODES = ("port2hz", "train")
RUN_MODES = ("port2hz", "autoware", "autoware_kf", "single", "single_quirk")


def frame_request(m, f, stream_id):
    from tt_bevdet.reference.config import CAMERA_ORDER

    root = Path(m["image_root"])
    cams = {}
    for name, k, rot, t in zip(CAMERA_ORDER, f["K"], f["cam2ego_R"], f["cam2ego_t"]):
        T = np.eye(4)
        T[:3, :3] = np.asarray(rot, np.float64)
        T[:3, 3] = np.asarray(t, np.float64)
        cams[name] = {"intrinsics": k, "T_ref_from_camera": T.tolist()}
    stream = {"id": stream_id, "reset": int(f["flag"]) == 0}
    if m["mode"] in EGO_MOTION_MODES:
        stream["T_world_from_ego"] = f["ego2global"]
    images = {name: str(root / rel) for name, rel in zip(CAMERA_ORDER, f["images"])}
    # the manifests' K is the intrinsic of the image the network sees (1600x900: README of public_data, section 2),
    # so the API must not scale it again when it resizes PandaSet's 1920x1080 images like the node
    return {"images": images, "calibration": {"cameras": cams, "scale_intrinsics": False}, "stream": stream}


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("manifests", nargs="+", type=Path)
    ap.add_argument("--tag", default="tt")
    ap.add_argument("--out-dir", type=Path, default=None,
                    help="write <out-dir>/<dataset>/<manifest>.json instead of <dataset>/results/<tag>/")
    ap.add_argument("--weights-dir", default=None)
    ap.add_argument("--threads", type=int, default=4)
    a = ap.parse_args()
    os.environ.setdefault("OMP_NUM_THREADS", str(a.threads))
    import torch

    torch.set_num_threads(a.threads)
    from tt_bevdet import BEVDet
    from tt_bevdet.host.agreement import agreement, vendor_boxes

    model = BEVDet.from_pretrained(weights_dir=a.weights_dir)
    summary = {}
    try:
        for man in a.manifests:
            m = json.loads(man.read_text())
            if m["mode"] not in RUN_MODES:
                print(f"skip {man.name}: mode {m['mode']} (per-frame time-compensated calibration and BEVDet's "
                      "history padding are not the port's semantics)")
                continue
            ds_dir = man.parents[1]
            out_dir = (a.out_dir / ds_dir.name) if a.out_dir else ds_dir / "results" / a.tag
            out_dir.mkdir(parents=True, exist_ok=True)
            cpu = ds_dir / "results" / man.name
            ref = {fr["name"]: fr for fr in json.loads(cpu.read_text())["frames"]} if cpu.is_file() else {}
            frames, totals, t0 = [], {"ref": 0, "dev": 0, "recalled": 0, "precise": 0}, time.time()
            sid = f"{a.tag}-{man.stem}"
            for f in m["frames"]:
                out = model(**frame_request(m, f, sid), score_threshold=0.0)
                boxes = vendor_boxes(out)
                rec = {k: f[k] for k in ("i", "name", "key_frame", "timestamp_us", "ego2global", "flag") if k in f}
                rec.update({"sample_token": f.get("sample_token"), "boxes": boxes, "n_boxes": len(boxes),
                            "n_boxes_ge_0.2": sum(b["score"] >= 0.2 for b in boxes),
                            "timing_ms": {k: round(v, 2) for k, v in out.timing_ms.items()}})
                frames.append(rec)
                if "boxes" in ref.get(f["name"], {}):           # the CPU run decodes every frame, or key-frames only
                    ag = agreement(boxes, ref[f["name"]]["boxes"])
                    for k in totals:
                        totals[k] += ag[k]
            res = {"manifest": str(man), "mode": m["mode"], "sequence": m["sequence"], "dataset": m["dataset"],
                   "backend": f"tt-{model.MODEL_NAME}", "n_frames": len(frames), "complete": True,
                   "device": model.device_info, "precision": model.knobs.as_dict()["PRECISION"],
                   "frames": frames}
            (out_dir / man.name).write_text(json.dumps(res))
            s = dict(totals)
            if totals["ref"]:
                s["recall"] = totals["recalled"] / totals["ref"]
                s["precision"] = totals["precise"] / max(totals["dev"], 1)
            s["frames"] = len(frames)
            s["s_per_frame"] = round((time.time() - t0) / max(len(frames), 1), 3)
            summary[man.name] = s
            print(f"{man.name}: {json.dumps(s)}", flush=True)
    finally:
        model.close()
    print(json.dumps(summary, indent=1))
    return 0


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