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