Brainrot_Muxa/tools/plot_benchmark.py

160 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Кривые дальности обнаружения по размеру предмета.
Читает результат `tools/make_benchmark.py` и строит то, что требует ТЗ, п. 5:
с какой дистанции предмет уверенно виден и как это зависит от его размера.
Считаются две величины:
* **вероятность обнаружения в поясе дальности** — доля кадров, в которых
подтверждённый трек совпал с истинным положением предмета;
* **рабочая дальность** — самая дальняя точка, начиная с которой вероятность
устойчиво держится выше порога при сближении. Именно она отвечает на вопрос
«за сколько метров поезд увидел препятствие».
python tools/plot_benchmark.py --in artifacts/benchmark.json
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
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
NL = chr(10)
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 _funnel(rec: list[dict], obj: str = "человек_стоя") -> None:
"""Где теряется предмет: лучи → кандидат → трек → решение."""
rows = [r for r in rec if r["obj"] == obj]
if not rows:
return
d = np.concatenate([np.array(r["d"]) for r in rows])
rays = np.concatenate([np.array(r["rays"]) for r in rows])
cand = np.concatenate([np.array(r["cand"]) for r in rows])
trk = np.concatenate([np.array(r["trk"]) for r in rows])
hit = np.concatenate([np.array(r["hit"]) for r in rows])
vis = rays >= MIN_VISIBLE_RAYS
print(f"{NL}воронка потерь для «{obj}», доля от ВИДИМЫХ наблюдений:")
print(f"{'полоса':>10}{'видимых':>9}{'кандидат':>10}{'трек':>8}{'решение':>9}{'нет кандидата':>15}{'трек без решения':>18}")
for lo, hi in ((6, 30), (30, 50), (50, 70), (70, 90), (90, 120), (120, 160)):
m = vis & (d >= lo) & (d < hi)
if m.sum() < 30:
continue
c, k, h = cand[m].mean(), trk[m].mean(), hit[m].mean()
print(f"{f'{lo}-{hi} м':>10}{int(m.sum()):9d}{c:10.2f}{k:8.2f}{h:9.2f}{1 - c:15.2f}{max(k - h, 0.0):18.2f}")
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)
ap.add_argument("--out", default="", help="куда сохранить рисунок")
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 = Path(args.out) if args.out else B.FIGURES / "detection_range.png"
fig.tight_layout(); fig.savefig(out)
print("\nсохранено:", out)
if all("cand" in r for r in rec):
_funnel(rec)
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()