Complete first pass (untested on hardware) of bg detect refactor.

This commit is contained in:
Julian Stirling 2025-07-18 19:49:36 +01:00
parent 2245d9357d
commit fe1b84a922
10 changed files with 90 additions and 88 deletions

View file

@ -20,7 +20,11 @@ import piexif
import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray
from openflexure_microscope_server.background_detect import BackgroundDetectLUV
from openflexure_microscope_server.background_detect import (
ColourChannelDetectLUV,
BackgroundDetectAlgorithm,
BackgroundDetectorStatus,
)
LOGGER = logging.getLogger(__name__)
@ -166,7 +170,7 @@ class BaseCamera(lt.Thing):
This must be run by all child camera classes.
"""
super().__init__()
self.background_detectors = {"Colour Channels (LUV)": BackgroundDetectLUV()}
self.background_detectors = {"Colour Channels (LUV)": ColourChannelDetectLUV()}
self._detector_name = "Colour Channels (LUV)"
def __enter__(self) -> None:
@ -484,23 +488,36 @@ class BaseCamera(lt.Thing):
self._detector_name = name
@property
def active_detector(self):
def active_detector(self) -> BackgroundDetectAlgorithm:
"""The active background detector instance."""
return self.background_detectors[self.detector_name]
@lt.thing_property
def background_detector_status(self) -> BackgroundDetectorStatus:
"""The status of the active detector for the UI."""
return self.active_detector.status
@lt.thing_setting
def background_detector_data(self) -> str:
"""The name of the active background selector."""
"""The data for each background detector, used to save to disk."""
data = {}
for name, obj in self.background_detectors.items():
bg_data = (
None
if obj.background_data is None
else obj.background_data.model_dump()
)
data[name] = {
"settings": obj.settings.model_dump(),
"background_data": obj.background_data.model_dump(),
"background_data": bg_data,
}
@detector_name.setter
def detector_name(self, data: str) -> None:
"""Validate and set detector_name."""
"""Set the data for each detector. This should only be called on init.
Do not call over HTTP. Would be good to have a way to make this private.
"""
for name, instance_data in data.items():
if name in self.background_detectors:
obj = self.background_detectors[name]

View file

@ -118,7 +118,7 @@ class StreamingPiCamera2(BaseCamera):
:param camera_num: The number of the camera. This should generally be left as 0
as most Raspberry Pi boards only support 1 camera.
"""
super().__init()
super().__init__()
self._setting_save_in_progress = False
self.camera_num = camera_num
self.camera_configs: dict[str, dict] = {}

View file

@ -58,7 +58,7 @@ class SimulatedCamera(BaseCamera):
:param frame_interval: Nominally the time between frames on the MJPEG stream,
however the rate may be slower due to calculation time for focus.
"""
super().__init()
super().__init__()
self.shape = shape
self.glyph_shape = glyph_shape
self.canvas_shape = canvas_shape

View file

@ -1,7 +1,7 @@
"""The core sample scanning functionality for the OpenFlexure Microscope.
SmartScan provides sample scanning functionality including automatic background
detection (via the `BackgroundDetectThing`) and automatic path planning via
detection (via the `CameraThing`) and automatic path planning via
`scan_planners`. It manages the directories of past scans via `scan_directories`.
It also controls external processes for live stitching composite images, and
the creation of the final stitched images.
@ -29,7 +29,6 @@ from openflexure_microscope_server import scan_planners
# Things
from .autofocus import AutofocusThing
from .camera_stage_mapping import CameraStageMapper
from .background_detect import BackgroundDetectThing
from .camera import CameraDependency as CamDep
from .stage import StageDependency as StageDep
@ -37,9 +36,6 @@ CSMDep = lt.deps.direct_thing_client_dependency(
CameraStageMapper, "/camera_stage_mapping/"
)
AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/")
BackgroundDep = lt.deps.direct_thing_client_dependency(
BackgroundDetectThing, "/background_detect/"
)
JPEGBlob = lt.blob.blob_type("image/jpeg")
ZipBlob = lt.blob.blob_type("application/zip")
@ -110,7 +106,6 @@ class SmartScanThing(lt.Thing):
self._cam: Optional[CamDep] = None
self._metadata_getter: Optional[lt.deps.GetThingStates] = None
self._csm: Optional[CSMDep] = None
self._background_detect: Optional[BackgroundDep] = None
self._ongoing_scan: Optional[scan_directories.ScanDirectory] = None
self._starting_position: Optional[Mapping[str, int]] = None
self._capture_thread: Optional[ErrorCapturingThread] = None
@ -128,14 +123,13 @@ class SmartScanThing(lt.Thing):
cam: CamDep,
metadata_getter: lt.deps.GetThingStates,
csm: CSMDep,
background_detect: BackgroundDep,
scan_name: str = "",
):
"""Move the stage to cover an area, taking images that can be tiled together.
The stage will move in a pattern that grows outwards from the starting point,
stopping once it is surrounded by "background" (as detected by the
background_detect Thing) or reaches the "max_range" measured in steps.
camera Thing) or reaches the "max_range" measured in steps.
"""
got_lock = self._scan_lock.acquire(timeout=0.1)
if not got_lock:
@ -149,7 +143,6 @@ class SmartScanThing(lt.Thing):
self._cam = cam
self._metadata_getter = metadata_getter
self._csm = csm
self._background_detect = background_detect
self._capture_thread = None
self._scan_images_taken = 0
@ -183,7 +176,6 @@ class SmartScanThing(lt.Thing):
self._cam = None
self._metadata_getter = None
self._csm = None
self._background_detect = None
self._capture_thread = None
self._ongoing_scan = None
self._scan_images_taken = None
@ -207,7 +199,7 @@ class SmartScanThing(lt.Thing):
)
if self.skip_background:
if not self._background_detect.background_distributions:
if not self._cam.background_detector_status.ready:
raise RuntimeError(
"Background is not set: you need to calibrate background detection."
)
@ -511,7 +503,7 @@ class SmartScanThing(lt.Thing):
capture_image = True
# If skipping background, take an image to check if current field of view is background
if self._scan_data["skip_background"]:
capture_image, bg_message = self._background_detect.image_is_sample()
capture_image, bg_message = self._cam.image_is_sample()
if not capture_image:
route_planner.mark_location_visited(