-
Notifications
You must be signed in to change notification settings - Fork 12
Record each ZED camera's read-back exposure, gain and white balance per episode #741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Stand-in for the ZED vendor package. | ||
|
|
||
| ``pyzed`` installs with the ZED SDK and nowhere else, so the driver cannot be imported from a default sync. | ||
| The tests read the driver's state against their own fake camera and never call into the vendor, so a module | ||
| carrying the names the driver binds at import is enough. Installed here, before any test module imports the | ||
| driver, and only when the real package is absent. | ||
| """ | ||
|
|
||
| import importlib.util | ||
| import sys | ||
| import types | ||
| from enum import Enum | ||
|
|
||
| PACKAGE = 'pyzed' | ||
| SL = f'{PACKAGE}.sl' | ||
|
|
||
|
|
||
| class ErrorCode(Enum): | ||
| SUCCESS = 0 | ||
| FAILURE = 1 | ||
|
|
||
|
|
||
| class VideoSettings(Enum): | ||
| EXPOSURE = 0 | ||
| GAIN = 1 | ||
| WHITEBALANCE_TEMPERATURE = 2 | ||
| AEC_AGC = 3 | ||
| WHITEBALANCE_AUTO = 4 | ||
|
|
||
|
|
||
| if importlib.util.find_spec(PACKAGE) is None: | ||
| sl = types.ModuleType(SL) | ||
| sl.__dict__.update(ERROR_CODE=ErrorCode, VIDEO_SETTINGS=VideoSettings) | ||
| package = types.ModuleType(PACKAGE) | ||
| package.__dict__.update(sl=sl) | ||
| sys.modules.update({PACKAGE: package, SL: sl}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| from positronic.drivers.camera import zed | ||
| from positronic.drivers.camera.zed import CAMERA_STATE_SETTINGS, read_camera_state | ||
|
|
||
| # The SDK namespace the driver bound at import: the vendor's when installed, the conftest's stand-in otherwise. | ||
| sl = zed.sl | ||
|
|
||
|
|
||
| class FakeCamera: | ||
| """Answers each setting with the value it holds, and refuses the ones it does not.""" | ||
|
|
||
| def __init__(self, values: dict): | ||
| self._values = values | ||
|
|
||
| def get_camera_settings(self, setting) -> tuple: | ||
| if setting in self._values: | ||
| return sl.ERROR_CODE.SUCCESS, self._values[setting] | ||
| return sl.ERROR_CODE.FAILURE, -1 | ||
|
|
||
|
|
||
| def test_read_camera_state_reports_every_setting_the_camera_answers(): | ||
| camera = FakeCamera({ | ||
| sl.VIDEO_SETTINGS.EXPOSURE: 45, | ||
| sl.VIDEO_SETTINGS.GAIN: 12, | ||
| sl.VIDEO_SETTINGS.WHITEBALANCE_TEMPERATURE: 4700, | ||
| sl.VIDEO_SETTINGS.AEC_AGC: 1, | ||
| sl.VIDEO_SETTINGS.WHITEBALANCE_AUTO: 1, | ||
| }) | ||
|
|
||
| assert read_camera_state(camera) == { | ||
| 'exposure': 45, | ||
| 'gain': 12, | ||
| 'white_balance_temperature': 4700, | ||
| 'auto_exposure': 1, | ||
| 'auto_white_balance': 1, | ||
| } | ||
|
|
||
|
|
||
| def test_read_camera_state_leaves_out_a_setting_the_camera_refuses(): | ||
| camera = FakeCamera({sl.VIDEO_SETTINGS.EXPOSURE: 45}) | ||
|
|
||
| assert read_camera_state(camera) == {'exposure': 45} | ||
|
|
||
|
|
||
| def test_every_recorded_setting_names_a_video_setting(): | ||
| assert all(hasattr(sl.VIDEO_SETTINGS, sdk_name) for sdk_name in CAMERA_STATE_SETTINGS.values()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,27 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # The settings a camera's automatic control moves, keyed by the name each records under, valued by the SDK's | ||
| # ``VIDEO_SETTINGS`` member. The two ``auto_*`` flags say whether the values beside them are the sensor's own | ||
| # choice or a set point. | ||
| CAMERA_STATE_SETTINGS = { | ||
| 'exposure': 'EXPOSURE', | ||
| 'gain': 'GAIN', | ||
| 'white_balance_temperature': 'WHITEBALANCE_TEMPERATURE', | ||
| 'auto_exposure': 'AEC_AGC', | ||
| 'auto_white_balance': 'WHITEBALANCE_AUTO', | ||
|
Comment on lines
+20
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rule primitive-type violated: AGENTS.md reference: AGENTS.md:L7-L8 Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
|
|
||
| def read_camera_state(zed) -> dict[str, int]: | ||
| """What the camera reports for each of ``CAMERA_STATE_SETTINGS`` now. A setting the SDK refuses is left out.""" | ||
| state = {} | ||
| for name, sdk_name in CAMERA_STATE_SETTINGS.items(): | ||
| error_code, value = zed.get_camera_settings(getattr(sl.VIDEO_SETTINGS, sdk_name)) | ||
| if error_code == sl.ERROR_CODE.SUCCESS: | ||
| state[name] = int(value) | ||
| return state | ||
|
|
||
|
|
||
| class SLCamera(pimm.ControlSystem): | ||
| def __init__( | ||
|
|
@@ -30,6 +51,7 @@ def __init__( | |
| max_recovery_time_sec: float = 10, | ||
| image_enhancement: bool = False, | ||
| mono: bool = False, | ||
| state_period_sec: float = 1.0, | ||
| ): | ||
| """ | ||
| StereoLabs camera driver. | ||
|
|
@@ -46,6 +68,10 @@ def __init__( | |
| max_recovery_time_sec: (float) Maximum time to wait for camera recovery. If exceeded, will stop the camera. | ||
| mono: (bool) Open a single-sensor camera (e.g. ZED X One) via ``sl.CameraOne``. Mono cameras | ||
| support only ``view='left'``, ``depth_mode='none'`` and no image enhancement. | ||
| state_period_sec: (float) How often ``state`` reports the exposure, gain and white balance the | ||
| camera runs at. Automatic control moves them as the scene changes, so one reading per | ||
| episode is not enough; each reading is a control request to the camera, so once a frame | ||
| is too many. | ||
| """ | ||
| super().__init__() | ||
| # IMPORTANT: This control system may be spawned under multiprocessing "spawn". | ||
|
|
@@ -59,6 +85,7 @@ def __init__( | |
| self._image_enhancement = image_enhancement | ||
| self._depth_mask_requested = depth_mask | ||
| self._mono = mono | ||
| self._state_period_sec = state_period_sec | ||
|
|
||
| self.max_depth = max_depth | ||
| self.max_recovery_time_sec = max_recovery_time_sec | ||
|
|
@@ -74,6 +101,9 @@ def __init__( | |
| self.depth_mask: pimm.SignalEmitter = pimm.ControlSystemEmitter(self) | ||
| self._depth_mask_adapter = None # Lazy init | ||
|
|
||
| # The exposure, gain and white balance the camera runs at, read back from it every ``state_period_sec``. | ||
| self.state: pimm.SignalEmitter[dict[str, int]] = pimm.ControlSystemEmitter(self) | ||
|
|
||
| @staticmethod | ||
| def _open_under_device_lock(zed, init_params) -> Iterator[pimm.Sleep]: | ||
| """Open the camera, retrying: the lock binds only openers that take it, so an open can still lose the bus.""" | ||
|
|
@@ -132,6 +162,7 @@ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[p | |
| yield from self._open_under_device_lock(zed, init_params) | ||
|
|
||
| self.recovery_start_time = None | ||
| next_state_read = clock.now() | ||
|
|
||
| while not should_stop.value: | ||
| result = zed.grab() | ||
|
|
@@ -149,6 +180,10 @@ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[p | |
| logger.info(f'Camera recovered after {clock.now() - self.recovery_start_time:.2f} seconds') | ||
| self.recovery_start_time = None | ||
|
|
||
| if clock.now() >= next_state_read: | ||
| self.state.emit(read_camera_state(zed)) | ||
| next_state_read = clock.now() + self._state_period_sec | ||
|
Comment on lines
+183
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rule grandfathered-violation violated: AGENTS.md reference: AGENTS.md:L7-L8 Useful? React with 👍 / 👎. |
||
|
|
||
| image = sl.Mat() | ||
| ts_s = zed.get_timestamp(TIME_REF_IMAGE).get_nanoseconds() / 1e9 | ||
| if zed.retrieve_image(image, view) == SUCCESS: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rule misleading-name violated:
auto_exposurerecords the SDK's combinedAEC_AGCsetting, so datasets hide that this flag also determines whether the recorded gain is automatic; name the signalauto_exposure_gain(or otherwise include both concepts) and update the expected dataset key.AGENTS.md reference: AGENTS.md:L7-L8
Useful? React with 👍 / 👎.