|
@@ -13,8 +13,9 @@ import copy
|
|
import itertools
|
|
import itertools
|
|
import json
|
|
import json
|
|
import base64
|
|
import base64
|
|
|
|
+from typing import Dict, List
|
|
from collections import defaultdict
|
|
from collections import defaultdict
|
|
-from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, PlasmaManager
|
|
|
|
|
|
+from frigate.util import draw_box_with_label, area, calculate_region, clipped, intersection_over_union, intersection, EventsPerSecond, listen, FrameManager, PlasmaFrameManager
|
|
from frigate.objects import ObjectTracker
|
|
from frigate.objects import ObjectTracker
|
|
from frigate.edgetpu import RemoteObjectDetector
|
|
from frigate.edgetpu import RemoteObjectDetector
|
|
from frigate.motion import MotionDetector
|
|
from frigate.motion import MotionDetector
|
|
@@ -53,7 +54,7 @@ def get_ffmpeg_input(ffmpeg_input):
|
|
frigate_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
|
|
frigate_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
|
|
return ffmpeg_input.format(**frigate_vars)
|
|
return ffmpeg_input.format(**frigate_vars)
|
|
|
|
|
|
-def filtered(obj, objects_to_track, object_filters, mask):
|
|
|
|
|
|
+def filtered(obj, objects_to_track, object_filters, mask=None):
|
|
object_name = obj[0]
|
|
object_name = obj[0]
|
|
|
|
|
|
if not object_name in objects_to_track:
|
|
if not object_name in objects_to_track:
|
|
@@ -82,7 +83,7 @@ def filtered(obj, objects_to_track, object_filters, mask):
|
|
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
|
|
x_location = min(int((obj[2][2]-obj[2][0])/2.0)+obj[2][0], len(mask[0])-1)
|
|
|
|
|
|
# if the object is in a masked location, don't add it to detected objects
|
|
# if the object is in a masked location, don't add it to detected objects
|
|
- if mask[y_location][x_location] == [0]:
|
|
|
|
|
|
+ if mask != None and mask[y_location][x_location] == [0]:
|
|
return True
|
|
return True
|
|
|
|
|
|
return False
|
|
return False
|
|
@@ -115,6 +116,53 @@ def start_or_restart_ffmpeg(ffmpeg_cmd, frame_size, ffmpeg_process=None):
|
|
process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
|
|
process = sp.Popen(ffmpeg_cmd, stdout = sp.PIPE, stdin = sp.DEVNULL, bufsize=frame_size*10, start_new_session=True)
|
|
return process
|
|
return process
|
|
|
|
|
|
|
|
+def capture_frames(ffmpeg_process, camera_name, frame_shape, frame_manager: FrameManager,
|
|
|
|
+ frame_queue, take_frame: int, fps:EventsPerSecond, skipped_fps: EventsPerSecond,
|
|
|
|
+ stop_event: mp.Event, detection_frame: mp.Value):
|
|
|
|
+
|
|
|
|
+ frame_num = 0
|
|
|
|
+ last_frame = 0
|
|
|
|
+ frame_size = frame_shape[0] * frame_shape[1] * frame_shape[2]
|
|
|
|
+ skipped_fps.start()
|
|
|
|
+ while True:
|
|
|
|
+ if stop_event.is_set():
|
|
|
|
+ print(f"{camera_name}: stop event set. exiting capture thread...")
|
|
|
|
+ break
|
|
|
|
+
|
|
|
|
+ frame_bytes = ffmpeg_process.stdout.read(frame_size)
|
|
|
|
+ current_frame = datetime.datetime.now().timestamp()
|
|
|
|
+
|
|
|
|
+ if len(frame_bytes) == 0:
|
|
|
|
+ print(f"{camera_name}: ffmpeg didnt return a frame. something is wrong.")
|
|
|
|
+
|
|
|
|
+ if ffmpeg_process.poll() != None:
|
|
|
|
+ print(f"{camera_name}: ffmpeg process is not running. exiting capture thread...")
|
|
|
|
+ break
|
|
|
|
+ else:
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ fps.update()
|
|
|
|
+
|
|
|
|
+ frame_num += 1
|
|
|
|
+ if (frame_num % take_frame) != 0:
|
|
|
|
+ skipped_fps.update()
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ # if the detection process is more than 1 second behind, skip this frame
|
|
|
|
+ if detection_frame.value > 0.0 and (last_frame - detection_frame.value) > 1:
|
|
|
|
+ skipped_fps.update()
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ # put the frame in the frame manager
|
|
|
|
+ frame_manager.put(f"{camera_name}{current_frame}",
|
|
|
|
+ np
|
|
|
|
+ .frombuffer(frame_bytes, np.uint8)
|
|
|
|
+ .reshape(frame_shape)
|
|
|
|
+ )
|
|
|
|
+ # add to the queue
|
|
|
|
+ frame_queue.put(current_frame)
|
|
|
|
+ last_frame = current_frame
|
|
|
|
+
|
|
class CameraCapture(threading.Thread):
|
|
class CameraCapture(threading.Thread):
|
|
def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame, stop_event):
|
|
def __init__(self, name, ffmpeg_process, frame_shape, frame_queue, take_frame, fps, detection_frame, stop_event):
|
|
threading.Thread.__init__(self)
|
|
threading.Thread.__init__(self)
|
|
@@ -125,7 +173,7 @@ class CameraCapture(threading.Thread):
|
|
self.take_frame = take_frame
|
|
self.take_frame = take_frame
|
|
self.fps = fps
|
|
self.fps = fps
|
|
self.skipped_fps = EventsPerSecond()
|
|
self.skipped_fps = EventsPerSecond()
|
|
- self.plasma_client = PlasmaManager(stop_event)
|
|
|
|
|
|
+ self.plasma_client = PlasmaFrameManager(stop_event)
|
|
self.ffmpeg_process = ffmpeg_process
|
|
self.ffmpeg_process = ffmpeg_process
|
|
self.current_frame = 0
|
|
self.current_frame = 0
|
|
self.last_frame = 0
|
|
self.last_frame = 0
|
|
@@ -133,47 +181,11 @@ class CameraCapture(threading.Thread):
|
|
self.stop_event = stop_event
|
|
self.stop_event = stop_event
|
|
|
|
|
|
def run(self):
|
|
def run(self):
|
|
- frame_num = 0
|
|
|
|
self.skipped_fps.start()
|
|
self.skipped_fps.start()
|
|
- while True:
|
|
|
|
- if self.stop_event.is_set():
|
|
|
|
- print(f"{self.name}: stop event set. exiting capture thread...")
|
|
|
|
- break
|
|
|
|
-
|
|
|
|
- if self.ffmpeg_process.poll() != None:
|
|
|
|
- print(f"{self.name}: ffmpeg process is not running. exiting capture thread...")
|
|
|
|
- break
|
|
|
|
-
|
|
|
|
- frame_bytes = self.ffmpeg_process.stdout.read(self.frame_size)
|
|
|
|
- self.current_frame = datetime.datetime.now().timestamp()
|
|
|
|
-
|
|
|
|
- if len(frame_bytes) == 0:
|
|
|
|
- print(f"{self.name}: ffmpeg didnt return a frame. something is wrong.")
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- self.fps.update()
|
|
|
|
-
|
|
|
|
- frame_num += 1
|
|
|
|
- if (frame_num % self.take_frame) != 0:
|
|
|
|
- self.skipped_fps.update()
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- # if the detection process is more than 1 second behind, skip this frame
|
|
|
|
- if self.detection_frame.value > 0.0 and (self.last_frame - self.detection_frame.value) > 1:
|
|
|
|
- self.skipped_fps.update()
|
|
|
|
- continue
|
|
|
|
|
|
+ capture_frames(self.ffmpeg_process, self.name, self.frame_shape, self.plasma_client, self.frame_queue, self.take_frame,
|
|
|
|
+ self.fps, self.skipped_fps, self.stop_event, self.detection_frame)
|
|
|
|
|
|
- # put the frame in the plasma store
|
|
|
|
- self.plasma_client.put(f"{self.name}{self.current_frame}",
|
|
|
|
- np
|
|
|
|
- .frombuffer(frame_bytes, np.uint8)
|
|
|
|
- .reshape(self.frame_shape)
|
|
|
|
- )
|
|
|
|
- # add to the queue
|
|
|
|
- self.frame_queue.put(self.current_frame)
|
|
|
|
- self.last_frame = self.current_frame
|
|
|
|
-
|
|
|
|
-def track_camera(name, config, global_objects_config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame):
|
|
|
|
|
|
+def track_camera(name, config, global_objects_config, frame_queue, frame_shape, detection_queue, detected_objects_queue, fps, detection_fps, read_start, detection_frame, stop_event):
|
|
print(f"Starting process for {name}: {os.getpid()}")
|
|
print(f"Starting process for {name}: {os.getpid()}")
|
|
listen()
|
|
listen()
|
|
|
|
|
|
@@ -191,8 +203,6 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
|
for obj in objects_with_config:
|
|
for obj in objects_with_config:
|
|
object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
|
|
object_filters[obj] = {**global_object_filters.get(obj, {}), **camera_object_filters.get(obj, {})}
|
|
|
|
|
|
- frame = np.zeros(frame_shape, np.uint8)
|
|
|
|
-
|
|
|
|
# load in the mask for object detection
|
|
# load in the mask for object detection
|
|
if 'mask' in config:
|
|
if 'mask' in config:
|
|
if config['mask'].startswith('base64,'):
|
|
if config['mask'].startswith('base64,'):
|
|
@@ -213,109 +223,96 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
|
|
|
|
|
object_tracker = ObjectTracker(10)
|
|
object_tracker = ObjectTracker(10)
|
|
|
|
|
|
- plasma_client = PlasmaManager()
|
|
|
|
- avg_wait = 0.0
|
|
|
|
|
|
+ plasma_client = PlasmaFrameManager()
|
|
|
|
+
|
|
|
|
+ process_frames(name, frame_queue, frame_shape, plasma_client, motion_detector, object_detector,
|
|
|
|
+ object_tracker, detected_objects_queue, fps, detection_frame, objects_to_track, object_filters, mask, stop_event)
|
|
|
|
+
|
|
|
|
+ print(f"{name}: exiting subprocess")
|
|
|
|
+
|
|
|
|
+def reduce_boxes(boxes):
|
|
|
|
+ if len(boxes) == 0:
|
|
|
|
+ return []
|
|
|
|
+ reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
|
|
|
|
+ return [tuple(b) for b in reduced_boxes]
|
|
|
|
+
|
|
|
|
+def detect(object_detector, frame, region, objects_to_track, object_filters, mask):
|
|
|
|
+ tensor_input = create_tensor_input(frame, region)
|
|
|
|
+
|
|
|
|
+ detections = []
|
|
|
|
+ region_detections = object_detector.detect(tensor_input)
|
|
|
|
+ for d in region_detections:
|
|
|
|
+ box = d[2]
|
|
|
|
+ size = region[2]-region[0]
|
|
|
|
+ x_min = int((box[1] * size) + region[0])
|
|
|
|
+ y_min = int((box[0] * size) + region[1])
|
|
|
|
+ x_max = int((box[3] * size) + region[0])
|
|
|
|
+ y_max = int((box[2] * size) + region[1])
|
|
|
|
+ det = (d[0],
|
|
|
|
+ d[1],
|
|
|
|
+ (x_min, y_min, x_max, y_max),
|
|
|
|
+ (x_max-x_min)*(y_max-y_min),
|
|
|
|
+ region)
|
|
|
|
+ # apply object filters
|
|
|
|
+ if filtered(det, objects_to_track, object_filters, mask):
|
|
|
|
+ continue
|
|
|
|
+ detections.append(det)
|
|
|
|
+ return detections
|
|
|
|
+
|
|
|
|
+def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape,
|
|
|
|
+ frame_manager: FrameManager, motion_detector: MotionDetector,
|
|
|
|
+ object_detector: RemoteObjectDetector, object_tracker: ObjectTracker,
|
|
|
|
+ detected_objects_queue: mp.Queue, fps: mp.Value, current_frame_time: mp.Value,
|
|
|
|
+ objects_to_track: List[str], object_filters: Dict, mask, stop_event: mp.Event,
|
|
|
|
+ exit_on_empty: bool = False):
|
|
|
|
+
|
|
fps_tracker = EventsPerSecond()
|
|
fps_tracker = EventsPerSecond()
|
|
fps_tracker.start()
|
|
fps_tracker.start()
|
|
- object_detector.fps.start()
|
|
|
|
|
|
+
|
|
while True:
|
|
while True:
|
|
- read_start.value = datetime.datetime.now().timestamp()
|
|
|
|
- frame_time = frame_queue.get()
|
|
|
|
- duration = datetime.datetime.now().timestamp()-read_start.value
|
|
|
|
- read_start.value = 0.0
|
|
|
|
- avg_wait = (avg_wait*99+duration)/100
|
|
|
|
- detection_frame.value = frame_time
|
|
|
|
-
|
|
|
|
- # Get frame from plasma store
|
|
|
|
- frame = plasma_client.get(f"{name}{frame_time}")
|
|
|
|
|
|
+ if stop_event.is_set() or (exit_on_empty and frame_queue.empty()):
|
|
|
|
+ print(f"Exiting track_objects...")
|
|
|
|
+ break
|
|
|
|
|
|
- if frame is plasma.ObjectNotAvailable:
|
|
|
|
|
|
+ try:
|
|
|
|
+ frame_time = frame_queue.get(True, 10)
|
|
|
|
+ except queue.Empty:
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
+
|
|
|
|
+ current_frame_time.value = frame_time
|
|
|
|
+
|
|
|
|
+ frame = frame_manager.get(f"{camera_name}{frame_time}")
|
|
|
|
+
|
|
fps_tracker.update()
|
|
fps_tracker.update()
|
|
fps.value = fps_tracker.eps()
|
|
fps.value = fps_tracker.eps()
|
|
- detection_fps.value = object_detector.fps.eps()
|
|
|
|
-
|
|
|
|
|
|
+
|
|
# look for motion
|
|
# look for motion
|
|
motion_boxes = motion_detector.detect(frame)
|
|
motion_boxes = motion_detector.detect(frame)
|
|
|
|
|
|
- tracked_objects = object_tracker.tracked_objects.values()
|
|
|
|
-
|
|
|
|
- # merge areas of motion that intersect with a known tracked object into a single area to look at
|
|
|
|
- areas_of_interest = []
|
|
|
|
- used_motion_boxes = []
|
|
|
|
- for obj in tracked_objects:
|
|
|
|
- x_min, y_min, x_max, y_max = obj['box']
|
|
|
|
- for m_index, motion_box in enumerate(motion_boxes):
|
|
|
|
- if intersection_over_union(motion_box, obj['box']) > .2:
|
|
|
|
- used_motion_boxes.append(m_index)
|
|
|
|
- x_min = min(obj['box'][0], motion_box[0])
|
|
|
|
- y_min = min(obj['box'][1], motion_box[1])
|
|
|
|
- x_max = max(obj['box'][2], motion_box[2])
|
|
|
|
- y_max = max(obj['box'][3], motion_box[3])
|
|
|
|
- areas_of_interest.append((x_min, y_min, x_max, y_max))
|
|
|
|
- unused_motion_boxes = set(range(0, len(motion_boxes))).difference(used_motion_boxes)
|
|
|
|
-
|
|
|
|
- # compute motion regions
|
|
|
|
- motion_regions = [calculate_region(frame_shape, motion_boxes[i][0], motion_boxes[i][1], motion_boxes[i][2], motion_boxes[i][3], 1.2)
|
|
|
|
- for i in unused_motion_boxes]
|
|
|
|
-
|
|
|
|
- # compute tracked object regions
|
|
|
|
- object_regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
|
|
|
|
- for a in areas_of_interest]
|
|
|
|
-
|
|
|
|
- # merge regions with high IOU
|
|
|
|
- merged_regions = motion_regions+object_regions
|
|
|
|
- while True:
|
|
|
|
- max_iou = 0.0
|
|
|
|
- max_indices = None
|
|
|
|
- region_indices = range(len(merged_regions))
|
|
|
|
- for a, b in itertools.combinations(region_indices, 2):
|
|
|
|
- iou = intersection_over_union(merged_regions[a], merged_regions[b])
|
|
|
|
- if iou > max_iou:
|
|
|
|
- max_iou = iou
|
|
|
|
- max_indices = (a, b)
|
|
|
|
- if max_iou > 0.1:
|
|
|
|
- a = merged_regions[max_indices[0]]
|
|
|
|
- b = merged_regions[max_indices[1]]
|
|
|
|
- merged_regions.append(calculate_region(frame_shape,
|
|
|
|
- min(a[0], b[0]),
|
|
|
|
- min(a[1], b[1]),
|
|
|
|
- max(a[2], b[2]),
|
|
|
|
- max(a[3], b[3]),
|
|
|
|
- 1
|
|
|
|
- ))
|
|
|
|
- del merged_regions[max(max_indices[0], max_indices[1])]
|
|
|
|
- del merged_regions[min(max_indices[0], max_indices[1])]
|
|
|
|
- else:
|
|
|
|
- break
|
|
|
|
|
|
+ tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
|
|
|
|
+
|
|
|
|
+ # combine motion boxes with known locations of existing objects
|
|
|
|
+ combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
|
|
|
|
+
|
|
|
|
+ # compute regions
|
|
|
|
+ regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
|
|
|
|
+ for a in combined_boxes]
|
|
|
|
+
|
|
|
|
+ # combine overlapping regions
|
|
|
|
+ combined_regions = reduce_boxes(regions)
|
|
|
|
|
|
|
|
+ # re-compute regions
|
|
|
|
+ regions = [calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
|
|
|
|
+ for a in combined_regions]
|
|
|
|
+
|
|
# resize regions and detect
|
|
# resize regions and detect
|
|
detections = []
|
|
detections = []
|
|
- for region in merged_regions:
|
|
|
|
-
|
|
|
|
- tensor_input = create_tensor_input(frame, region)
|
|
|
|
-
|
|
|
|
- region_detections = object_detector.detect(tensor_input)
|
|
|
|
-
|
|
|
|
- for d in region_detections:
|
|
|
|
- box = d[2]
|
|
|
|
- size = region[2]-region[0]
|
|
|
|
- x_min = int((box[1] * size) + region[0])
|
|
|
|
- y_min = int((box[0] * size) + region[1])
|
|
|
|
- x_max = int((box[3] * size) + region[0])
|
|
|
|
- y_max = int((box[2] * size) + region[1])
|
|
|
|
- det = (d[0],
|
|
|
|
- d[1],
|
|
|
|
- (x_min, y_min, x_max, y_max),
|
|
|
|
- (x_max-x_min)*(y_max-y_min),
|
|
|
|
- region)
|
|
|
|
- if filtered(det, objects_to_track, object_filters, mask):
|
|
|
|
- continue
|
|
|
|
- detections.append(det)
|
|
|
|
-
|
|
|
|
|
|
+ for region in regions:
|
|
|
|
+ detections.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
|
|
|
|
+
|
|
#########
|
|
#########
|
|
- # merge objects, check for clipped objects and look again up to N times
|
|
|
|
|
|
+ # merge objects, check for clipped objects and look again up to 4 times
|
|
#########
|
|
#########
|
|
refining = True
|
|
refining = True
|
|
refine_count = 0
|
|
refine_count = 0
|
|
@@ -345,40 +342,20 @@ def track_camera(name, config, global_objects_config, frame_queue, frame_shape,
|
|
box[0], box[1],
|
|
box[0], box[1],
|
|
box[2], box[3])
|
|
box[2], box[3])
|
|
|
|
|
|
- tensor_input = create_tensor_input(frame, region)
|
|
|
|
- # run detection on new region
|
|
|
|
- refined_detections = object_detector.detect(tensor_input)
|
|
|
|
- for d in refined_detections:
|
|
|
|
- box = d[2]
|
|
|
|
- size = region[2]-region[0]
|
|
|
|
- x_min = int((box[1] * size) + region[0])
|
|
|
|
- y_min = int((box[0] * size) + region[1])
|
|
|
|
- x_max = int((box[3] * size) + region[0])
|
|
|
|
- y_max = int((box[2] * size) + region[1])
|
|
|
|
- det = (d[0],
|
|
|
|
- d[1],
|
|
|
|
- (x_min, y_min, x_max, y_max),
|
|
|
|
- (x_max-x_min)*(y_max-y_min),
|
|
|
|
- region)
|
|
|
|
- if filtered(det, objects_to_track, object_filters, mask):
|
|
|
|
- continue
|
|
|
|
- selected_objects.append(det)
|
|
|
|
|
|
+ selected_objects.extend(detect(object_detector, frame, region, objects_to_track, object_filters, mask))
|
|
|
|
|
|
refining = True
|
|
refining = True
|
|
else:
|
|
else:
|
|
- selected_objects.append(obj)
|
|
|
|
-
|
|
|
|
|
|
+ selected_objects.append(obj)
|
|
# set the detections list to only include top, complete objects
|
|
# set the detections list to only include top, complete objects
|
|
# and new detections
|
|
# and new detections
|
|
detections = selected_objects
|
|
detections = selected_objects
|
|
|
|
|
|
if refining:
|
|
if refining:
|
|
refine_count += 1
|
|
refine_count += 1
|
|
-
|
|
|
|
|
|
+
|
|
# now that we have refined our detections, we need to track objects
|
|
# now that we have refined our detections, we need to track objects
|
|
object_tracker.match_and_update(frame_time, detections)
|
|
object_tracker.match_and_update(frame_time, detections)
|
|
|
|
|
|
# add to the queue
|
|
# add to the queue
|
|
- detected_objects_queue.put((name, frame_time, object_tracker.tracked_objects))
|
|
|
|
-
|
|
|
|
- print(f"{name}: exiting subprocess")
|
|
|
|
|
|
+ detected_objects_queue.put((camera_name, frame_time, object_tracker.tracked_objects))
|