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:
commit
6b8d34761c
16 changed files with 1918 additions and 196 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ export default {
|
|||
stride_size: [800, 600, 10],
|
||||
fast_autofocus: true,
|
||||
autofocus_dz: 2000,
|
||||
style: "raster",
|
||||
use_video_port: false
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue