Start fixing tests. This involves further blocking portal changes.
This will need PR 225 of labthings-fastapi to be merged to run.
This commit is contained in:
parent
9ef417971f
commit
cfbb7cf7f9
5 changed files with 33 additions and 35 deletions
|
|
@ -319,7 +319,6 @@ class BaseCamera(lt.Thing):
|
||||||
@lt.action
|
@lt.action
|
||||||
def grab_jpeg(
|
def grab_jpeg(
|
||||||
self,
|
self,
|
||||||
portal: lt.deps.BlockingPortal,
|
|
||||||
stream_name: Literal["main", "lores"] = "main",
|
stream_name: Literal["main", "lores"] = "main",
|
||||||
) -> JPEGBlob:
|
) -> JPEGBlob:
|
||||||
"""Acquire one image from the preview stream and return as blob of JPEG data.
|
"""Acquire one image from the preview stream and return as blob of JPEG data.
|
||||||
|
|
@ -337,13 +336,12 @@ class BaseCamera(lt.Thing):
|
||||||
stream = (
|
stream = (
|
||||||
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream
|
||||||
)
|
)
|
||||||
frame = portal.call(stream.grab_frame)
|
frame = self._thing_server_interface.call_async_task(stream.grab_frame)
|
||||||
return JPEGBlob.from_bytes(frame)
|
return JPEGBlob.from_bytes(frame)
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def grab_as_array(
|
def grab_as_array(
|
||||||
self,
|
self,
|
||||||
portal: lt.deps.BlockingPortal,
|
|
||||||
stream_name: Literal["main", "lores"] = "main",
|
stream_name: Literal["main", "lores"] = "main",
|
||||||
) -> ArrayModel:
|
) -> ArrayModel:
|
||||||
"""Acquire one image from the preview stream and return as an array.
|
"""Acquire one image from the preview stream and return as an array.
|
||||||
|
|
@ -361,7 +359,7 @@ class BaseCamera(lt.Thing):
|
||||||
tries = 0
|
tries = 0
|
||||||
while tries < 3:
|
while tries < 3:
|
||||||
try:
|
try:
|
||||||
frame = portal.call(stream.grab_frame)
|
frame = self._thing_server_interface.call_async_task(stream.grab_frame)
|
||||||
return np.asarray(Image.open(io.BytesIO(frame)))
|
return np.asarray(Image.open(io.BytesIO(frame)))
|
||||||
except OSError:
|
except OSError:
|
||||||
tries += 1
|
tries += 1
|
||||||
|
|
@ -370,14 +368,13 @@ class BaseCamera(lt.Thing):
|
||||||
@lt.action
|
@lt.action
|
||||||
def grab_jpeg_size(
|
def grab_jpeg_size(
|
||||||
self,
|
self,
|
||||||
portal: lt.deps.BlockingPortal,
|
|
||||||
stream_name: Literal["main", "lores"] = "main",
|
stream_name: Literal["main", "lores"] = "main",
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Acquire one image from the preview stream and return its size."""
|
"""Acquire one image from the preview stream and return its size."""
|
||||||
stream = (
|
stream = (
|
||||||
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 self._thing_server_interface.call_async_task(stream.next_frame_size)
|
||||||
|
|
||||||
def capture_image(
|
def capture_image(
|
||||||
self,
|
self,
|
||||||
|
|
@ -705,13 +702,13 @@ class BaseCamera(lt.Thing):
|
||||||
)
|
)
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def image_is_sample(self, portal: lt.deps.BlockingPortal) -> tuple[bool, str]:
|
def image_is_sample(self) -> tuple[bool, str]:
|
||||||
"""Label the current image as either background or sample."""
|
"""Label the current image as either background or sample."""
|
||||||
current_image = self.grab_as_array(portal, stream_name="lores")
|
current_image = self.grab_as_array(stream_name="lores")
|
||||||
return self.active_detector.image_is_sample(current_image)
|
return self.active_detector.image_is_sample(current_image)
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def set_background(self, portal: lt.deps.BlockingPortal) -> None:
|
def set_background(self) -> None:
|
||||||
"""Grab an image, and use its statistics to set the background.
|
"""Grab an image, and use its statistics to set the background.
|
||||||
|
|
||||||
This should be run when the microscope is looking at an empty region,
|
This should be run when the microscope is looking at an empty region,
|
||||||
|
|
@ -720,7 +717,7 @@ class BaseCamera(lt.Thing):
|
||||||
future images to the distribution, to determine if each pixel is
|
future images to the distribution, to determine if each pixel is
|
||||||
foreground or background.
|
foreground or background.
|
||||||
"""
|
"""
|
||||||
background = self.grab_as_array(portal, stream_name="lores")
|
background = self.grab_as_array(stream_name="lores")
|
||||||
self.active_detector.set_background(background)
|
self.active_detector.set_background(background)
|
||||||
# Manually save settings as the setter is not called.
|
# Manually save settings as the setter is not called.
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,8 @@ class PicameraStreamOutput(Output):
|
||||||
def __init__(self, stream: lt.outputs.MJPEGStream) -> None:
|
def __init__(self, stream: lt.outputs.MJPEGStream) -> None:
|
||||||
"""Create an output that puts frames in an MJPEGStream.
|
"""Create an output that puts frames in an MJPEGStream.
|
||||||
|
|
||||||
We need to pass the stream object, and also the blocking portal, because
|
We need to pass the stream object, because new frame notifications happen in
|
||||||
new frame notifications happen in the anyio event loop and frames are
|
the anyio event loop and frames are sent from a thread.
|
||||||
sent from a thread. The blocking portal enables thread-to-async
|
|
||||||
communication.
|
|
||||||
"""
|
"""
|
||||||
Output.__init__(self)
|
Output.__init__(self)
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
|
@ -744,7 +742,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
self._initialise_picamera()
|
self._initialise_picamera()
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None:
|
def full_auto_calibrate(self) -> None:
|
||||||
"""Perform a full auto-calibration.
|
"""Perform a full auto-calibration.
|
||||||
|
|
||||||
This function will call the other calibration actions in sequence:
|
This function will call the other calibration actions in sequence:
|
||||||
|
|
@ -763,7 +761,7 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
for _i in range(3):
|
for _i in range(3):
|
||||||
try:
|
try:
|
||||||
time.sleep(self._sensor_info.long_pause)
|
time.sleep(self._sensor_info.long_pause)
|
||||||
self.set_background(portal)
|
self.set_background()
|
||||||
# Return if background is set
|
# Return if background is set
|
||||||
return
|
return
|
||||||
except ChannelBlankError:
|
except ChannelBlankError:
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@ class SimulatedCamera(BaseCamera):
|
||||||
return self.generate_frame()
|
return self.generate_frame()
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None:
|
def full_auto_calibrate(self) -> None:
|
||||||
"""Perform a full auto-calibration.
|
"""Perform a full auto-calibration.
|
||||||
|
|
||||||
For the simulation microscope the process is:
|
For the simulation microscope the process is:
|
||||||
|
|
@ -361,7 +361,7 @@ class SimulatedCamera(BaseCamera):
|
||||||
"""
|
"""
|
||||||
self.remove_sample()
|
self.remove_sample()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
self.set_background(portal)
|
self.set_background()
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
self.load_sample()
|
self.load_sample()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ This doesn't check the behaviour of the JPEG shaprness monitor.
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from labthings_fastapi.testing import create_thing_without_server
|
||||||
|
|
||||||
from openflexure_microscope_server.things.autofocus import (
|
from openflexure_microscope_server.things.autofocus import (
|
||||||
AutofocusThing,
|
AutofocusThing,
|
||||||
NoFocusFoundError,
|
NoFocusFoundError,
|
||||||
|
|
@ -77,7 +79,7 @@ def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes,
|
||||||
|
|
||||||
sharpness_monitor.move_data.side_effect = return_sharpness
|
sharpness_monitor.move_data.side_effect = return_sharpness
|
||||||
|
|
||||||
autofocus_thing = AutofocusThing()
|
autofocus_thing = create_thing_without_server(AutofocusThing)
|
||||||
if passes:
|
if passes:
|
||||||
autofocus_thing.looping_autofocus(
|
autofocus_thing.looping_autofocus(
|
||||||
stage=stage,
|
stage=stage,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Use the Simulation camera to test base camera functionality."""
|
"""Use the Simulation camera to test base camera functionality."""
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -10,29 +9,30 @@ from PIL import Image
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
||||||
from openflexure_microscope_server.things.camera.simulation import SimulatedCamera
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
@contextmanager
|
def camera_server() -> lt.ThingClient:
|
||||||
def camera_server(camera: SimulatedCamera) -> lt.ThingClient:
|
|
||||||
"""Add the camera to a ThingServer and start a TestClient application.
|
"""Add the camera to a ThingServer and start a TestClient application.
|
||||||
|
|
||||||
The test client application is needed for the camera to have a blocking portal.
|
The test client will be needed for the camera to run async frame generation code.
|
||||||
"""
|
"""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
server = lt.ThingServer(settings_folder=tmpdir)
|
conf = {
|
||||||
server.add_thing(camera, "camera")
|
"camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
|
||||||
with TestClient(server.app):
|
"stage": "openflexure_microscope_server.things.stage.dummy:DummyStage",
|
||||||
yield server
|
}
|
||||||
|
|
||||||
|
server = lt.ThingServer(things=conf, settings_folder=tmpdir)
|
||||||
|
yield server
|
||||||
|
|
||||||
|
|
||||||
def test_handle_broken_frame():
|
def test_handle_broken_frame(camera_server):
|
||||||
"""Monkey patch the the mjpeg steam so 1 in 5 frames are broken, then test operation.
|
"""Monkey patch the the mjpeg steam so 1 in 5 frames are broken, then test operation.
|
||||||
|
|
||||||
This simulates the very occasional broken frames that can occur when grabbing
|
This simulates the very occasional broken frames that can occur when grabbing
|
||||||
directly from the MJPEG stream.
|
directly from the MJPEG stream.
|
||||||
"""
|
"""
|
||||||
camera = SimulatedCamera()
|
camera = camera_server.things["camera"]
|
||||||
|
|
||||||
# Money patch the mjpeg_stream grab_frame to break 1 in 5 frames.
|
# Money patch the mjpeg_stream grab_frame to break 1 in 5 frames.
|
||||||
frame_number = 0
|
frame_number = 0
|
||||||
|
|
@ -50,7 +50,8 @@ def test_handle_broken_frame():
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
camera.mjpeg_stream.grab_frame = flaky_grabber
|
camera.mjpeg_stream.grab_frame = flaky_grabber
|
||||||
with camera_server(camera):
|
|
||||||
|
with TestClient(camera_server.app):
|
||||||
# Check that this does cause broken frames.
|
# Check that this does cause broken frames.
|
||||||
# The noqa is because we don't know exactly when the error is thrown so we
|
# The noqa is because we don't know exactly when the error is thrown so we
|
||||||
# can't have a single simple statement in the pytest raises.
|
# can't have a single simple statement in the pytest raises.
|
||||||
|
|
@ -68,10 +69,10 @@ def test_handle_broken_frame():
|
||||||
assert isinstance(array, np.ndarray)
|
assert isinstance(array, np.ndarray)
|
||||||
|
|
||||||
|
|
||||||
def test_simulation_cam_calibration():
|
def test_simulation_cam_calibration(camera_server):
|
||||||
"""Test that the simulated camera can be calibrated and reports calibration correctly."""
|
"""Test that the simulated camera can be calibrated and reports calibration correctly."""
|
||||||
camera = SimulatedCamera()
|
camera = camera_server.things["camera"]
|
||||||
with camera_server(camera):
|
with TestClient(camera_server.app):
|
||||||
assert camera.calibration_required
|
assert camera.calibration_required
|
||||||
camera.full_auto_calibrate()
|
camera.full_auto_calibrate()
|
||||||
assert not camera.calibration_required
|
assert not camera.calibration_required
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue