mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	create typed config classes
This commit is contained in:
		
							parent
							
								
									b7c09a9b38
								
							
						
					
					
						commit
						af303cbf2a
					
				| @ -7,100 +7,31 @@ import multiprocessing as mp | ||||
| from playhouse.sqlite_ext import SqliteExtDatabase | ||||
| from typing import Dict, List | ||||
| 
 | ||||
| from frigate.config import FRIGATE_CONFIG_SCHEMA | ||||
| from frigate.config import FrigateConfig | ||||
| from frigate.edgetpu import EdgeTPUProcess | ||||
| from frigate.events import EventProcessor | ||||
| from frigate.http import create_app | ||||
| from frigate.models import Event | ||||
| from frigate.mqtt import create_mqtt_client | ||||
| from frigate.object_processing import TrackedObjectProcessor | ||||
| from frigate.video import get_frame_shape, track_camera, get_ffmpeg_input, capture_camera | ||||
| from frigate.video import track_camera, capture_camera | ||||
| from frigate.watchdog import FrigateWatchdog | ||||
| 
 | ||||
| class FrigateApp(): | ||||
|     def __init__(self): | ||||
|         self.stop_event = mp.Event() | ||||
|         self.config: dict = None | ||||
|         self.config: FrigateConfig = None | ||||
|         self.detection_queue = mp.Queue() | ||||
|         self.detectors: Dict[str: EdgeTPUProcess] = {} | ||||
|         self.detection_out_events: Dict[str: mp.Event] = {} | ||||
|         self.detectors: Dict[str, EdgeTPUProcess] = {} | ||||
|         self.detection_out_events: Dict[str, mp.Event] = {} | ||||
|         self.detection_shms: List[mp.shared_memory.SharedMemory] = [] | ||||
|         self.camera_metrics = {} | ||||
|      | ||||
|     def init_config(self): | ||||
|         # TODO: sub in FRIGATE_ENV vars | ||||
|         frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')} | ||||
|         config_file = os.environ.get('CONFIG_FILE', '/config/config.yml') | ||||
|         self.config = FrigateConfig(config_file=config_file) | ||||
| 
 | ||||
|         with open(config_file) as f: | ||||
|             raw_config = f.read() | ||||
|          | ||||
|         if config_file.endswith(".yml"):     | ||||
|             config = yaml.safe_load(raw_config) | ||||
|         elif config_file.endswith(".json"): | ||||
|             config = json.loads(raw_config) | ||||
|          | ||||
|         self.config = FRIGATE_CONFIG_SCHEMA(config) | ||||
| 
 | ||||
|         if 'password' in self.config['mqtt']: | ||||
|             self.config['mqtt']['password'] = self.config['mqtt']['password'].format(**frigate_env_vars) | ||||
| 
 | ||||
|         cache_dir = self.config['save_clips']['cache_dir'] | ||||
|         clips_dir = self.config['save_clips']['clips_dir'] | ||||
| 
 | ||||
|         if not os.path.exists(cache_dir) and not os.path.islink(cache_dir): | ||||
|             os.makedirs(cache_dir) | ||||
|         if not os.path.exists(clips_dir) and not os.path.islink(clips_dir): | ||||
|             os.makedirs(clips_dir) | ||||
| 
 | ||||
|         for camera_name, camera_config in self.config['cameras'].items(): | ||||
| 
 | ||||
|             # set shape | ||||
|             if 'width' in camera_config and 'height' in camera_config: | ||||
|                 frame_shape = (camera_config['height'], camera_config['width'], 3) | ||||
|             else: | ||||
|                 frame_shape = get_frame_shape(camera_config['ffmpeg']['input']) | ||||
|          | ||||
|             camera_config['frame_shape'] = frame_shape | ||||
| 
 | ||||
|             # build ffmpeg command | ||||
|             ffmpeg = camera_config['ffmpeg'] | ||||
|             ffmpeg_input = ffmpeg['input'].format(**frigate_env_vars) | ||||
|             ffmpeg_global_args = ffmpeg.get('global_args', self.config['ffmpeg']['global_args']) | ||||
|             ffmpeg_hwaccel_args = ffmpeg.get('hwaccel_args', self.config['ffmpeg']['hwaccel_args']) | ||||
|             ffmpeg_input_args = ffmpeg.get('input_args', self.config['ffmpeg']['input_args']) | ||||
|             ffmpeg_output_args = ffmpeg.get('output_args', self.config['ffmpeg']['output_args']) | ||||
|             if not camera_config.get('fps') is None: | ||||
|                 ffmpeg_output_args = ["-r", str(camera_config['fps'])] + ffmpeg_output_args | ||||
|             if camera_config['save_clips']['enabled']: | ||||
|                 ffmpeg_output_args = [ | ||||
|                     "-f", | ||||
|                     "segment", | ||||
|                     "-segment_time", | ||||
|                     "10", | ||||
|                     "-segment_format", | ||||
|                     "mp4", | ||||
|                     "-reset_timestamps", | ||||
|                     "1", | ||||
|                     "-strftime", | ||||
|                     "1", | ||||
|                     "-c", | ||||
|                     "copy", | ||||
|                     "-an", | ||||
|                     "-map", | ||||
|                     "0", | ||||
|                     f"{os.path.join(self.config['save_clips']['cache_dir'], camera_name)}-%Y%m%d%H%M%S.mp4" | ||||
|                 ] + ffmpeg_output_args | ||||
|             ffmpeg_cmd = (['ffmpeg'] + | ||||
|                     ffmpeg_global_args + | ||||
|                     ffmpeg_hwaccel_args + | ||||
|                     ffmpeg_input_args + | ||||
|                     ['-i', ffmpeg_input] + | ||||
|                     ffmpeg_output_args + | ||||
|                     ['pipe:']) | ||||
|              | ||||
|             camera_config['ffmpeg_cmd'] = ffmpeg_cmd | ||||
| 
 | ||||
|         for camera_name in self.config.cameras.keys(): | ||||
|             # create camera_metrics | ||||
|             self.camera_metrics[camera_name] = { | ||||
|                 'camera_fps': mp.Value('d', 0.0), | ||||
| @ -118,10 +49,10 @@ class FrigateApp(): | ||||
|         self.event_queue = mp.Queue() | ||||
| 
 | ||||
|         # Queue for cameras to push tracked objects to | ||||
|         self.detected_frames_queue = mp.Queue(maxsize=len(self.config['cameras'].keys())*2) | ||||
|         self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2) | ||||
| 
 | ||||
|     def init_database(self): | ||||
|         self.db = SqliteExtDatabase(f"/{os.path.join(self.config['save_clips']['clips_dir'], 'frigate.db')}") | ||||
|         self.db = SqliteExtDatabase(f"/{os.path.join(self.config.save_clips.clips_dir, 'frigate.db')}") | ||||
|         models = [Event] | ||||
|         self.db.bind(models) | ||||
|         self.db.create_tables(models, safe=True) | ||||
| @ -130,38 +61,29 @@ class FrigateApp(): | ||||
|         self.flask_app = create_app(self.config, self.db, self.camera_metrics, self.detectors, self.detected_frames_processor) | ||||
| 
 | ||||
|     def init_mqtt(self): | ||||
|         # TODO: create config class | ||||
|         mqtt_config = self.config['mqtt'] | ||||
|         self.mqtt_client = create_mqtt_client( | ||||
|             mqtt_config['host'], | ||||
|             mqtt_config['port'], | ||||
|             mqtt_config['topic_prefix'], | ||||
|             mqtt_config['client_id'], | ||||
|             mqtt_config.get('user'), | ||||
|             mqtt_config.get('password') | ||||
|         ) | ||||
|         self.mqtt_client = create_mqtt_client(self.config.mqtt) | ||||
| 
 | ||||
