video_monitor/app/analysis/motion.py
2026-08-30 22:23:12 +08:00

91 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""运动检测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