mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-19 19:06:16 +01:00
Birdseye enhancements (#9778)
* Center single camera view * Implement scaling factor * Add config for maximum number of cameras * Add config for inactivity threshold * update docs
This commit is contained in:
parent
00804a0f81
commit
91cdf64602
@ -1,6 +1,8 @@
|
||||
# Birdseye
|
||||
|
||||
Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
|
||||
Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
|
||||
|
||||
## Birdseye Behavior
|
||||
|
||||
### Birdseye Modes
|
||||
|
||||
@ -34,6 +36,29 @@ cameras:
|
||||
enabled: False
|
||||
```
|
||||
|
||||
### Birdseye Inactivity
|
||||
|
||||
By default birdseye shows all cameras that have had the configured activity in the last 30 seconds, this can be configured:
|
||||
|
||||
```yaml
|
||||
birdseye:
|
||||
enabled: True
|
||||
inactivity_threshold: 15
|
||||
```
|
||||
|
||||
## Birdseye Layout
|
||||
|
||||
### Birdseye Dimensions
|
||||
|
||||
The resolution and aspect ratio of birdseye can be configured. Resolution will increase the quality but does not affect the layout. Changing the aspect ratio of birdseye does affect how cameras are laid out.
|
||||
|
||||
```yaml
|
||||
birdseye:
|
||||
enabled: True
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
### Sorting cameras in the Birdseye view
|
||||
|
||||
It is possible to override the order of cameras that are being shown in the Birdseye view.
|
||||
@ -55,3 +80,27 @@ cameras:
|
||||
```
|
||||
|
||||
*Note*: Cameras are sorted by default using their name to ensure a constant view inside Birdseye.
|
||||
|
||||
### Birdseye Cameras
|
||||
|
||||
It is possible to limit the number of cameras shown on birdseye at one time. When this is enabled, birdseye will show the cameras with most recent activity. There is a cooldown to ensure that cameras do not switch too frequently.
|
||||
|
||||
For example, this can be configured to only show the most recently active camera.
|
||||
|
||||
```yaml
|
||||
birdseye:
|
||||
enabled: True
|
||||
layout:
|
||||
max_cameras: 1
|
||||
```
|
||||
|
||||
### Birdseye Scaling
|
||||
|
||||
By default birdseye tries to fit 2 cameras in each row and then double in size until a suitable layout is found. The scaling can be configured with a value between 1.0 and 5.0 depending on use case.
|
||||
|
||||
```yaml
|
||||
birdseye:
|
||||
enabled: True
|
||||
layout:
|
||||
scaling_factor: 3.0
|
||||
```
|
||||
|
@ -145,6 +145,14 @@ birdseye:
|
||||
# motion - cameras are included if motion was detected in the last 30 seconds
|
||||
# continuous - all cameras are included always
|
||||
mode: objects
|
||||
# Optional: Threshold for camera activity to stop showing camera (default: shown below)
|
||||
inactivity_threshold: 30
|
||||
# Optional: Configure the birdseye layout
|
||||
layout:
|
||||
# Optional: Scaling factor for the layout calculator (default: shown below)
|
||||
scaling_factor: 2.0
|
||||
# Optional: Maximum number of cameras to show at one time, showing the most recent (default: show all cameras)
|
||||
max_cameras: 1
|
||||
|
||||
# Optional: ffmpeg configuration
|
||||
# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets
|
||||
|
@ -528,6 +528,13 @@ class BirdseyeModeEnum(str, Enum):
|
||||
return list(cls)[index]
|
||||
|
||||
|
||||
class BirdseyeLayoutConfig(FrigateBaseModel):
|
||||
scaling_factor: float = Field(
|
||||
default=2.0, title="Birdseye Scaling Factor", ge=1.0, le=5.0
|
||||
)
|
||||
max_cameras: Optional[int] = Field(default=None, title="Max cameras")
|
||||
|
||||
|
||||
class BirdseyeConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="Enable birdseye view.")
|
||||
restream: bool = Field(default=False, title="Restream birdseye via RTSP.")
|
||||
@ -539,9 +546,15 @@ class BirdseyeConfig(FrigateBaseModel):
|
||||
ge=1,
|
||||
le=31,
|
||||
)
|
||||
inactivity_threshold: int = Field(
|
||||
default=30, title="Birdseye Inactivity Threshold", gt=0
|
||||
)
|
||||
mode: BirdseyeModeEnum = Field(
|
||||
default=BirdseyeModeEnum.objects, title="Tracking mode."
|
||||
)
|
||||
layout: BirdseyeLayoutConfig = Field(
|
||||
default_factory=BirdseyeLayoutConfig, title="Birdseye Layout Config"
|
||||
)
|
||||
|
||||
|
||||
# uses BaseModel because some global attributes are not available at the camera level
|
||||
|
@ -41,11 +41,13 @@ def get_standard_aspect_ratio(width: int, height: int) -> tuple[int, int]:
|
||||
(16, 9),
|
||||
(9, 16),
|
||||
(20, 10),
|
||||
(16, 3), # max wide camera
|
||||
(16, 6), # reolink duo 2
|
||||
(32, 9), # panoramic cameras
|
||||
(12, 9),
|
||||
(9, 12),
|
||||
(22, 15), # Amcrest, NTSC DVT
|
||||
(1, 1), # fisheye
|
||||
] # aspects are scaled to have common relative size
|
||||
known_aspects_ratios = list(
|
||||
map(lambda aspect: aspect[0] / aspect[1], known_aspects)
|
||||
@ -74,7 +76,13 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
|
||||
|
||||
|
||||
class Canvas:
|
||||
def __init__(self, canvas_width: int, canvas_height: int) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
canvas_width: int,
|
||||
canvas_height: int,
|
||||
scaling_factor: int,
|
||||
) -> None:
|
||||
self.scaling_factor = scaling_factor
|
||||
gcd = math.gcd(canvas_width, canvas_height)
|
||||
self.aspect = get_standard_aspect_ratio(
|
||||
(canvas_width / gcd), (canvas_height / gcd)
|
||||
@ -88,7 +96,7 @@ class Canvas:
|
||||
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)
|
||||
return self.coefficient_cache.get(camera_count, self.scaling_factor)
|
||||
|
||||
def set_coefficient(self, camera_count: int, coefficient: int) -> None:
|
||||
self.coefficient_cache[camera_count] = coefficient
|
||||
@ -276,9 +284,13 @@ 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.canvas = Canvas(width, height, config.birdseye.layout.scaling_factor)
|
||||
self.stop_event = stop_event
|
||||
self.camera_metrics = camera_metrics
|
||||
self.inactivity_threshold = config.birdseye.inactivity_threshold
|
||||
|
||||
if config.birdseye.layout.max_cameras:
|
||||
self.last_refresh_time = 0
|
||||
|
||||
# initialize the frame as black and with the Frigate logo
|
||||
self.blank_frame = np.zeros(self.yuv_shape, np.uint8)
|
||||
@ -384,16 +396,39 @@ class BirdsEyeFrameManager:
|
||||
def update_frame(self):
|
||||
"""Update to a new frame for birdseye."""
|
||||
|
||||
# determine how many cameras are tracking objects within the last 30 seconds
|
||||
active_cameras = set(
|
||||
# determine how many cameras are tracking objects within the last inactivity_threshold seconds
|
||||
active_cameras: set[str] = 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
|
||||
and cam_data["current_frame"] - cam_data["last_active_frame"]
|
||||
< self.inactivity_threshold
|
||||
]
|
||||
)
|
||||
|
||||
max_cameras = self.config.birdseye.layout.max_cameras
|
||||
max_camera_refresh = False
|
||||
if max_cameras:
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
if len(active_cameras) == max_cameras and now - self.last_refresh_time < 10:
|
||||
# don't refresh cameras too often
|
||||
active_cameras = self.active_cameras
|
||||
else:
|
||||
limited_active_cameras = sorted(
|
||||
active_cameras,
|
||||
key=lambda active_camera: (
|
||||
self.cameras[active_camera]["current_frame"]
|
||||
- self.cameras[active_camera]["last_active_frame"]
|
||||
),
|
||||
)
|
||||
active_cameras = limited_active_cameras[
|
||||
: self.config.birdseye.layout.max_cameras
|
||||
]
|
||||
max_camera_refresh = True
|
||||
self.last_refresh_time = now
|
||||
|
||||
# if there are no active cameras
|
||||
if len(active_cameras) == 0:
|
||||
# if the layout is already cleared
|
||||
@ -407,7 +442,18 @@ class BirdsEyeFrameManager:
|
||||
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
|
||||
if len(self.active_cameras) - len(active_cameras) == 0:
|
||||
if (
|
||||
len(self.active_cameras) == 1
|
||||
and self.active_cameras[0] == active_cameras[0]
|
||||
):
|
||||
reset_layout = True
|
||||
elif max_camera_refresh:
|
||||
reset_layout = True
|
||||
else:
|
||||
reset_layout = False
|
||||
else:
|
||||
reset_layout = True
|
||||
|
||||
# reset the layout if it needs to be different
|
||||
if reset_layout:
|
||||
@ -431,17 +477,23 @@ class BirdsEyeFrameManager:
|
||||
camera = active_cameras_to_add[0]
|
||||
camera_dims = self.cameras[camera]["dimensions"].copy()
|
||||
scaled_width = int(self.canvas.height * camera_dims[0] / camera_dims[1])
|
||||
coefficient = (
|
||||
1
|
||||
if scaled_width <= self.canvas.width
|
||||
else self.canvas.width / scaled_width
|
||||
)
|
||||
|
||||
# center camera view in canvas and ensure that it fits
|
||||
if scaled_width < self.canvas.width:
|
||||
coefficient = 1
|
||||
x_offset = int((self.canvas.width - scaled_width) / 2)
|
||||
else:
|
||||
coefficient = self.canvas.width / scaled_width
|
||||
x_offset = int(
|
||||
(self.canvas.width - (scaled_width * coefficient)) / 2
|
||||
)
|
||||
|
||||
self.camera_layout = [
|
||||
[
|
||||
(
|
||||
camera,
|
||||
(
|
||||
0,
|
||||
x_offset,
|
||||
0,
|
||||
int(scaled_width * coefficient),
|
||||
int(self.canvas.height * coefficient),
|
||||
@ -485,7 +537,11 @@ class BirdsEyeFrameManager:
|
||||
|
||||
return True
|
||||
|
||||
def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]:
|
||||
def calculate_layout(
|
||||
self,
|
||||
cameras_to_add: list[str],
|
||||
coefficient: float,
|
||||
) -> tuple[any]:
|
||||
"""Calculate the optimal layout for 2+ cameras."""
|
||||
|
||||
def map_layout(camera_layout: list[list[any]], row_height: int):
|
||||
|
Loading…
Reference in New Issue
Block a user