diff --git a/frigate/app.py b/frigate/app.py index a2e300526..64878d92d 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -49,6 +49,7 @@ from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes +from frigate.util.object import get_camera_regions_grid from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog @@ -69,6 +70,7 @@ class FrigateApp: self.feature_metrics: dict[str, FeatureMetricsTypes] = {} self.ptz_metrics: dict[str, PTZMetricsTypes] = {} self.processes: dict[str, int] = {} + self.region_grids: dict[str, list[list[dict[str, any]]]] = {} def set_environment_vars(self) -> None: for key, value in self.config.environment_vars.items(): @@ -452,6 +454,10 @@ class FrigateApp: output_processor.start() logger.info(f"Output process started: {output_processor.pid}") + def init_historical_regions(self) -> None: + for camera in self.config.cameras.values(): + self.region_grids[camera.name] = get_camera_regions_grid(camera) + def start_camera_processors(self) -> None: for name, config in self.config.cameras.items(): if not self.config.cameras[name].enabled: @@ -471,6 +477,7 @@ class FrigateApp: self.detected_frames_queue, self.camera_metrics[name], self.ptz_metrics[name], + self.region_grids[name], ), ) camera_process.daemon = True @@ -611,6 +618,7 @@ class FrigateApp: self.start_detectors() self.start_video_output_processor() self.start_ptz_autotracker() + self.init_historical_regions() self.start_detected_frames_processor() self.start_camera_processors() self.start_camera_capture_processes() diff --git a/frigate/util/object.py b/frigate/util/object.py new file mode 100644 index 000000000..967340fc6 --- /dev/null +++ b/frigate/util/object.py @@ -0,0 +1,145 @@ +"""Utils for reading and writing object detection data.""" + +import logging + +import numpy + +from frigate.config import CameraConfig +from frigate.models import Timeline +from frigate.util.image import calculate_region + +logger = logging.getLogger(__name__) + + +def get_camera_regions_grid( + camera: CameraConfig, grid_size: int = 8 +) -> list[list[dict[str, any]]]: + """Build a grid of expected region sizes for a camera.""" + # create a grid + grid = [] + for x in range(grid_size): + row = [] + for y in range(grid_size): + row.append({"sizes": []}) + grid.append(row) + + timeline = ( + Timeline.select( + *[ + Timeline.camera, + Timeline.source, + Timeline.data, + ] + ) + .where(Timeline.camera == camera.name) + .limit(10000) + .dicts() + ) + + if not timeline: + return grid + + logger.debug(f"There are {len(timeline)} entries for {camera.name}") + width = camera.detect.width + height = camera.detect.height + + logger.debug(f"The size of grid is {len(grid)} x {len(grid[grid_size - 1])}") + grid_coef = 1.0 / grid_size + + for t in timeline: + if t.get("source") != "tracked_object": + continue + + box = t["data"]["box"] + + # calculate centroid position + x = box[0] + (box[2] / 2) + y = box[1] + (box[3] / 2) + + x_pos = int(x * grid_size) + y_pos = int(y * grid_size) + + calculated_region = calculate_region( + (height, width), + box[0] * width, + box[1] * height, + (box[0] + box[2]) * width, + (box[1] + box[3]) * height, + 320, + 1.35, + ) + # save width of region to grid as relative + grid[x_pos][y_pos]["sizes"].append((calculated_region[2] - calculated_region[0]) / width) + + for x in range(grid_size): + for y in range(grid_size): + cell = grid[x][y] + logger.debug( + f"stats for cell {x * grid_coef * width},{y * grid_coef * height} -> {(x + 1) * grid_coef * width},{(y + 1) * grid_coef * height} :: {len(cell['sizes'])} objects" + ) + + if len(cell["sizes"]) == 0: + continue + + std_dev = numpy.std(cell["sizes"]) + mean = numpy.mean(cell["sizes"]) + logger.debug(f"std dev: {std_dev} mean: {mean}") + cell["std_dev"] = std_dev + cell["mean"] = mean + + return grid + + +def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region_grid): + 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 get_region_from_grid( + frame_shape, [min_x, min_y, max_x, max_y], min_region, region_grid + ) + + +def get_region_from_grid( + frame_shape: tuple[int], + box: list[int], + min_region: int, + region_grid: list[list[dict[str, any]]], +) -> list[int]: + """Get a region for a box based on the region grid.""" + centroid = (box[0] - (box[2] - box[0]), box[1] - (box[3] - box[1])) + grid_x = int(centroid[0] / frame_shape[1] * len(region_grid)) + grid_y = int(centroid[1] / frame_shape[0] * len(region_grid)) + + cell = region_grid[grid_x][grid_y] + + # if there is no known data, get standard region for motion box + if not cell or not cell["sizes"]: + return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region) + + calc_size = (box[2] - box[0]) / frame_shape[1] + + # if region is within expected size, don't resize + if (cell["mean"] - cell["std_dev"]) < calc_size < (cell["mean"] + cell["std_dev"]): + return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region) + # TODO not sure how to handle case where cluster is larger than expected region + elif calc_size > (cell["mean"] + cell["std_dev"]): + return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region) + + size = cell["mean"] * frame_shape[1] + + # get region based on grid size + new = calculate_region( + frame_shape, + max(0, centroid[0] - size / 2), + max(0, centroid[1] - size / 2), + min(frame_shape[1], centroid[0] + size / 2), + min(frame_shape[0], centroid[1] + size / 2), + min_region, + ) + return new diff --git a/frigate/video.py b/frigate/video.py index 47e65811d..3cf90785c 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -37,6 +37,7 @@ from frigate.util.image import ( yuv_region_2_rgb, yuv_region_2_yuv, ) +from frigate.util.object import get_cluster_region_from_grid from frigate.util.services import listen logger = logging.getLogger(__name__) @@ -457,6 +458,7 @@ def track_camera( detected_objects_queue, process_info, ptz_metrics, + region_grid, ): stop_event = mp.Event() @@ -515,6 +517,7 @@ def track_camera( motion_enabled, stop_event, ptz_metrics, + region_grid, ) logger.info(f"{name}: exiting subprocess") @@ -559,6 +562,14 @@ def intersects_any(box_a, boxes): return False +def inside_any(box_a, boxes): + for box in boxes: + # check if box_a is inside of box + if box_inside(box, box_a): + return True + return False + + def detect( detect_config: DetectConfig, object_detector, @@ -623,7 +634,9 @@ def get_cluster_boundary(box, min_region): ] -def get_cluster_candidates(frame_shape, min_region, boxes): +def get_cluster_candidates( + frame_shape, min_region, boxes, region_grid: list[list[dict[str, any]]] +): # 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 @@ -740,6 +753,7 @@ def process_frames( motion_enabled: mp.Value, stop_event, ptz_metrics: PTZMetricsTypes, + region_grid: list[list[dict[str, any]]], exit_on_empty: bool = False, ): fps = process_info["process_fps"] @@ -815,22 +829,37 @@ 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, region_grid ) - for candidate in cluster_candidates ] + # 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, region_min_size, standalone_motion_boxes, region_grid + ) + 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_counter < 9: ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)