"""声呐视频发布前:在 FishMeasure ``optical_flow/visualize_optical_flow.py`` 中做稠密光流 overlay。""" from __future__ import annotations import importlib.util import threading import time from pathlib import Path from types import ModuleType from typing import Optional from loguru import logger from app.settings import Settings _mod_lock = threading.Lock() _cached_mod: Optional[ModuleType] = None _cached_mod_path: Optional[str] = None def _load_optical_flow_module(settings: Settings) -> Optional[ModuleType]: global _cached_mod, _cached_mod_path path = settings.fish_measure_root / "optical_flow" / "visualize_optical_flow.py" path_str = str(path) with _mod_lock: if _cached_mod is not None and _cached_mod_path == path_str: return _cached_mod if not path.is_file(): logger.warning("[sonar-video] optical flow module not found: {}", path) return None spec = importlib.util.spec_from_file_location("fishmeasure_optical_flow_viz", path) if spec is None or spec.loader is None: return None mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) _cached_mod = mod _cached_mod_path = path_str return mod def run_sonar_optical_flow_overlay( src: Path, flow_out: Path, settings: Settings, ) -> bool: """将 ``src`` 处理为 overlay 光流视频写入 ``flow_out``。失败返回 False(调用方应回退直转码)。""" if not settings.biomass_sonar_optical_flow: return False mod = _load_optical_flow_module(settings) if mod is None: return False run_fn = getattr(mod, "run_optical_flow_video", None) if not callable(run_fn): logger.warning("[sonar-video] run_optical_flow_video missing in optical flow module") return False src_size_mb = src.stat().st_size / (1024 * 1024) if src.is_file() else 0 logger.info("[sonar-video] optical flow starting: {} ({:.1f} MB), resize={}", src.name, src_size_mb, settings.biomass_sonar_optical_flow_resize) t0 = time.monotonic() try: ok = run_fn( src, flow_out, mode="overlay", resize=float(settings.biomass_sonar_optical_flow_resize), progress_log=False, ) elapsed = time.monotonic() - t0 if ok: out_size_mb = flow_out.stat().st_size / (1024 * 1024) if flow_out.is_file() else 0 logger.info("[sonar-video] optical flow done in {:.1f}s: {} -> {} ({:.1f} MB)", elapsed, src.name, flow_out.name, out_size_mb) else: logger.warning("[sonar-video] optical flow returned False after {:.1f}s for {}", elapsed, src.name) return bool(ok) except Exception: elapsed = time.monotonic() - t0 logger.exception("[sonar-video] optical flow raised exception after {:.1f}s for {}", elapsed, src.name) return False