|     def start_detectors(self): | ||||
|         for name in self.config['cameras'].keys(): | ||||
|         for name in self.config.cameras.keys(): | ||||
|             self.detection_out_events[name] = mp.Event() | ||||
|             shm_in = mp.shared_memory.SharedMemory(name=name, create=True, size=300*300*3) | ||||
|             shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}", create=True, size=20*6*4) | ||||
|             self.detection_shms.append(shm_in) | ||||
|             self.detection_shms.append(shm_out) | ||||
| 
 | ||||
|         for name, detector in self.config['detectors'].items(): | ||||
|             if detector['type'] == 'cpu': | ||||
|         for name, detector in self.config.detectors.items(): | ||||
|             if detector.type == 'cpu': | ||||
|                 self.detectors[name] = EdgeTPUProcess(self.detection_queue, out_events=self.detection_out_events, tf_device='cpu') | ||||
|             if detector['type'] == 'edgetpu': | ||||
|                 self.detectors[name] = EdgeTPUProcess(self.detection_queue, out_events=self.detection_out_events, tf_device=detector['device']) | ||||
|             if detector.type == 'edgetpu': | ||||
|                 self.detectors[name] = EdgeTPUProcess(self.detection_queue, out_events=self.detection_out_events, tf_device=detector.device) | ||||
| 
 | ||||
|     def start_detected_frames_processor(self): | ||||
|         self.detected_frames_processor = TrackedObjectProcessor(self.config['cameras'], self.mqtt_client, self.config['mqtt']['topic_prefix'],  | ||||
|         self.detected_frames_processor = TrackedObjectProcessor(self.config.cameras, self.mqtt_client, self.config.mqtt.topic_prefix,  | ||||
|             self.detected_frames_queue, self.event_queue, self.stop_event) | ||||
|         self.detected_frames_processor.start() | ||||
| 
 | ||||
|     def start_camera_processors(self): | ||||
|         for name, config in self.config['cameras'].items(): | ||||
|         for name, config in self.config.cameras.items(): | ||||
|             camera_process = mp.Process(target=track_camera, args=(name, config, | ||||
|                 self.detection_queue, self.detection_out_events[name], self.detected_frames_queue,  | ||||
|                 self.camera_metrics[name])) | ||||
| @ -171,7 +93,7 @@ class FrigateApp(): | ||||
|             print(f"Camera processor started for {name}: {camera_process.pid}") | ||||
| 
 | ||||
|     def start_camera_capture_processes(self): | ||||
|         for name, config in self.config['cameras'].items(): | ||||
|         for name, config in self.config.cameras.items(): | ||||
|             capture_process = mp.Process(target=capture_camera, args=(name, config, | ||||
|                 self.camera_metrics[name])) | ||||
|             capture_process.daemon = True | ||||
| @ -199,7 +121,7 @@ class FrigateApp(): | ||||
|         self.init_web_server() | ||||
|         self.start_event_processor() | ||||
|         self.start_watchdog() | ||||
|         self.flask_app.run(host='0.0.0.0', port=self.config['web_port'], debug=False) | ||||
|         self.flask_app.run(host='0.0.0.0', port=self.config.web_port, debug=False) | ||||
|         self.stop() | ||||
|      | ||||
|     def stop(self): | ||||
|  | ||||
| @ -1,5 +1,17 @@ | ||||
| import base64 | ||||
| import json | ||||
| import os | ||||
| import yaml | ||||
| 
 | ||||
| from typing import Dict | ||||
| 
 | ||||
| import cv2 | ||||
| import matplotlib.pyplot as plt | ||||
| import numpy as np | ||||
| import voluptuous as vol | ||||
| 
 | ||||
| from frigate.util import get_frame_shape | ||||
| 
 | ||||
| DETECTORS_SCHEMA = vol.Schema( | ||||
|     { | ||||
|         vol.Required(str): { | ||||
| @ -66,12 +78,21 @@ FILTER_SCHEMA = vol.Schema( | ||||
|     } | ||||
| ) | ||||
| 
 | ||||
| OBJECTS_SCHEMA = vol.Schema( | ||||
| def filters_for_all_tracked_objects(object_config): | ||||
|     for tracked_object in object_config.get('track', ['person']): | ||||
|         if not 'filters' in object_config: | ||||
|             object_config['filters'] = {} | ||||
|         if not tracked_object in object_config['filters']: | ||||
|             object_config['filters'][tracked_object] = {} | ||||
|     return object_config | ||||
| 
 | ||||
| OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects, | ||||
|     { | ||||
|         vol.Optional('track', default=['person']): [str], | ||||
|         'filters': FILTER_SCHEMA.extend({vol.Optional('min_score', default=0.5): float}) | ||||
|         # TODO: this should populate filters for all tracked objects | ||||
|         vol.Optional('filters', default = {}): FILTER_SCHEMA.extend({ str: {vol.Optional('min_score', default=0.5): float}}) | ||||
|     } | ||||
| ) | ||||
| )) | ||||
| 
 | ||||
| DEFAULT_CAMERA_MQTT = { | ||||
|     'crop_to_region': True | ||||
| @ -99,8 +120,8 @@ CAMERAS_SCHEMA = vol.Schema( | ||||
|     { | ||||
|         str: { | ||||
|             vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA, | ||||
|             'height': int, | ||||
|             'width': int, | ||||
|             vol.Required('height'): int, | ||||
|             vol.Required('width'): int, | ||||
|             'fps': int, | ||||
|             'mask': str, | ||||
|             vol.Optional('best_image_timeout', default=60): int, | ||||
| @ -140,3 +161,438 @@ FRIGATE_CONFIG_SCHEMA = vol.Schema( | ||||
|         vol.Required('cameras', default={}): CAMERAS_SCHEMA | ||||
|     } | ||||
| ) | ||||
| 
 | ||||
| class DetectorConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._type = config['type'] | ||||
|         self._device = config['device'] | ||||
|      | ||||
|     @property | ||||
|     def type(self): | ||||
|         return self._type | ||||
|      | ||||
|     @property | ||||
|     def device(self): | ||||
|         return self._device | ||||
| 
 | ||||
| 
 | ||||
| class MqttConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._host = config['host'] | ||||
|         self._port = config['port'] | ||||
|         self._topic_prefix = config['topic_prefix'] | ||||
|         self._client_id = config['client_id'] | ||||
|         self._user = config.get('user') | ||||
|         self._password = config.get('password') | ||||
|      | ||||
|     @property | ||||
|     def host(self): | ||||
|         return self._host | ||||
|      | ||||
|     @property | ||||
|     def port(self): | ||||
|         return self._port | ||||
|      | ||||
|     @property | ||||
|     def topic_prefix(self): | ||||
|         return self._topic_prefix | ||||
|      | ||||
|     @property | ||||
|     def client_id(self): | ||||
|         return self._client_id | ||||
|      | ||||
|     @property | ||||
|     def user(self): | ||||
|         return self._user | ||||
|      | ||||
|     @property | ||||
|     def password(self): | ||||
|         return self._password | ||||
| 
 | ||||
| class SaveClipsConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._max_seconds = config['max_seconds'] | ||||
|         self._clips_dir = config['clips_dir'] | ||||
|         self._cache_dir = config['cache_dir'] | ||||
|      | ||||
|     @property | ||||
|     def max_seconds(self): | ||||
|         return self._max_seconds | ||||
|      | ||||
|     @property | ||||
|     def clips_dir(self): | ||||
|         return self._clips_dir | ||||
|      | ||||
|     @property | ||||
|     def cache_dir(self): | ||||
|         return self._cache_dir | ||||
| 
 | ||||
| class FfmpegConfig(): | ||||
|     def __init__(self, global_config, config): | ||||
|         self._input = config.get('input') | ||||
|         self._global_args = config.get('global_args', global_config['global_args']) | ||||
|         self._hwaccel_args = config.get('hwaccel_args', global_config['hwaccel_args']) | ||||
|         self._input_args = config.get('input_args', global_config['input_args']) | ||||
|         self._output_args = config.get('output_args', global_config['output_args']) | ||||
|      | ||||
|     @property | ||||
|     def input(self): | ||||
|         return self._input | ||||
|      | ||||
|     @property | ||||
|     def global_args(self): | ||||
|         return self._global_args | ||||
|      | ||||
|     @property | ||||
|     def hwaccel_args(self): | ||||
|         return self._hwaccel_args | ||||
| 
 | ||||
|     @property | ||||
|     def input_args(self): | ||||
|         return self._input_args | ||||
|      | ||||
|     @property | ||||
|     def output_args(self): | ||||
|         return self._output_args | ||||
| 
 | ||||
| class FilterConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._min_area = config['min_area'] | ||||
|         self._max_area = config['max_area'] | ||||
|         self._threshold = config['threshold'] | ||||
|         self._min_score = config.get('min_score') | ||||
|      | ||||
|     @property | ||||
|     def min_area(self): | ||||
|         return self._min_area | ||||
| 
 | ||||
|     @property | ||||
|     def max_area(self): | ||||
|         return self._max_area | ||||
| 
 | ||||
|     @property | ||||
|     def threshold(self): | ||||
|         return self._threshold | ||||
|      | ||||
|     @property | ||||
|     def min_score(self): | ||||
|         return self._min_score | ||||
| 
 | ||||
| class ObjectConfig(): | ||||
|     def __init__(self, global_config, config): | ||||
|         self._track = config.get('track', global_config['track']) | ||||
|         if 'filters' in config: | ||||
|             self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() } | ||||
|         else: | ||||
|             self._filters = { name: FilterConfig(c) for name, c in global_config['filters'].items() } | ||||
|      | ||||
|     @property | ||||
|     def track(self): | ||||
|         return self._track | ||||
|      | ||||
|     @property | ||||
|     def filters(self) -> Dict[str, FilterConfig]: | ||||
|         return self._filters | ||||
| 
 | ||||
| class CameraSnapshotsConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._show_timestamp = config['show_timestamp'] | ||||
|         self._draw_zones = config['draw_zones'] | ||||
|         self._draw_bounding_boxes = config['draw_bounding_boxes'] | ||||
|      | ||||
|     @property | ||||
|     def show_timestamp(self): | ||||
|         return self._show_timestamp | ||||
|      | ||||
|     @property | ||||
|     def draw_zones(self): | ||||
|         return self._draw_zones | ||||
| 
 | ||||
|     @property | ||||
|     def draw_bounding_boxes(self): | ||||
|         return self._draw_bounding_boxes | ||||
| 
 | ||||
| class CameraSaveClipsConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._enabled = config['enabled'] | ||||
|         self._pre_capture = config['pre_capture'] | ||||
|         self._objects = config.get('objects') | ||||
|      | ||||
|     @property | ||||
|     def enabled(self): | ||||
|         return self._enabled | ||||
|      | ||||
|     @property | ||||
|     def pre_capture(self): | ||||
|         return self._pre_capture | ||||
| 
 | ||||
|     @property | ||||
|     def objects(self): | ||||
|         return self._objects | ||||
| 
 | ||||
| class CameraMqttConfig(): | ||||
|     def __init__(self, config): | ||||
|         self._crop_to_region = config['crop_to_region'] | ||||
|         self._snapshot_height = config.get('snapshot_height') | ||||
|      | ||||
|     @property | ||||
|     def crop_to_region(self): | ||||
|         return self._crop_to_region | ||||
|      | ||||
|     @property | ||||
|     def snapshot_height(self): | ||||
|         return self._snapshot_height | ||||
| 
 | ||||
| class ZoneConfig(): | ||||
|     def __init__(self, name, config): | ||||
|         self._coordinates = config['coordinates'] | ||||
|         self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() } | ||||
| 
 | ||||
|         if isinstance(self._coordinates, list): | ||||
|             self._contour =  np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates]) | ||||
|         elif isinstance(self._coordinates, str): | ||||
|             points = self._coordinates.split(',') | ||||
|             self._contour =  np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)]) | ||||
|         else: | ||||
|             print(f"Unable to parse zone coordinates for {name}") | ||||
|             self._contour = np.array([]) | ||||
|          | ||||
|         self._color = (0,0,0) | ||||
|      | ||||
|     @property | ||||
|     def coordinates(self): | ||||
|         return self._coordinates | ||||
|      | ||||
|     @property | ||||
|     def contour(self): | ||||
|         return self._contour | ||||
|      | ||||
|     @contour.setter | ||||
|     def contour(self, val): | ||||
|         self._contour = val | ||||
|      | ||||
|     @property | ||||
|     def color(self): | ||||
|         return self._color | ||||
|      | ||||
|     @color.setter | ||||
|     def color(self, val): | ||||
|         self._color = val | ||||
|      | ||||
|     @property | ||||
|     def filters(self): | ||||
|         return self._filters | ||||
| 
 | ||||
