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

@ -75,6 +75,8 @@ def test_get_tuning_algo(picamera_thing):
def test_calibration(picamera_thing, client):
"""Check that full auto calibrate completes and set the expected values."""
# Check the calibration_required property used by the calibration wizard
assert picamera_thing.calibration_required
# Save copy of default tuning file for end of test
original_default = deepcopy(picamera_thing.default_tuning)
# Tuning should start the same as the server is loading with no settings.
@ -88,7 +90,9 @@ def test_calibration(picamera_thing, client):
# Run full auto calibrate
client.full_auto_calibrate()
# After calibration the tuning files should be different
# After calibration it should report that calibration is no longer required
assert not picamera_thing.calibration_required
# The tuning files should be different
assert picamera_thing.default_tuning != picamera_thing.tuning
# The default should be unchanged
assert picamera_thing.default_tuning == original_default

Binary file not shown.

View file

@ -37,15 +37,11 @@ For a step-by-step demonstration, watch the [full video walkthrough on running t
## Running a simulated scan
To run a simulated scan, you first need to set a background image:
To run a simulated scan, you must first complete the calibration wizard.
1. Click on the settings tab on the left
2. Navigate to the **Camera** settings under **Microscope Settings**
3. Click `Remove Sample`. This removes the simulated sample from the camera stream
4. Click on the **Background Detect** tab on the left and click `Set Background`
5. Repeat the steps 1 and 2 to return to the camera settings and click `Add Sample`
You are now ready to start a simulated scan in the Slide Scan tab.
1. Go to the **Slide Scan** tab
2. Click the **Start Smart Scan** Button.
3. This will scan until the whole sample has been imaged. You can cancel the scan at any time, with the **Cancel** button.
---
@ -53,27 +49,31 @@ You are now ready to start a simulated scan in the Slide Scan tab.
---
### Autocalibration
### Auto-calibration
* Your simulated microscope should prompt you to complete the auto-calibration wizard upon your first use of the simulated server.
* If the stage moves in the wrong direction in the **View** or **Navigate** tabs, you can re-run **Auto-calibration** from the UI to correct it.
* To auto-calibrate your microscope, go to: **Settings → Camera to Stage Mapping → Auto-Calibrate Using Camera**.
* You can press the **Escape** key at any point to exit the wizard, but this will leave your simulated microscope in an uncalibrated state.
* If your microscope is not calibrated, the wizard will launch each time you load the UI, prompting you to complete remaining calibrations.
* You can always launch the full calibration wizard by going to: **Settings → Launch Calibration Wizard**.
The calibration steps are:
1. The Camera Calibration will set the background image needed for background detect.
* You can rerun this calibration by going to: **Settings → Camera → Full Auto-Calibrate**.
2. The Camera-Stage Mapping calibration is used to find the relationship between stage and image coordinates.
* If the stage moves in the wrong direction when double clicking on the camera feed in the **View** or **Navigate** tabs, you may need to re-run Camera-Stage Mapping.
* To re-run Camera-Stage Mapping, go to: **Settings → Camera to Stage Mapping → Auto-Calibrate Using Camera**.
---
### Sample Coverage
* The default **sample coverage** of 25% is often too high for simulations. A figure closer to 10% performs better.
* You can adjust this in the **Background Detect tab** under:
**Configure → Sample Coverage**
---
### Background and Samples
### Background Detect
* You can **load a sample** to simulate imaging conditions, using the **Load Sample** options in the **Settings tab**.
* To remove a sample and reset the view, use the **Remove Sample** option in the **Settings tab**.
* You can load and remove simulated samples under: **Settings → Camera → Load/Remove Sample**
* You can load and remove simulated samples under: **Settings → Camera → Load/Remove Sample**.
* The Full Auto-Calibrate option in camera settings can be used to **Remove the sample → Run background detection → Re-load the sample**.
* You can adjust the sample coverage needed for the image not to be detected as background in the **Background Detect tab** under: **Configure → Sample Coverage**. The default value is 25%.
---

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:

View file

@ -68,3 +68,14 @@ def test_handle_broken_frame():
for _i in range(15):
array = camera.grab_as_array(portal)
assert isinstance(array, np.ndarray)
def test_simulation_cam_calibration():
"""Test that the simulated camera can be calibrated and reports calibration correctly."""
camera = SimulatedCamera()
with camera_server(camera):
portal = lt.get_blocking_portal(camera)
assert camera.calibration_required
camera.full_auto_calibrate(portal)
assert not camera.calibration_required
assert camera.background_detector_status.ready

View file

@ -5,10 +5,10 @@
uk-grid
>
<!-- Initialisation modals -->
<calibrationModal
ref="calibrationModal"
<calibrationWizard
ref="calibrationWizard"
@onClose="enterApp()"
></calibrationModal>
></calibrationWizard>
<!-- Vertical tab bar -->
<div id="switcher-left-container">
<div
@ -112,7 +112,7 @@ import loggingContent from "./tabContentComponents/loggingContent.vue";
import powerContent from "./tabContentComponents/powerContent.vue";
// Import modal components for device initialisation
import calibrationModal from "./modalComponents/calibrationModal.vue";
import calibrationWizard from "./modalComponents/calibrationWizard.vue";
import TabIcon from "./genericComponents/tabIcon.vue";
import ScanListContent from "./tabContentComponents/scanListContent.vue";
@ -127,7 +127,7 @@ export default {
slideScanContent,
viewContent,
settingsContent,
calibrationModal,
calibrationWizard,
aboutContent,
loggingContent,
TabIcon,
@ -246,7 +246,7 @@ export default {
this.currentTab = newId;
},
startModals: function() {
this.$refs.calibrationModal.show();
this.$refs.calibrationWizard.show_if_needed();
},
enterApp: function() {
// Stuff to do once connected and all init modals are finished

View file

@ -1,336 +0,0 @@
<template>
<div id="modal-example" ref="calibrationModalEl" uk-modal="bg-close: false;">
<div v-if="ready" class="uk-modal-dialog uk-modal-body">
<h2 class="uk-modal-title">Microscope Calibration</h2>
<div v-show="stepValue == 0">
<p>
<b
>Some important microscope calibration data is currently missing.</b
>
</p>
<p>
Your microscope will still function, however some functionality will
be limited, and image quality will likely suffer.
</p>
<p>
<b>Click Next to begin microscope calibration.</b>
</p>
</div>
<div v-show="stepValue == 1">
<h3>Lens-shading</h3>
<div v-if="isLSTCalibrated">
<p>
<b
>Your lens-shading table has already been calibrated. Click Next
to move on.</b
>
</p>
</div>
<div v-else>
<p>
<b
>Follow the important steps below before starting lens-shading
calibration!</b
>
</p>
<ul class="uk-list uk-list-bullet">
<li>Remove any samples from your microscope</li>
<li>Ensure your illumination is on and properly fixed in place</li>
</ul>
<miniStreamDisplay
v-if="stepValue == 1"
class="mini-preview"
></miniStreamDisplay>
<p>Once you're ready, click auto-calibrate.</p>
<cameraCalibrationSettings
:show-extra-settings="false"
:camera-uri="cameraUri"
></cameraCalibrationSettings>
</div>
</div>
<div v-show="stepValue == 2">
<h3>Adjust z height</h3>
<p>
Insert a sample and adjust the z position of
the stage using the buttons below, until the
sample is in focus.
</p>
<p>
You may also adjust the z position with <b>page up</b> and <b>page down</b>.
</p>
<miniStreamDisplay
v-if="stepValue == 2"
class="mini-preview"
></miniStreamDisplay>
<div class="action-button-container">
<action-button
class="moveZ"
thing="stage"
action="move_relative"
:button-primary="false"
:submit-data="{ x: 0, y: 0, z: -100}"
:submit-label="' - '"
:hideOnRun="false"
:can-terminate="false"
/>
<action-button
class="moveZ"
thing="stage"
action="move_relative"
:button-primary="false"
:submit-data="{ x: 0, y: 0, z: 100}"
:submit-label="'+'"
:can-terminate="false"
:hideOnRun="false"
/>
</div>
</div>
<div v-show="stepValue == 3">
<h3>Camera-stage mapping</h3>
<div v-if="isCSMCalibrated">
<p>
<b
>Your camera-stage mapping has already been calibrated. Click Next
to move on.</b
>
</p>
</div>
<div v-else-if="!canCSMCalibrated">
<p>
<b
>No stage connected. Please skip this step, or connect a valid
stage, then reboot your microscope.</b
>
</p>
</div>
<div v-else>
<p>
<b
>Follow the important steps below before starting camera-stage
mapping calibration!</b
>
</p>
<ul class="uk-list uk-list-bullet">
<li>Insert a clearly visible sample to the microscope</li>
<li>
Ensure the sample is reasonably well centered on the microscope
camera
</li>
</ul>
<miniStreamDisplay
v-if="stepValue == 3"
class="mini-preview"
></miniStreamDisplay>
<p>Once you're ready, click auto-calibrate.</p>
<CSMCalibrationSettings
:show-extra-settings="false"
></CSMCalibrationSettings>
</div>
</div>
<div v-show="stepValue == 4">
<p>
<b>Calibration complete</b>
</p>
<p>
Click Finish to return to your microscope, or Restart to re-run the
calibration routine
</p>
</div>
<p class="uk-text-right">
<button
v-show="stepValue == 0"
class="uk-button uk-button-default uk-modal-close"
type="button"
>
Cancel
</button>
<button
v-show="stepValue == 4"
class="uk-button uk-button-default"
type="button"
@click="restart()"
>
Restart
</button>
<button
v-show="stepValue < 4"
class="uk-button uk-button-primary uk-margin-left"
type="button"
@click="increment()"
>
Next
</button>
<button
v-show="stepValue == 4"
class="uk-button uk-button-primary uk-margin-left"
type="button"
@click="hide()"
>
Finish
</button>
</p>
</div>
</div>
</template>
<script>
import cameraCalibrationSettings from "../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
import CSMCalibrationSettings from "../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
import miniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
import ActionButton from "../labThingsComponents/actionButton.vue";
export default {
name: "CalibrationModal",
components: {
cameraCalibrationSettings,
CSMCalibrationSettings,
miniStreamDisplay,
ActionButton
},
data: function() {
return {
ready: false,
stepValue: 0,
isCSMCalibrated: undefined,
isLSTCalibrated: undefined
};
},
computed: {
canCSMCalibrated: function() {
// Assert CSM extension is enabled
return this.thingAvailable("camera_stage_mapping");
},
canLSTCalibrated: function() {
// Assert LST extension is enabled
return (
"calibrate_lens_shading" in this.thingDescription("camera").actions
);
},
isUseful: function() {
var CSMUseful = this.canCSMCalibrated && !this.isCSMCalibrated;
var LSTUseful = this.canLSTCalibrated && !this.isLSTCalibrated;
return CSMUseful || LSTUseful;
},
cameraUri: function() {
return `${this.$store.getters.baseUri}/camera/`;
}
},
mounted() {
this.$refs["calibrationModalEl"].addEventListener("hidden", this.onHide);
},
methods: {
show: async function() {
// Check if the camera and stage are calibrated, if they can be
if (this.canCSMCalibrated) {
let csm = await this.readThingProperty(
"camera_stage_mapping",
"image_to_stage_displacement_matrix",
true
);
this.isCSMCalibrated = Boolean(csm);
}
if (this.canLSTCalibrated) {
this.isLSTCalibrated = await this.readThingProperty(
"camera",
"lens_shading_is_static"
);
}
// Check if this calibration wizard can actually do anything useful
if (this.isUseful) {
this.ready = true;
this.stepValue = 0;
// Show the modal
var el = this.$refs["calibrationModalEl"];
this.showModalElement(el); // Calls the mixin
} else {
// If not useful, we just return the onClose event immediately
this.onHide();
}
},
// Forces modal to show on button press
force_show: function() {
this.ready = true;
this.stepValue = 1;
this.force = true
// Show the modal
var el = this.$refs["calibrationModalEl"];
this.showModalElement(el); // Calls the mixin
},
restart: function() {
if (this.force == true){
this.stepValue = 1
} else {
this.stepValue = 0
}
},
hide: function() {
// Show the modal
var el = this.$refs["calibrationModalEl"];
this.hideModalElement(el); // Calls the mixin
this.ready = false;
},
onHide: function() {
this.$emit("onClose");
},
decrement: function() {
if (this.stepValue > 0) {
this.stepValue = this.stepValue - 1;
}
},
increment: function() {
// Upper bound on section number
if (this.stepValue < 4) {
this.stepValue = this.stepValue + 1;
return true;
}
}
}
};
</script>
<style scoped>
.mini-preview {
width: 75%;
margin-left: auto;
margin-right: auto;
}
.action-button-container {
display: flex;
flex-direction: row; /* Stack vertically */
justify-content: center; /* Left align */
gap: 8px; /* Small space between buttons */
margin-top: 4px; /* Gap from image */
}
>>> .moveZ .uk-button.uk-width-1-1 {
line-height: 50px;
font-size: 50px !important;
height: 60px;
padding-bottom: 46px;
margin: 0; /* Remove default margin */
width: 120px;
min-width: 80px;
}
</style>

View file

@ -0,0 +1,198 @@
<template>
<div id="modal-example" ref="calibrationModalEl" uk-modal="bg-close: false;">
<div class="uk-modal-dialog uk-modal-body">
<h2 class="uk-modal-title">Microscope Calibration</h2>
<component
v-if="currentTask"
:is="currentTask.component"
:key="taskIndex"
v-bind="currentTask.props"
:first="isFirstTask"
:final="isFinalTask"
:startOnLast="movingBackward"
@next="nextTask"
@back="previousTask"
/>
</div>
</div>
</template>
<script>
import singleStepTask from "./calibrationWizardComponents/singleStepTask.vue";
import welcomeStep from "./calibrationWizardComponents/welcomeStep.vue";
import cameraCalibrationTask from "./calibrationWizardComponents/cameraCalibrationTask.vue";
import cameraStageMappingTask from "./calibrationWizardComponents/cameraStageMappingTask.vue";
import finalStep from "./calibrationWizardComponents/finalStep.vue";
export default {
name: "calibrationWizard",
components: {},
data: function() {
return {
isNeeded: undefined,
availableCalibrationTasks: {},
tasks: [],
taskIndex: 0,
movingBackward: false
};
},
computed: {
currentTask() {
return this.tasks[this.taskIndex] || null;
},
isFirstTask() {
return this.taskIndex === 0;
},
isFinalTask() {
return this.taskIndex === this.tasks.length - 1;
}
},
mounted() {
this.$refs["calibrationModalEl"].addEventListener("hidden", this.onHide);
// Check which Things are available on mount.
const allCalibrationTasks = {
camera: cameraCalibrationTask,
camera_stage_mapping: cameraStageMappingTask
};
this.availableCalibrationTasks = Object.fromEntries(
Object.entries(allCalibrationTasks).filter(([thing]) =>
this.thingAvailable(thing)
)
);
},
methods: {
/**
* Check all calibratable Things to see which require calibration.
*
* Iterates over `this.availableCalibrationTasks` (set during mounted()) and reads the
* `calibration_required` property.
*
* Returns a list of thing names that report calibration is required.
*/
async check_things_needing_calibration() {
const needsCalibration = [];
const calibrateableThings = Object.keys(this.availableCalibrationTasks);
for (const name of calibrateableThings) {
try {
const thingNeedsCal = await this.readThingProperty(
name,
"calibration_required"
);
if (thingNeedsCal) {
needsCalibration.push(name);
}
} catch (e) {
console.error(`${name}: missing calibration_required property`, e);
}
}
return needsCalibration;
},
resetData: function() {
this.movingBackward = false;
this.taskIndex = 0;
},
/**
* Create the calibration wizard task list dynamically.
*/
create_task_list: function(thingsToCal, includeWelcome = true) {
const tasks = [];
// Optionally include the welcome screen
if (includeWelcome) {
tasks.push({
component: singleStepTask,
props: { stepComponent: welcomeStep }
});
}
// Add calibration task for each thing
for (const thing of thingsToCal) {
const taskComponent = this.availableCalibrationTasks[thing];
tasks.push({ component: taskComponent });
}
// Always include the final step
tasks.push({
component: singleStepTask,
props: { stepComponent: finalStep }
});
this.tasks = tasks;
},
show_if_needed: async function() {
// Check if the calibration modal is needed, and only show it if it is.
let thingsToCal = await this.check_things_needing_calibration();
const needed = thingsToCal.length > 0;
// Check if this calibration wizard can actually do anything useful
if (needed) {
this.resetData();
this.create_task_list(thingsToCal);
this.show();
} else {
// If not needed, we just return the onClose event immediately
this.onHide();
}
},
// Forces modal to show on button press
force_show: function() {
const allThings = Object.keys(this.availableCalibrationTasks);
this.resetData();
this.create_task_list(allThings, false);
this.show();
},
show: function() {
// Show the modal element
var el = this.$refs["calibrationModalEl"];
this.showModalElement(el); // Calls the mixin
},
hide: function() {
// Show the modal
var el = this.$refs["calibrationModalEl"];
this.hideModalElement(el); // Calls the mixin
},
onHide: function() {
this.$emit("onClose");
},
/*
* Move to the previous task.
*/
previousTask: function() {
this.movingBackward = true;
if (this.taskIndex > 0) {
this.taskIndex = this.taskIndex - 1;
}
},
/*
* Move to the next task or close the modal if this is the final task.
*/
nextTask: function() {
this.movingBackward = false;
if (this.taskIndex < this.tasks.length - 1) {
this.taskIndex = this.taskIndex + 1;
return true;
} else {
this.hide();
}
}
}
};
</script>

View file

@ -0,0 +1,77 @@
# The Calibration Wizard Components
## Tasks vs Steps
The components for the wizard uses the terms "Task" and "Step" to divide up the wizard's behaviour:
* **Task**: A task is what we want to achieve. Such as "Calibrate the camera" or "Run camera stage mapping", there are multiple things to do for these complex tasks. A task can also be as simple as "welcome the user to their microscope."
* **Step**: A single page/pane of information shown to the user. The components for these can be as simple as a single `<div>` with no javascript logic.
This is done to allow complex tasks like camera stage mapping to have multiple steps:
1. An explanation and telling the user what sample to get
2. An interface for focusing
3. Actually running camera stage mapping
None of these individual steps makes sense in the wizard on their own, hence the need for the "Task" grouping. By grouping the steps we can:
- Easily include/exclude tasks based on state
- Have a consistent subtitle during a task
- Skip entire task rather than make the user click next through each pane of the task (not yet implemented)
Some simple tasks such as the welcome screen will have only one step.
## Components
### The main wizard component
The calibration wizard is a modal. The top level component in `calibrationWizard.vue` handles:
* Creating a modal
* Checking which Things that have calibration tasks are on the server
* Checking which of these Things need calibrating
* Starting modal on startup if not all Things are calibrated (with a welcome screen and just those tasks)
* Starting the modal when launched from settings, with all tasks but no welcome screen.
* Before starting the modal, a list of task components is dynamically generated.
### The main task component
The main task component `calibrationWizardTask.vue` is used to create multi-step tasks. Rather than using it directly it should be wrapped by a new component that:
* Forwards all the props
* Forwards the events back to the wizard
* Creates a list of steps.
Examples of this pattern are in `cameraCalibrationTask.vue` and `cameraStageMappingTask.vue`.
### The single step task component
For simple tasks with only one step there is no need to make both a specific task component. Instead `singleStepTask.vue` can be used, the step component can be supplied as a prop.
This is used in the main wizard to create the welcome screen and the final page.
## stepTemplateWithStream
Step components are simple enough that generally there is no need for a template. However, a number of steps have mini-stream views. To keep these consistent `stepTemplateWithStream.vue` can be used.
The template puts component content above the stream view, by default. But extra information can be added to the area below the stream by using the `<template #below-stream>` tag.
For example:
```html
<template>
<stepTemplateWithStream>
<p>
<b>This is above the stream.</b>
</p>
<template #below-stream>
<div>
This text is below the stream
</div>
</template>
</stepTemplateWithStream>
</template>
```
Note: Vue seems to get angry if the HTML within the `<template #below-stream>` tag is not all contained in a single `<div>`

View file

@ -0,0 +1,117 @@
<!-- The base component for a wizard task.
A task should be used for higher level groupings of individual steps in the wizard,
such as calibrating a Thing. Tasks can be divided into multiple steps.
## How to use this component:
*To make a task with lots of steps:*
Create a task component for a `Thing`, that wraps just this component and passes in
a list of steps. See `cameraCalibrationTask.vue` and `cameraStageMappingTask.vue`
as examples.
*To make a simple task with just one step:*
See `singleStepTask.vue`. This is what is used to display the welcome task.
## How not to use this component:
Do not import this directly into CalibrationWizard as the list of tasks will then
also need props to be passed with the list of step components and step props, and it
gets very confusing.
-->
<template>
<div>
<h3 v-if="title">{{ title }}</h3>
<component
v-if="currentStep"
:is="currentStep.component"
:key="stepIndex"
v-bind="currentStep.props"
/>
<p class="uk-text-right">
<button
v-if="showBackButton"
class="uk-button uk-button-default"
type="button"
@click="previousStep"
>
Back
</button>
<button
class="uk-button uk-button-primary uk-margin-left"
type="button"
@click="nextStep"
>
{{ nextButtonText }}
</button>
</p>
</div>
</template>
<script>
export default {
name: "calibrationWizardTask",
props: {
title: {
type: String,
default: null
},
first: Boolean,
final: Boolean,
startOnLast: {
type: Boolean,
default: false
},
steps: {
type: Array,
required: true
}
},
data() {
return {
stepIndex: this.startOnLast ? this.steps.length - 1 : 0
};
},
computed: {
currentStep() {
return this.steps[this.stepIndex] || null;
},
showBackButton() {
return !this.first || this.stepIndex > 0;
},
nextButtonText() {
return this.final && this.stepIndex === this.steps.length - 1
? "Finish"
: "Next";
}
},
methods: {
/*
* Move to the previous step in this task, or the previous task if first step.
*/
previousStep: function() {
if (this.stepIndex > 0) {
this.stepIndex = this.stepIndex - 1;
} else {
this.$emit("back");
}
},
/*
* Move to the next step in this task, or the next task if final step.
*/
nextStep: function() {
if (this.stepIndex < this.steps.length - 1) {
this.stepIndex = this.stepIndex + 1;
return true;
} else {
this.$emit("next");
}
}
}
};
</script>

View file

@ -0,0 +1,19 @@
<template>
<div>
<p>
<b>
Before starting camera calibration:
</b>
</p>
<ul class="uk-list uk-list-bullet">
<li>Remove any samples from your microscope</li>
<li>Ensure your illumination is on, well focused, and centred.</li>
</ul>
</div>
</template>
<script>
export default {
name: "camCalibrationExplanation"
};
</script>

View file

@ -0,0 +1,42 @@
<template>
<stepTemplateWithStream>
<p>
<b>Once your field of view is empty and well illuminated, click Full Auto-Calibrate.</b>
</p>
<template #below-stream>
<div class="action-button-container">
<cameraCalibrationSettings
:show-extra-settings="false"
:camera-uri="cameraUri"
/>
</div>
</template>
</stepTemplateWithStream>
</template>
<script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
export default {
name: "cameraMainCalibrationStep",
components: {
stepTemplateWithStream,
cameraCalibrationSettings
},
computed: {
cameraUri: function() {
return `${this.$store.getters.baseUri}/camera/`;
}
}
};
</script>
<style scoped>
.action-button-container {
padding: 4px;
}
</style>

View file

@ -0,0 +1,40 @@
<template>
<calibrationWizardTask
title="Camera Calibration"
:first="first"
:final="final"
:startOnLast="startOnLast"
:steps="steps"
@next="$emit('next')"
@back="$emit('back')"
/>
</template>
<script>
import calibrationWizardTask from "./calibrationWizardTask.vue";
import camCalibrationExplanation from "./cameraCalibrationSteps/camCalibrationExplanation.vue";
import cameraMainCalibrationStep from "./cameraCalibrationSteps/cameraMainCalibrationStep.vue";
export default {
name: "cameraCalibrationTask",
components: { calibrationWizardTask },
props: {
// Standard calibrationWizardTask props below:
first: Boolean,
final: Boolean,
startOnLast: {
type: Boolean,
default: false
}
},
data: function() {
return {
steps: [
{ component: camCalibrationExplanation },
{ component: cameraMainCalibrationStep }
]
};
}
};
</script>

View file

@ -0,0 +1,42 @@
<template>
<calibrationWizardTask
title="Camera-Stage Mapping"
:first="first"
:final="final"
:startOnLast="startOnLast"
:steps="steps"
@next="$emit('next')"
@back="$emit('back')"
/>
</template>
<script>
import calibrationWizardTask from "./calibrationWizardTask.vue";
import csmExplanation from "./csmSteps/csmExplanation.vue";
import focusStep from "./csmSteps/focusStep.vue";
import runCsmStep from "./csmSteps/runCsmStep.vue";
export default {
name: "cameraCalibrationTask",
components: { calibrationWizardTask },
props: {
// Standard calibrationWizardTask props below:
first: Boolean,
final: Boolean,
startOnLast: {
type: Boolean,
default: false
}
},
data: function() {
return {
steps: [
{ component: csmExplanation },
{ component: focusStep },
{ component: runCsmStep }
]
};
}
};
</script>

View file

@ -0,0 +1,22 @@
<template>
<div>
<p>
Camera-stage mapping will calibrate the stage movement using the camera.
</p>
<p>
<b>
Before starting camera-stage mapping:
</b>
</p>
<ul class="uk-list uk-list-bullet">
<li>Add a sample with dense features to the microscope.</li>
<li>Ensure the sample is reasonably well centered on the microscope.</li>
</ul>
</div>
</template>
<script>
export default {
name: "csmExplanation"
};
</script>

View file

@ -0,0 +1,66 @@
<template>
<stepTemplateWithStream>
<p>
Use the buttons below to bring the sample into focus.
</p>
<p>
You may also adjust the z position with <b>page up</b> and
<b>page down</b>.
</p>
<template #below-stream>
<div class="action-button-container">
<action-button
class="moveZ"
thing="stage"
action="move_relative"
:button-primary="false"
:submit-data="{ x: 0, y: 0, z: -100 }"
:submit-label="' - '"
:hideOnRun="false"
:can-terminate="false"
/>
<action-button
class="moveZ"
thing="stage"
action="move_relative"
:button-primary="false"
:submit-data="{ x: 0, y: 0, z: 100 }"
:submit-label="'+'"
:can-terminate="false"
:hideOnRun="false"
/>
</div>
</template>
</stepTemplateWithStream>
</template>
<script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import ActionButton from "../../../labThingsComponents/actionButton.vue";
export default {
name: "cameraMainCalibrationStep",
components: {
stepTemplateWithStream,
ActionButton
}
};
</script>
<style scoped>
.action-button-container {
display: flex;
flex-direction: row; /* Stack vertically */
justify-content: center; /* Left align */
gap: 8px; /* Small space between buttons */
margin-top: 4px; /* Gap from image */
}
.moveZ >>> .uk-button.uk-width-1-1 {
line-height: 50px;
font-size: 50px !important;
height: 60px;
padding-bottom: 46px;
margin: 0; /* Remove default margin */
width: 120px;
min-width: 80px;
}
</style>

View file

@ -0,0 +1,33 @@
<template>
<stepTemplateWithStream>
<p>
<b>If the sample is in focus, click Auto-Calibrate Using Camera.</b>
</p>
<p>If it is not in focus, click back and re-focus.</p>
<template #below-stream>
<div class="action-button-container">
<CSMCalibrationSettings :show-extra-settings="false" />
</div>
</template>
</stepTemplateWithStream>
</template>
<script>
import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import CSMCalibrationSettings from "../../../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
export default {
name: "cameraMainCalibrationStep",
components: {
stepTemplateWithStream,
CSMCalibrationSettings
}
};
</script>
<style scoped>
.action-button-container {
padding: 4px;
}
</style>

View file

@ -0,0 +1,19 @@
<template>
<div>
<p>
<b>Calibration complete</b>
</p>
<p>
You'll need to repeat these steps from the Settings tab if you swap your objective.
</p>
<p>
Click Finish to return to your microscope.
</p>
</div>
</template>
<script>
export default {
name: "finalStep"
};
</script>

View file

@ -0,0 +1,61 @@
<!-- A component to make a task with just one step
The calibration wizard makes a distinction between tasks (what you are trying to
achieve, such as calibrating the camera) and steps (individual pages/panes shown to the
user). The down side of this is there is some complexity in handling tasks that is
unhelpful when trying to add a single pane.
This component takes one step Component (which can be as simple as a <div> with some
text) as the :stepComponent prop, and optionally any props to be sent to the component
can be passed as an object to :stepProps. All other prop are automatically
supplied by the wizard.
-->
<template>
<calibrationWizardTask
:title="title"
:first="first"
:final="final"
:startOnLast="startOnLast"
:steps="steps"
@next="$emit('next')"
@back="$emit('back')"
/>
</template>
<script>
import calibrationWizardTask from "./calibrationWizardTask.vue";
export default {
name: "singleStepTask",
components: { calibrationWizardTask },
props: {
// This must be sent
stepComponent: {
type: Object,
required: true
},
// This is optional.
stepProps: {
type: Object,
default: () => ({})
},
// Standard calibrationWizardTask props below:
title: {
type: String,
default: null
},
first: Boolean,
final: Boolean,
startOnLast: {
type: Boolean,
default: false
}
},
data: function() {
return {
steps: [{ component: this.stepComponent, props: this.stepProps }]
};
}
};
</script>

View file

@ -0,0 +1,27 @@
<template>
<div>
<slot></slot>
<miniStreamDisplay class="mini-preview" />
<slot name="below-stream"></slot>
</div>
</template>
<script>
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
export default {
name: "stepTemplateWithStream",
components: {
miniStreamDisplay
}
};
</script>
<style scoped>
.mini-preview {
width: 75%;
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -0,0 +1,22 @@
<template>
<div>
<p>
<b>
Some important microscope calibration data is currently missing.
</b>
</p>
<p>
Your microscope will still function, however some functionality will be
limited, and image quality will likely suffer.
</p>
<p>
<b>Click Next to begin microscope calibration.</b>
</p>
</div>
</template>
<script>
export default {
name: "welcomeStep"
};
</script>

View file

@ -1,10 +1,9 @@
<template>
<!-- Grid managing tab content -->
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<calibrationModal
ref="calibrationModal"
@onClose="enterApp()"
></calibrationModal>
<calibrationWizard
ref="calibrationWizard"
></calibrationWizard>
<div class="settings-nav">
<ul class="uk-nav uk-nav-default">
<li class="uk-nav-header">Application Settings</li>
@ -116,7 +115,7 @@
<script>
import streamSettings from "./settingsComponents/streamSettings.vue";
import calibrationModal from "../modalComponents/calibrationModal.vue";
import calibrationWizard from "../modalComponents/calibrationWizard.vue";
import cameraSettings from "./settingsComponents/cameraSettings.vue";
import appSettings from "./settingsComponents/appSettings.vue";
import CSMSettings from "./settingsComponents/CSMSettings.vue";
@ -137,7 +136,7 @@ export default {
appSettings,
tabIcon,
tabContent,
calibrationModal
calibrationWizard
},
data: function() {
@ -154,7 +153,7 @@ export default {
}
},
startModals: function() {
this.$refs.calibrationModal.force_show();
this.$refs.calibrationWizard.force_show();
}
}
};