mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Birdseye Autolayout (#6734)
* Calculate possible layout * Working in ideal conditions * Fix issues with different heights * Remove logs * Optimally handle cameras that don't match the canvas aspect ratio * Make sure to copy so list is not overwritten * Remove unused import * Remove try catch * Optimize layout for low amount of cameras * Try to scale frames up if not enough space is used
This commit is contained in:
		
							parent
							
								
									8d941e5e26
								
							
						
					
					
						commit
						fd6eb78f41
					
				@ -1,7 +1,6 @@
 | 
				
			|||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import glob
 | 
					import glob
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import math
 | 
					 | 
				
			||||||
import multiprocessing as mp
 | 
					import multiprocessing as mp
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import queue
 | 
					import queue
 | 
				
			||||||
@ -210,6 +209,7 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            self.cameras[camera] = {
 | 
					            self.cameras[camera] = {
 | 
				
			||||||
 | 
					                "dimensions": [settings.detect.width, settings.detect.height],
 | 
				
			||||||
                "last_active_frame": 0.0,
 | 
					                "last_active_frame": 0.0,
 | 
				
			||||||
                "current_frame": 0.0,
 | 
					                "current_frame": 0.0,
 | 
				
			||||||
                "layout_frame": 0.0,
 | 
					                "layout_frame": 0.0,
 | 
				
			||||||
@ -224,7 +224,6 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.camera_layout = []
 | 
					        self.camera_layout = []
 | 
				
			||||||
        self.active_cameras = set()
 | 
					        self.active_cameras = set()
 | 
				
			||||||
        self.layout_dim = 0
 | 
					 | 
				
			||||||
        self.last_output_time = 0.0
 | 
					        self.last_output_time = 0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clear_frame(self):
 | 
					    def clear_frame(self):
 | 
				
			||||||
@ -250,8 +249,8 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        copy_yuv_to_position(
 | 
					        copy_yuv_to_position(
 | 
				
			||||||
            self.frame,
 | 
					            self.frame,
 | 
				
			||||||
            self.layout_offsets[position],
 | 
					            [position[1], position[0]],
 | 
				
			||||||
            self.layout_frame_shape,
 | 
					            [position[3], position[2]],
 | 
				
			||||||
            frame,
 | 
					            frame,
 | 
				
			||||||
            channel_dims,
 | 
					            channel_dims,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -267,6 +266,67 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_frame(self):
 | 
					    def update_frame(self):
 | 
				
			||||||
 | 
					        """Update to a new frame for birdseye."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def calculate_layout(
 | 
				
			||||||
 | 
					            canvas, cameras_to_add: list[str], coefficient
 | 
				
			||||||
 | 
					        ) -> tuple[any]:
 | 
				
			||||||
 | 
					            """Calculate the optimal layout for cameras."""
 | 
				
			||||||
 | 
					            camera_layout: list[list[any]] = []
 | 
				
			||||||
 | 
					            camera_layout.append([])
 | 
				
			||||||
 | 
					            canvas_aspect = canvas[0] / canvas[1]
 | 
				
			||||||
 | 
					            x = 0
 | 
				
			||||||
 | 
					            y = 0
 | 
				
			||||||
 | 
					            y_i = 0
 | 
				
			||||||
 | 
					            max_height = 0
 | 
				
			||||||
 | 
					            for camera in cameras_to_add:
 | 
				
			||||||
 | 
					                camera_dims = self.cameras[camera]["dimensions"].copy()
 | 
				
			||||||
 | 
					                camera_aspect = camera_dims[0] / camera_dims[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # if the camera aspect ratio is less than canvas aspect ratio, it needs to be scaled down to fit
 | 
				
			||||||
 | 
					                if camera_aspect < canvas_aspect:
 | 
				
			||||||
 | 
					                    camera_dims[0] *= camera_aspect / canvas_aspect
 | 
				
			||||||
 | 
					                    camera_dims[1] *= camera_aspect / canvas_aspect
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (x + camera_dims[0] * coefficient) <= canvas[0]:
 | 
				
			||||||
 | 
					                    # insert if camera can fit on current row
 | 
				
			||||||
 | 
					                    camera_layout[y_i].append(
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            camera,
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                x,
 | 
				
			||||||
 | 
					                                y,
 | 
				
			||||||
 | 
					                                int(camera_dims[0] * coefficient),
 | 
				
			||||||
 | 
					                                int(camera_dims[1] * coefficient),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    x += int(camera_dims[0] * coefficient)
 | 
				
			||||||
 | 
					                    max_height = max(
 | 
				
			||||||
 | 
					                        max_height,
 | 
				
			||||||
 | 
					                        int(camera_dims[1] * coefficient),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    # move on to the next row and insert
 | 
				
			||||||
 | 
					                    y += max_height
 | 
				
			||||||
 | 
					                    y_i += 1
 | 
				
			||||||
 | 
					                    camera_layout.append([])
 | 
				
			||||||
 | 
					                    x = 0
 | 
				
			||||||
 | 
					                    camera_layout[y_i].append(
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            camera,
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                x,
 | 
				
			||||||
 | 
					                                y,
 | 
				
			||||||
 | 
					                                int(camera_dims[0] * coefficient),
 | 
				
			||||||
 | 
					                                int(camera_dims[1] * coefficient),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    x += int(camera_dims[0] * coefficient)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (camera_layout, y + max_height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # determine how many cameras are tracking objects within the last 30 seconds
 | 
					        # determine how many cameras are tracking objects within the last 30 seconds
 | 
				
			||||||
        active_cameras = set(
 | 
					        active_cameras = set(
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
@ -285,115 +345,108 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
            # if the layout needs to be cleared
 | 
					            # if the layout needs to be cleared
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                self.camera_layout = []
 | 
					                self.camera_layout = []
 | 
				
			||||||
                self.layout_dim = 0
 | 
					                self.active_cameras = set()
 | 
				
			||||||
                self.clear_frame()
 | 
					                self.clear_frame()
 | 
				
			||||||
                return True
 | 
					                return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # calculate layout dimensions
 | 
					 | 
				
			||||||
        layout_dim = math.ceil(math.sqrt(len(active_cameras)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # check if we need to reset the layout because there are new cameras to add
 | 
					        # check if we need to reset the layout because there are new cameras to add
 | 
				
			||||||
        reset_layout = (
 | 
					        reset_layout = (
 | 
				
			||||||
            True if len(active_cameras.difference(self.active_cameras)) > 0 else False
 | 
					            True if len(active_cameras.difference(self.active_cameras)) > 0 else False
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # reset the layout if it needs to be different
 | 
					        # reset the layout if it needs to be different
 | 
				
			||||||
        if layout_dim != self.layout_dim or reset_layout:
 | 
					        if reset_layout:
 | 
				
			||||||
            if reset_layout:
 | 
					            logger.debug("Added new cameras, resetting layout...")
 | 
				
			||||||
                logger.debug("Added new cameras, resetting layout...")
 | 
					            self.clear_frame()
 | 
				
			||||||
 | 
					            self.active_cameras = active_cameras
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logger.debug(f"Changing layout size from {self.layout_dim} to {layout_dim}")
 | 
					            # this also converts added_cameras from a set to a list since we need
 | 
				
			||||||
            self.layout_dim = layout_dim
 | 
					            # to pop elements in order
 | 
				
			||||||
 | 
					            active_cameras_to_add = sorted(
 | 
				
			||||||
            self.camera_layout = [None] * layout_dim * layout_dim
 | 
					                active_cameras,
 | 
				
			||||||
 | 
					                # sort cameras by order and by name if the order is the same
 | 
				
			||||||
            # calculate resolution of each position in the layout
 | 
					                key=lambda active_camera: (
 | 
				
			||||||
            self.layout_frame_shape = (
 | 
					                    self.config.cameras[active_camera].birdseye.order,
 | 
				
			||||||
                self.frame_shape[0] // layout_dim,  # height
 | 
					                    active_camera,
 | 
				
			||||||
                self.frame_shape[1] // layout_dim,  # width
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.clear_frame()
 | 
					            canvas_width = self.config.birdseye.width
 | 
				
			||||||
 | 
					            canvas_height = self.config.birdseye.height
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for cam_data in self.cameras.values():
 | 
					            if len(active_cameras) == 1:
 | 
				
			||||||
                cam_data["layout_frame"] = 0.0
 | 
					                # show single camera as fullscreen
 | 
				
			||||||
 | 
					                camera = active_cameras_to_add[0]
 | 
				
			||||||
            self.active_cameras = set()
 | 
					                camera_dims = self.cameras[camera]["dimensions"].copy()
 | 
				
			||||||
 | 
					                scaled_width = int(canvas_height * camera_dims[0] / camera_dims[1])
 | 
				
			||||||
            self.layout_offsets = []
 | 
					                coefficient = (
 | 
				
			||||||
 | 
					                    1 if scaled_width <= canvas_width else canvas_width / scaled_width
 | 
				
			||||||
            # calculate the x and y offset for each position in the layout
 | 
					 | 
				
			||||||
            for position in range(0, len(self.camera_layout)):
 | 
					 | 
				
			||||||
                y_offset = self.layout_frame_shape[0] * math.floor(
 | 
					 | 
				
			||||||
                    position / self.layout_dim
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                x_offset = self.layout_frame_shape[1] * (position % self.layout_dim)
 | 
					                self.camera_layout = [
 | 
				
			||||||
                self.layout_offsets.append((y_offset, x_offset))
 | 
					                    [
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            camera,
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                0,
 | 
				
			||||||
 | 
					                                0,
 | 
				
			||||||
 | 
					                                int(scaled_width * coefficient),
 | 
				
			||||||
 | 
					                                int(canvas_height * coefficient),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            elif len(active_cameras) == 2:
 | 
				
			||||||
 | 
					                # split canvas in half for 2 cameras
 | 
				
			||||||
 | 
					                top_camera = active_cameras_to_add[0]
 | 
				
			||||||
 | 
					                top_camera_dims = self.cameras[top_camera]["dimensions"].copy()
 | 
				
			||||||
 | 
					                bottom_camera = active_cameras_to_add[1]
 | 
				
			||||||
 | 
					                bottom_camera_dims = self.cameras[bottom_camera]["dimensions"].copy()
 | 
				
			||||||
 | 
					                top_scaled_width = int(
 | 
				
			||||||
 | 
					                    (canvas_height / 2) * top_camera_dims[0] / top_camera_dims[1]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                bottom_scaled_width = int(
 | 
				
			||||||
 | 
					                    (canvas_height / 2) * bottom_camera_dims[0] / bottom_camera_dims[1]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                self.camera_layout = [
 | 
				
			||||||
 | 
					                    [(top_camera, (0, 0, top_scaled_width, int(canvas_height / 2)))],
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            bottom_camera,
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                0,
 | 
				
			||||||
 | 
					                                int(canvas_height / 2),
 | 
				
			||||||
 | 
					                                bottom_scaled_width,
 | 
				
			||||||
 | 
					                                int(canvas_height / 2),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # calculate optimal layout
 | 
				
			||||||
 | 
					                coefficient = 1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        removed_cameras = self.active_cameras.difference(active_cameras)
 | 
					                # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
 | 
				
			||||||
        added_cameras = active_cameras.difference(self.active_cameras)
 | 
					                while True:
 | 
				
			||||||
 | 
					                    layout_candidate, total_height = calculate_layout(
 | 
				
			||||||
        self.active_cameras = active_cameras
 | 
					                        (canvas_width, canvas_height),
 | 
				
			||||||
 | 
					                        active_cameras_to_add,
 | 
				
			||||||
        # this also converts added_cameras from a set to a list since we need
 | 
					                        coefficient,
 | 
				
			||||||
        # to pop elements in order
 | 
					 | 
				
			||||||
        added_cameras = sorted(
 | 
					 | 
				
			||||||
            added_cameras,
 | 
					 | 
				
			||||||
            # sort cameras by order and by name if the order is the same
 | 
					 | 
				
			||||||
            key=lambda added_camera: (
 | 
					 | 
				
			||||||
                self.config.cameras[added_camera].birdseye.order,
 | 
					 | 
				
			||||||
                added_camera,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            # we're popping out elements from the end, so this needs to be reverse
 | 
					 | 
				
			||||||
            # as we want the last element to be the first
 | 
					 | 
				
			||||||
            reverse=True,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # update each position in the layout
 | 
					 | 
				
			||||||
        for position, camera in enumerate(self.camera_layout, start=0):
 | 
					 | 
				
			||||||
            # if this camera was removed, replace it or clear it
 | 
					 | 
				
			||||||
            if camera in removed_cameras:
 | 
					 | 
				
			||||||
                # if replacing this camera with a newly added one
 | 
					 | 
				
			||||||
                if len(added_cameras) > 0:
 | 
					 | 
				
			||||||
                    added_camera = added_cameras.pop()
 | 
					 | 
				
			||||||
                    self.camera_layout[position] = added_camera
 | 
					 | 
				
			||||||
                    self.copy_to_position(
 | 
					 | 
				
			||||||
                        position,
 | 
					 | 
				
			||||||
                        added_camera,
 | 
					 | 
				
			||||||
                        self.cameras[added_camera]["current_frame"],
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    self.cameras[added_camera]["layout_frame"] = self.cameras[
 | 
					
 | 
				
			||||||
                        added_camera
 | 
					                    if (canvas_height * 0.9) < total_height <= canvas_height:
 | 
				
			||||||
                    ]["current_frame"]
 | 
					                        break
 | 
				
			||||||
                # if removing this camera with no replacement
 | 
					                    elif total_height < canvas_height * 0.8:
 | 
				
			||||||
                else:
 | 
					                        coefficient += 0.1
 | 
				
			||||||
                    self.camera_layout[position] = None
 | 
					                    else:
 | 
				
			||||||
                    self.copy_to_position(position)
 | 
					                        coefficient -= 0.1
 | 
				
			||||||
                removed_cameras.remove(camera)
 | 
					
 | 
				
			||||||
            # if an empty spot and there are cameras to add
 | 
					                self.camera_layout = layout_candidate
 | 
				
			||||||
            elif camera is None and len(added_cameras) > 0:
 | 
					
 | 
				
			||||||
                added_camera = added_cameras.pop()
 | 
					        for row in self.camera_layout:
 | 
				
			||||||
                self.camera_layout[position] = added_camera
 | 
					            for position in row:
 | 
				
			||||||
                self.copy_to_position(
 | 
					                self.copy_to_position(
 | 
				
			||||||
                    position,
 | 
					                    position[1], position[0], self.cameras[position[0]]["current_frame"]
 | 
				
			||||||
                    added_camera,
 | 
					 | 
				
			||||||
                    self.cameras[added_camera]["current_frame"],
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                self.cameras[added_camera]["layout_frame"] = self.cameras[added_camera][
 | 
					 | 
				
			||||||
                    "current_frame"
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            # if not an empty spot and the camera has a newer frame, copy it
 | 
					 | 
				
			||||||
            elif (
 | 
					 | 
				
			||||||
                camera is not None
 | 
					 | 
				
			||||||
                and self.cameras[camera]["current_frame"]
 | 
					 | 
				
			||||||
                != self.cameras[camera]["layout_frame"]
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                self.copy_to_position(
 | 
					 | 
				
			||||||
                    position, camera, self.cameras[camera]["current_frame"]
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                self.cameras[camera]["layout_frame"] = self.cameras[camera][
 | 
					 | 
				
			||||||
                    "current_frame"
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user