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.
MAI/MOTION_SALIENCY_INTEGRATION.py

214 lines
12 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.

# ============================================================
# 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. Это добивает последние ложные цели
# на ЛЭП и столбах.