Ретина, ламина, медулла, лобула, грибовидное тело, веерное тело, центральный комплекс, нисходящие нейроны. Обучение памяти тоннеля и считывания MBON, оценка leave-one-bag-out, полигон дальности, 24 теста. Реальный объект на 55 м — 98.9 % кадров, ложных 7.5 трека на км, кадр обрабатывается за 33 мс на CPU.
118 lines
5.2 KiB
Python
118 lines
5.2 KiB
Python
"""Извлечение таблицы каналов 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()
|