Smarter Regions

This commit is contained in:
Nick Mowen 2023-10-09 07:37:47 -06:00
parent cd35481e92
commit 6b49b81ba4
3 changed files with 194 additions and 12 deletions

View File

@ -49,6 +49,7 @@ from frigate.stats import StatsEmitter, stats_init
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
from frigate.util.object import get_camera_regions_grid
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog from frigate.watchdog import FrigateWatchdog
@ -69,6 +70,7 @@ class FrigateApp:
self.feature_metrics: dict[str, FeatureMetricsTypes] = {} self.feature_metrics: dict[str, FeatureMetricsTypes] = {}
self.ptz_metrics: dict[str, PTZMetricsTypes] = {} self.ptz_metrics: dict[str, PTZMetricsTypes] = {}
self.processes: dict[str, int] = {} self.processes: dict[str, int] = {}
self.region_grids: dict[str, list[list[dict[str, any]]]] = {}
def set_environment_vars(self) -> None: def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items(): for key, value in self.config.environment_vars.items():
@ -452,6 +454,10 @@ class FrigateApp:
output_processor.start() output_processor.start()
logger.info(f"Output process started: {output_processor.pid}") 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: def start_camera_processors(self) -> None:
for name, config in self.config.cameras.items(): for name, config in self.config.cameras.items():
if not self.config.cameras[name].enabled: if not self.config.cameras[name].enabled:
@ -471,6 +477,7 @@ class FrigateApp:
self.detected_frames_queue, self.detected_frames_queue,
self.camera_metrics[name], self.camera_metrics[name],
self.ptz_metrics[name], self.ptz_metrics[name],
self.region_grids[name],
), ),
) )
camera_process.daemon = True camera_process.daemon = True
@ -611,6 +618,7 @@ class FrigateApp:
self.start_detectors() self.start_detectors()
self.start_video_output_processor() self.start_video_output_processor()
self.start_ptz_autotracker() self.start_ptz_autotracker()
self.init_historical_regions()
self.start_detected_frames_processor() self.start_detected_frames_processor()
self.start_camera_processors() self.start_camera_processors()
self.start_camera_capture_processes() self.start_camera_capture_processes()

145
frigate/util/object.py Normal file
View File

@ -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

View File

@ -37,6 +37,7 @@ from frigate.util.image import (
yuv_region_2_rgb, yuv_region_2_rgb,
yuv_region_2_yuv, yuv_region_2_yuv,
) )
from frigate.util.object import get_cluster_region_from_grid
from frigate.util.services import listen from frigate.util.services import listen
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -457,6 +458,7 @@ def track_camera(
detected_objects_queue, detected_objects_queue,
process_info, process_info,
ptz_metrics, ptz_metrics,
region_grid,
): ):
stop_event = mp.Event() stop_event = mp.Event()
@ -515,6 +517,7 @@ def track_camera(
motion_enabled, motion_enabled,
stop_event, stop_event,
ptz_metrics, ptz_metrics,
region_grid,
) )
logger.info(f"{name}: exiting subprocess") logger.info(f"{name}: exiting subprocess")
@ -559,6 +562,14 @@ def intersects_any(box_a, boxes):
return False 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( def detect(
detect_config: DetectConfig, detect_config: DetectConfig,
object_detector, 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 # 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?) # 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 # 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, motion_enabled: mp.Value,
stop_event, stop_event,
ptz_metrics: PTZMetricsTypes, ptz_metrics: PTZMetricsTypes,
region_grid: list[list[dict[str, any]]],
exit_on_empty: bool = False, exit_on_empty: bool = False,
): ):
fps = process_info["process_fps"] fps = process_info["process_fps"]
@ -815,22 +829,37 @@ def process_frames(
if obj["id"] not in stationary_object_ids if obj["id"] not in stationary_object_ids
] ]
combined_boxes = tracked_object_boxes # get consolidated regions for tracked objects
# 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
)
regions = [ regions = [
get_cluster_region( 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 starting up, get the next startup scan region
if startup_scan_counter < 9: if startup_scan_counter < 9:
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3) ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)