Propagate logs from camera-stage-mapping
This commit is contained in:
parent
91cb514e08
commit
bcc13475b6
2 changed files with 67 additions and 29 deletions
|
|
@ -18,7 +18,7 @@ dependencies = [
|
||||||
"labthings-fastapi",
|
"labthings-fastapi",
|
||||||
"labthings-sangaboard",
|
"labthings-sangaboard",
|
||||||
"labthings-picamera2",
|
"labthings-picamera2",
|
||||||
"camera-stage-mapping",
|
"camera-stage-mapping ~= 0.1.6",
|
||||||
"numpy ~= 1.20",
|
"numpy ~= 1.20",
|
||||||
"scipy ~= 1.6",
|
"scipy ~= 1.6",
|
||||||
"opencv-python ~= 4.7.0",
|
"opencv-python ~= 4.7.0",
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,11 @@ from camera_stage_mapping.camera_stage_calibration_1d import (
|
||||||
image_to_stage_displacement_from_1d,
|
image_to_stage_displacement_from_1d,
|
||||||
)
|
)
|
||||||
from camera_stage_mapping.camera_stage_tracker import Tracker
|
from camera_stage_mapping.camera_stage_tracker import Tracker
|
||||||
|
|
||||||
from labthings_picamera2.thing import StreamingPiCamera2
|
from labthings_picamera2.thing import StreamingPiCamera2
|
||||||
from labthings_sangaboard import SangaboardThing
|
from labthings_sangaboard import SangaboardThing
|
||||||
|
|
||||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||||
|
from labthings_fastapi.dependencies.invocation import InvocationLogger
|
||||||
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
||||||
from labthings_fastapi.decorators import thing_action, thing_property
|
from labthings_fastapi.decorators import thing_action, thing_property
|
||||||
from labthings_fastapi.thing import Thing
|
from labthings_fastapi.thing import Thing
|
||||||
|
|
@ -42,9 +42,33 @@ class HardwareInterfaceModel(BaseModel):
|
||||||
get_position: Callable[[], NDArray]
|
get_position: Callable[[], NDArray]
|
||||||
grab_image: Callable[[], NDArray]
|
grab_image: Callable[[], NDArray]
|
||||||
settle: Callable[[], None]
|
settle: Callable[[], None]
|
||||||
|
grab_image_downsampling: float = 1
|
||||||
|
|
||||||
|
|
||||||
def make_hardware_interface(stage: Stage, camera: Camera) -> HardwareInterfaceModel:
|
def downsample(factor: int, image: np.ndarray) -> np.ndarray:
|
||||||
|
"""Downsample an image by taking the mean of each nxn region
|
||||||
|
|
||||||
|
This should be very efficient: we calculate the mean of each
|
||||||
|
`factor * factor` square, no interpolation. If the image is
|
||||||
|
not an integer multiple of the resampling factor, we discard
|
||||||
|
the left-over pixels. This avoids odd edge effects and keeps
|
||||||
|
performance quick.
|
||||||
|
"""
|
||||||
|
if factor == 1:
|
||||||
|
return image
|
||||||
|
new_size = [d // factor for d in image.shape[:2]]
|
||||||
|
# First, we ensure we have something that's an integer multiple
|
||||||
|
# of `factor`
|
||||||
|
cropped = image[:new_size[0] * factor, :new_size[1] * factor, ...]
|
||||||
|
reshaped = cropped.reshape(
|
||||||
|
(new_size[0], factor, new_size[1], factor) + image.shape[2:]
|
||||||
|
)
|
||||||
|
return reshaped.mean(axis=(1,3))
|
||||||
|
|
||||||
|
|
||||||
|
def make_hardware_interface(
|
||||||
|
stage: Stage, camera: Camera, downsample_factor: int = 2
|
||||||
|
) -> HardwareInterfaceModel:
|
||||||
"""Construct the functions we need to interface with the hardware"""
|
"""Construct the functions we need to interface with the hardware"""
|
||||||
axes = stage.axis_names
|
axes = stage.axis_names
|
||||||
def pos2dict(pos: Sequence[float]) -> Mapping[str, float]:
|
def pos2dict(pos: Sequence[float]) -> Mapping[str, float]:
|
||||||
|
|
@ -59,12 +83,13 @@ def make_hardware_interface(stage: Stage, camera: Camera) -> HardwareInterfaceMo
|
||||||
def get_position() -> CoordinateType:
|
def get_position() -> CoordinateType:
|
||||||
return dict2pos(stage.position)
|
return dict2pos(stage.position)
|
||||||
def grab_image() -> np.ndarray:
|
def grab_image() -> np.ndarray:
|
||||||
return camera.capture_array()
|
img = camera.capture_array()
|
||||||
|
return downsample(downsample_factor, img)
|
||||||
def settle() -> None:
|
def settle() -> None:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
camera.capture_metadata
|
camera.capture_metadata
|
||||||
return HardwareInterfaceModel(
|
return HardwareInterfaceModel(
|
||||||
move=move, get_position=get_position, grab_image=grab_image, settle=settle
|
move=move, get_position=get_position, grab_image=grab_image, settle=settle, grab_image_downsampling=downsample_factor
|
||||||
)
|
)
|
||||||
|
|
||||||
HardwareInterfaceDep = Annotated[HardwareInterfaceModel, Depends(make_hardware_interface)]
|
HardwareInterfaceDep = Annotated[HardwareInterfaceModel, Depends(make_hardware_interface)]
|
||||||
|
|
@ -130,40 +155,54 @@ class CameraStageMapper(Thing):
|
||||||
self.thing_settings.write_to_file()
|
self.thing_settings.write_to_file()
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def calibrate_1d(self, hw: HardwareInterfaceDep, direction: Tuple[float, float, float]) -> DenumpifyingDict:
|
def calibrate_1d(
|
||||||
|
self,
|
||||||
|
hw: HardwareInterfaceDep,
|
||||||
|
logger: InvocationLogger,
|
||||||
|
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"""
|
||||||
move = LoggingMoveWrapper(hw.move) # log positions and times for stage calibration
|
move = LoggingMoveWrapper(hw.move) # log positions and times for stage calibration
|
||||||
|
|
||||||
tracker = Tracker(hw.grab_image, hw.get_position, settle=hw.settle)
|
tracker = Tracker(hw.grab_image, hw.get_position, settle=hw.settle)
|
||||||
|
|
||||||
direction_array: np.ndarray = np.array(direction)
|
direction_array: np.ndarray = np.array(direction)
|
||||||
|
|
||||||
result: dict = calibrate_backlash_1d(tracker, move, direction_array)
|
result: dict = calibrate_backlash_1d(tracker, move, direction_array, logger=logger)
|
||||||
result["move_history"] = move.history
|
result["move_history"] = move.history
|
||||||
result["image_resolution"] = hw.grab_image().shape[:2]
|
result["image_resolution"] = hw.grab_image().shape[:2]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def calibrate_xy(self, hw: HardwareInterfaceDep) -> DenumpifyingDict:
|
def calibrate_xy(
|
||||||
|
self, hw: HardwareInterfaceDep, logger: InvocationLogger
|
||||||
|
) -> DenumpifyingDict:
|
||||||
"""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.
|
||||||
"""
|
"""
|
||||||
logging.info("Calibrating X axis:")
|
logger.info("Calibrating X axis:")
|
||||||
cal_x: dict = self.calibrate_1d(hw, (1, 0, 0))
|
cal_x: dict = self.calibrate_1d(hw, logger, (1, 0, 0))
|
||||||
logging.info("Calibrating Y axis:")
|
logger.info("Calibrating Y axis:")
|
||||||
cal_y: dict = self.calibrate_1d(hw, (0, 1, 0))
|
cal_y: dict = self.calibrate_1d(hw, logger, (0, 1, 0))
|
||||||
|
logger.info("Calibration complete, updating metadata.")
|
||||||
|
|
||||||
# Combine X and Y calibrations to make a 2D calibration
|
# Combine X and Y calibrations to make a 2D calibration
|
||||||
cal_xy: dict = denumpify(image_to_stage_displacement_from_1d([cal_x, cal_y]))
|
cal_xy: dict = image_to_stage_displacement_from_1d([cal_x, cal_y])
|
||||||
self.thing_settings.update(cal_xy)
|
# Correct the result for downsampling performed in the hardware interface
|
||||||
self.thing_settings["image_resolution"] = cal_x["image_resolution"]
|
# (this may be to speed up correlation, or to avoid debayering artifacts)
|
||||||
|
cal_xy["image_to_stage_displacement"] /= hw.grab_image_downsampling
|
||||||
|
corrected_resolution = tuple(
|
||||||
|
r * hw.grab_image_downsampling for r in cal_x["image_resolution"]
|
||||||
|
)
|
||||||
|
self.thing_settings.update(denumpify(cal_xy))
|
||||||
|
self.thing_settings["image_resolution"] = corrected_resolution
|
||||||
|
|
||||||
data: Dict[str, dict] = {
|
data: Dict[str, dict] = {
|
||||||
"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,
|
||||||
"image_resolution": cal_x["image_resolution"]
|
"downsampled_image_resolution": cal_x["image_resolution"],
|
||||||
|
"image_resolution": corrected_resolution,
|
||||||
|
"downsampling": hw.grab_image_downsampling,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump()
|
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump()
|
||||||
|
|
@ -194,6 +233,11 @@ class CameraStageMapper(Thing):
|
||||||
return None
|
return None
|
||||||
return np.array(displacement_matrix).tolist()
|
return np.array(displacement_matrix).tolist()
|
||||||
|
|
||||||
|
@thing_property
|
||||||
|
def image_resolution(self) -> Optional[Tuple[float, float]]:
|
||||||
|
"""The image size used to calibrate the image_to_stage_displacement_matrix"""
|
||||||
|
return self.thing_settings.get("image_resolution", None)
|
||||||
|
|
||||||
def assert_calibrated(self):
|
def assert_calibrated(self):
|
||||||
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
|
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
|
||||||
if self.image_to_stage_displacement_matrix is None:
|
if self.image_to_stage_displacement_matrix is None:
|
||||||
|
|
@ -231,15 +275,9 @@ class CameraStageMapper(Thing):
|
||||||
stage.move_relative(x=relative_move[0], y=relative_move[1])
|
stage.move_relative(x=relative_move[0], y=relative_move[1])
|
||||||
|
|
||||||
@thing_property
|
@thing_property
|
||||||
def thing_state(self) -> dict:
|
def thing_state(self) -> dict[str, Any]:
|
||||||
"""Summary metadata describing the current state of the Thing"""
|
"""Summary metadata describing the current state of the Thing"""
|
||||||
state: dict[str, Any] = {}
|
return {
|
||||||
state["image_to_stage_displacement_matrix"] = (
|
k: getattr(self, k)
|
||||||
self.image_to_stage_displacement_matrix
|
for k in ["image_to_stage_displacement_matrix", "image_resolution"]
|
||||||
)
|
}
|
||||||
if self.last_calibration:
|
|
||||||
try:
|
|
||||||
state["image_resolution"] = self.last_calibration["image_resolution"]
|
|
||||||
except KeyError:
|
|
||||||
logging.warning("Couldn't find `image_resolution` in CSM calibration")
|
|
||||||
return state
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue