forked from Github/frigate
Smarter Regions (#8194)
* Smarter Regions * Formatting * Cleanup * Fix motion region checking logic * Add database table and migration for regions * Update region grid on startup * Revert init delay change * Fix mypy * Move object related functions to util * Remove unused * Fix tests * Remove log * Update the region daily at 2 * Fix logic * Formatting * Initialize grid before starting processing frames * Move back to creating grid in main process * Formatting * Fixes * Formating * Fix region check * Accept all but true * Use regions grid for startup scan * Add clarifying comment * Fix new grid requests * Add tests * Delete stale region grids from DB
This commit is contained in:
355
frigate/video.py
355
frigate/video.py
@@ -1,6 +1,5 @@
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import queue
|
||||
@@ -15,8 +14,12 @@ import numpy as np
|
||||
from setproctitle import setproctitle
|
||||
|
||||
from frigate.config import CameraConfig, DetectConfig, ModelConfig
|
||||
from frigate.const import ALL_ATTRIBUTE_LABELS, ATTRIBUTE_LABEL_MAP, CACHE_DIR
|
||||
from frigate.detectors.detector_config import PixelFormatEnum
|
||||
from frigate.const import (
|
||||
ALL_ATTRIBUTE_LABELS,
|
||||
ATTRIBUTE_LABEL_MAP,
|
||||
CACHE_DIR,
|
||||
REQUEST_REGION_GRID,
|
||||
)
|
||||
from frigate.log import LogPipe
|
||||
from frigate.motion import MotionDetector
|
||||
from frigate.motion.improved_motion import ImprovedMotionDetector
|
||||
@@ -24,103 +27,30 @@ from frigate.object_detection import RemoteObjectDetector
|
||||
from frigate.track import ObjectTracker
|
||||
from frigate.track.norfair_tracker import NorfairTracker
|
||||
from frigate.types import PTZMetricsTypes
|
||||
from frigate.util.builtin import EventsPerSecond
|
||||
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_2
|
||||
from frigate.util.image import (
|
||||
FrameManager,
|
||||
SharedMemoryFrameManager,
|
||||
area,
|
||||
calculate_region,
|
||||
draw_box_with_label,
|
||||
intersection,
|
||||
intersection_over_union,
|
||||
yuv_region_2_bgr,
|
||||
yuv_region_2_rgb,
|
||||
yuv_region_2_yuv,
|
||||
)
|
||||
from frigate.util.object import (
|
||||
box_inside,
|
||||
create_tensor_input,
|
||||
get_cluster_candidates,
|
||||
get_cluster_region,
|
||||
get_cluster_region_from_grid,
|
||||
get_consolidated_object_detections,
|
||||
get_min_region_size,
|
||||
get_startup_regions,
|
||||
inside_any,
|
||||
intersects_any,
|
||||
is_object_filtered,
|
||||
)
|
||||
from frigate.util.services import listen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def filtered(obj, objects_to_track, object_filters):
|
||||
object_name = obj[0]
|
||||
object_score = obj[1]
|
||||
object_box = obj[2]
|
||||
object_area = obj[3]
|
||||
object_ratio = obj[4]
|
||||
|
||||
if object_name not in objects_to_track:
|
||||
return True
|
||||
|
||||
if object_name in object_filters:
|
||||
obj_settings = object_filters[object_name]
|
||||
|
||||
# if the min area is larger than the
|
||||
# detected object, don't add it to detected objects
|
||||
if obj_settings.min_area > object_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 < object_area:
|
||||
return True
|
||||
|
||||
# if the score is lower than the min_score, skip
|
||||
if obj_settings.min_score > object_score:
|
||||
return True
|
||||
|
||||
# if the object is not proportionally wide enough
|
||||
if obj_settings.min_ratio > object_ratio:
|
||||
return True
|
||||
|
||||
# if the object is proportionally too wide
|
||||
if obj_settings.max_ratio < object_ratio:
|
||||
return True
|
||||
|
||||
if obj_settings.mask is not None:
|
||||
# compute the coordinates of the object and make sure
|
||||
# the location isn't outside the bounds of the image (can happen from rounding)
|
||||
object_xmin = object_box[0]
|
||||
object_xmax = object_box[2]
|
||||
object_ymax = object_box[3]
|
||||
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
|
||||
x_location = min(
|
||||
int((object_xmax + object_xmin) / 2.0),
|
||||
len(obj_settings.mask[0]) - 1,
|
||||
)
|
||||
|
||||
# if the object is in a masked location, don't add it to detected objects
|
||||
if obj_settings.mask[y_location][x_location] == 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_min_region_size(model_config: ModelConfig) -> int:
|
||||
"""Get the min region size."""
|
||||
return max(model_config.height, model_config.width)
|
||||
|
||||
|
||||
def create_tensor_input(frame, model_config: ModelConfig, region):
|
||||
if model_config.input_pixel_format == PixelFormatEnum.rgb:
|
||||
cropped_frame = yuv_region_2_rgb(frame, region)
|
||||
elif model_config.input_pixel_format == PixelFormatEnum.bgr:
|
||||
cropped_frame = yuv_region_2_bgr(frame, region)
|
||||
else:
|
||||
cropped_frame = yuv_region_2_yuv(frame, region)
|
||||
|
||||
# Resize if needed
|
||||
if cropped_frame.shape != (model_config.height, model_config.width, 3):
|
||||
cropped_frame = cv2.resize(
|
||||
cropped_frame,
|
||||
dsize=(model_config.width, model_config.height),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
|
||||
return np.expand_dims(cropped_frame, axis=0)
|
||||
|
||||
|
||||
def stop_ffmpeg(ffmpeg_process, logger):
|
||||
logger.info("Terminating the existing ffmpeg process...")
|
||||
ffmpeg_process.terminate()
|
||||
@@ -455,8 +385,10 @@ def track_camera(
|
||||
detection_queue,
|
||||
result_connection,
|
||||
detected_objects_queue,
|
||||
inter_process_queue,
|
||||
process_info,
|
||||
ptz_metrics,
|
||||
region_grid,
|
||||
):
|
||||
stop_event = mp.Event()
|
||||
|
||||
@@ -471,6 +403,7 @@ def track_camera(
|
||||
listen()
|
||||
|
||||
frame_queue = process_info["frame_queue"]
|
||||
region_grid_queue = process_info["region_grid_queue"]
|
||||
detection_enabled = process_info["detection_enabled"]
|
||||
motion_enabled = process_info["motion_enabled"]
|
||||
improve_contrast_enabled = process_info["improve_contrast_enabled"]
|
||||
@@ -499,7 +432,9 @@ def track_camera(
|
||||
|
||||
process_frames(
|
||||
name,
|
||||
inter_process_queue,
|
||||
frame_queue,
|
||||
region_grid_queue,
|
||||
frame_shape,
|
||||
model_config,
|
||||
config.detect,
|
||||
@@ -515,50 +450,12 @@ def track_camera(
|
||||
motion_enabled,
|
||||
stop_event,
|
||||
ptz_metrics,
|
||||
region_grid,
|
||||
)
|
||||
|
||||
logger.info(f"{name}: exiting subprocess")
|
||||
|
||||
|
||||
def box_overlaps(b1, b2):
|
||||
if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def box_inside(b1, b2):
|
||||
# check if b2 is inside b1
|
||||
if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def reduce_boxes(boxes, iou_threshold=0.0):
|
||||
clusters = []
|
||||
|
||||
for box in boxes:
|
||||
matched = 0
|
||||
for cluster in clusters:
|
||||
if intersection_over_union(box, cluster) > iou_threshold:
|
||||
matched = 1
|
||||
cluster[0] = min(cluster[0], box[0])
|
||||
cluster[1] = min(cluster[1], box[1])
|
||||
cluster[2] = max(cluster[2], box[2])
|
||||
cluster[3] = max(cluster[3], box[3])
|
||||
|
||||
if not matched:
|
||||
clusters.append(list(box))
|
||||
|
||||
return [tuple(c) for c in clusters]
|
||||
|
||||
|
||||
def intersects_any(box_a, boxes):
|
||||
for box in boxes:
|
||||
if box_overlaps(box_a, box):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def detect(
|
||||
detect_config: DetectConfig,
|
||||
object_detector,
|
||||
@@ -597,134 +494,17 @@ def detect(
|
||||
region,
|
||||
)
|
||||
# apply object filters
|
||||
if filtered(det, objects_to_track, object_filters):
|
||||
if is_object_filtered(det, objects_to_track, object_filters):
|
||||
continue
|
||||
detections.append(det)
|
||||
return detections
|
||||
|
||||
|
||||
def get_cluster_boundary(box, min_region):
|
||||
# compute the max region size for the current box (box is 10% of region)
|
||||
box_width = box[2] - box[0]
|
||||
box_height = box[3] - box[1]
|
||||
max_region_area = abs(box_width * box_height) / 0.1
|
||||
max_region_size = max(min_region, int(math.sqrt(max_region_area)))
|
||||
|
||||
centroid = (box_width / 2 + box[0], box_height / 2 + box[1])
|
||||
|
||||
max_x_dist = int(max_region_size - box_width / 2 * 1.1)
|
||||
max_y_dist = int(max_region_size - box_height / 2 * 1.1)
|
||||
|
||||
return [
|
||||
int(centroid[0] - max_x_dist),
|
||||
int(centroid[1] - max_y_dist),
|
||||
int(centroid[0] + max_x_dist),
|
||||
int(centroid[1] + max_y_dist),
|
||||
]
|
||||
|
||||
|
||||
def get_cluster_candidates(frame_shape, min_region, boxes):
|
||||
# and create a cluster of other boxes using it's max region size
|
||||
# only include boxes where the region is an appropriate(except the region could possibly be smaller?)
|
||||
# size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset
|
||||
# determined by the max_region size minus half the box + 20%
|
||||
# TODO: see if we can do this with numpy
|
||||
cluster_candidates = []
|
||||
used_boxes = []
|
||||
# loop over each box
|
||||
for current_index, b in enumerate(boxes):
|
||||
if current_index in used_boxes:
|
||||
continue
|
||||
cluster = [current_index]
|
||||
used_boxes.append(current_index)
|
||||
cluster_boundary = get_cluster_boundary(b, min_region)
|
||||
# find all other boxes that fit inside the boundary
|
||||
for compare_index, compare_box in enumerate(boxes):
|
||||
if compare_index in used_boxes:
|
||||
continue
|
||||
|
||||
# if the box is not inside the potential cluster area, cluster them
|
||||
if not box_inside(cluster_boundary, compare_box):
|
||||
continue
|
||||
|
||||
# get the region if you were to add this box to the cluster
|
||||
potential_cluster = cluster + [compare_index]
|
||||
cluster_region = get_cluster_region(
|
||||
frame_shape, min_region, potential_cluster, boxes
|
||||
)
|
||||
# if region could be smaller and either box would be too small
|
||||
# for the resulting region, dont cluster
|
||||
should_cluster = True
|
||||
if (cluster_region[2] - cluster_region[0]) > min_region:
|
||||
for b in potential_cluster:
|
||||
box = boxes[b]
|
||||
# boxes should be more than 5% of the area of the region
|
||||
if area(box) / area(cluster_region) < 0.05:
|
||||
should_cluster = False
|
||||
break
|
||||
|
||||
if should_cluster:
|
||||
cluster.append(compare_index)
|
||||
used_boxes.append(compare_index)
|
||||
cluster_candidates.append(cluster)
|
||||
|
||||
# return the unique clusters only
|
||||
unique = {tuple(sorted(c)) for c in cluster_candidates}
|
||||
return [list(tup) for tup in unique]
|
||||
|
||||
|
||||
def get_cluster_region(frame_shape, min_region, cluster, boxes):
|
||||
min_x = frame_shape[1]
|
||||
min_y = frame_shape[0]
|
||||
max_x = 0
|
||||
max_y = 0
|
||||
for b in cluster:
|
||||
min_x = min(boxes[b][0], min_x)
|
||||
min_y = min(boxes[b][1], min_y)
|
||||
max_x = max(boxes[b][2], max_x)
|
||||
max_y = max(boxes[b][3], max_y)
|
||||
return calculate_region(
|
||||
frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.2
|
||||
)
|
||||
|
||||
|
||||
def get_consolidated_object_detections(detected_object_groups):
|
||||
"""Drop detections that overlap too much"""
|
||||
consolidated_detections = []
|
||||
for group in detected_object_groups.values():
|
||||
# if the group only has 1 item, skip
|
||||
if len(group) == 1:
|
||||
consolidated_detections.append(group[0])
|
||||
continue
|
||||
|
||||
# sort smallest to largest by area
|
||||
sorted_by_area = sorted(group, key=lambda g: g[3])
|
||||
|
||||
for current_detection_idx in range(0, len(sorted_by_area)):
|
||||
current_detection = sorted_by_area[current_detection_idx][2]
|
||||
overlap = 0
|
||||
for to_check_idx in range(
|
||||
min(current_detection_idx + 1, len(sorted_by_area)),
|
||||
len(sorted_by_area),
|
||||
):
|
||||
to_check = sorted_by_area[to_check_idx][2]
|
||||
intersect_box = intersection(current_detection, to_check)
|
||||
# if 90% of smaller detection is inside of another detection, consolidate
|
||||
if (
|
||||
intersect_box is not None
|
||||
and area(intersect_box) / area(current_detection) > 0.9
|
||||
):
|
||||
overlap = 1
|
||||
break
|
||||
if overlap == 0:
|
||||
consolidated_detections.append(sorted_by_area[current_detection_idx])
|
||||
|
||||
return consolidated_detections
|
||||
|
||||
|
||||
def process_frames(
|
||||
camera_name: str,
|
||||
inter_process_queue: mp.Queue,
|
||||
frame_queue: mp.Queue,
|
||||
region_grid_queue: mp.Queue,
|
||||
frame_shape,
|
||||
model_config: ModelConfig,
|
||||
detect_config: DetectConfig,
|
||||
@@ -740,20 +520,35 @@ def process_frames(
|
||||
motion_enabled: mp.Value,
|
||||
stop_event,
|
||||
ptz_metrics: PTZMetricsTypes,
|
||||
region_grid,
|
||||
exit_on_empty: bool = False,
|
||||
):
|
||||
fps = process_info["process_fps"]
|
||||
detection_fps = process_info["detection_fps"]
|
||||
current_frame_time = process_info["detection_frame"]
|
||||
next_region_update = get_tomorrow_at_2()
|
||||
|
||||
fps_tracker = EventsPerSecond()
|
||||
fps_tracker.start()
|
||||
|
||||
startup_scan_counter = 0
|
||||
startup_scan = True
|
||||
|
||||
region_min_size = get_min_region_size(model_config)
|
||||
|
||||
while not stop_event.is_set():
|
||||
if (
|
||||
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
||||
> next_region_update
|
||||
):
|
||||
inter_process_queue.put((REQUEST_REGION_GRID, camera_name))
|
||||
|
||||
try:
|
||||
region_grid = region_grid_queue.get(True, 10)
|
||||
except queue.Empty:
|
||||
logger.error(f"Unable to get updated region grid for {camera_name}")
|
||||
|
||||
next_region_update = get_tomorrow_at_2()
|
||||
|
||||
try:
|
||||
if exit_on_empty:
|
||||
frame_time = frame_queue.get(False)
|
||||
@@ -815,40 +610,48 @@ def process_frames(
|
||||
if obj["id"] not in stationary_object_ids
|
||||
]
|
||||
|
||||
combined_boxes = tracked_object_boxes
|
||||
# only add in the motion boxes when not calibrating
|
||||
if not motion_detector.is_calibrating():
|
||||
combined_boxes += motion_boxes
|
||||
|
||||
cluster_candidates = get_cluster_candidates(
|
||||
frame_shape, region_min_size, combined_boxes
|
||||
)
|
||||
|
||||
# get consolidated regions for tracked objects
|
||||
regions = [
|
||||
get_cluster_region(
|
||||
frame_shape, region_min_size, candidate, combined_boxes
|
||||
frame_shape, region_min_size, candidate, tracked_object_boxes
|
||||
)
|
||||
for candidate in get_cluster_candidates(
|
||||
frame_shape, region_min_size, tracked_object_boxes
|
||||
)
|
||||
for candidate in cluster_candidates
|
||||
]
|
||||
|
||||
# if starting up, get the next startup scan region
|
||||
if startup_scan_counter < 9:
|
||||
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)
|
||||
ymax = int(frame_shape[0] / 3 + ymin)
|
||||
xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3)
|
||||
xmax = int(frame_shape[1] / 3 + xmin)
|
||||
regions.append(
|
||||
calculate_region(
|
||||
# only add in the motion boxes when not calibrating
|
||||
if not motion_detector.is_calibrating():
|
||||
# find motion boxes that are not inside tracked object regions
|
||||
standalone_motion_boxes = [
|
||||
b for b in motion_boxes if not inside_any(b, regions)
|
||||
]
|
||||
|
||||
if standalone_motion_boxes:
|
||||
motion_clusters = get_cluster_candidates(
|
||||
frame_shape,
|
||||
xmin,
|
||||
ymin,
|
||||
xmax,
|
||||
ymax,
|
||||
region_min_size,
|
||||
multiplier=1.2,
|
||||
standalone_motion_boxes,
|
||||
)
|
||||
)
|
||||
startup_scan_counter += 1
|
||||
motion_regions = [
|
||||
get_cluster_region_from_grid(
|
||||
frame_shape,
|
||||
region_min_size,
|
||||
candidate,
|
||||
standalone_motion_boxes,
|
||||
region_grid,
|
||||
)
|
||||
for candidate in motion_clusters
|
||||
]
|
||||
regions += motion_regions
|
||||
|
||||
# if starting up, get the next startup scan region
|
||||
if startup_scan:
|
||||
for region in get_startup_regions(
|
||||
frame_shape, region_min_size, region_grid
|
||||
):
|
||||
regions.append(region)
|
||||
startup_scan = False
|
||||
|
||||
# resize regions and detect
|
||||
# seed with stationary objects
|
||||
|
||||
Reference in New Issue
Block a user