"""Сверка калибровки решётки по данным с паспортной таблицей каналов. Решение нигде не использует паспортные углы: решётка восстанавливается из самих облаков точек. Но раз таблица из руководства есть, ею стоит воспользоваться как **независимой проверкой** — совпадение показывает, что выпрямление образа и оценка углов сделаны правильно, а не подогнаны. python tools/validate_calibration.py --bag data/for_hackathon/roundT_doubleT """ from __future__ import annotations import argparse import csv 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 from flyguard.bag import Bag, find_bags # noqa: E402 from flyguard.retina import ScanLayout # noqa: E402 TABLE = B.PKG / "flyguard" / "data" / "pandar128_channels.csv" def load_table() -> dict[str, np.ndarray]: rows = list(csv.DictReader(open(TABLE, encoding="utf-8"))) rows.sort(key=lambda r: int(r["channel"])) return { "elevation": np.array([float(r["elevation_deg"]) for r in rows]), "az_offset": np.array([float(r["az_offset_deg"]) for r in rows]), "max_range": np.array([float(r["max_range_10pct_m"]) for r in rows]), "far_field": np.array([int(r["far_field"]) for r in rows], bool), "high_res": np.array([int(r["high_res"]) for r in rows], bool), } def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--bag", action="append") ap.add_argument("--root", default=str(B.DATA / "for_hackathon")) args = ap.parse_args() tab = load_table() from pathlib import Path bags = [Path(b) for b in args.bag] if args.bag else find_bags(args.root) fig, axes = plt.subplots(1, 3, figsize=(16, 4.6), dpi=110) print(f"{'бэг':42s} {'ошибка элевации, °':>22s} {'ошибка сдвига, °':>20s}") print("-" * 88) for p in bags: bag = Bag(p) layout = ScanLayout.calibrate([pc for _, pc in bag.frames(start=2, stop=14)]) # сдвиг канала в градусах = целочисленное выпрямление плюс остаток meas_off = layout.col_shift * layout.az_step_deg + layout.az_resid_deg meas_off = meas_off - np.median(meas_off) + np.median(tab["az_offset"]) de = layout.el_deg - tab["elevation"] da = meas_off - tab["az_offset"] print(f"{p.name:42s} медиана {np.median(np.abs(de)):7.4f} макс {np.abs(de).max():7.4f}" f" медиана {np.median(np.abs(da)):6.4f} макс {np.abs(da).max():6.4f}") axes[0].plot(np.arange(128), de, lw=0.9, alpha=0.8, label=p.name[:18]) axes[1].plot(np.arange(128), da, lw=0.9, alpha=0.8) axes[0].set_title("элевация: измерено − паспорт") axes[0].set_xlabel("канал"); axes[0].set_ylabel("°") axes[0].legend(fontsize=6) axes[1].set_title("азимутальный сдвиг канала: измерено − паспорт") axes[1].set_xlabel("канал"); axes[1].set_ylabel("°") for ax in axes[:2]: ax.axhline(0, color="k", lw=0.6) ax.grid(alpha=0.2) ax = axes[2] ax.plot(np.arange(128), tab["max_range"], lw=1.2, color="tab:blue") ff = np.flatnonzero(tab["far_field"]) ax.fill_between(ff, 0, 210, color="tab:orange", alpha=0.18, label=f"дальнобойные каналы {ff.min()+1}–{ff.max()+1}") hr = np.flatnonzero(tab["high_res"]) ax.fill_between(hr, 0, 210, color="tab:green", alpha=0.10, label=f"высокое разрешение {hr.min()+1}–{hr.max()+1}") ax.set_title("паспортная дальность канала при 10 % отражения") ax.set_xlabel("канал"); ax.set_ylabel("м") ax.legend(fontsize=7); ax.grid(alpha=0.2) B.FIGURES.mkdir(parents=True, exist_ok=True) out = B.FIGURES / "calibration_vs_datasheet.png" fig.tight_layout(); fig.savefig(out) print("\nсохранено:", out) if __name__ == "__main__": main()