From d7ee3c566576d6e64dd664113e95862f198f9f78 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 20 Jul 2022 20:48:42 +0200 Subject: [PATCH 001/109] add function to_360_degree --- simplebgc/units.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simplebgc/units.py b/simplebgc/units.py index 03f0885..a56a29f 100644 --- a/simplebgc/units.py +++ b/simplebgc/units.py @@ -12,6 +12,10 @@ def to_degree(angle: float) -> float: return angle * degree_factor +def to_360_degree(angle: float) -> float: + return to_degree(angle) % 360 + + def from_degree_per_sec(degree_per_sec: float) -> int: return int(degree_per_sec / degree_per_sec_factor) From 5aef63bea9edb0c8b020cc356fb84cf23b50516a Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 20 Jul 2022 21:23:07 +0200 Subject: [PATCH 002/109] change type of parameters of get_angles_in_cmd from int to float --- tests/robot_cameraman/test_camera_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/robot_cameraman/test_camera_controller.py b/tests/robot_cameraman/test_camera_controller.py index 5f9c1ef..54dde4e 100644 --- a/tests/robot_cameraman/test_camera_controller.py +++ b/tests/robot_cameraman/test_camera_controller.py @@ -12,8 +12,8 @@ from simplebgc.units import from_degree, from_degree_per_sec -def get_angles_in_cmd(pan_angle: int, pan_speed: int, - tilt_angle: int, tilt_speed: int): +def get_angles_in_cmd(pan_angle: float, pan_speed: float, + tilt_angle: float, tilt_speed: float): return GetAnglesInCmd( imu_angle_1=0, target_angle_1=0, From 9f2af26f4d94dad34195130d27b03cae374e76d8 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 20 Jul 2022 21:24:25 +0200 Subject: [PATCH 003/109] add CameraAngleLimitController --- robot_cameraman/camera_controller.py | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/camera_controller.py b/robot_cameraman/camera_controller.py index 802d09f..04e4e4b 100644 --- a/robot_cameraman/camera_controller.py +++ b/robot_cameraman/camera_controller.py @@ -18,7 +18,7 @@ from robot_cameraman.tracking import CameraSpeeds, ZoomSpeed from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import ControlMode -from simplebgc.units import to_degree, to_degree_per_sec +from simplebgc.units import to_degree, to_degree_per_sec, to_360_degree logger: Logger = logging.getLogger(__name__) @@ -183,6 +183,105 @@ def stop(self, camera_speeds: CameraSpeeds) -> None: self.update(camera_speeds) +class CameraAngleLimitController: + min_pan_angle: Optional[float] + max_pan_angle: Optional[float] + min_tilt_angle: Optional[float] + max_tilt_angle: Optional[float] + + def __init__(self, gimbal: Gimbal) -> None: + super().__init__() + self._gimbal = gimbal + self.min_pan_angle = None + self.max_pan_angle = None + self.min_tilt_angle = None + self.max_tilt_angle = None + # TODO keep None as default, i.e. remove the following lines + # self.min_pan_angle = 2.0 + # self.max_pan_angle = 40.0 + # self.min_tilt_angle = 350.0 + # self.max_tilt_angle = 5.0 + + # TODO remove if not used + def _get_angle_limits(self): + return ( + self.min_pan_angle, + self.max_pan_angle, + self.min_tilt_angle, + self.max_tilt_angle, + ) + + def update(self, camera_speeds: CameraSpeeds) -> None: + # TODO uncomment + # if all(map(lambda a: a is None, self._get_angle_limits())): + # return + # TODO get angles in main loop to only request them once from gimbal + # and not in every controller/component that requires them + angles = self._gimbal.get_angles() + pan_360_angle = to_360_degree(angles.target_angle_3) + tilt_360_angle = to_360_degree(angles.target_angle_2) + logger.debug(', '.join(( + f"pan angle: {pan_360_angle:4.1f}", + f"tilt angle: {tilt_360_angle:4.1f}", + ))) + + # Since camera might rotate full circle, a single limit could be + # reached rotating clockwise or counter clockwise. It is unrealistic + # that the exact limit is reached and kept. Thereby, the camera might + # pass over the limit. If there is only one limit, + # the camera may continue to move after it passed the limit, + # since the limit is in the opposite rotating direction than the camera + # is moving. + # + # Minimum and maximum have to be defined both. + # They define a range, in which the camera stops, + # i.e. the camera stops after a limit is passed. + # The camera may only move to the closer limit to exit this range. + if (self.min_pan_angle is not None + and self.max_pan_angle is not None + and is_angle_between( + left=self.min_pan_angle, + angle=pan_360_angle, + right=self.max_pan_angle, + clockwise=False)): + min_pan_delta = get_delta_angle_clockwise( + left=pan_360_angle, right=self.min_pan_angle) + max_pan_delta = get_delta_angle_counter_clockwise( + left=pan_360_angle, right=self.max_pan_angle) + if min_pan_delta < max_pan_delta: + if camera_speeds.pan_speed < 0: + logger.debug( + 'min pan angle reached, pan speed is set to 0') + camera_speeds.pan_speed = 0 + else: + if camera_speeds.pan_speed > 0: + logger.debug( + 'max pan angle reached, pan speed is set to 0') + camera_speeds.pan_speed = 0 + + if (self.min_tilt_angle is not None + and self.max_tilt_angle is not None + and is_angle_between( + left=self.min_tilt_angle, + angle=tilt_360_angle, + right=self.max_tilt_angle, + clockwise=False)): + min_tilt_delta = get_delta_angle_clockwise( + left=tilt_360_angle, right=self.min_tilt_angle) + max_tilt_delta = get_delta_angle_counter_clockwise( + left=tilt_360_angle, right=self.max_tilt_angle) + if min_tilt_delta < max_tilt_delta: + if camera_speeds.tilt_speed < 0: + logger.debug( + 'min tilt angle reached, tilt speed is set to 0') + camera_speeds.tilt_speed = 0 + else: + if camera_speeds.tilt_speed > 0: + logger.debug( + 'max tilt angle reached, tilt speed is set to 0') + camera_speeds.tilt_speed = 0 + + @dataclass() class PointOfMotion: pan_angle: float = 0 From af74a98768932a1088b994947cc3a92485b3feca Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 20 Jul 2022 21:24:41 +0200 Subject: [PATCH 004/109] test CameraAngleLimitController --- .../robot_cameraman/test_camera_controller.py | 140 +++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/tests/robot_cameraman/test_camera_controller.py b/tests/robot_cameraman/test_camera_controller.py index 54dde4e..fa95145 100644 --- a/tests/robot_cameraman/test_camera_controller.py +++ b/tests/robot_cameraman/test_camera_controller.py @@ -5,7 +5,7 @@ from robot_cameraman.camera_controller import \ BaseCamPathOfMotionCameraController, PointOfMotion, SpeedManager, \ ElapsedTime, CameraState, PointOfMotionTargetSpeedCalculator, \ - is_current_point_reached, is_angle_between + is_current_point_reached, is_angle_between, CameraAngleLimitController from robot_cameraman.tracking import CameraSpeeds from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import Gimbal, ControlMode @@ -839,3 +839,141 @@ def test_is_angle_between_counter_clockwise_overstep_360(self): assert is_angle_between(left=120, angle=110, right=300, clockwise=False) assert not is_angle_between( left=120, angle=180, right=300, clockwise=False) + + +class TestCameraAngleLimitController: + @pytest.fixture() + def gimbal(self): + # mock serial connection to avoid error, because port can not be opened + return Mock(spec=Gimbal(Mock())) + + @pytest.fixture() + def controller(self, gimbal): + return CameraAngleLimitController(gimbal) + + def test_stop_panning_forward_when_max_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=15.1, pan_speed=42, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=42) + controller.min_pan_angle = 0 + controller.max_pan_angle = 15.0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + + def test_allow_panning_back_when_max_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=15.1, pan_speed=0, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=-42) + controller.min_pan_angle = 0 + controller.max_pan_angle = 15.0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == -42 + + def test_stop_panning_forward_when_min_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=344.9, pan_speed=-42, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=-42) + controller.min_pan_angle = 345.0 + controller.max_pan_angle = 0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + + def test_allow_panning_back_when_min_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=344.9, pan_speed=0, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=42) + controller.min_pan_angle = 345.0 + controller.max_pan_angle = 0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == 42 + + def test_allow_panning_when_no_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=350, pan_speed=30, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=42) + controller.min_pan_angle = 345.0 + controller.max_pan_angle = 0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == 42 + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=350, pan_speed=-30, + tilt_angle=0, tilt_speed=0)) + camera_speeds = CameraSpeeds(pan_speed=-42) + controller.min_pan_angle = 345.0 + controller.max_pan_angle = 0 + controller.update(camera_speeds) + assert camera_speeds.pan_speed == -42 + + def test_stop_tilting_forward_when_max_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=5.1, tilt_speed=42)) + camera_speeds = CameraSpeeds(tilt_speed=42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == 0 + + def test_allow_tilting_back_when_max_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=5.1, tilt_speed=0)) + camera_speeds = CameraSpeeds(tilt_speed=-42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == -42 + + def test_stop_tilting_forward_when_min_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=349.9, tilt_speed=-42)) + camera_speeds = CameraSpeeds(tilt_speed=-42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == 0 + + def test_allow_tilting_back_when_min_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=349.9, tilt_speed=42)) + camera_speeds = CameraSpeeds(tilt_speed=42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == 42 + + def test_allow_tilting_when_no_limit_is_reached( + self, controller, gimbal): + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=1.0, tilt_speed=42)) + camera_speeds = CameraSpeeds(tilt_speed=42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == 42 + gimbal.get_angles = Mock( + return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, + tilt_angle=1.0, tilt_speed=-30)) + camera_speeds = CameraSpeeds(tilt_speed=-42) + controller.min_tilt_angle = 350.0 + controller.max_tilt_angle = 5.0 + controller.update(camera_speeds) + assert camera_speeds.tilt_speed == -42 + From a4e89858f0b33247a4d948a20294c3298416459e Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 20 Jul 2022 21:26:06 +0200 Subject: [PATCH 005/109] limit angles for all modes except "angle" --- robot_cameraman/__main__.py | 3 ++- robot_cameraman/cameraman_mode_manager.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index c7956ee..584c384 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -13,7 +13,7 @@ from panasonic_camera.camera_manager import PanasonicCameraManager from robot_cameraman.annotation import ImageAnnotator from robot_cameraman.camera_controller import SmoothCameraController, \ - SpeedManager + SpeedManager, CameraAngleLimitController from robot_cameraman.camera_observable import \ PanasonicCameraObservable, ObservableCameraProperty from robot_cameraman.cameraman import Cameraman @@ -277,6 +277,7 @@ def configure_logging(): camera_manager, rotate_speed_manager=rotate_speed_manager, tilt_speed_manager=tilt_speed_manager), + camera_angle_limit_controller=CameraAngleLimitController(gimbal=gimbal), align_tracking_strategy=max_speed_and_acceleration_updater.add( configurable_align_tracking_strategy), tracking_strategy=tracking_strategy, diff --git a/robot_cameraman/cameraman_mode_manager.py b/robot_cameraman/cameraman_mode_manager.py index 4f2799a..d8c506c 100644 --- a/robot_cameraman/cameraman_mode_manager.py +++ b/robot_cameraman/cameraman_mode_manager.py @@ -3,7 +3,8 @@ from typing import Optional from robot_cameraman.box import Box -from robot_cameraman.camera_controller import CameraController +from robot_cameraman.camera_controller import CameraController, \ + CameraAngleLimitController from robot_cameraman.gimbal import Gimbal from robot_cameraman.tracking import TrackingStrategy, CameraSpeeds, \ AlignTrackingStrategy, SearchTargetStrategy, ZoomSpeed @@ -17,11 +18,13 @@ class CameramanModeManager: def __init__( self, camera_controller: CameraController, + camera_angle_limit_controller: CameraAngleLimitController, align_tracking_strategy: AlignTrackingStrategy, tracking_strategy: TrackingStrategy, search_target_strategy: SearchTargetStrategy, gimbal: Gimbal) -> None: self._camera_controller = camera_controller + self._camera_angle_limit_controller = camera_angle_limit_controller self._align_tracking_strategy = align_tracking_strategy self._tracking_strategy = tracking_strategy self._search_target_strategy = search_target_strategy @@ -52,6 +55,10 @@ def update(self, target: Optional[Box], is_target_lost: bool) -> None: if self.mode_name != 'angle': if not self.is_zoom_enabled and self.mode_name != 'manual': self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED + # TODO add option to enable limit in manual mode + # if self.mode_name != 'manual': + self._camera_angle_limit_controller.update(self._camera_speeds) + # TODO limit zoom self._camera_controller.update(self._camera_speeds) def start(self): From e27d5db670a7eafa341e5831c8ba0636663b3483 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 24 Jul 2022 11:30:02 +0200 Subject: [PATCH 006/109] manage angle limits in UpdatableConfiguration --- robot_cameraman/__main__.py | 4 +++- robot_cameraman/updatable_configuration.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 584c384..78a20c2 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -271,13 +271,14 @@ def configure_logging(): configurable_align_tracking_strategy = \ ConfigurableAlignTrackingStrategy( destination, live_view_image_size, max_allowed_speed=16) +camera_angle_limit_controller = CameraAngleLimitController(gimbal=gimbal) cameraman_mode_manager = CameramanModeManager( camera_controller=SmoothCameraController( gimbal, camera_manager, rotate_speed_manager=rotate_speed_manager, tilt_speed_manager=tilt_speed_manager), - camera_angle_limit_controller=CameraAngleLimitController(gimbal=gimbal), + camera_angle_limit_controller=camera_angle_limit_controller, align_tracking_strategy=max_speed_and_acceleration_updater.add( configurable_align_tracking_strategy), tracking_strategy=tracking_strategy, @@ -365,6 +366,7 @@ def configure_logging(): _manual_camera_speeds=manual_camera_speeds, _updatable_configuration=UpdatableConfiguration( detection_engine=detection_engine, + camera_angle_limit_controller=camera_angle_limit_controller, configuration_file=args.config), ssl_certificate=args.ssl_certificate, ssl_key=args.ssl_key) diff --git a/robot_cameraman/updatable_configuration.py b/robot_cameraman/updatable_configuration.py index edfe18e..6aeba70 100644 --- a/robot_cameraman/updatable_configuration.py +++ b/robot_cameraman/updatable_configuration.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Optional, List +from robot_cameraman.camera_controller import CameraAngleLimitController from robot_cameraman.configuration import read_configuration_file from robot_cameraman.detection_engine.color import ColorDetectionEngine from robot_cameraman.image_detection import DetectionEngine @@ -10,10 +11,17 @@ class UpdatableConfiguration: def __init__( self, detection_engine: DetectionEngine, + camera_angle_limit_controller: CameraAngleLimitController, configuration_file: Optional[Path] = None): self.detection_engine = detection_engine + self.camera_angle_limit_controller = camera_angle_limit_controller self.configuration_file = configuration_file self.configuration = read_configuration_file(configuration_file) + if 'limits' not in self.configuration: + self.configuration['limits'] = { + 'pan': None, + 'tilt': None, + } def update_tracking_color( self, @@ -26,3 +34,17 @@ def update_tracking_color( if max_hsv is not None: self.detection_engine.max_hsv[:] = max_hsv self.configuration['tracking']['color']['max_hsv'] = max_hsv + + def update_limits(self, limits): + if 'pan' in limits: + pan_limit = limits['pan'] + minimum, maximum = (None, None) if pan_limit is None else pan_limit + self.camera_angle_limit_controller.min_pan_angle = minimum + self.camera_angle_limit_controller.max_pan_angle = maximum + self.configuration['limits']['pan'] = pan_limit + if 'tilt' in limits: + tilt_limit = limits['tilt'] + minimum, maximum = (None, None) if tilt_limit is None else tilt_limit + self.camera_angle_limit_controller.min_tilt_angle = minimum + self.camera_angle_limit_controller.max_tilt_angle = maximum + self.configuration['limits']['tilt'] = tilt_limit From e22cc9d8d0ad86a42773910a1cc99285fcf389e4 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 24 Jul 2022 11:31:40 +0200 Subject: [PATCH 007/109] update limits configuration on request PUT /api/configuration --- robot_cameraman/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/robot_cameraman/server.py b/robot_cameraman/server.py index 92395ff..9fe5a14 100644 --- a/robot_cameraman/server.py +++ b/robot_cameraman/server.py @@ -145,6 +145,8 @@ def update_configuration(): if 'max_hsv' in color: updatable_configuration.update_tracking_color( max_hsv=color['max_hsv']) + if 'limits' in request.json: + updatable_configuration.update_limits(request.json['limits']) return '', 200 From 4a932272ff2e433b3728f7f459ca619ef9485358 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 24 Jul 2022 11:32:59 +0200 Subject: [PATCH 008/109] add menu "limits" to web UI to configure angle limits --- .idea/inspectionProfiles/Project_Default.xml | 3 +- .../server_static_folder/index.html | 10 + .../server_static_folder/limits-menu.js | 188 ++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 robot_cameraman/server_static_folder/limits-menu.js diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e42a6f5..e538064 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -15,7 +15,7 @@ diff --git a/robot_cameraman/server_static_folder/index.html b/robot_cameraman/server_static_folder/index.html index 3db7a80..be55049 100644 --- a/robot_cameraman/server_static_folder/index.html +++ b/robot_cameraman/server_static_folder/index.html @@ -148,6 +148,7 @@ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"> +
@@ -188,6 +189,11 @@ overlay
+ + + diff --git a/robot_cameraman/ui.py b/robot_cameraman/ui.py index 4cca801..5e99785 100644 --- a/robot_cameraman/ui.py +++ b/robot_cameraman/ui.py @@ -46,6 +46,7 @@ def on_change(value, _user_data): class StatusBar(UserInterface): + text: str _pan_speed_manager: SpeedManager _tilt_speed_manager: SpeedManager _camera_speeds: CameraSpeeds @@ -57,6 +58,7 @@ def __init__( pan_speed_manager: SpeedManager, tilt_speed_manager: SpeedManager, camera_speeds: CameraSpeeds): + self.text = '' self._pan_speed_manager = pan_speed_manager self._tilt_speed_manager = tilt_speed_manager self._camera_speeds = camera_speeds @@ -86,10 +88,9 @@ def update(self) -> None: else f'{self._zoom_ratio:4.1f}') zoom_index = ('?' if self._zoom_index is None else f'{self._zoom_index:2}') - cv2.displayStatusBar( - 'Robot Cameraman', - f"pan: {pan_speed :3.2}, " - f"tilt: {tilt_speed :3.2}, " - f"zoom-ratio: {zoom_ratio}, " - f"zoom-index: {zoom_index}, " - f"{zoom_speed_str}") + self.text = f"pan: {pan_speed :3.2}, " \ + f"tilt: {tilt_speed :3.2}, " \ + f"zoom-ratio: {zoom_ratio}, " \ + f"zoom-index: {zoom_index}, " \ + f"{zoom_speed_str}" + cv2.displayStatusBar('Robot Cameraman', self.text) From b5cbddd89bfd2b5e3d9ee152c02dc259ab7df765 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 23 Oct 2022 18:36:56 +0200 Subject: [PATCH 070/109] feat: show debugLog (status bar) between live-view and menu in portrait orientation --- robot_cameraman/server_static_folder/index.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/robot_cameraman/server_static_folder/index.html b/robot_cameraman/server_static_folder/index.html index ac3cccb..6573288 100644 --- a/robot_cameraman/server_static_folder/index.html +++ b/robot_cameraman/server_static_folder/index.html @@ -99,11 +99,15 @@ } @media (orientation: portrait) { + #debugLog { + grid-row: 2; + } + .menu, .buttons { - /* show buttons below live view */ - grid-row: 2; - margin-top: 0.1em; + /* show buttons below debugLog and live view */ + grid-row: 3; + margin-top: 0.2em; } .buttons .grid-item { From c1beeea87fe4f82e42eb5844df74782d92719b03 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Fri, 28 Oct 2022 20:24:37 +0200 Subject: [PATCH 071/109] move `ZoomSpeed` and `CameraSpeeds` to module `robot_cameraman.camera_speeds` --- robot_cameraman/__main__.py | 5 ++- robot_cameraman/camera_controller.py | 2 +- robot_cameraman/camera_speeds.py | 38 ++++++++++++++++++ robot_cameraman/cameraman.py | 3 +- robot_cameraman/cameraman_mode_manager.py | 4 +- .../max_speed_and_acceleration_updater.py | 3 +- robot_cameraman/server.py | 2 +- robot_cameraman/tracking.py | 40 +------------------ robot_cameraman/ui.py | 2 +- .../robot_cameraman/test_camera_controller.py | 2 +- 10 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 robot_cameraman/camera_speeds.py diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 384276a..4d0d8c0 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -36,8 +36,9 @@ from robot_cameraman.resource import read_label_file from robot_cameraman.server import run_server, ImageContainer from robot_cameraman.tracking import Destination, StopIfLostTrackingStrategy, \ - RotateSearchTargetStrategy, CameraSpeeds, ConfigurableTrackingStrategy, \ - ConfigurableAlignTrackingStrategy, ConfigurableTrackingStrategyUi, ZoomSpeed + RotateSearchTargetStrategy, ConfigurableTrackingStrategy, \ + ConfigurableAlignTrackingStrategy, ConfigurableTrackingStrategyUi +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.ui import StatusBar from robot_cameraman.updatable_configuration import UpdatableConfiguration from robot_cameraman.zoom import parse_zoom_steps, parse_zoom_ratio_index_ranges diff --git a/robot_cameraman/camera_controller.py b/robot_cameraman/camera_controller.py index fa55d9a..cf8cacf 100644 --- a/robot_cameraman/camera_controller.py +++ b/robot_cameraman/camera_controller.py @@ -15,7 +15,7 @@ from robot_cameraman.angle import get_delta_angle_clockwise, \ get_delta_angle_counter_clockwise from robot_cameraman.gimbal import Gimbal -from robot_cameraman.tracking import CameraSpeeds, ZoomSpeed +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.zoom import ZoomSteps, ZoomStep from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import ControlMode diff --git a/robot_cameraman/camera_speeds.py b/robot_cameraman/camera_speeds.py new file mode 100644 index 0000000..26e6db7 --- /dev/null +++ b/robot_cameraman/camera_speeds.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from enum import IntEnum + + +class ZoomSpeed(IntEnum): + ZOOM_OUT_FAST = -200 + ZOOM_OUT_SLOW = -100 + ZOOM_STOPPED = 0 + ZOOM_IN_SLOW = 100 + ZOOM_IN_FAST = 200 + + +@dataclass +class CameraSpeeds: + pan_speed: float = 0 + """Speed in degree per second. Positive values mean clockwise, + negative values stand for counter clockwise moving direction from the + camera's point of view. + """ + + tilt_speed: float = 0 + """Speed in degree per second. Positive values mean upwards, + negative values stand for downwards moving direction from the camera's + point of view. + """ + + zoom_speed: ZoomSpeed = ZoomSpeed.ZOOM_STOPPED + """Abstract speed unit, i.e. the actual speed depends on camera model. + Positive values mean camera should zoom in (larger values mean that camera + should zoom faster), negative values stand for zooming out. + """ + + def reset(self) -> None: + """Stop camera movements. + """ + self.pan_speed = 0 + self.tilt_speed = 0 + self.zoom_speed = ZoomSpeed.ZOOM_STOPPED diff --git a/robot_cameraman/cameraman.py b/robot_cameraman/cameraman.py index d9be2ae..b5a7d6d 100644 --- a/robot_cameraman/cameraman.py +++ b/robot_cameraman/cameraman.py @@ -23,7 +23,8 @@ from robot_cameraman.live_view import LiveView, ImageSize from robot_cameraman.object_tracking import ObjectTracker from robot_cameraman.server import ImageContainer, ServerImageSource -from robot_cameraman.tracking import Destination, CameraSpeeds, ZoomSpeed +from robot_cameraman.tracking import Destination +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.ui import UserInterface, create_attribute_checkbox logger: Logger = logging.getLogger(__name__) diff --git a/robot_cameraman/cameraman_mode_manager.py b/robot_cameraman/cameraman_mode_manager.py index 95ff952..22829fe 100644 --- a/robot_cameraman/cameraman_mode_manager.py +++ b/robot_cameraman/cameraman_mode_manager.py @@ -6,8 +6,8 @@ from robot_cameraman.camera_controller import CameraController, \ CameraAngleLimitController, CameraZoomLimitController from robot_cameraman.gimbal import Gimbal -from robot_cameraman.tracking import TrackingStrategy, CameraSpeeds, \ - AlignTrackingStrategy, SearchTargetStrategy, ZoomSpeed +from robot_cameraman.tracking import TrackingStrategy, AlignTrackingStrategy, SearchTargetStrategy +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from simplebgc.gimbal import ControlMode logger: Logger = logging.getLogger(__name__) diff --git a/robot_cameraman/max_speed_and_acceleration_updater.py b/robot_cameraman/max_speed_and_acceleration_updater.py index d92dff0..633303f 100644 --- a/robot_cameraman/max_speed_and_acceleration_updater.py +++ b/robot_cameraman/max_speed_and_acceleration_updater.py @@ -3,7 +3,8 @@ from robot_cameraman.camera_controller import SpeedManager from robot_cameraman.tracking import SimpleTrackingStrategy, \ - RotateSearchTargetStrategy, CameraSpeeds + RotateSearchTargetStrategy +from robot_cameraman.camera_speeds import CameraSpeeds Updatable = TypeVar( 'Updatable', diff --git a/robot_cameraman/server.py b/robot_cameraman/server.py index ede3574..b1e069b 100644 --- a/robot_cameraman/server.py +++ b/robot_cameraman/server.py @@ -10,7 +10,7 @@ from flask import Flask, Response, request, redirect, jsonify from robot_cameraman.cameraman_mode_manager import CameramanModeManager -from robot_cameraman.tracking import ZoomSpeed, CameraSpeeds +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.ui import StatusBar from robot_cameraman.updatable_configuration import UpdatableConfiguration diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 399f112..6b3c95d 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -1,14 +1,14 @@ import logging import time from abc import abstractmethod -from dataclasses import dataclass -from enum import Enum, auto, IntEnum +from enum import Enum, auto from logging import Logger from typing import Optional from typing_extensions import Protocol from robot_cameraman.box import Box, TwoPointsBox, Point +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.live_view import ImageSize logger: Logger = logging.getLogger(__name__) @@ -44,42 +44,6 @@ def update_size_box_center(self, x: float, y: float): self.min_size_box.center.set(x, y) -class ZoomSpeed(IntEnum): - ZOOM_OUT_FAST = -200 - ZOOM_OUT_SLOW = -100 - ZOOM_STOPPED = 0 - ZOOM_IN_SLOW = 100 - ZOOM_IN_FAST = 200 - - -@dataclass -class CameraSpeeds: - pan_speed: float = 0 - """Speed in degree per second. Positive values mean clockwise, - negative values stand for counter clockwise moving direction from the - camera's point of view. - """ - - tilt_speed: float = 0 - """Speed in degree per second. Positive values mean upwards, - negative values stand for downwards moving direction from the camera's - point of view. - """ - - zoom_speed: ZoomSpeed = ZoomSpeed.ZOOM_STOPPED - """Abstract speed unit, i.e. the actual speed depends on camera model. - Positive values mean camera should zoom in (larger values mean that camera - should zoom faster), negative values stand for zooming out. - """ - - def reset(self) -> None: - """Stop camera movements. - """ - self.pan_speed = 0 - self.tilt_speed = 0 - self.zoom_speed = ZoomSpeed.ZOOM_STOPPED - - class TrackingStrategy(Protocol): @abstractmethod def update( diff --git a/robot_cameraman/ui.py b/robot_cameraman/ui.py index 5e99785..a4db6bd 100644 --- a/robot_cameraman/ui.py +++ b/robot_cameraman/ui.py @@ -6,7 +6,7 @@ from typing_extensions import Protocol from robot_cameraman.camera_controller import SpeedManager -from robot_cameraman.tracking import CameraSpeeds, ZoomSpeed +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds logger: Logger = logging.getLogger(__name__) diff --git a/tests/robot_cameraman/test_camera_controller.py b/tests/robot_cameraman/test_camera_controller.py index fa95145..23fe273 100644 --- a/tests/robot_cameraman/test_camera_controller.py +++ b/tests/robot_cameraman/test_camera_controller.py @@ -6,7 +6,7 @@ BaseCamPathOfMotionCameraController, PointOfMotion, SpeedManager, \ ElapsedTime, CameraState, PointOfMotionTargetSpeedCalculator, \ is_current_point_reached, is_angle_between, CameraAngleLimitController -from robot_cameraman.tracking import CameraSpeeds +from robot_cameraman.camera_speeds import CameraSpeeds from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import Gimbal, ControlMode from simplebgc.units import from_degree, from_degree_per_sec From 9fd3ac934951897810b6350a07990fc6a74678b5 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Fri, 28 Oct 2022 20:25:08 +0200 Subject: [PATCH 072/109] add TODOs to CameraSpeeds --- robot_cameraman/camera_speeds.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/robot_cameraman/camera_speeds.py b/robot_cameraman/camera_speeds.py index 26e6db7..4b29fef 100644 --- a/robot_cameraman/camera_speeds.py +++ b/robot_cameraman/camera_speeds.py @@ -12,6 +12,11 @@ class ZoomSpeed(IntEnum): @dataclass class CameraSpeeds: + # TODO add angles and zoom ratio rename dataclass to CameraState + # TODO use protocols to separate concerns (minimize interfaces), + # e.g. AngleSpeeds, Angles + + # TODO should be named target speed pan_speed: float = 0 """Speed in degree per second. Positive values mean clockwise, negative values stand for counter clockwise moving direction from the From 18c75dc4f42cef799ecd93bb16373c8e962ee06e Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 29 Oct 2022 14:41:16 +0200 Subject: [PATCH 073/109] feat: add EventEmitter --- robot_cameraman/events.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 robot_cameraman/events.py diff --git a/robot_cameraman/events.py b/robot_cameraman/events.py new file mode 100644 index 0000000..ab72719 --- /dev/null +++ b/robot_cameraman/events.py @@ -0,0 +1,30 @@ +from enum import Enum, auto +from typing import Callable, List, Dict + + +# TODO use StrEnum after switching to Python 3.11 +class Event(Enum): + ANGLES = auto() + """Current angles are emitted after they have been read.""" + + +Listener = Callable + + +class EventEmitter: + _listeners: Dict[Event, List[Listener]] + + def __init__(self) -> None: + self._listeners = dict() + + def add_listener(self, event: Event, listener: Listener): + listeners = self._get_event_listeners(event) + listeners.append(listener) + + def _get_event_listeners(self, event: Event): + return self._listeners.setdefault(event, []) + + def emit(self, event: Event, value): + property_listeners = self._get_event_listeners(event) + for listener in property_listeners: + listener(value) From 7bff7f48d5edad9d1994a544103d68f97e6bdaed Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 29 Oct 2022 14:46:57 +0200 Subject: [PATCH 074/109] feat: generic way to add updatable property to `MaxSpeedAndAccelerationUpdater` --- .../max_speed_and_acceleration_updater.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/max_speed_and_acceleration_updater.py b/robot_cameraman/max_speed_and_acceleration_updater.py index 633303f..2a33284 100644 --- a/robot_cameraman/max_speed_and_acceleration_updater.py +++ b/robot_cameraman/max_speed_and_acceleration_updater.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Tuple, List, TypeVar +from typing import Tuple, List, TypeVar, NamedTuple from robot_cameraman.camera_controller import SpeedManager from robot_cameraman.tracking import SimpleTrackingStrategy, \ @@ -14,6 +14,12 @@ RotateSearchTargetStrategy) +class _UpdateEntry(NamedTuple): + max_speed: float + property_name: str + object: object + + # See https://github.com/maiermic/robot-cameraman/issues/13 class MaxSpeedAndAccelerationUpdater: # the first value is the max speed at zoom ratio 1.0x @@ -23,13 +29,17 @@ class MaxSpeedAndAccelerationUpdater: # the first value is the max speed at zoom ratio 1.0x _simple_tracking_strategy: List[Tuple[float, SimpleTrackingStrategy]] # the first value is the max speed at zoom ratio 1.0x - _rotate_search_target_strategy: List[Tuple[float, RotateSearchTargetStrategy]] + _rotate_search_target_strategy: List[ + Tuple[float, RotateSearchTargetStrategy]] + # TODO remove lists above and move their entries to _entries + _entries: List[_UpdateEntry] def __init__(self): self._camera_speeds = [] self._speed_managers = [] self._simple_tracking_strategy = [] self._rotate_search_target_strategy = [] + self._entries = [] def add(self, updatable: Updatable) -> Updatable: if isinstance(updatable, CameraSpeeds): @@ -46,6 +56,16 @@ def add(self, updatable: Updatable) -> Updatable: (updatable.speed, updatable)) return updatable + def add_updatable_property(self, obj: object, property_name: str): + self._entries.append( + _UpdateEntry(max_speed=getattr(obj, property_name), + property_name=property_name, + object=obj)) + + def add_updatable_properties(self, obj: object, property_names: List[str]): + for property_name in property_names: + self.add_updatable_property(obj, property_name) + def on_zoom_ratio(self, zoom_ratio): for max_speeds, camera_speeds in self._camera_speeds: camera_speeds.pan_speed = max_speeds.pan_speed / zoom_ratio @@ -56,3 +76,5 @@ def on_zoom_ratio(self, zoom_ratio): search_strategy.max_allowed_speed = max_speed / zoom_ratio for max_speed, search_strategy in self._rotate_search_target_strategy: search_strategy.speed = max_speed / zoom_ratio + for e in self._entries: + setattr(e.object, e.property_name, e.max_speed / zoom_ratio) From e1f90a95ad5ff31b09650a610fbfe40cc53edd03 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 29 Oct 2022 14:51:40 +0200 Subject: [PATCH 075/109] feat: add class `Angles` that does not depend on module `simplebgc` --- robot_cameraman/gimbal.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/robot_cameraman/gimbal.py b/robot_cameraman/gimbal.py index 2bac9c3..2660835 100644 --- a/robot_cameraman/gimbal.py +++ b/robot_cameraman/gimbal.py @@ -1,4 +1,5 @@ from abc import abstractmethod +from dataclasses import dataclass from serial import Serial from typing_extensions import Protocol @@ -6,6 +7,16 @@ import simplebgc.gimbal from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import ControlMode +from simplebgc.units import to_360_degree, to_degree_per_sec + + +@dataclass +class Angles: + # TODO document units (360, degree/second) + pan_angle: float + tilt_angle: float + pan_speed: float + tilt_speed: float # TODO interface should be independent from simplebgc module, @@ -47,6 +58,7 @@ def control( def stop(self) -> None: raise NotImplementedError + # TODO should return Angles @abstractmethod def get_angles(self) -> GetAnglesInCmd: raise NotImplementedError @@ -121,3 +133,11 @@ def create_simple_bgc_gimbal(connection: Serial = None) -> Gimbal: # Tilt direction of simplebgc.gimbal.Gimbal has to be inverted, # since it uses the opposite direction than robot_cameraman.gimbal.Gimbal return TiltInvertedGimbal(simplebgc.gimbal.Gimbal(connection)) + + +def convert_simple_bgc_angles(angles: GetAnglesInCmd) -> Angles: + return Angles( + pan_angle=to_360_degree(angles.target_angle_3), + tilt_angle=to_360_degree(angles.target_angle_2), + pan_speed=to_degree_per_sec(angles.target_speed_3), + tilt_speed=to_degree_per_sec(angles.target_speed_2)) From c63536f7a18628842e68322d4e71d3f4e65f1def Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 29 Oct 2022 15:05:18 +0200 Subject: [PATCH 076/109] feat: read angles of gimbal in `CameramanModeManager` instead of `CameraAngleLimitController` --- robot_cameraman/__main__.py | 15 ++- robot_cameraman/camera_controller.py | 40 ++++--- robot_cameraman/cameraman_mode_manager.py | 17 ++- .../robot_cameraman/test_camera_controller.py | 103 +++++++++--------- 4 files changed, 99 insertions(+), 76 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 4d0d8c0..281a257 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -24,6 +24,7 @@ from robot_cameraman.configuration import read_configuration_file from robot_cameraman.detection_engine.color import ColorDetectionEngine, \ ColorDetectionEngineUI +from robot_cameraman.events import EventEmitter, Event from robot_cameraman.gimbal import DummyGimbal, TiltInvertedGimbal, \ create_simple_bgc_gimbal from robot_cameraman.image_detection import DummyDetectionEngine, \ @@ -55,6 +56,14 @@ def create_video_writer(output_file: Path, image_size: ImageSize): image_size) +def create_angle_limit_controller( + event_emitter: EventEmitter) -> CameraAngleLimitController: + controller = CameraAngleLimitController() + event_emitter.add_listener(Event.ANGLES, + controller.update_current_angles) + return controller + + class RobotCameramanArguments(Protocol): config: Path detectionEngine: str @@ -272,6 +281,7 @@ def configure_logging(): args = parse_arguments() configure_logging() configuration = read_configuration_file(args.config) +event_emitter = EventEmitter() labels = read_label_file(args.labels) font = PIL.ImageFont.truetype(str(args.font), args.fontSize) live_view_image_size = ImageSize(args.liveViewWith, args.liveViewHeight) @@ -309,7 +319,7 @@ def configure_logging(): else: camera_zoom_limit_controller = CameraZoomRatioLimitController() -camera_angle_limit_controller = CameraAngleLimitController(gimbal=gimbal) +camera_angle_limit_controller = create_angle_limit_controller(event_emitter) cameraman_mode_manager = CameramanModeManager( camera_controller=SmoothCameraController( gimbal, @@ -323,7 +333,8 @@ def configure_logging(): tracking_strategy=tracking_strategy, search_target_strategy=max_speed_and_acceleration_updater.add( RotateSearchTargetStrategy(args.rotatingSearchSpeed)), - gimbal=gimbal) + gimbal=gimbal, + event_emitter=event_emitter) # noinspection PyListCreation user_interfaces = [] diff --git a/robot_cameraman/camera_controller.py b/robot_cameraman/camera_controller.py index cf8cacf..54aba6e 100644 --- a/robot_cameraman/camera_controller.py +++ b/robot_cameraman/camera_controller.py @@ -14,12 +14,12 @@ from panasonic_camera.camera_manager import PanasonicCameraManager from robot_cameraman.angle import get_delta_angle_clockwise, \ get_delta_angle_counter_clockwise -from robot_cameraman.gimbal import Gimbal from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds +from robot_cameraman.gimbal import Gimbal, Angles from robot_cameraman.zoom import ZoomSteps, ZoomStep from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import ControlMode -from simplebgc.units import to_degree, to_degree_per_sec, to_360_degree +from simplebgc.units import to_degree, to_degree_per_sec logger: Logger = logging.getLogger(__name__) @@ -345,10 +345,11 @@ class CameraAngleLimitController: max_pan_angle: Optional[float] min_tilt_angle: Optional[float] max_tilt_angle: Optional[float] + _current_pan_angle: Optional[float] + _current_tilt_angle: Optional[float] - def __init__(self, gimbal: Gimbal) -> None: + def __init__(self) -> None: super().__init__() - self._gimbal = gimbal self.min_pan_angle = None self.max_pan_angle = None self.min_tilt_angle = None @@ -358,6 +359,12 @@ def __init__(self, gimbal: Gimbal) -> None: # self.max_pan_angle = 40.0 # self.min_tilt_angle = 350.0 # self.max_tilt_angle = 5.0 + self._current_pan_angle = None + self._current_tilt_angle = None + + def update_current_angles(self, angles: Angles): + self._current_pan_angle = angles.pan_angle + self._current_tilt_angle = angles.tilt_angle # TODO remove if not used def _get_angle_limits(self): @@ -369,17 +376,16 @@ def _get_angle_limits(self): ) def update(self, camera_speeds: CameraSpeeds) -> None: + if (self._current_pan_angle is None + or self._current_tilt_angle is None): + logger.debug('current angles are not set yet') + return # TODO uncomment # if all(map(lambda a: a is None, self._get_angle_limits())): # return - # TODO get angles in main loop to only request them once from gimbal - # and not in every controller/component that requires them - angles = self._gimbal.get_angles() - pan_360_angle = to_360_degree(angles.target_angle_3) - tilt_360_angle = to_360_degree(angles.target_angle_2) logger.debug(', '.join(( - f"pan angle: {pan_360_angle:4.1f}", - f"tilt angle: {tilt_360_angle:4.1f}", + f"pan angle: {self._current_pan_angle:4.1f}", + f"tilt angle: {self._current_tilt_angle:4.1f}", ))) # Since camera might rotate full circle, a single limit could be @@ -398,13 +404,13 @@ def update(self, camera_speeds: CameraSpeeds) -> None: and self.max_pan_angle is not None and is_angle_between( left=self.min_pan_angle, - angle=pan_360_angle, + angle=self._current_pan_angle, right=self.max_pan_angle, clockwise=False)): min_pan_delta = get_delta_angle_clockwise( - left=pan_360_angle, right=self.min_pan_angle) + left=self._current_pan_angle, right=self.min_pan_angle) max_pan_delta = get_delta_angle_counter_clockwise( - left=pan_360_angle, right=self.max_pan_angle) + left=self._current_pan_angle, right=self.max_pan_angle) if min_pan_delta < max_pan_delta: if camera_speeds.pan_speed < 0: logger.debug( @@ -420,13 +426,13 @@ def update(self, camera_speeds: CameraSpeeds) -> None: and self.max_tilt_angle is not None and is_angle_between( left=self.min_tilt_angle, - angle=tilt_360_angle, + angle=self._current_tilt_angle, right=self.max_tilt_angle, clockwise=False)): min_tilt_delta = get_delta_angle_clockwise( - left=tilt_360_angle, right=self.min_tilt_angle) + left=self._current_tilt_angle, right=self.min_tilt_angle) max_tilt_delta = get_delta_angle_counter_clockwise( - left=tilt_360_angle, right=self.max_tilt_angle) + left=self._current_tilt_angle, right=self.max_tilt_angle) if min_tilt_delta < max_tilt_delta: if camera_speeds.tilt_speed < 0: logger.debug( diff --git a/robot_cameraman/cameraman_mode_manager.py b/robot_cameraman/cameraman_mode_manager.py index 22829fe..d6e89e1 100644 --- a/robot_cameraman/cameraman_mode_manager.py +++ b/robot_cameraman/cameraman_mode_manager.py @@ -5,8 +5,10 @@ from robot_cameraman.box import Box from robot_cameraman.camera_controller import CameraController, \ CameraAngleLimitController, CameraZoomLimitController -from robot_cameraman.gimbal import Gimbal -from robot_cameraman.tracking import TrackingStrategy, AlignTrackingStrategy, SearchTargetStrategy +from robot_cameraman.events import Event, EventEmitter +from robot_cameraman.gimbal import Gimbal, convert_simple_bgc_angles +from robot_cameraman.tracking import TrackingStrategy, AlignTrackingStrategy, \ + SearchTargetStrategy from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from simplebgc.gimbal import ControlMode @@ -23,7 +25,9 @@ def __init__( align_tracking_strategy: AlignTrackingStrategy, tracking_strategy: TrackingStrategy, search_target_strategy: SearchTargetStrategy, - gimbal: Gimbal) -> None: + gimbal: Gimbal, + event_emitter: EventEmitter) -> None: + self._event_emitter = event_emitter self._camera_controller = camera_controller self._camera_zoom_limit_controller = camera_zoom_limit_controller self._camera_angle_limit_controller = camera_angle_limit_controller @@ -39,6 +43,7 @@ def __init__( def update(self, target: Optional[Box], is_target_lost: bool) -> None: # check calling convention: target can not be lost if it exists assert target is not None or is_target_lost + self._read_gimbal_angles() if self.mode_name not in ['manual', 'angle']: if target is None and is_target_lost: if self.mode_name == 'aligning': @@ -64,6 +69,12 @@ def update(self, target: Optional[Box], is_target_lost: bool) -> None: self._camera_angle_limit_controller.update(self._camera_speeds) self._camera_controller.update(self._camera_speeds) + def _read_gimbal_angles(self): + # TODO convert angles in gimbal + self._event_emitter.emit( + Event.ANGLES, + convert_simple_bgc_angles(self._gimbal.get_angles())) + def start(self): self._camera_controller.start() diff --git a/tests/robot_cameraman/test_camera_controller.py b/tests/robot_cameraman/test_camera_controller.py index 23fe273..e98a75d 100644 --- a/tests/robot_cameraman/test_camera_controller.py +++ b/tests/robot_cameraman/test_camera_controller.py @@ -7,6 +7,7 @@ ElapsedTime, CameraState, PointOfMotionTargetSpeedCalculator, \ is_current_point_reached, is_angle_between, CameraAngleLimitController from robot_cameraman.camera_speeds import CameraSpeeds +from robot_cameraman.gimbal import Angles from simplebgc.commands import GetAnglesInCmd from simplebgc.gimbal import Gimbal, ControlMode from simplebgc.units import from_degree, from_degree_per_sec @@ -843,19 +844,14 @@ def test_is_angle_between_counter_clockwise_overstep_360(self): class TestCameraAngleLimitController: @pytest.fixture() - def gimbal(self): - # mock serial connection to avoid error, because port can not be opened - return Mock(spec=Gimbal(Mock())) - - @pytest.fixture() - def controller(self, gimbal): - return CameraAngleLimitController(gimbal) + def controller(self): + return CameraAngleLimitController() def test_stop_panning_forward_when_max_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=15.1, pan_speed=42, - tilt_angle=0, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=15.1, pan_speed=42, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=42) controller.min_pan_angle = 0 controller.max_pan_angle = 15.0 @@ -863,10 +859,10 @@ def test_stop_panning_forward_when_max_limit_is_reached( assert camera_speeds.pan_speed == 0 def test_allow_panning_back_when_max_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=15.1, pan_speed=0, - tilt_angle=0, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=15.1, pan_speed=0, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=-42) controller.min_pan_angle = 0 controller.max_pan_angle = 15.0 @@ -874,10 +870,10 @@ def test_allow_panning_back_when_max_limit_is_reached( assert camera_speeds.pan_speed == -42 def test_stop_panning_forward_when_min_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=344.9, pan_speed=-42, - tilt_angle=0, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=344.9, pan_speed=-42, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=-42) controller.min_pan_angle = 345.0 controller.max_pan_angle = 0 @@ -885,10 +881,10 @@ def test_stop_panning_forward_when_min_limit_is_reached( assert camera_speeds.pan_speed == 0 def test_allow_panning_back_when_min_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=344.9, pan_speed=0, - tilt_angle=0, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=344.9, pan_speed=0, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=42) controller.min_pan_angle = 345.0 controller.max_pan_angle = 0 @@ -896,18 +892,18 @@ def test_allow_panning_back_when_min_limit_is_reached( assert camera_speeds.pan_speed == 42 def test_allow_panning_when_no_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=350, pan_speed=30, - tilt_angle=0, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=350, pan_speed=30, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=42) controller.min_pan_angle = 345.0 controller.max_pan_angle = 0 controller.update(camera_speeds) assert camera_speeds.pan_speed == 42 - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=350, pan_speed=-30, - tilt_angle=0, tilt_speed=0)) + controller.update_current_angles( + Angles(pan_angle=350, pan_speed=-30, + tilt_angle=0, tilt_speed=0)) camera_speeds = CameraSpeeds(pan_speed=-42) controller.min_pan_angle = 345.0 controller.max_pan_angle = 0 @@ -915,10 +911,10 @@ def test_allow_panning_when_no_limit_is_reached( assert camera_speeds.pan_speed == -42 def test_stop_tilting_forward_when_max_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=5.1, tilt_speed=42)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=5.1, tilt_speed=42)) camera_speeds = CameraSpeeds(tilt_speed=42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 @@ -926,10 +922,10 @@ def test_stop_tilting_forward_when_max_limit_is_reached( assert camera_speeds.tilt_speed == 0 def test_allow_tilting_back_when_max_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=5.1, tilt_speed=0)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=5.1, tilt_speed=0)) camera_speeds = CameraSpeeds(tilt_speed=-42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 @@ -937,10 +933,10 @@ def test_allow_tilting_back_when_max_limit_is_reached( assert camera_speeds.tilt_speed == -42 def test_stop_tilting_forward_when_min_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=349.9, tilt_speed=-42)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=349.9, tilt_speed=-42)) camera_speeds = CameraSpeeds(tilt_speed=-42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 @@ -948,10 +944,10 @@ def test_stop_tilting_forward_when_min_limit_is_reached( assert camera_speeds.tilt_speed == 0 def test_allow_tilting_back_when_min_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=349.9, tilt_speed=42)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=349.9, tilt_speed=42)) camera_speeds = CameraSpeeds(tilt_speed=42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 @@ -959,21 +955,20 @@ def test_allow_tilting_back_when_min_limit_is_reached( assert camera_speeds.tilt_speed == 42 def test_allow_tilting_when_no_limit_is_reached( - self, controller, gimbal): - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=1.0, tilt_speed=42)) + self, controller: CameraAngleLimitController): + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=1.0, tilt_speed=42)) camera_speeds = CameraSpeeds(tilt_speed=42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 controller.update(camera_speeds) assert camera_speeds.tilt_speed == 42 - gimbal.get_angles = Mock( - return_value=get_angles_in_cmd(pan_angle=0, pan_speed=0, - tilt_angle=1.0, tilt_speed=-30)) + controller.update_current_angles( + Angles(pan_angle=0, pan_speed=0, + tilt_angle=1.0, tilt_speed=-30)) camera_speeds = CameraSpeeds(tilt_speed=-42) controller.min_tilt_angle = 350.0 controller.max_tilt_angle = 5.0 controller.update(camera_speeds) assert camera_speeds.tilt_speed == -42 - From 81d09a39816edc3167ea38d64e6fc33d81ee60fe Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 29 Oct 2022 15:11:32 +0200 Subject: [PATCH 077/109] feat: show current angles in status bar --- robot_cameraman/__main__.py | 2 ++ .../server_static_folder/index.html | 3 +++ robot_cameraman/ui.py | 25 +++++++++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 281a257..f2b1a2d 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -343,10 +343,12 @@ def configure_logging(): ConfigurableTrackingStrategyUi( tracking_strategy=configurable_tracking_strategy, align_strategy=configurable_align_tracking_strategy)) + # noinspection PyProtectedMember status_bar = StatusBar(pan_speed_manager=rotate_speed_manager, tilt_speed_manager=tilt_speed_manager, camera_speeds=cameraman_mode_manager._camera_speeds) +event_emitter.add_listener(Event.ANGLES, status_bar.update_current_angles) user_interfaces.append(status_bar) if args.detectionEngine == 'Dummy': diff --git a/robot_cameraman/server_static_folder/index.html b/robot_cameraman/server_static_folder/index.html index 6573288..a693ef4 100644 --- a/robot_cameraman/server_static_folder/index.html +++ b/robot_cameraman/server_static_folder/index.html @@ -24,6 +24,9 @@ #angles, #statusBar { margin: 0.1em 0 0.1em 1.5em; } + #statusBar { + white-space: pre-wrap; + } @media (orientation: landscape) { body { diff --git a/robot_cameraman/ui.py b/robot_cameraman/ui.py index a4db6bd..75023df 100644 --- a/robot_cameraman/ui.py +++ b/robot_cameraman/ui.py @@ -7,6 +7,7 @@ from robot_cameraman.camera_controller import SpeedManager from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds +from robot_cameraman.gimbal import Angles logger: Logger = logging.getLogger(__name__) @@ -52,6 +53,8 @@ class StatusBar(UserInterface): _camera_speeds: CameraSpeeds _zoom_ratio: Optional[float] _zoom_index: Optional[int] + _current_pan_angle: Optional[float] + _current_tilt_angle: Optional[float] def __init__( self, @@ -64,6 +67,8 @@ def __init__( self._camera_speeds = camera_speeds self._zoom_ratio = None self._zoom_index = None + self._current_pan_angle = None + self._current_tilt_angle = None def open(self) -> None: pass @@ -74,6 +79,10 @@ def update_zoom_ratio(self, zoom_ratio: float): def update_zoom_index(self, zoom_index: int): self._zoom_index = zoom_index + def update_current_angles(self, angles: Angles): + self._current_pan_angle = angles.pan_angle + self._current_tilt_angle = angles.tilt_angle + def update(self) -> None: pan_speed = float(self._pan_speed_manager.current_speed) tilt_speed = float(self._tilt_speed_manager.current_speed) @@ -84,13 +93,15 @@ def update(self) -> None: ZoomSpeed.ZOOM_OUT_SLOW: 'zoom out slow', ZoomSpeed.ZOOM_OUT_FAST: 'zoom out fast', }[self._camera_speeds.zoom_speed] - zoom_ratio = ('?' if self._zoom_ratio is None + zoom_ratio = (' ? ' if self._zoom_ratio is None else f'{self._zoom_ratio:4.1f}') - zoom_index = ('?' if self._zoom_index is None + zoom_index = (' ?' if self._zoom_index is None else f'{self._zoom_index:2}') - self.text = f"pan: {pan_speed :3.2}, " \ - f"tilt: {tilt_speed :3.2}, " \ - f"zoom-ratio: {zoom_ratio}, " \ - f"zoom-index: {zoom_index}, " \ - f"{zoom_speed_str}" + # TODO ° is prepended with an unexpected character in native UI + self.text = \ + f"pan: {self._current_pan_angle :6.2f}° {pan_speed :6.2}°/s, " \ + f"tilt: {self._current_tilt_angle :6.2f}° {tilt_speed :6.2}°/s, " \ + f"zoom-ratio: {zoom_ratio}, " \ + f"zoom-index: {zoom_index}, " \ + f"{zoom_speed_str}" cv2.displayStatusBar('Robot Cameraman', self.text) From fe8c21283407d6091549eeeb1645c80b8b5fd86a Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 14:11:42 +0100 Subject: [PATCH 078/109] fix: pan and tilt speed should be formatted as floats in status bar --- robot_cameraman/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/ui.py b/robot_cameraman/ui.py index 75023df..d1a78f4 100644 --- a/robot_cameraman/ui.py +++ b/robot_cameraman/ui.py @@ -99,8 +99,8 @@ def update(self) -> None: else f'{self._zoom_index:2}') # TODO ° is prepended with an unexpected character in native UI self.text = \ - f"pan: {self._current_pan_angle :6.2f}° {pan_speed :6.2}°/s, " \ - f"tilt: {self._current_tilt_angle :6.2f}° {tilt_speed :6.2}°/s, " \ + f"pan: {self._current_pan_angle :6.2f}° {pan_speed :6.2f}°/s, " \ + f"tilt: {self._current_tilt_angle :6.2f}° {tilt_speed :6.2f}°/s, " \ f"zoom-ratio: {zoom_ratio}, " \ f"zoom-index: {zoom_index}, " \ f"{zoom_speed_str}" From e7257b765714319746400de94d60c56fbae2ab40 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 14:12:14 +0100 Subject: [PATCH 079/109] refactor: update TODO about degree sign in status bar --- robot_cameraman/ui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/ui.py b/robot_cameraman/ui.py index d1a78f4..f5d3279 100644 --- a/robot_cameraman/ui.py +++ b/robot_cameraman/ui.py @@ -97,7 +97,9 @@ def update(self) -> None: else f'{self._zoom_ratio:4.1f}') zoom_index = (' ?' if self._zoom_index is None else f'{self._zoom_index:2}') - # TODO ° is prepended with an unexpected character in native UI + # TODO ° is displayed as ° using cv2.displayStatusBar + # in the current version opencv-python==4.1.0.25, + # but it is fixed in at least opencv-python=4.6.0.66 self.text = \ f"pan: {self._current_pan_angle :6.2f}° {pan_speed :6.2f}°/s, " \ f"tilt: {self._current_tilt_angle :6.2f}° {tilt_speed :6.2f}°/s, " \ From 4233d1ea012cb1bba2c127b957379c2d3110a180 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 14:17:45 +0100 Subject: [PATCH 080/109] feat: add strategy to search target at static position (pan and tilt angle) --- robot_cameraman/__main__.py | 46 +++++++++- robot_cameraman/cameraman_mode_manager.py | 29 +++++- robot_cameraman/tracking.py | 103 ++++++++++++++++++++++ 3 files changed, 173 insertions(+), 5 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index f2b1a2d..5295a2f 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -19,6 +19,7 @@ CameraZoomIndexLimitController from robot_cameraman.camera_observable import \ PanasonicCameraObservable, ObservableCameraProperty +from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.cameraman import Cameraman from robot_cameraman.cameraman_mode_manager import CameramanModeManager from robot_cameraman.configuration import read_configuration_file @@ -38,8 +39,8 @@ from robot_cameraman.server import run_server, ImageContainer from robot_cameraman.tracking import Destination, StopIfLostTrackingStrategy, \ RotateSearchTargetStrategy, ConfigurableTrackingStrategy, \ - ConfigurableAlignTrackingStrategy, ConfigurableTrackingStrategyUi -from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds + ConfigurableAlignTrackingStrategy, ConfigurableTrackingStrategyUi, \ + StaticSearchTargetStrategy from robot_cameraman.ui import StatusBar from robot_cameraman.updatable_configuration import UpdatableConfiguration from robot_cameraman.zoom import parse_zoom_steps, parse_zoom_ratio_index_ranges @@ -82,6 +83,7 @@ class RobotCameramanArguments(Protocol): font: Path fontSize: int debug: bool + search_strategy: str rotatingSearchSpeed: int rotationalAccelerationPerSecond: int tiltingAccelerationPerSecond: int @@ -171,6 +173,19 @@ def parse_arguments() -> RobotCameramanArguments: parser.add_argument('--debug', action='store_true', help="Enable debug logging") + parser.add_argument('--search-strategy', + type=str, default='rotate', + help="If target is lost," + " the camera searches for a new target." + " If the strategy 'rotate' (default) is used," + " the gimbal pans clockwise at constant speed" + " (given by argument --rotatingSearchSpeed)." + " If the strategy 'static' is used," + " the gimbal moves (pans, tilts and zooms)" + " with constant speed" + " (given by argument --rotatingSearchSpeed)" + " to a certain position" + " (can be configured in the web UI).") parser.add_argument('--rotatingSearchSpeed', type=int, default=0, help="If target is lost, search for new target by" @@ -319,7 +334,30 @@ def configure_logging(): else: camera_zoom_limit_controller = CameraZoomRatioLimitController() +if args.search_strategy == 'rotate': + search_target_strategy = max_speed_and_acceleration_updater.add( + RotateSearchTargetStrategy(args.rotatingSearchSpeed)) +elif args.search_strategy == 'static': + search_strategy_zoom_limit_controller = CameraZoomIndexLimitController() + # TODO declare camera_observable before this statement or use event_emitter + # camera_observable.add_listener( + # ObservableCameraProperty.ZOOM_INDEX, + # search_strategy_zoom_limit_controller.update_zoom_index) + search_target_strategy = StaticSearchTargetStrategy( + pan_speed=args.rotatingSearchSpeed, + tilt_speed=args.rotatingSearchSpeed, + camera_zoom_limit_controller=search_strategy_zoom_limit_controller, + camera_angle_limit_controller=create_angle_limit_controller( + event_emitter)) + event_emitter.add_listener(Event.ANGLES, + search_target_strategy.update_current_angles) + max_speed_and_acceleration_updater.add_updatable_properties( + search_target_strategy, ['pan_speed', 'tilt_speed']) +else: + print(f"Unknown search strategy {args.search_strategy}") + exit(1) camera_angle_limit_controller = create_angle_limit_controller(event_emitter) +# noinspection PyUnboundLocalVariable cameraman_mode_manager = CameramanModeManager( camera_controller=SmoothCameraController( gimbal, @@ -331,8 +369,7 @@ def configure_logging(): align_tracking_strategy=max_speed_and_acceleration_updater.add( configurable_align_tracking_strategy), tracking_strategy=tracking_strategy, - search_target_strategy=max_speed_and_acceleration_updater.add( - RotateSearchTargetStrategy(args.rotatingSearchSpeed)), + search_target_strategy=search_target_strategy, gimbal=gimbal, event_emitter=event_emitter) @@ -381,6 +418,7 @@ def configure_logging(): # noinspection PyUnreachableCode if False: from robot_cameraman.camera_observable import ExHeaderToCsvWriter + live_view.add_ex_header_listener(ExHeaderToCsvWriter().on_ex_header) camera_observable.add_listener( ObservableCameraProperty.ZOOM_RATIO, diff --git a/robot_cameraman/cameraman_mode_manager.py b/robot_cameraman/cameraman_mode_manager.py index d6e89e1..8021a24 100644 --- a/robot_cameraman/cameraman_mode_manager.py +++ b/robot_cameraman/cameraman_mode_manager.py @@ -1,5 +1,6 @@ import logging from logging import Logger +from threading import RLock from typing import Optional from robot_cameraman.box import Box @@ -8,7 +9,7 @@ from robot_cameraman.events import Event, EventEmitter from robot_cameraman.gimbal import Gimbal, convert_simple_bgc_angles from robot_cameraman.tracking import TrackingStrategy, AlignTrackingStrategy, \ - SearchTargetStrategy + SearchTargetStrategy, StaticSearchTargetStrategy from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from simplebgc.gimbal import ControlMode @@ -36,10 +37,36 @@ def __init__( self._search_target_strategy = search_target_strategy self._gimbal = gimbal self._camera_speeds: CameraSpeeds = CameraSpeeds() + self._mode_name_lock = RLock() + self._mode_name = None self.mode_name = 'manual' + # TODO searching does not start if used as initial mode, since current + # angles have not been set on StaticSearchTargetStrategy yet + # self.mode_name = 'searching' self.is_zoom_enabled = True self.are_limits_applied_in_manual_mode = False + @property + def mode_name(self) -> str: + with self._mode_name_lock: + return self._mode_name + + @mode_name.setter + def mode_name(self, new_mode_name: str): + with self._mode_name_lock: + previous_mode_name = self._mode_name + self._mode_name = new_mode_name + # TODO decouple: introduce listeners to changes + # CameramanModeManager should not need to know that + # StaticSearchTargetStrategy is called. + if (previous_mode_name != new_mode_name + and isinstance(self._search_target_strategy, + StaticSearchTargetStrategy)): + if new_mode_name == 'searching': + self._search_target_strategy.start() + if previous_mode_name == 'searching': + self._search_target_strategy.stop() + def update(self, target: Optional[Box], is_target_lost: bool) -> None: # check calling convention: target can not be lost if it exists assert target is not None or is_target_lost diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 6b3c95d..001a7c9 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -7,8 +7,12 @@ from typing_extensions import Protocol +from robot_cameraman.angle import get_delta_angle_clockwise from robot_cameraman.box import Box, TwoPointsBox, Point +from robot_cameraman.camera_controller import CameraZoomLimitController, \ + CameraAngleLimitController from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds +from robot_cameraman.gimbal import Angles from robot_cameraman.live_view import ImageSize logger: Logger = logging.getLogger(__name__) @@ -368,3 +372,102 @@ def __init__(self, speed=200): def update(self, camera_speeds: CameraSpeeds) -> None: camera_speeds.pan_speed = self.speed + + +class StaticSearchTargetStrategy(SearchTargetStrategy): + _target_pan_angle: Optional[float] + _target_tilt_angle: Optional[float] + _target_zoom_index: Optional[int] + _target_zoom_ratio: Optional[float] + + _current_pan_angle: Optional[float] + _current_tilt_angle: Optional[float] + # TODO update zoom index and ratio + _current_zoom_index: Optional[int] + _current_zoom_ratio: Optional[float] + + _camera_speeds: CameraSpeeds + _is_searching: bool + + def __init__( + self, + pan_speed: float, + tilt_speed: float, + camera_zoom_limit_controller: CameraZoomLimitController, + camera_angle_limit_controller: CameraAngleLimitController): + self.pan_speed = pan_speed + self.tilt_speed = tilt_speed + self._camera_zoom_limit_controller = camera_zoom_limit_controller + self._camera_angle_limit_controller = camera_angle_limit_controller + self._target_pan_angle = None + self._target_tilt_angle = None + self._target_zoom_index = None + self._target_zoom_ratio = None + self._current_pan_angle = None + self._current_tilt_angle = None + self._current_zoom_index = None + self._current_zoom_ratio = None + self._camera_speeds = CameraSpeeds() + self._is_searching = False + + # TODO remove test targets below + self._target_pan_angle = 10.0 + self._target_tilt_angle = 5.0 + + # TODO add UI for target + + def start(self) -> None: + assert not self._is_searching + assert self._current_pan_angle is not None + assert self._current_tilt_angle is not None + self._is_searching = True + if (self._target_pan_angle is not None + and self._current_pan_angle is not None): + delta_pan_angle_clockwise = get_delta_angle_clockwise( + left=self._current_pan_angle, right=self._target_pan_angle) + if delta_pan_angle_clockwise < 180: + self._camera_speeds.pan_speed = self.pan_speed + self._camera_angle_limit_controller.min_pan_angle = \ + self._current_pan_angle + self._camera_angle_limit_controller.max_pan_angle = \ + self._target_pan_angle + else: + self._camera_speeds.pan_speed = -self.pan_speed + self._camera_angle_limit_controller.min_pan_angle = \ + self._target_pan_angle + self._camera_angle_limit_controller.max_pan_angle = \ + self._current_pan_angle + if (self._target_tilt_angle is not None + and self._current_tilt_angle is not None): + if self._current_tilt_angle < self._target_tilt_angle: + self._camera_speeds.tilt_speed = self.tilt_speed + self._camera_angle_limit_controller.min_tilt_angle = \ + self._current_tilt_angle + self._camera_angle_limit_controller.max_tilt_angle = \ + self._target_tilt_angle + else: + self._camera_speeds.tilt_speed = -self.tilt_speed + self._camera_angle_limit_controller.min_tilt_angle = \ + self._target_tilt_angle + self._camera_angle_limit_controller.max_tilt_angle = \ + self._current_tilt_angle + # TODO zoom + # self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + + def update_current_angles(self, angles: Angles): + self._current_pan_angle = angles.pan_angle + self._current_tilt_angle = angles.tilt_angle + # TODO calculate camera speeds if current angles are set the first time + # and target angles are already set + + def update(self, camera_speeds: CameraSpeeds) -> None: + assert self._is_searching + camera_speeds.pan_speed = self._camera_speeds.pan_speed + camera_speeds.tilt_speed = self._camera_speeds.tilt_speed + camera_speeds.zoom_speed = self._camera_speeds.zoom_speed + self._camera_zoom_limit_controller.update(camera_speeds) + self._camera_angle_limit_controller.update(camera_speeds) + + def stop(self): + self._is_searching = False + self._camera_speeds.reset() From bdc8cf1a1d2e8334df71b7ebfe58b1d04119fa9d Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 22:38:34 +0100 Subject: [PATCH 081/109] fix: PEP 8: E501 line too long (81 > 80 characters) --- robot_cameraman/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 5295a2f..073d81a 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -226,8 +226,8 @@ def parse_arguments() -> RobotCameramanArguments: " zoom-steps (see" " robot_cameraman/tools/analyze_zoom_of_camera.py)." " If this argument is given," - " the PredictiveCameraZoomRatioLimitController is used to limit the" - " zoom of the camera.") + " the PredictiveCameraZoomRatioLimitController is used to limit" + " the zoom of the camera.") parser.add_argument( '--camera-zoom-ratio-index-ranges', type=Path, From 45fd1c88ef85d29cec62671bedebb1ec99c45ddd Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 22:52:21 +0100 Subject: [PATCH 082/109] refactor: emit observable camera properties using EventEmitter --- robot_cameraman/__main__.py | 26 +++++----- robot_cameraman/camera_observable.py | 52 +++---------------- robot_cameraman/events.py | 3 ++ .../tools/analyze_zoom_of_camera.py | 16 +++--- 4 files changed, 32 insertions(+), 65 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 073d81a..3cb2b10 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -17,8 +17,7 @@ SpeedManager, CameraAngleLimitController, \ PredictiveCameraZoomRatioLimitController, CameraZoomRatioLimitController, \ CameraZoomIndexLimitController -from robot_cameraman.camera_observable import \ - PanasonicCameraObservable, ObservableCameraProperty +from robot_cameraman.camera_observable import PanasonicCameraObservable from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.cameraman import Cameraman from robot_cameraman.cameraman_mode_manager import CameramanModeManager @@ -412,7 +411,8 @@ def configure_logging(): elif args.liveView == 'Panasonic': live_view = PanasonicLiveView(args.ip, args.port) camera_observable = PanasonicCameraObservable( - min_focal_length=args.cameraMinFocalLength) + min_focal_length=args.cameraMinFocalLength, + event_emitter=event_emitter) live_view.add_ex_header_listener(camera_observable.on_ex_header) # TODO add CLI argument to enable ExHeaderToCsvWriter # noinspection PyUnreachableCode @@ -420,22 +420,22 @@ def configure_logging(): from robot_cameraman.camera_observable import ExHeaderToCsvWriter live_view.add_ex_header_listener(ExHeaderToCsvWriter().on_ex_header) - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_RATIO, + event_emitter.add_listener( + Event.ZOOM_RATIO, max_speed_and_acceleration_updater.on_zoom_ratio) - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_INDEX, + event_emitter.add_listener( + Event.ZOOM_INDEX, status_bar.update_zoom_index) - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_RATIO, + event_emitter.add_listener( + Event.ZOOM_RATIO, status_bar.update_zoom_ratio) if hasattr(camera_zoom_limit_controller, 'update_zoom_ratio'): - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_RATIO, + event_emitter.add_listener( + Event.ZOOM_RATIO, camera_zoom_limit_controller.update_zoom_ratio) if hasattr(camera_zoom_limit_controller, 'update_zoom_index'): - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_INDEX, + event_emitter.add_listener( + Event.ZOOM_INDEX, camera_zoom_limit_controller.update_zoom_index) else: print(f"Unknown live view {args.liveView}") diff --git a/robot_cameraman/camera_observable.py b/robot_cameraman/camera_observable.py index 809078c..f120b46 100644 --- a/robot_cameraman/camera_observable.py +++ b/robot_cameraman/camera_observable.py @@ -1,75 +1,37 @@ import csv import datetime import logging -from abc import ABC from dataclasses import fields -from enum import Enum, auto from pathlib import Path -from typing import List, Callable, Dict from panasonic_camera.live_view import ExHeader, ExHeader1, ExHeader2, ExHeader8 +from robot_cameraman.events import EventEmitter, Event logger: logging.Logger = logging.getLogger(__name__) -class ObservableCameraProperty(Enum): - FOCAL_LENGTH = auto() - ZOOM_RATIO = auto() - ZOOM_INDEX = auto() +class PanasonicCameraObservable: - -CameraObservableListener = Callable - - -class CameraObservable(ABC): - _listeners: Dict[ObservableCameraProperty, List[CameraObservableListener]] - - def __init__(self) -> None: - self._listeners = dict() - - def add_listener( - self, - observable_property: ObservableCameraProperty, - listener: CameraObservableListener): - property_listeners = self._get_property_listeners(observable_property) - property_listeners.append(listener) - - def _get_property_listeners(self, observable_property): - return self._listeners.setdefault(observable_property, []) - - def _notify_listeners( - self, - observable_property: ObservableCameraProperty, - value): - property_listeners = self._get_property_listeners(observable_property) - for listener in property_listeners: - listener(value) - - -class PanasonicCameraObservable(CameraObservable): - - def __init__(self, min_focal_length: float): + def __init__(self, min_focal_length: float, event_emitter: EventEmitter): super().__init__() self.min_focal_length = min_focal_length + self._event_emitter = event_emitter def on_ex_header(self, ex_header: ExHeader): if (isinstance(ex_header, ExHeader1) or isinstance(ex_header, ExHeader2)): zoom_index = ex_header.b logger.debug(f"zoom index {zoom_index}") - self._notify_listeners( - ObservableCameraProperty.ZOOM_INDEX, zoom_index) + self._event_emitter.emit(Event.ZOOM_INDEX, zoom_index) # Zoom ratio is encoded as integer, # e.g 1.5x is encoded as 15. # Convert it to float: zoom_ratio = ex_header.zoomRatio / 10 logger.debug(f"zoom ratio {zoom_ratio}") - self._notify_listeners( - ObservableCameraProperty.ZOOM_RATIO, zoom_ratio) + self._event_emitter.emit(Event.ZOOM_RATIO, zoom_ratio) focal_length = zoom_ratio * self.min_focal_length logger.debug(f"focal length {focal_length}") - self._notify_listeners( - ObservableCameraProperty.FOCAL_LENGTH, focal_length) + self._event_emitter.emit(Event.FOCAL_LENGTH, focal_length) class ExHeaderToCsvWriter: diff --git a/robot_cameraman/events.py b/robot_cameraman/events.py index ab72719..1d0a444 100644 --- a/robot_cameraman/events.py +++ b/robot_cameraman/events.py @@ -6,6 +6,9 @@ class Event(Enum): ANGLES = auto() """Current angles are emitted after they have been read.""" + FOCAL_LENGTH = auto() + ZOOM_RATIO = auto() + ZOOM_INDEX = auto() Listener = Callable diff --git a/robot_cameraman/tools/analyze_zoom_of_camera.py b/robot_cameraman/tools/analyze_zoom_of_camera.py index 1256978..8146359 100644 --- a/robot_cameraman/tools/analyze_zoom_of_camera.py +++ b/robot_cameraman/tools/analyze_zoom_of_camera.py @@ -17,8 +17,8 @@ from panasonic_camera.camera_manager import PanasonicCameraManager from robot_cameraman.camera_controller import ElapsedTime -from robot_cameraman.camera_observable import \ - PanasonicCameraObservable, ObservableCameraProperty +from robot_cameraman.camera_observable import PanasonicCameraObservable +from robot_cameraman.events import Event, EventEmitter from robot_cameraman.live_view import PanasonicLiveView from robot_cameraman.zoom import ZoomStep @@ -556,13 +556,15 @@ def _zoom_out_completely(self): args = parse_arguments() configure_logging() +event_emitter = EventEmitter() camera_manager = PanasonicCameraManager( identify_as=args.identifyToPanasonicCameraAs) if args.liveView == 'Panasonic': live_view = PanasonicLiveView(args.ip, args.port) camera_observable = PanasonicCameraObservable( - min_focal_length=args.cameraMinFocalLength) + min_focal_length=args.cameraMinFocalLength, + event_emitter=event_emitter) live_view.add_ex_header_listener(camera_observable.on_ex_header) else: print(f"Unsupported live view {args.liveView}") @@ -575,8 +577,8 @@ def _zoom_out_completely(self): zoom_speed=ZoomSpeed.SLOW, live_view=live_view) # noinspection PyUnboundLocalVariable -camera_observable.add_listener( - ObservableCameraProperty.ZOOM_RATIO, +event_emitter.add_listener( + Event.ZOOM_RATIO, zoom_analyzer_camera_controller.on_zoom_ratio) signal.signal(signal.SIGINT, quit) @@ -840,8 +842,8 @@ def print_zoom_steps_as_tsv(zoom_steps: List[ZoomStep]): zoom_speed=ZoomSpeed.SLOW, live_view=live_view) # noinspection PyUnboundLocalVariable - camera_observable.add_listener( - ObservableCameraProperty.ZOOM_RATIO, + event_emitter.add_listener( + Event.ZOOM_RATIO, zoom_step_by_step_camera_controller.on_zoom_ratio) zoom_step_by_step_camera_controller.start() From 74b3f86e7902e19afa8f9a3df8767fa540b977d8 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 23:36:34 +0100 Subject: [PATCH 083/109] fix: static search at tilt angle not working with angles >180 --- robot_cameraman/tracking.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 001a7c9..4e51929 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -439,7 +439,9 @@ def start(self) -> None: self._current_pan_angle if (self._target_tilt_angle is not None and self._current_tilt_angle is not None): - if self._current_tilt_angle < self._target_tilt_angle: + delta_tilt_angle_clockwise = get_delta_angle_clockwise( + left=self._current_tilt_angle, right=self._target_tilt_angle) + if delta_tilt_angle_clockwise < 180: self._camera_speeds.tilt_speed = self.tilt_speed self._camera_angle_limit_controller.min_tilt_angle = \ self._current_tilt_angle From 0e592742b057d3c31d9b485f56858b205d6092cb Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 30 Oct 2022 23:53:24 +0100 Subject: [PATCH 084/109] refactor: add type hints for camera limit controllers --- robot_cameraman/tracking.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 4e51929..4cdd695 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -375,6 +375,8 @@ def update(self, camera_speeds: CameraSpeeds) -> None: class StaticSearchTargetStrategy(SearchTargetStrategy): + _camera_zoom_limit_controller: CameraZoomLimitController + _camera_angle_limit_controller: CameraAngleLimitController _target_pan_angle: Optional[float] _target_tilt_angle: Optional[float] _target_zoom_index: Optional[int] From 43a2dc505bfc84d45d2464bf2074e8b9bf86d4f9 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Mon, 31 Oct 2022 00:02:31 +0100 Subject: [PATCH 085/109] feat: support target zoom in static search strategy --- robot_cameraman/__main__.py | 44 +++++++++++++++++++----------- robot_cameraman/tracking.py | 53 ++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 3cb2b10..46fa7f3 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -325,31 +325,45 @@ def configure_logging(): ConfigurableAlignTrackingStrategy( destination, live_view_image_size, max_allowed_speed=16) -if args.camera_zoom_ratio_index_ranges is not None: - camera_zoom_limit_controller = CameraZoomIndexLimitController() -elif args.camera_zoom_steps is not None: - camera_zoom_limit_controller = PredictiveCameraZoomRatioLimitController( - zoom_steps=parse_zoom_steps(args.camera_zoom_steps)) -else: - camera_zoom_limit_controller = CameraZoomRatioLimitController() + +def create_camera_zoom_limit_controller(): + global event_emitter + if args.camera_zoom_ratio_index_ranges is not None: + controller = CameraZoomIndexLimitController() + elif args.camera_zoom_steps is not None: + controller = PredictiveCameraZoomRatioLimitController( + zoom_steps=parse_zoom_steps(args.camera_zoom_steps)) + else: + controller = CameraZoomRatioLimitController() + if hasattr(controller, 'update_zoom_ratio'): + event_emitter.add_listener( + Event.ZOOM_RATIO, + controller.update_zoom_ratio) + if hasattr(controller, 'update_zoom_index'): + event_emitter.add_listener( + Event.ZOOM_INDEX, + controller.update_zoom_index) + return controller + + +camera_zoom_limit_controller = create_camera_zoom_limit_controller() if args.search_strategy == 'rotate': search_target_strategy = max_speed_and_acceleration_updater.add( RotateSearchTargetStrategy(args.rotatingSearchSpeed)) elif args.search_strategy == 'static': - search_strategy_zoom_limit_controller = CameraZoomIndexLimitController() - # TODO declare camera_observable before this statement or use event_emitter - # camera_observable.add_listener( - # ObservableCameraProperty.ZOOM_INDEX, - # search_strategy_zoom_limit_controller.update_zoom_index) search_target_strategy = StaticSearchTargetStrategy( pan_speed=args.rotatingSearchSpeed, tilt_speed=args.rotatingSearchSpeed, - camera_zoom_limit_controller=search_strategy_zoom_limit_controller, + camera_zoom_limit_controller=create_camera_zoom_limit_controller(), camera_angle_limit_controller=create_angle_limit_controller( event_emitter)) - event_emitter.add_listener(Event.ANGLES, - search_target_strategy.update_current_angles) + event_emitter.add_listener( + Event.ANGLES, search_target_strategy.update_current_angles) + event_emitter.add_listener( + Event.ZOOM_INDEX, search_target_strategy.update_current_zoom_index) + event_emitter.add_listener( + Event.ZOOM_RATIO, search_target_strategy.update_current_zoom_ratio) max_speed_and_acceleration_updater.add_updatable_properties( search_target_strategy, ['pan_speed', 'tilt_speed']) else: diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 4cdd695..b7443c2 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -10,7 +10,8 @@ from robot_cameraman.angle import get_delta_angle_clockwise from robot_cameraman.box import Box, TwoPointsBox, Point from robot_cameraman.camera_controller import CameraZoomLimitController, \ - CameraAngleLimitController + CameraAngleLimitController, CameraZoomIndexLimitController, \ + CameraZoomRatioLimitController from robot_cameraman.camera_speeds import ZoomSpeed, CameraSpeeds from robot_cameraman.gimbal import Angles from robot_cameraman.live_view import ImageSize @@ -415,6 +416,8 @@ def __init__( # TODO remove test targets below self._target_pan_angle = 10.0 self._target_tilt_angle = 5.0 + # self._target_zoom_index = 10 + # self._target_zoom_ratio = 2.0 # TODO add UI for target @@ -455,8 +458,42 @@ def start(self) -> None: self._target_tilt_angle self._camera_angle_limit_controller.max_tilt_angle = \ self._current_tilt_angle - # TODO zoom - # self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + if (self._target_zoom_ratio is not None + and self._current_zoom_ratio is not None): + assert isinstance(self._camera_zoom_limit_controller, + CameraZoomRatioLimitController) + if self._current_zoom_ratio < self._target_zoom_ratio: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + self._camera_zoom_limit_controller.min_zoom_ratio = None + self._camera_zoom_limit_controller.max_zoom_ratio = \ + self._target_zoom_ratio + elif self._current_zoom_ratio > self._target_zoom_ratio: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW + self._camera_zoom_limit_controller.min_zoom_ratio = \ + self._target_zoom_ratio + self._camera_zoom_limit_controller.max_zoom_ratio = None + else: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED + self._camera_zoom_limit_controller.min_zoom_ratio = None + self._camera_zoom_limit_controller.max_zoom_ratio = None + if (self._target_zoom_index is not None + and self._current_zoom_index is not None): + assert isinstance(self._camera_zoom_limit_controller, + CameraZoomIndexLimitController) + if self._current_zoom_index < self._target_zoom_index: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + self._camera_zoom_limit_controller.min_zoom_index = None + self._camera_zoom_limit_controller.max_zoom_index = \ + self._target_zoom_index + elif self._current_zoom_index > self._target_zoom_index: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW + self._camera_zoom_limit_controller.min_zoom_index = \ + self._target_zoom_index + self._camera_zoom_limit_controller.max_zoom_index = None + else: + self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED + self._camera_zoom_limit_controller.min_zoom_index = None + self._camera_zoom_limit_controller.max_zoom_index = None def update_current_angles(self, angles: Angles): self._current_pan_angle = angles.pan_angle @@ -464,6 +501,16 @@ def update_current_angles(self, angles: Angles): # TODO calculate camera speeds if current angles are set the first time # and target angles are already set + def update_current_zoom_ratio(self, zoom_ratio: float): + self._current_zoom_ratio = zoom_ratio + # TODO calculate camera zoom speed if current zoom ratio is set the + # first time and target zoom speed is already set + + def update_current_zoom_index(self, zoom_index: int): + self._current_zoom_index = zoom_index + # TODO calculate camera zoom speed if current zoom index is set the + # first time and target zoom speed is already set + def update(self, camera_speeds: CameraSpeeds) -> None: assert self._is_searching camera_speeds.pan_speed = self._camera_speeds.pan_speed From 38530c849321d1d9182e2892b93243cbc7bb4985 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 2 Nov 2022 17:02:30 +0100 Subject: [PATCH 086/109] feat: add function `get_angle_distance` --- robot_cameraman/angle.py | 17 +++++++++++++++++ tests/robot_cameraman/test_angle.py | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/angle.py b/robot_cameraman/angle.py index 057b040..76cbe48 100644 --- a/robot_cameraman/angle.py +++ b/robot_cameraman/angle.py @@ -9,3 +9,20 @@ def get_delta_angle_counter_clockwise(left: float, right: float) -> float: return left - right else: return abs(360 - right) + left + + +def get_angle_distance(left: float, right: float) -> float: + """Returns the smaller delta angle.""" + # To increase floating point precision, + # get_delta_angle_clockwise and get_delta_angle_counter_clockwise + # are not reused. Thereby, the required operations to calculate the result + # can be reduced. + if left >= right: + delta = left - right + if delta <= 180: + return delta + return 360 - delta + delta = abs(360 - right) + left + if delta <= 180: + return delta + return right - left diff --git a/tests/robot_cameraman/test_angle.py b/tests/robot_cameraman/test_angle.py index 2771750..eb74487 100644 --- a/tests/robot_cameraman/test_angle.py +++ b/tests/robot_cameraman/test_angle.py @@ -1,5 +1,5 @@ from robot_cameraman.angle import get_delta_angle_clockwise, \ - get_delta_angle_counter_clockwise + get_delta_angle_counter_clockwise, get_angle_distance def test_get_delta_angle_clockwise(): @@ -14,3 +14,20 @@ def test_get_delta_angle_counter_clockwise(): assert get_delta_angle_counter_clockwise(left=6, right=0) == 6 assert get_delta_angle_counter_clockwise(left=0, right=0) == 0 assert get_delta_angle_counter_clockwise(left=35, right=30) == 5 + + +def test_get_angle_distance(): + assert get_angle_distance(left=0, right=6) == 6 + assert get_angle_distance(left=6, right=0) == 6 + assert get_angle_distance(left=0, right=0) == 0 + assert get_angle_distance(left=30, right=35) == 5 + assert get_angle_distance(left=355, right=6) == 11 + assert get_angle_distance(left=355, right=0) == 5 + assert get_angle_distance(left=0, right=180) == 180 + assert get_angle_distance(left=0, right=181) == 179 + assert get_angle_distance(left=359, right=180) == 179 + assert get_angle_distance(left=359, right=181) == 178 + assert get_angle_distance(left=15.4, right=10.0) == 5.4 + assert get_angle_distance(left=10.0, right=15.4) == 5.4 + assert get_angle_distance(left=0, right=0) == 0 + assert get_angle_distance(left=359.1, right=359.1) == 0 From da85bf15a729c7d8ed76f40dded16afffa8b119d Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 2 Nov 2022 17:08:10 +0100 Subject: [PATCH 087/109] feat: move camera fast and accurate to searching target location --- robot_cameraman/__main__.py | 2 - robot_cameraman/tracking.py | 92 +++++++- tests/robot_cameraman/test_tracking.py | 286 +++++++++++++++++++++++++ 3 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 tests/robot_cameraman/test_tracking.py diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index 46fa7f3..af996e8 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -364,8 +364,6 @@ def create_camera_zoom_limit_controller(): Event.ZOOM_INDEX, search_target_strategy.update_current_zoom_index) event_emitter.add_listener( Event.ZOOM_RATIO, search_target_strategy.update_current_zoom_ratio) - max_speed_and_acceleration_updater.add_updatable_properties( - search_target_strategy, ['pan_speed', 'tilt_speed']) else: print(f"Unknown search strategy {args.search_strategy}") exit(1) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index b7443c2..0d52848 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -5,9 +5,10 @@ from logging import Logger from typing import Optional +import numpy from typing_extensions import Protocol -from robot_cameraman.angle import get_delta_angle_clockwise +from robot_cameraman.angle import get_delta_angle_clockwise, get_angle_distance from robot_cameraman.box import Box, TwoPointsBox, Point from robot_cameraman.camera_controller import CameraZoomLimitController, \ CameraAngleLimitController, CameraZoomIndexLimitController, \ @@ -495,6 +496,18 @@ def start(self) -> None: self._camera_zoom_limit_controller.min_zoom_index = None self._camera_zoom_limit_controller.max_zoom_index = None + def update_target( + self, + pan_angle: Optional[float], + tilt_angle: Optional[float], + zoom_index: Optional[int], + zoom_ratio: Optional[float]): + self._target_pan_angle = pan_angle + self._target_tilt_angle = tilt_angle + self._target_zoom_index = zoom_index + self._target_zoom_ratio = zoom_ratio + # TODO update camera speeds if called before start + def update_current_angles(self, angles: Angles): self._current_pan_angle = angles.pan_angle self._current_tilt_angle = angles.tilt_angle @@ -513,8 +526,81 @@ def update_current_zoom_index(self, zoom_index: int): def update(self, camera_speeds: CameraSpeeds) -> None: assert self._is_searching - camera_speeds.pan_speed = self._camera_speeds.pan_speed - camera_speeds.tilt_speed = self._camera_speeds.tilt_speed + # The live view of the camera moves faster at higher zoom ratios. + # Usually the pan and tilt speed should depend on the zoom ratio + # (see https://github.com/maiermic/robot-cameraman/issues/13). + # However, the camera should move in appropriate time + # to the target position. With increasing target zoom, + # the camera moves slower. If the pan/tilt distance is quite large, + # the camera would take inappropriately long to pan/tilt to the target. + # Hence, the camera should pan/tilt at maximum speed (as if zoom ratio + # is 1.0) to the target. However, the speed is decreased near the + # target to reach it more accurately. Angle deviations are reflected in + # greater deviations in the live view with increasing zom ratio. + # + # Speed-Distance Diagram: + # speed axis (ascending from bottom to top) + # A + # | + # max speed | ----\ + # close speed | \ + # accurate speed | \----| + # 0 | \---- + # | + # distance axis (desc) --|--|--|||----|--------> + # | ||| 0 + # distance > max speed ----/ ||| | + # distance = max speed -------/|| | + # distance > accurate speed ---/| | + # distance = accurate speed ----/ | + # distance = 0 ----------------------/ + zoom_ratio = self._current_zoom_ratio or 1.0 + + pan_distance = get_angle_distance(left=self._target_pan_angle, + right=self._current_pan_angle) + if pan_distance < abs(self._camera_speeds.pan_speed): + accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio + # use "accurate speed" + camera_speeds.pan_speed = accurate_pan_speed + # increase to "close speed" + if pan_distance > abs(accurate_pan_speed): + percentage = ( + abs(pan_distance - abs(accurate_pan_speed)) + / abs(self._camera_speeds.pan_speed + - accurate_pan_speed)) + camera_speeds.pan_speed += ( + numpy.sign(self._camera_speeds.pan_speed) + * percentage + * abs(self._camera_speeds.pan_speed + - accurate_pan_speed)) + else: + # use "max speed", since: distance > max speed + camera_speeds.pan_speed = self._camera_speeds.pan_speed + + tilt_distance = get_angle_distance(left=self._target_tilt_angle, + right=self._current_tilt_angle) + if tilt_distance < abs(self._camera_speeds.tilt_speed): + accurate_tilt_speed = self._camera_speeds.tilt_speed / zoom_ratio + # use "accurate speed" + camera_speeds.tilt_speed = accurate_tilt_speed + # increase to "close speed" + if tilt_distance > abs(accurate_tilt_speed): + percentage = ( + abs(tilt_distance - abs(accurate_tilt_speed)) + / abs(self._camera_speeds.tilt_speed + - accurate_tilt_speed)) + camera_speeds.tilt_speed += ( + numpy.sign(self._camera_speeds.tilt_speed) + * percentage + * abs(self._camera_speeds.tilt_speed + - accurate_tilt_speed)) + else: + # use "max speed", since: distance > max speed + camera_speeds.tilt_speed = self._camera_speeds.tilt_speed + + # TODO add option to zoom not until "close speed" (pan and tilt) + # is reached, since focus might be lost (=> blurry image), + # while camera pans/tilts (too) fast (for current zoom ratio). camera_speeds.zoom_speed = self._camera_speeds.zoom_speed self._camera_zoom_limit_controller.update(camera_speeds) self._camera_angle_limit_controller.update(camera_speeds) diff --git a/tests/robot_cameraman/test_tracking.py b/tests/robot_cameraman/test_tracking.py new file mode 100644 index 0000000..ae141a8 --- /dev/null +++ b/tests/robot_cameraman/test_tracking.py @@ -0,0 +1,286 @@ +import pytest + +from robot_cameraman.camera_controller import CameraZoomIndexLimitController, \ + CameraAngleLimitController +from robot_cameraman.camera_speeds import CameraSpeeds, ZoomSpeed +from robot_cameraman.gimbal import Angles +from robot_cameraman.tracking import StaticSearchTargetStrategy + + +class TestStaticSearchTargetStrategy: + @pytest.fixture() + def camera_speeds(self): + return CameraSpeeds() + + @pytest.fixture() + def camera_zoom_limit_controller(self): + return CameraZoomIndexLimitController() + + @pytest.fixture() + def camera_angle_limit_controller(self): + return CameraAngleLimitController() + + @pytest.fixture() + def max_pan_speed(self): + return 9 + + @pytest.fixture() + def max_tilt_speed(self): + return 9 + + @pytest.fixture() + def search_target_strategy( + self, + camera_zoom_limit_controller, + camera_angle_limit_controller, + max_pan_speed, + max_tilt_speed): + return StaticSearchTargetStrategy( + pan_speed=max_pan_speed, + tilt_speed=max_tilt_speed, + camera_zoom_limit_controller=camera_zoom_limit_controller, + camera_angle_limit_controller=camera_angle_limit_controller) + + @pytest.fixture() + def update_current_camera_state( + self, + search_target_strategy, + camera_speeds, + max_pan_speed, + max_tilt_speed, + camera_zoom_limit_controller, + camera_angle_limit_controller): + def update_current_camera_state( + pan_angle: float, + pan_speed: float, + tilt_angle: float, + tilt_speed: float, + zoom_index: int, + zoom_ratio: float): + angles = Angles(pan_angle=pan_angle, pan_speed=pan_speed, + tilt_angle=tilt_angle, tilt_speed=tilt_speed) + search_target_strategy.update_current_angles(angles) + search_target_strategy.update_current_zoom_index(zoom_index) + search_target_strategy.update_current_zoom_ratio(zoom_ratio) + + camera_angle_limit_controller.update_current_angles(angles) + + camera_zoom_limit_controller.update_zoom_index(zoom_index) + + return update_current_camera_state + + def test_update_moves_to_target_from_left( + self, + search_target_strategy, + camera_speeds, + max_pan_speed, + max_tilt_speed, + update_current_camera_state): + update_current_camera_state( + pan_angle=0, + pan_speed=0, + tilt_angle=0, + tilt_speed=0, + zoom_index=0, + zoom_ratio=1.0) + search_target_strategy.update_target( + pan_angle=10.0, + tilt_angle=15.0, + zoom_index=20, + zoom_ratio=None) + + search_target_strategy.start() + + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW + + # target is quite close => camera should pan/tilt slower: + # max_speed / ratio + "percentage based on distance" + update_current_camera_state( + pan_angle=3.25, + pan_speed=max_pan_speed, + tilt_angle=8.25, + tilt_speed=max_tilt_speed, + zoom_index=11, + zoom_ratio=2.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 6.75 # distance to target + assert camera_speeds.tilt_speed == 6.75 # distance to target + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW + + # target is close + # => camera should pan/tilt even slower (according to zoom ratio): + # max_speed / ratio + update_current_camera_state( + pan_angle=8, + pan_speed=max_pan_speed, + tilt_angle=13, + tilt_speed=max_tilt_speed, + zoom_index=17, + zoom_ratio=3.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed / 3.0 + assert camera_speeds.tilt_speed == max_tilt_speed / 3.0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW + + # reach target + update_current_camera_state( + pan_angle=10.0, + pan_speed=max_pan_speed, + tilt_angle=15.0, + tilt_speed=max_tilt_speed, + zoom_index=20, + zoom_ratio=4.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.stop() + + # Strategy should consider that angles may pass over 360 to 0. + # Strategy should consider that zoom target might already be reached. + update_current_camera_state( + pan_angle=359, + pan_speed=0, + tilt_angle=351, + tilt_speed=0, + zoom_index=0, + zoom_ratio=1.0) + search_target_strategy.update_target( + pan_angle=10.0, + tilt_angle=5.0, + zoom_index=0, + zoom_ratio=None) + + search_target_strategy.start() + + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + # reach target + update_current_camera_state( + pan_angle=10.0, + pan_speed=max_pan_speed, + tilt_angle=5.0, + tilt_speed=max_tilt_speed, + zoom_index=21, + zoom_ratio=4.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.stop() + + def test_update_moves_to_target_from_right( + self, + search_target_strategy, + camera_speeds, + max_pan_speed, + max_tilt_speed, + update_current_camera_state): + update_current_camera_state( + pan_angle=20.0, + pan_speed=0, + tilt_angle=15.0, + tilt_speed=0, + zoom_index=40, + zoom_ratio=10.0) + search_target_strategy.update_target( + pan_angle=10.0, + tilt_angle=5.0, + zoom_index=20, + zoom_ratio=None) + + search_target_strategy.start() + + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == -max_pan_speed + assert camera_speeds.tilt_speed == -max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_OUT_SLOW + + # target is quite close => camera should pan/tilt slower: + # max_speed / ratio + "percentage based on distance" + update_current_camera_state( + pan_angle=16.75, + pan_speed=max_pan_speed, + tilt_angle=11.75, + tilt_speed=max_tilt_speed, + zoom_index=30, + zoom_ratio=6.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == -6.75 # distance to target + assert camera_speeds.tilt_speed == -6.75 # distance to target + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_OUT_SLOW + + # target is close + # => camera should pan/tilt even slower (according to zoom ratio): + # max_speed / ratio + update_current_camera_state( + pan_angle=11.5, + pan_speed=max_pan_speed, + tilt_angle=6.5, + tilt_speed=max_tilt_speed, + zoom_index=25, + zoom_ratio=5.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == -max_pan_speed / 5.0 + assert camera_speeds.tilt_speed == -max_tilt_speed / 5.0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_OUT_SLOW + + # reach target + update_current_camera_state( + pan_angle=10.0, + pan_speed=-max_pan_speed, + tilt_angle=5.0, + tilt_speed=-max_tilt_speed, + zoom_index=20, + zoom_ratio=4.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.stop() + + # Strategy should consider that angles may pass over 360 to 0. + # Strategy should consider that zoom target might already be reached. + update_current_camera_state( + pan_angle=1, + pan_speed=0, + tilt_angle=11, + tilt_speed=0, + zoom_index=0, + zoom_ratio=1.0) + search_target_strategy.update_target( + pan_angle=351.0, + tilt_angle=356.0, + zoom_index=0, + zoom_ratio=None) + + search_target_strategy.start() + + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == -max_pan_speed + assert camera_speeds.tilt_speed == -max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + # reach target + update_current_camera_state( + pan_angle=351.0, + pan_speed=-max_pan_speed, + tilt_angle=356.0, + tilt_speed=-max_tilt_speed, + zoom_index=0, + zoom_ratio=1.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.stop() From 735e7ad4ffd45577f214b758ff03999a95307a50 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 2 Nov 2022 17:12:00 +0100 Subject: [PATCH 088/109] refactor: simplify calculation of pan and tilt "close speed" --- robot_cameraman/tracking.py | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 0d52848..91975e5 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -560,19 +560,14 @@ def update(self, camera_speeds: CameraSpeeds) -> None: right=self._current_pan_angle) if pan_distance < abs(self._camera_speeds.pan_speed): accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio - # use "accurate speed" - camera_speeds.pan_speed = accurate_pan_speed - # increase to "close speed" + # use "close speed" if pan_distance > abs(accurate_pan_speed): - percentage = ( - abs(pan_distance - abs(accurate_pan_speed)) - / abs(self._camera_speeds.pan_speed - - accurate_pan_speed)) - camera_speeds.pan_speed += ( + camera_speeds.pan_speed = ( numpy.sign(self._camera_speeds.pan_speed) - * percentage - * abs(self._camera_speeds.pan_speed - - accurate_pan_speed)) + * pan_distance) + else: + # use "accurate speed" + camera_speeds.pan_speed = accurate_pan_speed else: # use "max speed", since: distance > max speed camera_speeds.pan_speed = self._camera_speeds.pan_speed @@ -581,19 +576,14 @@ def update(self, camera_speeds: CameraSpeeds) -> None: right=self._current_tilt_angle) if tilt_distance < abs(self._camera_speeds.tilt_speed): accurate_tilt_speed = self._camera_speeds.tilt_speed / zoom_ratio - # use "accurate speed" - camera_speeds.tilt_speed = accurate_tilt_speed - # increase to "close speed" + # use "close speed" if tilt_distance > abs(accurate_tilt_speed): - percentage = ( - abs(tilt_distance - abs(accurate_tilt_speed)) - / abs(self._camera_speeds.tilt_speed - - accurate_tilt_speed)) - camera_speeds.tilt_speed += ( + camera_speeds.tilt_speed = ( numpy.sign(self._camera_speeds.tilt_speed) - * percentage - * abs(self._camera_speeds.tilt_speed - - accurate_tilt_speed)) + * tilt_distance) + else: + # use "accurate speed" + camera_speeds.tilt_speed = accurate_tilt_speed else: # use "max speed", since: distance > max speed camera_speeds.tilt_speed = self._camera_speeds.tilt_speed From fdf2b296310517738ee311f9af2e5098a561ae5e Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 2 Nov 2022 17:15:08 +0100 Subject: [PATCH 089/109] refactor: invert if condition to move case "max speed" before others Same order as in diagram. --- robot_cameraman/tracking.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 91975e5..67ca69e 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -558,7 +558,10 @@ def update(self, camera_speeds: CameraSpeeds) -> None: pan_distance = get_angle_distance(left=self._target_pan_angle, right=self._current_pan_angle) - if pan_distance < abs(self._camera_speeds.pan_speed): + if pan_distance >= abs(self._camera_speeds.pan_speed): + # use "max speed", since: distance > max speed + camera_speeds.pan_speed = self._camera_speeds.pan_speed + else: accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio # use "close speed" if pan_distance > abs(accurate_pan_speed): @@ -568,13 +571,13 @@ def update(self, camera_speeds: CameraSpeeds) -> None: else: # use "accurate speed" camera_speeds.pan_speed = accurate_pan_speed - else: - # use "max speed", since: distance > max speed - camera_speeds.pan_speed = self._camera_speeds.pan_speed tilt_distance = get_angle_distance(left=self._target_tilt_angle, right=self._current_tilt_angle) - if tilt_distance < abs(self._camera_speeds.tilt_speed): + if tilt_distance >= abs(self._camera_speeds.tilt_speed): + # use "max speed", since: distance > max speed + camera_speeds.tilt_speed = self._camera_speeds.tilt_speed + else: accurate_tilt_speed = self._camera_speeds.tilt_speed / zoom_ratio # use "close speed" if tilt_distance > abs(accurate_tilt_speed): @@ -584,9 +587,6 @@ def update(self, camera_speeds: CameraSpeeds) -> None: else: # use "accurate speed" camera_speeds.tilt_speed = accurate_tilt_speed - else: - # use "max speed", since: distance > max speed - camera_speeds.tilt_speed = self._camera_speeds.tilt_speed # TODO add option to zoom not until "close speed" (pan and tilt) # is reached, since focus might be lost (=> blurry image), From 2faca236b995afd897f04716047cd5105a2e9936 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Wed, 2 Nov 2022 17:58:55 +0100 Subject: [PATCH 090/109] feat. add option to zoom not before pan and tilt searching targets are reached --- robot_cameraman/tracking.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 67ca69e..05eef96 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -413,6 +413,7 @@ def __init__( self._current_zoom_ratio = None self._camera_speeds = CameraSpeeds() self._is_searching = False + self.is_zoom_while_rotating = True # TODO remove test targets below self._target_pan_angle = 10.0 @@ -420,7 +421,7 @@ def __init__( # self._target_zoom_index = 10 # self._target_zoom_ratio = 2.0 - # TODO add UI for target + # TODO add UI for target and is_zoom_while_rotating def start(self) -> None: assert not self._is_searching @@ -588,12 +589,12 @@ def update(self, camera_speeds: CameraSpeeds) -> None: # use "accurate speed" camera_speeds.tilt_speed = accurate_tilt_speed - # TODO add option to zoom not until "close speed" (pan and tilt) - # is reached, since focus might be lost (=> blurry image), - # while camera pans/tilts (too) fast (for current zoom ratio). - camera_speeds.zoom_speed = self._camera_speeds.zoom_speed - self._camera_zoom_limit_controller.update(camera_speeds) self._camera_angle_limit_controller.update(camera_speeds) + if (self.is_zoom_while_rotating + or (camera_speeds.pan_speed == 0 + and camera_speeds.tilt_speed == 0)): + camera_speeds.zoom_speed = self._camera_speeds.zoom_speed + self._camera_zoom_limit_controller.update(camera_speeds) def stop(self): self._is_searching = False From 1cb5f4c82e3d5271f6c7c479625ad775db796cea Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 14:59:07 +0100 Subject: [PATCH 091/109] fix: avoid verbose error logs of ssdp module --- robot_cameraman/__main__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index af996e8..fdd6ac5 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -291,6 +291,10 @@ def configure_logging(): # add the handler to the root logger logging.getLogger('').addHandler(console) + # some devices on the network may cause upnpclient.discover to log quite + # verbose error messages. The log level is changed to avoid this + logging.getLogger('ssdp').setLevel(logging.CRITICAL) + args = parse_arguments() configure_logging() From 203b2cd3d4cb5b77efe74b6a6dec442341f2e521 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:04:32 +0100 Subject: [PATCH 092/109] fix: change log level of werkzeug to WARNING --- robot_cameraman/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index fdd6ac5..e1c5394 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -294,6 +294,11 @@ def configure_logging(): # some devices on the network may cause upnpclient.discover to log quite # verbose error messages. The log level is changed to avoid this logging.getLogger('ssdp').setLevel(logging.CRITICAL) + # The web UI does frequent requests regarding the status bar + # that spam the logs. Usually, those INFO logs are not important, + # since the relevant interaction is covered by logs of other modules. + # Hence, the log level of werkzeug is changed. + logging.getLogger('werkzeug').setLevel(logging.WARNING) args = parse_arguments() From 9fb1de42e0a29a783dbe77d2357a574a8aef98f4 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:20:23 +0100 Subject: [PATCH 093/109] fix: only update camera pan and tilt speed if target and current angles are set `get_angle_distance` throws an exception if passed angles is `None`. --- robot_cameraman/tracking.py | 63 ++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 05eef96..1dffff5 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -557,37 +557,42 @@ def update(self, camera_speeds: CameraSpeeds) -> None: # distance = 0 ----------------------/ zoom_ratio = self._current_zoom_ratio or 1.0 - pan_distance = get_angle_distance(left=self._target_pan_angle, - right=self._current_pan_angle) - if pan_distance >= abs(self._camera_speeds.pan_speed): - # use "max speed", since: distance > max speed - camera_speeds.pan_speed = self._camera_speeds.pan_speed - else: - accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio - # use "close speed" - if pan_distance > abs(accurate_pan_speed): - camera_speeds.pan_speed = ( - numpy.sign(self._camera_speeds.pan_speed) - * pan_distance) + if (self._target_pan_angle is not None + and self._current_pan_angle is not None): + pan_distance = get_angle_distance(left=self._target_pan_angle, + right=self._current_pan_angle) + if pan_distance >= abs(self._camera_speeds.pan_speed): + # use "max speed", since: distance > max speed + camera_speeds.pan_speed = self._camera_speeds.pan_speed else: - # use "accurate speed" - camera_speeds.pan_speed = accurate_pan_speed - - tilt_distance = get_angle_distance(left=self._target_tilt_angle, - right=self._current_tilt_angle) - if tilt_distance >= abs(self._camera_speeds.tilt_speed): - # use "max speed", since: distance > max speed - camera_speeds.tilt_speed = self._camera_speeds.tilt_speed - else: - accurate_tilt_speed = self._camera_speeds.tilt_speed / zoom_ratio - # use "close speed" - if tilt_distance > abs(accurate_tilt_speed): - camera_speeds.tilt_speed = ( - numpy.sign(self._camera_speeds.tilt_speed) - * tilt_distance) + accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio + # use "close speed" + if pan_distance > abs(accurate_pan_speed): + camera_speeds.pan_speed = ( + numpy.sign(self._camera_speeds.pan_speed) + * pan_distance) + else: + # use "accurate speed" + camera_speeds.pan_speed = accurate_pan_speed + + if (self._target_tilt_angle is not None + and self._current_tilt_angle is not None): + tilt_distance = get_angle_distance(left=self._target_tilt_angle, + right=self._current_tilt_angle) + if tilt_distance >= abs(self._camera_speeds.tilt_speed): + # use "max speed", since: distance > max speed + camera_speeds.tilt_speed = self._camera_speeds.tilt_speed else: - # use "accurate speed" - camera_speeds.tilt_speed = accurate_tilt_speed + accurate_tilt_speed = \ + self._camera_speeds.tilt_speed / zoom_ratio + # use "close speed" + if tilt_distance > abs(accurate_tilt_speed): + camera_speeds.tilt_speed = ( + numpy.sign(self._camera_speeds.tilt_speed) + * tilt_distance) + else: + # use "accurate speed" + camera_speeds.tilt_speed = accurate_tilt_speed self._camera_angle_limit_controller.update(camera_speeds) if (self.is_zoom_while_rotating From 20d0dad82bed9a0ddeec4b29418ac84f3b5a69d9 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:41:50 +0100 Subject: [PATCH 094/109] refactor: rename `_camera_speeds` to `_camera_base_speeds` --- robot_cameraman/tracking.py | 45 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 1dffff5..6b8c78f 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -390,7 +390,7 @@ class StaticSearchTargetStrategy(SearchTargetStrategy): _current_zoom_index: Optional[int] _current_zoom_ratio: Optional[float] - _camera_speeds: CameraSpeeds + _camera_base_speeds: CameraSpeeds _is_searching: bool def __init__( @@ -411,7 +411,7 @@ def __init__( self._current_tilt_angle = None self._current_zoom_index = None self._current_zoom_ratio = None - self._camera_speeds = CameraSpeeds() + self._camera_base_speeds = CameraSpeeds() self._is_searching = False self.is_zoom_while_rotating = True @@ -433,13 +433,13 @@ def start(self) -> None: delta_pan_angle_clockwise = get_delta_angle_clockwise( left=self._current_pan_angle, right=self._target_pan_angle) if delta_pan_angle_clockwise < 180: - self._camera_speeds.pan_speed = self.pan_speed + self._camera_base_speeds.pan_speed = self.pan_speed self._camera_angle_limit_controller.min_pan_angle = \ self._current_pan_angle self._camera_angle_limit_controller.max_pan_angle = \ self._target_pan_angle else: - self._camera_speeds.pan_speed = -self.pan_speed + self._camera_base_speeds.pan_speed = -self.pan_speed self._camera_angle_limit_controller.min_pan_angle = \ self._target_pan_angle self._camera_angle_limit_controller.max_pan_angle = \ @@ -449,13 +449,13 @@ def start(self) -> None: delta_tilt_angle_clockwise = get_delta_angle_clockwise( left=self._current_tilt_angle, right=self._target_tilt_angle) if delta_tilt_angle_clockwise < 180: - self._camera_speeds.tilt_speed = self.tilt_speed + self._camera_base_speeds.tilt_speed = self.tilt_speed self._camera_angle_limit_controller.min_tilt_angle = \ self._current_tilt_angle self._camera_angle_limit_controller.max_tilt_angle = \ self._target_tilt_angle else: - self._camera_speeds.tilt_speed = -self.tilt_speed + self._camera_base_speeds.tilt_speed = -self.tilt_speed self._camera_angle_limit_controller.min_tilt_angle = \ self._target_tilt_angle self._camera_angle_limit_controller.max_tilt_angle = \ @@ -465,17 +465,17 @@ def start(self) -> None: assert isinstance(self._camera_zoom_limit_controller, CameraZoomRatioLimitController) if self._current_zoom_ratio < self._target_zoom_ratio: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW self._camera_zoom_limit_controller.min_zoom_ratio = None self._camera_zoom_limit_controller.max_zoom_ratio = \ self._target_zoom_ratio elif self._current_zoom_ratio > self._target_zoom_ratio: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW self._camera_zoom_limit_controller.min_zoom_ratio = \ self._target_zoom_ratio self._camera_zoom_limit_controller.max_zoom_ratio = None else: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED self._camera_zoom_limit_controller.min_zoom_ratio = None self._camera_zoom_limit_controller.max_zoom_ratio = None if (self._target_zoom_index is not None @@ -483,17 +483,17 @@ def start(self) -> None: assert isinstance(self._camera_zoom_limit_controller, CameraZoomIndexLimitController) if self._current_zoom_index < self._target_zoom_index: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_IN_SLOW self._camera_zoom_limit_controller.min_zoom_index = None self._camera_zoom_limit_controller.max_zoom_index = \ self._target_zoom_index elif self._current_zoom_index > self._target_zoom_index: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_OUT_SLOW self._camera_zoom_limit_controller.min_zoom_index = \ self._target_zoom_index self._camera_zoom_limit_controller.max_zoom_index = None else: - self._camera_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED + self._camera_base_speeds.zoom_speed = ZoomSpeed.ZOOM_STOPPED self._camera_zoom_limit_controller.min_zoom_index = None self._camera_zoom_limit_controller.max_zoom_index = None @@ -561,15 +561,16 @@ def update(self, camera_speeds: CameraSpeeds) -> None: and self._current_pan_angle is not None): pan_distance = get_angle_distance(left=self._target_pan_angle, right=self._current_pan_angle) - if pan_distance >= abs(self._camera_speeds.pan_speed): + if pan_distance >= abs(self._camera_base_speeds.pan_speed): # use "max speed", since: distance > max speed - camera_speeds.pan_speed = self._camera_speeds.pan_speed + camera_speeds.pan_speed = self._camera_base_speeds.pan_speed else: - accurate_pan_speed = self._camera_speeds.pan_speed / zoom_ratio + accurate_pan_speed = \ + self._camera_base_speeds.pan_speed / zoom_ratio # use "close speed" if pan_distance > abs(accurate_pan_speed): camera_speeds.pan_speed = ( - numpy.sign(self._camera_speeds.pan_speed) + numpy.sign(self._camera_base_speeds.pan_speed) * pan_distance) else: # use "accurate speed" @@ -579,16 +580,16 @@ def update(self, camera_speeds: CameraSpeeds) -> None: and self._current_tilt_angle is not None): tilt_distance = get_angle_distance(left=self._target_tilt_angle, right=self._current_tilt_angle) - if tilt_distance >= abs(self._camera_speeds.tilt_speed): + if tilt_distance >= abs(self._camera_base_speeds.tilt_speed): # use "max speed", since: distance > max speed - camera_speeds.tilt_speed = self._camera_speeds.tilt_speed + camera_speeds.tilt_speed = self._camera_base_speeds.tilt_speed else: accurate_tilt_speed = \ - self._camera_speeds.tilt_speed / zoom_ratio + self._camera_base_speeds.tilt_speed / zoom_ratio # use "close speed" if tilt_distance > abs(accurate_tilt_speed): camera_speeds.tilt_speed = ( - numpy.sign(self._camera_speeds.tilt_speed) + numpy.sign(self._camera_base_speeds.tilt_speed) * tilt_distance) else: # use "accurate speed" @@ -598,9 +599,9 @@ def update(self, camera_speeds: CameraSpeeds) -> None: if (self.is_zoom_while_rotating or (camera_speeds.pan_speed == 0 and camera_speeds.tilt_speed == 0)): - camera_speeds.zoom_speed = self._camera_speeds.zoom_speed + camera_speeds.zoom_speed = self._camera_base_speeds.zoom_speed self._camera_zoom_limit_controller.update(camera_speeds) def stop(self): self._is_searching = False - self._camera_speeds.reset() + self._camera_base_speeds.reset() From b48959eea49574d2833bb84decfeee01a4eb2b88 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:42:14 +0100 Subject: [PATCH 095/109] doc: document `_camera_base_speeds` --- robot_cameraman/tracking.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 6b8c78f..5727292 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -391,6 +391,12 @@ class StaticSearchTargetStrategy(SearchTargetStrategy): _current_zoom_ratio: Optional[float] _camera_base_speeds: CameraSpeeds + """The fastest speeds that move to the target. They are used in + :py:meth:`~robot_cameraman.tracking.StaticSearchTargetStrategy.update` + to calculate the actual camera speeds. The actual speeds might be decreased, + when close to the target. + """ + _is_searching: bool def __init__( From a7ed92a8f897efa2007efd35baf25460bf9d04d5 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:55:03 +0100 Subject: [PATCH 096/109] fix: uncomment test targets used in early development Unit tests require/assume no (implicit) default target. --- robot_cameraman/tracking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 5727292..f341c23 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -422,8 +422,8 @@ def __init__( self.is_zoom_while_rotating = True # TODO remove test targets below - self._target_pan_angle = 10.0 - self._target_tilt_angle = 5.0 + # self._target_pan_angle = 10.0 + # self._target_tilt_angle = 5.0 # self._target_zoom_index = 10 # self._target_zoom_ratio = 2.0 From 442740517be66b2e9f3c5bc0ea3a581dcbcfb3f4 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 16:56:42 +0100 Subject: [PATCH 097/109] fix: update camera base speeds if target is set after start --- robot_cameraman/tracking.py | 10 +++++++- tests/robot_cameraman/test_tracking.py | 32 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index f341c23..55937b5 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -397,6 +397,7 @@ class StaticSearchTargetStrategy(SearchTargetStrategy): when close to the target. """ + _is_update_od_camera_base_speeds_required: bool _is_searching: bool def __init__( @@ -418,6 +419,7 @@ def __init__( self._current_zoom_index = None self._current_zoom_ratio = None self._camera_base_speeds = CameraSpeeds() + self._is_update_od_camera_base_speeds_required = False self._is_searching = False self.is_zoom_while_rotating = True @@ -434,6 +436,9 @@ def start(self) -> None: assert self._current_pan_angle is not None assert self._current_tilt_angle is not None self._is_searching = True + self._update_camera_base_speeds() + + def _update_camera_base_speeds(self) -> None: if (self._target_pan_angle is not None and self._current_pan_angle is not None): delta_pan_angle_clockwise = get_delta_angle_clockwise( @@ -513,7 +518,7 @@ def update_target( self._target_tilt_angle = tilt_angle self._target_zoom_index = zoom_index self._target_zoom_ratio = zoom_ratio - # TODO update camera speeds if called before start + self._is_update_od_camera_base_speeds_required = True def update_current_angles(self, angles: Angles): self._current_pan_angle = angles.pan_angle @@ -533,6 +538,9 @@ def update_current_zoom_index(self, zoom_index: int): def update(self, camera_speeds: CameraSpeeds) -> None: assert self._is_searching + if self._is_update_od_camera_base_speeds_required: + self._update_camera_base_speeds() + self._is_update_od_camera_base_speeds_required = False # The live view of the camera moves faster at higher zoom ratios. # Usually the pan and tilt speed should depend on the zoom ratio # (see https://github.com/maiermic/robot-cameraman/issues/13). diff --git a/tests/robot_cameraman/test_tracking.py b/tests/robot_cameraman/test_tracking.py index ae141a8..a035d25 100644 --- a/tests/robot_cameraman/test_tracking.py +++ b/tests/robot_cameraman/test_tracking.py @@ -284,3 +284,35 @@ def test_update_moves_to_target_from_right( assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED search_target_strategy.stop() + + def test_update_does_not_move_before_target_is_set( + self, + search_target_strategy, + camera_speeds, + max_pan_speed, + max_tilt_speed, + update_current_camera_state): + update_current_camera_state( + pan_angle=0, + pan_speed=0, + tilt_angle=0, + tilt_speed=0, + zoom_index=0, + zoom_ratio=1.0) + + search_target_strategy.start() + + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.update_target( + pan_angle=30.0, + tilt_angle=15.0, + zoom_index=20, + zoom_ratio=None) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW From a2c1e1243248f90d5cf2f0f7aec261322e4b24d9 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 17:00:46 +0100 Subject: [PATCH 098/109] fix: remove assertions that current angles have to be set at start There are checks in place that allow the current angles to be set later. --- robot_cameraman/tracking.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 55937b5..8e6c8d7 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -433,8 +433,6 @@ def __init__( def start(self) -> None: assert not self._is_searching - assert self._current_pan_angle is not None - assert self._current_tilt_angle is not None self._is_searching = True self._update_camera_base_speeds() From f735432f7dfc0185f64eee5d00cf4464fc1c61d5 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 17:02:01 +0100 Subject: [PATCH 099/109] fix: update camera base speeds if current angles are set after target --- robot_cameraman/tracking.py | 7 ++++-- tests/robot_cameraman/test_tracking.py | 31 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 8e6c8d7..ffa322b 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -519,10 +519,13 @@ def update_target( self._is_update_od_camera_base_speeds_required = True def update_current_angles(self, angles: Angles): + if (self._current_pan_angle is None + and self._current_tilt_angle is None + and self._target_pan_angle is not None + and self._target_tilt_angle is not None): + self._is_update_od_camera_base_speeds_required = True self._current_pan_angle = angles.pan_angle self._current_tilt_angle = angles.tilt_angle - # TODO calculate camera speeds if current angles are set the first time - # and target angles are already set def update_current_zoom_ratio(self, zoom_ratio: float): self._current_zoom_ratio = zoom_ratio diff --git a/tests/robot_cameraman/test_tracking.py b/tests/robot_cameraman/test_tracking.py index a035d25..1fa4e32 100644 --- a/tests/robot_cameraman/test_tracking.py +++ b/tests/robot_cameraman/test_tracking.py @@ -316,3 +316,34 @@ def test_update_does_not_move_before_target_is_set( assert camera_speeds.pan_speed == max_pan_speed assert camera_speeds.tilt_speed == max_tilt_speed assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW + + def test_update_does_not_move_before_current_angles_are_set( + self, + search_target_strategy, + camera_speeds, + max_pan_speed, + max_tilt_speed, + update_current_camera_state): + search_target_strategy.start() + + search_target_strategy.update_target( + pan_angle=30.0, + tilt_angle=15.0, + zoom_index=20, + zoom_ratio=None) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + update_current_camera_state( + pan_angle=0, + pan_speed=0, + tilt_angle=0, + tilt_speed=0, + zoom_index=0, + zoom_ratio=1.0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW From f55034ab4e7dcbb9fce505db18f85a07b3de248e Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 17:10:56 +0100 Subject: [PATCH 100/109] fix: update camera base speeds if zoom index is set after target --- robot_cameraman/tracking.py | 10 ++++--- tests/robot_cameraman/test_tracking.py | 38 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index ffa322b..adf6c41 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -528,14 +528,16 @@ def update_current_angles(self, angles: Angles): self._current_tilt_angle = angles.tilt_angle def update_current_zoom_ratio(self, zoom_ratio: float): + if (self._current_zoom_ratio is None + and self._target_zoom_ratio is not None): + self._is_update_od_camera_base_speeds_required = True self._current_zoom_ratio = zoom_ratio - # TODO calculate camera zoom speed if current zoom ratio is set the - # first time and target zoom speed is already set def update_current_zoom_index(self, zoom_index: int): + if (self._current_zoom_index is None + and self._target_zoom_index is not None): + self._is_update_od_camera_base_speeds_required = True self._current_zoom_index = zoom_index - # TODO calculate camera zoom speed if current zoom index is set the - # first time and target zoom speed is already set def update(self, camera_speeds: CameraSpeeds) -> None: assert self._is_searching diff --git a/tests/robot_cameraman/test_tracking.py b/tests/robot_cameraman/test_tracking.py index 1fa4e32..20b8ebd 100644 --- a/tests/robot_cameraman/test_tracking.py +++ b/tests/robot_cameraman/test_tracking.py @@ -347,3 +347,41 @@ def test_update_does_not_move_before_current_angles_are_set( assert camera_speeds.pan_speed == max_pan_speed assert camera_speeds.tilt_speed == max_tilt_speed assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW + + def test_update_does_not_zoom_before_current_zoom_is_set( + self, + search_target_strategy, + camera_angle_limit_controller, + camera_zoom_limit_controller, + camera_speeds, + max_pan_speed, + max_tilt_speed, + update_current_camera_state): + search_target_strategy.start() + + search_target_strategy.update_target( + pan_angle=30.0, + tilt_angle=15.0, + zoom_index=20, + zoom_ratio=None) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == 0 + assert camera_speeds.tilt_speed == 0 + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + # only update angles + angles = Angles(pan_angle=0, pan_speed=0, tilt_angle=0, tilt_speed=0) + search_target_strategy.update_current_angles(angles) + camera_angle_limit_controller.update_current_angles(angles) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_STOPPED + + search_target_strategy.update_current_zoom_index(0) + search_target_strategy.update_current_zoom_ratio(1.0) + camera_zoom_limit_controller.update_zoom_index(0) + search_target_strategy.update(camera_speeds) + assert camera_speeds.pan_speed == max_pan_speed + assert camera_speeds.tilt_speed == max_tilt_speed + assert camera_speeds.zoom_speed == ZoomSpeed.ZOOM_IN_SLOW From 3b8c572dc780edd0f9880530524e5168738b5395 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sat, 5 Nov 2022 17:12:51 +0100 Subject: [PATCH 101/109] refactor: remove done TODO update zoom index and ratio --- robot_cameraman/tracking.py | 1 - 1 file changed, 1 deletion(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index adf6c41..0bba3fe 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -386,7 +386,6 @@ class StaticSearchTargetStrategy(SearchTargetStrategy): _current_pan_angle: Optional[float] _current_tilt_angle: Optional[float] - # TODO update zoom index and ratio _current_zoom_index: Optional[int] _current_zoom_ratio: Optional[float] From 25441bfcfc186ecc5bb7198de66167217eb5d246 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 6 Nov 2022 00:12:32 +0100 Subject: [PATCH 102/109] feat: add dummy live view --- robot_cameraman/__main__.py | 6 ++++-- robot_cameraman/live_view.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index e1c5394..b55e7ac 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -30,7 +30,7 @@ from robot_cameraman.image_detection import DummyDetectionEngine, \ EdgeTpuDetectionEngine from robot_cameraman.live_view import WebcamLiveView, PanasonicLiveView, \ - ImageSize + ImageSize, DummyLiveView from robot_cameraman.max_speed_and_acceleration_updater import \ MaxSpeedAndAccelerationUpdater from robot_cameraman.object_tracking import ObjectTracker @@ -142,7 +142,7 @@ def parse_arguments() -> RobotCameramanArguments: parser.add_argument('--liveView', type=str, default='Panasonic', help="The live view (camera) to use." - "Either 'Panasonic' or 'Webcam'") + "Either 'Panasonic', 'Webcam' or 'Dummy'") parser.add_argument('--ip', type=str, default='0.0.0.0', help="UDP Socket IP address of Panasonic live view.") @@ -429,6 +429,8 @@ def create_camera_zoom_limit_controller(): if args.liveView == 'Webcam': live_view = WebcamLiveView() +elif args.liveView == 'Dummy': + live_view = DummyLiveView(live_view_image_size) elif args.liveView == 'Panasonic': live_view = PanasonicLiveView(args.ip, args.port) camera_observable = PanasonicCameraObservable( diff --git a/robot_cameraman/live_view.py b/robot_cameraman/live_view.py index 56c9538..d6089c0 100644 --- a/robot_cameraman/live_view.py +++ b/robot_cameraman/live_view.py @@ -2,6 +2,7 @@ import logging import socket from logging import Logger +from time import sleep from typing import Optional, NamedTuple import PIL.Image @@ -10,6 +11,7 @@ import PIL.ImageFile import PIL.ImageFont import cv2 +import PIL from PIL.Image import Image from typing_extensions import Protocol @@ -51,3 +53,12 @@ def image(self) -> Optional[Image]: image = self._video_stream.read() rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) return PIL.Image.fromarray(rgb_image) + + +class DummyLiveView(LiveView): + def __init__(self, size: ImageSize) -> None: + self._image = PIL.Image.new('RGB', size, (228, 150, 150)) + + def image(self) -> Optional[Image]: + sleep(0.2) + return self._image From 69e6447885cd4a114226d4ca2a4bf174c217975a Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 6 Nov 2022 20:14:05 +0100 Subject: [PATCH 103/109] fix: only update defined targets --- robot_cameraman/tracking.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/robot_cameraman/tracking.py b/robot_cameraman/tracking.py index 0bba3fe..f6ba09e 100644 --- a/robot_cameraman/tracking.py +++ b/robot_cameraman/tracking.py @@ -511,10 +511,14 @@ def update_target( tilt_angle: Optional[float], zoom_index: Optional[int], zoom_ratio: Optional[float]): - self._target_pan_angle = pan_angle - self._target_tilt_angle = tilt_angle - self._target_zoom_index = zoom_index - self._target_zoom_ratio = zoom_ratio + if pan_angle is not None: + self._target_pan_angle = pan_angle + if tilt_angle is not None: + self._target_tilt_angle = tilt_angle + if zoom_index is not None: + self._target_zoom_index = zoom_index + if zoom_ratio is not None: + self._target_zoom_ratio = zoom_ratio self._is_update_od_camera_base_speeds_required = True def update_current_angles(self, angles: Angles): From ce7681e8205ef0d48fdecd639cde85d7590de7d8 Mon Sep 17 00:00:00 2001 From: Michael Maier Date: Sun, 6 Nov 2022 20:15:36 +0100 Subject: [PATCH 104/109] feat: add UI for static search strategy --- .idea/inspectionProfiles/Project_Default.xml | 3 +- robot_cameraman/__main__.py | 3 +- robot_cameraman/server.py | 3 + .../server_static_folder/index.html | 10 + .../search-strategy-menu.js | 240 ++++++++++++++++++ robot_cameraman/tracking.py | 8 - robot_cameraman/updatable_configuration.py | 52 +++- 7 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 robot_cameraman/server_static_folder/search-strategy-menu.js diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e538064..aab4c57 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -15,7 +15,7 @@ diff --git a/robot_cameraman/__main__.py b/robot_cameraman/__main__.py index b55e7ac..d9fad7d 100644 --- a/robot_cameraman/__main__.py +++ b/robot_cameraman/__main__.py @@ -504,7 +504,8 @@ def create_camera_zoom_limit_controller(): camera_zoom_limit_controller=camera_zoom_limit_controller, camera_angle_limit_controller=camera_angle_limit_controller, configuration_file=args.config, - camera_zoom_ratio_index_ranges=camera_zoom_ratio_index_ranges), + camera_zoom_ratio_index_ranges=camera_zoom_ratio_index_ranges, + search_target_strategy=search_target_strategy), _status_bar=status_bar, ssl_certificate=args.ssl_certificate, ssl_key=args.ssl_key) diff --git a/robot_cameraman/server.py b/robot_cameraman/server.py index b1e069b..22a4fab 100644 --- a/robot_cameraman/server.py +++ b/robot_cameraman/server.py @@ -155,6 +155,9 @@ def update_configuration(): max_hsv=color['max_hsv']) if 'limits' in request.json: updatable_configuration.update_limits(request.json['limits']) + if 'searchTarget' in request.json: + updatable_configuration.update_search_target( + request.json['searchTarget']) return '', 200 diff --git a/robot_cameraman/server_static_folder/index.html b/robot_cameraman/server_static_folder/index.html index a693ef4..6c97d6c 100644 --- a/robot_cameraman/server_static_folder/index.html +++ b/robot_cameraman/server_static_folder/index.html @@ -164,6 +164,7 @@ +
@@ -209,6 +210,11 @@ limits
+ + +