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