mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
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:
parent
0f68fbc8db
commit
339b6944f1
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user