From 0eb90f6e6d1d71b89f7e30cacbf2b6ee9854bcb6 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 4 Sep 2023 12:59:44 -0500 Subject: [PATCH] relative zooming option for dahua/amcrest cams --- docs/docs/configuration/autotracking.md | 4 + docs/docs/configuration/index.md | 2 + frigate/config.py | 3 + frigate/ptz/autotrack.py | 97 +++++++++++++++++-------- frigate/ptz/onvif.py | 26 +++++-- 5 files changed, 95 insertions(+), 37 deletions(-) diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md index 23ce2830d..77bb4998b 100644 --- a/docs/docs/configuration/autotracking.md +++ b/docs/docs/configuration/autotracking.md @@ -52,6 +52,8 @@ cameras: enabled: False # Optional: enable/disable camera zooming in/out on objects during autotracking. (default: shown below) zooming: False + # Optional: enable/disable relative zooming for the camera (default: shown below) + zoom_relative: False # Optional: list of objects to track from labelmap.txt (default: shown below) track: - person @@ -76,6 +78,8 @@ The autotracker will add PTZ motion requests to a queue while the motor is movin Zooming is an experimental feature and may use significantly more CPU when tracking objects than panning/tilting only. It may be helpful to tweak your camera's autofocus settings if you are noticing focus problems when using zooming. +Relative zooming makes a zoom movement concurrently with any pan/tilt movements and was tested to work with some Dahua and Amcrest PTZs. If zooming behavior is erratic or relative zooming is unsupported, the autotracker will fall back to absolute zooming where any zoom movements are separate from pan/tilt movements. + ## 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. diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index faf963483..594cbc8df 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -575,6 +575,8 @@ cameras: enabled: False # Optional: enable/disable camera zooming in/out on objects during autotracking. (default: shown below) zooming: False + # Optional: enable/disable relative zooming for the camera (default: shown below) + zoom_relative: False # Optional: list of objects to track from labelmap.txt (default: shown below) track: - person diff --git a/frigate/config.py b/frigate/config.py index 4e8996e60..f55dcc525 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -140,6 +140,9 @@ class MqttConfig(FrigateBaseModel): class PtzAutotrackConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable PTZ object autotracking.") zooming: bool = Field(default=False, title="Enable zooming on autotracked object.") + zoom_relative: bool = Field( + default=False, title="Use relative zooming instead of absolute." + ) track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.") required_zones: List[str] = Field( default_factory=list, diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 621fb1125..2fff296d8 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -232,17 +232,23 @@ class PtzAutoTracker: continue else: - if zoom > 0: - self.onvif._zoom_absolute(camera, zoom, 1) - - # on some cameras with cheaper motors it seems like small values can cause jerky movement - # TODO: double check, might not need this - if abs(pan) > 0.02 or abs(tilt) > 0.02: - self.onvif._move_relative(camera, pan, tilt, 1) + if ( + self.config.cameras[camera].onvif.autotracking.zooming + and self.config.cameras[camera].onvif.autotracking.zoom_relative + ): + self.onvif._move_relative(camera, pan, tilt, zoom, 1) else: - logger.debug( - f"Not moving, pan and tilt too small: {pan}, {tilt}" - ) + if zoom > 0: + self.onvif._zoom_absolute(camera, zoom, 1) + + # on some cameras with cheaper motors it seems like small values can cause jerky movement + # TODO: double check, might not need this + if abs(pan) > 0.02 or abs(tilt) > 0.02: + self.onvif._move_relative(camera, pan, tilt, 0, 1) + else: + logger.debug( + f"Not moving, pan and tilt too small: {pan}, {tilt}" + ) # Wait until the camera finishes moving while not self.ptz_metrics[camera]["ptz_stopped"].is_set(): @@ -263,6 +269,30 @@ class PtzAutoTracker: ) self.move_queues[camera].put(move_data) + def _should_zoom_in(self, obj, camera): + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_area = camera_width * camera_height + + bb_left, bb_top, bb_right, bb_bottom = obj.obj_data["box"] + + # If bounding box is not within 5% of an edge + # If object area is less than 70% of frame + # Then zoom in, otherwise try zooming out + # should we make these configurable? + edge_threshold = 0.05 + area_threshold = 0.7 + + # returns True to zoom in, False to zoom out + return ( + bb_left > edge_threshold * camera_width + and bb_right < (1 - edge_threshold) * camera_width + and bb_top > edge_threshold * camera_height + and bb_bottom < (1 - edge_threshold) * camera_height + and obj.obj_data["area"] < area_threshold * camera_area + ) + def _autotrack_move_ptz(self, camera, obj): camera_config = self.config.cameras[camera] @@ -275,35 +305,38 @@ class PtzAutoTracker: tilt = (0.5 - (obj.obj_data["centroid"][1] / camera_height)) * 2 # ideas: check object velocity for camera speed? - self._enqueue_move(camera, obj.obj_data["frame_time"], pan, tilt, 0) + if ( + camera_config.onvif.autotracking.zooming + and camera_config.onvif.autotracking.zoom_relative + ): + zoom = obj.obj_data["area"] / (camera_width * camera_height) + + # test if we need to zoom out + if not self._should_zoom_in(obj, camera): + zoom = -(1 - zoom) + + self._enqueue_move( + camera, + obj.obj_data["frame_time"], + pan, + tilt, + zoom, + ) + else: + self._enqueue_move(camera, obj.obj_data["frame_time"], pan, tilt, 0) def _autotrack_zoom_ptz(self, camera, obj): camera_config = self.config.cameras[camera] - if camera_config.onvif.autotracking.zooming: - camera_width = camera_config.frame_shape[1] - camera_height = camera_config.frame_shape[0] - camera_area = camera_width * camera_height - - bb_left, bb_top, bb_right, bb_bottom = obj.obj_data["box"] - + if ( + camera_config.onvif.autotracking.zooming + and not camera_config.onvif.autotracking.zoom_relative + ): + # absolute zooming zoom_level = self.ptz_metrics[camera]["ptz_zoom_level"].value - # ensure zooming level is in range - # if so, check if bounding box is 10% of an edge - # if so, try zooming in, otherwise try zooming out - # should we make these configurable? - edge_threshold = 0.1 - area_threshold = 0.6 - if 0 < zoom_level <= 1: - if ( - bb_left > edge_threshold * camera_width - and bb_right < (1 - edge_threshold) * camera_width - and bb_top > edge_threshold * camera_height - and bb_bottom < (1 - edge_threshold) * camera_height - and obj.obj_data["area"] < area_threshold * camera_area - ): + if self._should_zoom_in(obj, camera): zoom = min(1.0, zoom_level + 0.1) else: zoom = max(0.0, zoom_level - 0.1) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 4cb01b41c..242104859 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -95,6 +95,7 @@ class OnvifController: ) # autoracking relative panning/tilting needs a relative zoom value set to 0 + # if camera supports relative movement if self.config.cameras[camera_name].onvif.autotracking.zooming: zoom_space_id = next( ( @@ -131,7 +132,13 @@ class OnvifController: "RelativeZoomTranslationSpace" ][0]["URI"] except Exception: - pass + if self.config.cameras[camera_name].onvif.autotracking.zoom_relative: + self.config.cameras[ + camera_name + ].onvif.autotracking.zoom_relative = False + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported" + ) if move_request.Speed is None: move_request.Speed = ptz.GetStatus({"ProfileToken": profile.token}).Position @@ -173,7 +180,6 @@ class OnvifController: if ptz_config.Spaces and ptz_config.Spaces.RelativeZoomTranslationSpace: supported_features.append("zoom-r") - # autotracker uses absolute zooming if ptz_config.Spaces and ptz_config.Spaces.AbsoluteZoomPositionSpace: supported_features.append("zoom-a") try: @@ -245,7 +251,7 @@ class OnvifController: onvif.get_service("ptz").ContinuousMove(move_request) - def _move_relative(self, camera_name: str, pan, tilt, speed) -> None: + def _move_relative(self, camera_name: str, pan, tilt, zoom, speed) -> None: if "pt-r-fov" not in self.cams[camera_name]["features"]: logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).") return @@ -298,7 +304,14 @@ class OnvifController: move_request.Translation.PanTilt.y = tilt if "zoom-r" in self.cams[camera_name]["features"]: - move_request.Translation.Zoom.x = 0 + move_request.Speed = { + "PanTilt": { + "x": speed, + "y": speed, + }, + "Zoom": {"x": speed}, + } + move_request.Translation.Zoom.x = zoom onvif.get_service("ptz").RelativeMove(move_request) @@ -306,6 +319,9 @@ class OnvifController: move_request.Translation.PanTilt.x = 0 move_request.Translation.PanTilt.y = 0 + if "zoom-r" in self.cams[camera_name]["features"]: + move_request.Translation.Zoom.x = 0 + self.cams[camera_name]["active"] = False def _move_to_preset(self, camera_name: str, preset: str) -> None: @@ -465,7 +481,7 @@ class OnvifController: self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0 if self.config.cameras[camera_name].onvif.autotracking.zooming: - # store 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( round(status.Position.Zoom.x, 2), [0, 1],