Lidar_Muxa/tools/plot_benchmark.py
Данил Омелечко b8e95eccbe ML-ядро детектора: конвейер на схемах мозга дрозофилы
Ретина, ламина, медулла, лобула, грибовидное тело, веерное тело,
центральный комплекс, нисходящие нейроны. Обучение памяти тоннеля и
считывания MBON, оценка leave-one-bag-out, полигон дальности, 24 теста.

Реальный объект на 55 м — 98.9 % кадров, ложных 7.5 трека на км,
кадр обрабатывается за 33 мс на CPU.
2026-09-21 17:24:25 +03:00

131 lines
6 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
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()