create tracked object class and save thumbnails

This commit is contained in:
Blake Blackshear 2020-11-08 16:05:15 -06:00
parent 373ca87887
commit aff87d4372
3 changed files with 237 additions and 226 deletions

View File

@ -88,7 +88,7 @@ class FrigateApp():
self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, out_events=self.detection_out_events, tf_device=detector.device) self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, out_events=self.detection_out_events, tf_device=detector.device)
def start_detected_frames_processor(self): 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, self.mqtt_client, self.config.mqtt.topic_prefix,
self.detected_frames_queue, self.event_queue, self.stop_event) self.detected_frames_queue, self.event_queue, self.stop_event)
self.detected_frames_processor.start() self.detected_frames_processor.start()

View File

@ -91,16 +91,14 @@ OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
} }
)) ))
DEFAULT_CAMERA_MQTT = {
'crop_to_region': True
}
DEFAULT_CAMERA_SAVE_CLIPS = { DEFAULT_CAMERA_SAVE_CLIPS = {
'enabled': False 'enabled': False
} }
DEFAULT_CAMERA_SNAPSHOTS = { DEFAULT_CAMERA_SNAPSHOTS = {
'show_timestamp': True, 'show_timestamp': True,
'draw_zones': False, 'draw_zones': False,
'draw_bounding_boxes': True 'draw_bounding_boxes': True,
'crop_to_region': True
} }
CAMERA_FFMPEG_SCHEMA = vol.Schema( CAMERA_FFMPEG_SCHEMA = vol.Schema(
@ -122,10 +120,6 @@ CAMERAS_SCHEMA = vol.Schema(
'fps': int, 'fps': int,
'mask': str, 'mask': str,
vol.Optional('best_image_timeout', default=60): int, vol.Optional('best_image_timeout', default=60): int,
vol.Optional('mqtt', default=DEFAULT_CAMERA_MQTT): {
vol.Optional('crop_to_region', default=True): bool,
'snapshot_height': int
},
vol.Optional('zones', default={}): { vol.Optional('zones', default={}): {
str: { str: {
vol.Required('coordinates'): vol.Any(str, [str]), vol.Required('coordinates'): vol.Any(str, [str]),
@ -140,7 +134,9 @@ CAMERAS_SCHEMA = vol.Schema(
vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): { vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): {
vol.Optional('show_timestamp', default=True): bool, vol.Optional('show_timestamp', default=True): bool,
vol.Optional('draw_zones', default=False): bool, vol.Optional('draw_zones', default=False): bool,
vol.Optional('draw_bounding_boxes', default=True): bool vol.Optional('draw_bounding_boxes', default=True): bool,
vol.Optional('crop_to_region', default=True): bool,
'height': int
}, },
'objects': OBJECTS_SCHEMA 'objects': OBJECTS_SCHEMA
} }
@ -296,6 +292,8 @@ class CameraSnapshotsConfig():
self._show_timestamp = config['show_timestamp'] self._show_timestamp = config['show_timestamp']
self._draw_zones = config['draw_zones'] self._draw_zones = config['draw_zones']
self._draw_bounding_boxes = config['draw_bounding_boxes'] self._draw_bounding_boxes = config['draw_bounding_boxes']
self._crop_to_region = config['crop_to_region']
self._height = config.get('height')
@property @property
def show_timestamp(self): def show_timestamp(self):
@ -309,6 +307,14 @@ class CameraSnapshotsConfig():
def draw_bounding_boxes(self): def draw_bounding_boxes(self):
return self._draw_bounding_boxes return self._draw_bounding_boxes
@property
def crop_to_region(self):
return self._crop_to_region
@property
def height(self):
return self._height
class CameraSaveClipsConfig(): class CameraSaveClipsConfig():
def __init__(self, config): def __init__(self, config):
self._enabled = config['enabled'] self._enabled = config['enabled']
@ -327,19 +333,6 @@ class CameraSaveClipsConfig():
def objects(self): def objects(self):
return self._objects 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(): class ZoneConfig():
def __init__(self, name, config): def __init__(self, name, config):
self._coordinates = config['coordinates'] self._coordinates = config['coordinates']
@ -391,7 +384,6 @@ class CameraConfig():
self._fps = config.get('fps') self._fps = config.get('fps')
self._mask = self._create_mask(config.get('mask')) self._mask = self._create_mask(config.get('mask'))
self._best_image_timeout = config['best_image_timeout'] 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._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
self._save_clips = CameraSaveClipsConfig(config['save_clips']) self._save_clips = CameraSaveClipsConfig(config['save_clips'])
self._snapshots = CameraSnapshotsConfig(config['snapshots']) self._snapshots = CameraSnapshotsConfig(config['snapshots'])

View File

@ -4,6 +4,7 @@ import hashlib
import itertools import itertools
import json import json
import logging import logging
import os
import queue import queue
import threading import threading
import time import time
@ -15,7 +16,7 @@ import cv2
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from frigate.config import CameraConfig from frigate.config import FrigateConfig, CameraConfig
from frigate.edgetpu import load_labels from frigate.edgetpu import load_labels
from frigate.util import SharedMemoryFrameManager, draw_box_with_label from frigate.util import SharedMemoryFrameManager, draw_box_with_label
@ -30,28 +31,6 @@ COLOR_MAP = {}
for key, val in LABELS.items(): for key, val in LABELS.items():
COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3]) 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): def on_edge(box, frame_shape):
if ( if (
box[0] == 0 or box[0] == 0 or
@ -80,19 +59,182 @@ def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool:
return False 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 # Maintains the state of a camera
class CameraState(): class CameraState():
def __init__(self, name, config, frame_manager): def __init__(self, name, config, frame_manager):
self.name = name self.name = name
self.config = config self.config = config
self.camera_config = config.cameras[name]
self.frame_manager = frame_manager self.frame_manager = frame_manager
self.best_objects = {} self.best_objects = {}
self.object_status = defaultdict(lambda: 'OFF') self.object_status = defaultdict(lambda: 'OFF')
self.tracked_objects = {} self.tracked_objects: Dict[str, TrackedObject] = {}
self.thumbnail_frames = {} self.thumbnail_frames = {}
self.zone_objects = defaultdict(lambda: []) 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_lock = threading.Lock()
self.current_frame_time = 0.0 self.current_frame_time = 0.0
self.previous_frame_id = None self.previous_frame_id = None
@ -102,7 +244,7 @@ class CameraState():
with self.current_frame_lock: with self.current_frame_lock:
frame_copy = np.copy(self._current_frame) frame_copy = np.copy(self._current_frame)
frame_time = self.current_frame_time 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) frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420)
# draw on the frame # draw on the frame
@ -123,42 +265,25 @@ class CameraState():
region = obj['region'] region = obj['region']
cv2.rectangle(frame_copy, (region[0], region[1]), (region[2], region[3]), (0,255,0), 1) 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") 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) 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: if self.camera_config.snapshots.draw_zones:
for name, zone in self.config.zones.items(): for name, zone in self.camera_config.zones.items():
thickness = 8 if any([name in obj['zones'] for obj in tracked_objects.values()]) else 2 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) cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness)
return frame_copy 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]): def on(self, event_type: str, callback: Callable[[Dict], None]):
self.callbacks[event_type].append(callback) self.callbacks[event_type].append(callback)
def update(self, frame_time, tracked_objects): def update(self, frame_time, tracked_objects):
self.current_frame_time = frame_time 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}" 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() current_ids = tracked_objects.keys()
previous_ids = self.tracked_objects.keys() previous_ids = self.tracked_objects.keys()
@ -167,59 +292,20 @@ class CameraState():
updated_ids = list(set(current_ids).intersection(previous_ids)) updated_ids = list(set(current_ids).intersection(previous_ids))
for id in new_ids: for id in new_ids:
new_obj = self.tracked_objects[id] = tracked_objects[id] new_obj = self.tracked_objects[id] = TrackedObject(self.name, self.camera_config, self.thumbnail_frames, 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])
# call event handlers # call event handlers
for c in self.callbacks['start']: for c in self.callbacks['start']:
c(self.name, new_obj) c(self.name, new_obj)
for id in updated_ids: for id in updated_ids:
self.tracked_objects[id].update(tracked_objects[id])
updated_obj = self.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 (not updated_obj._false_positive
if updated_obj['frame_time'] != self.current_frame_time: and updated_obj.thumbnail_data['frame_time'] == frame_time
updated_obj['score_history'].append(0.0) and frame_time not in self.thumbnail_frames):
else: self.thumbnail_frames[frame_time] = np.copy(current_frame)
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']
}
# call event handlers # call event handlers
for c in self.callbacks['update']: for c in self.callbacks['update']:
@ -227,53 +313,35 @@ class CameraState():
for id in removed_ids: for id in removed_ids:
# publish events to mqtt # 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']: for c in self.callbacks['end']:
c(self.name, self.tracked_objects[id]) c(self.name, removed_obj)
del self.tracked_objects[id] del self.tracked_objects[id]
# check to see if the objects are in any zones # TODO: can i switch to looking this up and only changing when an event ends?
for obj in self.tracked_objects.values(): # maybe make an api endpoint that pulls the thumbnail from the file system?
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]
# maintain best objects # maintain best objects
for obj in self.tracked_objects.values(): for obj in self.tracked_objects.values():
object_type = obj['label'] object_type = obj.obj_data['label']
# if the object wasn't seen on the current frame, skip it # if the object's thumbnail is not from the current frame
if obj['frame_time'] != self.current_frame_time or obj['false_positive']: if obj.thumbnail_data['frame_time'] != self.current_frame_time or obj.false_positive:
continue continue
obj_copy = copy.deepcopy(obj)
if object_type in self.best_objects: if object_type in self.best_objects:
current_best = self.best_objects[object_type] current_best = self.best_objects[object_type]
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
# if the object is a higher score than the current best score # if the object is a higher score than the current best score
# or the current object is older than desired, use the new object # 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) obj_copy['frame'] = np.copy(current_frame)
self.best_objects[object_type] = obj_copy self.best_objects[object_type] = obj_copy
for c in self.callbacks['snapshot']: for c in self.callbacks['snapshot']:
c(self.name, self.best_objects[object_type]) c(self.name, self.best_objects[object_type])
else: else:
obj_copy = copy.deepcopy(obj)
obj_copy['thumbnail'] = copy.deepcopy(obj.thumbnail_data)
obj_copy['frame'] = np.copy(current_frame) obj_copy['frame'] = np.copy(current_frame)
self.best_objects[object_type] = obj_copy self.best_objects[object_type] = obj_copy
for c in self.callbacks['snapshot']: for c in self.callbacks['snapshot']:
@ -282,8 +350,8 @@ class CameraState():
# update overall camera state for each object type # update overall camera state for each object type
obj_counter = Counter() obj_counter = Counter()
for obj in self.tracked_objects.values(): for obj in self.tracked_objects.values():
if not obj['false_positive']: if not obj.false_positive:
obj_counter[obj['label']] += 1 obj_counter[obj.obj_data['label']] += 1
# report on detected objects # report on detected objects
for obj_name, count in obj_counter.items(): for obj_name, count in obj_counter.items():
@ -302,6 +370,12 @@ class CameraState():
for c in self.callbacks['snapshot']: for c in self.callbacks['snapshot']:
c(self.name, self.best_objects[obj_name]) 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: with self.current_frame_lock:
self._current_frame = current_frame self._current_frame = current_frame
if not self.previous_frame_id is None: if not self.previous_frame_id is None:
@ -309,10 +383,10 @@ class CameraState():
self.previous_frame_id = frame_id self.previous_frame_id = frame_id
class TrackedObjectProcessor(threading.Thread): 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) threading.Thread.__init__(self)
self.name = "detected_frames_processor" self.name = "detected_frames_processor"
self.camera_config = camera_config self.config = config
self.client = client self.client = client
self.topic_prefix = topic_prefix self.topic_prefix = topic_prefix
self.tracked_objects_queue = tracked_objects_queue self.tracked_objects_queue = tracked_objects_queue
@ -321,76 +395,29 @@ class TrackedObjectProcessor(threading.Thread):
self.camera_states: Dict[str, CameraState] = {} self.camera_states: Dict[str, CameraState] = {}
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
def start(camera, obj): def start(camera, obj: TrackedObject):
# publish events to mqtt self.client.publish(f"{self.topic_prefix}/{camera}/events/start", json.dumps(obj.to_dict()), retain=False)
event_data = { self.event_queue.put(('start', camera, obj.to_dict()))
'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 update(camera, obj): def update(camera, obj: TrackedObject):
pass pass
def end(camera, obj): def end(camera, obj: TrackedObject):
event_data = { self.client.publish(f"{self.topic_prefix}/{camera}/events/end", json.dumps(obj.to_dict()), retain=False)
'id': obj['id'], if self.config.cameras[camera].save_clips.enabled:
'label': obj['label'], thumbnail_file_name = f"{camera}-{obj.obj_data['id']}.jpg"
'camera': camera, with open(os.path.join(self.config.save_clips.clips_dir, thumbnail_file_name), 'wb') as f:
'start_time': obj['start_time'], f.write(obj.snapshot_jpg)
'end_time': obj['end_time'], self.event_queue.put(('end', camera, obj.to_dict()))
'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 snapshot(camera, obj): def snapshot(camera, obj: TrackedObject):
if not 'frame' in obj: self.client.publish(f"{self.topic_prefix}/{camera}/{obj['label']}/snapshot", obj.snapshot_jpg, retain=True)
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 object_status(camera, object_name, status): def object_status(camera, object_name, status):
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False) self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
for camera in self.camera_config.keys(): for camera in self.config.cameras.keys():
camera_state = CameraState(camera, self.camera_config[camera], self.frame_manager) camera_state = CameraState(camera, self.config, self.frame_manager)
camera_state.on('start', start) camera_state.on('start', start)
camera_state.on('update', update) camera_state.on('update', update)
camera_state.on('end', end) camera_state.on('end', end)
@ -398,14 +425,6 @@ class TrackedObjectProcessor(threading.Thread):
camera_state.on('object_status', object_status) camera_state.on('object_status', object_status)
self.camera_states[camera] = camera_state 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': { # 'zone_name': {
# 'person': ['camera_1', 'camera_2'] # 'person': ['camera_1', 'camera_2']
@ -439,9 +458,9 @@ class TrackedObjectProcessor(threading.Thread):
camera_state.update(frame_time, current_tracked_objects) camera_state.update(frame_time, current_tracked_objects)
# update zone status for each label # 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 # 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()) labels_to_check = labels_for_camera | set(self.zone_data[zone].keys())
# for each label in zone # for each label in zone
for label in labels_to_check: for label in labels_to_check: