You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

160 lines
6.1 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

from __future__ import annotations
import time
from typing import Optional
from . import state, config
from .events import _send_record_event
from .ptz_io import stop_ptz # <-- важно для "abort goto"
def _ema_update(prev: Optional[float], value: float, alpha: float = 0.2) -> float:
return value if prev is None else (1.0 - alpha) * prev + alpha * value
def _stat_on_detection(cam_idx: int, ts: float) -> None:
g = state.detect_stats_global
if g["last_ts"] is not None:
dt = ts - g["last_ts"]
if dt > 0.05:
g["ema_interval"] = _ema_update(g["ema_interval"], dt)
g["times"].append(ts)
g["last_ts"] = ts
c = state.detect_stats_cam[cam_idx]
if c["last_ts"] is not None:
dtc = ts - c["last_ts"]
if dtc > 0.05:
c["ema_interval"] = _ema_update(c["ema_interval"], dtc)
c["times"].append(ts)
c["last_ts"] = ts
def notify_detected(cam_idx: int, detected: bool, force: bool = False) -> None:
"""
detected=True/False — «сырое» состояние текущего кадра.
Формирует START/STOP записи (с гистерезисом) ТОЛЬКО для PTZ (или force).
Также ведёт режимы TRACK/SEARCH с hold на потерю, чтобы не срывать трекинг
при прерывистой детекции.
"""
# Панорамы не создают событий записи, если не включено явно
if cam_idx not in config.PTZ_CAM_IDS and not config.ALLOW_PANORAMA_RECORD and not force:
return
st = state.ptz_states[cam_idx]
now = time.time()
# -------------------- Параметры поведения трекинга --------------------
TRACK_LOSS_HOLD_SEC = float(getattr(config, "TRACK_LOSS_HOLD_SEC", 2.0))
TRACK_HOLD_WHILE_REC_ACTIVE_SEC = float(
getattr(config, "TRACK_HOLD_WHILE_REC_ACTIVE_SEC", TRACK_LOSS_HOLD_SEC)
)
# -------------------- Внутренняя логика трекинга --------------------
last_raw = st.get("last_detected_state")
if detected:
# статистика по фронту False->True
if last_raw is not True:
_stat_on_detection(cam_idx, now)
# фиксируем "видели цель"
st["last_seen"] = now
# Если в этот момент камера ехала на пресет (goto) — прерываем,
# иначе goto/патруль будет мешать трекингу первые секунды
if now < float(st.get("goto_in_progress_until", 0.0) or 0.0):
try:
stop_ptz(cam_idx)
except Exception:
pass
st["goto_in_progress_until"] = 0.0
# сброс кандидата на потерю
st.pop("track_loss_since", None)
# ВАЖНО: сообщаем патрулю, что идёт трекинг
st["tracking_active"] = True
# трекинг включается сразу
st["fallback_done"] = False
st["mode"] = "TRACK"
# опционально: если используешь где-то пост-трекинг флаги
st.pop("post_track_since", None)
st["post_track_park_done"] = False
else:
# detected == False
if st.get("mode") == "TRACK":
if "track_loss_since" not in st:
st["track_loss_since"] = now
else:
hold = TRACK_HOLD_WHILE_REC_ACTIVE_SEC if bool(st.get("rec_active", False)) else TRACK_LOSS_HOLD_SEC
if (now - float(st["track_loss_since"])) >= hold:
st.pop("track_loss_since", None)
st["mode"] = "SEARCH"
st["tracking_active"] = False
st["post_track_since"] = now
else:
# не трекаем — на всякий случай
st["tracking_active"] = False
st["last_detected_state"] = detected
# -------------------- форс-STOP --------------------
if force:
if st.get("rec_active", False) or st.get("rec_last_sent") is not False:
_send_record_event(cam_idx, False)
st["rec_active"] = False
st["rec_first_seen"] = None
st["rec_started_at"] = 0.0
st["rec_last_off_at"] = now
st["rec_last_sent"] = False
# сброс трекинга
st.pop("track_loss_since", None)
st["tracking_active"] = False
if st.get("mode") == "TRACK":
st["mode"] = "SEARCH"
return
# -------------------- гистерезис записи --------------------
if detected:
st["rec_last_seen"] = now
if not st.get("rec_active", False):
if now - float(st.get("rec_last_off_at", 0.0) or 0.0) < float(getattr(config, "REC_MIN_OFF_SEC", 0.0)):
return
if st.get("rec_first_seen") is None:
st["rec_first_seen"] = now
if (now - float(st["rec_first_seen"])) >= float(getattr(config, "REC_ON_CONFIRM_SEC", 0.35)):
st["rec_active"] = True
st["rec_started_at"] = now
if st.get("rec_last_sent") is not True:
_send_record_event(cam_idx, True) # START → C++
st["rec_last_sent"] = True
return
# detected == False
st["rec_first_seen"] = None
if st.get("rec_active", False):
rec_last_seen = float(st.get("rec_last_seen", 0.0) or 0.0)
rec_started_at = float(st.get("rec_started_at", 0.0) or 0.0)
long_no_see = (now - rec_last_seen) >= float(getattr(config, "REC_OFF_GRACE_SEC", 1.2))
long_enough = (now - rec_started_at) >= float(getattr(config, "REC_MIN_ON_SEC", 0.0))
too_long = (now - rec_started_at) >= float(getattr(config, "REC_MAX_ON_SEC", 1e9))
if (long_no_see and long_enough) or too_long:
st["rec_active"] = False
st["rec_last_off_at"] = now
if st.get("rec_last_sent") is not False:
_send_record_event(cam_idx, False) # STOP → C++
st["rec_last_sent"] = False