Apply suggestions from code review of branch jpeg-capture-in-stacking

Co-authored-by: Beth Probert <beth_probert@outlook.com>
This commit is contained in:
Julian Stirling 2025-06-25 14:22:01 +00:00
parent 2d49bf2061
commit 0933ece63a
8 changed files with 43 additions and 15 deletions

View file

@ -11,6 +11,11 @@ from openflexure_microscope_server.things.camera.picamera import StreamingPiCame
@fixture(scope="module") @fixture(scope="module")
def client(): def client():
"""
A pytest fixture that initialises a test client for the StreamingPiCamera2 Thing.
This fixture sets up a ThingServer, registers a StreamingPiCamera2 instance at the
"/camera/" endpoint, and provides a ThingClient for interacting with it during tests.
"""
server = ThingServer() server = ThingServer()
server.add_thing(StreamingPiCamera2(), "/camera/") server.add_thing(StreamingPiCamera2(), "/camera/")
with TestClient(server.app) as test_client: with TestClient(server.app) as test_client:
@ -30,13 +35,20 @@ def test_jpeg_and_array(client):
Check that grabbing a jpeg from the stream results in the same size Check that grabbing a jpeg from the stream results in the same size
image as a array capture or a jpeg capture. image as a array capture or a jpeg capture.
""" """
# Grab a jpeg from the stream
blob = client.grab_jpeg() blob = client.grab_jpeg()
mjpeg_frame = Image.open(blob.open()) mjpeg_frame = Image.open(blob.open())
assert mjpeg_frame assert mjpeg_frame
# Capture a jpeg
blob = client.capture_jpeg(resolution="main") blob = client.capture_jpeg(resolution="main")
jpeg_capture = Image.open(blob.open()) jpeg_capture = Image.open(blob.open())
assert jpeg_capture assert jpeg_capture
# Capture an array
arrlist = client.capture_array(stream_name="main") arrlist = client.capture_array(stream_name="main")
array_main = np.array(arrlist) array_main = np.array(arrlist)
# Verify image sizes are the same
assert mjpeg_frame.size == jpeg_capture.size assert mjpeg_frame.size == jpeg_capture.size
assert array_main.shape[1::-1] == jpeg_capture.size assert array_main.shape[1::-1] == jpeg_capture.size

View file

@ -1,7 +1,7 @@
""" """
Check exposure times do not drift. Check exposure times do not drift.
This can get very tedious. Recommende running pytest with -s option This can get very tedious. Recommend running pytest with -s option
to monitor progress. to monitor progress.
""" """
@ -38,16 +38,18 @@ def _test_exposure_time_drift(desired_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 - pre_capture_et) < exposure_tol
frame_et = client.exposure_time else:
print(f"Frame {i} captured with exposure time {frame_et}") frame_et = client.exposure_time
# Check no further drift in value print(f"Frame {i} captured with exposure time {frame_et}")
assert first_et == frame_et # Check no further drift in value
assert first_et == frame_et
# Set the exposure time to the value it already is. To check it doesn't shift # Set the exposure time to the value it already is. To check it doesn't shift
print(f"Setting exposure time to {frame_et} to check it doesn't change") print(f"Setting exposure time to {frame_et} to check it doesn't change")
@ -62,6 +64,9 @@ def _test_exposure_time_drift(desired_time):
def test_exposure_time_drift(): def test_exposure_time_drift():
"""
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)

View file

@ -24,6 +24,9 @@ def test_sensor_mode():
for size in [(3280, 2464), (1640, 1232)]: for size in [(3280, 2464), (1640, 1232)]:
client.sensor_mode = {"output_size": size, "bit_depth": 10} client.sensor_mode = {"output_size": size, "bit_depth": 10}
arr = np.array(client.capture_array(stream_name="raw")) arr = np.array(client.capture_array(stream_name="raw"))
# Check that the array dimensions match the requested image size.
# Note: Numpy array shape is (y,x), but the sensor is set with (x,y)
# hence the need to compare index 0 with index 1.
assert arr.shape[0] == size[1] assert arr.shape[0] == size[1]

View file

@ -33,14 +33,14 @@ def generate_bad_tuning():
return bad_tuning return bad_tuning
def print_tuning(read_file=False): def print_tuning(read_file: bool = False):
""" """
Print the path of the default tuning file from the the environment variable. Print the path of the default tuning file from the the environment variable.
:param read_file: Boolean, set true to also print the file contents. :param read_file: Boolean, set true to also print the file contents.
This is useful for debuging. As PyTest suppresses the printing by default the This is useful for debugging. As pytest suppresses the printing by default the
-s option is needed when running pylint to see this. -s option is needed when running pytest to see this.
""" """
key = "LIBCAMERA_RPI_TUNING_FILE" key = "LIBCAMERA_RPI_TUNING_FILE"
if key in os.environ: if key in os.environ:
@ -52,7 +52,7 @@ def print_tuning(read_file=False):
print("Tuning file environment variable not set") print("Tuning file environment variable not set")
def _test_bad_tuning_after_good_tuning(configure): def _test_bad_tuning_after_good_tuning(configure: bool = False):
""" """
Load the default tuning file into the camera, re-load with a broken tuning file, Load the default tuning file into the camera, re-load with a broken tuning file,
check it errors. Finally check the default tuning file will load again afterwards. check it errors. Finally check the default tuning file will load again afterwards.
@ -66,14 +66,14 @@ def _test_bad_tuning_after_good_tuning(configure):
bad_tuning = generate_bad_tuning() bad_tuning = generate_bad_tuning()
default_tuning = load_default_tuning() default_tuning = load_default_tuning()
print_tuning() print_tuning()
print("opening camera with explicitly specified tuning") print("opening camera with default tuning")
with Picamera2(tuning=default_tuning) as cam: with Picamera2(tuning=default_tuning) as cam:
print_tuning() print_tuning()
if configure: if configure:
cam.configure(cam.create_preview_configuration()) cam.configure(cam.create_preview_configuration())
del cam del cam
recalibrate_utils.recreate_camera_manager() recalibrate_utils.recreate_camera_manager()
print(f"Opening camera with tuning['version'] = {bad_tuning['version']}") print(f"Opening camera with bad tuning - ['version'] = {bad_tuning['version']}")
with pytest.raises(IndexError): with pytest.raises(IndexError):
# The bad version should cause a problem # The bad version should cause a problem
cam = Picamera2(tuning=bad_tuning) cam = Picamera2(tuning=bad_tuning)

View file

@ -40,7 +40,7 @@ dev = [
"matplotlib~=3.10" "matplotlib~=3.10"
] ]
pi = [ pi = [
"picamera2~=0.3.12", "picamera2~=0.3.27",
] ]
[project.scripts] [project.scripts]

View file

@ -294,6 +294,7 @@ class AutofocusThing(Thing):
stack_z_range = stack_dz * (images_to_capture - 1) stack_z_range = stack_dz * (images_to_capture - 1)
if stack_z_range > 0: if stack_z_range > 0:
# Perform backlash corrected move. See issue #420
stage.move_relative(z=-(STACK_OVERSHOOT + stack_z_range / 2)) stage.move_relative(z=-(STACK_OVERSHOOT + stack_z_range / 2))
stage.move_relative(z=STACK_OVERSHOOT) stage.move_relative(z=STACK_OVERSHOOT)
time.sleep(0.3) time.sleep(0.3)
@ -348,7 +349,7 @@ class AutofocusThing(Thing):
images_dir: str, images_dir: str,
stack_dir: str, stack_dir: str,
logger: InvocationLogger, logger: InvocationLogger,
): ) -> None:
"""Gets a list of images in a folder (stack_dir), sorts them by filesize, and copies the sharpest """Gets a list of images in a folder (stack_dir), sorts them by filesize, and copies the sharpest
image to images dir.""" image to images dir."""
image_list = glob.glob(os.path.join(stack_dir, "*")) image_list = glob.glob(os.path.join(stack_dir, "*"))

View file

@ -31,6 +31,8 @@ class JPEGBlob(Blob):
class PNGBlob(Blob): class PNGBlob(Blob):
"""A class representing a PNG image as a LabThings FastAPI Blob"""
media_type: str = "image/png" media_type: str = "image/png"

View file

@ -377,10 +377,13 @@ class SmartScanThing(Thing):
return (next_point[0], next_point[1], z_estimate) return (next_point[0], next_point[1], z_estimate)
@_scan_running @_scan_running
def _calc_displacement_from_test_image(self, overlap): def _calc_displacement_from_test_image(self, overlap: int) -> tuple[int, int]:
""" """
Take a test image and use camera stage mapping to calculate x and y displacement Take a test image and use camera stage mapping to calculate x and y displacement
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
that each image should overlap its nearest neighbour by 50%.
Return (dx, dy) - the x and y displacments in steps Return (dx, dy) - the x and y displacments in steps
""" """
test_jpg = self._cam.grab_jpeg() test_jpg = self._cam.grab_jpeg()
@ -422,7 +425,9 @@ class SmartScanThing(Thing):
dx, dy = self._calc_displacement_from_test_image(overlap) dx, dy = self._calc_displacement_from_test_image(overlap)
stitch_resize = STITCHING_RESOLUTION[0] / self.capture_resolution[0] stitch_resize = STITCHING_RESOLUTION[0] / self.capture_resolution[0]
self._scan_logger.debug(f"Resizing images when stitching by {stitch_resize}") self._scan_logger.debug(
f"Resizing images when stitching by a factor of {stitch_resize}"
)
self._scan_logger.info( self._scan_logger.info(
f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}" f"Based on an overlap of {overlap}, we will make steps of {dx}, {dy}"