Merge remote-tracking branch 'origin/v3' into v3-webapp
This commit is contained in:
commit
45d2884159
3 changed files with 95 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ from labthings_picamera2.thing import StreamingPiCamera2
|
||||||
|
|
||||||
from .things.autofocus import AutofocusThing
|
from .things.autofocus import AutofocusThing
|
||||||
from .things.camera_stage_mapping import CameraStageMapper
|
from .things.camera_stage_mapping import CameraStageMapper
|
||||||
|
from .things.system_control import SystemControlThing
|
||||||
from .serve_static_files import add_static_files
|
from .serve_static_files import add_static_files
|
||||||
import openflexure_microscope_server
|
import openflexure_microscope_server
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ thing_server.add_thing(StreamingPiCamera2(), "/camera/")
|
||||||
thing_server.add_thing(SangaboardThing(), "/stage/")
|
thing_server.add_thing(SangaboardThing(), "/stage/")
|
||||||
thing_server.add_thing(AutofocusThing(), "/autofocus/")
|
thing_server.add_thing(AutofocusThing(), "/autofocus/")
|
||||||
thing_server.add_thing(CameraStageMapper(), "/camera_stage_mapping/")
|
thing_server.add_thing(CameraStageMapper(), "/camera_stage_mapping/")
|
||||||
|
thing_server.add_thing(SystemControlThing(), "/system_control/")
|
||||||
try:
|
try:
|
||||||
add_static_files(thing_server.app)
|
add_static_files(thing_server.app)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
|
|
||||||
|
|
@ -132,11 +132,15 @@ class CameraStageMapper(Thing):
|
||||||
|
|
||||||
result: dict = calibrate_backlash_1d(tracker, move, direction_array)
|
result: dict = calibrate_backlash_1d(tracker, move, direction_array)
|
||||||
result["move_history"] = move.history
|
result["move_history"] = move.history
|
||||||
|
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) -> 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.
|
||||||
|
"""
|
||||||
logging.info("Calibrating X axis:")
|
logging.info("Calibrating X axis:")
|
||||||
cal_x: dict = self.calibrate_1d(hw, (1, 0, 0))
|
cal_x: dict = self.calibrate_1d(hw, (1, 0, 0))
|
||||||
logging.info("Calibrating Y axis:")
|
logging.info("Calibrating Y axis:")
|
||||||
|
|
@ -145,11 +149,13 @@ class CameraStageMapper(Thing):
|
||||||
# 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 = denumpify(image_to_stage_displacement_from_1d([cal_x, cal_y]))
|
||||||
self.thing_settings.update(cal_xy)
|
self.thing_settings.update(cal_xy)
|
||||||
|
self.thing_settings["image_resolution"] = cal_x["image_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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump()
|
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump()
|
||||||
|
|
@ -158,12 +164,34 @@ class CameraStageMapper(Thing):
|
||||||
|
|
||||||
@thing_property
|
@thing_property
|
||||||
def image_to_stage_displacement_matrix(self) -> List[List[float]]: # 2x2 integer array
|
def image_to_stage_displacement_matrix(self) -> List[List[float]]: # 2x2 integer array
|
||||||
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates."""
|
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates.
|
||||||
|
|
||||||
|
Note that this matrix is defined using "matrix coordinates", i.e. image coordinates
|
||||||
|
may be (y,x). This is an artifact of the way numpy, opencv, etc. define images. If
|
||||||
|
you are making use of this matrix in your own code, you will need to take care of
|
||||||
|
that conversion.
|
||||||
|
|
||||||
|
It is often helpful to give a concrete example: to make a move in image coordinates
|
||||||
|
(`dy`, `dx`), where `dx` is horizontal, i.e. the longer dimension of the image, you
|
||||||
|
should move the stage by:
|
||||||
|
```
|
||||||
|
stage_disp = np.dot(
|
||||||
|
np.array(image_to_stage_displacement_matrix),
|
||||||
|
np.array([dy,dx]),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
displacement_matrix = self.thing_settings.get("image_to_stage_displacement")
|
displacement_matrix = self.thing_settings.get("image_to_stage_displacement")
|
||||||
if not displacement_matrix:
|
if not displacement_matrix:
|
||||||
raise ValueError("The microscope has not yet been calibrated.")
|
raise ValueError("The microscope has not yet been calibrated.")
|
||||||
return np.array(displacement_matrix).tolist()
|
return np.array(displacement_matrix).tolist()
|
||||||
|
|
||||||
|
@thing_property
|
||||||
|
def last_calibration(self) -> Optional[Dict]: # 2x2 integer array
|
||||||
|
"""The results of the last calibration that was run
|
||||||
|
"""
|
||||||
|
return self.thing_settings.get("last_calibration", None)
|
||||||
|
|
||||||
@thing_action
|
@thing_action
|
||||||
def move_in_image_coordinates(
|
def move_in_image_coordinates(
|
||||||
self,
|
self,
|
||||||
|
|
@ -188,11 +216,14 @@ 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])
|
||||||
|
|
||||||
@property
|
@thing_property
|
||||||
def thing_state(self) -> dict:
|
def thing_state(self) -> dict:
|
||||||
"""Summary metadata describing the current state of the Thing"""
|
"""Summary metadata describing the current state of the Thing"""
|
||||||
return {
|
return {
|
||||||
"image_to_stage_displacement_matrix": (
|
"image_to_stage_displacement_matrix": (
|
||||||
self.image_to_stage_displacement_matrix
|
self.image_to_stage_displacement_matrix
|
||||||
),
|
),
|
||||||
|
"image_resolution": (
|
||||||
|
self.thing_settings.get("image_to_stage_displacement", None)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
59
src/openflexure_microscope_server/things/system_control.py
Normal file
59
src/openflexure_microscope_server/things/system_control.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
OpenFlexure Microscope API extension for stage calibration
|
||||||
|
|
||||||
|
This file contains the HTTP API for camera/stage calibration. It
|
||||||
|
includes calibration functions that measure the relationship between
|
||||||
|
stage coordinates and camera coordinates, as well as functions that
|
||||||
|
move by a specified displacement in pixels, perform closed-loop moves,
|
||||||
|
and return the calibration data.
|
||||||
|
|
||||||
|
This module is only intended to be called from the OpenFlexure Microscope
|
||||||
|
server, and depends on that server and its underlying LabThings library.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from labthings_fastapi.thing import Thing
|
||||||
|
from labthings_fastapi.decorators import thing_action, thing_property
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class CommandOutput(BaseModel):
|
||||||
|
output: str
|
||||||
|
error: str
|
||||||
|
|
||||||
|
|
||||||
|
class SystemControlThing(Thing):
|
||||||
|
"""
|
||||||
|
Attempt to shutdown the device
|
||||||
|
"""
|
||||||
|
|
||||||
|
@thing_action
|
||||||
|
def shutdown(self) -> CommandOutput:
|
||||||
|
"""
|
||||||
|
Attempt to shutdown the device
|
||||||
|
"""
|
||||||
|
p = subprocess.Popen(
|
||||||
|
["sudo", "shutdown", "-h", "now"],
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
out, err = p.communicate()
|
||||||
|
return CommandOutput(output=out, error=err)
|
||||||
|
|
||||||
|
@thing_property
|
||||||
|
def is_raspberrypi() -> bool:
|
||||||
|
"""
|
||||||
|
Checks if we are running on a Raspberry Pi.
|
||||||
|
"""
|
||||||
|
return os.path.exists("/usr/bin/raspi-config")
|
||||||
|
|
||||||
|
@thing_action
|
||||||
|
def reboot(self) -> CommandOutput:
|
||||||
|
"""Attempt to reboot the device"""
|
||||||
|
p = subprocess.Popen(
|
||||||
|
["sudo", "shutdown", "-r", "now"],
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
out, err = p.communicate()
|
||||||
|
return CommandOutput(output=out, error=err)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue