mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Scale birdseye layout up to max size after it has been calculated (#6825)
* Scale layout up to max size after it has been calculated * Limit portrait cameras to taking up 2 rows * Fix bug * Fix birdsye not removing cameras once objects are no longer visible * Fix lint
This commit is contained in:
		
							parent
							
								
									c25367221e
								
							
						
					
					
						commit
						83edf9574e
					
				@ -276,10 +276,157 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
    def update_frame(self):
 | 
					    def update_frame(self):
 | 
				
			||||||
        """Update to a new frame for birdseye."""
 | 
					        """Update to a new frame for birdseye."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # determine how many cameras are tracking objects within the last 30 seconds
 | 
				
			||||||
 | 
					        active_cameras = set(
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                cam
 | 
				
			||||||
 | 
					                for cam, cam_data in self.cameras.items()
 | 
				
			||||||
 | 
					                if cam_data["last_active_frame"] > 0
 | 
				
			||||||
 | 
					                and cam_data["current_frame"] - cam_data["last_active_frame"] < 30
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # if there are no active cameras
 | 
				
			||||||
 | 
					        if len(active_cameras) == 0:
 | 
				
			||||||
 | 
					            # if the layout is already cleared
 | 
				
			||||||
 | 
					            if len(self.camera_layout) == 0:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            # if the layout needs to be cleared
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.camera_layout = []
 | 
				
			||||||
 | 
					                self.active_cameras = set()
 | 
				
			||||||
 | 
					                self.clear_frame()
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if we need to reset the layout because there is a different number of cameras
 | 
				
			||||||
 | 
					        reset_layout = len(self.active_cameras) - len(active_cameras) != 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # reset the layout if it needs to be different
 | 
				
			||||||
 | 
					        if reset_layout:
 | 
				
			||||||
 | 
					            logger.debug("Added new cameras, resetting layout...")
 | 
				
			||||||
 | 
					            self.clear_frame()
 | 
				
			||||||
 | 
					            self.active_cameras = active_cameras
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # this also converts added_cameras from a set to a list since we need
 | 
				
			||||||
 | 
					            # to pop elements in order
 | 
				
			||||||
 | 
					            active_cameras_to_add = sorted(
 | 
				
			||||||
 | 
					                active_cameras,
 | 
				
			||||||
 | 
					                # sort cameras by order and by name if the order is the same
 | 
				
			||||||
 | 
					                key=lambda active_camera: (
 | 
				
			||||||
 | 
					                    self.config.cameras[active_camera].birdseye.order,
 | 
				
			||||||
 | 
					                    active_camera,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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])
 | 
				
			||||||
 | 
					                coefficient = (
 | 
				
			||||||
 | 
					                    1 if scaled_width <= canvas_width else canvas_width / scaled_width
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                self.camera_layout = [
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        (
 | 
				
			||||||
 | 
					                            camera,
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                0,
 | 
				
			||||||
 | 
					                                0,
 | 
				
			||||||
 | 
					                                int(scaled_width * coefficient),
 | 
				
			||||||
 | 
					                                int(canvas_height * coefficient),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # calculate optimal layout
 | 
				
			||||||
 | 
					                coefficient = 2
 | 
				
			||||||
 | 
					                calculating = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
 | 
				
			||||||
 | 
					                while calculating:
 | 
				
			||||||
 | 
					                    if self.stop_event.is_set():
 | 
				
			||||||
 | 
					                        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    layout_candidate = self.calculate_layout(
 | 
				
			||||||
 | 
					                        (canvas_width, canvas_height),
 | 
				
			||||||
 | 
					                        active_cameras_to_add,
 | 
				
			||||||
 | 
					                        coefficient,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for row in self.camera_layout:
 | 
				
			||||||
 | 
					            for position in row:
 | 
				
			||||||
 | 
					                self.copy_to_position(
 | 
				
			||||||
 | 
					                    position[1], position[0], self.cameras[position[0]]["current_frame"]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def calculate_layout(
 | 
					    def calculate_layout(
 | 
				
			||||||
            canvas, cameras_to_add: list[str], coefficient
 | 
					        self, canvas, cameras_to_add: list[str], coefficient
 | 
				
			||||||
    ) -> tuple[any]:
 | 
					    ) -> tuple[any]:
 | 
				
			||||||
        """Calculate the optimal layout for 2+ cameras."""
 | 
					        """Calculate the optimal layout for 2+ cameras."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def map_layout(row_height: int):
 | 
				
			||||||
 | 
					            """Map the calculated layout."""
 | 
				
			||||||
 | 
					            candidate_layout = []
 | 
				
			||||||
 | 
					            starting_x = 0
 | 
				
			||||||
 | 
					            x = 0
 | 
				
			||||||
 | 
					            max_width = 0
 | 
				
			||||||
 | 
					            y = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for row in camera_layout:
 | 
				
			||||||
 | 
					                final_row = []
 | 
				
			||||||
 | 
					                max_width = max(max_width, x)
 | 
				
			||||||
 | 
					                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 * 2)
 | 
				
			||||||
 | 
					                        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]
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # layout is too large
 | 
				
			||||||
 | 
					                    if (
 | 
				
			||||||
 | 
					                        x + scaled_width > canvas_width
 | 
				
			||||||
 | 
					                        or y + scaled_height > canvas_height
 | 
				
			||||||
 | 
					                    ):
 | 
				
			||||||
 | 
					                        return 0, 0, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    final_row.append((cameras[0], (x, y, scaled_width, scaled_height)))
 | 
				
			||||||
 | 
					                    x += scaled_width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                y += row_height
 | 
				
			||||||
 | 
					                candidate_layout.append(final_row)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return max_width, y, candidate_layout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        canvas_width = canvas[0]
 | 
				
			||||||
 | 
					        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_gcd = math.gcd(canvas[0], canvas[1])
 | 
				
			||||||
@ -353,145 +500,22 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        row_height = int(canvas_height / coefficient)
 | 
					        row_height = int(canvas_height / coefficient)
 | 
				
			||||||
 | 
					        total_width, total_height, standard_candidate_layout = map_layout(row_height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            final_camera_layout = []
 | 
					        # layout can't be optimized more
 | 
				
			||||||
            starting_x = 0
 | 
					        if total_width / canvas_width >= 0.99:
 | 
				
			||||||
            y = 0
 | 
					            return standard_candidate_layout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for row in camera_layout:
 | 
					        scale_up_percent = min(
 | 
				
			||||||
                final_row = []
 | 
					            1 - (total_width / canvas_width), 1 - (total_height / canvas_height)
 | 
				
			||||||
                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
 | 
					        row_height = int(row_height * (1 + round(scale_up_percent, 1)))
 | 
				
			||||||
 | 
					        _, _, scaled_layout = map_layout(row_height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if scaled_layout:
 | 
				
			||||||
 | 
					            return scaled_layout
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
                        scaled_height = row_height
 | 
					            return standard_candidate_layout
 | 
				
			||||||
                        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(
 | 
					 | 
				
			||||||
            [
 | 
					 | 
				
			||||||
                cam
 | 
					 | 
				
			||||||
                for cam, cam_data in self.cameras.items()
 | 
					 | 
				
			||||||
                if cam_data["last_active_frame"] > 0
 | 
					 | 
				
			||||||
                and cam_data["current_frame"] - cam_data["last_active_frame"] < 30
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # if there are no active cameras
 | 
					 | 
				
			||||||
        if len(active_cameras) == 0:
 | 
					 | 
				
			||||||
            # if the layout is already cleared
 | 
					 | 
				
			||||||
            if len(self.camera_layout) == 0:
 | 
					 | 
				
			||||||
                return False
 | 
					 | 
				
			||||||
            # if the layout needs to be cleared
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                self.camera_layout = []
 | 
					 | 
				
			||||||
                self.active_cameras = set()
 | 
					 | 
				
			||||||
                self.clear_frame()
 | 
					 | 
				
			||||||
                return True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # check if we need to reset the layout because there are new cameras to add
 | 
					 | 
				
			||||||
        reset_layout = (
 | 
					 | 
				
			||||||
            True if len(active_cameras.difference(self.active_cameras)) > 0 else False
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # reset the layout if it needs to be different
 | 
					 | 
				
			||||||
        if reset_layout:
 | 
					 | 
				
			||||||
            logger.debug("Added new cameras, resetting layout...")
 | 
					 | 
				
			||||||
            self.clear_frame()
 | 
					 | 
				
			||||||
            self.active_cameras = active_cameras
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # this also converts added_cameras from a set to a list since we need
 | 
					 | 
				
			||||||
            # to pop elements in order
 | 
					 | 
				
			||||||
            active_cameras_to_add = sorted(
 | 
					 | 
				
			||||||
                active_cameras,
 | 
					 | 
				
			||||||
                # sort cameras by order and by name if the order is the same
 | 
					 | 
				
			||||||
                key=lambda active_camera: (
 | 
					 | 
				
			||||||
                    self.config.cameras[active_camera].birdseye.order,
 | 
					 | 
				
			||||||
                    active_camera,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            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])
 | 
					 | 
				
			||||||
                coefficient = (
 | 
					 | 
				
			||||||
                    1 if scaled_width <= canvas_width else canvas_width / scaled_width
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                self.camera_layout = [
 | 
					 | 
				
			||||||
                    [
 | 
					 | 
				
			||||||
                        (
 | 
					 | 
				
			||||||
                            camera,
 | 
					 | 
				
			||||||
                            (
 | 
					 | 
				
			||||||
                                0,
 | 
					 | 
				
			||||||
                                0,
 | 
					 | 
				
			||||||
                                int(scaled_width * coefficient),
 | 
					 | 
				
			||||||
                                int(canvas_height * coefficient),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    ]
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                # calculate optimal layout
 | 
					 | 
				
			||||||
                coefficient = 2
 | 
					 | 
				
			||||||
                calculating = True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
 | 
					 | 
				
			||||||
                while calculating:
 | 
					 | 
				
			||||||
                    if self.stop_event.is_set():
 | 
					 | 
				
			||||||
                        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    layout_candidate = calculate_layout(
 | 
					 | 
				
			||||||
                        (canvas_width, canvas_height),
 | 
					 | 
				
			||||||
                        active_cameras_to_add,
 | 
					 | 
				
			||||||
                        coefficient,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for row in self.camera_layout:
 | 
					 | 
				
			||||||
            for position in row:
 | 
					 | 
				
			||||||
                self.copy_to_position(
 | 
					 | 
				
			||||||
                    position[1], position[0], self.cameras[position[0]]["current_frame"]
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
 | 
					    def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
 | 
				
			||||||
        # don't process if birdseye is disabled for this camera
 | 
					        # don't process if birdseye is disabled for this camera
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user