Force birdseye cameras into standard aspect ratios (#7026)

* Force birdseye cameras into standard aspect ratios

* Clarify comment

* Formatting

* Actually use the calculated aspect ratio when building the layout

* Fix Y aspect

* Force canvas into known aspect ratio as well

* Save canvas size and don't recalculate

* Cache coefficients that are used for different size layouts

* Further optimize calculations to not be done multiple times
This commit is contained in:
Nicolas Mowen 2023-07-06 06:30:05 -06:00 committed by GitHub
parent 0f68fbc8db
commit 339b6944f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -29,6 +29,61 @@ from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_standard_aspect_ratio(width, height) -> tuple[int, int]:
"""Ensure that only standard aspect ratios are used."""
known_aspects = [
(16, 9),
(9, 16),
(32, 9),
(12, 9),
(9, 12),
] # aspects are scaled to have common relative size
known_aspects_ratios = list(
map(lambda aspect: aspect[0] / aspect[1], known_aspects)
)
closest = min(
known_aspects_ratios,
key=lambda x: abs(x - (width / height)),
)
return known_aspects[known_aspects_ratios.index(closest)]
class Canvas:
def __init__(self, canvas_width: int, canvas_height: int) -> None:
gcd = math.gcd(canvas_width, canvas_height)
self.aspect = get_standard_aspect_ratio(
(canvas_width / gcd), (canvas_height / gcd)
)
self.width = canvas_width
self.height = (self.width * self.aspect[1]) / self.aspect[0]
self.coefficient_cache: dict[int, int] = {}
self.aspect_cache: dict[str, tuple[int, int]] = {}
def get_aspect(self, coefficient: int) -> tuple[int, int]:
return (self.aspect[0] * coefficient, self.aspect[1] * coefficient)
def get_coefficient(self, camera_count: int) -> int:
return self.coefficient_cache.get(camera_count, 2)
def set_coefficient(self, camera_count: int, coefficient: int) -> None:
self.coefficient_cache[camera_count] = coefficient
def get_camera_aspect(
self, cam_name: str, camera_width: int, camera_height: int
) -> tuple[int, int]:
cached = self.aspect_cache.get(cam_name)
if cached:
return cached
gcd = math.gcd(camera_width, camera_height)
camera_aspect = get_standard_aspect_ratio(
camera_width / gcd, camera_height / gcd
)
self.aspect_cache[cam_name] = camera_aspect
return camera_aspect
class FFMpegConverter: class FFMpegConverter:
def __init__( def __init__(
self, self,
@ -170,6 +225,7 @@ class BirdsEyeFrameManager:
self.frame_shape = (height, width) self.frame_shape = (height, width)
self.yuv_shape = (height * 3 // 2, width) self.yuv_shape = (height * 3 // 2, width)
self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8) self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
self.canvas = Canvas(width, height)
self.stop_event = stop_event self.stop_event = stop_event
# initialize the frame as black and with the Frigate logo # initialize the frame as black and with the Frigate logo
@ -318,16 +374,15 @@ class BirdsEyeFrameManager:
), ),
) )
canvas_width = self.config.birdseye.width
canvas_height = self.config.birdseye.height
if len(active_cameras) == 1: if len(active_cameras) == 1:
# show single camera as fullscreen # show single camera as fullscreen
camera = active_cameras_to_add[0] camera = active_cameras_to_add[0]
camera_dims = self.cameras[camera]["dimensions"].copy() camera_dims = self.cameras[camera]["dimensions"].copy()
scaled_width = int(canvas_height * camera_dims[0] / camera_dims[1]) scaled_width = int(self.canvas.height * camera_dims[0] / camera_dims[1])
coefficient = ( coefficient = (
1 if scaled_width <= canvas_width else canvas_width / scaled_width 1
if scaled_width <= self.canvas.width
else self.canvas.width / scaled_width
) )
self.camera_layout = [ self.camera_layout = [
[ [
@ -337,14 +392,14 @@ class BirdsEyeFrameManager:
0, 0,
0, 0,
int(scaled_width * coefficient), int(scaled_width * coefficient),
int(canvas_height * coefficient), int(self.canvas.height * coefficient),
), ),
) )
] ]
] ]
else: else:
# calculate optimal layout # calculate optimal layout
coefficient = 2 coefficient = self.canvas.get_coefficient(len(active_cameras))
calculating = True calculating = True
# decrease scaling coefficient until height of all cameras can fit into the birdseye canvas # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
@ -353,7 +408,6 @@ class BirdsEyeFrameManager:
return return
layout_candidate = self.calculate_layout( layout_candidate = self.calculate_layout(
(canvas_width, canvas_height),
active_cameras_to_add, active_cameras_to_add,
coefficient, coefficient,
) )
@ -367,6 +421,7 @@ class BirdsEyeFrameManager:
return return
calculating = False calculating = False
self.canvas.set_coefficient(len(active_cameras), coefficient)
self.camera_layout = layout_candidate self.camera_layout = layout_candidate
@ -378,9 +433,7 @@ class BirdsEyeFrameManager:
return True return True
def calculate_layout( def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]:
self, canvas, cameras_to_add: list[str], coefficient
) -> tuple[any]:
"""Calculate the optimal layout for 2+ cameras.""" """Calculate the optimal layout for 2+ cameras."""
def map_layout(row_height: int): def map_layout(row_height: int):
@ -397,23 +450,20 @@ class BirdsEyeFrameManager:
x = starting_x x = starting_x
for cameras in row: for cameras in row:
camera_dims = self.cameras[cameras[0]]["dimensions"].copy() camera_dims = self.cameras[cameras[0]]["dimensions"].copy()
camera_aspect = cameras[1]
if camera_dims[1] > camera_dims[0]: if camera_dims[1] > camera_dims[0]:
scaled_height = int(row_height * 2) scaled_height = int(row_height * 2)
scaled_width = int( scaled_width = int(scaled_height * camera_aspect)
scaled_height * camera_dims[0] / camera_dims[1]
)
starting_x = scaled_width starting_x = scaled_width
else: else:
scaled_height = row_height scaled_height = row_height
scaled_width = int( scaled_width = int(scaled_height * camera_aspect)
scaled_height * camera_dims[0] / camera_dims[1]
)
# layout is too large # layout is too large
if ( if (
x + scaled_width > canvas_width x + scaled_width > self.canvas.width
or y + scaled_height > canvas_height or y + scaled_height > self.canvas.height
): ):
return 0, 0, None return 0, 0, None
@ -425,13 +475,9 @@ class BirdsEyeFrameManager:
return max_width, y, candidate_layout return max_width, y, candidate_layout
canvas_width = canvas[0] canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient)
canvas_height = canvas[1]
camera_layout: list[list[any]] = [] camera_layout: list[list[any]] = []
camera_layout.append([]) camera_layout.append([])
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 starting_x = 0
x = starting_x x = starting_x
y = 0 y = 0
@ -439,18 +485,9 @@ class BirdsEyeFrameManager:
max_y = 0 max_y = 0
for camera in cameras_to_add: for camera in cameras_to_add:
camera_dims = self.cameras[camera]["dimensions"].copy() camera_dims = self.cameras[camera]["dimensions"].copy()
camera_gcd = math.gcd(camera_dims[0], camera_dims[1]) camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect(
camera_aspect_x = camera_dims[0] / camera_gcd camera, camera_dims[0], camera_dims[1]
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]: if camera_dims[1] > camera_dims[0]:
portrait = True portrait = True
@ -462,10 +499,7 @@ class BirdsEyeFrameManager:
camera_layout[y_i].append( camera_layout[y_i].append(
( (
camera, camera,
( camera_aspect_x / camera_aspect_y,
camera_aspect_x,
camera_aspect_y,
),
) )
) )
@ -491,7 +525,7 @@ class BirdsEyeFrameManager:
camera_layout[y_i].append( camera_layout[y_i].append(
( (
camera, camera,
(camera_aspect_x, camera_aspect_y), camera_aspect_x / camera_aspect_y,
) )
) )
x += camera_aspect_x x += camera_aspect_x
@ -499,15 +533,16 @@ class BirdsEyeFrameManager:
if y + max_y > canvas_aspect_y: if y + max_y > canvas_aspect_y:
return None return None
row_height = int(canvas_height / coefficient) row_height = int(self.canvas.height / coefficient)
total_width, total_height, standard_candidate_layout = map_layout(row_height) total_width, total_height, standard_candidate_layout = map_layout(row_height)
# layout can't be optimized more # layout can't be optimized more
if total_width / canvas_width >= 0.99: if total_width / self.canvas.width >= 0.99:
return standard_candidate_layout return standard_candidate_layout
scale_up_percent = min( scale_up_percent = min(
1 - (total_width / canvas_width), 1 - (total_height / canvas_height) 1 - (total_width / self.canvas.width),
1 - (total_height / self.canvas.height),
) )
row_height = int(row_height * (1 + round(scale_up_percent, 1))) row_height = int(row_height * (1 + round(scale_up_percent, 1)))
_, _, scaled_layout = map_layout(row_height) _, _, scaled_layout = map_layout(row_height)