create typed config classes

This commit is contained in:
Blake Blackshear 2020-11-03 08:15:58 -06:00
parent b7c09a9b38
commit af303cbf2a
9 changed files with 733 additions and 241 deletions

View File

@ -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):

View File

@ -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

View File

@ -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']]

View File

@ -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:

View File

@ -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

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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']