Camera calibration buttons specified by server. Simulation has option to remove sample

This commit is contained in:
Julian Stirling 2025-07-27 15:07:35 +01:00
parent 1e89ef0ff7
commit 0794d4777f
5 changed files with 175 additions and 79 deletions

View file

@ -20,6 +20,7 @@ import piexif
import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray
from openflexure_microscope_server.ui import ActionButton
from openflexure_microscope_server.background_detect import (
ColourChannelDetectLUV,
BackgroundDetectAlgorithm,
@ -476,6 +477,16 @@ class BaseCamera(lt.Thing):
time.sleep(self.settling_time)
self.discard_frames()
@lt.thing_property
def primary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions for both calibration wizard and settings panel."""
return []
@lt.thing_property
def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel."""
return []
# Note that the default detector name is set at init. This is over written if
# setting is loaded from disk.
@lt.thing_setting

View file

@ -35,6 +35,7 @@ from picamera2.outputs import Output
import labthings_fastapi as lt
from labthings_fastapi.exceptions import NotConnectedToServerError
from openflexure_microscope_server.ui import ActionButton, action_button_for
from . import picamera_recalibrate_utils as recalibrate_utils
from . import BaseCamera, JPEGBlob, ArrayModel
@ -777,6 +778,59 @@ class StreamingPiCamera2(BaseCamera):
)
self._initialise_picamera()
@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",
can_terminate=False,
requires_confirmation=True,
confirmation_message=(
"Start recalibration? This may take a while, and the microscope "
"will be locked during this time."
),
),
action_button_for(
self.auto_expose_from_minimum,
submit_label="Auto Gain & Shutter Speed",
can_terminate=False,
),
action_button_for(
self.calibrate_white_balance,
submit_label="Auto White Balance",
can_terminate=False,
),
action_button_for(
self.calibrate_lens_shading,
submit_label="Auto Flat Field Correction",
can_terminate=False,
requires_confirmation=True,
confirmation_message=(
"Is the microscope looking at an evenly illuminated, empty field "
"of view? If not, the current image will show through in any "
"images captured afterwards."
),
),
]
@lt.thing_property
def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel."""
return [
action_button_for(
self.flat_lens_shading,
submit_label="Disable Flat Field Correction",
can_terminate=False,
),
action_button_for(
self.reset_lens_shading,
submit_label="Reset Flat Field Correction",
can_terminate=False,
),
]
@lt.thing_property
def lens_shading_tables(self) -> Optional[LensShading]:
"""The current lens shading (i.e. flat-field correction).

View file

@ -21,6 +21,8 @@ from scipy.ndimage import gaussian_filter
import labthings_fastapi as lt
from openflexure_microscope_server.ui import ActionButton, action_button_for
from . import BaseCamera, JPEGBlob, ArrayModel
from ..stage import BaseStage
@ -40,6 +42,7 @@ class SimulatedCamera(BaseCamera):
_stage: Optional[BaseStage] = None
_server: Optional[lt.ThingServer] = None
_show_sample: bool = True
def __init__(
self,
@ -124,10 +127,11 @@ class SimulatedCamera(BaseCamera):
Canvas is int16 so that random noise can be added to simulation image before
changing to unit8 to stop wrapping.
"""
self.canvas = np.ones(self.canvas_shape, dtype=np.int16)
self.canvas[:, :, 0] *= BG_COLOR[0]
self.canvas[:, :, 1] *= BG_COLOR[1]
self.canvas[:, :, 2] *= BG_COLOR[2]
self.blank_canvas = np.ones(self.canvas_shape, dtype=np.int16)
self.blank_canvas[:, :, 0] *= BG_COLOR[0]
self.blank_canvas[:, :, 1] *= BG_COLOR[1]
self.blank_canvas[:, :, 2] *= BG_COLOR[2]
self.canvas = self.blank_canvas.copy()
w, h, _ = self.glyph_shape
for x, y, sprite_size_index in self.blobs:
self.canvas[
@ -149,7 +153,8 @@ class SimulatedCamera(BaseCamera):
x_slice = slice(top_left[0], top_left[0] + self.shape[0])
y_slice = slice(top_left[1], top_left[1] + self.shape[1])
z_slice = slice(None)
focused_image = self.canvas[(x_slice, y_slice, z_slice)]
canvas = self.canvas if self._show_sample else self.blank_canvas
focused_image = canvas[(x_slice, y_slice, z_slice)]
image = gaussian_filter(
focused_image,
sigma=np.abs(pos[2]) / 5,
@ -279,3 +284,25 @@ class SimulatedCamera(BaseCamera):
output = io.BytesIO()
piexif.insert(piexif.dump(exif_dict), jpeg, output)
return JPEGBlob.from_bytes(output.getvalue())
@lt.thing_action
def remove_sample(self):
"""Show the simulated background with no sample."""
if not self._show_sample:
raise RuntimeError("Sample is already removed.")
self._show_sample = False
@lt.thing_action
def load_sample(self):
"""Show the simulated sample."""
if self._show_sample:
raise RuntimeError("Sample is already in place.")
self._show_sample = True
@lt.thing_property
def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel."""
return [
action_button_for(self.load_sample, submit_label="Load Sample"),
action_button_for(self.remove_sample, submit_label="Remove Sample"),
]

View file

@ -0,0 +1,41 @@
"""Functionality for communicating the required user interface for a thing."""
from pydantic import BaseModel
def action_button_for(action, **kwargs):
"""Create an action button for the specified Thing Action.
:param action: The thing action to create a button for.
:param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
"""
thing = action.args[0]
thing_path = thing.path.strip("/")
action_name = action.func.__name__
return ActionButton(thing=thing_path, action=action_name, **kwargs)
class ActionButton(BaseModel):
"""The data required for creating an actionButton in Vue.
Currently this cannot be used to submitData, or to submitOnEvent.
"""
thing: str
"""The Thing "path" for the Thing instance."""
action: str
"""The name of the action to be triggered."""
poll_interval: int = 1
"""The interval for polling in seconds."""
submit_label: str = "Submit"
"""The label for the action button."""
can_terminate: bool = True
"""Specify whether the action can be terminated."""
requires_confirmation: bool = False
"""Specify whether a confirmation modal is needed."""
confirmation_message: str = "Start task?"
"""The message for the confirmation modal."""
button_primary: bool = True
"""Specify whether the button is styled as a primary button."""
modal_progress: bool = False
"""Specify whether to show a progress modal."""