|
|
from __future__ import annotations
|
|
|
import logging
|
|
|
from math import isfinite
|
|
|
|
|
|
# configure module logger (app sets handlers/level)
|
|
|
logger = logging.getLogger("PTZTracker")
|
|
|
|
|
|
# ---------- fast json ----------
|
|
|
try:
|
|
|
import orjson
|
|
|
def json_dumps(obj) -> str:
|
|
|
return orjson.dumps(obj).decode("utf-8")
|
|
|
json_loads = orjson.loads
|
|
|
except Exception:
|
|
|
import json
|
|
|
def json_dumps(obj) -> str:
|
|
|
return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
|
|
|
json_loads = json.loads
|
|
|
|
|
|
# ---------- math helpers ----------
|
|
|
def clipf(v: float, lo: float, hi: float) -> float:
|
|
|
return lo if v < lo else hi if v > hi else v
|
|
|
|
|
|
def quantf(v: float, q: float) -> float:
|
|
|
return round(v / q) * q
|
|
|
|
|
|
# ---------- angle / compass helpers ----------
|
|
|
def wrap_deg(a: float) -> float:
|
|
|
"""[-180, +180)"""
|
|
|
return (a + 180.0) % 360.0 - 180.0
|
|
|
|
|
|
def norm360(a: float) -> float:
|
|
|
"""[0, 360)"""
|
|
|
return a % 360.0
|
|
|
|
|
|
def shortest_delta_deg(a: float, b: float) -> float:
|
|
|
"""(b - a) в [-180, +180)"""
|
|
|
return wrap_deg(b - a)
|
|
|
|
|
|
# --- компас 8 румбов (как на картинке) ---
|
|
|
def deg_to_compass(deg: float) -> str:
|
|
|
labels_ru_8 = ["С", "СВ", "В", "ЮВ", "Ю", "ЮЗ", "З", "СЗ"]
|
|
|
idx = int((norm360(deg) + 22.5) // 45) % 8
|
|
|
return labels_ru_8[idx]
|
|
|
|
|
|
def format_bearing_ru(deg: float) -> str:
|
|
|
return f"{deg_to_compass(deg)} ({norm360(deg):.1f}°)"
|
|
|
|
|
|
# ---------- optics helpers ----------
|
|
|
def hfov_ptz_from_abs_zoom(abs_zoom: float,
|
|
|
zmin: float = 0.0, zmax: float = 100.0,
|
|
|
hfov_wide: float = 59.0,
|
|
|
hfov_tele: float = 2.0) -> float:
|
|
|
"""
|
|
|
Аппроксимация HFOV от абсолютного зума (экспоненциальная интерполяция).
|
|
|
abs_zoom приводим к [0..1]. Под реальные значения можно подогнать константы.
|
|
|
"""
|
|
|
if not isfinite(abs_zoom):
|
|
|
return hfov_wide
|
|
|
if zmax == zmin:
|
|
|
return hfov_wide
|
|
|
t = clipf((abs_zoom - zmin) / (zmax - zmin), 0.0, 1.0)
|
|
|
r = hfov_tele / hfov_wide
|
|
|
return hfov_wide * (r ** t)
|
|
|
|
|
|
def bearing_from_xcenter(x_center_norm: float,
|
|
|
hfov_deg: float,
|
|
|
base_axis_deg: float) -> float:
|
|
|
"""
|
|
|
Преобразует x-норм. центр бокса (0..1) в глобальный азимут:
|
|
|
bearing = base_axis_deg + (x-0.5)*HFOV
|
|
|
где base_axis_deg — азимут оптической оси (с учётом севера).
|
|
|
"""
|
|
|
delta = (x_center_norm - 0.5) * hfov_deg
|
|
|
return norm360(base_axis_deg + delta)
|