| class CameraConfig(): | ||||
|     def __init__(self, name, config, cache_dir, global_ffmpeg, global_objects): | ||||
|         self._name = name | ||||
|         self._ffmpeg = FfmpegConfig(global_ffmpeg, config['ffmpeg']) | ||||
|         self._height = config.get('height') | ||||
|         self._width = config.get('width') | ||||
|         self._frame_shape = (self._height, self._width) | ||||
|         self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1]) | ||||
|         self._fps = config.get('fps') | ||||
|         self._mask = self._create_mask(config.get('mask')) | ||||
|         self._best_image_timeout = config['best_image_timeout'] | ||||
|         self._mqtt = CameraMqttConfig(config['mqtt']) | ||||
|         self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() } | ||||
|         self._save_clips = CameraSaveClipsConfig(config['save_clips']) | ||||
|         self._snapshots = CameraSnapshotsConfig(config['snapshots']) | ||||
|         self._objects = ObjectConfig(global_objects, config.get('objects', {})) | ||||
| 
 | ||||
|         self._ffmpeg_cmd = self._get_ffmpeg_cmd(cache_dir) | ||||
| 
 | ||||
|         self._set_zone_colors(self._zones) | ||||
| 
 | ||||
|     def _create_mask(self, mask): | ||||
|         if mask: | ||||
|             if mask.startswith('base64,'): | ||||
|                 img = base64.b64decode(mask[7:])  | ||||
|                 np_img = np.fromstring(img, dtype=np.uint8) | ||||
|                 mask_img = cv2.imdecode(np_img, cv2.IMREAD_GRAYSCALE) | ||||
|             elif mask.startswith('poly,'): | ||||
|                 points = mask.split(',')[1:] | ||||
|                 contour =  np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)]) | ||||
|                 mask_img = np.zeros(self.frame_shape, np.uint8) | ||||
|                 mask_img[:] = 255 | ||||
|                 cv2.fillPoly(mask_img, pts=[contour], color=(0)) | ||||
|             else: | ||||
|                 mask_img = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE) | ||||
|         else: | ||||
|             mask_img = None | ||||
| 
 | ||||
