Sped up camera and got CSM passing

Using lower resolution helps speed it up, and skipping CSM settling time makes a big difference.
This commit is contained in:
Richard Bowman 2024-12-03 06:35:16 +00:00
parent 1e7069b375
commit 57177c6a56
3 changed files with 46 additions and 18 deletions

View file

@ -23,21 +23,31 @@ 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 labthings_fastapi.server import ThingServer from labthings_fastapi.server import ThingServer
from pydantic import RootModel
from . import BaseCamera, JPEGBlob from . import BaseCamera, JPEGBlob
from ..stage import StageProtocol as Stage from ..stage import StageProtocol as Stage
class ArrayModel(RootModel):
"""A model for an array"""
root: NDArray
class SimulatedCamera(BaseCamera): class SimulatedCamera(BaseCamera):
"""A Thing representing an OpenCV camera""" """A Thing representing an OpenCV camera"""
shape = (600, 800, 3)
glyph_shape = (51, 51, 3)
canvas_shape = (3000, 4000, 3)
frame_interval = 0.1
_stage: Optional[Stage] = None _stage: Optional[Stage] = None
_server: Optional[ThingServer] = None _server: Optional[ThingServer] = None
def __init__(self): def __init__(
self,
shape: tuple[int, int, int] = (600, 800, 3),
glyph_shape: tuple[int, int, int] = (51, 51, 3),
canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
frame_interval: float = 0.1,
):
self.shape = shape
self.glyph_shape = glyph_shape
self.canvas_shape = canvas_shape
self.frame_interval = frame_interval
self._capture_thread: Optional[Thread] = None self._capture_thread: Optional[Thread] = None
self._capture_enabled = False self._capture_enabled = False
self.generate_sprites() self.generate_sprites()
@ -85,9 +95,12 @@ class SimulatedCamera(BaseCamera):
cw, ch, _ = self.canvas_shape cw, ch, _ = self.canvas_shape
w, h, _ = self.shape w, h, _ = self.shape
tl = (int(pos[0]) - w//2 - cw//2, int(pos[1]) - h//2 - ch//2) tl = (int(pos[0]) - w//2 - cw//2, int(pos[1]) - h//2 - ch//2)
return self.canvas[ image = self.canvas[
tuple(slice(tl[i],tl[i] + self.shape[i]) for i in range(2)) + (slice(None),) tuple(slice(tl[i],tl[i] + self.shape[i]) for i in range(2)) + (slice(None),)
] ]
if image.shape != self.shape:
raise ValueError(f"Image shape {image.shape} does not match intended shape {self.shape}")
return image
def attach_to_server(self, server: ThingServer, path: str): def attach_to_server(self, server: ThingServer, path: str):
self._server = server self._server = server
@ -131,17 +144,20 @@ class SimulatedCamera(BaseCamera):
portal = get_blocking_portal(self) portal = get_blocking_portal(self)
while self._capture_enabled: while self._capture_enabled:
time.sleep(self.frame_interval) time.sleep(self.frame_interval)
frame = self.generate_frame() try:
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() frame = self.generate_frame()
self.mjpeg_stream.add_frame(jpeg, portal) jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[1].tobytes() self.mjpeg_stream.add_frame(jpeg, portal)
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal) jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[1].tobytes()
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
except Exception as e:
logging.error(f"Failed to capture frame: {e}, retrying...")
@thing_action @thing_action
def capture_array( def capture_array(
self, self,
resolution: Literal["main", "full"] = "full", resolution: Literal["main", "full"] = "full",
) -> NDArray: ) -> ArrayModel:
"""Acquire one image from the camera and return as an array """Acquire one image from the camera and return as an array
This function will produce a nested list containing an uncompressed RGB image. This function will produce a nested list containing an uncompressed RGB image.

View file

@ -61,6 +61,9 @@ def downsample(factor: int, image: np.ndarray) -> np.ndarray:
return reshaped.mean(axis=(1,3)) return reshaped.mean(axis=(1,3))
DEFAULT_SETTLING_TIME = 0.2
def make_hardware_interface( def make_hardware_interface(
stage: Stage, camera: Camera, downsample_factor: int = 2 stage: Stage, camera: Camera, downsample_factor: int = 2
) -> HardwareInterfaceModel: ) -> HardwareInterfaceModel:
@ -81,7 +84,7 @@ def make_hardware_interface(
img = camera.capture_array() img = camera.capture_array()
return downsample(downsample_factor, img) return downsample(downsample_factor, img)
def settle() -> None: def settle() -> None:
time.sleep(0.2) time.sleep(DEFAULT_SETTLING_TIME)
try: try:
camera.capture_metadata # This discards frames on a picamera camera.capture_metadata # This discards frames on a picamera
except AttributeError: except AttributeError:

View file

@ -6,6 +6,7 @@ from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from labthings_fastapi.client import ThingClient from labthings_fastapi.client import ThingClient
from PIL import Image from PIL import Image
import numpy as np
import piexif import piexif
import pytest import pytest
@ -14,12 +15,15 @@ from openflexure_microscope_server.things.camera.simulation import SimulatedCame
from openflexure_microscope_server.things.stage.dummy import DummyStage from openflexure_microscope_server.things.stage.dummy import DummyStage
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 import camera_stage_mapping
camera_stage_mapping.DEFAULT_SETTLING_TIME = 0 # skip the settling time for tests
@pytest.fixture @pytest.fixture
def thing_server(): def thing_server():
temp_folder = tempfile.TemporaryDirectory() temp_folder = tempfile.TemporaryDirectory()
server = ThingServer(settings_folder=temp_folder.name) server = ThingServer(settings_folder=temp_folder.name)
server.add_thing(SimulatedCamera(), "/camera/") server.add_thing(SimulatedCamera(shape=(240, 320, 3), canvas_shape=(960, 1240, 3), frame_interval=0.01), "/camera/")
server.add_thing(DummyStage(step_time=0.000001), "/stage/") server.add_thing(DummyStage(step_time=0.000001), "/stage/")
server.add_thing(AutofocusThing(), "/autofocus/") server.add_thing(AutofocusThing(), "/autofocus/")
server.add_thing(CameraStageMapper(), "/camera_stage_mapping/") server.add_thing(CameraStageMapper(), "/camera_stage_mapping/")
@ -34,7 +38,7 @@ def client(thing_server):
@pytest.fixture @pytest.fixture
def slower_client(thing_server): def slower_client(thing_server):
thing_server.things["/stage/"].step_time = 0.0002 thing_server.things["/stage/"].step_time = 0.0001
with TestClient(thing_server.app) as client: with TestClient(thing_server.app) as client:
yield client yield client
@ -70,7 +74,12 @@ def test_stage(client):
for s, p in zip(start.values(), pos.values()): for s, p in zip(start.values(), pos.values()):
assert s == p assert s == p
def test_capture_array(client):
camera = ThingClient.from_url("/camera/", client)
array = np.asarray(camera.capture_array())
assert array.shape == (240, 320, 3)
# Currently this fails, not yet sure why. # Currently this fails, not yet sure why.
#def test_camera_stage_mapping_calibration(client): def test_camera_stage_mapping_calibration(client):
# camera_stage_mapping = ThingClient.from_url("/camera_stage_mapping/", client) camera_stage_mapping = ThingClient.from_url("/camera_stage_mapping/", client)
# camera_stage_mapping.calibrate_xy() camera_stage_mapping.calibrate_xy()