From 0592cedcdee32a0a74742f3acbda279c4ff80c53 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 16 Jun 2023 06:35:36 -0600 Subject: [PATCH] Rewrite birdseye auto layout (#6818) * Prefer horizontal layout to vertical * Rewrite birdseye to use aspect ratios instead of resolutions as layout configurator * Improve layout with slightly larger than 16:9 cameras * Remove manual 2 camera layout * Lint * Remove log --- frigate/output.py | 208 ++++++++++++++++++---------------------------- 1 file changed, 82 insertions(+), 126 deletions(-) diff --git a/frigate/output.py b/frigate/output.py index d04519997..942bed12e 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -1,6 +1,7 @@ import datetime import glob import logging +import math import multiprocessing as mp import os import queue @@ -269,161 +270,118 @@ class BirdsEyeFrameManager: def update_frame(self): """Update to a new frame for birdseye.""" - def calculate_two_cam_layout(canvas, cameras_to_add: list[str]) -> tuple[any]: - """Calculate the optimal layout for 2 cameras.""" - first_camera = cameras_to_add[0] - first_camera_dims = self.cameras[first_camera]["dimensions"].copy() - second_camera = cameras_to_add[1] - second_camera_dims = self.cameras[second_camera]["dimensions"].copy() - - # check for optimal layout - if first_camera_dims[0] + second_camera_dims[0] < canvas_width: - # place cameras horizontally - first_scaled_width = int( - canvas_height * first_camera_dims[0] / first_camera_dims[1] - ) - second_scaled_width = int( - canvas_height * second_camera_dims[0] / second_camera_dims[1] - ) - first_height = canvas_height - second_height = canvas_height - - if first_scaled_width + second_scaled_width > canvas_width: - if first_scaled_width > second_scaled_width: - first_scaled_width = canvas_width - second_scaled_width - first_height = int( - first_scaled_width - * first_camera_dims[1] - / first_camera_dims[0] - ) - else: - second_scaled_width = canvas_width - first_scaled_width - second_height = int( - second_scaled_width - * second_camera_dims[1] - / second_camera_dims[0] - ) - - return [ - [ - ( - first_camera, - (0, 0, first_scaled_width, first_height), - ), - ( - second_camera, - ( - first_scaled_width + 1, - 0, - second_scaled_width, - second_height, - ), - ), - ], - ] - else: - # place cameras vertically - top_scaled_width = int( - (canvas_height / 2) * first_camera_dims[0] / first_camera_dims[1] - ) - bottom_scaled_width = int( - (canvas_height / 2) * second_camera_dims[0] / second_camera_dims[1] - ) - return [ - [ - ( - first_camera, - (0, 0, top_scaled_width, int(canvas_height / 2)), - ) - ], - [ - ( - second_camera, - ( - 0, - int(canvas_height / 2), - bottom_scaled_width, - int(canvas_height / 2), - ), - ) - ], - ] - def calculate_layout( canvas, cameras_to_add: list[str], coefficient ) -> tuple[any]: - """Calculate the optimal layout for 3+ cameras.""" + """Calculate the optimal layout for 2+ cameras.""" camera_layout: list[list[any]] = [] camera_layout.append([]) - canvas_aspect = canvas[0] / canvas[1] + canvas_gcd = math.gcd(canvas[0], canvas[1]) + canvas_aspect_x = (canvas[0] / canvas_gcd) * coefficient + canvas_aspect_y = (canvas[0] / canvas_gcd) * coefficient starting_x = 0 x = starting_x y = 0 y_i = 0 - max_height = 0 + max_y = 0 for camera in cameras_to_add: camera_dims = self.cameras[camera]["dimensions"].copy() - camera_aspect = camera_dims[0] / camera_dims[1] + camera_gcd = math.gcd(camera_dims[0], camera_dims[1]) + camera_aspect_x = camera_dims[0] / camera_gcd + camera_aspect_y = camera_dims[1] / camera_gcd + + if round(camera_aspect_x / camera_aspect_y, 1) == 1.8: + # account for slightly off 16:9 cameras + camera_aspect_x = 16 + camera_aspect_y = 9 + elif round(camera_aspect_x / camera_aspect_y, 1) == 1.3: + # make 4:3 cameras the same relative size as 16:9 + camera_aspect_x = 12 + camera_aspect_y = 9 if camera_dims[1] > camera_dims[0]: portrait = True - elif camera_aspect < canvas_aspect: - # if the camera aspect ratio is less than canvas aspect ratio, it needs to be scaled down to fit - camera_dims[0] *= camera_aspect / canvas_aspect - camera_dims[1] *= camera_aspect / canvas_aspect - portrait = False else: portrait = False - if (x + camera_dims[0] * coefficient) <= canvas[0]: + if (x + camera_aspect_x) <= canvas_aspect_x: # insert if camera can fit on current row - scaled_width = int(camera_dims[0] * coefficient) camera_layout[y_i].append( ( camera, ( - x, - y, - scaled_width, - int(camera_dims[1] * coefficient), + camera_aspect_x, + camera_aspect_y, ), ) ) - x += scaled_width if portrait: - starting_x = scaled_width + starting_x = camera_aspect_x else: - max_height = max( - max_height, - int(camera_dims[1] * coefficient), + max_y = max( + max_y, + camera_aspect_y, ) + + x += camera_aspect_x else: # move on to the next row and insert - y += max_height + y += max_y y_i += 1 camera_layout.append([]) x = starting_x - if camera_dims[0] * coefficient > canvas_width: - safe_coefficient = 1 - else: - safe_coefficient = coefficient + if x + camera_aspect_x > canvas_aspect_x: + return None camera_layout[y_i].append( ( camera, - ( - x, - y, - int(camera_dims[0] * safe_coefficient), - int(camera_dims[1] * safe_coefficient), - ), + (camera_aspect_x, camera_aspect_y), ) ) - x += int(camera_dims[0] * safe_coefficient) + x += camera_aspect_x - return (camera_layout, y + max_height) + if y + max_y > canvas_aspect_y: + return None + + row_height = int(canvas_height / coefficient) + + final_camera_layout = [] + starting_x = 0 + y = 0 + + for row in camera_layout: + final_row = [] + x = starting_x + for cameras in row: + camera_dims = self.cameras[cameras[0]]["dimensions"].copy() + + if camera_dims[1] > camera_dims[0]: + scaled_height = int(row_height * coefficient) + scaled_width = int( + scaled_height * camera_dims[0] / camera_dims[1] + ) + starting_x = scaled_width + else: + scaled_height = row_height + scaled_width = int( + scaled_height * camera_dims[0] / camera_dims[1] + ) + + if ( + x + scaled_width > canvas_width + or y + scaled_height > canvas_height + ): + return None + + final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) + x += scaled_width + y += row_height + final_camera_layout.append(final_row) + + return final_camera_layout # determine how many cameras are tracking objects within the last 30 seconds active_cameras = set( @@ -493,30 +451,28 @@ class BirdsEyeFrameManager: ) ] ] - elif len(active_cameras) == 2: - self.camera_layout = calculate_two_cam_layout( - (canvas_width, canvas_height), active_cameras_to_add - ) else: # calculate optimal layout - coefficient = 1.0 + coefficient = 2 calculating = True # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas while calculating: - layout_candidate, total_height = calculate_layout( + layout_candidate = calculate_layout( (canvas_width, canvas_height), active_cameras_to_add, coefficient, ) - if (canvas_height * 0.75) < total_height <= canvas_height: - calculating = False - elif total_height < canvas_height * 0.75: - coefficient += 0.1 - calculating = False - else: - coefficient -= 0.1 + if not layout_candidate: + if coefficient < 10: + coefficient += 1 + continue + else: + logger.error("Error finding appropriate birdseye layout") + return + + calculating = False self.camera_layout = layout_candidate