|         if mask_img is None or mask_img.size == 0: | ||||
|             mask_img = np.zeros(self.frame_shape, np.uint8) | ||||
|             mask_img[:] = 255 | ||||
|          | ||||
|         return mask_img | ||||
| 
 | ||||
|     def _get_ffmpeg_cmd(self, cache_dir): | ||||
|         ffmpeg_output_args = self.ffmpeg.output_args | ||||
|         if self.fps: | ||||
|             ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args | ||||
|         if self.save_clips.enabled: | ||||
|             ffmpeg_output_args = [ | ||||
|                 "-f", | ||||
|                 "segment", | ||||
|                 "-segment_time", | ||||
|                 "10", | ||||
|                 "-segment_format", | ||||
|                 "mp4", | ||||
|                 "-reset_timestamps", | ||||
|                 "1", | ||||
|                 "-strftime", | ||||
|                 "1", | ||||
|                 "-c", | ||||
|                 "copy", | ||||
|                 "-an", | ||||
|                 f"{os.path.join(cache_dir, self.name)}-%Y%m%d%H%M%S.mp4" | ||||
|             ] + ffmpeg_output_args | ||||
|         return (['ffmpeg'] + | ||||
|                 self.ffmpeg.global_args + | ||||
|                 self.ffmpeg.hwaccel_args + | ||||
|                 self.ffmpeg.input_args + | ||||
|                 ['-i', self.ffmpeg.input] + | ||||
|                 ffmpeg_output_args + | ||||
|                 ['pipe:']) | ||||
|      | ||||
|     def _set_zone_colors(self, zones: Dict[str, ZoneConfig]): | ||||
|         # set colors for zones | ||||
|         all_zone_names = zones.keys() | ||||
|         zone_colors = {} | ||||
|         colors = plt.cm.get_cmap('tab10', len(all_zone_names)) | ||||
|         for i, zone in enumerate(all_zone_names): | ||||
|             zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3]) | ||||
|          | ||||
|         for name, zone in zones.items(): | ||||
|             zone.color = zone_colors[name] | ||||
|      | ||||
|     @property | ||||
|     def name(self): | ||||
|         return self._name | ||||
| 
 | ||||
|     @property | ||||
|     def ffmpeg(self): | ||||
|         return self._ffmpeg | ||||
|      | ||||
|     @property | ||||
|     def height(self): | ||||
|         return self._height | ||||
|      | ||||
|     @property | ||||
|     def width(self): | ||||
|         return self._width | ||||
|      | ||||
|     @property | ||||
|     def fps(self): | ||||
|         return self._fps | ||||
|      | ||||
|     @property | ||||
|     def mask(self): | ||||
|         return self._mask | ||||
|      | ||||
|     @property | ||||
|     def best_image_timeout(self): | ||||
|         return self._best_image_timeout | ||||
|      | ||||
|     @property | ||||
|     def mqtt(self): | ||||
|         return self._mqtt | ||||
|      | ||||
|     @property | ||||
|     def zones(self)-> Dict[str, ZoneConfig]: | ||||
|         return self._zones | ||||
|      | ||||
|     @property | ||||
|     def save_clips(self): | ||||
|         return self._save_clips | ||||
|      | ||||
|     @property | ||||
|     def snapshots(self): | ||||
|         return self._snapshots | ||||
|      | ||||
|     @property | ||||
|     def objects(self): | ||||
|         return self._objects | ||||
| 
 | ||||
|     @property | ||||
|     def frame_shape(self): | ||||
|         return self._frame_shape | ||||
| 
 | ||||
|     @property | ||||
|     def frame_shape_yuv(self): | ||||
|         return self._frame_shape_yuv | ||||
| 
 | ||||
|     @property | ||||
|     def ffmpeg_cmd(self): | ||||
|         return self._ffmpeg_cmd | ||||
| 
 | ||||
| class FrigateConfig(): | ||||
|     def __init__(self, config_file=None, config=None): | ||||
|         if config is None and config_file is None: | ||||
|             raise ValueError('config or config_file must be defined') | ||||
|         elif not config_file is None: | ||||
|             config = self._load_file(config_file) | ||||
|          | ||||
|         config = FRIGATE_CONFIG_SCHEMA(config) | ||||
| 
 | ||||
|         config = self._sub_env_vars(config) | ||||
| 
 | ||||
|         self._web_port = config['web_port'] | ||||
|         self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() } | ||||
|         self._mqtt = MqttConfig(config['mqtt']) | ||||
|         self._save_clips = SaveClipsConfig(config['save_clips']) | ||||
|         self._cameras = { name: CameraConfig(name, c, self._save_clips.cache_dir, config['ffmpeg'], config['objects']) for name, c in config['cameras'].items() } | ||||
| 
 | ||||
|         self._ensure_dirs() | ||||
| 
 | ||||
|     def _sub_env_vars(self, config): | ||||
|         frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')} | ||||
| 
 | ||||
|         if 'password' in config['mqtt']: | ||||
|             config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)  | ||||
|          | ||||
|         for camera in config['cameras'].values(): | ||||
|             camera['ffmpeg']['input'] = camera['ffmpeg']['input'].format(**frigate_env_vars) | ||||
|          | ||||
|         return config | ||||
|      | ||||
|     def _ensure_dirs(self): | ||||
|         cache_dir = self.save_clips.cache_dir | ||||
|         clips_dir = self.save_clips.clips_dir | ||||
| 
 | ||||
|         if not os.path.exists(cache_dir) and not os.path.islink(cache_dir): | ||||
|             os.makedirs(cache_dir) | ||||
|         if not os.path.exists(clips_dir) and not os.path.islink(clips_dir): | ||||
|             os.makedirs(clips_dir) | ||||
| 
 | ||||
|     def _load_file(self, config_file): | ||||
|         with open(config_file) as f: | ||||
|             raw_config = f.read() | ||||
|          | ||||
|         if config_file.endswith(".yml"):     | ||||
|             config = yaml.safe_load(raw_config) | ||||
|         elif config_file.endswith(".json"): | ||||
|             config = json.loads(raw_config) | ||||
|          | ||||
|         return config | ||||
|      | ||||
|     @property | ||||
|     def web_port(self): | ||||
|         return self._web_port | ||||
|      | ||||
|     @property | ||||
|     def detectors(self) -> Dict[str, DetectorConfig]: | ||||
|         return self._detectors | ||||
|      | ||||
|     @property | ||||
|     def mqtt(self): | ||||
|         return self._mqtt | ||||
|      | ||||
|     @property | ||||
|     def save_clips(self): | ||||
|         return self._save_clips | ||||
| 
 | ||||
|     @property | ||||
|     def cameras(self) -> Dict[str, CameraConfig]: | ||||
|         return self._cameras | ||||
| @ -14,8 +14,8 @@ class EventProcessor(threading.Thread): | ||||
|     def __init__(self, config, camera_processes, event_queue, stop_event): | ||||
|         threading.Thread.__init__(self) | ||||
|         self.config = config | ||||
|         self.cache_dir = self.config['save_clips']['cache_dir'] | ||||
|         self.clips_dir = self.config['save_clips']['clips_dir'] | ||||
|         self.cache_dir = self.config.save_clips.cache_dir | ||||
|         self.clips_dir = self.config.save_clips.clips_dir | ||||
|         self.camera_processes = camera_processes | ||||
|         self.cached_clips = {} | ||||
|         self.event_queue = event_queue | ||||
| @ -77,7 +77,7 @@ class EventProcessor(threading.Thread): | ||||
|             earliest_event = datetime.datetime.now().timestamp() | ||||
| 
 | ||||
