Autotracking optimizations (#7109)

* much improved motion estimation and tracking

* docs updates

* move ptz specific mp values to ptz_metrics dict

* only check if moving at frame time

* pass full dict instead of individual values
This commit is contained in:
Josh Hawkins 2023-07-11 06:23:20 -05:00 committed by GitHub
parent a94346e17e
commit 7a2d09dc35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 97 deletions

View File

@ -3,13 +3,13 @@ id: autotracking
title: Autotracking title: Autotracking
--- ---
An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. An ONVIF-capable, PTZ (pan-tilt-zoom) camera that supports relative movement within the field of view (FOV) can be configured to automatically track moving objects and keep them in the center of the frame.
## Autotracking behavior ## Autotracking behavior
Once Frigate determines that an object is not a false positive and has entered one of the required zones, the autotracker will move the PTZ camera to keep the object centered in the frame until the object either moves out of the frame, the PTZ is not capable of any more movement, or Frigate loses track of it. Once Frigate determines that an object is not a false positive and has entered one of the required zones, the autotracker will move the PTZ camera to keep the object centered in the frame until the object either moves out of the frame, the PTZ is not capable of any more movement, or Frigate loses track of it.
Upon loss of tracking, Frigate will scan the region of the lost object for `timeout` seconds. If an object of the same type is found in that region, Frigate will track that new object. Upon loss of tracking, Frigate will scan the region of the lost object for `timeout` seconds. If an object of the same type is found in that region, Frigate will autotrack that new object.
When tracking has ended, Frigate will return to the camera preset specified by the `return_preset` configuration entry. When tracking has ended, Frigate will return to the camera preset specified by the `return_preset` configuration entry.
@ -17,13 +17,15 @@ When tracking has ended, Frigate will return to the camera preset specified by t
Frigate autotracking functions with PTZ cameras capable of relative movement within the field of view (as specified in the [ONVIF spec](https://www.onvif.org/specs/srv/ptz/ONVIF-PTZ-Service-Spec-v1712.pdf) as `RelativePanTiltTranslationSpace` having a `TranslationSpaceFov` entry). Frigate autotracking functions with PTZ cameras capable of relative movement within the field of view (as specified in the [ONVIF spec](https://www.onvif.org/specs/srv/ptz/ONVIF-PTZ-Service-Spec-v1712.pdf) as `RelativePanTiltTranslationSpace` having a `TranslationSpaceFov` entry).
Many cheaper PTZs likely don't support this standard. Frigate will report an error message in the log and disable autotracking if your PTZ is unsupported. Many cheaper or older PTZs may not support this standard. Frigate will report an error message in the log and disable autotracking if your PTZ is unsupported.
Alternatively, you can download and run [this simple Python script](https://gist.github.com/hawkeye217/152a1d4ba80760dac95d46e143d37112), replacing the details on line 4 with your camera's IP address, ONVIF port, username, and password to check your camera. Alternatively, you can download and run [this simple Python script](https://gist.github.com/hawkeye217/152a1d4ba80760dac95d46e143d37112), replacing the details on line 4 with your camera's IP address, ONVIF port, username, and password to check your camera.
## Configuration ## Configuration
First, configure the ONVIF parameters for your camera, then specify the object types to track, a required zone the object must enter, and a camera preset name to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset. First, set up a PTZ preset in your camera's firmware and give it a name.
Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset.
An [ONVIF connection](cameras.md) is required for autotracking to function. An [ONVIF connection](cameras.md) is required for autotracking to function.
@ -69,3 +71,7 @@ The object tracker in Frigate estimates the motion of the PTZ so that tracked ob
A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. If Frigate already has trouble keeping track of your object, the autotracker will struggle as well. A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. If Frigate already has trouble keeping track of your object, the autotracker will struggle as well.
The autotracker will add PTZ motion requests to a queue while the motor is moving. Once the motor stops, the events in the queue will be executed together as one large move (rather than incremental moves). If your PTZ's motor is slow, you may not be able to reliably autotrack fast moving objects. The autotracker will add PTZ motion requests to a queue while the motor is moving. Once the motor stops, the events in the queue will be executed together as one large move (rather than incremental moves). If your PTZ's motor is slow, you may not be able to reliably autotrack fast moving objects.
## Usage applications
In security and surveillance, it's common to use "spotter" cameras in combination with your PTZ. When your fixed spotter camera detects an object, you could use an automation platform like Home Assistant to move the PTZ to a specific preset so that Frigate can begin automatically tracking the object. For example: a residence may have fixed cameras on the east and west side of the property, capturing views up and down a street. When the spotter camera on the west side detects a person, a Home Assistant automation could move the PTZ to a camera preset aimed toward the west. When the object enters the specified zone, Frigate's autotracker could then continue to track the person as it moves out of view of any of the fixed cameras.

View File

@ -48,7 +48,7 @@ from frigate.record.record import manage_recordings
from frigate.stats import StatsEmitter, stats_init from frigate.stats import StatsEmitter, stats_init
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog from frigate.watchdog import FrigateWatchdog
@ -67,6 +67,7 @@ class FrigateApp:
self.plus_api = PlusApi() self.plus_api = PlusApi()
self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.camera_metrics: dict[str, CameraMetricsTypes] = {}
self.feature_metrics: dict[str, FeatureMetricsTypes] = {} self.feature_metrics: dict[str, FeatureMetricsTypes] = {}
self.ptz_metrics: dict[str, PTZMetricsTypes] = {}
self.processes: dict[str, int] = {} self.processes: dict[str, int] = {}
def set_environment_vars(self) -> None: def set_environment_vars(self) -> None:
@ -135,13 +136,6 @@ class FrigateApp:
"i", "i",
self.config.cameras[camera_name].motion.improve_contrast, self.config.cameras[camera_name].motion.improve_contrast,
), ),
"ptz_autotracker_enabled": mp.Value( # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"i",
self.config.cameras[camera_name].onvif.autotracking.enabled,
),
"ptz_stopped": mp.Event(),
"motion_threshold": mp.Value( # type: ignore[typeddict-item] "motion_threshold": mp.Value( # 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
@ -170,7 +164,22 @@ class FrigateApp:
"capture_process": None, "capture_process": None,
"process": None, "process": None,
} }
self.camera_metrics[camera_name]["ptz_stopped"].set() self.ptz_metrics[camera_name] = {
"ptz_autotracker_enabled": mp.Value( # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"i",
self.config.cameras[camera_name].onvif.autotracking.enabled,
),
"ptz_stopped": mp.Event(),
"ptz_start_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
"ptz_stop_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
}
self.ptz_metrics[camera_name]["ptz_stopped"].set()
self.feature_metrics[camera_name] = { self.feature_metrics[camera_name] = {
"audio_enabled": mp.Value( # type: ignore[typeddict-item] "audio_enabled": mp.Value( # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799 # issue https://github.com/python/typeshed/issues/8799
@ -317,7 +326,7 @@ class FrigateApp:
) )
def init_onvif(self) -> None: def init_onvif(self) -> None:
self.onvif_controller = OnvifController(self.config, self.camera_metrics) self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
def init_dispatcher(self) -> None: def init_dispatcher(self) -> None:
comms: list[Communicator] = [] comms: list[Communicator] = []
@ -331,6 +340,7 @@ class FrigateApp:
self.onvif_controller, self.onvif_controller,
self.camera_metrics, self.camera_metrics,
self.feature_metrics, self.feature_metrics,
self.ptz_metrics,
comms, comms,
) )
@ -375,7 +385,7 @@ class FrigateApp:
self.ptz_autotracker_thread = PtzAutoTrackerThread( self.ptz_autotracker_thread = PtzAutoTrackerThread(
self.config, self.config,
self.onvif_controller, self.onvif_controller,
self.camera_metrics, self.ptz_metrics,
self.stop_event, self.stop_event,
) )
self.ptz_autotracker_thread.start() self.ptz_autotracker_thread.start()
@ -426,6 +436,7 @@ class FrigateApp:
self.detection_out_events[name], self.detection_out_events[name],
self.detected_frames_queue, self.detected_frames_queue,
self.camera_metrics[name], self.camera_metrics[name],
self.ptz_metrics[name],
), ),
) )
camera_process.daemon = True camera_process.daemon = True

