| 
					
				 | 
			
			
				@@ -4,6 +4,7 @@ import hashlib 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import itertools 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import json 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import logging 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import os 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import queue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import threading 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import time 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -15,7 +16,7 @@ import cv2 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import matplotlib.pyplot as plt 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import numpy as np 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-from frigate.config import CameraConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+from frigate.config import FrigateConfig, CameraConfig 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 from frigate.edgetpu import load_labels 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 from frigate.util import SharedMemoryFrameManager, draw_box_with_label 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -30,28 +31,6 @@ COLOR_MAP = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 for key, val in LABELS.items(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-def zone_filtered(obj, object_config): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    object_name = obj['label'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    if object_name in object_config: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        obj_settings = object_config[object_name] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # if the min area is larger than the 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # detected object, don't add it to detected objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        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.max_area < obj['area']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # if the score is lower than the threshold, skip 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        if obj_settings.threshold > obj['computed_score']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 def on_edge(box, frame_shape): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     if ( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         box[0] == 0 or 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -80,19 +59,182 @@ def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				      
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+class TrackedObject(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def __init__(self, camera, camera_config: CameraConfig, thumbnail_frames, obj_data): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.obj_data = obj_data 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.camera = camera 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.camera_config = camera_config 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.thumbnail_frames = thumbnail_frames 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.current_zones = [] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.entered_zones = set() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self._false_positive = True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.top_score = self.computed_score = 0.0 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.thumbnail_data = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'frame_time': obj_data['frame_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'box': obj_data['box'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'area': obj_data['area'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'region': obj_data['region'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'score': obj_data['score'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.frame = None 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self._snapshot_jpg_time = 0 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self._snapshot_jpg = None 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # start the score history 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.score_history = [self.obj_data['score']] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def false_positive(self): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # once a true positive, always a true positive 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if not self._false_positive: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        threshold = self.camera_config.objects.filters[self.obj_data['label']].threshold 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if self.computed_score < threshold: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def compute_score(self): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        scores = self.score_history[:] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # pad with zeros if you dont have at least 3 scores 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if len(scores) < 3: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            scores += [0.0]*(3 - len(scores)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return median(scores) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+     
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def update(self, current_frame_time, obj_data): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.obj_data.update(obj_data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # if the object is not in the current frame, add a 0.0 to the score history 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if self.obj_data['frame_time'] != current_frame_time: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.score_history.append(0.0) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        else: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.score_history.append(self.obj_data['score']) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # only keep the last 10 scores 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if len(self.score_history) > 10: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.score_history = self.score_history[-10:] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # calculate if this is a false positive 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.computed_score = self.compute_score() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if self.computed_score > self.top_score: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.top_score = self.computed_score 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self._false_positive = self.false_positive() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # determine if this frame is a better thumbnail 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if is_better_thumbnail(self.thumbnail_data, self.obj_data, self.camera_config.frame_shape): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.thumbnail_data = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'frame_time': self.obj_data['frame_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'box': self.obj_data['box'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'area': self.obj_data['area'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'region': self.obj_data['region'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                'score': self.obj_data['score'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # check zones 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        current_zones = [] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        bottom_center = (self.obj_data['centroid'][0], self.obj_data['box'][3]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # check each zone 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        for name, zone in self.camera_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 self.current_zones or not zone_filtered(self, zone.filters): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    current_zones.append(name) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    self.entered_zones.add(name) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.current_zones = current_zones 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+     
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def to_dict(self): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'id': self.obj_data['id'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'camera': self.camera, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'frame_time': self.obj_data['frame_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'label': self.obj_data['label'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'top_score': self.top_score, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'false_positive': self._false_positive, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'start_time': self.obj_data['start_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'end_time': self.obj_data.get('end_time', None), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'score': self.obj_data['score'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'box': self.obj_data['box'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'area': self.obj_data['area'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'region': self.obj_data['region'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'current_zones': self.current_zones.copy(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            'entered_zones': list(self.entered_zones).copy() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+     
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    @property 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def snapshot_jpg(self): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if self._snapshot_jpg_time == self.thumbnail_data['frame_time']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return self._snapshot_jpg 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # TODO: crop first to avoid converting the entire frame? 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        snapshot_config = self.camera_config.snapshots 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        best_frame = cv2.cvtColor(self.thumbnail_frames[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if snapshot_config.draw_bounding_boxes: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            thickness = 2 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            color = COLOR_MAP[self.obj_data['label']] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            box = self.thumbnail_data['box'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            draw_box_with_label(best_frame, box[0], box[1], box[2], box[3], self.obj_data['label'],  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                f"{int(self.thumbnail_data['score']*100)}% {int(self.thumbnail_data['area'])}", thickness=thickness, color=color) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+             
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if snapshot_config.crop_to_region: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            region = self.thumbnail_data['region'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            best_frame = best_frame[region[1]:region[3], region[0]:region[2]] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if snapshot_config.height:  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            height = snapshot_config.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 snapshot_config.show_timestamp: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            time_to_show = datetime.datetime.fromtimestamp(self.thumbnail_data['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] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            desired_size = max(200, 0.33*best_frame.shape[1]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            font_scale = desired_size/text_width 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX,  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                fontScale=font_scale, color=(255, 255, 255), thickness=2) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ret, jpg = cv2.imencode('.jpg', best_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if ret: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self._snapshot_jpg = jpg.tobytes() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return self._snapshot_jpg 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+def zone_filtered(obj: TrackedObject, object_config): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    object_name = obj.obj_data['label'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if object_name in object_config: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        obj_settings = object_config[object_name] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # if the min area is larger than the 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # detected object, don't add it to detected objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if obj_settings.min_area > obj.obj_data['area']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # if the detected object is larger than the 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # max area, don't add it to detected objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if obj_settings.max_area < obj.obj_data['area']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # if the score is lower than the threshold, skip 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if obj_settings.threshold > obj.computed_score: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 # Maintains the state of a camera 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     def __init__(self, name, config, frame_manager): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.name = name 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.config = config 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.camera_config = config.cameras[name] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.frame_manager = frame_manager 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.best_objects = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.object_status = defaultdict(lambda: 'OFF') 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        self.tracked_objects = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.tracked_objects: Dict[str, TrackedObject] = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.thumbnail_frames = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.zone_objects = defaultdict(lambda: []) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        self._current_frame = np.zeros(self.config.frame_shape_yuv, np.uint8) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.current_frame_lock = threading.Lock() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.current_frame_time = 0.0 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.previous_frame_id = None 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -102,7 +244,7 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         with self.current_frame_lock: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             frame_copy = np.copy(self._current_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             frame_time = self.current_frame_time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            tracked_objects = copy.deepcopy(self.tracked_objects) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            tracked_objects = {k: v.to_dict() for k,v in self.tracked_objects.items()} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         # draw on the frame 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -123,42 +265,25 @@ 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.camera_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(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    thickness = 8 if any([name in obj['zones'] for obj in tracked_objects.values()]) else 2 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if self.camera_config.snapshots.draw_zones: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                for name, zone in self.camera_config.zones.items(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    thickness = 8 if any([name in obj['current_zones'] for obj in tracked_objects.values()]) else 2 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                     cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         return frame_copy 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    def false_positive(self, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # once a true positive, always a true positive 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        if not obj.get('false_positive', True): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        threshold = self.config.objects.filters[obj['label']].threshold 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        if obj['computed_score'] < threshold: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            return True 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        return False 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    def compute_score(self, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        scores = obj['score_history'][:] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # pad with zeros if you dont have at least 3 scores 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        if len(scores) < 3: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            scores += [0.0]*(3 - len(scores)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        return median(scores) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     def on(self, event_type: str, callback: Callable[[Dict], None]): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.callbacks[event_type].append(callback) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     def update(self, frame_time, tracked_objects): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.current_frame_time = frame_time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # get the new frame and delete the old frame 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # get the new frame 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         frame_id = f"{self.name}{frame_time}" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        current_frame = self.frame_manager.get(frame_id, self.config.frame_shape_yuv) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        current_frame = self.frame_manager.get(frame_id, self.camera_config.frame_shape_yuv) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         current_ids = tracked_objects.keys() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         previous_ids = self.tracked_objects.keys() 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -167,59 +292,20 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         updated_ids = list(set(current_ids).intersection(previous_ids)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for id in new_ids: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj = self.tracked_objects[id] = tracked_objects[id] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['zones'] = [] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['entered_zones'] = set() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['thumbnail'] = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                'frame': new_obj['frame_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                'box': new_obj['box'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                'area': new_obj['area'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                'region': new_obj['region'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                'score': new_obj['score'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # start the score history 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['score_history'] = [self.tracked_objects[id]['score']] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # calculate if this is a false positive 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['computed_score'] = self.compute_score(self.tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['top_score'] = self.tracked_objects[id]['computed_score'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            new_obj['false_positive'] = self.false_positive(self.tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            new_obj = self.tracked_objects[id] = TrackedObject(self.name, self.camera_config, self.thumbnail_frames, tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             # call event handlers 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             for c in self.callbacks['start']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 c(self.name, new_obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for id in updated_ids: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.tracked_objects[id].update(tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             updated_obj = self.tracked_objects[id] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            updated_obj.update(frame_time, tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # if the object is not in the current frame, add a 0.0 to the score history 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if updated_obj['frame_time'] != self.current_frame_time: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                updated_obj['score_history'].append(0.0) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            else: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                updated_obj['score_history'].append(updated_obj['score']) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # only keep the last 10 scores 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if len(updated_obj['score_history']) > 10: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                updated_obj['score_history'] = updated_obj['score_history'][-10:] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # calculate if this is a false positive 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            computed_score = self.compute_score(updated_obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            updated_obj['computed_score'] = computed_score 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if computed_score > updated_obj['top_score']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                updated_obj['top_score'] = computed_score 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            updated_obj['false_positive'] = self.false_positive(updated_obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # determine if this frame is a better thumbnail 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if is_better_thumbnail(updated_obj['thumbnail'], updated_obj, self.config.frame_shape): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                updated_obj['thumbnail'] = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    'frame': updated_obj['frame_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    'box': updated_obj['box'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    'area': updated_obj['area'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    'region': updated_obj['region'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                    'score': updated_obj['score'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if (not updated_obj._false_positive  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                and updated_obj.thumbnail_data['frame_time'] == frame_time  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                and frame_time not in self.thumbnail_frames): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                self.thumbnail_frames[frame_time] = np.copy(current_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             # call event handlers 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             for c in self.callbacks['update']: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -227,53 +313,35 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for id in removed_ids: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             # publish events to mqtt 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.tracked_objects[id]['end_time'] = frame_time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            removed_obj = self.tracked_objects[id] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            removed_obj.obj_data['end_time'] = frame_time 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             for c in self.callbacks['end']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                c(self.name, self.tracked_objects[id]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                c(self.name, removed_obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             del self.tracked_objects[id] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # check to see if the objects are in any zones 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        for obj in self.tracked_objects.values(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            current_zones = [] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            bottom_center = (obj['centroid'][0], obj['box'][3]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # check each zone 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            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.filters): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                        current_zones.append(name) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                        obj['entered_zones'].add(name) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                     
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            obj['zones'] = current_zones 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        # update frame storage for thumbnails based on thumbnails for all tracked objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        current_thumb_frames = set([obj['thumbnail']['frame'] for obj in self.tracked_objects.values()]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        if self.current_frame_time in current_thumb_frames: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.thumbnail_frames[self.current_frame_time] = np.copy(current_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        thumb_frames_to_delete = [t for t in self.thumbnail_frames.keys() if not t in current_thumb_frames] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        for t in thumb_frames_to_delete: del self.thumbnail_frames[t] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # TODO: can i switch to looking this up and only changing when an event ends? 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        #       maybe make an api endpoint that pulls the thumbnail from the file system? 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         # maintain best objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for obj in self.tracked_objects.values(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            object_type = obj['label'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # if the object wasn't seen on the current frame, skip it 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if obj['frame_time'] != self.current_frame_time or obj['false_positive']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            object_type = obj.obj_data['label'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            # if the object's thumbnail is not from the current frame 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if obj.thumbnail_data['frame_time'] != self.current_frame_time or obj.false_positive: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 continue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            obj_copy = copy.deepcopy(obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             if object_type in self.best_objects: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 current_best = self.best_objects[object_type] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 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.best_image_timeout: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                if is_better_thumbnail(current_best['thumbnail'], obj.thumbnail, self.camera_config.frame_shape) or (now - current_best['frame_time']) > self.config.best_image_timeout: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    obj_copy = copy.deepcopy(obj.obj_data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    obj_copy['thumbnail'] = copy.deepcopy(obj.thumbnail_data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                     obj_copy['frame'] = np.copy(current_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                     self.best_objects[object_type] = obj_copy 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                     for c in self.callbacks['snapshot']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                         c(self.name, self.best_objects[object_type]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             else: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                obj_copy = copy.deepcopy(obj) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                obj_copy['thumbnail'] = copy.deepcopy(obj.thumbnail_data) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 obj_copy['frame'] = np.copy(current_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 self.best_objects[object_type] = obj_copy 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 for c in self.callbacks['snapshot']: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -282,8 +350,8 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         # update overall camera state for each object type 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         obj_counter = Counter() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for obj in self.tracked_objects.values(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if not obj['false_positive']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                obj_counter[obj['label']] += 1 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if not obj.false_positive: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                obj_counter[obj.obj_data['label']] += 1 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         # report on detected objects 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         for obj_name, count in obj_counter.items(): 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -302,6 +370,12 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             for c in self.callbacks['snapshot']: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 c(self.name, self.best_objects[obj_name]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        # cleanup thumbnail frame cache 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        current_thumb_frames = set([obj.thumbnail_data['frame_time'] for obj in self.tracked_objects.values() if not obj._false_positive]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        thumb_frames_to_delete = [t for t in self.thumbnail_frames.keys() if not t in current_thumb_frames] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        for t in thumb_frames_to_delete: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            del self.thumbnail_frames[t] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+         
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         with self.current_frame_lock: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             self._current_frame = current_frame 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             if not self.previous_frame_id is None: 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -309,10 +383,10 @@ class CameraState(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             self.previous_frame_id = frame_id 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 class TrackedObjectProcessor(threading.Thread): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    def __init__(self, camera_config: Dict[str, CameraConfig], client, topic_prefix, tracked_objects_queue, event_queue, stop_event): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    def __init__(self, config: FrigateConfig, client, topic_prefix, tracked_objects_queue, event_queue, stop_event): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         threading.Thread.__init__(self) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.name = "detected_frames_processor" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        self.camera_config = camera_config 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        self.config = config 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.client = client 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.topic_prefix = topic_prefix 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.tracked_objects_queue = tracked_objects_queue 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -321,76 +395,29 @@ class TrackedObjectProcessor(threading.Thread): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.camera_states: Dict[str, CameraState] = {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         self.frame_manager = SharedMemoryFrameManager() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        def start(camera, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            # publish events to mqtt 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            event_data = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'id': obj['id'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'label': obj['label'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'camera': camera, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'start_time': obj['start_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'top_score': obj['top_score'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'false_positive': obj['false_positive'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'zones': list(obj['entered_zones']) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.client.publish(f"{self.topic_prefix}/{camera}/events/start", json.dumps(event_data), retain=False) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.event_queue.put(('start', camera, obj)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        def start(camera, obj: TrackedObject): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.client.publish(f"{self.topic_prefix}/{camera}/events/start", json.dumps(obj.to_dict()), retain=False) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.event_queue.put(('start', camera, obj.to_dict())) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        def update(camera, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        def update(camera, obj: TrackedObject): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             pass 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        def end(camera, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            event_data = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'id': obj['id'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'label': obj['label'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'camera': camera, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'start_time': obj['start_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'end_time': obj['end_time'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'top_score': obj['top_score'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'false_positive': obj['false_positive'], 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-              'zones': list(obj['entered_zones']) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.client.publish(f"{self.topic_prefix}/{camera}/events/end", json.dumps(event_data), retain=False) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            self.event_queue.put(('end', camera, obj)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        def end(camera, obj: TrackedObject): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.client.publish(f"{self.topic_prefix}/{camera}/events/end", json.dumps(obj.to_dict()), retain=False) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            if self.config.cameras[camera].save_clips.enabled: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                thumbnail_file_name = f"{camera}-{obj.obj_data['id']}.jpg" 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                with open(os.path.join(self.config.save_clips.clips_dir, thumbnail_file_name), 'wb') as f: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    f.write(obj.snapshot_jpg) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.event_queue.put(('end', camera, obj.to_dict())) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        def snapshot(camera, obj): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if not 'frame' in obj: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                return 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-             
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            best_frame = cv2.cvtColor(obj['frame'], cv2.COLOR_YUV2BGR_I420) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            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].mqtt 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if mqtt_config.crop_to_region: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                region = obj['region'] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                best_frame = best_frame[region[1]:region[3], region[0]:region[2]] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            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: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                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] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                text_height = size[0][1] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                desired_size = max(200, 0.33*best_frame.shape[1]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                font_scale = desired_size/text_width 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX, fontScale=font_scale, color=(255, 255, 255), thickness=2) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            ret, jpg = cv2.imencode('.jpg', best_frame) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            if ret: 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                jpg_bytes = jpg.tobytes() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                self.client.publish(f"{self.topic_prefix}/{camera}/{obj['label']}/snapshot", jpg_bytes, retain=True) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        def snapshot(camera, obj: TrackedObject): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            self.client.publish(f"{self.topic_prefix}/{camera}/{obj['label']}/snapshot", obj.snapshot_jpg, retain=True) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				          
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         def object_status(camera, object_name, status): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        for camera in self.camera_config.keys(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            camera_state = CameraState(camera, self.camera_config[camera], self.frame_manager) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        for camera in self.config.cameras.keys(): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            camera_state = CameraState(camera, self.config, self.frame_manager) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             camera_state.on('start', start) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             camera_state.on('update', update) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             camera_state.on('end', end) 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -398,14 +425,6 @@ class TrackedObjectProcessor(threading.Thread): 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             camera_state.on('object_status', object_status) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				             self.camera_states[camera] = camera_state 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        self.camera_data = defaultdict(lambda: { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'best_objects': {}, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'object_status': defaultdict(lambda: defaultdict(lambda: 'OFF')), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'tracked_objects': {}, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'current_frame': np.zeros((720,1280,3), np.uint8), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'current_frame_time': 0.0, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            'object_id': None 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        }) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         # { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         #   'zone_name': { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         #       'person': ['camera_1', 'camera_2'] 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -439,9 +458,9 @@ 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 self.config.cameras[camera].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_for_camera = set([obj.obj_data['label'] for obj in camera_state.tracked_objects.values() if zone in obj.current_zones and not obj._false_positive]) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 labels_to_check = labels_for_camera | set(self.zone_data[zone].keys()) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 # for each label in zone 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				                 for label in labels_to_check: 
			 |