|         # if the earliest event exceeds the max seconds, cap it | ||||
|         max_seconds = self.config['save_clips']['max_seconds'] | ||||
|         max_seconds = self.config.save_clips.max_seconds | ||||
|         if datetime.datetime.now().timestamp()-earliest_event > max_seconds: | ||||
|             earliest_event = datetime.datetime.now().timestamp()-max_seconds | ||||
|          | ||||
| @ -163,15 +163,16 @@ class EventProcessor(threading.Thread): | ||||
| 
 | ||||
|             self.refresh_cache() | ||||
| 
 | ||||
|             save_clips_config = self.config['cameras'][camera].get('save_clips', {}) | ||||
|             save_clips_config = self.config.cameras[camera].save_clips | ||||
| 
 | ||||
|             # if save clips is not enabled for this camera, just continue | ||||
|             if not save_clips_config.get('enabled', False): | ||||
|             if not save_clips_config.enabled: | ||||
|                 continue | ||||
| 
 | ||||
|             # if specific objects are listed for this camera, only save clips for them | ||||
|             if 'objects' in save_clips_config: | ||||
|                 if not event_data['label'] in save_clips_config['objects']: | ||||
|             # TODO: default to all tracked objects rather than checking for None | ||||
|             if save_clips_config.objects: | ||||
|                 if not event_data['label'] in save_clips_config.objects: | ||||
|                     continue | ||||
| 
 | ||||
|             if event_type == 'start': | ||||
| @ -190,7 +191,7 @@ class EventProcessor(threading.Thread): | ||||
|                 ) | ||||
| 
 | ||||
|                 if len(self.cached_clips) > 0 and not event_data['false_positive']: | ||||
|                     self.create_clip(camera, event_data, save_clips_config.get('pre_capture', 30)) | ||||
|                     self.create_clip(camera, event_data, save_clips_config.pre_capture) | ||||
|                 del self.events_in_process[event_data['id']] | ||||
| 
 | ||||
|                  | ||||
| @ -75,7 +75,7 @@ def stats(): | ||||
| 
 | ||||
| @bp.route('/<camera_name>/<label>/best.jpg') | ||||
| def best(camera_name, label): | ||||
|     if camera_name in current_app.frigate_config['cameras']: | ||||
|     if camera_name in current_app.frigate_config.cameras: | ||||
|         best_object = current_app.detected_frames_processor.get_best(camera_name, label) | ||||
|         best_frame = best_object.get('frame') | ||||
|         if best_frame is None: | ||||
| @ -103,7 +103,7 @@ def best(camera_name, label): | ||||
| def mjpeg_feed(camera_name): | ||||
|     fps = int(request.args.get('fps', '3')) | ||||
|     height = int(request.args.get('h', '360')) | ||||
|     if camera_name in current_app.frigate_config['cameras']: | ||||
|     if camera_name in current_app.frigate_config.cameras: | ||||
|         # return a multipart response | ||||
|         return Response(imagestream(current_app.detected_frames_processor, camera_name, fps, height), | ||||
|                         mimetype='multipart/x-mixed-replace; boundary=frame') | ||||
| @ -112,7 +112,7 @@ def mjpeg_feed(camera_name): | ||||
| 
 | ||||
| @bp.route('/<camera_name>/latest.jpg') | ||||
| def latest_frame(camera_name): | ||||
|     if camera_name in current_app.frigate_config['cameras']: | ||||
|     if camera_name in current_app.frigate_config.cameras: | ||||
|         # max out at specified FPS | ||||
|         frame = current_app.detected_frames_processor.get_current_frame(camera_name) | ||||
|         if frame is None: | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import paho.mqtt.client as mqtt | ||||
| 
 | ||||
| def create_mqtt_client(host: str, port: int, client_id: str, topic_prefix: str, user: str, password: str): | ||||
|     client = mqtt.Client(client_id=client_id) | ||||
| from frigate.config import MqttConfig | ||||
| 
 | ||||
| def create_mqtt_client(config: MqttConfig): | ||||
|     client = mqtt.Client(client_id=config.client_id) | ||||
|     def on_connect(client, userdata, flags, rc): | ||||
|         # TODO: use logging library | ||||
|         print("On connect called") | ||||
| @ -14,11 +16,11 @@ def create_mqtt_client(host: str, port: int, client_id: str, topic_prefix: str, | ||||
|                 print ("MQTT Not authorized") | ||||
|             else: | ||||
|                 print ("Unable to connect to MQTT: Connection refused. Error code: " + str(rc)) | ||||
|         client.publish(topic_prefix+'/available', 'online', retain=True)        | ||||
|         client.publish(config.topic_prefix+'/available', 'online', retain=True)        | ||||
|     client.on_connect = on_connect | ||||
|     client.will_set(topic_prefix+'/available', payload='offline', qos=1, retain=True) | ||||
|     if not user is None: | ||||
|         client.username_pw_set(user, password=password) | ||||
|     client.connect(host, port, 60) | ||||
|     client.will_set(config.topic_prefix+'/available', payload='offline', qos=1, retain=True) | ||||
|     if not config.user is None: | ||||
|         client.username_pw_set(config.user, password=config.password) | ||||
|     client.connect(config.host, config.port, 60) | ||||
|     client.loop_start() | ||||
|     return client | ||||
| @ -13,6 +13,7 @@ import itertools | ||||
| import matplotlib.pyplot as plt | ||||
| from frigate.util import draw_box_with_label, SharedMemoryFrameManager | ||||
| from frigate.edgetpu import load_labels | ||||
| from frigate.config import CameraConfig | ||||
| from typing import Callable, Dict | ||||
| from statistics import mean, median | ||||
| 
 | ||||
| @ -33,16 +34,16 @@ def zone_filtered(obj, object_config): | ||||
| 
 | ||||
|         # if the min area is larger than the | ||||
|         # detected object, don't add it to detected objects | ||||
|         if obj_settings.get('min_area',-1) > obj['area']: | ||||
|         if obj_settings.min_area > obj['area']: | ||||
|             return True | ||||
|          | ||||
|         # if the detected object is larger than the | ||||
|         # max area, don't add it to detected objects | ||||
|         if obj_settings.get('max_area', 24000000) < obj['area']: | ||||
|         if obj_settings.max_area < obj['area']: | ||||
|             return True | ||||
| 
 | ||||
|         # if the score is lower than the threshold, skip | ||||
|         if obj_settings.get('threshold', 0) > obj['computed_score']: | ||||
|         if obj_settings.threshold > obj['computed_score']: | ||||
|             return True | ||||
|          | ||||
|     return False | ||||
| @ -58,7 +59,7 @@ class CameraState(): | ||||
|         self.object_status = defaultdict(lambda: 'OFF') | ||||
|         self.tracked_objects = {} | ||||
|         self.zone_objects = defaultdict(lambda: []) | ||||
|         self._current_frame = np.zeros((self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1]), np.uint8) | ||||
|         self._current_frame = np.zeros(self.config.frame_shape_yuv, np.uint8) | ||||
|         self.current_frame_lock = threading.Lock() | ||||
|         self.current_frame_time = 0.0 | ||||
|         self.previous_frame_id = None | ||||
| @ -89,14 +90,14 @@ class CameraState(): | ||||
|                 region = obj['region'] | ||||
|                 cv2.rectangle(frame_copy, (region[0], region[1]), (region[2], region[3]), (0,255,0), 1) | ||||
|              | ||||
|             if self.config['snapshots']['show_timestamp']: | ||||
|             if self.config.snapshots.show_timestamp: | ||||
|                 time_to_show = datetime.datetime.fromtimestamp(frame_time).strftime("%m/%d/%Y %H:%M:%S") | ||||
|                 cv2.putText(frame_copy, time_to_show, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, fontScale=.8, color=(255, 255, 255), thickness=2) | ||||
| 
 | ||||