View File

@ -6,7 +6,7 @@ from typing import Any, Callable
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.ptz.onvif import OnvifCommandEnum, OnvifController from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
from frigate.util.services import restart_frigate from frigate.util.services import restart_frigate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,12 +40,14 @@ class Dispatcher:
onvif: OnvifController, onvif: OnvifController,
camera_metrics: dict[str, CameraMetricsTypes], camera_metrics: dict[str, CameraMetricsTypes],
feature_metrics: dict[str, FeatureMetricsTypes], feature_metrics: dict[str, FeatureMetricsTypes],
ptz_metrics: dict[str, PTZMetricsTypes],
communicators: list[Communicator], communicators: list[Communicator],
) -> None: ) -> None:
self.config = config self.config = config
self.onvif = onvif self.onvif = onvif
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
self.feature_metrics = feature_metrics self.feature_metrics = feature_metrics
self.ptz_metrics = ptz_metrics
self.comms = communicators self.comms = communicators
for comm in self.comms: for comm in self.comms:
@ -165,16 +167,14 @@ class Dispatcher:
ptz_autotracker_settings = self.config.cameras[camera_name].onvif.autotracking ptz_autotracker_settings = self.config.cameras[camera_name].onvif.autotracking
if payload == "ON": if payload == "ON":
if not self.camera_metrics[camera_name]["ptz_autotracker_enabled"].value: if not self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value:
logger.info(f"Turning on ptz autotracker for {camera_name}") logger.info(f"Turning on ptz autotracker for {camera_name}")
self.camera_metrics[camera_name]["ptz_autotracker_enabled"].value = True self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value = True
ptz_autotracker_settings.enabled = True ptz_autotracker_settings.enabled = True
elif payload == "OFF": elif payload == "OFF":
if self.camera_metrics[camera_name]["ptz_autotracker_enabled"].value: if self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value:
logger.info(f"Turning off ptz autotracker for {camera_name}") logger.info(f"Turning off ptz autotracker for {camera_name}")
self.camera_metrics[camera_name][ self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value = False
"ptz_autotracker_enabled"
].value = False
ptz_autotracker_settings.enabled = False ptz_autotracker_settings.enabled = False
self.publish(f"{camera_name}/ptz_autotracker/state", payload, retain=True) self.publish(f"{camera_name}/ptz_autotracker/state", payload, retain=True)

View File

@ -2,6 +2,7 @@
import copy import copy
import logging import logging
import math
import queue import queue
import threading import threading
import time import time
@ -14,33 +15,48 @@ from norfair.camera_motion import MotionEstimator, TranslationTransformationGett
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, FrigateConfig
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.types import CameraMetricsTypes from frigate.types import PTZMetricsTypes
from frigate.util.image import SharedMemoryFrameManager, intersection_over_union from frigate.util.image import SharedMemoryFrameManager, intersection_over_union
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time):
# Determine if the PTZ was in motion at the set frame time
# for non ptz/autotracking cameras, this will always return False
# ptz_start_time is initialized to 0 on startup and only changes
# when autotracking movements are made
# the offset "primes" the motion estimator with a few frames before movement
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)
)
class PtzMotionEstimator: class PtzMotionEstimator:
def __init__(self, config: CameraConfig, ptz_stopped) -> None: def __init__(self, config: CameraConfig, ptz_metrics: PTZMetricsTypes) -> None:
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
# homography is nice (zooming) but slow, translation is pan/tilt only but fast. # homography is nice (zooming) but slow, translation is pan/tilt only but fast.
self.norfair_motion_estimator = MotionEstimator( self.norfair_motion_estimator = MotionEstimator(
transformations_getter=TranslationTransformationGetter(), transformations_getter=TranslationTransformationGetter(),
min_distance=30, min_distance=30,
max_points=500, max_points=900,
) )
self.camera_config = config self.camera_config = config
self.coord_transformations = None self.coord_transformations = None
self.ptz_stopped = ptz_stopped self.ptz_metrics = ptz_metrics
self.ptz_start_time = self.ptz_metrics["ptz_start_time"]
self.ptz_stop_time = self.ptz_metrics["ptz_stop_time"]
logger.debug(f"Motion estimator init for cam: {config.name}") logger.debug(f"Motion estimator init for cam: {config.name}")
def motion_estimator(self, detections, frame_time, camera_name): def motion_estimator(self, detections, frame_time, camera_name):
if ( if ptz_moving_at_frame_time(
self.camera_config.onvif.autotracking.enabled frame_time, self.ptz_start_time.value, self.ptz_stop_time.value
and not self.ptz_stopped.is_set()
): ):
logger.debug( logger.debug(
f"Motion estimator running for {camera_name} - frame time: {frame_time}" f"Motion estimator running for {camera_name} - frame time: {frame_time}, {self.ptz_start_time.value}, {self.ptz_stop_time.value}"
) )
frame_id = f"{camera_name}{frame_time}" frame_id = f"{camera_name}{frame_time}"
@ -74,9 +90,7 @@ class PtzMotionEstimator:
f"Motion estimator transformation: {self.coord_transformations.rel_to_abs((0,0))}" f"Motion estimator transformation: {self.coord_transformations.rel_to_abs((0,0))}"
) )
return self.coord_transformations return self.coord_transformations
return None
class PtzAutoTrackerThread(threading.Thread): class PtzAutoTrackerThread(threading.Thread):
@ -84,12 +98,12 @@ class PtzAutoTrackerThread(threading.Thread):
self, self,
config: FrigateConfig, config: FrigateConfig,
onvif: OnvifController, onvif: OnvifController,
camera_metrics: dict[str, CameraMetricsTypes], ptz_metrics: dict[str, PTZMetricsTypes],
stop_event: MpEvent, stop_event: MpEvent,
) -> None: ) -> None:
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "ptz_autotracker" self.name = "ptz_autotracker"
self.ptz_autotracker = PtzAutoTracker(config, onvif, camera_metrics) self.ptz_autotracker = PtzAutoTracker(config, onvif, ptz_metrics)
self.stop_event = stop_event self.stop_event = stop_event
self.config = config self.config = config
@ -112,11 +126,11 @@ class PtzAutoTracker:
self, self,
config: FrigateConfig, config: FrigateConfig,
onvif: OnvifController, onvif: OnvifController,
camera_metrics: CameraMetricsTypes, ptz_metrics: PTZMetricsTypes,
) -> None: ) -> None:
self.config = config self.config = config
self.onvif = onvif self.onvif = onvif
self.camera_metrics = camera_metrics self.ptz_metrics = ptz_metrics
self.tracked_object: dict[str, object] = {} self.tracked_object: dict[str, object] = {}
self.tracked_object_previous: dict[str, object] = {} self.tracked_object_previous: dict[str, object] = {}
self.object_types = {} self.object_types = {}
@ -146,17 +160,13 @@ class PtzAutoTracker:
if not self.onvif._init_onvif(camera_name): if not self.onvif._init_onvif(camera_name):
logger.warning(f"Unable to initialize onvif for {camera_name}") logger.warning(f"Unable to initialize onvif for {camera_name}")
cam.onvif.autotracking.enabled = False cam.onvif.autotracking.enabled = False
self.camera_metrics[camera_name][ self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value = False
"ptz_autotracker_enabled"
].value = False
return return
if not self.onvif.cams[camera_name]["relative_fov_supported"]: if not self.onvif.cams[camera_name]["relative_fov_supported"]:
cam.onvif.autotracking.enabled = False cam.onvif.autotracking.enabled = False
self.camera_metrics[camera_name][ self.ptz_metrics[camera_name]["ptz_autotracker_enabled"].value = False
"ptz_autotracker_enabled"
].value = False
logger.warning( logger.warning(
f"Disabling autotracking for {camera_name}: FOV relative movement not supported" f"Disabling autotracking for {camera_name}: FOV relative movement not supported"
) )
@ -198,16 +208,10 @@ class PtzAutoTracker:
move_data = self.move_queues[camera].get() move_data = self.move_queues[camera].get()
pan, tilt = move_data pan, tilt = move_data
# check if ptz is moving
self.onvif.get_camera_status(camera)
# Wait until the camera finishes moving
self.camera_metrics[camera]["ptz_stopped"].wait()
self.onvif._move_relative(camera, pan, tilt, 1) self.onvif._move_relative(camera, pan, tilt, 1)
# Wait until the camera finishes moving # Wait until the camera finishes moving
while not self.camera_metrics[camera]["ptz_stopped"].is_set(): while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
# check if ptz is moving # check if ptz is moving
self.onvif.get_camera_status(camera) self.onvif.get_camera_status(camera)
time.sleep(1 / (self.config.cameras[camera].detect.fps / 2)) time.sleep(1 / (self.config.cameras[camera].detect.fps / 2))
@ -237,10 +241,7 @@ class PtzAutoTracker:
def autotrack_object(self, camera, obj): def autotrack_object(self, camera, obj):
camera_config = self.config.cameras[camera] camera_config = self.config.cameras[camera]
if ( if camera_config.onvif.autotracking.enabled:
camera_config.onvif.autotracking.enabled
and self.camera_metrics[camera]["ptz_stopped"].is_set()
):
# either this is a brand new object that's on our camera, has our label, entered the zone, is not a false positive, # either this is a brand new object that's on our camera, has our label, entered the zone, is not a false positive,
# and is not initially motionless - or one we're already tracking, which assumes all those things are already true # and is not initially motionless - or one we're already tracking, which assumes all those things are already true
if ( if (
@ -259,7 +260,11 @@ class PtzAutoTracker:
) )
self.tracked_object[camera] = obj self.tracked_object[camera] = obj
self.tracked_object_previous[camera] = copy.deepcopy(obj) self.tracked_object_previous[camera] = copy.deepcopy(obj)
self._autotrack_move_ptz(camera, obj) # only enqueue another move if the camera isn't moving
if self.ptz_metrics[camera]["ptz_stopped"].is_set():
self.ptz_metrics[camera]["ptz_stopped"].clear()
logger.debug("Autotrack: New object, moving ptz")
self._autotrack_move_ptz(camera, obj)
return return
@ -271,28 +276,57 @@ class PtzAutoTracker:
and obj.obj_data["frame_time"] and obj.obj_data["frame_time"]
!= self.tracked_object_previous[camera].obj_data["frame_time"] != self.tracked_object_previous[camera].obj_data["frame_time"]
): ):
# don't move the ptz if we're relatively close to the existing box # Don't move ptz if Euclidean distance from object to center of frame is
# should we use iou or euclidean distance or both? # less than 15% of the of the larger dimension (width or height) of the frame,
# distance = math.sqrt((obj.obj_data["centroid"][0] - camera_width/2)**2 + (obj.obj_data["centroid"][1] - obj.camera_height/2)**2) # multiplied by a scaling factor for object size.
# if distance <= (self.camera_width * .15) or distance <= (self.camera_height * .15) # Adjusting this percentage slightly lower will effectively cause the camera to move
if ( # more often to keep the object in the center. Raising the percentage will cause less
intersection_over_union( # movement and will be more flexible with objects not quite being centered.
self.tracked_object_previous[camera].obj_data["box"], # TODO: there's probably a better way to approach this
obj.obj_data["box"], distance = math.sqrt(
) (obj.obj_data["centroid"][0] - camera_config.detect.width / 2) ** 2
> 0.5 + (obj.obj_data["centroid"][1] - camera_config.detect.height / 2)
): ** 2
)
obj_width = obj.obj_data["box"][2] - obj.obj_data["box"][0]
obj_height = obj.obj_data["box"][3] - obj.obj_data["box"][1]
max_obj = max(obj_width, obj_height)
max_frame = max(camera_config.detect.width, camera_config.detect.height)
# larger objects should lower the threshold, smaller objects should raise it
scaling_factor = 1 - (max_obj / max_frame)
distance_threshold = 0.15 * (max_frame) * scaling_factor
iou = intersection_over_union(
self.tracked_object_previous[camera].obj_data["box"],
obj.obj_data["box"],
)
logger.debug(
f"Distance: {distance}, threshold: {distance_threshold}, iou: {iou}"
)
if (distance < distance_threshold or iou > 0.5) and self.ptz_metrics[
camera
]["ptz_stopped"].is_set():
logger.debug( logger.debug(
f"Autotrack: Existing object (do NOT move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" f"Autotrack: Existing object (do NOT move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}"
) )
self.tracked_object_previous[camera] = copy.deepcopy(obj)
return return
logger.debug( logger.debug(
f"Autotrack: Existing object (move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" f"Autotrack: Existing object (need to move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}"
) )
self.tracked_object_previous[camera] = copy.deepcopy(obj) self.tracked_object_previous[camera] = copy.deepcopy(obj)
self._autotrack_move_ptz(camera, obj)
# only enqueue another move if the camera isn't moving
if self.ptz_metrics[camera]["ptz_stopped"].is_set():
self.ptz_metrics[camera]["ptz_stopped"].clear()
logger.debug("Autotrack: Existing object, moving ptz")
self._autotrack_move_ptz(camera, obj)
return return
@ -320,7 +354,11 @@ class PtzAutoTracker:
) )
self.tracked_object[camera] = obj self.tracked_object[camera] = obj
self.tracked_object_previous[camera] = copy.deepcopy(obj) self.tracked_object_previous[camera] = copy.deepcopy(obj)
self._autotrack_move_ptz(camera, obj) # only enqueue another move if the camera isn't moving
if self.ptz_metrics[camera]["ptz_stopped"].is_set():
self.ptz_metrics[camera]["ptz_stopped"].clear()
logger.debug("Autotrack: Reacquired object, moving ptz")
self._autotrack_move_ptz(camera, obj)
return return
@ -334,7 +372,6 @@ class PtzAutoTracker:
f"Autotrack: End object: {obj.obj_data['id']} {obj.obj_data['box']}" f"Autotrack: End object: {obj.obj_data['id']} {obj.obj_data['box']}"
) )
self.tracked_object[camera] = None self.tracked_object[camera] = None
self.onvif.get_camera_status(camera)
def camera_maintenance(self, camera): def camera_maintenance(self, camera):
# calls get_camera_status to check/update ptz movement # calls get_camera_status to check/update ptz movement
@ -344,7 +381,7 @@ class PtzAutoTracker:
if not self.autotracker_init[camera]: if not self.autotracker_init[camera]:
self._autotracker_setup(self.config.cameras[camera], camera) self._autotracker_setup(self.config.cameras[camera], camera)
# regularly update camera status # regularly update camera status
if not self.camera_metrics[camera]["ptz_stopped"].is_set(): if not self.ptz_metrics[camera]["ptz_stopped"].is_set():
self.onvif.get_camera_status(camera) self.onvif.get_camera_status(camera)
# return to preset if tracking is over # return to preset if tracking is over
@ -359,7 +396,7 @@ class PtzAutoTracker:
) )
and autotracker_config.return_preset and autotracker_config.return_preset
): ):
self.camera_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 {time.time()}, returning to preset: {autotracker_config.return_preset}"
) )

