mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 10:45:21 +03:00
Merge remote-tracking branch 'upstream/dev' into 230615-fix-birdseye-player-size
This commit is contained in:
commit
ea1ed827a2
@ -73,7 +73,11 @@
|
|||||||
"isort.args": ["--settings-path=./pyproject.toml"],
|
"isort.args": ["--settings-path=./pyproject.toml"],
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": true,
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"[json][jsonc]": {
|
"[json][jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
@ -86,7 +90,7 @@
|
|||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
},
|
},
|
||||||
"cSpell.ignoreWords": ["rtmp"],
|
"cSpell.ignoreWords": ["rtmp"],
|
||||||
"cSpell.words": ["preact"]
|
"cSpell.words": ["preact", "astype", "hwaccel", "mqtt"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -206,10 +206,10 @@ detect:
|
|||||||
max_disappeared: 25
|
max_disappeared: 25
|
||||||
# Optional: Configuration for stationary object tracking
|
# Optional: Configuration for stationary object tracking
|
||||||
stationary:
|
stationary:
|
||||||
# Optional: Frequency for confirming stationary objects (default: shown below)
|
# Optional: Frequency for confirming stationary objects (default: same as threshold)
|
||||||
# When set to 0, object detection will not confirm stationary objects until movement is detected.
|
# When set to 1, object detection will run to confirm the object still exists on every frame.
|
||||||
# If set to 10, object detection will run to confirm the object still exists on every 10th frame.
|
# If set to 10, object detection will run to confirm the object still exists on every 10th frame.
|
||||||
interval: 0
|
interval: 50
|
||||||
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
|
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
|
||||||
threshold: 50
|
threshold: 50
|
||||||
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
|
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
|
||||||
@ -225,6 +225,20 @@ detect:
|
|||||||
# Optional: Object specific values
|
# Optional: Object specific values
|
||||||
objects:
|
objects:
|
||||||
person: 1000
|
person: 1000
|
||||||
|
# Optional: Milliseconds to offset detect annotations by (default: shown below).
|
||||||
|
# There can often be latency between a recording and the detect process,
|
||||||
|
# especially when using separate streams for detect and record.
|
||||||
|
# Use this setting to make the timeline bounding boxes more closely align
|
||||||
|
# with the recording. The value can be positive or negative.
|
||||||
|
# TIP: Imagine there is an event clip with a person walking from left to right.
|
||||||
|
# If the event timeline bounding box is consistently to the left of the person
|
||||||
|
# then the value should be decreased. Similarly, if a person is walking from
|
||||||
|
# left to right and the bounding box is consistently ahead of the person
|
||||||
|
# then the value should be increased.
|
||||||
|
# TIP: This offset is dynamic so you can change the value and it will update existing
|
||||||
|
# events, this makes it easy to tune.
|
||||||
|
# WARNING: Fast moving objects will likely not have the bounding box align.
|
||||||
|
annotation_offset: 0
|
||||||
|
|
||||||
# Optional: Object configuration
|
# Optional: Object configuration
|
||||||
# NOTE: Can be overridden at the camera level
|
# NOTE: Can be overridden at the camera level
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Stationary Objects
|
# Stationary Objects
|
||||||
|
|
||||||
An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs near the object at which point object detection will start running again. If the object changes location, it will be considered active.
|
An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs within the object at which point object detection will start running again. If the object changes location, it will be considered active.
|
||||||
|
|
||||||
## Why does it matter if an object is stationary?
|
## Why does it matter if an object is stationary?
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ The default config is:
|
|||||||
```yaml
|
```yaml
|
||||||
detect:
|
detect:
|
||||||
stationary:
|
stationary:
|
||||||
interval: 0
|
interval: 50
|
||||||
threshold: 50
|
threshold: 50
|
||||||
```
|
```
|
||||||
|
|
||||||
`interval` is defined as the frequency for running detection on stationary objects. This means that by default once an object is considered stationary, detection will not be run on it until motion is detected. With `interval > 0`, every nth frames detection will be run to make sure the object is still there.
|
`interval` is defined as the frequency for running detection on stationary objects. This means that by default once an object is considered stationary, detection will not be run on it until motion is detected or until the interval (every 50th frame by default). With `interval >= 1`, every nth frames detection will be run to make sure the object is still there.
|
||||||
|
|
||||||
NOTE: There is no way to disable stationary object tracking with this value.
|
NOTE: There is no way to disable stationary object tracking with this value.
|
||||||
|
|
||||||
|
|||||||
@ -13,8 +13,6 @@ from pydantic.fields import PrivateAttr
|
|||||||
|
|
||||||
from frigate.const import CACHE_DIR, DEFAULT_DB_PATH, REGEX_CAMERA_NAME, YAML_EXT
|
from frigate.const import CACHE_DIR, DEFAULT_DB_PATH, REGEX_CAMERA_NAME, YAML_EXT
|
||||||
from frigate.detectors import DetectorConfig, ModelConfig
|
from frigate.detectors import DetectorConfig, ModelConfig
|
||||||
from frigate.detectors.detector_config import InputTensorEnum # noqa: F401
|
|
||||||
from frigate.detectors.detector_config import PixelFormatEnum # noqa: F401
|
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig
|
from frigate.detectors.detector_config import BaseDetectorConfig
|
||||||
from frigate.ffmpeg_presets import (
|
from frigate.ffmpeg_presets import (
|
||||||
parse_preset_hardware_acceleration_decode,
|
parse_preset_hardware_acceleration_decode,
|
||||||
@ -251,9 +249,8 @@ class StationaryMaxFramesConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
class StationaryConfig(FrigateBaseModel):
|
class StationaryConfig(FrigateBaseModel):
|
||||||
interval: Optional[int] = Field(
|
interval: Optional[int] = Field(
|
||||||
default=0,
|
|
||||||
title="Frame interval for checking stationary objects.",
|
title="Frame interval for checking stationary objects.",
|
||||||
ge=0,
|
gt=0,
|
||||||
)
|
)
|
||||||
threshold: Optional[int] = Field(
|
threshold: Optional[int] = Field(
|
||||||
title="Number of frames without a position change for an object to be considered stationary",
|
title="Number of frames without a position change for an object to be considered stationary",
|
||||||
@ -963,6 +960,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
stationary_threshold = camera_config.detect.fps * 10
|
stationary_threshold = camera_config.detect.fps * 10
|
||||||
if camera_config.detect.stationary.threshold is None:
|
if camera_config.detect.stationary.threshold is None:
|
||||||
camera_config.detect.stationary.threshold = stationary_threshold
|
camera_config.detect.stationary.threshold = stationary_threshold
|
||||||
|
# default to the stationary_threshold if not defined
|
||||||
|
if camera_config.detect.stationary.interval is None:
|
||||||
|
camera_config.detect.stationary.interval = stationary_threshold
|
||||||
|
|
||||||
# FFMPEG input substitution
|
# FFMPEG input substitution
|
||||||
for input in camera_config.ffmpeg.inputs:
|
for input in camera_config.ffmpeg.inputs:
|
||||||
|
|||||||
@ -10,8 +10,8 @@ from abc import ABC, abstractmethod
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
from frigate.config import InputTensorEnum
|
|
||||||
from frigate.detectors import create_detector
|
from frigate.detectors import create_detector
|
||||||
|
from frigate.detectors.detector_config import InputTensorEnum
|
||||||
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels
|
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@ -269,161 +270,118 @@ class BirdsEyeFrameManager:
|
|||||||
def update_frame(self):
|
def update_frame(self):
|
||||||
"""Update to a new frame for birdseye."""
|
"""Update to a new frame for birdseye."""
|
||||||
|
|
||||||
def calculate_two_cam_layout(canvas, cameras_to_add: list[str]) -> tuple[any]:
|
|
||||||
"""Calculate the optimal layout for 2 cameras."""
|
|
||||||
first_camera = cameras_to_add[0]
|
|
||||||
first_camera_dims = self.cameras[first_camera]["dimensions"].copy()
|
|
||||||
second_camera = cameras_to_add[1]
|
|
||||||
second_camera_dims = self.cameras[second_camera]["dimensions"].copy()
|
|
||||||
|
|
||||||
# check for optimal layout
|
|
||||||
if first_camera_dims[0] + second_camera_dims[0] < canvas_width:
|
|
||||||
# place cameras horizontally
|
|
||||||
first_scaled_width = int(
|
|
||||||
canvas_height * first_camera_dims[0] / first_camera_dims[1]
|
|
||||||
)
|
|
||||||
second_scaled_width = int(
|
|
||||||
canvas_height * second_camera_dims[0] / second_camera_dims[1]
|
|
||||||
)
|
|
||||||
first_height = canvas_height
|
|
||||||
second_height = canvas_height
|
|
||||||
|
|
||||||
if first_scaled_width + second_scaled_width > canvas_width:
|
|
||||||
if first_scaled_width > second_scaled_width:
|
|
||||||
first_scaled_width = canvas_width - second_scaled_width
|
|
||||||
first_height = int(
|
|
||||||
first_scaled_width
|
|
||||||
* first_camera_dims[1]
|
|
||||||
/ first_camera_dims[0]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
second_scaled_width = canvas_width - first_scaled_width
|
|
||||||
second_height = int(
|
|
||||||
second_scaled_width
|
|
||||||
* second_camera_dims[1]
|
|
||||||
/ second_camera_dims[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
(
|
|
||||||
first_camera,
|
|
||||||
(0, 0, first_scaled_width, first_height),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
second_camera,
|
|
||||||
(
|
|
||||||
first_scaled_width + 1,
|
|
||||||
0,
|
|
||||||
second_scaled_width,
|
|
||||||
second_height,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# place cameras vertically
|
|
||||||
top_scaled_width = int(
|
|
||||||
(canvas_height / 2) * first_camera_dims[0] / first_camera_dims[1]
|
|
||||||
)
|
|
||||||
bottom_scaled_width = int(
|
|
||||||
(canvas_height / 2) * second_camera_dims[0] / second_camera_dims[1]
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
(
|
|
||||||
first_camera,
|
|
||||||
(0, 0, top_scaled_width, int(canvas_height / 2)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
[
|
|
||||||
(
|
|
||||||
second_camera,
|
|
||||||
(
|
|
||||||
0,
|
|
||||||
int(canvas_height / 2),
|
|
||||||
bottom_scaled_width,
|
|
||||||
int(canvas_height / 2),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
def calculate_layout(
|
def calculate_layout(
|
||||||
canvas, cameras_to_add: list[str], coefficient
|
canvas, cameras_to_add: list[str], coefficient
|
||||||
) -> tuple[any]:
|
) -> tuple[any]:
|
||||||
"""Calculate the optimal layout for 3+ cameras."""
|
"""Calculate the optimal layout for 2+ cameras."""
|
||||||
camera_layout: list[list[any]] = []
|
camera_layout: list[list[any]] = []
|
||||||
camera_layout.append([])
|
camera_layout.append([])
|
||||||
canvas_aspect = canvas[0] / canvas[1]
|
canvas_gcd = math.gcd(canvas[0], canvas[1])
|
||||||
|
canvas_aspect_x = (canvas[0] / canvas_gcd) * coefficient
|
||||||
|
canvas_aspect_y = (canvas[0] / canvas_gcd) * coefficient
|
||||||
starting_x = 0
|
starting_x = 0
|
||||||
x = starting_x
|
x = starting_x
|
||||||
y = 0
|
y = 0
|
||||||
y_i = 0
|
y_i = 0
|
||||||
max_height = 0
|
max_y = 0
|
||||||
for camera in cameras_to_add:
|
for camera in cameras_to_add:
|
||||||
camera_dims = self.cameras[camera]["dimensions"].copy()
|
camera_dims = self.cameras[camera]["dimensions"].copy()
|
||||||
camera_aspect = camera_dims[0] / camera_dims[1]
|
camera_gcd = math.gcd(camera_dims[0], camera_dims[1])
|
||||||
|
camera_aspect_x = camera_dims[0] / camera_gcd
|
||||||
|
camera_aspect_y = camera_dims[1] / camera_gcd
|
||||||
|
|
||||||
|
if round(camera_aspect_x / camera_aspect_y, 1) == 1.8:
|
||||||
|
# account for slightly off 16:9 cameras
|
||||||
|
camera_aspect_x = 16
|
||||||
|
camera_aspect_y = 9
|
||||||
|
elif round(camera_aspect_x / camera_aspect_y, 1) == 1.3:
|
||||||
|
# make 4:3 cameras the same relative size as 16:9
|
||||||
|
camera_aspect_x = 12
|
||||||
|
camera_aspect_y = 9
|
||||||
|
|
||||||
if camera_dims[1] > camera_dims[0]:
|
if camera_dims[1] > camera_dims[0]:
|
||||||
portrait = True
|
portrait = True
|
||||||
elif camera_aspect < canvas_aspect:
|
|
||||||
# if the camera aspect ratio is less than canvas aspect ratio, it needs to be scaled down to fit
|
|
||||||
camera_dims[0] *= camera_aspect / canvas_aspect
|
|
||||||
camera_dims[1] *= camera_aspect / canvas_aspect
|
|
||||||
portrait = False
|
|
||||||
else:
|
else:
|
||||||
portrait = False
|
portrait = False
|
||||||
|
|
||||||
if (x + camera_dims[0] * coefficient) <= canvas[0]:
|
if (x + camera_aspect_x) <= canvas_aspect_x:
|
||||||
# insert if camera can fit on current row
|
# insert if camera can fit on current row
|
||||||
scaled_width = int(camera_dims[0] * coefficient)
|
|
||||||
camera_layout[y_i].append(
|
camera_layout[y_i].append(
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
(
|
(
|
||||||
x,
|
camera_aspect_x,
|
||||||
y,
|
camera_aspect_y,
|
||||||
scaled_width,
|
|
||||||
int(camera_dims[1] * coefficient),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
x += scaled_width
|
|
||||||
|
|
||||||
if portrait:
|
if portrait:
|
||||||
starting_x = scaled_width
|
starting_x = camera_aspect_x
|
||||||
else:
|
else:
|
||||||
max_height = max(
|
max_y = max(
|
||||||
max_height,
|
max_y,
|
||||||
int(camera_dims[1] * coefficient),
|
camera_aspect_y,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
x += camera_aspect_x
|
||||||
else:
|
else:
|
||||||
# move on to the next row and insert
|
# move on to the next row and insert
|
||||||
y += max_height
|
y += max_y
|
||||||
y_i += 1
|
y_i += 1
|
||||||
camera_layout.append([])
|
camera_layout.append([])
|
||||||
x = starting_x
|
x = starting_x
|
||||||
|
|
||||||
if camera_dims[0] * coefficient > canvas_width:
|
if x + camera_aspect_x > canvas_aspect_x:
|
||||||
safe_coefficient = 1
|
return None
|
||||||
else:
|
|
||||||
safe_coefficient = coefficient
|
|
||||||
|
|
||||||
camera_layout[y_i].append(
|
camera_layout[y_i].append(
|
||||||
(
|
(
|
||||||
camera,
|
camera,
|
||||||
(
|
(camera_aspect_x, camera_aspect_y),
|
||||||
x,
|
|
||||||
y,
|
|
||||||
int(camera_dims[0] * safe_coefficient),
|
|
||||||
int(camera_dims[1] * safe_coefficient),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
x += int(camera_dims[0] * safe_coefficient)
|
x += camera_aspect_x
|
||||||
|
|
||||||
return (camera_layout, y + max_height)
|
if y + max_y > canvas_aspect_y:
|
||||||
|
return None
|
||||||
|
|
||||||
|
row_height = int(canvas_height / coefficient)
|
||||||
|
|
||||||
|
final_camera_layout = []
|
||||||
|
starting_x = 0
|
||||||
|
y = 0
|
||||||
|
|
||||||
|
for row in camera_layout:
|
||||||
|
final_row = []
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
scaled_height = row_height
|
||||||
|
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
|
# determine how many cameras are tracking objects within the last 30 seconds
|
||||||
active_cameras = set(
|
active_cameras = set(
|
||||||
@ -493,30 +451,28 @@ class BirdsEyeFrameManager:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
elif len(active_cameras) == 2:
|
|
||||||
self.camera_layout = calculate_two_cam_layout(
|
|
||||||
(canvas_width, canvas_height), active_cameras_to_add
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# calculate optimal layout
|
# calculate optimal layout
|
||||||
coefficient = 1.0
|
coefficient = 2
|
||||||
calculating = True
|
calculating = True
|
||||||
|
|
||||||
# decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
|
# decrease scaling coefficient until height of all cameras can fit into the birdseye canvas
|
||||||
while calculating:
|
while calculating:
|
||||||
layout_candidate, total_height = calculate_layout(
|
layout_candidate = calculate_layout(
|
||||||
(canvas_width, canvas_height),
|
(canvas_width, canvas_height),
|
||||||
active_cameras_to_add,
|
active_cameras_to_add,
|
||||||
coefficient,
|
coefficient,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (canvas_height * 0.75) < total_height <= canvas_height:
|
if not layout_candidate:
|
||||||
calculating = False
|
if coefficient < 10:
|
||||||
elif total_height < canvas_height * 0.75:
|
coefficient += 1
|
||||||
coefficient += 0.1
|
continue
|
||||||
calculating = False
|
else:
|
||||||
else:
|
logger.error("Error finding appropriate birdseye layout")
|
||||||
coefficient -= 0.1
|
return
|
||||||
|
|
||||||
|
calculating = False
|
||||||
|
|
||||||
self.camera_layout = layout_candidate
|
self.camera_layout = layout_candidate
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Maintain recording segments in cache."""
|
"""Maintain recording segments in cache."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
@ -20,7 +21,7 @@ from frigate.config import FrigateConfig, RetainModeEnum
|
|||||||
from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
|
from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
|
||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Recordings
|
||||||
from frigate.types import RecordMetricsTypes
|
from frigate.types import RecordMetricsTypes
|
||||||
from frigate.util import area
|
from frigate.util import area, get_video_properties
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
self.recordings_info: dict[str, Any] = defaultdict(list)
|
self.recordings_info: dict[str, Any] = defaultdict(list)
|
||||||
self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
|
self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
|
||||||
|
|
||||||
def move_files(self) -> None:
|
async def move_files(self) -> None:
|
||||||
cache_files = sorted(
|
cache_files = sorted(
|
||||||
[
|
[
|
||||||
d
|
d
|
||||||
@ -121,115 +122,100 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
.order_by(Event.start_time)
|
.order_by(Event.start_time)
|
||||||
)
|
)
|
||||||
for r in recordings:
|
|
||||||
cache_path = r["cache_path"]
|
|
||||||
start_time = r["start_time"]
|
|
||||||
|
|
||||||
# Just delete files if recordings are turned off
|
await asyncio.gather(
|
||||||
if (
|
*(self.validate_and_move_segment(camera, events, r) for r in recordings)
|
||||||
camera not in self.config.cameras
|
)
|
||||||
or not self.process_info[camera]["record_enabled"].value
|
|
||||||
):
|
async def validate_and_move_segment(
|
||||||
|
self, camera: str, events: Event, recording: dict[str, any]
|
||||||
|
) -> None:
|
||||||
|
cache_path = recording["cache_path"]
|
||||||
|
start_time = recording["start_time"]
|
||||||
|
|
||||||
|
# Just delete files if recordings are turned off
|
||||||
|
if (
|
||||||
|
camera not in self.config.cameras
|
||||||
|
or not self.process_info[camera]["record_enabled"].value
|
||||||
|
):
|
||||||
|
Path(cache_path).unlink(missing_ok=True)
|
||||||
|
self.end_time_cache.pop(cache_path, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cache_path in self.end_time_cache:
|
||||||
|
end_time, duration = self.end_time_cache[cache_path]
|
||||||
|
else:
|
||||||
|
segment_info = get_video_properties(cache_path, get_duration=True)
|
||||||
|
|
||||||
|
if segment_info["duration"]:
|
||||||
|
duration = float(segment_info["duration"])
|
||||||
|
else:
|
||||||
|
duration = -1
|
||||||
|
|
||||||
|
# ensure duration is within expected length
|
||||||
|
if 0 < duration < MAX_SEGMENT_DURATION:
|
||||||
|
end_time = start_time + datetime.timedelta(seconds=duration)
|
||||||
|
self.end_time_cache[cache_path] = (end_time, duration)
|
||||||
|
else:
|
||||||
|
if duration == -1:
|
||||||
|
logger.warning(f"Failed to probe corrupt segment {cache_path}")
|
||||||
|
|
||||||
|
logger.warning(f"Discarding a corrupt recording segment: {cache_path}")
|
||||||
|
Path(cache_path).unlink(missing_ok=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# if cached file's start_time is earlier than the retain days for the camera
|
||||||
|
if start_time <= (
|
||||||
|
(
|
||||||
|
datetime.datetime.now()
|
||||||
|
- datetime.timedelta(
|
||||||
|
days=self.config.cameras[camera].record.retain.days
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the cached segment overlaps with the events:
|
||||||
|
overlaps = False
|
||||||
|
for event in events:
|
||||||
|
# if the event starts in the future, stop checking events
|
||||||
|
# and remove this segment
|
||||||
|
if event.start_time > end_time.timestamp():
|
||||||
|
overlaps = False
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
Path(cache_path).unlink(missing_ok=True)
|
||||||
self.end_time_cache.pop(cache_path, None)
|
self.end_time_cache.pop(cache_path, None)
|
||||||
continue
|
break
|
||||||
|
|
||||||
if cache_path in self.end_time_cache:
|
# if the event is in progress or ends after the recording starts, keep it
|
||||||
end_time, duration = self.end_time_cache[cache_path]
|
# and stop looking at events
|
||||||
else:
|
if event.end_time is None or event.end_time >= start_time.timestamp():
|
||||||
ffprobe_cmd = [
|
overlaps = True
|
||||||
"ffprobe",
|
break
|
||||||
"-v",
|
|
||||||
"error",
|
|
||||||
"-show_entries",
|
|
||||||
"format=duration",
|
|
||||||
"-of",
|
|
||||||
"default=noprint_wrappers=1:nokey=1",
|
|
||||||
f"{cache_path}",
|
|
||||||
]
|
|
||||||
p = sp.run(ffprobe_cmd, capture_output=True)
|
|
||||||
if p.returncode == 0 and p.stdout.decode():
|
|
||||||
duration = float(p.stdout.decode().strip())
|
|
||||||
else:
|
|
||||||
duration = -1
|
|
||||||
|
|
||||||
# ensure duration is within expected length
|
if overlaps:
|
||||||
if 0 < duration < MAX_SEGMENT_DURATION:
|
record_mode = self.config.cameras[camera].record.events.retain.mode
|
||||||
end_time = start_time + datetime.timedelta(seconds=duration)
|
# move from cache to recordings immediately
|
||||||
self.end_time_cache[cache_path] = (end_time, duration)
|
self.store_segment(
|
||||||
else:
|
camera,
|
||||||
if duration == -1:
|
start_time,
|
||||||
logger.warning(
|
end_time,
|
||||||
f"Failed to probe corrupt segment {cache_path} : {p.returncode} - {str(p.stderr)}"
|
duration,
|
||||||
)
|
cache_path,
|
||||||
|
record_mode,
|
||||||
logger.warning(
|
)
|
||||||
f"Discarding a corrupt recording segment: {cache_path}"
|
# if it doesn't overlap with an event, go ahead and drop the segment
|
||||||
)
|
# if it ends more than the configured pre_capture for the camera
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
else:
|
||||||
continue
|
pre_capture = self.config.cameras[camera].record.events.pre_capture
|
||||||
|
most_recently_processed_frame_time = self.recordings_info[camera][-1][0]
|
||||||
# if cached file's start_time is earlier than the retain days for the camera
|
retain_cutoff = most_recently_processed_frame_time - pre_capture
|
||||||
if start_time <= (
|
if end_time.timestamp() < retain_cutoff:
|
||||||
(
|
Path(cache_path).unlink(missing_ok=True)
|
||||||
datetime.datetime.now()
|
self.end_time_cache.pop(cache_path, None)
|
||||||
- datetime.timedelta(
|
# else retain days includes this segment
|
||||||
days=self.config.cameras[camera].record.retain.days
|
else:
|
||||||
)
|
record_mode = self.config.cameras[camera].record.retain.mode
|
||||||
)
|
self.store_segment(
|
||||||
):
|
camera, start_time, end_time, duration, cache_path, record_mode
|
||||||
# if the cached segment overlaps with the events:
|
)
|
||||||
overlaps = False
|
|
||||||
for event in events:
|
|
||||||
# if the event starts in the future, stop checking events
|
|
||||||
# and remove this segment
|
|
||||||
if event.start_time > end_time.timestamp():
|
|
||||||
overlaps = False
|
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
break
|
|
||||||
|
|
||||||
# if the event is in progress or ends after the recording starts, keep it
|
|
||||||
# and stop looking at events
|
|
||||||
if (
|
|
||||||
event.end_time is None
|
|
||||||
or event.end_time >= start_time.timestamp()
|
|
||||||
):
|
|
||||||
overlaps = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if overlaps:
|
|
||||||
record_mode = self.config.cameras[
|
|
||||||
camera
|
|
||||||
].record.events.retain.mode
|
|
||||||
# move from cache to recordings immediately
|
|
||||||
self.store_segment(
|
|
||||||
camera,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
duration,
|
|
||||||
cache_path,
|
|
||||||
record_mode,
|
|
||||||
)
|
|
||||||
# if it doesn't overlap with an event, go ahead and drop the segment
|
|
||||||
# if it ends more than the configured pre_capture for the camera
|
|
||||||
else:
|
|
||||||
pre_capture = self.config.cameras[
|
|
||||||
camera
|
|
||||||
].record.events.pre_capture
|
|
||||||
most_recently_processed_frame_time = self.recordings_info[
|
|
||||||
camera
|
|
||||||
][-1][0]
|
|
||||||
retain_cutoff = most_recently_processed_frame_time - pre_capture
|
|
||||||
if end_time.timestamp() < retain_cutoff:
|
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
# else retain days includes this segment
|
|
||||||
else:
|
|
||||||
record_mode = self.config.cameras[camera].record.retain.mode
|
|
||||||
self.store_segment(
|
|
||||||
camera, start_time, end_time, duration, cache_path, record_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
def segment_stats(
|
def segment_stats(
|
||||||
self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime
|
self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime
|
||||||
@ -386,7 +372,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.move_files()
|
asyncio.run(self.move_files())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Error occurred when attempting to maintain recording cache"
|
"Error occurred when attempting to maintain recording cache"
|
||||||
|
|||||||
@ -6,8 +6,9 @@ from pydantic import parse_obj_as
|
|||||||
|
|
||||||
import frigate.detectors as detectors
|
import frigate.detectors as detectors
|
||||||
import frigate.object_detection
|
import frigate.object_detection
|
||||||
from frigate.config import DetectorConfig, InputTensorEnum, ModelConfig
|
from frigate.config import DetectorConfig, ModelConfig
|
||||||
from frigate.detectors import DetectorTypeEnum
|
from frigate.detectors import DetectorTypeEnum
|
||||||
|
from frigate.detectors.detector_config import InputTensorEnum
|
||||||
|
|
||||||
|
|
||||||
class TestLocalObjectDetector(unittest.TestCase):
|
class TestLocalObjectDetector(unittest.TestCase):
|
||||||
|
|||||||
@ -91,9 +91,13 @@ class NorfairTracker(ObjectTracker):
|
|||||||
"ymax": self.detect_config.height,
|
"ymax": self.detect_config.height,
|
||||||
}
|
}
|
||||||
|
|
||||||
def deregister(self, id):
|
def deregister(self, id, track_id):
|
||||||
del self.tracked_objects[id]
|
del self.tracked_objects[id]
|
||||||
del self.disappeared[id]
|
del self.disappeared[id]
|
||||||
|
self.tracker.tracked_objects = [
|
||||||
|
o for o in self.tracker.tracked_objects if o.global_id != track_id
|
||||||
|
]
|
||||||
|
del self.track_id_map[track_id]
|
||||||
|
|
||||||
# tracks the current position of the object based on the last N bounding boxes
|
# tracks the current position of the object based on the last N bounding boxes
|
||||||
# returns False if the object has moved outside its previous position
|
# returns False if the object has moved outside its previous position
|
||||||
@ -167,7 +171,7 @@ class NorfairTracker(ObjectTracker):
|
|||||||
if self.update_position(id, obj["box"]):
|
if self.update_position(id, obj["box"]):
|
||||||
self.tracked_objects[id]["motionless_count"] += 1
|
self.tracked_objects[id]["motionless_count"] += 1
|
||||||
if self.is_expired(id):
|
if self.is_expired(id):
|
||||||
self.deregister(id)
|
self.deregister(id, track_id)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# register the first position change and then only increment if
|
# register the first position change and then only increment if
|
||||||
@ -261,8 +265,7 @@ class NorfairTracker(ObjectTracker):
|
|||||||
# clear expired tracks
|
# clear expired tracks
|
||||||
expired_ids = [k for k in self.track_id_map.keys() if k not in active_ids]
|
expired_ids = [k for k in self.track_id_map.keys() if k not in active_ids]
|
||||||
for e_id in expired_ids:
|
for e_id in expired_ids:
|
||||||
self.deregister(self.track_id_map[e_id])
|
self.deregister(self.track_id_map[e_id], e_id)
|
||||||
del self.track_id_map[e_id]
|
|
||||||
|
|
||||||
def debug_draw(self, frame, frame_time):
|
def debug_draw(self, frame, frame_time):
|
||||||
active_detections = [
|
active_detections = [
|
||||||
|
|||||||
@ -1144,3 +1144,54 @@ def to_relative_box(
|
|||||||
(box[2] - box[0]) / width, # w
|
(box[2] - box[0]) / width, # w
|
||||||
(box[3] - box[1]) / height, # h
|
(box[3] - box[1]) / height, # h
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_properties(url, get_duration=False):
|
||||||
|
width = height = 0
|
||||||
|
# Open the video stream
|
||||||
|
video = cv2.VideoCapture(url)
|
||||||
|
|
||||||
|
# Check if the video stream was opened successfully
|
||||||
|
if not video.isOpened():
|
||||||
|
logger.debug(f"Error opening video stream {url}.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the width of frames in the video stream
|
||||||
|
width = video.get(cv2.CAP_PROP_FRAME_WIDTH)
|
||||||
|
|
||||||
|
# Get the height of frames in the video stream
|
||||||
|
height = video.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
||||||
|
|
||||||
|
# Release the video stream
|
||||||
|
video.release()
|
||||||
|
|
||||||
|
result = {"width": round(width), "height": round(height)}
|
||||||
|
|
||||||
|
if get_duration:
|
||||||
|
# Get the frames per second (fps) of the video stream
|
||||||
|
fps = video.get(cv2.CAP_PROP_FPS)
|
||||||
|
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
|
||||||
|
if fps and total_frames:
|
||||||
|
duration = total_frames / fps
|
||||||
|
else:
|
||||||
|
# if cv2 failed need to use ffprobe
|
||||||
|
ffprobe_cmd = [
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-show_entries",
|
||||||
|
"format=duration",
|
||||||
|
"-of",
|
||||||
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
|
f"{url}",
|
||||||
|
]
|
||||||
|
p = sp.run(ffprobe_cmd, capture_output=True)
|
||||||
|
if p.returncode == 0 and p.stdout.decode():
|
||||||
|
duration = float(p.stdout.decode().strip())
|
||||||
|
else:
|
||||||
|
duration = -1
|
||||||
|
|
||||||
|
result["duration"] = duration
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@ -14,8 +14,9 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
from frigate.config import CameraConfig, DetectConfig, PixelFormatEnum
|
from frigate.config import CameraConfig, DetectConfig
|
||||||
from frigate.const import CACHE_DIR
|
from frigate.const import CACHE_DIR
|
||||||
|
from frigate.detectors.detector_config import PixelFormatEnum
|
||||||
from frigate.log import LogPipe
|
from frigate.log import LogPipe
|
||||||
from frigate.motion import MotionDetector
|
from frigate.motion import MotionDetector
|
||||||
from frigate.motion.improved_motion import ImprovedMotionDetector
|
from frigate.motion.improved_motion import ImprovedMotionDetector
|
||||||
@ -769,8 +770,8 @@ def process_frames(
|
|||||||
stationary_object_ids = [
|
stationary_object_ids = [
|
||||||
obj["id"]
|
obj["id"]
|
||||||
for obj in object_tracker.tracked_objects.values()
|
for obj in object_tracker.tracked_objects.values()
|
||||||
# if there hasn't been motion for 10 frames
|
# if it has exceeded the stationary threshold
|
||||||
if obj["motionless_count"] >= 10
|
if obj["motionless_count"] >= detect_config.stationary.threshold
|
||||||
# and it isn't due for a periodic check
|
# and it isn't due for a periodic check
|
||||||
and (
|
and (
|
||||||
detect_config.stationary.interval == 0
|
detect_config.stationary.interval == 0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user