Cleanup camera

This commit is contained in:
Nicolas Mowen 2026-03-25 17:19:45 -06:00
parent e7b25584c9
commit d2742c1cb4
7 changed files with 79 additions and 81 deletions

View File

@ -1,26 +1,27 @@
import multiprocessing as mp import multiprocessing as mp
from multiprocessing.managers import SyncManager import queue
from multiprocessing.managers import SyncManager, ValueProxy
from multiprocessing.sharedctypes import Synchronized from multiprocessing.sharedctypes import Synchronized
from multiprocessing.synchronize import Event from multiprocessing.synchronize import Event
class CameraMetrics: class CameraMetrics:
camera_fps: Synchronized camera_fps: ValueProxy[float]
detection_fps: Synchronized detection_fps: ValueProxy[float]
detection_frame: Synchronized detection_frame: ValueProxy[float]
process_fps: Synchronized process_fps: ValueProxy[float]
skipped_fps: Synchronized skipped_fps: ValueProxy[float]
read_start: Synchronized read_start: ValueProxy[float]
audio_rms: Synchronized audio_rms: ValueProxy[float]
audio_dBFS: Synchronized audio_dBFS: ValueProxy[float]
frame_queue: mp.Queue frame_queue: queue.Queue
process_pid: Synchronized process_pid: ValueProxy[int]
capture_process_pid: Synchronized capture_process_pid: ValueProxy[int]
ffmpeg_pid: Synchronized ffmpeg_pid: ValueProxy[int]
reconnects_last_hour: Synchronized reconnects_last_hour: ValueProxy[int]
stalls_last_hour: Synchronized stalls_last_hour: ValueProxy[int]
def __init__(self, manager: SyncManager): def __init__(self, manager: SyncManager):
self.camera_fps = manager.Value("d", 0) self.camera_fps = manager.Value("d", 0)
@ -56,14 +57,14 @@ class PTZMetrics:
reset: Event reset: Event
def __init__(self, *, autotracker_enabled: bool): def __init__(self, *, autotracker_enabled: bool):
self.autotracker_enabled = mp.Value("i", autotracker_enabled) self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment]
self.start_time = mp.Value("d", 0) self.start_time = mp.Value("d", 0) # type: ignore[assignment]
self.stop_time = mp.Value("d", 0) self.stop_time = mp.Value("d", 0) # type: ignore[assignment]
self.frame_time = mp.Value("d", 0) self.frame_time = mp.Value("d", 0) # type: ignore[assignment]
self.zoom_level = mp.Value("d", 0) self.zoom_level = mp.Value("d", 0) # type: ignore[assignment]
self.max_zoom = mp.Value("d", 0) self.max_zoom = mp.Value("d", 0) # type: ignore[assignment]
self.min_zoom = mp.Value("d", 0) self.min_zoom = mp.Value("d", 0) # type: ignore[assignment]
self.tracking_active = mp.Event() self.tracking_active = mp.Event()
self.motor_stopped = mp.Event() self.motor_stopped = mp.Event()

View File

