Get images as a PIL image from camera, use resizing and saving as settling time in stack
This commit is contained in:
parent
dbed1c6971
commit
744ec28f93
3 changed files with 46 additions and 31 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue