mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01: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__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
@ -170,6 +225,7 @@ class BirdsEyeFrameManager:
 | 
			
		||||
        self.frame_shape = (height, width)
 | 
			
		||||
        self.yuv_shape = (height * 3 // 2, width)
 | 
			
		||||
        self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
 | 
			
		||||
        self.canvas = Canvas(width, height)
 | 
			
		||||
        self.stop_event = stop_event
 | 
			
		||||
 | 
			
		||||
        # 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:
 | 
			
		||||
                # show single camera as fullscreen
 | 
			
		||||
                camera = active_cameras_to_add[0]
 | 
			
		||||
                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 = (
 | 
			
		||||
                    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 = [
 | 
			
		||||
                    [
 | 
			
		||||
@ -337,14 +392,14 @@ class BirdsEyeFrameManager:
 | 
			
		||||
                                0,
 | 
			
		||||
                                0,
 | 
			
		||||
                                int(scaled_width * coefficient),
 | 
			
		||||
                                int(canvas_height * coefficient),
 | 
			
		||||
                                int(self.canvas.height * coefficient),
 | 
			
		||||
                            ),
 | 
			
		||||
                        )
 | 
			
		||||
                    ]
 | 
			
		||||
                ]
 | 
			
		||||
            else:
 | 
			
		||||
                # calculate optimal layout
 | 
			
		||||
                coefficient = 2
 | 
			
		||||
                coefficient = self.canvas.get_coefficient(len(active_cameras))
 | 
			
		||||
                calculating = True
 | 
			
		||||
 | 
			
		||||
                # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
 | 
			
		||||
@ -353,7 +408,6 @@ class BirdsEyeFrameManager:
 | 
			
		||||
                        return
 | 
			
		||||
 | 
			
		||||
                    layout_candidate = self.calculate_layout(
 | 
			
		||||
                        (canvas_width, canvas_height),
 | 
			
		||||
                        active_cameras_to_add,
 | 
			
		||||
                        coefficient,
 | 
			
		||||
                    )
 | 
			
		||||
@ -367,6 +421,7 @@ class BirdsEyeFrameManager:
 | 
			
		||||
                            return
 | 
			
		||||
 | 
			
		||||
                    calculating = False
 | 
			
		||||
                    self.canvas.set_coefficient(len(active_cameras), coefficient)
 | 
			
		||||
 | 
			
		||||
                self.camera_layout = layout_candidate
 | 
			
		||||
 | 
			
		||||
@ -378,9 +433,7 @@ class BirdsEyeFrameManager:
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def calculate_layout(
 | 
			
		||||
        self, canvas, cameras_to_add: list[str], coefficient
 | 
			
		||||
    ) -> tuple[any]:
 | 
			
		||||
    def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]:
 | 
			
		||||
        """Calculate the optimal layout for 2+ cameras."""
 | 
			
		||||
 | 
			
		||||
        def map_layout(row_height: int):
 | 
			
		||||
@ -397,23 +450,20 @@ class BirdsEyeFrameManager:
 | 
			
		||||
                x = starting_x
 | 
			
		||||
                for cameras in row:
 | 
			
		||||
                    camera_dims = self.cameras[cameras[0]]["dimensions"].copy()
 | 
			
		||||
                    camera_aspect = cameras[1]
 | 
			
		||||
 | 
			
		||||
                    if camera_dims[1] > camera_dims[0]:
 | 
			
		||||
                        scaled_height = int(row_height * 2)
 | 
			
		||||
                        scaled_width = int(
 | 
			
		||||
                            scaled_height * camera_dims[0] / camera_dims[1]
 | 
			
		||||
                        )
 | 
			
		||||
                        scaled_width = int(scaled_height * camera_aspect)
 | 
			
		||||
                        starting_x = scaled_width
 | 
			
		||||
                    else:
 | 
			
		||||
                        scaled_height = row_height
 | 
			
		||||
                        scaled_width = int(
 | 
			
		||||
                            scaled_height * camera_dims[0] / camera_dims[1]
 | 
			
		||||
                        )
 | 
			
		||||
                        scaled_width = int(scaled_height * camera_aspect)
 | 
			
		||||
 | 
			
		||||
                    # layout is too large
 | 
			
		||||
                    if (
 | 
			
		||||
                        x + scaled_width > canvas_width
 | 
			
		||||
                        or y + scaled_height > canvas_height
 | 
			
		||||
                        x + scaled_width > self.canvas.width
 | 
			
		||||
                        or y + scaled_height > self.canvas.height
 | 
			
		||||
                    ):
 | 
			
		||||
                        return 0, 0, None
 | 
			
		||||
 | 
			
		||||
@ -425,13 +475,9 @@ class BirdsEyeFrameManager:
 | 
			
		||||
 | 
			
		||||
            return max_width, y, candidate_layout
 | 
			
		||||
 | 
			
		||||
        canvas_width = canvas[0]
 | 
			
		||||
        canvas_height = canvas[1]
 | 
			
		||||
        canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient)
 | 
			
		||||
        camera_layout: list[list[any]] = []
 | 
			
		||||
        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
 | 
			
		||||
        x = starting_x
 | 
			
		||||
        y = 0
 | 
			
		||||
@ -439,18 +485,9 @@ class BirdsEyeFrameManager:
 | 
			
		||||
        max_y = 0
 | 
			
		||||
        for camera in cameras_to_add:
 | 
			
		||||
            camera_dims = self.cameras[camera]["dimensions"].copy()
 | 
			
		||||
            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
 | 
			
		||||
            camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect(
 | 
			
		||||
                camera, camera_dims[0], camera_dims[1]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if camera_dims[1] > camera_dims[0]:
 | 
			
		||||
                portrait = True
 | 
			
		||||
@ -462,10 +499,7 @@ class BirdsEyeFrameManager:
 | 
			
		||||
                camera_layout[y_i].append(
 | 
			
		||||
                    (
 | 
			
		||||
                        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,
 | 
			
		||||
                        (camera_aspect_x, camera_aspect_y),
 | 
			
		||||
                        camera_aspect_x / camera_aspect_y,
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                x += camera_aspect_x
 | 
			
		||||
@ -499,15 +533,16 @@ class BirdsEyeFrameManager:
 | 
			
		||||
        if y + max_y > canvas_aspect_y:
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
 | 
			
		||||
        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)))
 | 
			
		||||
        _, _, scaled_layout = map_layout(row_height)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user