@ -37,6 +37,9 @@ class CameraActivityManager:
self.__init_camera(camera_config) self.__init_camera(camera_config)
def __init_camera(self, camera_config: CameraConfig) -> None: def __init_camera(self, camera_config: CameraConfig) -> None:
if camera_config.name is None:
return
self.last_camera_activity[camera_config.name] = {} self.last_camera_activity[camera_config.name] = {}
self.camera_all_object_counts[camera_config.name] = Counter() self.camera_all_object_counts[camera_config.name] = Counter()
self.camera_active_object_counts[camera_config.name] = Counter() self.camera_active_object_counts[camera_config.name] = Counter()
@ -114,7 +117,7 @@ class CameraActivityManager:
self.last_camera_activity = new_activity self.last_camera_activity = new_activity
def compare_camera_activity( def compare_camera_activity(
self, camera: str, new_activity: dict[str, Any] self, camera: str, new_activity: list[dict[str, Any]]
) -> None: ) -> None:
all_objects = Counter( all_objects = Counter(
obj["label"].replace("-verified", "") for obj in new_activity obj["label"].replace("-verified", "") for obj in new_activity
@ -175,6 +178,9 @@ class AudioActivityManager:
self.__init_camera(camera_config) self.__init_camera(camera_config)
def __init_camera(self, camera_config: CameraConfig) -> None: def __init_camera(self, camera_config: CameraConfig) -> None:
if camera_config.name is None:
return
self.current_audio_detections[camera_config.name] = {} self.current_audio_detections[camera_config.name] = {}
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
@ -202,7 +208,7 @@ class AudioActivityManager:
def compare_audio_activity( def compare_audio_activity(
self, camera: str, new_detections: list[tuple[str, float]], now: float self, camera: str, new_detections: list[tuple[str, float]], now: float
) -> None: ) -> bool:
camera_config = self.config.cameras.get(camera) camera_config = self.config.cameras.get(camera)
if camera_config is None: if camera_config is None:
return False return False

View File

@ -102,7 +102,7 @@ class CameraMaintainer(threading.Thread):
f"recommend increasing it to at least {shm_stats['min_shm']}MB." f"recommend increasing it to at least {shm_stats['min_shm']}MB."
) )
return shm_stats["shm_frame_count"] return int(shm_stats["shm_frame_count"])
def __start_camera_processor( def __start_camera_processor(
self, name: str, config: CameraConfig, runtime: bool = False self, name: str, config: CameraConfig, runtime: bool = False
@ -152,10 +152,10 @@ class CameraMaintainer(threading.Thread):
camera_stop_event, camera_stop_event,
self.config.logger, self.config.logger,
) )
self.camera_processes[config.name] = camera_process self.camera_processes[name] = camera_process
camera_process.start() camera_process.start()
self.camera_metrics[config.name].process_pid.value = camera_process.pid self.camera_metrics[name].process_pid.value = camera_process.pid
logger.info(f"Camera processor started for {config.name}: {camera_process.pid}") logger.info(f"Camera processor started for {name}: {camera_process.pid}")
def __start_camera_capture( def __start_camera_capture(
self, name: str, config: CameraConfig, runtime: bool = False self, name: str, config: CameraConfig, runtime: bool = False
@ -219,7 +219,7 @@ class CameraMaintainer(threading.Thread):
logger.info(f"Closing frame queue for {camera}") logger.info(f"Closing frame queue for {camera}")
empty_and_close_queue(self.camera_metrics[camera].frame_queue) empty_and_close_queue(self.camera_metrics[camera].frame_queue)
def run(self): def run(self) -> None:
self.__init_historical_regions() self.__init_historical_regions()
# start camera processes # start camera processes

View File

@ -31,26 +31,26 @@ logger = logging.getLogger(__name__)
class CameraState: class CameraState:
def __init__( def __init__(
self, self,
name, name: str,
config: FrigateConfig, config: FrigateConfig,
frame_manager: SharedMemoryFrameManager, frame_manager: SharedMemoryFrameManager,
ptz_autotracker_thread: PtzAutoTrackerThread, ptz_autotracker_thread: PtzAutoTrackerThread,
): ) -> None:
self.name = name self.name = name
self.config = config self.config = config
self.camera_config = config.cameras[name] self.camera_config = config.cameras[name]
self.frame_manager = frame_manager self.frame_manager = frame_manager
self.best_objects: dict[str, TrackedObject] = {} self.best_objects: dict[str, TrackedObject] = {}
self.tracked_objects: dict[str, TrackedObject] = {} self.tracked_objects: dict[str, TrackedObject] = {}
self.frame_cache = {} self.frame_cache: dict[float, dict[str, Any]] = {}
self.zone_objects = defaultdict(list) self.zone_objects: defaultdict[str, list[Any]] = defaultdict(list)
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
self.current_frame_lock = threading.Lock() self.current_frame_lock = threading.Lock()
self.current_frame_time = 0.0 self.current_frame_time = 0.0
self.motion_boxes = [] self.motion_boxes: list[tuple[int, int, int, int]] = []
self.regions = [] self.regions: list[tuple[int, int, int, int]] = []
self.previous_frame_id = None self.previous_frame_id: str | None = None
self.callbacks = defaultdict(list) self.callbacks: defaultdict[str, list[Callable]] = defaultdict(list)
self.ptz_autotracker_thread = ptz_autotracker_thread self.ptz_autotracker_thread = ptz_autotracker_thread
self.prev_enabled = self.camera_config.enabled self.prev_enabled = self.camera_config.enabled
@ -62,10 +62,10 @@ class CameraState:
motion_boxes = self.motion_boxes.copy() motion_boxes = self.motion_boxes.copy()
regions = self.regions.copy() regions = self.regions.copy()
frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420) frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420) # type: ignore[assignment]
# draw on the frame # draw on the frame
if draw_options.get("mask"): if draw_options.get("mask"):
mask_overlay = np.where(self.camera_config.motion.rasterized_mask == [0]) mask_overlay = np.where(self.camera_config.motion.rasterized_mask == [0]) # type: ignore[attr-defined]
frame_copy[mask_overlay] = [0, 0, 0] frame_copy[mask_overlay] = [0, 0, 0]
if draw_options.get("bounding_boxes"): if draw_options.get("bounding_boxes"):
@ -97,7 +97,7 @@ class CameraState:
and obj["id"] and obj["id"]
== self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ == self.ptz_autotracker_thread.ptz_autotracker.tracked_object[
self.name self.name
].obj_data["id"] ].obj_data["id"] # type: ignore[attr-defined]
and obj["frame_time"] == frame_time and obj["frame_time"] == frame_time
): ):
thickness = 5 thickness = 5
@ -109,10 +109,12 @@ class CameraState:
if ( if (
self.camera_config.onvif.autotracking.zooming self.camera_config.onvif.autotracking.zooming
!= ZoomingModeEnum.disabled != ZoomingModeEnum.disabled
and self.camera_config.detect.width is not None
and self.camera_config.detect.height is not None
): ):
max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[ max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[
self.name self.name
]["max_target_box"] ]["max_target_box"] # type: ignore[index]
side_length = max_target_box * ( side_length = max_target_box * (
max( max(
self.camera_config.detect.width, self.camera_config.detect.width,
@ -221,14 +223,14 @@ class CameraState:
) )
if draw_options.get("timestamp"): if draw_options.get("timestamp"):
color = self.camera_config.timestamp_style.color ts_color = self.camera_config.timestamp_style.color
draw_timestamp( draw_timestamp(
frame_copy, frame_copy,
frame_time, frame_time,
self.camera_config.timestamp_style.format, self.camera_config.timestamp_style.format,
font_effect=self.camera_config.timestamp_style.effect, font_effect=self.camera_config.timestamp_style.effect,
font_thickness=self.camera_config.timestamp_style.thickness, font_thickness=self.camera_config.timestamp_style.thickness,
font_color=(color.blue, color.green, color.red), font_color=(ts_color.blue, ts_color.green, ts_color.red),
position=self.camera_config.timestamp_style.position, position=self.camera_config.timestamp_style.position,
) )
@ -273,10 +275,10 @@ class CameraState:
return frame_copy return frame_copy
def finished(self, obj_id): def finished(self, obj_id: str) -> None:
del self.tracked_objects[obj_id] del self.tracked_objects[obj_id]
def on(self, event_type: str, callback: Callable): def on(self, event_type: str, callback: Callable[..., Any]) -> None:
self.callbacks[event_type].append(callback) self.callbacks[event_type].append(callback)
def update( def update(
@ -286,7 +288,7 @@ class CameraState:
current_detections: dict[str, dict[str, Any]], current_detections: dict[str, dict[str, Any]],
motion_boxes: list[tuple[int, int, int, int]], motion_boxes: list[tuple[int, int, int, int]],
regions: list[tuple[int, int, int, int]], regions: list[tuple[int, int, int, int]],
): ) -> None:
current_frame = self.frame_manager.get( current_frame = self.frame_manager.get(
frame_name, self.camera_config.frame_shape_yuv frame_name, self.camera_config.frame_shape_yuv
) )
@ -313,7 +315,7 @@ class CameraState:
f"{self.name}: New object, adding {frame_time} to frame cache for {id}" f"{self.name}: New object, adding {frame_time} to frame cache for {id}"
) )
self.frame_cache[frame_time] = { self.frame_cache[frame_time] = {
"frame": np.copy(current_frame), "frame": np.copy(current_frame), # type: ignore[arg-type]
"object_id": id, "object_id": id,
} }
@ -356,7 +358,8 @@ class CameraState:
if thumb_update and current_frame is not None: if thumb_update and current_frame is not None:
# ensure this frame is stored in the cache # ensure this frame is stored in the cache
if ( if (
updated_obj.thumbnail_data["frame_time"] == frame_time updated_obj.thumbnail_data is not None
and updated_obj.thumbnail_data["frame_time"] == frame_time
and frame_time not in self.frame_cache and frame_time not in self.frame_cache
): ):
logger.debug( logger.debug(
@ -397,7 +400,7 @@ class CameraState:
# TODO: can i switch to looking this up and only changing when an event ends? # TODO: can i switch to looking this up and only changing when an event ends?
# maintain best objects # maintain best objects
camera_activity: dict[str, list[Any]] = { camera_activity: dict[str, Any] = {
"motion": len(motion_boxes) > 0, "motion": len(motion_boxes) > 0,
"objects": [], "objects": [],
} }
@ -411,10 +414,7 @@ class CameraState:
sub_label = None sub_label = None
if obj.obj_data.get("sub_label"): if obj.obj_data.get("sub_label"):
if ( if obj.obj_data["sub_label"][0] in self.config.model.all_attributes:
obj.obj_data.get("sub_label")[0]
in self.config.model.all_attributes
):
label = obj.obj_data["sub_label"][0] label = obj.obj_data["sub_label"][0]
else: else:
label = f"{object_type}-verified" label = f"{object_type}-verified"
@ -449,14 +449,19 @@ class CameraState:
# if the object is a higher score than the current best score # if the object is a higher score than the current best score
# or the current object is older than desired, use the new object # or the current object is older than desired, use the new object
if ( if (
is_better_thumbnail( current_best.thumbnail_data is not None
and obj.thumbnail_data is not None
and is_better_thumbnail(
object_type, object_type,
current_best.thumbnail_data, current_best.thumbnail_data,
obj.thumbnail_data, obj.thumbnail_data,
self.camera_config.frame_shape, self.camera_config.frame_shape,
) )
or (now - current_best.thumbnail_data["frame_time"]) or (
> self.camera_config.best_image_timeout current_best.thumbnail_data is not None
and (now - current_best.thumbnail_data["frame_time"])
> self.camera_config.best_image_timeout
)
): ):
self.send_mqtt_snapshot(obj, object_type) self.send_mqtt_snapshot(obj, object_type)
else: else:
@ -472,7 +477,9 @@ class CameraState:
if obj.thumbnail_data is not None if obj.thumbnail_data is not None
} }
current_best_frames = { current_best_frames = {
obj.thumbnail_data["frame_time"] for obj in self.best_objects.values() obj.thumbnail_data["frame_time"]
for obj in self.best_objects.values()
if obj.thumbnail_data is not None
} }
thumb_frames_to_delete = [ thumb_frames_to_delete = [
t t
@ -540,7 +547,7 @@ class CameraState:
with open( with open(
os.path.join( os.path.join(
CLIPS_DIR, CLIPS_DIR,
f"{self.camera_config.name}-{event_id}-clean.webp", f"{self.name}-{event_id}-clean.webp",
), ),
"wb", "wb",
) as p: ) as p:
@ -549,7 +556,7 @@ class CameraState:
# create thumbnail with max height of 175 and save # create thumbnail with max height of 175 and save
width = int(175 * img_frame.shape[1] / img_frame.shape[0]) width = int(175 * img_frame.shape[1] / img_frame.shape[0])
thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA) thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA)
thumb_path = os.path.join(THUMB_DIR, self.camera_config.name) thumb_path = os.path.join(THUMB_DIR, self.name)
os.makedirs(thumb_path, exist_ok=True) os.makedirs(thumb_path, exist_ok=True)
cv2.imwrite(os.path.join(thumb_path, f"{event_id}.webp"), thumb) cv2.imwrite(os.path.join(thumb_path, f"{event_id}.webp"), thumb)

View File

@ -530,7 +530,9 @@ PRESETS_RECORD_OUTPUT = {
} }
def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> Optional[list[str]]: def parse_preset_output_record(
arg: Any, force_record_hvc1: bool
) -> Optional[list[str]]:
"""Return the correct preset if in preset format otherwise return None.""" """Return the correct preset if in preset format otherwise return None."""
if not isinstance(arg, str): if not isinstance(arg, str):
return None return None

View File

@ -24,21 +24,9 @@ no_implicit_reexport = true
[mypy-frigate.*] [mypy-frigate.*]
ignore_errors = false ignore_errors = false
[mypy-frigate.__main__]
disallow_untyped_calls = false
[mypy-frigate.app]
disallow_untyped_calls = false
[mypy-frigate.watchdog]
disallow_untyped_calls = false
[mypy-frigate.api.*] [mypy-frigate.api.*]
ignore_errors = true ignore_errors = true
[mypy-frigate.camera.*]
ignore_errors = true
[mypy-frigate.config.*] [mypy-frigate.config.*]
ignore_errors = true ignore_errors = true
@ -66,13 +54,7 @@ ignore_errors = true
[mypy-frigate.ptz.*] [mypy-frigate.ptz.*]
ignore_errors = true ignore_errors = true
[mypy-frigate.stats.emitter] [mypy-frigate.stats.*]
ignore_errors = true
[mypy-frigate.stats.prometheus]
ignore_errors = true
[mypy-frigate.stats.util]
ignore_errors = true ignore_errors = true
[mypy-frigate.test.*] [mypy-frigate.test.*]

View File

@ -67,8 +67,8 @@ class TrackedObject:
self.has_snapshot = False self.has_snapshot = False
self.top_score = self.computed_score = 0.0 self.top_score = self.computed_score = 0.0
self.thumbnail_data: dict[str, Any] | None = None self.thumbnail_data: dict[str, Any] | None = None
self.last_updated = 0 self.last_updated: float = 0
self.last_published = 0 self.last_published: float = 0
self.frame = None self.frame = None
self.active = True self.active = True
self.pending_loitering = False self.pending_loitering = False