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

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

118 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""Извлечение таблицы каналов Pandar128E3X из руководства (Приложение A).
Даёт для каждого из 128 каналов: проектный азимутальный сдвиг и угол места,
диапазон измерения, признак ближнего поля, максимальную дальность при 10 %
отражения, признак «дальнобойного» канала и минимальную различимую
отражательную способность.
Зачем: поканальная дальность делает модель сенсора в синтетическом полигоне
честной (каналы 34–65 видят 200 м, а 98–128 — только ближнее поле), а
проектные углы служат независимой проверкой калибровки решётки по данным.
python tools/extract_channel_table.py --pdf <путь> --out ros2_ws/src/flyguard/flyguard/data/pandar128_channels.csv
"""
from __future__ import annotations
import argparse
import csv
import re
from pathlib import Path
ANGLE = re.compile(r"^-?\d+\.\d+°$")
METERS = re.compile(r"^\(?(\d+(?:\.\d+)?)\s*m\)?$")
# в таблице встречаются типографские варианты: минус, полноширинные скобки
_NORMALISE = str.maketrans({"–": "-", "—": "-", "−": "-", "(": "(", ")": ")", " ": " "})
def _clean(s: str) -> str:
return s.translate(_NORMALISE).strip()
def parse(pdf_path: str) -> list[dict]:
import fitz
doc = fitz.open(pdf_path)
tokens: list[str] = []
for page in doc:
text = page.get_text()
if "Appendix A: Channel distribution data" not in text:
continue
if "Chann" not in text and not re.search(r"^\d+\s*$", text, re.M):
continue
tokens.extend(_clean(line) for line in text.splitlines() if line.strip())
# строки собираются в словарь по номеру канала, а не последовательно:
# один сбой синхронизации на границе страницы иначе обрывает весь разбор
found: dict[int, dict] = {}
i = 0
while i < len(tokens) - 9:
# строка начинается с номера канала, за которым идут два угла
if (tokens[i].isdigit() and ANGLE.match(tokens[i + 1] or "")
and ANGLE.match(tokens[i + 2] or "")):
ch = int(tokens[i])
if not 1 <= ch <= 128 or ch in found:
i += 1
continue
chunk = tokens[i:i + 10]
m_min = METERS.match(chunk[3])
m_max = METERS.match(chunk[4])
near = chunk[5].upper() == "YES"
m_10 = METERS.match(chunk[6])
far = chunk[7].upper() == "YES"
refl = chunk[8]
highres = chunk[9].upper() == "YES"
if not (m_min and m_max and m_10):
i += 1
continue
found[ch] = dict(
channel=ch,
az_offset_deg=float(chunk[1].rstrip("°")),
elevation_deg=float(chunk[2].rstrip("°")),
range_min_m=float(m_min.group(1)),
range_max_m=float(m_max.group(1)),
near_field=int(near),
max_range_10pct_m=float(m_10.group(1)),
far_field=int(far),
min_reflectivity=refl,
high_res=int(highres),
)
i += 10
else:
i += 1
return [found[k] for k in sorted(found)]
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--pdf", required=True)
ap.add_argument("--out", required=True)
args = ap.parse_args()
rows = parse(args.pdf)
if len(rows) != 128:
print(f"ВНИМАНИЕ: разобрано {len(rows)} каналов вместо 128")
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
with open(out, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0]))
w.writeheader()
w.writerows(rows)
import numpy as np
az = np.array([r["az_offset_deg"] for r in rows])
el = np.array([r["elevation_deg"] for r in rows])
r10 = np.array([r["max_range_10pct_m"] for r in rows])
print(f"разобрано каналов: {len(rows)} → {out}")
print(f" азимутальный сдвиг: {az.min():+.3f}°…{az.max():+.3f}° "
f"(размах {az.ptp() if hasattr(az,'ptp') else np.ptp(az):.3f}°)")
print(f" угол места: {el.min():+.3f}°…{el.max():+.3f}°")
print(f" макс. дальность @10%: {r10.min():.0f}…{r10.max():.0f} м, "
f"каналов с 200 м: {(r10 >= 200).sum()}")
print(f" ближнего поля: {sum(r['near_field'] for r in rows)}, "
f"дальнобойных: {sum(r['far_field'] for r in rows)}, "
f"высокого разрешения: {sum(r['high_res'] for r in rows)}")
if __name__ == "__main__":
main()