Depend only on generic Camera and Stage
I've now defined parent classes for camera and stage, and depend only on those. This allows proper typing while enabling the actual hardware classes to be swapped. The interfaces definitely aren't final - this is a first draft, largely to enable me to test out the concept of a configuration server.
This commit is contained in:
parent
aa49cfcba3
commit
2268425bf3
8 changed files with 204 additions and 100 deletions
|
|
@ -1,17 +1,16 @@
|
|||
import numpy as np
|
||||
import logging
|
||||
import time
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_sangaboard import SangaboardThing
|
||||
from labthings_picamera2.thing import StreamingPiCamera2
|
||||
from .stage import Stage
|
||||
from .camera import Camera
|
||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||
|
||||
StageDep = direct_thing_client_dependency(SangaboardThing, "/stage/")
|
||||
CamDep = direct_thing_client_dependency(StreamingPiCamera2, "/camera/")
|
||||
StageDep = direct_thing_client_dependency(Stage, "/stage/")
|
||||
CamDep = direct_thing_client_dependency(Camera, "/camera/")
|
||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
|||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
from labthings_picamera2.thing import StreamingPiCamera2
|
||||
from labthings_sangaboard import SangaboardThing
|
||||
from .camera import Camera as CameraThing
|
||||
from .stage import Stage as StageThing
|
||||
import numpy as np
|
||||
from pydantic import BaseModel
|
||||
|
||||
Stage = direct_thing_client_dependency(SangaboardThing, "/stage/")
|
||||
Camera = raw_thing_dependency(StreamingPiCamera2)
|
||||
WrappedCamera = direct_thing_client_dependency(StreamingPiCamera2, "/camera/")
|
||||
Stage = direct_thing_client_dependency(StageThing, "/stage/")
|
||||
Camera = raw_thing_dependency(CameraThing)
|
||||
WrappedCamera = direct_thing_client_dependency(CameraThing, "/camera/")
|
||||
|
||||
### Autofocus utilities
|
||||
|
||||
|
|
|
|||
106
src/openflexure_microscope_server/things/camera/__init__.py
Normal file
106
src/openflexure_microscope_server/things/camera/__init__.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""OpenFlexure Microscope Camera
|
||||
|
||||
This module defines the interface for cameras. Any compatible Thing
|
||||
should enabe the server to work.
|
||||
|
||||
See repository root for licensing information.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||
from labthings_fastapi.outputs.blob import BlobOutput
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
|
||||
class JPEGBlob(BlobOutput):
|
||||
media_type = "image/jpeg"
|
||||
|
||||
|
||||
class Camera(Thing):
|
||||
"""A Thing representing a camera"""
|
||||
|
||||
def __enter__(self):
|
||||
raise NotImplementedError("Subclasses must implement __enter__")
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
raise NotImplementedError("Subclasses must implement __exit__")
|
||||
|
||||
@thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
raise NotImplementedError("Subclasses must implement stream_active")
|
||||
mjpeg_stream = MJPEGStreamDescriptor()
|
||||
lores_mjpeg_stream = MJPEGStreamDescriptor()
|
||||
|
||||
@thing_action
|
||||
def snap_image(self) -> NDArray:
|
||||
"""Acquire one image from the camera.
|
||||
|
||||
This action cannot run if the camera is in use by a background thread, for
|
||||
example if a preview stream is running.
|
||||
"""
|
||||
return self.capture_array()
|
||||
|
||||
@thing_action
|
||||
def capture_array(
|
||||
self,
|
||||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
) -> NDArray:
|
||||
"""Acquire one image from the camera and return as an array
|
||||
|
||||
This function will produce a nested list containing an uncompressed RGB image.
|
||||
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
||||
binary image formats will be added in due course.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement capture_array")
|
||||
|
||||
@thing_action
|
||||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
|
||||
This function will produce a JPEG image.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement capture_jpeg")
|
||||
|
||||
@thing_action
|
||||
def grab_jpeg(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the preview stream and return as an array
|
||||
|
||||
This differs from `capture_jpeg` in that it does not pause the MJPEG
|
||||
preview stream. Instead, we simply return the next frame from that
|
||||
stream (either "main" for the preview stream, or "lores" for the low
|
||||
resolution preview). No metadata is returned.
|
||||
"""
|
||||
logging.info(f"StreamingPiCamera2.grab_jpeg(stream_name={stream_name}) starting")
|
||||
stream = (
|
||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||
)
|
||||
frame = portal.call(stream.grab_frame)
|
||||
logging.info(f"StreamingPiCamera2.grab_jpeg(stream_name={stream_name}) got frame")
|
||||
return JPEGBlob.from_bytes(frame)
|
||||
|
||||
@thing_action
|
||||
def grab_jpeg_size(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> int:
|
||||
"""Acquire one image from the preview stream and return its size"""
|
||||
stream = (
|
||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||
)
|
||||
return portal.call(stream.next_frame_size)
|
||||
|
|
@ -15,21 +15,21 @@ from threading import Thread
|
|||
import cv2
|
||||
import piexif
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.utilities import get_blocking_portal
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||
from labthings_fastapi.outputs.blob import BlobOutput
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from . import Camera
|
||||
|
||||
|
||||
class JPEGBlob(BlobOutput):
|
||||
media_type = "image/jpeg"
|
||||
|
||||
|
||||
class OpenCVCamera(Thing):
|
||||
class OpenCVCamera(Camera):
|
||||
"""A Thing representing an OpenCV camera"""
|
||||
def __init__(self, camera_index: int=0):
|
||||
self.camera_index = camera_index
|
||||
|
|
@ -68,16 +68,10 @@ class OpenCVCamera(Thing):
|
|||
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
|
||||
|
||||
@thing_action
|
||||
def snap_image(self) -> NDArray:
|
||||
"""Acquire one image from the camera.
|
||||
|
||||
This action cannot run if the camera is in use by a background thread, for
|
||||
example if a preview stream is running.
|
||||
"""
|
||||
return self.capture_array()
|
||||
|
||||
@thing_action
|
||||
def capture_array(self) -> NDArray:
|
||||
def capture_array(
|
||||
self,
|
||||
resolution: Literal["main", "full"] = "full",
|
||||
) -> NDArray:
|
||||
"""Acquire one image from the camera and return as an array
|
||||
|
||||
This function will produce a nested list containing an uncompressed RGB image.
|
||||
|
|
@ -93,6 +87,7 @@ class OpenCVCamera(Thing):
|
|||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
resolution: Literal["main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
|
||||
|
|
@ -112,36 +107,3 @@ class OpenCVCamera(Thing):
|
|||
output = io.BytesIO()
|
||||
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
||||
return JPEGBlob.from_bytes(output.getvalue())
|
||||
|
||||
@thing_action
|
||||
def grab_jpeg(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the preview stream and return as an array
|
||||
|
||||
This differs from `capture_jpeg` in that it does not pause the MJPEG
|
||||
preview stream. Instead, we simply return the next frame from that
|
||||
stream (either "main" for the preview stream, or "lores" for the low
|
||||
resolution preview). No metadata is returned.
|
||||
"""
|
||||
logging.info(f"StreamingPiCamera2.grab_jpeg(stream_name={stream_name}) starting")
|
||||
stream = (
|
||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||
)
|
||||
frame = portal.call(stream.grab_frame)
|
||||
logging.info(f"StreamingPiCamera2.grab_jpeg(stream_name={stream_name}) got frame")
|
||||
return JPEGBlob.from_bytes(frame)
|
||||
|
||||
@thing_action
|
||||
def grab_jpeg_size(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> int:
|
||||
"""Acquire one image from the preview stream and return its size"""
|
||||
stream = (
|
||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||
)
|
||||
return portal.call(stream.next_frame_size)
|
||||
|
|
@ -22,8 +22,8 @@ from camera_stage_mapping.camera_stage_calibration_1d import (
|
|||
image_to_stage_displacement_from_1d,
|
||||
)
|
||||
from camera_stage_mapping.camera_stage_tracker import Tracker
|
||||
from labthings_picamera2.thing import StreamingPiCamera2
|
||||
from labthings_sangaboard import SangaboardThing
|
||||
from .camera import Camera as CameraThing
|
||||
from .stage import Stage as StageThing
|
||||
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
from labthings_fastapi.dependencies.invocation import InvocationCancelledError, InvocationLogger
|
||||
|
|
@ -31,8 +31,8 @@ from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
|||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.thing import Thing
|
||||
|
||||
Camera = direct_thing_client_dependency(StreamingPiCamera2, "/camera/")
|
||||
Stage = direct_thing_client_dependency(SangaboardThing, "/stage/")
|
||||
Camera = direct_thing_client_dependency(CameraThing, "/camera/")
|
||||
Stage = direct_thing_client_dependency(StageThing, "/stage/")
|
||||
|
||||
CoordinateType = Tuple[float, float, float]
|
||||
XYCoordinateType = Tuple[float, float]
|
||||
|
|
@ -87,7 +87,10 @@ def make_hardware_interface(
|
|||
return downsample(downsample_factor, img)
|
||||
def settle() -> None:
|
||||
time.sleep(0.2)
|
||||
camera.capture_metadata
|
||||
try:
|
||||
camera.capture_metadata # This discards frames on a picamera
|
||||
except AttributeError:
|
||||
pass # Don't raise an error for other cameras (may consider grabbing a frame)
|
||||
return HardwareInterfaceModel(
|
||||
move=move, get_position=get_position, grab_image=grab_image, settle=settle, grab_image_downsampling=downsample_factor
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,14 +28,15 @@ from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
|||
from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogger, InvocationCancelledError
|
||||
from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint
|
||||
from labthings_fastapi.outputs.blob import BlobOutput
|
||||
from labthings_sangaboard import SangaboardThing
|
||||
from labthings_picamera2.thing import StreamingPiCamera2
|
||||
from .camera import Camera
|
||||
from .stage import Stage
|
||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||
from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing
|
||||
|
||||
StageDep = direct_thing_client_dependency(SangaboardThing, "/stage/")
|
||||
CamDep = direct_thing_client_dependency(StreamingPiCamera2, "/camera/")
|
||||
|
||||
CamDep = direct_thing_client_dependency(Camera, "/camera/")
|
||||
StageDep = direct_thing_client_dependency(Stage, "/stage/")
|
||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/")
|
||||
|
|
|
|||
63
src/openflexure_microscope_server/things/stage/__init__.py
Normal file
63
src/openflexure_microscope_server/things/stage/__init__.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from __future__ import annotations
|
||||
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.invocation import CancelHook
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
|
||||
class Stage(Thing):
|
||||
"""A dummy stage for testing purposes
|
||||
|
||||
This stage should work similarly to a Sangaboard stage, but without any
|
||||
hardware attached.
|
||||
"""
|
||||
_axis_names = ("x", "y", "z")
|
||||
|
||||
@thing_property
|
||||
def axis_names(self) -> Sequence[str]:
|
||||
"""The names of the stage's axes, in order."""
|
||||
return self._axis_names
|
||||
|
||||
position = PropertyDescriptor(
|
||||
Mapping[str, int],
|
||||
{k: 0 for k in _axis_names},
|
||||
description="Current position of the stage",
|
||||
readonly=True,
|
||||
observable=True,
|
||||
)
|
||||
|
||||
moving = PropertyDescriptor(
|
||||
bool,
|
||||
False,
|
||||
description="Whether the stage is in motion",
|
||||
readonly=True,
|
||||
observable=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def thing_state(self):
|
||||
"""Summary metadata describing the current state of the stage"""
|
||||
return {
|
||||
"position": self.position
|
||||
}
|
||||
|
||||
@thing_action
|
||||
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||
"""Make a relative move. Keyword arguments should be axis names."""
|
||||
raise NotImplementedError("Subclasses should implement this method")
|
||||
|
||||
@thing_action
|
||||
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||
"""Make an absolute move. Keyword arguments should be axis names."""
|
||||
raise NotImplementedError("Subclasses should implement this method")
|
||||
|
||||
@thing_action
|
||||
def set_zero_position(self):
|
||||
"""Make the current position zero in all axes
|
||||
|
||||
This action does not move the stage, but resets the position to zero.
|
||||
It is intended for use after manually or automatically recentring the
|
||||
stage.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses should implement this method")
|
||||
|
|
@ -1,54 +1,24 @@
|
|||
from __future__ import annotations
|
||||
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.dependencies.invocation import CancelHook, InvocationCancelledError
|
||||
from collections.abc import Sequence, Mapping
|
||||
from collections.abc import Mapping
|
||||
import time
|
||||
|
||||
from . import Stage
|
||||
|
||||
class DummyStage(Thing):
|
||||
|
||||
class DummyStage(Stage):
|
||||
"""A dummy stage for testing purposes
|
||||
|
||||
This stage should work similarly to a Sangaboard stage, but without any
|
||||
hardware attached.
|
||||
"""
|
||||
_axis_names = ("x", "y", "z")
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
pass
|
||||
|
||||
@thing_property
|
||||
def axis_names(self) -> Sequence[str]:
|
||||
"""The names of the stage's axes, in order."""
|
||||
return self._axis_names
|
||||
|
||||
position = PropertyDescriptor(
|
||||
Mapping[str, int],
|
||||
{k: 0 for k in _axis_names},
|
||||
description="Current position of the stage",
|
||||
readonly=True,
|
||||
observable=True,
|
||||
)
|
||||
|
||||
moving = PropertyDescriptor(
|
||||
bool,
|
||||
False,
|
||||
description="Whether the stage is in motion",
|
||||
readonly=True,
|
||||
observable=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def thing_state(self):
|
||||
"""Summary metadata describing the current state of the stage"""
|
||||
return {
|
||||
"position": self.position
|
||||
}
|
||||
|
||||
@thing_action
|
||||
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||
"""Make a relative move. Keyword arguments should be axis names."""
|
||||
Loading…
Add table
Add a link
Reference in a new issue