Merge remote-tracking branch 'origin/master' into imjoy-support

Merge in changes from master,

This matters because we need to get the fixes to Python
package management and the stage API for absolute moves.
This commit is contained in:
Richard 2021-04-27 12:25:12 +01:00
commit 6b8d34761c
16 changed files with 1918 additions and 196 deletions

View file

@ -218,10 +218,16 @@ def cleanup():
atexit.register(cleanup)
# Start the app
if __name__ == "__main__":
def ofm_serve():
# Start a debug server
from labthings import Server
logging.info("Starting OpenFlexure Microscope Server...")
server: Server = Server(app)
server.run(host="0.0.0.0", port=5000, debug=debug_app, zeroconf=True)
# Start the app if the module is run directly
if __name__ == "__main__":
ofm_serve()

View file

@ -127,20 +127,20 @@ def monitor_sharpness(microscope: Microscope):
m.stop()
def sharpness_sum_lap2(rgb_image: np.ndarray) -> np.float:
def sharpness_sum_lap2(rgb_image: np.ndarray) -> float:
"""Return an image sharpness metric: sum(laplacian(image)**")"""
image_bw: np.float = np.mean(rgb_image, 2)
image_lap: np.float = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float) ** 4)
image_bw = np.mean(rgb_image, 2)
image_lap = ndimage.filters.laplace(image_bw)
return float(np.mean(image_lap.astype(float) ** 4))
def sharpness_edge(image: np.ndarray) -> np.float:
def sharpness_edge(image: np.ndarray) -> float:
"""Return a sharpness metric optimised for vertical lines"""
gray: np.float = np.mean(image.astype(float), 2)
gray = np.mean(image.astype(float), 2)
n: int = 20
edge: np.ndarray = np.array([[-1] * n + [1] * n])
return np.sum(
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
return float(
np.sum([np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]])
)
@ -169,7 +169,7 @@ class AutofocusExtension(BaseExtension):
def measure_sharpness(
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
) -> np.float:
) -> float:
"""Measure the sharpness of the camera's current view."""
if hasattr(microscope.camera, "array") and callable(
@ -185,7 +185,7 @@ class AutofocusExtension(BaseExtension):
dz: List[int],
settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2,
) -> Tuple[List[int], List[np.float]]:
) -> Tuple[List[int], List[float]]:
"""Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz,
and at each position an image is captured and the sharpness function
@ -197,7 +197,7 @@ class AutofocusExtension(BaseExtension):
stage: BaseStage = microscope.stage
with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses: List[np.float] = []
sharpnesses: List[float] = []
positions: List[int] = []
# Some cameras may not have annotate_text. Reset if it does

View file

@ -106,7 +106,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
lens_shading: np.ndarray = np.zeros(
[channels.shape[0]] + lst_resolution, dtype=np.float
[channels.shape[0]] + lst_resolution, dtype=float
)
for i in range(lens_shading.shape[0]):
image_channel: np.ndarray = channels[i, :, :]
@ -193,7 +193,7 @@ def recalibrate_camera(camera: PiCamera):
_ = rgb_image(camera)
# Fix the AWB gains so the image is neutral
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=float), axis=0)
old_gains = camera.awb_gains
camera.awb_gains = (
channel_means[1] / channel_means[0] * old_gains[0],

View file

@ -2,7 +2,7 @@
<div>
<form class="uk-form-stacked" @submit.prevent="overrideAPIHost">
<label class="uk-form-label">Override API origin</label>
<input v-model="currentOrigin" class="uk-input" type="text" />
<input v-model="newOrigin" class="uk-input" type="text" />
<button class="uk-button uk-button-default uk-margin-small">
Apply
</button>
@ -19,19 +19,22 @@ export default {
data: function() {
return {
currentOrigin: this.$store.state.origin
newOrigin: this.$store.state.origin
};
},
mounted() {
if (!this.$store.getters.ready) {
this.currentOrigin = "http://microscope.local:5000";
if (localStorage.overrideOrigin){
this.newOrigin = localStorage.overrideOrigin;
}else{
this.newOrigin = "http://microscope.local:5000";
}
},
methods: {
overrideAPIHost: function() {
this.$store.commit("changeOrigin", this.currentOrigin);
this.$store.commit("changeOrigin", this.newOrigin);
localStorage.overrideOrigin = this.newOrigin
}
}
};

View file

@ -160,6 +160,7 @@ export default {
stride_size: [800, 600, 10],
fast_autofocus: true,
autofocus_dz: 2000,
style: "raster",
use_video_port: false
};
},

View file

@ -1,50 +1,47 @@
import logging
from typing import List, Tuple
from labthings import fields, find_component
from labthings.views import ActionView
from openflexure_microscope.utilities import axes_to_array
class MoveStageAPI(ActionView):
args = {
"absolute": fields.Boolean(
missing=False, example=False, description="Move to an absolute position"
),
"x": fields.Int(missing=0, example=100),
"y": fields.Int(missing=0, example=100),
"z": fields.Int(missing=0, example=20),
"x": fields.Int(missing=None, example=100),
"y": fields.Int(missing=None, example=100),
"z": fields.Int(missing=None, example=20),
}
def post(self, args):
"""
Move the microscope stage in x, y, z
Any axes that are not specifed will not move.
"""
microscope = find_component("org.openflexure.microscope")
# Handle absolute positioning (calculate a relative move from current position and target)
if (args.get("absolute")) and (microscope.stage): # Only if stage exists
target_position: List[int] = axes_to_array(args, ["x", "y", "z"])
logging.debug("TARGET: %s", (target_position))
position: Tuple[int, int, int] = (
target_position[i] - microscope.stage.position[i] for i in range(3)
)
logging.debug("DELTA: %s", (position))
else:
# Get coordinates from payload
position = axes_to_array(args, ["x", "y", "z"], [0, 0, 0])
logging.debug(position)
# Move if stage exists
if microscope.stage:
# Explicitally acquire lock with 1s timeout
with microscope.stage.lock(timeout=1):
microscope.stage.move_rel(position)
else:
if not microscope.stage:
logging.warning("Unable to move. No stage found.")
return microscope.state["stage"]["position"]
absolute_move = args.get("absolute")
move = [0, 0, 0] # Default to no motion
for i, axis in enumerate(["x", "y", "z"]):
if axis in args and args[axis] is not None:
if absolute_move:
# We emulate absolute moves by calculating a relative move that
# will take us to the right position.
move[i] = args[axis] - microscope.stage.position[i]
else:
move[i] = args[axis]
logging.debug(f"Moving stage by {move}, request was {args}")
# Explicitly acquire lock with 1s timeout
with microscope.stage.lock(timeout=1):
microscope.stage.move_rel(move)
return microscope.state["stage"]["position"]

View file

@ -21,8 +21,8 @@ class JSONEncoder(LabThingsJSONEncoder):
# Numpy integers
elif isinstance(o, np.integer):
return int(o)
# Numpy floats
elif isinstance(o, np.float):
# Numpy floats are just Python floats
elif isinstance(o, float):
return float(o)
# Numpy arrays
elif isinstance(o, np.ndarray):

View file

@ -89,7 +89,7 @@ class MissingStage(BaseStage):
)
displacement = move
initial_move = np.array(displacement, dtype=np.int)
initial_move = np.array(displacement, dtype=np.integer)
self._position = list(np.array(self._position) + np.array(initial_move))
logging.debug(np.array(self._position) + np.array(initial_move))

View file

@ -277,13 +277,11 @@ class SangaDeltaStage(SangaStage):
logging.debug(self.R_camera)
# Transformation matrix converting delta into cartesian
x_fac: np.float = -1 * np.multiply(
x_fac: float = -1 * np.multiply(
np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h)
)
y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h)
z_fac: np.float = np.multiply(
np.divide(1, 3), np.divide(self.flex_b, self.flex_a)
)
y_fac: float = -1 * np.divide(self.flex_b, self.flex_h)
z_fac: float = np.multiply(np.divide(1, 3), np.divide(self.flex_b, self.flex_a))
self.Tvd: np.ndarray = np.array(
[

View file

@ -4,7 +4,7 @@ import logging
import sys
import time
from contextlib import contextmanager
from typing import Dict, List, Optional, Tuple, Type, Union
from typing import Dict, List, Optional, Sequence, Tuple, Type, Union
import numpy as np
@ -102,12 +102,29 @@ def set_properties(obj, **kwargs):
def axes_to_array(
coordinate_dictionary: Dict[str, int],
axis_keys=("x", "y", "z"),
coordinate_dictionary: Dict[str, Optional[int]],
axis_keys: Sequence[str] = ("x", "y", "z"),
base_array: Optional[List[int]] = None,
asint: bool = True,
) -> List[int]:
"""Takes key-value pairs of a JSON value, and maps onto an array"""
"""Takes key-value pairs of a JSON value, and maps onto an array
This is designed to take a dictionary like `{"x": 1, "y":2, "z":3}`
and return a list like `[1, 2, 3]` to convert between the argument
format expected by most of our stages, and the usual argument
format in JSON.
`axis_keys` is an ordered sequence of key names to extract from
the input dictionary.
`base_array` specifies a default value for each axis. It must
have the same length as `axis_keys`.
`asint` casts values to integers if it is `True` (default).
Missing keys, or keys that have a `None` value will be left
at the specified default value, or zero if none is specified.
"""
# If no base array is given
if not base_array:
# Create an array of zeros
@ -119,8 +136,14 @@ def axes_to_array(
# Do the mapping
for axis, key in enumerate(axis_keys):
if key in coordinate_dictionary:
base_array[axis] = (
int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key]
)
value = coordinate_dictionary[key]
if value is None:
# Values set to None should be treated as if they
# are missing
# i.e. we leave the default value in place.
break
if asint:
value = int(value)
base_array[axis] = value
return base_array