From b90b02cf7d20877566a8ed18ebd783eac32e6975 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 29 Jul 2025 12:11:45 +0100 Subject: [PATCH] Better fix for exposure drift with more complete testing. Also fix return value for capture_to_memory --- .../picamera2/test_exposure_time_drift.py | 59 +++++++++++++++++-- .../things/camera/__init__.py | 2 +- .../things/camera/picamera.py | 5 +- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/hardware-specific-tests/picamera2/test_exposure_time_drift.py b/hardware-specific-tests/picamera2/test_exposure_time_drift.py index 8e566612..e0177f4d 100644 --- a/hardware-specific-tests/picamera2/test_exposure_time_drift.py +++ b/hardware-specific-tests/picamera2/test_exposure_time_drift.py @@ -16,11 +16,19 @@ from openflexure_microscope_server.things.camera.picamera import StreamingPiCame logging.basicConfig(level=logging.DEBUG) +# The tolerance used to be a setting of the camera, and any changes within tolerance +# were ignored. Now this isn't needed we always set a value 1 larger than the last +# accepted value as the PiCamera always rounds down. +# The tolerance is needed in this test as when setting an arbitrary value it is rounded +# down to a hardware compatible one. +EXPOSURE_TOL = 30 + def _test_exposure_time_drift(desired_time): - """Capture 10 full resolution images and check that the exposure time remains constant. + """Capture 10 full res images and check that the exposure time remains constant. - This confirms that automatic exposure time adjustment is fully turned off + This confirms that automatic exposure time adjustment is fully turned off during + capture. """ cam = StreamingPiCamera2() server = ThingServer() @@ -28,21 +36,20 @@ def _test_exposure_time_drift(desired_time): with TestClient(server.app) as test_client: client = ThingClient.from_url("/camera/", client=test_client) - exposure_tol = cam.persistent_control_tolerances["ExposureTime"] client.exposure_time = desired_time print(f"Setting desired time of {desired_time}") time.sleep(0.5) pre_capture_et = client.exposure_time print(f"Pre-capture the time is set to {pre_capture_et}") # Check exp is set correctly within known tolerance - assert abs(pre_capture_et - desired_time) < exposure_tol + assert abs(pre_capture_et - desired_time) < EXPOSURE_TOL for i in range(10): client.capture_jpeg(resolution="full") if i == 0: # Exposure can update on first capture, due to frame rate restrictions first_et = client.exposure_time - assert abs(first_et - pre_capture_et) < exposure_tol + assert abs(first_et - desired_time) < EXPOSURE_TOL else: frame_et = client.exposure_time print(f"Frame {i} captured with exposure time {frame_et}") @@ -65,3 +72,45 @@ def test_exposure_time_drift(): """Performs the exposure time test for a range of exposure time values.""" for desired_time in [100, 1000, 10000]: _test_exposure_time_drift(desired_time) + + +def test_exposure_time_on_start_and_stop_stream(): + """Capture 10 full res images and check that the exposure time remains constant. + + This confirms that automatic exposure time adjustment is fully turned off during + capture. + """ + cam = StreamingPiCamera2() + server = ThingServer() + server.add_thing(cam, "/camera/") + desired_time = 1000 + # Create a test client + with TestClient(server.app) as test_client: + client = ThingClient.from_url("/camera/", client=test_client) + # Set a desired exposure time + client.exposure_time = desired_time + print(f"Setting desired time of {desired_time}") + time.sleep(0.5) + # Take a couple of images to make sure that the exposure is adjusted to + # a hardware compatible value. + for i in range(2): + client.capture_jpeg(resolution="full") + # Save this time. + set_time = client.exposure_time + assert abs(set_time - desired_time) < EXPOSURE_TOL + + # Mimick doing 10 smart scans. Change to full res, take some images. Return + # to standard preview resolution. + for i in range(10): + print(f"Starting simulation scan {i}") + # This will need updating if we start supporting other Picamera models + # It is currently used here to mimick the behaviour in in scanning. + client.start_streaming(main_resolution=(3280, 2464)) + time.sleep(0.5) + for _j in range(5): + client.capture_to_memory(buffer_max=1) + # Reset to main resolution + time.sleep(0.5) + client.start_streaming() + # Check after all of this the exposure time is the same. + assert abs(client.exposure_time - set_time) < EXPOSURE_TOL diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 661bdf33..4225b43d 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -342,7 +342,7 @@ class BaseCamera(lt.Thing): logger: lt.deps.InvocationLogger, metadata_getter: lt.deps.GetThingStates, buffer_max: int = 1, - ) -> None: + ) -> int: """Capture an image to memory. This can be saved later with ``save_from_memory``. Note that only one image is held in memory so this will overwrite any image diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index 5993e627..fca1433e 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -226,7 +226,7 @@ class StreamingPiCamera2(BaseCamera): if not self._setting_save_in_progress and self.streaming: with self._streaming_picamera() as cam: cam_value = cam.capture_metadata()["ExposureTime"] - if abs(cam_value - self._exposure_time) > 30: + if cam_value != self._exposure_time: self._exposure_time = cam_value self.save_settings() return self._exposure_time @@ -250,7 +250,8 @@ class StreamingPiCamera2(BaseCamera): "Brightness": 0, "ColourGains": self.colour_gains, "Contrast": 1, - "ExposureTime": self.exposure_time, + # Must also set plus 1 or the exposure drifts with start and stop stream. + "ExposureTime": self.exposure_time + 1, "Saturation": 1, "Sharpness": 1, }