# ============================================================
# MOTION_SALIENCY_INTEGRATION.py
# Где и как встроить motion_saliency в существующий пайплайн.
# НЕ ЗАПУСКАТЬ — это документация с фрагментами.
# ============================================================

# ─────────────────────────────────────────────────────────────
# ШАГ 1: ЗАМЕНИТЬ camera_motion.py на новую версию
# ─────────────────────────────────────────────────────────────
# Старая estimate_global_affine продолжает работать — новые
# места используют estimate_global_affine_ex, которая возвращает
# также inliers и outliers. Ничего в существующих вызовах ломать
# не нужно.

# ─────────────────────────────────────────────────────────────
# ШАГ 2: ИМПОРТЫ в main.py
# ─────────────────────────────────────────────────────────────
"""
from camera_motion import estimate_global_affine_ex, affine_is_plausible
from motion_saliency import MotionSaliency
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 3: ИНИЦИАЛИЗАЦИЯ (рядом с другими воркерами)
# ─────────────────────────────────────────────────────────────
"""
    motion_sal = MotionSaliency()
    print(motion_sal.status_line())
    prev_gray_for_ms = None
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 4: В ГЛАВНОМ ЦИКЛЕ — сразу после получения effective frame,
# ДО вызова YOLO submit.
# ─────────────────────────────────────────────────────────────
"""
        gray_eff = cv2.cvtColor(frame_eff_bgr, cv2.COLOR_BGR2GRAY)

        ego_A, ego_inliers, ego_outliers = estimate_global_affine_ex(
            prev_gray_for_ms, gray_eff
        )

        if not affine_is_plausible(ego_A):
            ego_A = None            # fallback: без ego-compensation
            ego_outliers = np.zeros((0, 2), dtype=np.float32)

        # Во время терминальной фазы сближения motion saliency
        # становится шумной (большой parallax). Отключаем.
        ms_active = True
        if intercept_params is not None:
            phase = intercept_params.get("phase", "")
            if phase in ("terminal", "critical"):
                ms_active = False

        if ms_active:
            motion_sal.update(gray_eff, prev_gray_for_ms, ego_A, ego_outliers)
        else:
            motion_sal.reset()

        prev_gray_for_ms = gray_eff
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 5: RE-WEIGHTING YOLO ДЕТЕКТОВ
# Когда забираете результат из YOLOWorker.try_get(), прогоняете
# детекты через motion_sal.score_box() и корректируете confidence.
# ─────────────────────────────────────────────────────────────
"""
MOTION_BOOST_WEIGHT = 0.6     # сила буста (0..1)
MOTION_FLOOR        = 0.15    # минимальный motion score, ниже которого
                              # confidence начинает УМЕНЬШАТЬСЯ

def reweight_by_motion(dets, motion_sal):
    if motion_sal.saliency is None or not dets:
        return dets
    out = []
    for d in dets:
        box = d[:4]
        conf = float(d[4])
        m = motion_sal.score_box(box)
        # Буст для движущихся, штраф для статичных
        factor = 1.0 + MOTION_BOOST_WEIGHT * (m - MOTION_FLOOR)
        factor = max(0.25, min(1.75, factor))
        new_conf = float(min(1.0, conf * factor))
        new_d = d.copy()
        new_d[4] = new_conf
        out.append(new_d)
    return out
"""
# Применение:
"""
        yolo_result = yolo_worker.try_get()
        if yolo_result is not None:
            dets, ts, mode, infer_ms, used_roi = yolo_result
            dets = reweight_by_motion(dets, motion_sal)
            # дальше — как раньше (ByteTrack update и т.д.)
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 6: FALLBACK ROI ДЛЯ YOLO ИЗ MOTION SALIENCY
# Если AutoGaze stale / выключен — берём ROI из motion_sal.get_rois().
# Вставляется в блок выбора roi_box_eff перед yolo_worker.submit().
# ─────────────────────────────────────────────────────────────
"""
        roi_box_eff = None
        # 1. Сначала пробуем AutoGaze (как раньше)
        ag_roi, ag_all, ag_info = autogaze_worker.get_latest(
            frame_id, ew, eh, pred_ref_box=pred_box_eff
        )
        if ag_roi is not None:
            roi_box_eff = ag_roi

        # 2. Fallback: ROI из motion saliency
        if roi_box_eff is None and motion_sal.saliency is not None:
            ms_rois = motion_sal.get_rois(thr=0.35, min_area=25, max_rois=3)
            if ms_rois:
                # Если есть трек — берём ROI ближайший к предсказанию
                if pred_box_eff is not None:
                    pc = box_center(pred_box_eff)
                    best = min(
                        ms_rois,
                        key=lambda r: float(np.linalg.norm(box_center(r) - pc)),
                    )
                    roi_box_eff = best
                else:
                    roi_box_eff = ms_rois[0]
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 7: MOTION CONSISTENCY В TARGET SELECTION
# Когда ByteTrack возвращает кандидатов в target, ранжируете их
# с учётом того, сколько кадров подряд их bbox попадает в motion map.
# Для этого каждый трек получает скользящее среднее motion score.
# ─────────────────────────────────────────────────────────────
"""
# В классе трека (или где у вас хранятся track state'ы):
#   self.motion_history = deque(maxlen=10)
#   self.motion_consistency = 0.0

# После обновления трека на кадре:
for trk in active_tracks:
    if trk.box is not None:
        m = motion_sal.score_box(trk.box)
        trk.motion_history.append(m)
        trk.motion_consistency = float(np.mean(trk.motion_history))

# В target selection — добавляем motion_consistency в score:
#   track_score = base_score + 0.4 * trk.motion_consistency
# Таким образом статичные ложные треки (дерево похожее на дрон)
# получают motion_consistency ≈ 0 и проигрывают настоящему дрону.
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 8: ЛОГИРОВАНИЕ
# Добавить поля в decision_logger.py:
# ─────────────────────────────────────────────────────────────
"""
FIELDNAMES добавить:
    "ms_enabled",
    "ms_active_frac",
    "ms_score_locked",       # saliency внутри locked_box
    "ms_score_pred",         # saliency внутри pred_box
    "ms_peak_locked",

В log_frame kwargs:
    ms_enabled=motion_sal.enabled,
    ms_active_frac=motion_sal.last_active_frac,
    ms_score_locked=motion_sal.score_box(locked_box_eff) if locked_box_eff is not None else 0.0,
    ms_score_pred=motion_sal.score_box(pred_box_eff) if pred_box_eff is not None else 0.0,
    ms_peak_locked=motion_sal.peak_box(locked_box_eff) if locked_box_eff is not None else 0.0,
"""

# ─────────────────────────────────────────────────────────────
# ШАГ 9: ОТРИСОВКА (опционально, для отладки)
# ─────────────────────────────────────────────────────────────
"""
        if DRAW_MOTION_SALIENCY:
            motion_sal.draw_overlay(frame_orig, sx, sy)
"""

# ─────────────────────────────────────────────────────────────
# ПОДСКАЗКИ ПО ТЮНИНГУ НА ВАШЕМ ДАТАСЕТЕ
# ─────────────────────────────────────────────────────────────
#
# 1) Если слишком много ложных residual на деревьях внизу кадра
#    (parallax при полёте над лесом) — увеличить MS_MASK_BOTTOM_FRAC
#    до 0.2-0.3 при высоких скоростях носителя. Альтернатива:
#    переехать в camera_motion.py на cv2.findHomography вместо
#    affine, это лучше описывает плоскость земли.
#
# 2) Для аналогового шумного видео повысить MS_BLUR_KSIZE до 7,
#    MS_DIFF_THR до 22-25, MS_EMA_ALPHA до 0.65. Это замедлит
#    реакцию, но сильно уменьшит ложные вспышки.
#
# 3) Для далёких мелких дронов (3-6 px) — diff map почти не
#    сработает, основной вклад даст outlier density. Увеличить
#    MS_OUTLIER_WEIGHT до 0.7, MS_DIFF_WEIGHT до 0.3.
#
# 4) Порог get_rois(thr=...) подбирается по гистограмме saliency
#    на ваших клипах: 0.35 — среднее, для чистого неба можно 0.25,
#    для сложных сцен — 0.45.
#
# 5) MOTION_BOOST_WEIGHT в reweight_by_motion — начните с 0.4,
#    посмотрите по логам как меняется распределение confidence
#    на true positives vs false positives, подкрутите.
#
# 6) Если в IMM хотите добавить "static" модель:
#    - Q с околонулевой velocity noise (IMM_STATIC_Q_VEL = 0.05)
#    - Трек, у которого static-модель имеет вероятность > 0.7
#      в течение 15+ кадров — помечаем как «stationary clutter»
#      и либо снижаем его track_score, либо вообще убираем из
#      кандидатов в target. Это добивает последние ложные цели
#      на ЛЭП и столбах.
