Better fix for exposure drift with more complete testing. Also fix return value for capture_to_memory

This commit is contained in:
Julian Stirling 2025-07-29 12:11:45 +01:00
parent 376780ea28
commit b90b02cf7d
3 changed files with 58 additions and 8 deletions

View file

@ -16,11 +16,19 @@ from openflexure_microscope_server.things.camera.picamera import StreamingPiCame
logging.basicConfig(level=logging.DEBUG) 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): 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() cam = StreamingPiCamera2()
server = ThingServer() server = ThingServer()
@ -28,21 +36,20 @@ def _test_exposure_time_drift(desired_time):
with TestClient(server.app) as test_client: with TestClient(server.app) as test_client:
client = ThingClient.from_url("/camera/", client=test_client) client = ThingClient.from_url("/camera/", client=test_client)
exposure_tol = cam.persistent_control_tolerances["ExposureTime"]
client.exposure_time = desired_time client.exposure_time = desired_time
print(f"Setting desired time of {desired_time}") print(f"Setting desired time of {desired_time}")
time.sleep(0.5) time.sleep(0.5)
pre_capture_et = client.exposure_time pre_capture_et = client.exposure_time
print(f"Pre-capture the time is set to {pre_capture_et}") print(f"Pre-capture the time is set to {pre_capture_et}")
# Check exp is set correctly within known tolerance # 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): for i in range(10):
client.capture_jpeg(resolution="full") client.capture_jpeg(resolution="full")
if i == 0: if i == 0:
# Exposure can update on first capture, due to frame rate restrictions # Exposure can update on first capture, due to frame rate restrictions
first_et = client.exposure_time first_et = client.exposure_time
assert abs(first_et - pre_capture_et) < exposure_tol assert abs(first_et - desired_time) < EXPOSURE_TOL
else: else:
frame_et = client.exposure_time frame_et = client.exposure_time
print(f"Frame {i} captured with exposure time {frame_et}") 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.""" """Performs the exposure time test for a range of exposure time values."""
for desired_time in [100, 1000, 10000]: for desired_time in [100, 1000, 10000]:
_test_exposure_time_drift(desired_time) _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

View file

@ -342,7 +342,7 @@ class BaseCamera(lt.Thing):
logger: lt.deps.InvocationLogger, logger: lt.deps.InvocationLogger,
metadata_getter: lt.deps.GetThingStates, metadata_getter: lt.deps.GetThingStates,
buffer_max: int = 1, buffer_max: int = 1,
) -> None: ) -> int:
"""Capture an image to memory. This can be saved later with ``save_from_memory``. """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 Note that only one image is held in memory so this will overwrite any image

View file

@ -226,7 +226,7 @@ class StreamingPiCamera2(BaseCamera):
if not self._setting_save_in_progress and self.streaming: if not self._setting_save_in_progress and self.streaming:
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
cam_value = cam.capture_metadata()["ExposureTime"] 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._exposure_time = cam_value
self.save_settings() self.save_settings()
return self._exposure_time return self._exposure_time
@ -250,7 +250,8 @@ class StreamingPiCamera2(BaseCamera):
"Brightness": 0, "Brightness": 0,
"ColourGains": self.colour_gains, "ColourGains": self.colour_gains,
"Contrast": 1, "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, "Saturation": 1,
"Sharpness": 1, "Sharpness": 1,
} }