tweaks for fast objects and large movements

This commit is contained in:
Josh Hawkins 2023-09-24 11:10:57 -05:00
parent 514feb8f3e
commit 744da0ef6d
5 changed files with 80 additions and 53 deletions

View File

@ -179,6 +179,9 @@ class FrigateApp:
"ptz_stop_time": mp.Value("d", 0.0), # type: ignore[typeddict-item] "ptz_stop_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799 # issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards # from mypy 0.981 onwards
"ptz_frame_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"ptz_zoom_level": mp.Value("d", 0.0), # type: ignore[typeddict-item] "ptz_zoom_level": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799 # issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards # from mypy 0.981 onwards

View File

@ -6,7 +6,6 @@ import math
import os import os
import queue import queue
import threading import threading
import time
from functools import partial from functools import partial
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
@ -33,12 +32,8 @@ def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time):
# for non ptz/autotracking cameras, this will always return False # for non ptz/autotracking cameras, this will always return False
# ptz_start_time is initialized to 0 on startup and only changes # ptz_start_time is initialized to 0 on startup and only changes
# when autotracking movements are made # when autotracking movements are made
return (ptz_start_time != 0.0 and frame_time > ptz_start_time) and (
# the offset "primes" the motion estimator with a few frames before movement ptz_stop_time == 0.0 or (ptz_start_time <= frame_time <= ptz_stop_time)
offset = 0.5
return (ptz_start_time != 0.0 and frame_time >= ptz_start_time - offset) and (
ptz_stop_time == 0.0 or (ptz_start_time - offset <= frame_time <= ptz_stop_time)
) )
@ -190,6 +185,7 @@ class PtzAutoTracker:
self.calibrating[camera_name] = False self.calibrating[camera_name] = False
self.move_metrics[camera_name] = [] self.move_metrics[camera_name] = []
self.intercept[camera_name] = None
self.move_coefficients[camera_name] = [] self.move_coefficients[camera_name] = []
self.move_queues[camera_name] = queue.Queue() self.move_queues[camera_name] = queue.Queue()
@ -396,18 +392,23 @@ class PtzAutoTracker:
) )
# save metrics for better estimate calculations # save metrics for better estimate calculations
self.move_metrics[camera].append( if (
{ self.intercept[camera] is not None
"pan": pan, and len(self.move_metrics[camera]) < 500
"tilt": tilt, ):
"start_timestamp": self.ptz_metrics[camera][ logger.debug("Adding new values to move metrics")
"ptz_start_time" self.move_metrics[camera].append(
].value, {
"end_timestamp": self.ptz_metrics[camera][ "pan": pan,
"ptz_stop_time" "tilt": tilt,
].value, "start_timestamp": self.ptz_metrics[camera][
} "ptz_start_time"
) ].value,
"end_timestamp": self.ptz_metrics[camera][
"ptz_stop_time"
].value,
}
)
# calculate new coefficients if we have enough data # calculate new coefficients if we have enough data
self._calculate_move_coefficients(camera) self._calculate_move_coefficients(camera)
@ -416,9 +417,14 @@ class PtzAutoTracker:
continue continue
def _enqueue_move(self, camera, frame_time, pan, tilt, zoom): def _enqueue_move(self, camera, frame_time, pan, tilt, zoom):
def split_value(value):
clipped = np.clip(value, -1, 1)
return clipped, value - clipped
if ( if (
frame_time > self.ptz_metrics[camera]["ptz_start_time"].value frame_time > self.ptz_metrics[camera]["ptz_start_time"].value
and frame_time > self.ptz_metrics[camera]["ptz_stop_time"].value and frame_time > self.ptz_metrics[camera]["ptz_stop_time"].value
and self.move_queues[camera].qsize() == 0
): ):
# don't make small movements # don't make small movements
if abs(pan) < 0.02: if abs(pan) < 0.02:
@ -426,13 +432,22 @@ class PtzAutoTracker:
if abs(tilt) < 0.02: if abs(tilt) < 0.02:
tilt = 0 tilt = 0
if pan != 0 or tilt != 0 or zoom != 0: # split up any large moves caused by velocity estimated movements
while pan != 0 or tilt != 0 or zoom != 0:
pan, pan_excess = split_value(pan)
tilt, tilt_excess = split_value(tilt)
zoom, zoom_excess = split_value(zoom)
logger.debug( logger.debug(
f"enqueue pan: {pan}, enqueue tilt: {tilt}, enqueue zoom: {zoom}" f"Enqueue movement for frame time: {frame_time} pan: {pan}, enqueue tilt: {tilt}, enqueue zoom: {zoom}"
) )
move_data = (frame_time, pan, tilt, zoom) move_data = (frame_time, pan, tilt, zoom)
self.move_queues[camera].put(move_data) self.move_queues[camera].put(move_data)
pan = pan_excess
tilt = tilt_excess
zoom = zoom_excess
def _should_zoom_in(self, camera, box, area): def _should_zoom_in(self, camera, box, area):
camera_config = self.config.cameras[camera] camera_config = self.config.cameras[camera]
camera_width = camera_config.frame_shape[1] camera_width = camera_config.frame_shape[1]
@ -445,6 +460,8 @@ class PtzAutoTracker:
# If object area is less than 70% of frame # If object area is less than 70% of frame
# Then zoom in, otherwise try zooming out # Then zoom in, otherwise try zooming out
# should we make these configurable? # should we make these configurable?
#
# TODO: Take into account the area changing when an object is moving out of frame
edge_threshold = 0.15 edge_threshold = 0.15
area_threshold = 0.7 area_threshold = 0.7
@ -486,18 +503,11 @@ class PtzAutoTracker:
(y1 + y2) / 2, (y1 + y2) / 2,
) )
# ensure bounding box remains in the frame # this box could exceed the frame boundaries if velocity is high
# but we'll handle that in _enqueue_move() as two separate moves
predicted_box = [ predicted_box = [
round( round(x + camera_fps * predicted_movement_time * v)
max( for x, v in zip(obj.obj_data["box"], average_velocity)
0,
min(
x + camera_fps * predicted_movement_time * v,
camera_width if i % 2 == 0 else camera_height,
),
)
)
for i, (x, v) in enumerate(zip(obj.obj_data["box"], average_velocity))
] ]
centroid_x = round((predicted_box[0] + predicted_box[2]) / 2) centroid_x = round((predicted_box[0] + predicted_box[2]) / 2)
@ -511,10 +521,12 @@ class PtzAutoTracker:
logger.debug(f"Predicted box: {predicted_box}") logger.debug(f"Predicted box: {predicted_box}")
logger.debug(f'Velocity: {obj.obj_data["estimate_velocity"]}') logger.debug(f'Velocity: {obj.obj_data["estimate_velocity"]}')
# ideas: check object velocity for camera speed?
if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative: if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative:
# relative zooming concurrently with pan/tilt # relative zooming concurrently with pan/tilt
zoom = obj.obj_data["area"] / (camera_width * camera_height) # double the object area so we leave some room around the object
# perhaps make this a configurable value?
area_scale = 2
zoom = (obj.obj_data["area"] * area_scale) / (camera_width * camera_height)
# test if we need to zoom out # test if we need to zoom out
if not self._should_zoom_in( if not self._should_zoom_in(
@ -715,7 +727,7 @@ class PtzAutoTracker:
and self.tracked_object_previous[camera] is not None and self.tracked_object_previous[camera] is not None
and ( and (
# might want to use a different timestamp here? # might want to use a different timestamp here?
time.time() self.ptz_metrics[camera]["ptz_frame_time"].value
- self.tracked_object_previous[camera].obj_data["frame_time"] - self.tracked_object_previous[camera].obj_data["frame_time"]
> autotracker_config.timeout > autotracker_config.timeout
) )
@ -731,7 +743,7 @@ class PtzAutoTracker:
self.ptz_metrics[camera]["ptz_stopped"].wait() self.ptz_metrics[camera]["ptz_stopped"].wait()
logger.debug( logger.debug(
f"Autotrack: Time is {time.time()}, returning to preset: {autotracker_config.return_preset}" f"Autotrack: Time is {self.ptz_metrics[camera]['ptz_frame_time'].value}, returning to preset: {autotracker_config.return_preset}"
) )
self.onvif._move_to_preset( self.onvif._move_to_preset(
camera, camera,

View File

@ -1,6 +1,5 @@
"""Configure and control camera via onvif.""" """Configure and control camera via onvif."""
import datetime
import logging import logging
import site import site
from enum import Enum from enum import Enum
@ -8,7 +7,7 @@ from enum import Enum
import numpy import numpy
from onvif import ONVIFCamera, ONVIFError from onvif import ONVIFCamera, ONVIFError
from frigate.config import FrigateConfig from frigate.config import FrigateConfig, ZoomingModeEnum
from frigate.types import PTZMetricsTypes from frigate.types import PTZMetricsTypes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -266,10 +265,12 @@ class OnvifController:
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
self.ptz_metrics[camera_name]["ptz_stopped"].clear() self.ptz_metrics[camera_name]["ptz_stopped"].clear()
logger.debug(f"PTZ start time: {datetime.datetime.now().timestamp()}") logger.debug(
self.ptz_metrics[camera_name][ f"PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
"ptz_start_time" )
].value = datetime.datetime.now().timestamp() self.ptz_metrics[camera_name]["ptz_start_time"].value = self.ptz_metrics[
camera_name
]["ptz_frame_time"].value
self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0 self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0
onvif: ONVIFCamera = self.cams[camera_name]["onvif"] onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
move_request = self.cams[camera_name]["relative_move_request"] move_request = self.cams[camera_name]["relative_move_request"]
@ -376,10 +377,12 @@ class OnvifController:
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
self.ptz_metrics[camera_name]["ptz_stopped"].clear() self.ptz_metrics[camera_name]["ptz_stopped"].clear()
logger.debug(f"PTZ start time: {datetime.datetime.now().timestamp()}") logger.debug(
self.ptz_metrics[camera_name][ f"PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
"ptz_start_time" )
].value = datetime.datetime.now().timestamp() self.ptz_metrics[camera_name]["ptz_start_time"].value = self.ptz_metrics[
camera_name
]["ptz_frame_time"].value
self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0 self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0
onvif: ONVIFCamera = self.cams[camera_name]["onvif"] onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
move_request = self.cams[camera_name]["absolute_move_request"] move_request = self.cams[camera_name]["absolute_move_request"]
@ -463,24 +466,31 @@ class OnvifController:
if not self.ptz_metrics[camera_name]["ptz_stopped"].is_set(): if not self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].set() self.ptz_metrics[camera_name]["ptz_stopped"].set()
logger.debug(f"PTZ stop time: {datetime.datetime.now().timestamp()}") logger.debug(
f"PTZ stop time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
)
self.ptz_metrics[camera_name][ self.ptz_metrics[camera_name]["ptz_stop_time"].value = self.ptz_metrics[
"ptz_stop_time" camera_name
].value = datetime.datetime.now().timestamp() ]["ptz_frame_time"].value
else: else:
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
if self.ptz_metrics[camera_name]["ptz_stopped"].is_set(): if self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].clear() self.ptz_metrics[camera_name]["ptz_stopped"].clear()
logger.debug(f"PTZ start time: {datetime.datetime.now().timestamp()}") logger.debug(
f"PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
)
self.ptz_metrics[camera_name][ self.ptz_metrics[camera_name][
"ptz_start_time" "ptz_start_time"
].value = datetime.datetime.now().timestamp() ].value = self.ptz_metrics[camera_name]["ptz_frame_time"].value
self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0 self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0
if self.config.cameras[camera_name].onvif.autotracking.zooming: if (
self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.absolute
):
# store absolute zoom level as 0 to 1 interpolated from the values of the camera # store absolute zoom level as 0 to 1 interpolated from the values of the camera
self.ptz_metrics[camera_name]["ptz_zoom_level"].value = numpy.interp( self.ptz_metrics[camera_name]["ptz_zoom_level"].value = numpy.interp(
round(status.Position.Zoom.x, 2), round(status.Position.Zoom.x, 2),

View File

@ -31,6 +31,7 @@ class PTZMetricsTypes(TypedDict):
ptz_reset: Event ptz_reset: Event
ptz_start_time: Synchronized ptz_start_time: Synchronized
ptz_stop_time: Synchronized ptz_stop_time: Synchronized
ptz_frame_time: Synchronized
ptz_zoom_level: Synchronized ptz_zoom_level: Synchronized

View File

@ -772,6 +772,7 @@ def process_frames(
continue continue
current_frame_time.value = frame_time current_frame_time.value = frame_time
ptz_metrics["ptz_frame_time"].value = frame_time
frame = frame_manager.get( frame = frame_manager.get(
f"{camera_name}{frame_time}", (frame_shape[0] * 3 // 2, frame_shape[1]) f"{camera_name}{frame_time}", (frame_shape[0] * 3 // 2, frame_shape[1])