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 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)

View file

@ -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)

View file

@ -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: