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:
parent
1e7069b375
commit
57177c6a56
3 changed files with 46 additions and 18 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue