Get images as a PIL image from camera, use resizing and saving as settling time in stack

This commit is contained in:
jaknapper 2025-05-07 22:16:38 +01:00 committed by Julian Stirling
parent dbed1c6971
commit 744ec28f93
3 changed files with 46 additions and 31 deletions

View file

@ -14,13 +14,11 @@ from typing import Annotated, Mapping, Optional, Sequence
import os import os
import shutil import shutil
import glob import glob
import os
import shutil
import glob
from fastapi import Depends from fastapi import Depends
import numpy as np import numpy as np
from pydantic import BaseModel from pydantic import BaseModel
from PIL import Image
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal 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 .camera import CameraDependency as WrappedCamera
from .stage import StageDependency as Stage from .stage import StageDependency as Stage
from .capture import CaptureThing from .capture import CaptureThing
import numpy as np
from pydantic import BaseModel
CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/") CaptureDep = direct_thing_client_dependency(CaptureThing, "/capture/")
SETTLING_TIME = 0.3 SETTLING_TIME = 0.3
STACK_OVERSHOOT = 200
class JPEGSharpnessMonitor: class JPEGSharpnessMonitor:
@ -302,7 +299,8 @@ class AutofocusThing(Thing):
images_to_capture = self.stack_images_to_capture images_to_capture = self.stack_images_to_capture
stack_z_range = stack_dz * (images_to_capture - 1) 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): for capture_count in range(images_to_capture):
time.sleep(SETTLING_TIME) time.sleep(SETTLING_TIME)
@ -311,17 +309,29 @@ class AutofocusThing(Thing):
stack_dir, stack_dir,
f"{capture_count}.jpeg", f"{capture_count}.jpeg",
) )
capture._capture_and_save( start = time.time()
jpeg_path=jpeg_path, image, metadata = capture._capture_image(
cam=cam, cam=cam,
logger=logger,
metadata_getter=metadata_getter, metadata_getter=metadata_getter,
target_resolution=target_resolution, logger=logger,
) )
captured = time.time()
# If the stack isn't complete yet, move # There's an unnecessary move up at the end of the stack
if capture_count + 1 < images_to_capture: stage.move_relative(z=stack_dz)
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) self.copy_central_image_from_stack(images_dir, stack_dir)

View file

@ -84,6 +84,11 @@ class CameraProtocol(Protocol):
for the main stream""" 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): class BaseCamera(Thing):
"""A Thing representing a camera """A Thing representing a camera
@ -194,6 +199,11 @@ class CameraStub(BaseCamera):
for the main stream""" for the main stream"""
raise NotImplementedError("Cameras must not inherit from CameraStub") 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/") CameraDependency = direct_thing_client_dependency(CameraStub, "/camera/")
RawCameraDependency = raw_thing_dependency(CameraProtocol) RawCameraDependency = raw_thing_dependency(CameraProtocol)

View file

@ -1,5 +1,3 @@
import numpy as np
from PIL import Image
import time import time
import piexif import piexif
import json import json
@ -28,7 +26,6 @@ class CaptureThing(Thing):
cam: CamDep, cam: CamDep,
logger: InvocationLogger, logger: InvocationLogger,
metadata_getter: GetThingStates, metadata_getter: GetThingStates,
target_resolution: tuple[int, int],
) -> None: ) -> None:
"""Capture an image and save it to disk """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. that the stage may be moved while it's saved.
""" """
capture_start = time.time() capture_start = time.time()
image, metadata = self._capture_array( image, metadata = self._capture_image(
cam, cam,
metadata_getter, metadata_getter,
target_resolution=target_resolution,
logger=logger, logger=logger,
) )
acquisition_time = time.time() acquisition_time = time.time()
self._save_capture(jpeg_path, image, metadata, logger) self._save_capture(
jpeg_path,
image,
metadata,
logger,
)
save_time = time.time() save_time = time.time()
acquisition_duration = round(acquisition_time - capture_start, 1) acquisition_duration = round(acquisition_time - capture_start, 1)
saving_duration = round(save_time - acquisition_time, 1) saving_duration = round(save_time - acquisition_time, 1)
@ -56,7 +57,7 @@ class CaptureThing(Thing):
"""Capture a JPEG (from a JPEGBlob) to disk""" """Capture a JPEG (from a JPEGBlob) to disk"""
for capture_attempts in range(5): for capture_attempts in range(5):
try: try:
jpeg = cam.capture_jpeg(resolution="full", wait=5) jpeg = cam.capture_jpeg(resolution="main", wait=5)
jpeg.save(filename) jpeg.save(filename)
return return
except TimeoutError: except TimeoutError:
@ -76,12 +77,11 @@ class CaptureThing(Thing):
cam.restart_stream() cam.restart_stream()
@thing_action @thing_action
def _capture_array( def _capture_image(
self, self,
cam: CamDep, cam: CamDep,
metadata_getter: GetThingStates, metadata_getter: GetThingStates,
logger: InvocationLogger, logger: InvocationLogger,
target_resolution: tuple[int, int] = (1640, 1232),
): ):
"""Capture an image in memory and return it with metadata """Capture an image in memory and return it with metadata
CaptureError raised if the capture fails for any reason CaptureError raised if the capture fails for any reason
@ -90,13 +90,7 @@ class CaptureThing(Thing):
for capture_attempts in range(5): for capture_attempts in range(5):
try: try:
metadata = metadata_getter() metadata = metadata_getter()
image = Image.fromarray( image = cam.capture_image(stream_name="main", wait=5)
cam.capture_array(stream_name="main", wait=5)[..., :3].astype(
"uint8"
),
"RGB",
)
image = image.resize(target_resolution, Image.LANCZOS)
return image, metadata return image, metadata
except TimeoutError: except TimeoutError:
logger.warning( logger.warning(
@ -104,10 +98,11 @@ class CaptureThing(Thing):
) )
raise CaptureError("An error occurred while capturing after 5 attempts") raise CaptureError("An error occurred while capturing after 5 attempts")
@thing_action
def _save_capture( def _save_capture(
self, self,
jpeg_path: str, jpeg_path: str,
image: np.ndarray, image,
metadata: dict, metadata: dict,
logger: InvocationLogger, logger: InvocationLogger,
) -> None: ) -> None: