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."""

View file

@ -1,87 +1,38 @@
<template>
<div>
<!--Show auto calibrate if default plugin is enabled-->
<div v-if="'full_auto_calibrate' in actions" class="uk-margin-small">
<action-button
:can-terminate="false"
:requires-confirmation="true"
:confirmation-message="
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
"
thing="camera"
action="full_auto_calibrate"
:submit-label="'Full Auto-Calibrate'"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
<div v-if="'auto_expose_from_minimum' in actions" class="uk-margin-small">
<action-button
:can-terminate="false"
:requires-confirmation="false"
thing="camera"
action="auto_expose_from_minimum"
:submit-label="'Auto Gain &amp; Shutter Speed'"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
<div v-if="'calibrate_white_balance' in actions" class="uk-margin-small">
<action-button
:can-terminate="false"
:requires-confirmation="false"
thing="camera"
action="calibrate_white_balance"
:submit-label="'Auto White Balance'"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
<div v-if="'calibrate_lens_shading' in actions" class="uk-margin-small">
<action-button
: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.'
"
thing="camera"
action="calibrate_lens_shading"
:submit-label="'Auto Flat Field Correction'"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
<div
v-show="showExtraSettings"
v-if="'flatten_lens_shading_table' in actions"
<div
v-for="(action, index) in secondaryCalibrationActions"
class="uk-child-width-expand"
:key = "'primary_cal' + index"
>
<action-button
:can-terminate="false"
:requires-confirmation="false"
thing="camera"
action="flat_lens_shading"
:submit-label="'Disable Flat Field Correction'"
:can-terminate="action.can_terminate"
:requires-confirmation="action.requires_confirmation"
:thing="action.thing"
:action="action.action"
:submit-label="action.submit_label"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
<div
v-show="showExtraSettings"
v-if="'reset_lens_shading' in actions"
class="uk-child-width-expand"
>
<action-button
:can-terminate="false"
:requires-confirmation="false"
thing="camera"
action="reset_lens_shading"
:submit-label="'Reset Flat Field Correction'"
@response="onRecalibrateResponse"
@error="modalError"
/>
<div v-if="showExtraSettings">
<div
v-for="(action, index) in primaryCalibrationActions"
class="uk-child-width-expand"
:key = "'secondary_cal' + index"
>
<action-button
:can-terminate="action.can_terminate"
:requires-confirmation="action.requires_confirmation"
:thing="action.thing"
:action="action.action"
:submit-label="action.submit_label"
@response="onRecalibrateResponse"
@error="modalError"
/>
</div>
</div>
</div>
</template>
@ -97,6 +48,13 @@ export default {
ActionButton
},
data() {
return {
primaryCalibrationActions: [],
secondaryCalibrationActions: []
};
},
props: {
showExtraSettings: {
type: Boolean,
@ -115,6 +73,11 @@ export default {
}
},
async created() {
this.primaryCalibrationActions = await this.readThingProperty("camera", "primary_calibration_actions");
this.secondaryCalibrationActions = await this.readThingProperty("camera", "secondary_calibration_actions");
},
methods: {
onRecalibrateResponse: function() {
this.modalNotify("Finished recalibration.");