diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 73ab23bb8..c17f320c3 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -36,7 +36,7 @@ Message published for each changed event. The first message is published when th ```json { - "type": "update", // new, update, end or clip_ready + "type": "update", // new, update, end "before": { "id": "1607123955.475377-mxklsc", "camera": "front_door", @@ -53,7 +53,9 @@ Message published for each changed event. The first message is published when th "region": [264, 450, 667, 853], "current_zones": ["driveway"], "entered_zones": ["yard", "driveway"], - "thumbnail": null + "thumbnail": null, + "has_snapshot": false, + "has_clip": false }, "after": { "id": "1607123955.475377-mxklsc", @@ -71,7 +73,9 @@ Message published for each changed event. The first message is published when th "region": [218, 440, 693, 915], "current_zones": ["yard", "driveway"], "entered_zones": ["yard", "driveway"], - "thumbnail": null + "thumbnail": null, + "has_snapshot": false, + "has_clip": false } } ``` diff --git a/frigate/events.py b/frigate/events.py index 156cf80ce..cb238bb74 100644 --- a/frigate/events.py +++ b/frigate/events.py @@ -29,38 +29,6 @@ class EventProcessor(threading.Thread): self.events_in_process = {} self.stop_event = stop_event - def should_create_clip(self, camera, event_data): - if event_data["false_positive"]: - return False - - record_config: RecordConfig = self.config.cameras[camera].record - - # Recording is disabled - if not record_config.enabled: - return False - - # If there are required zones and there is no overlap - required_zones = record_config.events.required_zones - if len(required_zones) > 0 and not set(event_data["entered_zones"]) & set( - required_zones - ): - logger.debug( - f"Not creating clip for {event_data['id']} because it did not enter required zones" - ) - return False - - # If the required objects are not present - if ( - record_config.events.objects is not None - and event_data["label"] not in record_config.events.objects - ): - logger.debug( - f"Not creating clip for {event_data['id']} because it did not contain required objects" - ) - return False - - return True - def run(self): while not self.stop_event.is_set(): try: @@ -74,11 +42,9 @@ class EventProcessor(threading.Thread): self.events_in_process[event_data["id"]] = event_data if event_type == "end": - has_clip = self.should_create_clip(camera, event_data) - event_config: EventsConfig = self.config.cameras[camera].record.events - if has_clip or event_data["has_snapshot"]: + if event_data["has_clip"] or event_data["has_snapshot"]: Event.create( id=event_data["id"], label=event_data["label"], @@ -89,12 +55,12 @@ class EventProcessor(threading.Thread): false_positive=event_data["false_positive"], zones=list(event_data["entered_zones"]), thumbnail=event_data["thumbnail"], - has_clip=has_clip, + has_clip=event_data["has_clip"], has_snapshot=event_data["has_snapshot"], ) del self.events_in_process[event_data["id"]] - self.event_processed_queue.put((event_data["id"], camera, has_clip)) + self.event_processed_queue.put((event_data["id"], camera)) logger.info(f"Exiting event processor...") diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 38bacc0e2..2597893d7 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -16,7 +16,7 @@ from typing import Callable, Dict import cv2 import numpy as np -from frigate.config import CameraConfig, FrigateConfig +from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.edgetpu import load_labels from frigate.util import ( @@ -73,6 +73,8 @@ class TrackedObject: self.current_zones = [] self.entered_zones = set() self.false_positive = True + self.has_clip = False + self.has_snapshot = False self.top_score = self.computed_score = 0.0 self.thumbnail_data = None self.last_updated = 0 @@ -176,6 +178,8 @@ class TrackedObject: "region": self.obj_data["region"], "current_zones": self.current_zones.copy(), "entered_zones": list(self.entered_zones).copy(), + "has_clip": self.has_clip, + "has_snapshot": self.has_snapshot, } if include_thumbnail: @@ -611,9 +615,46 @@ class TrackedObjectProcessor(threading.Thread): obj.previous = after def end(camera, obj: TrackedObject, current_frame_time): - snapshot_config = self.config.cameras[camera].snapshots - event_data = obj.to_dict(include_thumbnail=True) - event_data["has_snapshot"] = False + # populate has_snapshot + obj.has_snapshot = self.should_save_snapshot(camera, obj) + obj.has_clip = self.should_retain_recording(camera, obj) + + # write the snapshot to disk + if obj.has_snapshot: + snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots + jpg_bytes = obj.get_jpg_bytes( + timestamp=snapshot_config.timestamp, + bounding_box=snapshot_config.bounding_box, + crop=snapshot_config.crop, + height=snapshot_config.height, + quality=snapshot_config.quality, + ) + if jpg_bytes is None: + logger.warning(f"Unable to save snapshot for {obj.obj_data['id']}.") + else: + with open( + os.path.join(CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"), + "wb", + ) as j: + j.write(jpg_bytes) + + # write clean snapshot if enabled + if snapshot_config.clean_copy: + png_bytes = obj.get_clean_png() + if png_bytes is None: + logger.warning( + f"Unable to save clean snapshot for {obj.obj_data['id']}." + ) + else: + with open( + os.path.join( + CLIPS_DIR, + f"{camera}-{obj.obj_data['id']}-clean.png", + ), + "wb", + ) as p: + p.write(png_bytes) + if not obj.false_positive: message = { "before": obj.previous, @@ -623,46 +664,8 @@ class TrackedObjectProcessor(threading.Thread): self.client.publish( f"{self.topic_prefix}/events", json.dumps(message), retain=False ) - # write snapshot to disk if enabled - if snapshot_config.enabled and self.should_save_snapshot(camera, obj): - jpg_bytes = obj.get_jpg_bytes( - timestamp=snapshot_config.timestamp, - bounding_box=snapshot_config.bounding_box, - crop=snapshot_config.crop, - height=snapshot_config.height, - quality=snapshot_config.quality, - ) - if jpg_bytes is None: - logger.warning( - f"Unable to save snapshot for {obj.obj_data['id']}." - ) - else: - with open( - os.path.join( - CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg" - ), - "wb", - ) as j: - j.write(jpg_bytes) - event_data["has_snapshot"] = True - # write clean snapshot if enabled - if snapshot_config.clean_copy: - png_bytes = obj.get_clean_png() - if png_bytes is None: - logger.warning( - f"Unable to save clean snapshot for {obj.obj_data['id']}." - ) - else: - with open( - os.path.join( - CLIPS_DIR, - f"{camera}-{obj.obj_data['id']}-clean.png", - ), - "wb", - ) as p: - p.write(png_bytes) - self.event_queue.put(("end", camera, event_data)) + self.event_queue.put(("end", camera, obj.to_dict(include_thumbnail=True))) def snapshot(camera, obj: TrackedObject, current_frame_time): mqtt_config = self.config.cameras[camera].mqtt @@ -711,8 +714,16 @@ class TrackedObjectProcessor(threading.Thread): self.zone_data = defaultdict(lambda: defaultdict(dict)) def should_save_snapshot(self, camera, obj: TrackedObject): + if obj.false_positive: + return False + + snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots + + if not snapshot_config.enabled: + return False + # if there are required zones and there is no overlap - required_zones = self.config.cameras[camera].snapshots.required_zones + required_zones = snapshot_config.required_zones if len(required_zones) > 0 and not obj.entered_zones & set(required_zones): logger.debug( f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones" @@ -721,6 +732,36 @@ class TrackedObjectProcessor(threading.Thread): return True + def should_retain_recording(self, camera, obj: TrackedObject): + if obj.false_positive: + return False + + record_config: RecordConfig = self.config.cameras[camera].record + + # Recording is disabled + if not record_config.enabled: + return False + + # If there are required zones and there is no overlap + required_zones = record_config.events.required_zones + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): + logger.debug( + f"Not creating clip for {obj.obj_data['id']} because it did not enter required zones" + ) + return False + + # If the required objects are not present + if ( + record_config.events.objects is not None + and obj.obj_data["label"] not in record_config.events.objects + ): + logger.debug( + f"Not creating clip for {obj.obj_data['id']} because it did not contain required objects" + ) + return False + + return True + def should_mqtt_snapshot(self, camera, obj: TrackedObject): # if there are required zones and there is no overlap required_zones = self.config.cameras[camera].mqtt.required_zones @@ -815,17 +856,7 @@ class TrackedObjectProcessor(threading.Thread): # cleanup event finished queue while not self.event_processed_queue.empty(): - event_id, camera, clip_created = self.event_processed_queue.get() - if clip_created: - obj = self.camera_states[camera].tracked_objects[event_id] - message = { - "before": obj.previous, - "after": obj.to_dict(), - "type": "clip_ready", - } - self.client.publish( - f"{self.topic_prefix}/events", json.dumps(message), retain=False - ) + event_id, camera = self.event_processed_queue.get() self.camera_states[camera].finished(event_id) logger.info(f"Exiting object processor...")