ソースを参照

configurable motion and detect settings

Blake Blackshear 4 年 前
コミット
3bc7cdaab6
6 ファイル変更141 行追加32 行削除
  1. 32 0
      README.md
  2. 87 13
      frigate/config.py
  3. 15 13
      frigate/motion.py
  4. 3 2
      frigate/objects.py
  5. 2 2
      frigate/process_clip.py
  6. 2 2
      frigate/video.py

+ 32 - 0
README.md

@@ -281,6 +281,38 @@ objects:
       # Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
       threshold: 0.7
 
+# Optional: Global motion detection config. These may also be defined at the camera level.
+# ADVANCED: Most users will not need to set these values in their config
+motion:
+  # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
+  # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
+  # The value should be between 1 and 255.
+  threshold: 25
+  # Optional: Minimum size in pixels in the resized motion image that counts as motion
+  # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller
+  # moving objects.
+  contour_area: 100
+  # Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
+  # Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
+  # Too low and a fast moving person wont be detected as motion.
+  delta_alpha: 0.2
+  # Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
+  # Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
+  # Low values will cause things like moving shadows to be detected as motion for longer.
+  # https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
+  frame_alpha: 0.2
+  # Optional: Height of the resized motion frame  (default: 1/6th of the original frame height)
+  # This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage.
+  # Lower values result in less CPU, but small changes may not register as motion.
+  frame_height: 180
+
+# Optional: Global detecttion settings. These may also be defined at the camera level.
+# ADVANCED: Most users will not need to set these values in their config
+detect:
+  # Optional: Number of frames without a detection before frigate considers an object to be gone. (default: double the frame rate)
+  max_disappeared: 10
+
+
 # Required: configuration section for cameras
 cameras:
   # Required: name of the camera

+ 87 - 13
frigate/config.py

@@ -84,6 +84,22 @@ GLOBAL_FFMPEG_SCHEMA = vol.Schema(
     }
 )
 
+MOTION_SCHEMA = vol.Schema(
+    {
+        'threshold': vol.Range(min=1, max=255),
+        'contour_area': int,
+        'delta_alpha': float,
+        'frame_alpha': float,
+        'frame_height': int
+    }
+)
+
+DETECT_SCHEMA = vol.Schema(
+    {
+        'max_disappeared': int
+    }
+)
+
 FILTER_SCHEMA = vol.Schema(
     { 
         str: {
@@ -109,16 +125,6 @@ OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
     }
 ))
 
-DEFAULT_CAMERA_SAVE_CLIPS = {
-    'enabled': False
-}
-DEFAULT_CAMERA_SNAPSHOTS = {
-    'show_timestamp': True,
-    'draw_zones': False,
-    'draw_bounding_boxes': True,
-    'crop_to_region': True
-}
-
 def each_role_used_once(inputs):
     roles = [role for i in inputs for role in i['roles']]
     roles_set = set(roles)
@@ -166,7 +172,7 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
                     vol.Optional('filters', default={}): FILTER_SCHEMA
                 }
             },
-            vol.Optional('save_clips', default=DEFAULT_CAMERA_SAVE_CLIPS): {
+            vol.Optional('save_clips', default={}): {
                 vol.Optional('enabled', default=False): bool,
                 vol.Optional('pre_capture', default=30): int,
                 'objects': [str],
@@ -179,14 +185,16 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
             vol.Optional('rtmp', default={}): {
                 vol.Required('enabled', default=True): bool,
             },
-            vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): {
+            vol.Optional('snapshots', default={}): {
                 vol.Optional('show_timestamp', default=True): bool,
                 vol.Optional('draw_zones', default=False): bool,
                 vol.Optional('draw_bounding_boxes', default=True): bool,
                 vol.Optional('crop_to_region', default=True): bool,
                 vol.Optional('height', default=175): int
             },
-            'objects': OBJECTS_SCHEMA
+            'objects': OBJECTS_SCHEMA,
+            vol.Optional('motion', default={}): MOTION_SCHEMA,
+            vol.Optional('detect', default={}): DETECT_SCHEMA
         }
     }, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
 )
@@ -213,6 +221,8 @@ FRIGATE_CONFIG_SCHEMA = vol.Schema(
         },
         vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
         vol.Optional('objects', default={}): OBJECTS_SCHEMA,
+        vol.Optional('motion', default={}): MOTION_SCHEMA,
+        vol.Optional('detect', default={}): DETECT_SCHEMA,
         vol.Required('cameras', default={}): CAMERAS_SCHEMA
     }
 )
@@ -561,6 +571,58 @@ class CameraRtmpConfig():
             'enabled': self.enabled,
         }
 
+class MotionConfig():
+    def __init__(self, global_config, config, camera_height: int):
+        self._threshold = config.get('threshold', global_config.get('threshold', 25))
+        self._contour_area = config.get('contour_area', global_config.get('contour_area', 100))
+        self._delta_alpha = config.get('delta_alpha', global_config.get('delta_alpha', 0.2))
+        self._frame_alpha = config.get('frame_alpha', global_config.get('frame_alpha', 0.2))
+        self._frame_height = config.get('frame_height', global_config.get('frame_height', camera_height//6))
+    
+    @property
+    def threshold(self):
+        return self._threshold
+
+    @property
+    def contour_area(self):
+        return self._contour_area
+
+    @property
+    def delta_alpha(self):
+        return self._delta_alpha
+
+    @property
+    def frame_alpha(self):
+        return self._frame_alpha
+
+    @property
+    def frame_height(self):
+        return self._frame_height
+    
+    def to_dict(self):
+        return {
+            'threshold': self.threshold,
+            'contour_area': self.contour_area,
+            'delta_alpha': self.delta_alpha,
+            'frame_alpha': self.frame_alpha,
+            'frame_height': self.frame_height,
+        }
+
+
+
+class DetectConfig():
+    def __init__(self, global_config, config, camera_fps):
+        self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
+    
+    @property
+    def max_disappeared(self):
+        return self._max_disappeared
+    
+    def to_dict(self):
+        return {
+            'max_disappeared': self._max_disappeared,
+        }
+
 class ZoneConfig():
     def __init__(self, name, config):
         self._coordinates = config['coordinates']
@@ -623,6 +685,8 @@ class CameraConfig():
         self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
         self._snapshots = CameraSnapshotsConfig(config['snapshots'])
         self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
+        self._motion = MotionConfig(global_config['motion'], config['motion'], self._height)
+        self._detect = DetectConfig(global_config['detect'], config['detect'], config.get('fps', 5))
 
         self._ffmpeg_cmds = []
         for ffmpeg_input in self._ffmpeg.inputs:
@@ -755,6 +819,14 @@ class CameraConfig():
     @property
     def objects(self):
         return self._objects
+    
+    @property
+    def motion(self):
+        return self._motion
+    
+    @property
+    def detect(self):
+        return self._detect
 
     @property
     def frame_shape(self):
@@ -781,6 +853,8 @@ class CameraConfig():
             'rtmp': self.rtmp.to_dict(),
             'snapshots': self.snapshots.to_dict(),
             'objects': self.objects.to_dict(),
+            'motion': self.motion.to_dict(),
+            'detect': self.detect.to_dict(),
             'frame_shape': self.frame_shape,
             'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
         }

+ 15 - 13
frigate/motion.py

@@ -1,13 +1,15 @@
 import cv2
 import imutils
 import numpy as np
+from frigate.config import MotionConfig
 
 
 class MotionDetector():
-    def __init__(self, frame_shape, mask, resize_factor=4):
+    def __init__(self, frame_shape, mask, config: MotionConfig):
+        self.config = config
         self.frame_shape = frame_shape
-        self.resize_factor = resize_factor
-        self.motion_frame_size = (int(frame_shape[0]/resize_factor), int(frame_shape[1]/resize_factor))
+        self.resize_factor = frame_shape[0]/config.frame_height
+        self.motion_frame_size = (config.frame_height, config.frame_height*frame_shape[1]//frame_shape[0])
         self.avg_frame = np.zeros(self.motion_frame_size, np.float)
         self.avg_delta = np.zeros(self.motion_frame_size, np.float)
         self.motion_frame_count = 0
@@ -23,6 +25,8 @@ class MotionDetector():
         # resize frame
         resized_frame = cv2.resize(gray, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), interpolation=cv2.INTER_LINEAR)
 
+        # TODO: can I improve the contrast of the grayscale image here?
+
         # convert to grayscale
         # resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY)
 
@@ -38,14 +42,13 @@ class MotionDetector():
             frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
 
             # compute the average delta over the past few frames
-            # the alpha value can be modified to configure how sensitive the motion detection is.
             # higher values mean the current frame impacts the delta a lot, and a single raindrop may
             # register as motion, too low and a fast moving person wont be detected as motion
-            # this also assumes that a person is in the same location across more than a single frame
-            cv2.accumulateWeighted(frameDelta, self.avg_delta, 0.2)
+            cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
 
             # compute the threshold image for the current frame
-            current_thresh = cv2.threshold(frameDelta, 25, 255, cv2.THRESH_BINARY)[1]
+            # TODO: threshold
+            current_thresh = cv2.threshold(frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
 
             # black out everything in the avg_delta where there isnt motion in the current frame
             avg_delta_image = cv2.convertScaleAbs(self.avg_delta)
@@ -53,7 +56,7 @@ class MotionDetector():
 
             # then look for deltas above the threshold, but only in areas where there is a delta
             # in the current frame. this prevents deltas from previous frames from being included
-            thresh = cv2.threshold(avg_delta_image, 25, 255, cv2.THRESH_BINARY)[1]
+            thresh = cv2.threshold(avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY)[1]
 
             # dilate the thresholded image to fill in holes, then find contours
             # on thresholded image
@@ -65,19 +68,18 @@ class MotionDetector():
             for c in cnts:
                 # if the contour is big enough, count it as motion
                 contour_area = cv2.contourArea(c)
-                if contour_area > 100:
+                if contour_area > self.config.contour_area:
                     x, y, w, h = cv2.boundingRect(c)
                     motion_boxes.append((x*self.resize_factor, y*self.resize_factor, (x+w)*self.resize_factor, (y+h)*self.resize_factor))
         
         if len(motion_boxes) > 0:
             self.motion_frame_count += 1
-            # TODO: this really depends on FPS
             if self.motion_frame_count >= 10:
-                # only average in the current frame if the difference persists for at least 3 frames
-                cv2.accumulateWeighted(resized_frame, self.avg_frame, 0.2)
+                # only average in the current frame if the difference persists for a bit
+                cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
         else:
             # when no motion, just keep averaging the frames together
-            cv2.accumulateWeighted(resized_frame, self.avg_frame, 0.2)
+            cv2.accumulateWeighted(resized_frame, self.avg_frame, self.config.frame_alpha)
             self.motion_frame_count = 0
 
         return motion_boxes

+ 3 - 2
frigate/objects.py

@@ -12,14 +12,15 @@ import cv2
 import numpy as np
 from scipy.spatial import distance as dist
 
+from frigate.config import DetectConfig
 from frigate.util import draw_box_with_label
 
 
 class ObjectTracker():
-    def __init__(self, max_disappeared):
+    def __init__(self, config: DetectConfig):
         self.tracked_objects = {}
         self.disappeared = {}
-        self.max_disappeared = max_disappeared
+        self.max_disappeared = config.max_disappeared
 
     def register(self, index, obj):
         rand_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))

+ 2 - 2
frigate/process_clip.py

@@ -81,10 +81,10 @@ class ProcessClip():
     def process_frames(self, objects_to_track=['person'], object_filters={}):
         mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
         mask[:] = 255
-        motion_detector = MotionDetector(self.frame_shape, mask)
+        motion_detector = MotionDetector(self.frame_shape, mask, self.camera_config.motion)
 
         object_detector = LocalObjectDetector(labels='/labelmap.txt')
-        object_tracker = ObjectTracker(10)
+        object_tracker = ObjectTracker(self.camera_config.detect)
         process_info = {
             'process_fps': mp.Value('d', 0.0),
             'detection_fps': mp.Value('d', 0.0),

+ 2 - 2
frigate/video.py

@@ -258,10 +258,10 @@ def track_camera(name, config: CameraConfig, model_shape, detection_queue, resul
     object_filters = config.objects.filters
     mask = config.mask
 
-    motion_detector = MotionDetector(frame_shape, mask, resize_factor=6)
+    motion_detector = MotionDetector(frame_shape, mask, config.motion)
     object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection, model_shape)
 
-    object_tracker = ObjectTracker(10)
+    object_tracker = ObjectTracker(config.detect)
 
     frame_manager = SharedMemoryFrameManager()