mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +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...")
|
||||||
|
|
||||||
logger.debug(f"Changing layout size from {self.layout_dim} to {layout_dim}")
|
|
||||||
self.layout_dim = layout_dim
|
|
||||||
|
|
||||||
self.camera_layout = [None] * layout_dim * layout_dim
|
|
||||||
|
|
||||||
# calculate resolution of each position in the layout
|
|
||||||
self.layout_frame_shape = (
|
|
||||||
self.frame_shape[0] // layout_dim, # height
|
|
||||||
self.frame_shape[1] // layout_dim, # width
|
|
||||||
)
|
|
||||||
|
|
||||||
self.clear_frame()
|
self.clear_frame()
|
||||||
|
|
||||||
for cam_data in self.cameras.values():
|
|
||||||
cam_data["layout_frame"] = 0.0
|
|
||||||
|
|
||||||
self.active_cameras = set()
|
|
||||||
|
|
||||||
self.layout_offsets = []
|
|
||||||
|
|
||||||
# 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.layout_offsets.append((y_offset, x_offset))
|
|
||||||
|
|
||||||
removed_cameras = self.active_cameras.difference(active_cameras)
|
|
||||||
added_cameras = active_cameras.difference(self.active_cameras)
|
|
||||||
|
|
||||||
self.active_cameras = active_cameras
|
self.active_cameras = active_cameras
|
||||||
|
|
||||||
# this also converts added_cameras from a set to a list since we need
|
# this also converts added_cameras from a set to a list since we need
|
||||||
# to pop elements in order
|
# to pop elements in order
|
||||||
added_cameras = sorted(
|
active_cameras_to_add = sorted(
|
||||||
added_cameras,
|
active_cameras,
|
||||||
# sort cameras by order and by name if the order is the same
|
# sort cameras by order and by name if the order is the same
|
||||||
key=lambda added_camera: (
|
key=lambda active_camera: (
|
||||||
self.config.cameras[added_camera].birdseye.order,
|
self.config.cameras[active_camera].birdseye.order,
|
||||||
added_camera,
|
active_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
|
canvas_width = self.config.birdseye.width
|
||||||
for position, camera in enumerate(self.camera_layout, start=0):
|
canvas_height = self.config.birdseye.height
|
||||||
# if this camera was removed, replace it or clear it
|
|
||||||
if camera in removed_cameras:
|
if len(active_cameras) == 1:
|
||||||
# if replacing this camera with a newly added one
|
# show single camera as fullscreen
|
||||||
if len(added_cameras) > 0:
|
camera = active_cameras_to_add[0]
|
||||||
added_camera = added_cameras.pop()
|
camera_dims = self.cameras[camera]["dimensions"].copy()
|
||||||
self.camera_layout[position] = added_camera
|
scaled_width = int(canvas_height * camera_dims[0] / camera_dims[1])
|
||||||
self.copy_to_position(
|
coefficient = (
|
||||||
position,
|
1 if scaled_width <= canvas_width else canvas_width / scaled_width
|
||||||
added_camera,
|
|
||||||
self.cameras[added_camera]["current_frame"],
|
|
||||||
)
|
)
|
||||||
self.cameras[added_camera]["layout_frame"] = self.cameras[
|
self.camera_layout = [
|
||||||
added_camera
|
[
|
||||||
]["current_frame"]
|
(
|
||||||
# if removing this camera with no replacement
|
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:
|
else:
|
||||||
self.camera_layout[position] = None
|
# calculate optimal layout
|
||||||
self.copy_to_position(position)
|
coefficient = 1.0
|
||||||
removed_cameras.remove(camera)
|
|
||||||
# if an empty spot and there are cameras to add
|
# decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
|
||||||
elif camera is None and len(added_cameras) > 0:
|
while True:
|
||||||
added_camera = added_cameras.pop()
|
layout_candidate, total_height = calculate_layout(
|
||||||
self.camera_layout[position] = added_camera
|
(canvas_width, canvas_height),
|
||||||
self.copy_to_position(
|
active_cameras_to_add,
|
||||||
position,
|
coefficient,
|
||||||
added_camera,
|
|
||||||
self.cameras[added_camera]["current_frame"],
|
|
||||||
)
|
)
|
||||||
self.cameras[added_camera]["layout_frame"] = self.cameras[added_camera][
|
|
||||||
"current_frame"
|
if (canvas_height * 0.9) < total_height <= canvas_height:
|
||||||
]
|
break
|
||||||
# if not an empty spot and the camera has a newer frame, copy it
|
elif total_height < canvas_height * 0.8:
|
||||||
elif (
|
coefficient += 0.1
|
||||||
camera is not None
|
else:
|
||||||
and self.cameras[camera]["current_frame"]
|
coefficient -= 0.1
|
||||||
!= self.cameras[camera]["layout_frame"]
|
|
||||||
):
|
self.camera_layout = layout_candidate
|
||||||
|
|
||||||
|
for row in self.camera_layout:
|
||||||
|
for position in row:
|
||||||
self.copy_to_position(
|
self.copy_to_position(
|
||||||
position, camera, self.cameras[camera]["current_frame"]
|
position[1], position[0], self.cameras[position[0]]["current_frame"]
|
||||||
)
|
)
|
||||||
self.cameras[camera]["layout_frame"] = self.cameras[camera][
|
|
||||||
"current_frame"
|
|
||||||
]
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user