mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-02-18 00:16:41 +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