"""运动检测(OpenCV 背景减除 + 形态学优化) 参考 Frigate 的运动门控思路:先做轻量运动检测,只在有运动的区域跑目标检测。 """ import logging logger = logging.getLogger("analysis.motion") try: import numpy as np _NP_AVAILABLE = True except Exception: np = None _NP_AVAILABLE = False try: import cv2 _CV2_AVAILABLE = True except Exception: cv2 = None _CV2_AVAILABLE = False class MotionDetector(object): """基于 MOG2 背景减除的运动检测器,输出运动框列表""" def __init__(self, frame_width=320, frame_height=180, min_area=80, max_area_ratio=0.6, variance_threshold=25, history=100, contrast_threshold=0.4): self.frame_width = frame_width self.frame_height = frame_height self.min_area = min_area self.max_area_ratio = max_area_ratio self.contrast_threshold = contrast_threshold self._bg = None if _CV2_AVAILABLE: self._bg = cv2.createBackgroundSubtractorMOG2( history=history, varThreshold=variance_threshold, detectShadows=False) self._frame_area = frame_width * frame_height self._max_area = self._frame_area * max_area_ratio def detect(self, frame_bgr): """返回 list[dict(box=[x1,y1,x2,y2], area=int)](坐标基于原始 frame 尺寸)""" if not _CV2_AVAILABLE or not _NP_AVAILABLE or frame_bgr is None: return [] try: h, w = frame_bgr.shape[:2] # 缩放降低计算量 small = cv2.resize(frame_bgr, (self.frame_width, self.frame_height), interpolation=cv2.INTER_AREA) # 对比度过低帧跳过(避免夜视噪声误报) gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) if cv2.mean(gray)[0] < 8 or self._low_contrast(gray): return [] mask = self._bg.apply(small) mask = cv2.threshold(mask, 200, 255, cv2.THRESH_BINARY)[1] mask = cv2.dilate(mask, None, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, None, iterations=1) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) sx = float(w) / self.frame_width sy = float(h) / self.frame_height boxes = [] for c in contours: a = cv2.contourArea(c) if a < self.min_area or a > self._max_area: continue x, y, bw, bh = cv2.boundingRect(c) x1 = int(x * sx); y1 = int(y * sy) x2 = int((x + bw) * sx); y2 = int((y + bh) * sy) boxes.append({"box": [x1, y1, x2, y2], "area": int(a)}) return boxes except Exception as e: logger.warning("MotionDetector.detect() error: %s" % str(e)) return [] def _low_contrast(self, gray): try: if float(np.std(gray)) < self.contrast_threshold: return True except Exception: return False return False @staticmethod def is_available(): return _CV2_AVAILABLE and _NP_AVAILABLE