View File

@ -1,5 +1,6 @@
"""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 +9,7 @@ import numpy
from onvif import ONVIFCamera, ONVIFError from onvif import ONVIFCamera, ONVIFError
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.types import CameraMetricsTypes from frigate.types import PTZMetricsTypes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,10 +30,10 @@ class OnvifCommandEnum(str, Enum):
class OnvifController: class OnvifController:
def __init__( def __init__(
self, config: FrigateConfig, camera_metrics: dict[str, CameraMetricsTypes] self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetricsTypes]
) -> None: ) -> None:
self.cams: dict[str, ONVIFCamera] = {} self.cams: dict[str, ONVIFCamera] = {}
self.camera_metrics = camera_metrics self.ptz_metrics = ptz_metrics
for cam_name, cam in config.cameras.items(): for cam_name, cam in config.cameras.items():
if not cam.enabled: if not cam.enabled:
@ -207,7 +208,6 @@ class OnvifController:
return return
logger.debug(f"{camera_name} called RelativeMove: pan: {pan} tilt: {tilt}") logger.debug(f"{camera_name} called RelativeMove: pan: {pan} tilt: {tilt}")
self.get_camera_status(camera_name)
if self.cams[camera_name]["active"]: if self.cams[camera_name]["active"]:
logger.warning( logger.warning(
@ -216,7 +216,12 @@ class OnvifController:
return return
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
self.camera_metrics[camera_name]["ptz_stopped"].clear() self.ptz_metrics[camera_name]["ptz_stopped"].clear()
logger.debug(f"PTZ start time: {datetime.datetime.now().timestamp()}")
self.ptz_metrics[camera_name][
"ptz_start_time"
].value = datetime.datetime.now().timestamp()
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"]
@ -261,7 +266,7 @@ class OnvifController:
return return
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
self.camera_metrics[camera_name]["ptz_stopped"].clear() self.ptz_metrics[camera_name]["ptz_stopped"].clear()
move_request = self.cams[camera_name]["move_request"] move_request = self.cams[camera_name]["move_request"]
onvif: ONVIFCamera = self.cams[camera_name]["onvif"] onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
preset_token = self.cams[camera_name]["presets"][preset] preset_token = self.cams[camera_name]["presets"][preset]
@ -271,7 +276,7 @@ class OnvifController:
"PresetToken": preset_token, "PresetToken": preset_token,
} }
) )
self.camera_metrics[camera_name]["ptz_stopped"].set() self.ptz_metrics[camera_name]["ptz_stopped"].set()
self.cams[camera_name]["active"] = False self.cams[camera_name]["active"] = False
def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None: def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None:
@ -345,10 +350,25 @@ class OnvifController:
if status.MoveStatus.PanTilt == "IDLE" and status.MoveStatus.Zoom == "IDLE": if status.MoveStatus.PanTilt == "IDLE" and status.MoveStatus.Zoom == "IDLE":
self.cams[camera_name]["active"] = False self.cams[camera_name]["active"] = False
self.camera_metrics[camera_name]["ptz_stopped"].set() if not self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].set()
logger.debug(f"PTZ stop time: {datetime.datetime.now().timestamp()}")
self.ptz_metrics[camera_name][
"ptz_stop_time"
].value = datetime.datetime.now().timestamp()
else: else:
self.cams[camera_name]["active"] = True self.cams[camera_name]["active"] = True
self.camera_metrics[camera_name]["ptz_stopped"].clear() if self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
logger.debug(f"PTZ start time: {datetime.datetime.now().timestamp()}")
self.ptz_metrics[camera_name][
"ptz_start_time"
].value = datetime.datetime.now().timestamp()
self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0
return { return {
"pan": status.Position.PanTilt.x, "pan": status.Position.PanTilt.x,

View File

@ -8,6 +8,7 @@ from norfair.drawing.drawer import Drawer
from frigate.config import CameraConfig from frigate.config import CameraConfig
from frigate.ptz.autotrack import PtzMotionEstimator from frigate.ptz.autotrack import PtzMotionEstimator
from frigate.track import ObjectTracker from frigate.track import ObjectTracker
from frigate.types import PTZMetricsTypes
from frigate.util.image import intersection_over_union from frigate.util.image import intersection_over_union
@ -55,15 +56,18 @@ def frigate_distance(detection: Detection, tracked_object) -> float:
class NorfairTracker(ObjectTracker): class NorfairTracker(ObjectTracker):
def __init__(self, config: CameraConfig, ptz_autotracker_enabled, ptz_stopped): def __init__(
self,
config: CameraConfig,
ptz_metrics: PTZMetricsTypes,
):
self.tracked_objects = {} self.tracked_objects = {}
self.disappeared = {} self.disappeared = {}
self.positions = {} self.positions = {}
self.max_disappeared = config.detect.max_disappeared self.max_disappeared = config.detect.max_disappeared
self.camera_config = config self.camera_config = config
self.detect_config = config.detect self.detect_config = config.detect
self.ptz_autotracker_enabled = ptz_autotracker_enabled.value self.ptz_autotracker_enabled = ptz_metrics["ptz_autotracker_enabled"]
self.ptz_stopped = ptz_stopped
self.camera_name = config.name self.camera_name = config.name
self.track_id_map = {} self.track_id_map = {}
# TODO: could also initialize a tracker per object class if there # TODO: could also initialize a tracker per object class if there
@ -74,8 +78,8 @@ class NorfairTracker(ObjectTracker):
initialization_delay=0, initialization_delay=0,
hit_counter_max=self.max_disappeared, hit_counter_max=self.max_disappeared,
) )
if self.ptz_autotracker_enabled: if self.ptz_autotracker_enabled.value:
self.ptz_motion_estimator = PtzMotionEstimator(config, self.ptz_stopped) self.ptz_motion_estimator = PtzMotionEstimator(config, ptz_metrics)
def register(self, track_id, obj): def register(self, track_id, obj):
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
@ -239,7 +243,7 @@ class NorfairTracker(ObjectTracker):
coord_transformations = None coord_transformations = None
if self.ptz_autotracker_enabled: if self.ptz_autotracker_enabled.value:
coord_transformations = self.ptz_motion_estimator.motion_estimator( coord_transformations = self.ptz_motion_estimator.motion_estimator(
detections, frame_time, self.camera_name detections, frame_time, self.camera_name
) )

View File

@ -18,8 +18,6 @@ class CameraMetricsTypes(TypedDict):
frame_queue: Queue frame_queue: Queue
motion_enabled: Synchronized motion_enabled: Synchronized
improve_contrast_enabled: Synchronized improve_contrast_enabled: Synchronized
ptz_autotracker_enabled: Synchronized
ptz_stopped: Event
motion_threshold: Synchronized motion_threshold: Synchronized
motion_contour_area: Synchronized motion_contour_area: Synchronized
process: Optional[Process] process: Optional[Process]
@ -28,6 +26,13 @@ class CameraMetricsTypes(TypedDict):
skipped_fps: Synchronized skipped_fps: Synchronized
class PTZMetricsTypes(TypedDict):
ptz_autotracker_enabled: Synchronized
ptz_stopped: Event
ptz_start_time: Synchronized
ptz_stop_time: Synchronized
class FeatureMetricsTypes(TypedDict): class FeatureMetricsTypes(TypedDict):
audio_enabled: Synchronized audio_enabled: Synchronized
record_enabled: Synchronized record_enabled: Synchronized

View File

@ -22,8 +22,10 @@ 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
from frigate.object_detection import RemoteObjectDetector from frigate.object_detection import RemoteObjectDetector
from frigate.ptz.autotrack import ptz_moving_at_frame_time
from frigate.track import ObjectTracker from frigate.track import ObjectTracker
from frigate.track.norfair_tracker import NorfairTracker from frigate.track.norfair_tracker import NorfairTracker
from frigate.types import PTZMetricsTypes
from frigate.util.builtin import EventsPerSecond from frigate.util.builtin import EventsPerSecond
from frigate.util.image import ( from frigate.util.image import (
FrameManager, FrameManager,
@ -461,6 +463,7 @@ def track_camera(
result_connection, result_connection,
detected_objects_queue, detected_objects_queue,
process_info, process_info,
ptz_metrics,
): ):
stop_event = mp.Event() stop_event = mp.Event()
@ -478,8 +481,6 @@ def track_camera(
detection_enabled = process_info["detection_enabled"] detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"] motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"] improve_contrast_enabled = process_info["improve_contrast_enabled"]
ptz_autotracker_enabled = process_info["ptz_autotracker_enabled"]
ptz_stopped = process_info["ptz_stopped"]
motion_threshold = process_info["motion_threshold"] motion_threshold = process_info["motion_threshold"]
motion_contour_area = process_info["motion_contour_area"] motion_contour_area = process_info["motion_contour_area"]
@ -499,7 +500,7 @@ def track_camera(
name, labelmap, detection_queue, result_connection, model_config, stop_event name, labelmap, detection_queue, result_connection, model_config, stop_event
) )
object_tracker = NorfairTracker(config, ptz_autotracker_enabled, ptz_stopped) object_tracker = NorfairTracker(config, ptz_metrics)
frame_manager = SharedMemoryFrameManager() frame_manager = SharedMemoryFrameManager()
@ -520,7 +521,7 @@ def track_camera(
detection_enabled, detection_enabled,
motion_enabled, motion_enabled,
stop_event, stop_event,
ptz_stopped, ptz_metrics,
) )
logger.info(f"{name}: exiting subprocess") logger.info(f"{name}: exiting subprocess")
@ -745,7 +746,7 @@ def process_frames(
detection_enabled: mp.Value, detection_enabled: mp.Value,
motion_enabled: mp.Value, motion_enabled: mp.Value,
stop_event, stop_event,
ptz_stopped: mp.Event, ptz_metrics: PTZMetricsTypes,
exit_on_empty: bool = False, exit_on_empty: bool = False,
): ):
fps = process_info["process_fps"] fps = process_info["process_fps"]
@ -781,10 +782,17 @@ def process_frames(
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.") logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
continue continue
# look for motion if enabled # look for motion if enabled and ptz is not moving
# ptz_moving_at_frame_time() always returns False for
# non ptz/autotracking cameras
motion_boxes = ( motion_boxes = (
motion_detector.detect(frame) motion_detector.detect(frame)
if motion_enabled.value and ptz_stopped.is_set() if motion_enabled.value
and not ptz_moving_at_frame_time(
frame_time,
ptz_metrics["ptz_start_time"].value,
ptz_metrics["ptz_stop_time"].value,
)
else [] else []
) )