diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index f332278f..0d90e70d 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -14,13 +14,11 @@ from typing import Annotated, Mapping, Optional, Sequence import os import shutil import glob -import os -import shutil -import glob from fastapi import Depends import numpy as np from pydantic import BaseModel +from PIL import Image from labthings_fastapi.thing import Thing from labthings_fastapi.dependencies.blocking_portal import BlockingPortal @@ -34,13 +32,12 @@ from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera from .stage import StageDependency as Stage from .capture import CaptureThing -import numpy as np -from pydantic import BaseModel CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/") SETTLING_TIME = 0.3 +STACK_OVERSHOOT = 200 class JPEGSharpnessMonitor: @@ -302,7 +299,8 @@ class AutofocusThing(Thing): images_to_capture = self.stack_images_to_capture stack_z_range = stack_dz * (images_to_capture - 1) - stage.move_relative(z=-stack_z_range / 2) + stage.move_relative(z=-(STACK_OVERSHOOT + stack_z_range / 2)) + stage.move_relative(z=STACK_OVERSHOOT) for capture_count in range(images_to_capture): time.sleep(SETTLING_TIME) @@ -311,17 +309,29 @@ class AutofocusThing(Thing): stack_dir, f"{capture_count}.jpeg", ) - capture._capture_and_save( - jpeg_path=jpeg_path, + start = time.time() + image, metadata = capture._capture_image( cam=cam, - logger=logger, metadata_getter=metadata_getter, - target_resolution=target_resolution, + logger=logger, ) - - # If the stack isn't complete yet, move - if capture_count + 1 < images_to_capture: - stage.move_relative(z=stack_dz) + captured = time.time() + # There's an unnecessary move up at the end of the stack + stage.move_relative(z=stack_dz) + moved = time.time() + image = image.resize(target_resolution, Image.LANCZOS) + downsampled = time.time() + capture._save_capture( + jpeg_path=jpeg_path, + image=image, + metadata=metadata, + logger=logger, + ) + saved = time.time() + logger.info(f"Capturing took {round(captured - start, 2)} s") + logger.info(f"Resizing took {round(downsampled - moved, 2)} s") + logger.info(f"Saving took {round(saved - downsampled, 2)} s") + logger.info(f"Effective settling time was {round(saved - moved, 2)} s") self.copy_central_image_from_stack(images_dir, stack_dir) diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 6c18765a..12424116 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -84,6 +84,11 @@ class CameraProtocol(Protocol): for the main stream""" ... + @thing_action + def capture_image(self, stream_name, wait): + """Capture a PIL image from stream stream_name with timeout wait""" + ... + class BaseCamera(Thing): """A Thing representing a camera @@ -194,6 +199,11 @@ class CameraStub(BaseCamera): for the main stream""" raise NotImplementedError("Cameras must not inherit from CameraStub") + @thing_action + def capture_image(self, stream_name, wait): + """Capture a PIL image from stream stream_name with timeout wait""" + raise Exception + CameraDependency = direct_thing_client_dependency(CameraStub, "/camera/") RawCameraDependency = raw_thing_dependency(CameraProtocol) diff --git a/src/openflexure_microscope_server/things/capture.py b/src/openflexure_microscope_server/things/capture.py index e353ecba..71b7c00c 100644 --- a/src/openflexure_microscope_server/things/capture.py +++ b/src/openflexure_microscope_server/things/capture.py @@ -1,5 +1,3 @@ -import numpy as np -from PIL import Image import time import piexif import json @@ -28,7 +26,6 @@ class CaptureThing(Thing): cam: CamDep, logger: InvocationLogger, metadata_getter: GetThingStates, - target_resolution: tuple[int, int], ) -> None: """Capture an image and save it to disk @@ -36,14 +33,18 @@ class CaptureThing(Thing): that the stage may be moved while it's saved. """ capture_start = time.time() - image, metadata = self._capture_array( + image, metadata = self._capture_image( cam, metadata_getter, - target_resolution=target_resolution, logger=logger, ) acquisition_time = time.time() - self._save_capture(jpeg_path, image, metadata, logger) + self._save_capture( + jpeg_path, + image, + metadata, + logger, + ) save_time = time.time() acquisition_duration = round(acquisition_time - capture_start, 1) saving_duration = round(save_time - acquisition_time, 1) @@ -56,7 +57,7 @@ class CaptureThing(Thing): """Capture a JPEG (from a JPEGBlob) to disk""" for capture_attempts in range(5): try: - jpeg = cam.capture_jpeg(resolution="full", wait=5) + jpeg = cam.capture_jpeg(resolution="main", wait=5) jpeg.save(filename) return except TimeoutError: @@ -76,12 +77,11 @@ class CaptureThing(Thing): cam.restart_stream() @thing_action - def _capture_array( + def _capture_image( self, cam: CamDep, metadata_getter: GetThingStates, logger: InvocationLogger, - target_resolution: tuple[int, int] = (1640, 1232), ): """Capture an image in memory and return it with metadata CaptureError raised if the capture fails for any reason @@ -90,13 +90,7 @@ class CaptureThing(Thing): for capture_attempts in range(5): try: metadata = metadata_getter() - image = Image.fromarray( - cam.capture_array(stream_name="main", wait=5)[..., :3].astype( - "uint8" - ), - "RGB", - ) - image = image.resize(target_resolution, Image.LANCZOS) + image = cam.capture_image(stream_name="main", wait=5) return image, metadata except TimeoutError: logger.warning( @@ -104,10 +98,11 @@ class CaptureThing(Thing): ) raise CaptureError("An error occurred while capturing after 5 attempts") + @thing_action def _save_capture( self, jpeg_path: str, - image: np.ndarray, + image, metadata: dict, logger: InvocationLogger, ) -> None: