Create basic gallery class that finds Things that want to show data

This commit is contained in:
Julian Stirling 2026-05-06 10:05:34 +01:00
parent 964a907edc
commit 93d7d75b0d
9 changed files with 97 additions and 35 deletions

View file

@ -8,6 +8,7 @@
}, },
"stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing", "stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing",
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem",
"illumination": "openflexure_microscope_server.things.illumination:SangaIllumination", "illumination": "openflexure_microscope_server.things.illumination:SangaIllumination",

View file

@ -3,6 +3,7 @@
"camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
"stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage",
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination", "illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination",
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem",

View file

@ -15,6 +15,13 @@ class OFMThing(lt.Thing):
_data_dir: Optional[str] = None _data_dir: Optional[str] = None
_show_in_gallery: bool = False
@property
def show_in_gallery(self) -> bool:
"""Whether to show in the Gallery."""
return self._show_in_gallery
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Set the data directory when the Thing is entered.""" """Set the data directory when the Thing is entered."""
# Note that the `application_config` was already validated when the # Note that the `application_config` was already validated when the

View file

@ -25,6 +25,7 @@ from pydantic import BaseModel, Field
import labthings_fastapi as lt import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.types.numpy import NDArray
from openflexure_microscope_server.things import OFMThing
from openflexure_microscope_server.things.background_detect import ( from openflexure_microscope_server.things.background_detect import (
BackgroundDetectAlgorithm, BackgroundDetectAlgorithm,
) )
@ -164,7 +165,7 @@ class CameraMemoryBuffer:
del self._storage[key] del self._storage[key]
class BaseCamera(lt.Thing): class BaseCamera(OFMThing):
"""The base class for all cameras. All cameras must directly inherit from this class. """The base class for all cameras. All cameras must directly inherit from this class.
The connection to the camera hardware should be added to the ``__enter__`` method not The connection to the camera hardware should be added to the ``__enter__`` method not

View file

@ -0,0 +1,31 @@
"""Server-side gallery functionality.
The types of data that are captured and how they display in the gallery are defined by
the Things that capture the Data. This thing just provides a unified way for the gallery
front end to access the data.
"""
from typing import Mapping, Optional
import labthings_fastapi as lt
from openflexure_microscope_server.things import OFMThing
class GalleryThing(lt.Thing):
"""A Thing for communicating with the front end gallery."""
all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot()
_gallery_providing_things: Optional[Mapping[str, OFMThing]] = None
@property
def gallery_providing_things(self) -> Mapping[str, OFMThing]:
"""All Things that provide data to the gallery."""
if self._gallery_providing_things is None:
self._gallery_providing_things = {
name: thing
for name, thing in self.all_ofm_things.items()
if thing.show_in_gallery
}
return self._gallery_providing_things

View file

@ -176,6 +176,9 @@ class SmartScanThing(OFMThing):
In this case it doesn't need to do anything. In this case it doesn't need to do anything.
""" """
# Register with gallery.
_show_in_gallery = True
# Note that the default detector name is set at init. This is over written if # Note that the default detector name is set at init. This is over written if
# setting is loaded from disk. # setting is loaded from disk.
@lt.setting @lt.setting

View file

@ -1,6 +1,8 @@
"""Fixtures for all test suites.""" """Fixtures for all test suites."""
import json
import logging import logging
import os
import re import re
from collections.abc import Iterable from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
@ -10,6 +12,24 @@ import pytest
from labthings_fastapi.testing import create_thing_without_server from labthings_fastapi.testing import create_thing_without_server
from .shared_utils.lt_test_utils import LabThingsTestEnv
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(THIS_DIR)
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
@pytest.fixture
def simulation_test_env():
"""Yield a server with the configuration from the simulation json."""
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
config_dict = json.load(f_obj)
with LabThingsTestEnv(
things=config_dict["things"],
application_config=config_dict["application_config"],
) as env:
yield env
@pytest.fixture @pytest.fixture
def check_side_effect(caplog): def check_side_effect(caplog):

View file

@ -12,7 +12,6 @@ it has been moved as it artificaially inflated coverage.
""" """
import json import json
import os
import numpy as np import numpy as np
import piexif import piexif
@ -21,29 +20,11 @@ from PIL import Image
import labthings_fastapi as lt import labthings_fastapi as lt
from ..shared_utils.lt_test_utils import LabThingsTestEnv
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) def test_autofocus(simulation_test_env):
REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR))
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
@pytest.fixture
def test_env():
"""Yield a server with a very basic configuration."""
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
config_dict = json.load(f_obj)
with LabThingsTestEnv(
things=config_dict["things"],
application_config=config_dict["application_config"],
) as env:
yield env
def test_autofocus(test_env):
"""Test Fast Autofocus can run doesn't raise an exception.""" """Test Fast Autofocus can run doesn't raise an exception."""
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
autofocus = test_env.get_thing_client("autofocus") autofocus = simulation_test_env.get_thing_client("autofocus")
assert stage.position["z"] == 0 assert stage.position["z"] == 0
# Autofocus 5 times and check each ends within 500 steps # Autofocus 5 times and check each ends within 500 steps
for i in range(5): for i in range(5):
@ -51,17 +32,17 @@ def test_autofocus(test_env):
assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}" assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}"
def test_grab_jpeg(test_env): def test_grab_jpeg(simulation_test_env):
"""Check that grab_jpeg returns a blob that can be opened.""" """Check that grab_jpeg returns a blob that can be opened."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
blob = camera.grab_jpeg() blob = camera.grab_jpeg()
image = Image.open(blob.open()) image = Image.open(blob.open())
assert image.size == (820, 616) assert image.size == (820, 616)
def test_capture_jpeg_metadata(test_env): def test_capture_jpeg_metadata(simulation_test_env):
"""Check that the position is encoded into the image metadata.""" """Check that the position is encoded into the image metadata."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
blob = camera.capture_jpeg() blob = camera.capture_jpeg()
image = Image.open(blob.open()) image = Image.open(blob.open())
exif_dict = piexif.load(image.info["exif"]) exif_dict = piexif.load(image.info["exif"])
@ -72,9 +53,9 @@ def test_capture_jpeg_metadata(test_env):
assert image.size == (820, 616) assert image.size == (820, 616)
def test_stage(test_env): def test_stage(simulation_test_env):
"""Test moving the stage forwards and backwards.""" """Test moving the stage forwards and backwards."""
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
start = stage.position start = stage.position
move = {"x": 1, "y": 2, "z": 3} move = {"x": 1, "y": 2, "z": 3}
move_back = {"x": -1, "y": -2, "z": -3} move_back = {"x": -1, "y": -2, "z": -3}
@ -95,20 +76,20 @@ def test_stage(test_env):
assert start["z"] == pos["z"] assert start["z"] == pos["z"]
def test_capture_array(test_env): def test_capture_array(simulation_test_env):
"""Capture array from simulation and check the size is as expected.""" """Capture array from simulation and check the size is as expected."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
array = np.asarray(camera.capture_array()) array = np.asarray(camera.capture_array())
assert array.shape == (616, 820, 3) assert array.shape == (616, 820, 3)
def test_camera_stage_mapping_calibration(test_env): def test_camera_stage_mapping_calibration(simulation_test_env):
"""Check that camera stage mapping runs and returns the expected result.""" """Check that camera stage mapping runs and returns the expected result."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
# Remove camera settling time for speed. # Remove camera settling time for speed.
camera.settling_time = 0 camera.settling_time = 0
csm = test_env.get_thing_client("camera_stage_mapping") csm = simulation_test_env.get_thing_client("camera_stage_mapping")
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
# Check it starts uncalibrated # Check it starts uncalibrated
assert csm.calibration_required assert csm.calibration_required

View file

@ -0,0 +1,17 @@
"""Tests that captures have the expected metadata."""
def test_gallery_thing_finds_all_providers(simulation_test_env):
"""Check the gallery identifies the correct Things that will provide data."""
gallery = simulation_test_env.get_thing_by_name("gallery")
snake_workflow = simulation_test_env.get_thing_by_name("snake_workflow")
smart_scan = simulation_test_env.get_thing_by_name("smart_scan")
camera = simulation_test_env.get_thing_by_name("camera")
assert snake_workflow not in gallery.all_ofm_things.values()
assert camera in gallery.all_ofm_things.values()
assert smart_scan in gallery.all_ofm_things.values()
assert snake_workflow not in gallery.gallery_providing_things.values()
assert camera not in gallery.gallery_providing_things.values()
assert smart_scan in gallery.gallery_providing_things.values()