"""Кривые дальности обнаружения по размеру предмета. Читает результат `tools/make_benchmark.py` и строит то, что требует ТЗ, п. 5: с какой дистанции предмет уверенно виден и как это зависит от его размера. Считаются две величины: * **вероятность обнаружения в поясе дальности** — доля кадров, в которых подтверждённый трек совпал с истинным положением предмета; * **рабочая дальность** — самая дальняя точка, начиная с которой вероятность устойчиво держится выше порога при сближении. Именно она отвечает на вопрос «за сколько метров поезд увидел препятствие». python tools/plot_benchmark.py --in artifacts/benchmark.json """ from __future__ import annotations import argparse import json import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt # noqa: E402 import numpy as np # noqa: E402 import _bootstrap as B # noqa: F401,E402 EDGES = np.array([0, 15, 25, 40, 55, 70, 90, 110, 135, 160, 190, 230], float) MIN_VISIBLE_RAYS = 2 # меньше — предмет физически не освещён лучами def curve(d: np.ndarray, hit: np.ndarray): """Вероятность обнаружения по поясам дальности.""" idx = np.digitize(d, EDGES) - 1 p, n, centres = [], [], [] for b in range(len(EDGES) - 1): m = idx == b if m.sum() < 5: continue p.append(hit[m].mean()) n.append(int(m.sum())) centres.append(0.5 * (EDGES[b] + EDGES[b + 1])) return np.array(centres), np.array(p), np.array(n) def working_range(centres: np.ndarray, p: np.ndarray, thr: float = 0.5) -> float: """Дальняя граница устойчивого обнаружения. Идём от ближнего пояса к дальнему и останавливаемся там, где вероятность впервые опускается ниже порога: дальше объект уже теряется. """ best = 0.0 for c, v in sorted(zip(centres, p)): if v < thr: break best = c return best def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--in", dest="path", default=str(B.ARTIFACTS / "benchmark.json")) ap.add_argument("--thr", type=float, default=0.5) args = ap.parse_args() rec = json.load(open(args.path, encoding="utf-8")) by_obj: dict[str, list] = {} for r in rec: by_obj.setdefault(r["obj"], []).append(r) print(f"{'предмет':16s} {'площадь':>11s} | {'алгоритм':>8s} {'как есть':>9s} | " f"{'P@50м':>6s} {'P@100м':>7s} {'P@150м':>7s} | {'видим':>7s} | {'набл.':>6s}") print(f"{'':16s} {'':11s} | {'рабочая дальность, м':>18s} | " f"{'при условии видимости':>22s} |") print("-" * 104) fig, axes = plt.subplots(1, 3, figsize=(19, 5.2), dpi=110) for name, rows in sorted(by_obj.items(), key=lambda kv: -_size(kv[0])): d = np.concatenate([np.array(r["d"]) for r in rows]) hit = np.concatenate([np.array(r["hit"]) for r in rows]) rays = np.concatenate([np.array(r["rays"]) for r in rows]) # видимость: в кривом тоннеле предмет за поворотом просто не освещён # лучами, и «непопадание» там ничего не говорит об алгоритме vis = rays >= MIN_VISIBLE_RAYS cv, pv, _ = curve(d, vis.astype(float)) c, p, _ = curve(d, hit) cc, pc_, _ = curve(d[vis], hit[vis]) if vis.any() else (np.array([]),) * 3 if c.size == 0: continue wr_alg = working_range(cc, pc_, args.thr) if cc.size else 0.0 wr_op = working_range(c, p, args.thr) at = {x: float(np.interp(x, cc, pc_)) if cc.size else 0.0 for x in (50, 100, 150)} print(f"{name:16s} {_size(name):8.2f} м² | {wr_alg:8.0f} {wr_op:9.0f} | " f"{at[50]:6.2f} {at[100]:7.2f} {at[150]:7.2f} | {vis.mean():7.1%} | {d.size:6d}") axes[0].plot(cv, pv, marker=".", lw=1.2, label=name) if cc.size: axes[1].plot(cc, pc_, marker="o", ms=3.5, lw=1.4, label=name) axes[2].plot(c, p, marker="o", ms=3.5, lw=1.4, label=name) axes[0].set_title(f"1. Виден ли предмет вообще\n(≥{MIN_VISIBLE_RAYS} лучей попало)") axes[0].set_ylabel("доля кадров") axes[1].set_title("2. Обнаружение, когда предмет виден\n(качество алгоритма)") axes[1].set_ylabel("вероятность обнаружения") axes[2].set_title("3. Обнаружение как есть\n(эксплуатационная величина)") axes[2].set_ylabel("вероятность обнаружения") for ax in axes: ax.axhline(args.thr, color="k", ls="--", lw=0.8) ax.set_xlabel("истинная дистанция до предмета, м") ax.set_ylim(-0.03, 1.03); ax.grid(alpha=0.25) ax.legend(fontsize=7, ncol=2) B.FIGURES.mkdir(parents=True, exist_ok=True) out = B.FIGURES / "detection_range.png" fig.tight_layout(); fig.savefig(out) print("\nсохранено:", out) fp = np.concatenate([np.array(r["fp"]) for r in rec]) print(f"посторонних тревог: {fp.sum():.0f} на {fp.size} наблюдений " f"({fp.mean():.3f} на кадр)") def _size(name: str) -> float: from flyguard.synth import catalogue w, h = catalogue()[name].size return w * h if __name__ == "__main__": main()