Merge branch 'v3-camera-protocol' into 'v3'
Formal Protocol for camera and stage interfaces See merge request openflexure/openflexure-microscope-server!192
This commit is contained in:
commit
4d4ca831b3
20 changed files with 215 additions and 1794 deletions
|
|
@ -4,13 +4,10 @@ import logging
|
||||||
from labthings_fastapi.thing import Thing
|
from labthings_fastapi.thing import Thing
|
||||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
from labthings_fastapi.decorators import thing_action
|
from labthings_fastapi.decorators import thing_action
|
||||||
from .stage import Stage
|
from .stage import StageDependency as StageDep
|
||||||
from .camera import Camera
|
|
||||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||||
|
|
||||||
StageDep = direct_thing_client_dependency(Stage, "/stage/")
|
|
||||||
CamDep = direct_thing_client_dependency(Camera, "/camera/")
|
|
||||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@ from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||||
from labthings_fastapi.decorators import thing_action
|
from labthings_fastapi.decorators import thing_action
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
from .camera import Camera as CameraThing
|
from .camera import RawCameraDependency as Camera
|
||||||
from .stage import Stage as StageThing
|
from .camera import CameraDependency as WrappedCamera
|
||||||
|
from .stage import StageDependency as Stage
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
Stage = direct_thing_client_dependency(StageThing, "/stage/")
|
|
||||||
Camera = raw_thing_dependency(CameraThing)
|
|
||||||
WrappedCamera = direct_thing_client_dependency(CameraThing, "/camera/")
|
|
||||||
|
|
||||||
### Autofocus utilities
|
### Autofocus utilities
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,54 +7,84 @@ See repository root for licensing information.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
from typing import Literal
|
from typing import Literal, Protocol, runtime_checkable
|
||||||
|
|
||||||
from labthings_fastapi.thing import Thing
|
from labthings_fastapi.thing import Thing
|
||||||
from labthings_fastapi.decorators import thing_action, thing_property
|
from labthings_fastapi.decorators import thing_action, thing_property
|
||||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||||
|
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
|
from labthings_fastapi.dependencies.raw_thing import raw_thing_dependency
|
||||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||||
from labthings_fastapi.outputs.blob import blob_type
|
from labthings_fastapi.outputs.blob import Blob
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
|
|
||||||
|
|
||||||
JPEGBlob = blob_type("image/jpeg")
|
class JPEGBlob(Blob):
|
||||||
|
media_type: str = "image/jpeg"
|
||||||
|
|
||||||
class CameraMeta:
|
@runtime_checkable
|
||||||
def __subclasscheck__(cls, C):
|
class CameraProtocol(Protocol):
|
||||||
"""Override Python's default behaviour and use duck typing"""
|
|
||||||
print(f"Checking if {C} is a camera...")
|
|
||||||
for action in ["snap_image", "capture_array", "capture_jpeg", "grab_jpeg", "grab_jpeg_size"]:
|
|
||||||
a = getattr(C, action, None)
|
|
||||||
if not callable(a):
|
|
||||||
return False
|
|
||||||
for prop in ["stream_active"]:
|
|
||||||
if not hasattr(C, prop):
|
|
||||||
return False
|
|
||||||
for k in ["mjpeg_stream", "lores_mjpeg_stream"]:
|
|
||||||
stream = getattr(C, k)
|
|
||||||
if not isinstance(stream, MJPEGStreamDescriptor):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __instancecheck__(cls, I):
|
|
||||||
return cls.__subclasscheck__(I)
|
|
||||||
|
|
||||||
|
|
||||||
class Camera(Thing):
|
|
||||||
__metaclass__ = CameraMeta
|
|
||||||
"""A Thing representing a camera"""
|
"""A Thing representing a camera"""
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> None:
|
||||||
raise NotImplementedError("Subclasses must implement __enter__")
|
...
|
||||||
|
|
||||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
def __exit__(self, _exc_type, _exc_value, _traceback) -> None:
|
||||||
raise NotImplementedError("Subclasses must implement __exit__")
|
...
|
||||||
|
|
||||||
@thing_property
|
@property
|
||||||
def stream_active(self) -> bool:
|
def stream_active(self) -> bool:
|
||||||
"Whether the MJPEG stream is active"
|
"Whether the MJPEG stream is active"
|
||||||
raise NotImplementedError("Subclasses must implement stream_active")
|
...
|
||||||
|
|
||||||
|
def snap_image(self) -> NDArray:
|
||||||
|
"""Acquire one image from the camera."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def capture_array(
|
||||||
|
self,
|
||||||
|
resolution: Literal["lores", "main", "full"] = "main",
|
||||||
|
) -> NDArray:
|
||||||
|
...
|
||||||
|
|
||||||
|
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"""
|
||||||
|
...
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
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"""
|
||||||
|
...
|
||||||
|
|
||||||
|
class BaseCamera(Thing):
|
||||||
|
"""A Thing representing a camera
|
||||||
|
|
||||||
|
This is a concrete base class for `Thing`s implementing the `CameraProtocol`.
|
||||||
|
It provides the stream descriptors and actions to grab from the stream.
|
||||||
|
"""
|
||||||
|
|
||||||
mjpeg_stream = MJPEGStreamDescriptor()
|
mjpeg_stream = MJPEGStreamDescriptor()
|
||||||
lores_mjpeg_stream = MJPEGStreamDescriptor()
|
lores_mjpeg_stream = MJPEGStreamDescriptor()
|
||||||
|
|
||||||
|
|
@ -67,31 +97,6 @@ class Camera(Thing):
|
||||||
"""
|
"""
|
||||||
return self.capture_array()
|
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
|
@thing_action
|
||||||
def grab_jpeg(
|
def grab_jpeg(
|
||||||
self,
|
self,
|
||||||
|
|
@ -124,4 +129,51 @@ class Camera(Thing):
|
||||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||||
)
|
)
|
||||||
return portal.call(stream.next_frame_size)
|
return portal.call(stream.next_frame_size)
|
||||||
|
|
||||||
|
|
||||||
|
class CameraStub(BaseCamera):
|
||||||
|
"""A stub for a camera, to allow dependencies
|
||||||
|
|
||||||
|
|
||||||
|
As of LabThings-FastAPI 0.0.7, we can't make a thing client dependency
|
||||||
|
based on a protocol, because the protocol is not a Thing, and its
|
||||||
|
methods/properties are not decorated as Affordances. This stub class
|
||||||
|
is a workaround for that limitation, and should not be used directly.
|
||||||
|
|
||||||
|
This stub class should be used for dependencies on the CameraProtocol.
|
||||||
|
"""
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
def __exit__(self, _exc_type, _exc_value, _traceback) -> None:
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
@thing_property
|
||||||
|
def stream_active(self) -> bool:
|
||||||
|
"Whether the MJPEG stream is active"
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
@thing_action
|
||||||
|
def snap_image(self) -> NDArray:
|
||||||
|
"""Acquire one image from the camera."""
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
@thing_action
|
||||||
|
def capture_array(
|
||||||
|
self,
|
||||||
|
resolution: Literal["lores", "main", "full"] = "main",
|
||||||
|
) -> NDArray:
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
@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"""
|
||||||
|
raise NotImplementedError("Cameras must not inherit from CameraStub")
|
||||||
|
|
||||||
|
|
||||||
|
CameraDependency = direct_thing_client_dependency(CameraStub, "/camera/")
|
||||||
|
RawCameraDependency = raw_thing_dependency(CameraProtocol)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
|
|
||||||
from . import Camera, JPEGBlob
|
from . import BaseCamera, JPEGBlob
|
||||||
|
|
||||||
|
|
||||||
class OpenCVCamera(Camera):
|
class OpenCVCamera(BaseCamera):
|
||||||
"""A Thing representing an OpenCV camera"""
|
"""A Thing representing an OpenCV camera"""
|
||||||
def __init__(self, camera_index: int=0):
|
def __init__(self, camera_index: int=0):
|
||||||
self.camera_index = camera_index
|
self.camera_index = camera_index
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
from labthings_fastapi.server import ThingServer
|
from labthings_fastapi.server import ThingServer
|
||||||
|
|
||||||
from . import Camera, JPEGBlob
|
from . import BaseCamera, JPEGBlob
|
||||||
from ..stage import Stage
|
from ..stage import StageProtocol as Stage
|
||||||
|
|
||||||
|
|
||||||
class SimulatedCamera(Camera):
|
class SimulatedCamera(BaseCamera):
|
||||||
"""A Thing representing an OpenCV camera"""
|
"""A Thing representing an OpenCV camera"""
|
||||||
shape = (600, 800, 3)
|
shape = (600, 800, 3)
|
||||||
glyph_shape = (51, 51, 3)
|
glyph_shape = (51, 51, 3)
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,13 @@ from camera_stage_mapping.camera_stage_calibration_1d import (
|
||||||
calibrate_backlash_1d,
|
calibrate_backlash_1d,
|
||||||
image_to_stage_displacement_from_1d,
|
image_to_stage_displacement_from_1d,
|
||||||
)
|
)
|
||||||
from camera_stage_mapping.camera_stage_tracker import Tracker
|
|
||||||
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
|
from labthings_fastapi.dependencies.invocation import InvocationCancelledError, InvocationLogger
|
||||||
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
||||||
from labthings_fastapi.decorators import thing_action, thing_property
|
from labthings_fastapi.decorators import thing_action, thing_property
|
||||||
from labthings_fastapi.thing import Thing
|
from labthings_fastapi.thing import Thing
|
||||||
|
from camera_stage_mapping.camera_stage_tracker import Tracker
|
||||||
Camera = direct_thing_client_dependency(CameraThing, "/camera/")
|
from .camera import CameraDependency as Camera
|
||||||
Stage = direct_thing_client_dependency(StageThing, "/stage/")
|
from .stage import StageDependency as Stage
|
||||||
|
|
||||||
CoordinateType = Tuple[float, float, float]
|
CoordinateType = Tuple[float, float, float]
|
||||||
XYCoordinateType = Tuple[float, float]
|
XYCoordinateType = Tuple[float, float]
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,13 @@ from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogger, InvocationCancelledError
|
from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogger, InvocationCancelledError
|
||||||
from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint
|
from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint
|
||||||
from labthings_fastapi.outputs.blob import blob_type
|
from labthings_fastapi.outputs.blob import blob_type
|
||||||
from .camera import Camera
|
from .camera import CameraDependency as CamDep
|
||||||
from .stage import Stage
|
from .stage import StageDependency as StageDep
|
||||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||||
from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing
|
from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing
|
||||||
|
|
||||||
|
|
||||||
CamDep = direct_thing_client_dependency(Camera, "/camera/")
|
|
||||||
StageDep = direct_thing_client_dependency(Stage, "/stage/")
|
|
||||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||||
RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/")
|
RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,63 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from typing import Any, Protocol, TypeAlias, runtime_checkable
|
||||||
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
||||||
from labthings_fastapi.thing import Thing
|
from labthings_fastapi.thing import Thing
|
||||||
from labthings_fastapi.decorators import thing_action, thing_property
|
from labthings_fastapi.decorators import thing_action, thing_property
|
||||||
from labthings_fastapi.dependencies.invocation import CancelHook
|
from labthings_fastapi.dependencies.invocation import CancelHook
|
||||||
|
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
from collections.abc import Sequence, Mapping
|
from collections.abc import Sequence, Mapping
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class StageProtocol(Protocol):
|
||||||
|
"""A protocol for the OpenFlexure translation stage
|
||||||
|
"""
|
||||||
|
_axis_names: Sequence[str]
|
||||||
|
|
||||||
class Stage(Thing):
|
@property
|
||||||
"""A dummy stage for testing purposes
|
def axis_names(self) -> Sequence[str]:
|
||||||
|
"""The names of the stage's axes, in order."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self) -> Mapping[str, int]:
|
||||||
|
"""Current position of the stage"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def moving() -> bool:
|
||||||
|
"Whether the stage is in motion"
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thing_state(self) -> Mapping[str, Any]:
|
||||||
|
"""Summary metadata describing the current state of the stage"""
|
||||||
|
...
|
||||||
|
|
||||||
This stage should work similarly to a Sangaboard stage, but without any
|
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||||
hardware attached.
|
"""Make a relative move. Keyword arguments should be axis names."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||||
|
"""Make an absolute move. Keyword arguments should be axis names."""
|
||||||
|
...
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class BaseStage(Thing):
|
||||||
|
"""A base stage class for OpenFlexure translation stages
|
||||||
|
|
||||||
|
This can't be used directly but should reduce boilerplate code when
|
||||||
|
implementing new stages. A minimal working stage must implement
|
||||||
|
`move_relative` and `move_absolute` actions, which update the
|
||||||
|
`position` property on completion, and provide `set_zero_position`.
|
||||||
"""
|
"""
|
||||||
_axis_names = ("x", "y", "z")
|
_axis_names = ("x", "y", "z")
|
||||||
|
|
||||||
|
|
@ -41,17 +88,26 @@ class Stage(Thing):
|
||||||
return {
|
return {
|
||||||
"position": self.position
|
"position": self.position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StageStub(BaseStage):
|
||||||
|
"""A Stub Thing for the OpenFlexure translation stage
|
||||||
|
|
||||||
|
As of LabThings-FastAPI 0.0.7, we can't make a thing client dependency
|
||||||
|
based on a protocol, because the protocol is not a Thing, and its
|
||||||
|
methods/properties are not decorated as Affordances. This stub class
|
||||||
|
is a workaround for that limitation, and should not be used directly.
|
||||||
|
"""
|
||||||
@thing_action
|
@thing_action
|
||||||
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||||
"""Make a relative move. Keyword arguments should be axis names."""
|
"""Make a relative move. Keyword arguments should be axis names."""
|
||||||
raise NotImplementedError("Subclasses should implement this method")
|
raise NotImplementedError("StageStub should not be used directly")
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
|
||||||
"""Make an absolute move. Keyword arguments should be axis names."""
|
"""Make an absolute move. Keyword arguments should be axis names."""
|
||||||
raise NotImplementedError("Subclasses should implement this method")
|
raise NotImplementedError("StageStub should not be used directly")
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def set_zero_position(self):
|
def set_zero_position(self):
|
||||||
"""Make the current position zero in all axes
|
"""Make the current position zero in all axes
|
||||||
|
|
@ -60,4 +116,7 @@ class Stage(Thing):
|
||||||
It is intended for use after manually or automatically recentring the
|
It is intended for use after manually or automatically recentring the
|
||||||
stage.
|
stage.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Subclasses should implement this method")
|
raise NotImplementedError("StageStub should not be used directly")
|
||||||
|
|
||||||
|
|
||||||
|
StageDependency: TypeAlias = direct_thing_client_dependency(StageStub, "/stage/")
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ from labthings_fastapi.dependencies.invocation import CancelHook, InvocationCanc
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from . import Stage
|
from . import BaseStage
|
||||||
|
|
||||||
|
|
||||||
class DummyStage(Stage):
|
class DummyStage(BaseStage):
|
||||||
"""A dummy stage for testing purposes
|
"""A dummy stage for testing purposes
|
||||||
|
|
||||||
This stage should work similarly to a Sangaboard stage, but without any
|
This stage should work similarly to a Sangaboard stage, but without any
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 906 B |
|
|
@ -1,19 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from openflexure_microscope.api import utilities
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input,expected",
|
|
||||||
[
|
|
||||||
("true", True),
|
|
||||||
("True", True),
|
|
||||||
("1", True),
|
|
||||||
(True, True),
|
|
||||||
("false", False),
|
|
||||||
("somestring", False),
|
|
||||||
(False, False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_get_bool(test_input, expected):
|
|
||||||
assert utilities.get_bool(test_input) == expected
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
from apispec.utils import validate_spec
|
|
||||||
import json
|
|
||||||
import jsonschema
|
|
||||||
import os
|
|
||||||
|
|
||||||
from openflexure_microscope.api.app import api_microscope, app, labthing
|
|
||||||
from openflexure_microscope.camera.base import BaseCamera
|
|
||||||
from openflexure_microscope.microscope import Microscope
|
|
||||||
from openflexure_microscope.stage.base import BaseStage
|
|
||||||
from labthings.json import encode_json
|
|
||||||
|
|
||||||
|
|
||||||
def test_app_creation():
|
|
||||||
assert labthing.app is app
|
|
||||||
|
|
||||||
|
|
||||||
def test_microscope_creation():
|
|
||||||
assert isinstance(api_microscope, Microscope)
|
|
||||||
assert isinstance(api_microscope.camera, BaseCamera)
|
|
||||||
assert isinstance(api_microscope.stage, BaseStage)
|
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_valid():
|
|
||||||
assert validate_spec(labthing.spec)
|
|
||||||
|
|
||||||
|
|
||||||
def test_thing_description_valid():
|
|
||||||
# TODO: it would be nice to put this into LabThings
|
|
||||||
# First load the schema from file and check it validates
|
|
||||||
schema_fname = os.path.join(os.path.dirname(__file__), "w3c_td_schema.json")
|
|
||||||
schema = json.load(open(schema_fname, "r"), encoding="utf-8")
|
|
||||||
jsonschema.Draft7Validator.check_schema(schema)
|
|
||||||
|
|
||||||
# Build a TD dictionary
|
|
||||||
with labthing.app.test_request_context():
|
|
||||||
td_dict = labthing.thing_description.to_dict()
|
|
||||||
|
|
||||||
# Allow our LabThingsJSONEncoder to encode the RD
|
|
||||||
td_json = encode_json(td_dict)
|
|
||||||
# Decode the JSON back into a primitive dictionary
|
|
||||||
td_json_dict = json.loads(td_json)
|
|
||||||
# Validate
|
|
||||||
jsonschema.validate(instance=td_json_dict, schema=schema)
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from openflexure_microscope.camera.base import FrameStream
|
|
||||||
|
|
||||||
|
|
||||||
def test_framestream_write():
|
|
||||||
d = b"openflexure"
|
|
||||||
|
|
||||||
with FrameStream() as f:
|
|
||||||
f.write(d)
|
|
||||||
assert f.getvalue() == d
|
|
||||||
|
|
||||||
|
|
||||||
def test_framestream_overwrite():
|
|
||||||
d1 = b"openflexure"
|
|
||||||
d2 = b"microscope"
|
|
||||||
|
|
||||||
with FrameStream() as f:
|
|
||||||
f.write(d1)
|
|
||||||
assert f.getvalue() == d1
|
|
||||||
f.write(d2)
|
|
||||||
assert f.getvalue() == d2
|
|
||||||
# d1 should be removed, so f.getbuffer() should only contain d2
|
|
||||||
assert f.getbuffer().nbytes == len(d2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracker():
|
|
||||||
sizes = range(1, 100)
|
|
||||||
|
|
||||||
with FrameStream() as f:
|
|
||||||
for length in sizes:
|
|
||||||
time.sleep(0.01)
|
|
||||||
d = b"\x00" + os.urandom(length) + b"\x00"
|
|
||||||
f.write(d)
|
|
||||||
assert f.getbuffer().nbytes == len(d)
|
|
||||||
|
|
||||||
# Make sure we have one TrackerFrame for each f.write() call
|
|
||||||
assert len(f.frames) == len(sizes)
|
|
||||||
# For each tested write size
|
|
||||||
for i, frame in enumerate(f.frames):
|
|
||||||
# Make sure the recorded size is what we expect
|
|
||||||
# Should be size +2 for our padding bytes \x00
|
|
||||||
assert frame.size == sizes[i] + 2
|
|
||||||
# Make sure we have a valid time value
|
|
||||||
assert isinstance(frame.time, float)
|
|
||||||
# Make sure the recorded time increases for each frame
|
|
||||||
if i > 0:
|
|
||||||
assert f.frames[i].time > f.frames[i - 1].time
|
|
||||||
|
|
||||||
# Test FrameStream.last
|
|
||||||
assert f.last.size == sizes[-1] + 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_frame_event():
|
|
||||||
d = b"openflexure"
|
|
||||||
|
|
||||||
def target(framestream):
|
|
||||||
time.sleep(0.01)
|
|
||||||
framestream.write(d)
|
|
||||||
|
|
||||||
with FrameStream() as f:
|
|
||||||
# Start a thread to write some data
|
|
||||||
thread = threading.Thread(target=target, args=(f,))
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
# Make sure the stream is empty before we wait for a frame
|
|
||||||
assert not f.getvalue()
|
|
||||||
# Wait for a frame to be written
|
|
||||||
frame = f.getframe()
|
|
||||||
assert frame == d
|
|
||||||
# Make sure getframe() cleared the event
|
|
||||||
assert not f.new_frame.events[threading.get_ident()][0].is_set()
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from freezegun import freeze_time
|
|
||||||
|
|
||||||
from openflexure_microscope.captures.capture_manager import (
|
|
||||||
BASE_CAPTURE_PATH,
|
|
||||||
TEMP_CAPTURE_PATH,
|
|
||||||
CaptureManager,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_manager():
|
|
||||||
m = CaptureManager()
|
|
||||||
assert BASE_CAPTURE_PATH
|
|
||||||
assert TEMP_CAPTURE_PATH
|
|
||||||
assert m.paths["default"] == BASE_CAPTURE_PATH
|
|
||||||
assert m.paths["temp"] == TEMP_CAPTURE_PATH
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_settings():
|
|
||||||
m = CaptureManager()
|
|
||||||
assert m.paths["default"] == BASE_CAPTURE_PATH
|
|
||||||
assert m.paths["temp"] == TEMP_CAPTURE_PATH
|
|
||||||
|
|
||||||
new_settings = {
|
|
||||||
"paths": {"default": "BASE_CAPTURE_PATH", "temp": "TEMP_CAPTURE_PATH"}
|
|
||||||
}
|
|
||||||
m.update_settings(new_settings)
|
|
||||||
assert m.paths["default"] == "BASE_CAPTURE_PATH"
|
|
||||||
assert m.paths["temp"] == "TEMP_CAPTURE_PATH"
|
|
||||||
assert m.read_settings() == new_settings
|
|
||||||
|
|
||||||
|
|
||||||
def test__new_output():
|
|
||||||
m = CaptureManager()
|
|
||||||
|
|
||||||
out = m._new_output(True, "filename", "subfolder", "fmt")
|
|
||||||
assert out.file == os.path.join(TEMP_CAPTURE_PATH, "subfolder", "filename.fmt")
|
|
||||||
|
|
||||||
|
|
||||||
def test__new_output_generate_filename():
|
|
||||||
m = CaptureManager()
|
|
||||||
|
|
||||||
out = m._new_output(True, None, "subfolder", "fmt")
|
|
||||||
# Assert a filename was actually generated
|
|
||||||
assert out.name != ".fmt"
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_image():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_image()
|
|
||||||
capture_key = str(out.id)
|
|
||||||
assert capture_key in m.images.keys()
|
|
||||||
assert m.images[capture_key] is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_video():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_video()
|
|
||||||
capture_key = str(out.id)
|
|
||||||
assert capture_key in m.videos.keys()
|
|
||||||
assert m.videos[capture_key] is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_from_id_str():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_image()
|
|
||||||
capture_key = str(out.id)
|
|
||||||
|
|
||||||
assert m.image_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_from_id_int():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_image()
|
|
||||||
capture_key = int(out.id)
|
|
||||||
|
|
||||||
assert m.image_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_from_id_uuid():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_image()
|
|
||||||
capture_key = out.id
|
|
||||||
|
|
||||||
assert m.image_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_from_id_invalid():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_image()
|
|
||||||
capture_key = object()
|
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
m.image_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_video_from_id_str():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_video()
|
|
||||||
capture_key = str(out.id)
|
|
||||||
|
|
||||||
assert m.video_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_video_from_id_int():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_video()
|
|
||||||
capture_key = int(out.id)
|
|
||||||
|
|
||||||
assert m.video_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_video_from_id_uuid():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_video()
|
|
||||||
capture_key = out.id
|
|
||||||
|
|
||||||
assert m.video_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
def test_video_from_id_invalid():
|
|
||||||
m = CaptureManager()
|
|
||||||
out = m.new_video()
|
|
||||||
capture_key = object()
|
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
m.video_from_id(capture_key) is out
|
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2020-11-27 12:00:01")
|
|
||||||
def test_generate_numbered_basename():
|
|
||||||
m = CaptureManager()
|
|
||||||
for _ in range(10):
|
|
||||||
m.new_image()
|
|
||||||
generated_filenames = [obj.basename for obj in m.images.values()]
|
|
||||||
# Assert enough names were generated
|
|
||||||
assert len(generated_filenames) == 10
|
|
||||||
# Assert all names are unique
|
|
||||||
len(generated_filenames) > len(set(generated_filenames))
|
|
||||||
# Assert all filenames are in the expected form
|
|
||||||
for fname in generated_filenames:
|
|
||||||
assert fname.startswith("2020-11-27_12-00-01")
|
|
||||||
|
|
||||||
|
|
||||||
def test_context_manager():
|
|
||||||
with CaptureManager() as m:
|
|
||||||
m.new_image()
|
|
||||||
m.new_video()
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
import datetime
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import piexif
|
|
||||||
from PIL import Image, ImageChops
|
|
||||||
|
|
||||||
from openflexure_microscope.captures import THUMBNAIL_SIZE, CaptureObject
|
|
||||||
from openflexure_microscope.captures.capture import (
|
|
||||||
build_captures_from_exif,
|
|
||||||
capture_from_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
IN_DIR = "tests/images/"
|
|
||||||
OUT_DIR = "tests/images/out/"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_small_image():
|
|
||||||
# Create a dummy image to serve in the stream
|
|
||||||
image = Image.new("RGB", (20, 20), color=(0, 0, 0))
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def generate_small_thumbnail():
|
|
||||||
# Create a dummy image to serve in the stream
|
|
||||||
image = Image.new("RGB", (20, 20), color=(0, 0, 0))
|
|
||||||
image.thumbnail(THUMBNAIL_SIZE)
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def generate_small_capture(filename: str):
|
|
||||||
out = os.path.join(OUT_DIR, filename)
|
|
||||||
obj = CaptureObject(out)
|
|
||||||
generate_small_image().save(obj, format="JPEG")
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def check_valid_capture(obj: CaptureObject):
|
|
||||||
assert isinstance(obj.time, datetime.datetime)
|
|
||||||
assert isinstance(obj.id, uuid.UUID)
|
|
||||||
|
|
||||||
assert isinstance(obj.annotations, dict)
|
|
||||||
assert isinstance(obj.tags, list)
|
|
||||||
|
|
||||||
assert obj.file
|
|
||||||
assert obj.format
|
|
||||||
assert obj.basename
|
|
||||||
assert obj.filefolder
|
|
||||||
assert obj.name
|
|
||||||
|
|
||||||
|
|
||||||
def check_valid_openflexure_metadata(d: dict):
|
|
||||||
assert "image" in d
|
|
||||||
assert d["image"].get("id")
|
|
||||||
assert d["image"].get("name")
|
|
||||||
assert d["image"].get("time")
|
|
||||||
assert d["image"].get("format")
|
|
||||||
|
|
||||||
assert isinstance(d["image"].get("tags"), list)
|
|
||||||
assert isinstance(d["image"].get("annotations"), dict)
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_capture_object():
|
|
||||||
obj = generate_small_capture("capture0.jpg")
|
|
||||||
check_valid_capture(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def test_initial_metadata():
|
|
||||||
obj = generate_small_capture("capture1.jpg")
|
|
||||||
|
|
||||||
data = obj.read_full_metadata()
|
|
||||||
check_valid_openflexure_metadata(data)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tags():
|
|
||||||
obj = generate_small_capture("capture2.jpg")
|
|
||||||
|
|
||||||
obj.put_tags(["foo", "bar"])
|
|
||||||
assert obj.read_full_metadata()["image"]["tags"] == ["foo", "bar"]
|
|
||||||
|
|
||||||
obj.delete_tag("foo")
|
|
||||||
assert obj.read_full_metadata()["image"]["tags"] == ["bar"]
|
|
||||||
|
|
||||||
# Delete nonexistent tag, should change nothing
|
|
||||||
obj.delete_tag("foo")
|
|
||||||
assert obj.read_full_metadata()["image"]["tags"] == ["bar"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_annotations():
|
|
||||||
obj = generate_small_capture("capture3.jpg")
|
|
||||||
|
|
||||||
obj.put_annotations({"foo": "zoo", "bar": "zar"})
|
|
||||||
assert obj.read_full_metadata()["image"]["annotations"] == {
|
|
||||||
"foo": "zoo",
|
|
||||||
"bar": "zar",
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.delete_annotation("foo")
|
|
||||||
assert obj.read_full_metadata()["image"]["annotations"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
# Delete nonexistent annotation, should change nothing
|
|
||||||
obj.delete_annotation("foo")
|
|
||||||
assert obj.read_full_metadata()["image"]["annotations"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_put_metadata():
|
|
||||||
obj = generate_small_capture("capture4.jpg")
|
|
||||||
|
|
||||||
obj.put_metadata({"foo": {"bar": "zar"}})
|
|
||||||
assert obj.read_full_metadata()["foo"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_put_and_save():
|
|
||||||
obj = generate_small_capture("capture_all.jpg")
|
|
||||||
obj.put_and_save(
|
|
||||||
tags=["foo", "bar"],
|
|
||||||
annotations={"foo": "zoo", "bar": "zar"},
|
|
||||||
metadata={"foo": {"bar": "zar"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
full_metadata = obj.read_full_metadata()
|
|
||||||
assert full_metadata["image"]["tags"] == ["foo", "bar"]
|
|
||||||
assert full_metadata["image"]["annotations"] == {"foo": "zoo", "bar": "zar"}
|
|
||||||
assert full_metadata["foo"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_capture_from_path_this_version():
|
|
||||||
"""Tests reloading a capture object from a file created in the working server version
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _check_metadata(data: dict):
|
|
||||||
assert data["image"]["tags"] == ["foo", "bar"]
|
|
||||||
assert data["image"]["annotations"] == {"foo": "zoo", "bar": "zar"}
|
|
||||||
assert data["foo"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
# Create the capture file
|
|
||||||
obj_in = generate_small_capture("capture_reload.jpg")
|
|
||||||
obj_in.put_and_save(
|
|
||||||
tags=["foo", "bar"],
|
|
||||||
annotations={"foo": "zoo", "bar": "zar"},
|
|
||||||
metadata={"foo": {"bar": "zar"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
full_metadata_in = obj_in.read_full_metadata()
|
|
||||||
check_valid_openflexure_metadata(full_metadata_in)
|
|
||||||
_check_metadata(full_metadata_in)
|
|
||||||
|
|
||||||
# Reload the capture file
|
|
||||||
obj = capture_from_path(os.path.join(OUT_DIR, "capture_reload.jpg"))
|
|
||||||
check_valid_capture(obj)
|
|
||||||
full_metadata = obj.read_full_metadata()
|
|
||||||
check_valid_openflexure_metadata(full_metadata)
|
|
||||||
_check_metadata(full_metadata)
|
|
||||||
|
|
||||||
|
|
||||||
def test_capture_from_path_v280():
|
|
||||||
"""Tests reloading a capture object from a file created in server v2.8.0
|
|
||||||
"""
|
|
||||||
obj = capture_from_path(os.path.join(IN_DIR, "capture_v280.jpg"))
|
|
||||||
check_valid_capture(obj)
|
|
||||||
full_metadata = obj.read_full_metadata()
|
|
||||||
check_valid_openflexure_metadata(full_metadata)
|
|
||||||
assert full_metadata["image"]["tags"] == ["foo", "bar"]
|
|
||||||
assert full_metadata["image"]["annotations"] == {"foo": "zoo", "bar": "zar"}
|
|
||||||
assert full_metadata["foo"] == {"bar": "zar"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_captures_from_exif():
|
|
||||||
# Load the whole tests captures directory
|
|
||||||
objs = build_captures_from_exif(IN_DIR)
|
|
||||||
# Make sure we have a valid, populated OrderedDict
|
|
||||||
assert isinstance(objs, OrderedDict)
|
|
||||||
assert len(objs) > 0
|
|
||||||
# Check each reloaded capture
|
|
||||||
for obj in objs.values():
|
|
||||||
check_valid_capture(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def test_data():
|
|
||||||
# Get a reference PIL image
|
|
||||||
ref_data = generate_small_image()
|
|
||||||
# Generate a capture with the same image data as our reference
|
|
||||||
obj = generate_small_capture("capture5.jpg")
|
|
||||||
# Pull the capture data out
|
|
||||||
obj_data = Image.open(obj.data)
|
|
||||||
# Compare the images
|
|
||||||
diff = ImageChops.difference(obj_data, ref_data)
|
|
||||||
assert not diff.getbbox()
|
|
||||||
|
|
||||||
|
|
||||||
def test_binary():
|
|
||||||
# Get a reference PIL image
|
|
||||||
ref_data = generate_small_image()
|
|
||||||
# Generate a capture with the same image data as our reference
|
|
||||||
obj = generate_small_capture("capture6.jpg")
|
|
||||||
# Pull the capture data out
|
|
||||||
obj_data = Image.open(io.BytesIO(obj.binary))
|
|
||||||
# Compare the images
|
|
||||||
diff = ImageChops.difference(obj_data, ref_data)
|
|
||||||
assert not diff.getbbox()
|
|
||||||
|
|
||||||
|
|
||||||
def test_thumbnail():
|
|
||||||
# Get a reference PIL image
|
|
||||||
ref_thumb = generate_small_thumbnail()
|
|
||||||
# Generate a capture with the same image data as our reference
|
|
||||||
obj = generate_small_capture("capture7.jpg")
|
|
||||||
# Pull the generated thumbnail out
|
|
||||||
obj_thumb = Image.open(obj.thumbnail)
|
|
||||||
# Compare the images
|
|
||||||
diff = ImageChops.difference(obj_thumb, ref_thumb)
|
|
||||||
assert not diff.getbbox()
|
|
||||||
|
|
||||||
# Assert thumbnail was saved to capture file EXIF data
|
|
||||||
exif_dict = piexif.load(obj.file)
|
|
||||||
thumbnail = exif_dict.pop("thumbnail")
|
|
||||||
assert thumbnail
|
|
||||||
# Compare the images
|
|
||||||
diff = ImageChops.difference(Image.open(io.BytesIO(thumbnail)), ref_thumb)
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete():
|
|
||||||
# Generate a capture with the same image data as our reference
|
|
||||||
obj = generate_small_capture("capture_del.jpg")
|
|
||||||
assert os.path.isfile(obj.file)
|
|
||||||
obj.delete()
|
|
||||||
assert not os.path.isfile(obj.file)
|
|
||||||
22
tests/test_dummy_server.py
Normal file
22
tests/test_dummy_server.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from labthings_fastapi.client import ThingClient
|
||||||
|
|
||||||
|
from openflexure_microscope_server.server import ThingServer
|
||||||
|
from openflexure_microscope_server.things.camera.simulation import SimulatedCamera
|
||||||
|
from openflexure_microscope_server.things.stage.dummy import DummyStage
|
||||||
|
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||||
|
|
||||||
|
temp_folder = tempfile.TemporaryDirectory()
|
||||||
|
server = ThingServer(temp_folder.name)
|
||||||
|
server.add_thing(SimulatedCamera(), "/camera/")
|
||||||
|
server.add_thing(DummyStage(), "/stage/")
|
||||||
|
server.add_thing(AutofocusThing(), "/autofocus/")
|
||||||
|
|
||||||
|
def test_autofocus():
|
||||||
|
with TestClient(server.app) as client:
|
||||||
|
autofocus = ThingClient.from_url("/autofocus/", client)
|
||||||
|
_ = autofocus.fast_autofocus()
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
from openflexure_microscope.api.default_extensions import scan
|
|
||||||
|
|
||||||
|
|
||||||
def test_construct_grid_raster():
|
|
||||||
grid = scan.construct_grid((0, 0), (100, 100), (3, 3), style="raster")
|
|
||||||
assert grid == [
|
|
||||||
[(0, 0), (0, 100), (0, 200)],
|
|
||||||
[(100, 0), (100, 100), (100, 200)],
|
|
||||||
[(200, 0), (200, 100), (200, 200)],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_construct_grid_snake():
|
|
||||||
grid = scan.construct_grid((0, 0), (100, 100), (3, 3), style="snake")
|
|
||||||
assert grid == [
|
|
||||||
[(0, 0), (0, 100), (0, 200)],
|
|
||||||
[(100, 200), (100, 100), (100, 0)],
|
|
||||||
[(200, 0), (200, 100), (200, 200)],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_construct_grid_spiral():
|
|
||||||
grid = scan.construct_grid((0, 0), (100, 100), (3, 0), style="spiral")
|
|
||||||
assert grid == [
|
|
||||||
[(0, 0)],
|
|
||||||
[
|
|
||||||
(0, 100),
|
|
||||||
(100, 100),
|
|
||||||
(100, 0),
|
|
||||||
(100, -100),
|
|
||||||
(0, -100),
|
|
||||||
(-100, -100),
|
|
||||||
(-100, 0),
|
|
||||||
(-100, 100),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
(-100, 200),
|
|
||||||
(0, 200),
|
|
||||||
(100, 200),
|
|
||||||
(200, 200),
|
|
||||||
(200, 100),
|
|
||||||
(200, 0),
|
|
||||||
(200, -100),
|
|
||||||
(200, -200),
|
|
||||||
(100, -200),
|
|
||||||
(0, -200),
|
|
||||||
(-100, -200),
|
|
||||||
(-200, -200),
|
|
||||||
(-200, -100),
|
|
||||||
(-200, 0),
|
|
||||||
(-200, 100),
|
|
||||||
(-200, 200),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
# Ensure second n_steps value makes no difference
|
|
||||||
assert grid == scan.construct_grid((0, 0), (100, 100), (3, 3), style="spiral")
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
from fractions import Fraction
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from openflexure_microscope.json import JSONEncoder
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input,expected",
|
|
||||||
[
|
|
||||||
(uuid.uuid1(), 1),
|
|
||||||
(uuid.uuid3(uuid.NAMESPACE_DNS, "openflexure.org"), 3),
|
|
||||||
(uuid.uuid4(), 4),
|
|
||||||
(uuid.uuid5(uuid.NAMESPACE_DNS, "openflexure.org"), 5),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_encode_uuid(test_input, expected):
|
|
||||||
encoded = json.dumps(test_input, cls=JSONEncoder)
|
|
||||||
decoded = json.loads(encoded)
|
|
||||||
assert uuid.UUID(decoded).version == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input",
|
|
||||||
[
|
|
||||||
Fraction(1, 2),
|
|
||||||
Fraction(1, 3),
|
|
||||||
Fraction(-27, 4),
|
|
||||||
Fraction(27, -4),
|
|
||||||
Fraction(10),
|
|
||||||
Fraction(18, 63),
|
|
||||||
Fraction(1, 1000000),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_encode_fraction(test_input):
|
|
||||||
encoded = json.dumps(test_input, cls=JSONEncoder)
|
|
||||||
decoded = json.loads(encoded)
|
|
||||||
assert Fraction(decoded).limit_denominator() == test_input
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input", [np.random.randint(100, dtype=np.int64) for _ in range(10)]
|
|
||||||
)
|
|
||||||
def test_encode_np_int(test_input):
|
|
||||||
encoded = json.dumps(test_input, cls=JSONEncoder)
|
|
||||||
decoded = json.loads(encoded)
|
|
||||||
assert np.int64(decoded) == test_input
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input", [np.float64(np.random.random()) for _ in range(10)]
|
|
||||||
)
|
|
||||||
def test_encode_np_float(test_input):
|
|
||||||
encoded = json.dumps(test_input, cls=JSONEncoder)
|
|
||||||
decoded = json.loads(encoded)
|
|
||||||
assert np.float64(decoded) == test_input
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from openflexure_microscope import utilities
|
|
||||||
|
|
||||||
|
|
||||||
def test_serialise_array_b64():
|
|
||||||
shape_in = (3, 2)
|
|
||||||
arr_in = np.random.randint(100, size=shape_in, dtype=np.int32)
|
|
||||||
|
|
||||||
b64_string, dtype, shape = utilities.serialise_array_b64(arr_in)
|
|
||||||
assert b64_string
|
|
||||||
assert dtype == "int32"
|
|
||||||
assert shape == shape_in
|
|
||||||
|
|
||||||
arr_out = utilities.deserialise_array_b64(b64_string, dtype, shape)
|
|
||||||
assert np.array_equal(arr_out, arr_in)
|
|
||||||
|
|
||||||
|
|
||||||
def test_ndarray_to_json():
|
|
||||||
shape_in = (3, 2)
|
|
||||||
arr_in = np.random.randint(100, size=shape_in, dtype=np.int32)
|
|
||||||
|
|
||||||
json_out = utilities.ndarray_to_json(arr_in)
|
|
||||||
|
|
||||||
arr_out = utilities.json_to_ndarray(json_out)
|
|
||||||
assert np.array_equal(arr_out, arr_in)
|
|
||||||
|
|
||||||
|
|
||||||
def test_axes_to_array():
|
|
||||||
dict_in = {"x": 1, "y": 2, "z": 3}
|
|
||||||
assert utilities.axes_to_array(dict_in) == [1, 2, 3]
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue