From 57177c6a565d887f11af297fd99d9dbb4153ee5c Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Tue, 3 Dec 2024 06:35:16 +0000 Subject: [PATCH] Sped up camera and got CSM passing Using lower resolution helps speed it up, and skipping CSM settling time makes a big difference. --- .../things/camera/simulation.py | 40 +++++++++++++------ .../things/camera_stage_mapping.py | 5 ++- tests/test_dummy_server.py | 19 ++++++--- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index dd169283..15eb57d3 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -23,21 +23,31 @@ from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.server import ThingServer +from pydantic import RootModel from . import BaseCamera, JPEGBlob from ..stage import StageProtocol as Stage +class ArrayModel(RootModel): + """A model for an array""" + root: NDArray class SimulatedCamera(BaseCamera): """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 _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_enabled = False self.generate_sprites() @@ -85,9 +95,12 @@ class SimulatedCamera(BaseCamera): cw, ch, _ = self.canvas_shape w, h, _ = self.shape 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),) ] + 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): self._server = server @@ -131,17 +144,20 @@ class SimulatedCamera(BaseCamera): portal = get_blocking_portal(self) while self._capture_enabled: time.sleep(self.frame_interval) - frame = self.generate_frame() - jpeg = cv2.imencode(".jpg", frame)[1].tobytes() - self.mjpeg_stream.add_frame(jpeg, portal) - jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[1].tobytes() - self.lores_mjpeg_stream.add_frame(jpeg_lores, portal) + try: + frame = self.generate_frame() + jpeg = cv2.imencode(".jpg", frame)[1].tobytes() + self.mjpeg_stream.add_frame(jpeg, 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 def capture_array( self, resolution: Literal["main", "full"] = "full", - ) -> NDArray: + ) -> ArrayModel: """Acquire one image from the camera and return as an array This function will produce a nested list containing an uncompressed RGB image. diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index 72415336..4fd9757e 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -61,6 +61,9 @@ def downsample(factor: int, image: np.ndarray) -> np.ndarray: return reshaped.mean(axis=(1,3)) +DEFAULT_SETTLING_TIME = 0.2 + + def make_hardware_interface( stage: Stage, camera: Camera, downsample_factor: int = 2 ) -> HardwareInterfaceModel: @@ -81,7 +84,7 @@ def make_hardware_interface( img = camera.capture_array() return downsample(downsample_factor, img) def settle() -> None: - time.sleep(0.2) + time.sleep(DEFAULT_SETTLING_TIME) try: camera.capture_metadata # This discards frames on a picamera except AttributeError: diff --git a/tests/test_dummy_server.py b/tests/test_dummy_server.py index 9ebe8754..fdc7dbb1 100644 --- a/tests/test_dummy_server.py +++ b/tests/test_dummy_server.py @@ -6,6 +6,7 @@ from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from labthings_fastapi.client import ThingClient from PIL import Image +import numpy as np import piexif 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.autofocus import AutofocusThing 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 def thing_server(): temp_folder = tempfile.TemporaryDirectory() 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(AutofocusThing(), "/autofocus/") server.add_thing(CameraStageMapper(), "/camera_stage_mapping/") @@ -34,7 +38,7 @@ def client(thing_server): @pytest.fixture 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: yield client @@ -70,7 +74,12 @@ def test_stage(client): for s, p in zip(start.values(), pos.values()): 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. -#def test_camera_stage_mapping_calibration(client): -# camera_stage_mapping = ThingClient.from_url("/camera_stage_mapping/", client) -# camera_stage_mapping.calibrate_xy() +def test_camera_stage_mapping_calibration(client): + camera_stage_mapping = ThingClient.from_url("/camera_stage_mapping/", client) + camera_stage_mapping.calibrate_xy()