Fix a number of issues with camera_stage_mapping
This commit is contained in:
parent
18e89aa148
commit
58aa3df587
2 changed files with 52 additions and 36 deletions
|
|
@ -10,16 +10,9 @@ This module is only intended to be called from the OpenFlexure Microscope
|
||||||
server, and depends on that server and its underlying LabThings library.
|
server, and depends on that server and its underlying LabThings library.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import Any, List, Mapping, NamedTuple, Optional, Tuple, cast
|
||||||
Any,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
Mapping,
|
|
||||||
NamedTuple,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
)
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
@ -35,9 +28,6 @@ from labthings_fastapi.types.numpy import DenumpifyingDict
|
||||||
from .camera import BaseCamera
|
from .camera import BaseCamera
|
||||||
from .stage import BaseStage
|
from .stage import BaseStage
|
||||||
|
|
||||||
CoordinateType = Tuple[float, float, float]
|
|
||||||
XYCoordinateType = Tuple[float, float]
|
|
||||||
|
|
||||||
|
|
||||||
class MoveHistory(NamedTuple):
|
class MoveHistory(NamedTuple):
|
||||||
"""A named tuple containing the position over time for a single move.
|
"""A named tuple containing the position over time for a single move.
|
||||||
|
|
@ -49,7 +39,27 @@ class MoveHistory(NamedTuple):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
times: List[float]
|
times: List[float]
|
||||||
stage_positions: List[CoordinateType]
|
stage_positions: List[tuple[int, int, int]]
|
||||||
|
|
||||||
|
|
||||||
|
def _array_to_stage_tuple(pos: np.ndarray) -> tuple[int, int, int]:
|
||||||
|
"""Convert a numpy array into a tuple of ints.
|
||||||
|
|
||||||
|
:param pos: Input position array must be 3 elements long.
|
||||||
|
:return: a tuple of 3 integers
|
||||||
|
:raises ValueError: If the array is not of length 3.
|
||||||
|
"""
|
||||||
|
pos_tuple = tuple(int(i) for i in pos)
|
||||||
|
if len(pos_tuple) == 3:
|
||||||
|
return cast(tuple[int, int, int], pos_tuple)
|
||||||
|
raise ValueError("Input array was not 3 elements long.")
|
||||||
|
|
||||||
|
|
||||||
|
def _serialise_numpy_in_dict(dict_with_numpy: dict) -> dict:
|
||||||
|
serialised = json.loads(DenumpifyingDict(dict_with_numpy).model_dump_json())
|
||||||
|
if not isinstance(serialised, dict):
|
||||||
|
raise TypeError(f"Expecting a dictionary to serialise not a {type(serialised)}")
|
||||||
|
return serialised
|
||||||
|
|
||||||
|
|
||||||
class RecordedMove:
|
class RecordedMove:
|
||||||
|
|
@ -68,21 +78,24 @@ class RecordedMove:
|
||||||
be called whenever the instance is called.
|
be called whenever the instance is called.
|
||||||
"""
|
"""
|
||||||
self._stage = stage
|
self._stage = stage
|
||||||
self._current_position: Optional[CoordinateType] = None
|
self._current_position: Optional[tuple[int, int, int]] = None
|
||||||
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
|
self._history: List[Tuple[float, tuple[int, int, int]]] = []
|
||||||
|
|
||||||
def __call__(self, new_position: CoordinateType) -> None:
|
def __call__(self, new_position: np.ndarray) -> None:
|
||||||
"""Move to a new position, and record it."""
|
"""Move to a new position, and record it."""
|
||||||
self._history.append((time.time(), self._current_position))
|
new_stage_pos = _array_to_stage_tuple(new_position)
|
||||||
self._stage.move_to_xyz_position(xyz_pos=new_position)
|
starting_pos = self._current_position
|
||||||
self._current_position = new_position
|
if starting_pos is not None:
|
||||||
self._history.append((time.time(), self._current_position))
|
self._history.append((time.time(), starting_pos))
|
||||||
|
self._stage.move_to_xyz_position(xyz_pos=new_stage_pos)
|
||||||
|
self._current_position = new_stage_pos
|
||||||
|
self._history.append((time.time(), new_stage_pos))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def history(self) -> MoveHistory:
|
def history(self) -> MoveHistory:
|
||||||
"""The history, as a numpy array of times and another of positions."""
|
"""The history, as a numpy array of times and another of positions."""
|
||||||
times: List[float] = [t for t, p in self._history if p is not None]
|
times: List[float] = [t for t, p in self._history]
|
||||||
positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
|
positions: List[tuple[int, int, int]] = [p for t, p in self._history]
|
||||||
return MoveHistory(times, positions)
|
return MoveHistory(times, positions)
|
||||||
|
|
||||||
def clear_history(self) -> None:
|
def clear_history(self) -> None:
|
||||||
|
|
@ -109,8 +122,7 @@ class CameraStageMapper(lt.Thing):
|
||||||
_cam: BaseCamera = lt.thing_slot()
|
_cam: BaseCamera = lt.thing_slot()
|
||||||
_stage: BaseStage = lt.thing_slot()
|
_stage: BaseStage = lt.thing_slot()
|
||||||
|
|
||||||
@lt.action
|
def calibrate_1d(self, direction: Tuple[int, int, int]) -> dict:
|
||||||
def calibrate_1d(self, direction: Tuple[float, float, float]) -> DenumpifyingDict:
|
|
||||||
"""Move a microscope's stage in 1D, and figure out the relationship with the camera."""
|
"""Move a microscope's stage in 1D, and figure out the relationship with the camera."""
|
||||||
# Record positions and times for stage calibration
|
# Record positions and times for stage calibration
|
||||||
recorded_move = RecordedMove(self._stage)
|
recorded_move = RecordedMove(self._stage)
|
||||||
|
|
@ -140,7 +152,7 @@ class CameraStageMapper(lt.Thing):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def calibrate_xy(self) -> DenumpifyingDict:
|
def calibrate_xy(self) -> dict:
|
||||||
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera.
|
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera.
|
||||||
|
|
||||||
This performs two 1d calibrations in x and y, then combines their results.
|
This performs two 1d calibrations in x and y, then combines their results.
|
||||||
|
|
@ -170,7 +182,7 @@ class CameraStageMapper(lt.Thing):
|
||||||
)
|
)
|
||||||
self.logger.info(f"CSM matrix is {csm_as_string}.")
|
self.logger.info(f"CSM matrix is {csm_as_string}.")
|
||||||
|
|
||||||
data: Dict[str, dict] = {
|
data = {
|
||||||
"camera_stage_mapping_calibration": cal_xy,
|
"camera_stage_mapping_calibration": cal_xy,
|
||||||
"linear_calibration_x": cal_x,
|
"linear_calibration_x": cal_x,
|
||||||
"linear_calibration_y": cal_y,
|
"linear_calibration_y": cal_y,
|
||||||
|
|
@ -179,7 +191,8 @@ class CameraStageMapper(lt.Thing):
|
||||||
"downsampling": downsampling_factor,
|
"downsampling": downsampling_factor,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_calibration = DenumpifyingDict(data).model_dump()
|
data = _serialise_numpy_in_dict(data)
|
||||||
|
self.last_calibration = data
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -226,13 +239,14 @@ class CameraStageMapper(lt.Thing):
|
||||||
"""Whether the camera stage mapper needs calibrating."""
|
"""Whether the camera stage mapper needs calibrating."""
|
||||||
return self.image_to_stage_displacement_matrix is None
|
return self.image_to_stage_displacement_matrix is None
|
||||||
|
|
||||||
def assert_calibrated(self) -> None:
|
def assert_calibration(self) -> List[List[float]]:
|
||||||
"""Raise an exception if the image_to_stage_displacement matrix is not set."""
|
"""Return image_to_stage_displacement matrix or raise error if it's not set."""
|
||||||
if self.image_to_stage_displacement_matrix is None:
|
if self.image_to_stage_displacement_matrix is None:
|
||||||
raise CSMUncalibratedError(
|
raise CSMUncalibratedError(
|
||||||
"The camera_stage_mapping calibration is not yet available. "
|
"The camera_stage_mapping calibration is not yet available. "
|
||||||
"This probably means you need to run the calibration routine."
|
"This probably means you need to run the calibration routine."
|
||||||
)
|
)
|
||||||
|
return self.image_to_stage_displacement_matrix
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def move_in_image_coordinates(self, x: float, y: float) -> None:
|
def move_in_image_coordinates(self, x: float, y: float) -> None:
|
||||||
|
|
@ -247,24 +261,26 @@ class CameraStageMapper(lt.Thing):
|
||||||
and ``y`` to the shorter one. Checking what shape your chosen toolkit reports for
|
and ``y`` to the shorter one. Checking what shape your chosen toolkit reports for
|
||||||
an image usually helps resolve any ambiguity.
|
an image usually helps resolve any ambiguity.
|
||||||
"""
|
"""
|
||||||
self.assert_calibrated()
|
self._stage.move_relative(
|
||||||
self._stage.move_relative(**self.convert_image_to_stage_coordinates(x=x, y=y))
|
**self.convert_image_to_stage_coordinates(x=x, y=y),
|
||||||
|
block_cancellation=False,
|
||||||
|
)
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def convert_image_to_stage_coordinates(
|
def convert_image_to_stage_coordinates(
|
||||||
self, x: float, y: float, **_kwargs: float
|
self, x: float, y: float, **_kwargs: float
|
||||||
) -> Mapping[str, int]:
|
) -> Mapping[str, int]:
|
||||||
"""Convert image coordinates to stage coordinates. Only x and y are returned."""
|
"""Convert image coordinates to stage coordinates. Only x and y are returned."""
|
||||||
self.assert_calibrated()
|
csm_matrix = self.assert_calibration()
|
||||||
return csm_img_to_stage(self.image_to_stage_displacement_matrix, x=x, y=y)
|
return csm_img_to_stage(csm_matrix, x=x, y=y)
|
||||||
|
|
||||||
@lt.action
|
@lt.action
|
||||||
def convert_stage_to_image_coordinates(
|
def convert_stage_to_image_coordinates(
|
||||||
self, x: int, y: int, **_kwargs: int
|
self, x: int, y: int, **_kwargs: int
|
||||||
) -> Mapping[str, float]:
|
) -> Mapping[str, float]:
|
||||||
"""Convert stage coordinates to image coordinates. Only x and y are returned."""
|
"""Convert stage coordinates to image coordinates. Only x and y are returned."""
|
||||||
self.assert_calibrated()
|
csm_matrix = self.assert_calibration()
|
||||||
return csm_stage_to_img(self.image_to_stage_displacement_matrix, x=x, y=y)
|
return csm_stage_to_img(csm_matrix, x=x, y=y)
|
||||||
|
|
||||||
@lt.property
|
@lt.property
|
||||||
def thing_state(self) -> Mapping[str, Any]:
|
def thing_state(self) -> Mapping[str, Any]:
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ class SmartScanThing(lt.Thing):
|
||||||
|
|
||||||
Raise warning if not using background detect that scan will go on until max steps reached
|
Raise warning if not using background detect that scan will go on until max steps reached
|
||||||
"""
|
"""
|
||||||
self._csm.assert_calibrated()
|
self._csm.assert_calibration()
|
||||||
|
|
||||||
if self.skip_background:
|
if self.skip_background:
|
||||||
if not self._cam.background_detector_status.ready:
|
if not self._cam.background_detector_status.ready:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue