91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""运动检测(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
|