diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index ce83ab82a..41a3296c9 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -14,7 +14,8 @@ RUN groupadd --gid $USER_GID $USERNAME \ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ && chmod 0440 /etc/sudoers.d/$USERNAME -RUN apt-get install -y git curl vim htop +RUN apt-get update \ + && apt-get install -y git curl vim htop RUN pip3 install pylint black diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index faf871f07..707cc830a 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -170,6 +170,9 @@ snapshots: # Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below) # This value can be set via MQTT and will be updated in startup based on retained value enabled: False + # Optional: Enable writing a clean copy png snapshot to /media/frigate/clips (default: shown below) + # Only works if snapshots are enabled. This image is intended to be used for training purposes. + clean_copy: True # Optional: print a timestamp on the snapshots (default: shown below) timestamp: False # Optional: draw bounding box on the snapshots (default: shown below) diff --git a/docs/docs/usage/mqtt.md b/docs/docs/usage/mqtt.md index d91429b16..27712c124 100644 --- a/docs/docs/usage/mqtt.md +++ b/docs/docs/usage/mqtt.md @@ -37,6 +37,7 @@ Message published for each changed event. The first message is published when th "id": "1607123955.475377-mxklsc", "camera": "front_door", "frame_time": 1607123961.837752, + "snapshot_time": 1607123961.837752, "label": "person", "top_score": 0.958984375, "false_positive": false, @@ -54,6 +55,7 @@ Message published for each changed event. The first message is published when th "id": "1607123955.475377-mxklsc", "camera": "front_door", "frame_time": 1607123962.082975, + "snapshot_time": 1607123961.837752, "label": "person", "top_score": 0.958984375, "false_positive": false, diff --git a/frigate/app.py b/frigate/app.py index 1e000d401..a6994d6d9 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -180,14 +180,23 @@ class FrigateApp: model_shape = (self.config.model.height, self.config.model.width) 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=self.config.model.height * self.config.model.width * 3, - ) - shm_out = mp.shared_memory.SharedMemory( - name=f"out-{name}", create=True, size=20 * 6 * 4 - ) + + try: + shm_in = mp.shared_memory.SharedMemory( + name=name, + create=True, + size=self.config.model.height*self.config.model.width * 3, + ) + except FileExistsError: + shm_in = mp.shared_memory.SharedMemory(name=name) + + try: + shm_out = mp.shared_memory.SharedMemory( + name=f"out-{name}", create=True, size=20 * 6 * 4 + ) + except FileExistsError: + shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}") + self.detection_shms.append(shm_in) self.detection_shms.append(shm_out) diff --git a/frigate/config.py b/frigate/config.py index 393eecf00..688c98d2f 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -634,6 +634,7 @@ CAMERAS_SCHEMA = vol.Schema( }, vol.Optional("snapshots", default={}): { vol.Optional("enabled", default=False): bool, + vol.Optional("clean_copy", default=True): bool, vol.Optional("timestamp", default=False): bool, vol.Optional("bounding_box", default=False): bool, vol.Optional("crop", default=False): bool, @@ -665,6 +666,7 @@ CAMERAS_SCHEMA = vol.Schema( @dataclasses.dataclass class CameraSnapshotsConfig: enabled: bool + clean_copy: bool timestamp: bool bounding_box: bool crop: bool @@ -676,6 +678,7 @@ class CameraSnapshotsConfig: def build(cls, config, global_config) -> CameraSnapshotsConfig: return CameraSnapshotsConfig( enabled=config["enabled"], + clean_copy=config["clean_copy"], timestamp=config["timestamp"], bounding_box=config["bounding_box"], crop=config["crop"], @@ -689,6 +692,7 @@ class CameraSnapshotsConfig: def to_dict(self) -> Dict[str, Any]: return { "enabled": self.enabled, + "clean_copy": self.clean_copy, "timestamp": self.timestamp, "bounding_box": self.bounding_box, "crop": self.crop, diff --git a/frigate/events.py b/frigate/events.py index 2325635b5..71e9990dc 100644 --- a/frigate/events.py +++ b/frigate/events.py @@ -281,9 +281,9 @@ class EventCleanup(threading.Thread): self.stop_event = stop_event self.camera_keys = list(self.config.cameras.keys()) - def expire(self, media): + def expire(self, media_type): ## Expire events from unlisted cameras based on the global config - if media == "clips": + if media_type == "clips": retain_config = self.config.clips.retain file_extension = "mp4" update_params = {"has_clip": False} @@ -314,8 +314,16 @@ class EventCleanup(threading.Thread): # delete the media from disk for event in expired_events: media_name = f"{event.camera}-{event.id}" - media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}") - media.unlink(missing_ok=True) + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}" + ) + media_path.unlink(missing_ok=True) + if file_extension == "jpg": + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" + ) + media_path.unlink(missing_ok=True) + # update the clips attribute for the db entry update_query = Event.update(update_params).where( Event.camera.not_in(self.camera_keys), @@ -326,7 +334,7 @@ class EventCleanup(threading.Thread): ## Expire events from cameras based on the camera config for name, camera in self.config.cameras.items(): - if media == "clips": + if media_type == "clips": retain_config = camera.clips.retain else: retain_config = camera.snapshots.retain @@ -351,10 +359,15 @@ class EventCleanup(threading.Thread): # delete the grabbed clips from disk for event in expired_events: media_name = f"{event.camera}-{event.id}" - media = Path( + media_path = Path( f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}" ) - media.unlink(missing_ok=True) + media_path.unlink(missing_ok=True) + if file_extension == "jpg": + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" + ) + media_path.unlink(missing_ok=True) # update the clips attribute for the db entry update_query = Event.update(update_params).where( Event.camera == name, @@ -385,11 +398,11 @@ class EventCleanup(threading.Thread): logger.debug(f"Removing duplicate: {event.id}") media_name = f"{event.camera}-{event.id}" if event.has_snapshot: - media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") - media.unlink(missing_ok=True) + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") + media_path.unlink(missing_ok=True) if event.has_clip: - media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4") - media.unlink(missing_ok=True) + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4") + media_path.unlink(missing_ok=True) ( Event.delete() diff --git a/frigate/object_processing.py b/frigate/object_processing.py index b483940f3..85d238f24 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -153,10 +153,16 @@ class TrackedObject: return significant_update def to_dict(self, include_thumbnail: bool = False): + snapshot_time = ( + self.thumbnail_data["frame_time"] + if not self.thumbnail_data is None + else 0.0 + ) event = { "id": self.obj_data["id"], "camera": self.camera, "frame_time": self.obj_data["frame_time"], + "snapshot_time": snapshot_time, "label": self.obj_data["label"], "top_score": self.top_score, "false_positive": self.false_positive, @@ -192,6 +198,27 @@ class TrackedObject: ret, jpg = cv2.imencode(".jpg", np.zeros((175, 175, 3), np.uint8)) return jpg.tobytes() + def get_clean_png(self): + if self.thumbnail_data is None: + return None + + try: + best_frame = cv2.cvtColor( + self.frame_cache[self.thumbnail_data["frame_time"]], + cv2.COLOR_YUV2BGR_I420, + ) + except KeyError: + logger.warning( + f"Unable to create clean png because frame {self.thumbnail_data['frame_time']} is not in the cache" + ) + return None + + ret, png = cv2.imencode(".png", best_frame) + if ret: + return png.tobytes() + else: + return None + def get_jpg_bytes( self, timestamp=False, bounding_box=False, crop=False, height=None ): @@ -615,6 +642,23 @@ class TrackedObjectProcessor(threading.Thread): ) 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)) def snapshot(camera, obj: TrackedObject, current_frame_time): diff --git a/web/src/components/JSMpegPlayer.jsx b/web/src/components/JSMpegPlayer.jsx index 323b30771..7543091ff 100644 --- a/web/src/components/JSMpegPlayer.jsx +++ b/web/src/components/JSMpegPlayer.jsx @@ -5,14 +5,13 @@ import JSMpeg from '@cycjimmy/jsmpeg-player'; export default function JSMpegPlayer({ camera }) { const playerRef = useRef(); - const canvasRef = useRef(); const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}` useEffect(() => { const video = new JSMpeg.VideoElement( playerRef.current, url, - {canvas: canvasRef.current}, + {}, {protocols: [], audio: false} ); @@ -22,8 +21,6 @@ export default function JSMpegPlayer({ camera }) { }, [url]); return ( -