|             if self.config['snapshots']['draw_zones']: | ||||
|                 for name, zone in self.config['zones'].items(): | ||||
|             if self.config.snapshots.draw_zones: | ||||
|                 for name, zone in self.config.zones.items(): | ||||
|                     thickness = 8 if any([name in obj['zones'] for obj in tracked_objects.values()]) else 2 | ||||
|                     cv2.drawContours(frame_copy, [zone['contour']], -1, zone['color'], thickness) | ||||
|                     cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness) | ||||
|          | ||||
|         return frame_copy | ||||
| 
 | ||||
| @ -105,7 +106,7 @@ class CameraState(): | ||||
|         if not obj.get('false_positive', True): | ||||
|             return False | ||||
| 
 | ||||
|         threshold = self.config['objects'].get('filters', {}).get(obj['label'], {}).get('threshold', 0.85) | ||||
|         threshold = self.config.objects.filters[obj['label']].threshold | ||||
|         if obj['computed_score'] < threshold: | ||||
|             return True | ||||
|         return False | ||||
| @ -124,7 +125,7 @@ class CameraState(): | ||||
|         self.current_frame_time = frame_time | ||||
|         # get the new frame and delete the old frame | ||||
|         frame_id = f"{self.name}{frame_time}" | ||||
|         current_frame = self.frame_manager.get(frame_id, (self.config['frame_shape'][0]*3//2, self.config['frame_shape'][1])) | ||||
|         current_frame = self.frame_manager.get(frame_id, self.config.frame_shape_yuv) | ||||
| 
 | ||||
|         current_ids = tracked_objects.keys() | ||||
|         previous_ids = self.tracked_objects.keys() | ||||
| @ -184,12 +185,12 @@ class CameraState(): | ||||
|             current_zones = [] | ||||
|             bottom_center = (obj['centroid'][0], obj['box'][3]) | ||||
|             # check each zone | ||||
|             for name, zone in self.config['zones'].items(): | ||||
|                 contour = zone['contour'] | ||||
|             for name, zone in self.config.zones.items(): | ||||
|                 contour = zone.contour | ||||
|                 # check if the object is in the zone | ||||
|                 if (cv2.pointPolygonTest(contour, bottom_center, False) >= 0): | ||||
|                     # if the object passed the filters once, dont apply again | ||||
|                     if name in obj.get('zones', []) or not zone_filtered(obj, zone.get('filters', {})): | ||||
|                     if name in obj.get('zones', []) or not zone_filtered(obj, zone.filters): | ||||
|                         current_zones.append(name) | ||||
|                         obj['entered_zones'].add(name) | ||||
| 
 | ||||
| @ -208,7 +209,7 @@ class CameraState(): | ||||
|                 now = datetime.datetime.now().timestamp() | ||||
|                 # if the object is a higher score than the current best score  | ||||
|                 # or the current object is older than desired, use the new object | ||||
|                 if obj_copy['score'] > current_best['score'] or (now - current_best['frame_time']) > self.config.get('best_image_timeout', 60): | ||||
|                 if obj_copy['score'] > current_best['score'] or (now - current_best['frame_time']) > self.config.best_image_timeout: | ||||
|                     obj_copy['frame'] = np.copy(current_frame) | ||||
|                     self.best_objects[object_type] = obj_copy | ||||
|                     for c in self.callbacks['snapshot']: | ||||
| @ -249,7 +250,7 @@ class CameraState(): | ||||
|             self.previous_frame_id = frame_id | ||||
| 
 | ||||
| class TrackedObjectProcessor(threading.Thread): | ||||
|     def __init__(self, camera_config, client, topic_prefix, tracked_objects_queue, event_queue, stop_event): | ||||
|     def __init__(self, camera_config: Dict[str, CameraConfig], client, topic_prefix, tracked_objects_queue, event_queue, stop_event): | ||||
|         threading.Thread.__init__(self) | ||||
|         self.camera_config = camera_config | ||||
|         self.client = client | ||||
| @ -296,22 +297,22 @@ class TrackedObjectProcessor(threading.Thread): | ||||
|                 return | ||||
|              | ||||
|             best_frame = cv2.cvtColor(obj['frame'], cv2.COLOR_YUV2BGR_I420) | ||||
|             if self.camera_config[camera]['snapshots']['draw_bounding_boxes']: | ||||
|             if self.camera_config[camera].snapshots.draw_bounding_boxes: | ||||
|                 thickness = 2 | ||||
|                 color = COLOR_MAP[obj['label']] | ||||
|                 box = obj['box'] | ||||
|                 draw_box_with_label(best_frame, box[0], box[1], box[2], box[3], obj['label'], f"{int(obj['score']*100)}% {int(obj['area'])}", thickness=thickness, color=color) | ||||
|                  | ||||
|             mqtt_config = self.camera_config[camera].get('mqtt', {'crop_to_region': False}) | ||||
|             if mqtt_config.get('crop_to_region'): | ||||
|             mqtt_config = self.camera_config[camera].mqtt | ||||
|             if mqtt_config.crop_to_region: | ||||
|                 region = obj['region'] | ||||
|                 best_frame = best_frame[region[1]:region[3], region[0]:region[2]] | ||||
|             if 'snapshot_height' in mqtt_config:  | ||||
|                 height = int(mqtt_config['snapshot_height']) | ||||
|             if mqtt_config.snapshot_height:  | ||||
|                 height = mqtt_config.snapshot_height | ||||
|                 width = int(height*best_frame.shape[1]/best_frame.shape[0]) | ||||
|                 best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA) | ||||
|              | ||||
|             if self.camera_config[camera]['snapshots']['show_timestamp']: | ||||
|             if self.camera_config[camera].snapshots.show_timestamp: | ||||
|                 time_to_show = datetime.datetime.fromtimestamp(obj['frame_time']).strftime("%m/%d/%Y %H:%M:%S") | ||||
|                 size = cv2.getTextSize(time_to_show, cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=2) | ||||
|                 text_width = size[0][0] | ||||
| @ -351,26 +352,6 @@ class TrackedObjectProcessor(threading.Thread): | ||||
|         #   } | ||||
|         # } | ||||
|         self.zone_data = defaultdict(lambda: defaultdict(lambda: set())) | ||||
| 
 | ||||
|         # set colors for zones | ||||
|         all_zone_names = set([zone for config in self.camera_config.values() for zone in config['zones'].keys()]) | ||||
|         zone_colors = {} | ||||
|         colors = plt.cm.get_cmap('tab10', len(all_zone_names)) | ||||
|         for i, zone in enumerate(all_zone_names): | ||||
|             zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3]) | ||||
| 
 | ||||
|         # create zone contours | ||||
|         for camera_config in self.camera_config.values(): | ||||
|             for zone_name, zone_config in camera_config['zones'].items(): | ||||
|                 zone_config['color'] = zone_colors[zone_name] | ||||
|                 coordinates = zone_config['coordinates'] | ||||
|                 if isinstance(coordinates, list): | ||||
|                     zone_config['contour'] =  np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in coordinates]) | ||||
|                 elif isinstance(coordinates, str): | ||||
|                     points = coordinates.split(',') | ||||
|                     zone_config['contour'] =  np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)]) | ||||
|                 else: | ||||
|                     print(f"Unable to parse zone coordinates for {zone_name} - {camera}") | ||||
|          | ||||
|     def get_best(self, camera, label): | ||||
|         best_objects = self.camera_states[camera].best_objects | ||||
| @ -398,7 +379,7 @@ class TrackedObjectProcessor(threading.Thread): | ||||
|             camera_state.update(frame_time, current_tracked_objects) | ||||
| 
 | ||||
|             # update zone status for each label | ||||
|             for zone in camera_state.config['zones'].keys(): | ||||
|             for zone in camera_state.config.zones.keys(): | ||||
|                 # get labels for current camera and all labels in current zone | ||||
|                 labels_for_camera = set([obj['label'] for obj in camera_state.tracked_objects.values() if zone in obj['zones'] and not obj['false_positive']]) | ||||
|                 labels_to_check = labels_for_camera | set(self.zone_data[zone].keys()) | ||||
|  | ||||
| @ -1,14 +1,11 @@ | ||||
| import json | ||||
| from unittest import TestCase, main | ||||
| import voluptuous as vol | ||||
| from frigate.config import FRIGATE_CONFIG_SCHEMA | ||||
| from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig | ||||
| 
 | ||||
| class TestConfig(TestCase): | ||||
|     def test_empty(self): | ||||
|         FRIGATE_CONFIG_SCHEMA({}) | ||||
| 
 | ||||
|     def test_minimal(self): | ||||
|         minimal = { | ||||
|     def setUp(self): | ||||
|         self.minimal = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
| @ -16,11 +13,169 @@ class TestConfig(TestCase): | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     def test_empty(self): | ||||
|         FRIGATE_CONFIG_SCHEMA({}) | ||||
| 
 | ||||
|     def test_minimal(self): | ||||
|         FRIGATE_CONFIG_SCHEMA(self.minimal) | ||||
|      | ||||
|     def test_config_class(self): | ||||
|         FrigateConfig(config=self.minimal) | ||||
|      | ||||
|     def test_inherit_tracked_objects(self): | ||||
|         config = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'objects': { | ||||
|                 'track': ['person', 'dog'] | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('dog' in frigate_config.cameras['back'].objects.track) | ||||
|      | ||||
|     def test_override_tracked_objects(self): | ||||
|         config = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'objects': { | ||||
|                 'track': ['person', 'dog'] | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920, | ||||
|                     'objects': { | ||||
|                         'track': ['cat'] | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         FRIGATE_CONFIG_SCHEMA(minimal) | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('cat' in frigate_config.cameras['back'].objects.track) | ||||
|      | ||||
|     def test_default_object_filters(self): | ||||
|         config = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'objects': { | ||||
|                 'track': ['person', 'dog'] | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('dog' in frigate_config.cameras['back'].objects.filters) | ||||
|      | ||||
|     def test_inherit_object_filters(self): | ||||
|         config = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'objects': { | ||||
|                 'track': ['person', 'dog'], | ||||
|                 'filters': { | ||||
|                     'dog': { | ||||
|                         'threshold': 0.7 | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('dog' in frigate_config.cameras['back'].objects.filters) | ||||
|         assert(frigate_config.cameras['back'].objects.filters['dog'].threshold == 0.7) | ||||
|      | ||||
|     def test_override_object_filters(self): | ||||
|         config = { | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920, | ||||
|                     'objects': { | ||||
|                         'track': ['person', 'dog'], | ||||
|                         'filters': { | ||||
|                             'dog': { | ||||
|                                 'threshold': 0.7 | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('dog' in frigate_config.cameras['back'].objects.filters) | ||||
|         assert(frigate_config.cameras['back'].objects.filters['dog'].threshold == 0.7) | ||||
|      | ||||
|     def test_ffmpeg_params(self): | ||||
|         config = { | ||||
|             'ffmpeg': { | ||||
|                 'input_args': ['-re'] | ||||
|             }, | ||||
|             'mqtt': { | ||||
|                 'host': 'mqtt' | ||||
|             }, | ||||
|             'cameras': { | ||||
|                 'back': { | ||||
|                     'ffmpeg': { | ||||
|                         'input': 'rtsp://10.0.0.1:554/video' | ||||
|                     }, | ||||
|                     'height': 1080, | ||||
|                     'width': 1920, | ||||
|                     'objects': { | ||||
|                         'track': ['person', 'dog'], | ||||
|                         'filters': { | ||||
|                             'dog': { | ||||
|                                 'threshold': 0.7 | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         frigate_config = FrigateConfig(config=config) | ||||
|         assert('-re' in frigate_config.cameras['back'].ffmpeg_cmd) | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main(verbosity=2) | ||||
|  | ||||
| @ -4,7 +4,9 @@ import time | ||||
| import signal | ||||
| import traceback | ||||
| import collections | ||||
| import json | ||||
| import numpy as np | ||||
| import subprocess as sp | ||||
| import cv2 | ||||
| import threading | ||||
| import matplotlib.pyplot as plt | ||||
| @ -12,6 +14,36 @@ import hashlib | ||||
| from multiprocessing import shared_memory | ||||
| from typing import AnyStr | ||||
| 
 | ||||
| def get_frame_shape(source): | ||||
|     ffprobe_cmd = " ".join([ | ||||
|         'ffprobe', | ||||
|         '-v', | ||||
|         'panic', | ||||
|         '-show_error', | ||||
|         '-show_streams', | ||||
|         '-of', | ||||
|         'json', | ||||
|         '"'+source+'"' | ||||
|     ]) | ||||
|     print(ffprobe_cmd) | ||||
|     p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True) | ||||
|     (output, err) = p.communicate() | ||||
|     p_status = p.wait() | ||||
|     info = json.loads(output) | ||||
|     print(info) | ||||
| 
 | ||||
|     video_info = [s for s in info['streams'] if s['codec_type'] == 'video'][0] | ||||
| 
 | ||||
|     if video_info['height'] != 0 and video_info['width'] != 0: | ||||
|         return (video_info['height'], video_info['width'], 3) | ||||
|      | ||||
|     # fallback to using opencv if ffprobe didnt succeed | ||||
|     video = cv2.VideoCapture(source) | ||||
|     ret, frame = video.read() | ||||
|     frame_shape = frame.shape | ||||
|     video.release() | ||||
|     return frame_shape | ||||
| 
 | ||||
| def draw_box_with_label(frame, x_min, y_min, x_max, y_max, label, info, thickness=2, color=None, position='ul'): | ||||
|     if color is None: | ||||
|         color = (0,0,255) | ||||
|  | ||||
| @ -14,45 +14,12 @@ import json | ||||
| import base64 | ||||
| from typing import Dict, List | ||||
| from collections import defaultdict | ||||
| from frigate.config import CameraConfig | ||||
| from frigate.util import draw_box_with_label, yuv_region_2_rgb, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, FrameManager, SharedMemoryFrameManager | ||||
| from frigate.objects import ObjectTracker | ||||
| from frigate.edgetpu import RemoteObjectDetector | ||||
| from frigate.motion import MotionDetector | ||||
| 
 | ||||
| def get_frame_shape(source): | ||||
|     ffprobe_cmd = " ".join([ | ||||
|         'ffprobe', | ||||
|         '-v', | ||||
|         'panic', | ||||
|         '-show_error', | ||||
|         '-show_streams', | ||||
|         '-of', | ||||
|         'json', | ||||
|         '"'+source+'"' | ||||
|     ]) | ||||
|     print(ffprobe_cmd) | ||||
|     p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True) | ||||
|     (output, err) = p.communicate() | ||||
|     p_status = p.wait() | ||||
|     info = json.loads(output) | ||||
|     print(info) | ||||
| 
 | ||||
|     video_info = [s for s in info['streams'] if s['codec_type'] == 'video'][0] | ||||
| 
 | ||||
|     if video_info['height'] != 0 and video_info['width'] != 0: | ||||
|         return (video_info['height'], video_info['width'], 3) | ||||
|      | ||||
|     # fallback to using opencv if ffprobe didnt succeed | ||||
|     video = cv2.VideoCapture(source) | ||||
|     ret, frame = video.read() | ||||
|     frame_shape = frame.shape | ||||
|     video.release() | ||||
|     return frame_shape | ||||
| 
 | ||||
| def get_ffmpeg_input(ffmpeg_input): | ||||
|     frigate_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')} | ||||
|     return ffmpeg_input.format(**frigate_vars) | ||||
| 
 | ||||
| def filtered(obj, objects_to_track, object_filters, mask=None): | ||||
|     object_name = obj[0] | ||||
| 
 | ||||
| @ -64,16 +31,16 @@ def filtered(obj, objects_to_track, object_filters, mask=None): | ||||
| 
 | ||||
|         # if the min area is larger than the | ||||
|         # detected object, don't add it to detected objects | ||||
|         if obj_settings.get('min_area',-1) > obj[3]: | ||||
|         if obj_settings.min_area > obj[3]: | ||||
|             return True | ||||
|          | ||||
|         # if the detected object is larger than the | ||||
|         # max area, don't add it to detected objects | ||||
|         if obj_settings.get('max_area', 24000000) < obj[3]: | ||||
|         if obj_settings.max_area < obj[3]: | ||||
|             return True | ||||
| 
 | ||||
|         # if the score is lower than the min_score, skip | ||||
|         if obj_settings.get('min_score', 0) > obj[1]: | ||||
|         if obj_settings.min_score > obj[1]: | ||||
|             return True | ||||
|      | ||||
|         # compute the coordinates of the object and make sure | ||||
| @ -118,7 +85,7 @@ def start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process=None): | ||||
| def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,  | ||||
|     frame_queue, fps:mp.Value, skipped_fps: mp.Value, current_frame: mp.Value): | ||||
| 
 | ||||
|     frame_size = frame_shape[0] * frame_shape[1] * 3 // 2 | ||||
|     frame_size = frame_shape[0] * frame_shape[1] | ||||
|     frame_rate = EventsPerSecond() | ||||
|     frame_rate.start() | ||||
|     skipped_eps = EventsPerSecond() | ||||
| @ -166,8 +133,8 @@ class CameraWatchdog(threading.Thread): | ||||
|         self.camera_fps = camera_fps | ||||
|         self.ffmpeg_pid = ffmpeg_pid | ||||
|         self.frame_queue = frame_queue | ||||
|         self.frame_shape = self.config['frame_shape'] | ||||
|         self.frame_size = self.frame_shape[0] * self.frame_shape[1] * 3 // 2 | ||||
|         self.frame_shape = self.config.frame_shape_yuv | ||||
|         self.frame_size = self.frame_shape[0] * self.frame_shape[1] | ||||
| 
 | ||||
|     def run(self): | ||||
|         self.start_ffmpeg() | ||||
| @ -192,7 +159,7 @@ class CameraWatchdog(threading.Thread): | ||||
|             time.sleep(10) | ||||
|      | ||||
|     def start_ffmpeg(self): | ||||
|         self.ffmpeg_process = start_or_restart_ffmpeg(self.config['ffmpeg_cmd'], self.frame_size) | ||||
|         self.ffmpeg_process = start_or_restart_ffmpeg(self.config.ffmpeg_cmd, self.frame_size) | ||||
|         self.ffmpeg_pid.value = self.ffmpeg_process.pid | ||||
|         self.capture_thread = CameraCapture(self.name, self.ffmpeg_process, self.frame_shape, self.frame_queue,  | ||||
|             self.camera_fps) | ||||
| @ -203,7 +170,6 @@ class CameraCapture(threading.Thread): | ||||
|         threading.Thread.__init__(self) | ||||
|         self.name = name | ||||
|         self.frame_shape = frame_shape | ||||
|         self.frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2] | ||||
|         self.frame_queue = frame_queue | ||||
|         self.fps = fps | ||||
|         self.skipped_fps = EventsPerSecond() | ||||
| @ -217,44 +183,21 @@ class CameraCapture(threading.Thread): | ||||
|         capture_frames(self.ffmpeg_process, self.name, self.frame_shape, self.frame_manager, self.frame_queue, | ||||
|             self.fps, self.skipped_fps, self.current_frame) | ||||
| 
 | ||||
| def capture_camera(name, config, process_info): | ||||
| def capture_camera(name, config: CameraConfig, process_info): | ||||
|     frame_queue = process_info['frame_queue'] | ||||
|     camera_watchdog = CameraWatchdog(name, config, frame_queue, process_info['camera_fps'], process_info['ffmpeg_pid']) | ||||
|     camera_watchdog.start() | ||||
|     camera_watchdog.join() | ||||
| 
 | ||||
| def track_camera(name, config, detection_queue, result_connection, detected_objects_queue, process_info): | ||||
| def track_camera(name, config: CameraConfig, detection_queue, result_connection, detected_objects_queue, process_info): | ||||
|     listen() | ||||
| 
 | ||||
|     frame_queue = process_info['frame_queue'] | ||||
| 
 | ||||
|     frame_shape = config['frame_shape'] | ||||
| 
 | ||||
|     # Merge the tracked object config with the global config | ||||
|     camera_objects_config = config.get('objects', {}) | ||||
|     objects_to_track = camera_objects_config.get('track', []) | ||||
|     object_filters = camera_objects_config.get('filters', {}) | ||||
| 
 | ||||
|     # load in the mask for object detection | ||||
|     if 'mask' in config: | ||||
|         if config['mask'].startswith('base64,'): | ||||
|             img = base64.b64decode(config['mask'][7:])  | ||||
|             npimg = np.fromstring(img, dtype=np.uint8) | ||||
|             mask = cv2.imdecode(npimg, cv2.IMREAD_GRAYSCALE) | ||||
|         elif config['mask'].startswith('poly,'): | ||||
|             points = config['mask'].split(',')[1:] | ||||
|             contour =  np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)]) | ||||
|             mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8) | ||||
|             mask[:] = 255 | ||||
|             cv2.fillPoly(mask, pts=[contour], color=(0)) | ||||
|         else: | ||||
|             mask = cv2.imread("/config/{}".format(config['mask']), cv2.IMREAD_GRAYSCALE) | ||||
|     else: | ||||
|         mask = None | ||||
| 
 | ||||
|     if mask is None or mask.size == 0: | ||||
|         mask = np.zeros((frame_shape[0], frame_shape[1]), np.uint8) | ||||
|         mask[:] = 255 | ||||
|     frame_shape = config.frame_shape | ||||
|     objects_to_track = config.objects.track | ||||
|     object_filters = config.objects.filters | ||||
|     mask = config.mask | ||||
| 
 | ||||
|     motion_detector = MotionDetector(frame_shape, mask, resize_factor=6) | ||||
|     object_detector = RemoteObjectDetector(name, '/labelmap.txt', detection_queue, result_connection) | ||||
| @ -301,7 +244,7 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, | ||||
|     frame_manager: FrameManager, motion_detector: MotionDetector,  | ||||
|     object_detector: RemoteObjectDetector, object_tracker: ObjectTracker, | ||||
|     detected_objects_queue: mp.Queue, process_info: Dict, | ||||
|     objects_to_track: List[str], object_filters: Dict, mask, | ||||
|     objects_to_track: List[str], object_filters, mask, | ||||
|     exit_on_empty: bool = False): | ||||
|      | ||||
|     fps = process_info['process_fps'] | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user