Merge branch 'calibration-wizard-refactor' into 'v3'

Refactor Calibration Wizard

Closes #578

See merge request openflexure/openflexure-microscope-server!415
This commit is contained in:
Julian Stirling 2025-10-27 14:15:05 +00:00
commit 418a6b5364
25 changed files with 883 additions and 371 deletions

View file

@ -198,6 +198,15 @@ class BaseCamera(lt.Thing):
"""Close hardware connection when the Thing context manager is closed."""
raise NotImplementedError("CameraThings must define their own __exit__ method")
@lt.thing_property
def calibration_required(self) -> bool:
"""Whether the camera needs calibrating.
This always returns False in BaseCamera. It should be reimplemented by child
classes if calibration is required.
"""
return False
@lt.thing_action
def start_streaming(
self, main_resolution: tuple[int, int], buffer_count: int

View file

@ -213,6 +213,11 @@ class StreamingPiCamera2(BaseCamera):
finally:
self._setting_save_in_progress = False
@lt.thing_property
def calibration_required(self) -> bool:
"""Whether the camera needs calibrating."""
return not self.lens_shading_is_static
## Persistent controls! These are settings
_analogue_gain: float = 1.0

View file

@ -86,6 +86,11 @@ class SimulatedCamera(BaseCamera):
self.generate_blobs()
self.generate_canvas()
@lt.thing_property
def calibration_required(self) -> bool:
"""Whether the camera needs calibrating."""
return not self.background_detector_status.ready
def validate_inputs(self) -> None:
"""Validate the inputs passed to the simulation, and raises an error if invalid.
@ -367,6 +372,22 @@ class SimulatedCamera(BaseCamera):
LOGGER.warning(f"Simulation camera camera doesn't respect {stream_name=}")
return Image.fromarray(self.generate_frame())
@lt.thing_action
def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None:
"""Perform a full auto-calibration.
For the simulation microscope the process is:
* ``remove_sample``
* ``set_background``
* ``load_sample``
"""
self.remove_sample()
time.sleep(0.2)
self.set_background(portal)
time.sleep(0.2)
self.load_sample()
@lt.thing_action
def remove_sample(self) -> None:
"""Show the simulated background with no sample."""
@ -381,6 +402,15 @@ class SimulatedCamera(BaseCamera):
raise RuntimeError("Sample is already in place.")
self._show_sample = True
@lt.thing_property
def primary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions for both calibration wizard and settings panel."""
return [
action_button_for(
self.full_auto_calibrate, submit_label="Full Auto-Calibrate"
),
]
@lt.thing_property
def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel."""

View file

@ -242,6 +242,11 @@ class CameraStageMapper(lt.Thing):
return None
return self.last_calibration["image_resolution"]
@lt.thing_property
def calibration_required(self) -> bool:
"""Whether the camera stage mapper needs calibrating."""
return self.image_to_stage_displacement_matrix is None
def assert_calibrated(self) -> None:
"""Raise an exception if the image_to_stage_displacement matrix is not set."""
if self.image_to_stage_displacement_matrix is None: