diff --git a/hardware-specific-tests/picamera2/test_calibration.py b/hardware-specific-tests/picamera2/test_calibration.py
index 6c803aed..909b533e 100644
--- a/hardware-specific-tests/picamera2/test_calibration.py
+++ b/hardware-specific-tests/picamera2/test_calibration.py
@@ -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
diff --git a/picamera_coverage.zip b/picamera_coverage.zip
index e3c3946d..9592a404 100644
Binary files a/picamera_coverage.zip and b/picamera_coverage.zip differ
diff --git a/simulation_guide.md b/simulation_guide.md
index 22ac0004..258f1e3d 100644
--- a/simulation_guide.md
+++ b/simulation_guide.md
@@ -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%.
---
diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py
index bca3c63f..fc11ca1f 100644
--- a/src/openflexure_microscope_server/things/camera/__init__.py
+++ b/src/openflexure_microscope_server/things/camera/__init__.py
@@ -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
diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py
index 2afc1d7b..68f5c281 100644
--- a/src/openflexure_microscope_server/things/camera/picamera.py
+++ b/src/openflexure_microscope_server/things/camera/picamera.py
@@ -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
diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py
index 7c32aa08..31a0a32f 100644
--- a/src/openflexure_microscope_server/things/camera/simulation.py
+++ b/src/openflexure_microscope_server/things/camera/simulation.py
@@ -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."""
diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py
index 57f3e742..75ce1968 100644
--- a/src/openflexure_microscope_server/things/camera_stage_mapping.py
+++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py
@@ -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:
diff --git a/tests/test_camera.py b/tests/test_camera.py
index 39a35d89..66091593 100644
--- a/tests/test_camera.py
+++ b/tests/test_camera.py
@@ -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
diff --git a/webapp/src/components/appContent.vue b/webapp/src/components/appContent.vue
index a466ab61..31d7b50d 100644
--- a/webapp/src/components/appContent.vue
+++ b/webapp/src/components/appContent.vue
@@ -5,10 +5,10 @@
uk-grid
>
-
- Some important microscope calibration data is currently missing. -
-- Your microscope will still function, however some functionality will - be limited, and image quality will likely suffer. -
-- Click Next to begin microscope calibration. -
-- Your lens-shading table has already been calibrated. Click Next - to move on. -
-- Follow the important steps below before starting lens-shading - calibration! -
-Once you're ready, click auto-calibrate.
- -- Insert a sample and adjust the z position of - the stage using the buttons below, until the - sample is in focus. -
-- You may also adjust the z position with page up and page down. -
- -- Your camera-stage mapping has already been calibrated. Click Next - to move on. -
-- No stage connected. Please skip this step, or connect a valid - stage, then reboot your microscope. -
-- Follow the important steps below before starting camera-stage - mapping calibration! -
-Once you're ready, click auto-calibrate.
- -- Calibration complete -
-- Click Finish to return to your microscope, or Restart to re-run the - calibration routine -
-- - - - -
-+ This is above the stream. +
+ ++ + +
++ + Before starting camera calibration: + +
++ Once your field of view is empty and well illuminated, click Full Auto-Calibrate. +
+ + + + ++ Camera-stage mapping will calibrate the stage movement using the camera. +
++ + Before starting camera-stage mapping: + +
++ Use the buttons below to bring the sample into focus. +
++ You may also adjust the z position with page up and + page down. +
+ + + ++ If the sample is in focus, click Auto-Calibrate Using Camera. +
+If it is not in focus, click back and re-focus.
+ + + ++ Calibration complete +
++ You'll need to repeat these steps from the Settings tab if you swap your objective. +
++ Click Finish to return to your microscope. +
++ + Some important microscope calibration data is currently missing. + +
++ Your microscope will still function, however some functionality will be + limited, and image quality will likely suffer. +
++ Click Next to begin microscope calibration. +
+