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
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue