"""Устойчивость к формату облака: тот же проезд в других обличьях. Контрольная запись может прийти не в том виде, что выданные: другой драйвер, другая раскладка, другой порядок и типы полей. Инструмент берёт настоящие кадры, переписывает их, прогоняет через CDR — тот же разбор, что в узле, — и через конвейер и сравнивает решения с исходными: сколько кадров обработано, были ли сбои, совпадает ли тревога и расстояние. python tools/check_formats.py python tools/check_formats.py --bag data/for_hackathon/doubleT_obstacle python tools/check_formats.py --only "без intensity,ось X вперёд" """ from __future__ import annotations import argparse import struct import time from pathlib import Path import numpy as np import _bootstrap as B from flyguard.bag import Bag from flyguard.cdr import PointCloud2, parse_pointcloud2 from flyguard.mbon_readout import MbonReadout from flyguard.mushroom_body import MushroomBody from flyguard.pipeline import FlyGuard, Params _ROS_TYPE = {("i", 1): 1, ("u", 1): 2, ("i", 2): 3, ("u", 2): 4, ("i", 4): 5, ("u", 4): 6, ("f", 4): 7, ("f", 8): 8} def to_cdr(pc: PointCloud2) -> bytes: """Обратно в CDR sensor_msgs/msg/PointCloud2 — в том виде, в каком пишет ROS.""" out = bytearray(b"\x00\x01\x00\x00") # заголовок инкапсуляции def align(n: int) -> None: out.extend(b"\x00" * ((-(len(out) - 4)) % n)) def u32(v: int) -> None: align(4) out.extend(struct.pack(" None: b = s.encode() + b"\x00" u32(len(b)) out.extend(b) sec = int(np.floor(pc.stamp)) align(4) out.extend(struct.pack(" PointCloud2: return PointCloud2(stamp=pc.stamp if stamp is None else stamp, frame_id=pc.frame_id, height=height, width=pts.size // height, point_step=pts.dtype.itemsize, is_dense=pc.is_dense if dense is None else dense, points=pts) def _fields(pc: PointCloud2) -> list[str]: return [n for n in pc.points.dtype.names if not n.startswith("_pad")] def _recast(pc: PointCloud2, spec: list[tuple[str, str, int]], itemsize: int) -> np.ndarray: """Новая раскладка точки: (имя, тип, смещение); значения — по имени поля.""" names = [s[0] for s in spec if s[0] in pc.points.dtype.names] dt = np.dtype({"names": names, "formats": [s[1] for s in spec if s[0] in names], "offsets": [s[2] for s in spec if s[0] in names], "itemsize": itemsize}) out = np.zeros(pc.points.size, dt) for n in names: out[n] = pc.points[n] return out def _drop(pc: PointCloud2, gone: set[str]) -> np.ndarray: keep = [n for n in _fields(pc) if n not in gone] spec, off = [], 0 for n in keep: t = pc.points.dtype.fields[n][0] spec.append((n, t.str, off)) off += t.itemsize return _recast(pc, spec, off) def _echo(pc: PointCloud2) -> np.ndarray: p = pc.points return ((p["x"] != 0) | (p["y"] != 0) | (p["z"] != 0)) & np.isfinite(p["x"]) def as_is(pc, k): return pc def nan_for_no_return(pc, k): pts = np.array(pc.points) miss = ~_echo(pc) for n in ("x", "y", "z"): pts[n][miss] = np.nan return _like(pc, pts, dense=False) def header_128(pc, k): n = pc.points.size return _like(pc, np.array(pc.points), height=128) if n % 128 == 0 else pc def ring_major(pc, k): if "ring" not in pc.points.dtype.names: return pc order = np.argsort(pc.points["ring"], kind="stable") return _like(pc, np.array(pc.points)[order]) def reordered_fields(pc, k): spec = [("intensity", " dict: # кадры читаются заново для каждого обличья: 24-мегабайтные кадры # doubleT_obstacle целиком в память не помещаются fg = FlyGuard(Params(device="cpu"), memory=memory, readout=readout) out, errors, ms = {}, [], [] for k, (_, pc) in enumerate(Bag(bag).frames(stop=n)): if k % step: continue try: blob = to_cdr(fn(pc, k)) t0 = time.perf_counter() res = fg.process(parse_pointcloud2(blob)) ms.append((time.perf_counter() - t0) * 1e3) except Exception as e: # noqa: BLE001 — сбой и есть результат проверки errors.append(f"{type(e).__name__}: {e}") continue if res is not None: d = res.decision out[k] = (bool(d.detected), float(d.distance) if d.detected else np.nan) return {"dec": out, "errors": errors, "ms": ms} def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--bag", action="append", help="запись; можно несколько (по умолчанию doubleT_obstacle и синтетика)") ap.add_argument("--frames", type=int, default=600, help="сколько кадров с начала") ap.add_argument("--only", default="", help="обличья через запятую") args = ap.parse_args() bags = args.bag or [str(B.DATA / "for_hackathon" / "doubleT_obstacle"), str(B.DATA / "cloud_with_fake_obj")] only = [s.strip() for s in args.only.split(",") if s.strip()] memory = MushroomBody.load(B.ARTIFACTS / "mushroom_body.npz") readout = MbonReadout.load(B.ARTIFACTS / "mbon_readout.npz") for bag in map(Path, bags): first = next(pc for _, pc in Bag(bag).frames(stop=1)) print(f"\n== {bag.name}: до {args.frames} кадров, " f"поля {', '.join(_fields(first))}, точек {first.points.size}") base = run(bag, args.frames, as_is, 1, memory, readout)["dec"] n_alarm = sum(v[0] for v in base.values()) print(f" исходно: обработано {len(base)}, с тревогой {n_alarm}") print(f" {'обличье':34s} {'обраб.':>6s} {'сбоев':>5s} {'тревог':>6s} " f"{'совпало':>7s} {'Δд, м':>6s} {'мс':>5s}") for name, (fn, step) in VARIANTS.items(): if only and name not in only: continue r = run(bag, args.frames, fn, step, memory, readout) dec = r["dec"] common = [k for k in dec if k in base] same = np.mean([dec[k][0] == base[k][0] for k in common]) if common else np.nan dd = [abs(dec[k][1] - base[k][1]) for k in common if dec[k][0] and base[k][0]] alarms = sum(v[0] for v in dec.values()) print(f" {name:34s} {len(dec):6d} {len(r['errors']):5d} {alarms:6d} " f"{same:7.1%} {np.median(dd) if dd else float('nan'):6.2f} " f"{np.median(r['ms']) if r['ms'] else float('nan'):5.0f}") if r["errors"]: print(f" первый сбой: {r['errors'][0][:160]}") if __name__ == "__main__": main()