2.9 dev numpy types
This commit is contained in:
parent
f2a2d880f2
commit
63b633ba44
10 changed files with 156 additions and 90 deletions
|
|
@ -10,9 +10,15 @@ This includes installing the server in a mode better suited for active developme
|
||||||
|
|
||||||
# Developer guidelines
|
# Developer guidelines
|
||||||
|
|
||||||
|
The Raspberry Pi image we use currently ships with Python 3.7.3. For local development, please use PyEnv or similar to make sure you're running on this version. For example, Windows users can use [Scoop](https://scoop.sh/) to install specific Python versions.
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
* `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git`
|
* `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git`
|
||||||
|
* `cd openflexure-microscope-server`
|
||||||
|
* (Optional) Set local Python version
|
||||||
|
* `pyenv init`
|
||||||
|
* `pyenv install 3.7.3`
|
||||||
|
* `pyenv local 3.7.3`
|
||||||
* `poetry install`
|
* `poetry install`
|
||||||
* Building the static interface will require a valid Node.js installation
|
* Building the static interface will require a valid Node.js installation
|
||||||
* To build on a Raspberry Pi:
|
* To build on a Raspberry Pi:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Callable, List, Optional, Tuple
|
from typing import Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from labthings import current_action, fields, find_component
|
from labthings import current_action, fields, find_component
|
||||||
|
|
@ -39,7 +39,7 @@ class JPEGSharpnessMonitor:
|
||||||
self.camera.stream.stop_tracking()
|
self.camera.stream.stop_tracking()
|
||||||
self.camera.stream.reset_tracking()
|
self.camera.stream.reset_tracking()
|
||||||
|
|
||||||
def focus_rel(self, dz: int, backlash: bool = False, **kwargs):
|
def focus_rel(self, dz: int, backlash: bool = False, **kwargs) -> Tuple[int, int]:
|
||||||
# Store the start time and position
|
# Store the start time and position
|
||||||
self.camera.stream.start_tracking()
|
self.camera.stream.start_tracking()
|
||||||
self.stage_times.append(time.time())
|
self.stage_times.append(time.time())
|
||||||
|
|
@ -62,22 +62,28 @@ class JPEGSharpnessMonitor:
|
||||||
self.camera.stream.reset_tracking()
|
self.camera.stream.reset_tracking()
|
||||||
|
|
||||||
# Index of the data for this movement
|
# Index of the data for this movement
|
||||||
data_index = len(self.stage_positions) - 2
|
data_index: int = len(self.stage_positions) - 2
|
||||||
# Final z position after move
|
# Final z position after move
|
||||||
final_z_position = self.stage_positions[-1][2]
|
final_z_position: int = self.stage_positions[-1][2]
|
||||||
return data_index, final_z_position
|
return data_index, final_z_position
|
||||||
|
|
||||||
def move_data(self, istart: int, istop: Optional[int] = None):
|
def move_data(
|
||||||
|
self, istart: int, istop: Optional[int] = None
|
||||||
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
"""Extract sharpness as a function of (interpolated) z"""
|
"""Extract sharpness as a function of (interpolated) z"""
|
||||||
if istop is None:
|
if istop is None:
|
||||||
istop = istart + 2
|
istop = istart + 2
|
||||||
jpeg_times = np.array(self.jpeg_times)
|
jpeg_times: np.ndarray = np.array(self.jpeg_times) # np.ndarray[float]
|
||||||
jpeg_sizes = np.array(self.jpeg_sizes)
|
jpeg_sizes: np.ndarray = np.array(self.jpeg_sizes) # np.ndarray[int]
|
||||||
stage_times = np.array(self.stage_times)[istart:istop]
|
stage_times: np.ndarray = np.array(self.stage_times)[
|
||||||
stage_zs = np.array(self.stage_positions)[istart:istop, 2]
|
istart:istop
|
||||||
|
] # np.ndarray[float]
|
||||||
|
stage_zs: np.ndarray = np.array(self.stage_positions)[
|
||||||
|
istart:istop, 2
|
||||||
|
] # np.ndarray[int]
|
||||||
try:
|
try:
|
||||||
start = np.argmax(jpeg_times > stage_times[0])
|
start: int = int(np.argmax(jpeg_times > stage_times[0]))
|
||||||
stop = np.argmax(jpeg_times > stage_times[1])
|
stop: int = int(np.argmax(jpeg_times > stage_times[1]))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if np.sum(jpeg_times > stage_times[0]) == 0:
|
if np.sum(jpeg_times > stage_times[0]) == 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
@ -89,10 +95,12 @@ class JPEGSharpnessMonitor:
|
||||||
stop = len(jpeg_times)
|
stop = len(jpeg_times)
|
||||||
logging.debug("changing stop to %s", (stop))
|
logging.debug("changing stop to %s", (stop))
|
||||||
jpeg_times = jpeg_times[start:stop]
|
jpeg_times = jpeg_times[start:stop]
|
||||||
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
|
jpeg_zs: np.ndarray = np.interp(
|
||||||
|
jpeg_times, stage_times, stage_zs
|
||||||
|
) # np.ndarray[float]
|
||||||
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
|
||||||
|
|
||||||
def sharpest_z_on_move(self, index: int):
|
def sharpest_z_on_move(self, index: int) -> int:
|
||||||
"""Return the z position of the sharpest image on a given move"""
|
"""Return the z position of the sharpest image on a given move"""
|
||||||
_, jz, js = self.move_data(index)
|
_, jz, js = self.move_data(index)
|
||||||
if len(js) == 0:
|
if len(js) == 0:
|
||||||
|
|
@ -101,7 +109,7 @@ class JPEGSharpnessMonitor:
|
||||||
)
|
)
|
||||||
return jz[np.argmax(js)]
|
return jz[np.argmax(js)]
|
||||||
|
|
||||||
def data_dict(self):
|
def data_dict(self) -> Dict[str, np.ndarray]:
|
||||||
"""Return the gathered data as a single convenient dictionary"""
|
"""Return the gathered data as a single convenient dictionary"""
|
||||||
data = {}
|
data = {}
|
||||||
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
|
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
|
||||||
|
|
@ -111,7 +119,7 @@ class JPEGSharpnessMonitor:
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def monitor_sharpness(microscope: Microscope):
|
def monitor_sharpness(microscope: Microscope):
|
||||||
m = JPEGSharpnessMonitor(microscope)
|
m: JPEGSharpnessMonitor = JPEGSharpnessMonitor(microscope)
|
||||||
m.start()
|
m.start()
|
||||||
try:
|
try:
|
||||||
yield m
|
yield m
|
||||||
|
|
@ -119,18 +127,18 @@ def monitor_sharpness(microscope: Microscope):
|
||||||
m.stop()
|
m.stop()
|
||||||
|
|
||||||
|
|
||||||
def sharpness_sum_lap2(rgb_image: np.ndarray):
|
def sharpness_sum_lap2(rgb_image: np.ndarray) -> np.float:
|
||||||
"""Return an image sharpness metric: sum(laplacian(image)**")"""
|
"""Return an image sharpness metric: sum(laplacian(image)**")"""
|
||||||
image_bw = np.mean(rgb_image, 2)
|
image_bw: np.float = np.mean(rgb_image, 2)
|
||||||
image_lap = ndimage.filters.laplace(image_bw)
|
image_lap: np.float = ndimage.filters.laplace(image_bw)
|
||||||
return np.mean(image_lap.astype(np.float) ** 4)
|
return np.mean(image_lap.astype(np.float) ** 4)
|
||||||
|
|
||||||
|
|
||||||
def sharpness_edge(image: np.ndarray):
|
def sharpness_edge(image: np.ndarray) -> np.float:
|
||||||
"""Return a sharpness metric optimised for vertical lines"""
|
"""Return a sharpness metric optimised for vertical lines"""
|
||||||
gray = np.mean(image.astype(float), 2)
|
gray: np.float = np.mean(image.astype(float), 2)
|
||||||
n = 20
|
n: int = 20
|
||||||
edge = np.array([[-1] * n + [1] * n])
|
edge: np.ndarray = np.array([[-1] * n + [1] * n])
|
||||||
return np.sum(
|
return np.sum(
|
||||||
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
|
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
|
||||||
)
|
)
|
||||||
|
|
@ -161,7 +169,7 @@ class AutofocusExtension(BaseExtension):
|
||||||
|
|
||||||
def measure_sharpness(
|
def measure_sharpness(
|
||||||
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
|
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
|
||||||
):
|
) -> np.float:
|
||||||
"""Measure the sharpness of the camera's current view."""
|
"""Measure the sharpness of the camera's current view."""
|
||||||
|
|
||||||
if hasattr(microscope.camera, "array") and callable(
|
if hasattr(microscope.camera, "array") and callable(
|
||||||
|
|
@ -177,7 +185,7 @@ class AutofocusExtension(BaseExtension):
|
||||||
dz: List[int],
|
dz: List[int],
|
||||||
settle: float = 0.5,
|
settle: float = 0.5,
|
||||||
metric_fn: Callable = sharpness_sum_lap2,
|
metric_fn: Callable = sharpness_sum_lap2,
|
||||||
):
|
) -> Tuple[List[int], List[np.float]]:
|
||||||
"""Perform a simple autofocus routine.
|
"""Perform a simple autofocus routine.
|
||||||
The stage is moved to z positions (relative to current position) in dz,
|
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
|
and at each position an image is captured and the sharpness function
|
||||||
|
|
@ -185,12 +193,12 @@ class AutofocusExtension(BaseExtension):
|
||||||
highest. No interpolation is performed.
|
highest. No interpolation is performed.
|
||||||
dz is assumed to be in ascending order (starting at -ve values)
|
dz is assumed to be in ascending order (starting at -ve values)
|
||||||
"""
|
"""
|
||||||
camera = microscope.camera
|
camera: BaseCamera = microscope.camera
|
||||||
stage = microscope.stage
|
stage: BaseStage = microscope.stage
|
||||||
|
|
||||||
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
||||||
sharpnesses = []
|
sharpnesses: List[np.float] = []
|
||||||
positions = []
|
positions: List[int] = []
|
||||||
|
|
||||||
# Some cameras may not have annotate_text. Reset if it does
|
# Some cameras may not have annotate_text. Reset if it does
|
||||||
if getattr(camera, "annotate_text"):
|
if getattr(camera, "annotate_text"):
|
||||||
|
|
@ -198,29 +206,33 @@ class AutofocusExtension(BaseExtension):
|
||||||
|
|
||||||
for _ in stage.scan_z(dz, return_to_start=False):
|
for _ in stage.scan_z(dz, return_to_start=False):
|
||||||
if current_action() and current_action().stopped:
|
if current_action() and current_action().stopped:
|
||||||
return
|
return [], []
|
||||||
positions.append(stage.position[2])
|
positions.append(stage.position[2])
|
||||||
time.sleep(settle)
|
time.sleep(settle)
|
||||||
sharpnesses.append(self.measure_sharpness(microscope, metric_fn))
|
sharpnesses.append(self.measure_sharpness(microscope, metric_fn))
|
||||||
|
|
||||||
newposition = positions[np.argmax(sharpnesses)]
|
newposition: int = positions[int(np.argmax(sharpnesses))]
|
||||||
stage.move_rel((0, 0, newposition - stage.position[2]))
|
stage.move_rel((0, 0, newposition - stage.position[2]))
|
||||||
|
|
||||||
return positions, sharpnesses
|
return positions, sharpnesses
|
||||||
|
|
||||||
def move_and_find_focus(self, microscope: Microscope, dz: int):
|
def move_and_find_focus(self, microscope: Microscope, dz: int) -> int:
|
||||||
"""Make a relative Z move and return the peak sharpness position"""
|
"""Make a relative Z move and return the peak sharpness position"""
|
||||||
with monitor_sharpness(microscope) as m:
|
with monitor_sharpness(microscope) as m:
|
||||||
m.focus_rel(dz)
|
m.focus_rel(dz)
|
||||||
return m.sharpest_z_on_move(0)
|
return m.sharpest_z_on_move(0)
|
||||||
|
|
||||||
def move_and_measure(self, microscope: Microscope, dz: int):
|
def move_and_measure(
|
||||||
|
self, microscope: Microscope, dz: int
|
||||||
|
) -> Dict[str, np.ndarray]:
|
||||||
"""Make a relative Z move and return the sharpness data"""
|
"""Make a relative Z move and return the sharpness data"""
|
||||||
with monitor_sharpness(microscope) as m:
|
with monitor_sharpness(microscope) as m:
|
||||||
m.focus_rel(dz)
|
m.focus_rel(dz)
|
||||||
return m.data_dict()
|
return m.data_dict()
|
||||||
|
|
||||||
def fast_autofocus(self, microscope: Microscope, dz: int = 2000):
|
def fast_autofocus(
|
||||||
|
self, microscope: Microscope, dz: int = 2000
|
||||||
|
) -> Dict[str, np.ndarray]:
|
||||||
"""Perform a down-up-down-up autofocus"""
|
"""Perform a down-up-down-up autofocus"""
|
||||||
with microscope.camera.lock, microscope.stage.lock:
|
with microscope.camera.lock, microscope.stage.lock:
|
||||||
with monitor_sharpness(microscope) as m:
|
with monitor_sharpness(microscope) as m:
|
||||||
|
|
@ -231,7 +243,7 @@ class AutofocusExtension(BaseExtension):
|
||||||
# z: Final z position after move
|
# z: Final z position after move
|
||||||
i, z = m.focus_rel(dz)
|
i, z = m.focus_rel(dz)
|
||||||
# Get the z position with highest sharpness from the previous move (index i)
|
# Get the z position with highest sharpness from the previous move (index i)
|
||||||
fz = m.sharpest_z_on_move(i)
|
fz: int = m.sharpest_z_on_move(i)
|
||||||
# Move all the way to the start so it's consistent
|
# Move all the way to the start so it's consistent
|
||||||
# Store final absolute z position from this return move
|
# Store final absolute z position from this return move
|
||||||
i, z = m.focus_rel(-dz)
|
i, z = m.focus_rel(-dz)
|
||||||
|
|
@ -248,7 +260,7 @@ class AutofocusExtension(BaseExtension):
|
||||||
target_z: int = 0,
|
target_z: int = 0,
|
||||||
initial_move_up: bool = True,
|
initial_move_up: bool = True,
|
||||||
mini_backlash: int = 25,
|
mini_backlash: int = 25,
|
||||||
):
|
) -> Dict[str, np.ndarray]:
|
||||||
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
||||||
|
|
||||||
This autofocus method is very efficient, as it only passes the peak once.
|
This autofocus method is very efficient, as it only passes the peak once.
|
||||||
|
|
@ -294,11 +306,15 @@ class AutofocusExtension(BaseExtension):
|
||||||
m.focus_rel(dz / 2)
|
m.focus_rel(dz / 2)
|
||||||
# move down
|
# move down
|
||||||
logging.debug("Move down")
|
logging.debug("Move down")
|
||||||
|
i: int
|
||||||
|
z: int
|
||||||
i, z = m.focus_rel(-dz)
|
i, z = m.focus_rel(-dz)
|
||||||
# now inspect where the sharpest point is, and estimate the sharpness
|
# now inspect where the sharpest point is, and estimate the sharpness
|
||||||
# (JPEG size) that we should find at the start of the Z stack
|
# (JPEG size) that we should find at the start of the Z stack
|
||||||
|
jz: np.ndarray # np.ndarray[float]
|
||||||
|
js: np.ndarray # np.ndarray[float]
|
||||||
_, jz, js = m.move_data(i)
|
_, jz, js = m.move_data(i)
|
||||||
best_z = jz[np.argmax(js)]
|
best_z: int = jz[np.argmax(js)]
|
||||||
|
|
||||||
# now move to the start of the z stack
|
# now move to the start of the z stack
|
||||||
logging.debug("Move to the start of the z stack")
|
logging.debug("Move to the start of the z stack")
|
||||||
|
|
@ -309,17 +325,19 @@ class AutofocusExtension(BaseExtension):
|
||||||
# We've deliberately undershot - figure out how much further we should move based on the curve
|
# We've deliberately undershot - figure out how much further we should move based on the curve
|
||||||
logging.debug("Calculate remining movement")
|
logging.debug("Calculate remining movement")
|
||||||
current_js = m.camera.stream.last.size
|
current_js = m.camera.stream.last.size
|
||||||
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
imax: int = int(
|
||||||
|
np.argmax(js)
|
||||||
|
) # we want to crop out just the bit below the peak
|
||||||
js = js[imax:] # NB z is in DECREASING order
|
js = js[imax:] # NB z is in DECREASING order
|
||||||
jz = jz[imax:]
|
jz = jz[imax:]
|
||||||
inow = np.argmax(
|
inow: int = int(
|
||||||
js < current_js
|
np.argmax(js < current_js)
|
||||||
) # use the curve we recorded to estimate our position
|
) # use the curve we recorded to estimate our position
|
||||||
|
|
||||||
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
||||||
# That means we should move forwards, by best_z - zs[inow]
|
# That means we should move forwards, by best_z - zs[inow]
|
||||||
logging.debug("Correction move")
|
logging.debug("Correction move")
|
||||||
correction_move = best_z + target_z - jz[inow]
|
correction_move: int = best_z + target_z - jz[inow]
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Fast autofocus scan: correcting backlash by moving %s steps",
|
"Fast autofocus scan: correcting backlash by moving %s steps",
|
||||||
(correction_move),
|
(correction_move),
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,12 @@ def auto_expose_and_freeze_settings(camera: PiCamera):
|
||||||
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
|
||||||
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
|
||||||
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||||
channels: np.ndarray = np.zeros(
|
channels_shape: Tuple[int, ...] = (
|
||||||
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
|
4,
|
||||||
dtype=bayer_array.dtype,
|
bayer_array.shape[0] // 2,
|
||||||
|
bayer_array.shape[1] // 2,
|
||||||
)
|
)
|
||||||
|
channels: np.ndarray = np.zeros(channels_shape, dtype=bayer_array.dtype)
|
||||||
for i, offset in enumerate(bayer_pattern):
|
for i, offset in enumerate(bayer_pattern):
|
||||||
# We simplify life by dealing with only one channel at a time.
|
# We simplify life by dealing with only one channel at a time.
|
||||||
channels[i, :, :] = np.sum(
|
channels[i, :, :] = np.sum(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from .tools import devtools_extension_v2
|
from .tools import DevToolsExtension
|
||||||
|
|
||||||
LABTHINGS_EXTENSIONS = [devtools_extension_v2]
|
LABTHINGS_EXTENSIONS = [DevToolsExtension]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,17 @@ from labthings.extensions import BaseExtension
|
||||||
from labthings.views import ActionView
|
from labthings.views import ActionView
|
||||||
|
|
||||||
|
|
||||||
|
class DevToolsExtension(BaseExtension):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(
|
||||||
|
"org.openflexure.dev.tools",
|
||||||
|
version="0.1.0",
|
||||||
|
description="Actions to cause various traumatic events in the microscope, used for testing.",
|
||||||
|
)
|
||||||
|
self.add_view(RaiseException, "/raise")
|
||||||
|
self.add_view(SleepFor, "/sleep")
|
||||||
|
|
||||||
|
|
||||||
class RaiseException(ActionView):
|
class RaiseException(ActionView):
|
||||||
def post(self):
|
def post(self):
|
||||||
raise Exception("The developer raised an exception")
|
raise Exception("The developer raised an exception")
|
||||||
|
|
@ -23,13 +34,3 @@ class SleepFor(ActionView):
|
||||||
end = time.time()
|
end = time.time()
|
||||||
logging.info("Waking up!")
|
logging.info("Waking up!")
|
||||||
return {"TimeAsleep": (end - start)}
|
return {"TimeAsleep": (end - start)}
|
||||||
|
|
||||||
|
|
||||||
devtools_extension_v2 = BaseExtension(
|
|
||||||
"org.openflexure.dev.tools",
|
|
||||||
version="0.1.0",
|
|
||||||
description="Actions to cause various traumatic events in the microscope, used for testing.",
|
|
||||||
)
|
|
||||||
|
|
||||||
devtools_extension_v2.add_view(RaiseException, "/raise")
|
|
||||||
devtools_extension_v2.add_view(SleepFor, "/sleep")
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@ import io
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from collections import namedtuple
|
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import BinaryIO, List, Optional, Tuple, Type, Union
|
from typing import BinaryIO, List, NamedTuple, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from labthings import ClientEvent, StrictLock
|
from labthings import ClientEvent, StrictLock
|
||||||
|
|
||||||
|
|
||||||
# Class to store a frames metadata
|
# Class to store a frames metadata
|
||||||
TrackerFrame = namedtuple("TrackerFrame", ["size", "time"])
|
class TrackerFrame(NamedTuple):
|
||||||
|
size: int
|
||||||
|
time: float
|
||||||
|
|
||||||
|
|
||||||
class FrameStream(io.BytesIO):
|
class FrameStream(io.BytesIO):
|
||||||
|
|
@ -172,16 +174,17 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
def start_worker(self, **_) -> bool:
|
def start_worker(self, **_) -> bool:
|
||||||
"""Start the background camera thread if it isn't running yet."""
|
"""Start the background camera thread if it isn't running yet."""
|
||||||
logging.debug(
|
logging.warning(
|
||||||
"`start_worker` method has been deprecated and is no longer required. Please avoid calling this method."
|
"`start_worker` method has been deprecated and is no longer required. Please avoid calling this method."
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_frame(self):
|
def get_frame(self):
|
||||||
"""Return the current camera frame."""
|
"""
|
||||||
logging.debug(
|
Return the current camera frame.
|
||||||
"`camera.get_frame` method has been deprecated. Please use `camera.stream.getframe()."
|
|
||||||
)
|
Just an alias of self.stream.getframe()
|
||||||
|
"""
|
||||||
return self.stream.getframe()
|
return self.stream.getframe()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class SangaStage(BaseStage):
|
||||||
if "backlash" in config:
|
if "backlash" in config:
|
||||||
# Construct backlash array
|
# Construct backlash array
|
||||||
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
||||||
self.backlash = backlash
|
self.backlash = np.array(backlash)
|
||||||
if "settle_time" in config:
|
if "settle_time" in config:
|
||||||
self.settle_time = config.get("settle_time")
|
self.settle_time = config.get("settle_time")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
import base64
|
import base64
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
# TypedDict was added to typing in 3.8. Use typing_extensions for <3.8
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
from typing import TypedDict # pylint: disable=no-name-in-module
|
||||||
|
else:
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
|
||||||
class Timer(object):
|
class Timer(object):
|
||||||
def __init__(self, name):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.start = None
|
self.start: Optional[float] = None
|
||||||
self.end = None
|
self.end: Optional[float] = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.start = time.time()
|
self.start = time.time()
|
||||||
|
|
@ -22,19 +29,27 @@ class Timer(object):
|
||||||
logging.debug("%s time: %s", self.name, self.end - self.start)
|
logging.debug("%s time: %s", self.name, self.end - self.start)
|
||||||
|
|
||||||
|
|
||||||
def deserialise_array_b64(b64_string: str, dtype: str, shape: List[int]):
|
JSONArrayType = TypedDict(
|
||||||
flat_arr = np.frombuffer(base64.b64decode(b64_string), dtype)
|
"JSONArrayType",
|
||||||
|
{"@type": str, "base64": str, "dtype": str, "shape": Tuple[int, ...]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deserialise_array_b64(
|
||||||
|
b64_string: str, dtype: Union[Type[np.dtype], str], shape: Tuple[int, ...]
|
||||||
|
):
|
||||||
|
flat_arr: np.ndarray = np.frombuffer(base64.b64decode(b64_string), dtype)
|
||||||
return flat_arr.reshape(shape)
|
return flat_arr.reshape(shape)
|
||||||
|
|
||||||
|
|
||||||
def serialise_array_b64(npy_arr: np.ndarray):
|
def serialise_array_b64(npy_arr: np.ndarray) -> Tuple[str, str, Tuple[int, ...]]:
|
||||||
b64_string = base64.b64encode(npy_arr).decode("ascii")
|
b64_string: str = base64.b64encode(npy_arr.tobytes()).decode("ascii")
|
||||||
dtype = str(npy_arr.dtype)
|
dtype: str = str(npy_arr.dtype)
|
||||||
shape = npy_arr.shape
|
shape: Tuple[int, ...] = npy_arr.shape
|
||||||
return b64_string, dtype, shape
|
return b64_string, dtype, shape
|
||||||
|
|
||||||
|
|
||||||
def ndarray_to_json(arr: np.ndarray):
|
def ndarray_to_json(arr: np.ndarray) -> JSONArrayType:
|
||||||
if isinstance(arr, memoryview):
|
if isinstance(arr, memoryview):
|
||||||
# We can transparently convert memoryview objects to arrays
|
# We can transparently convert memoryview objects to arrays
|
||||||
# This comes in very handy for the lens shading table.
|
# This comes in very handy for the lens shading table.
|
||||||
|
|
@ -43,7 +58,7 @@ def ndarray_to_json(arr: np.ndarray):
|
||||||
return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string}
|
return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string}
|
||||||
|
|
||||||
|
|
||||||
def json_to_ndarray(json_dict: dict):
|
def json_to_ndarray(json_dict: JSONArrayType):
|
||||||
if not json_dict.get("@type") != "ndarray":
|
if not json_dict.get("@type") != "ndarray":
|
||||||
logging.warning("No valid @type attribute found. Conversion may fail.")
|
logging.warning("No valid @type attribute found. Conversion may fail.")
|
||||||
for required_param in ("dtype", "shape", "base64"):
|
for required_param in ("dtype", "shape", "base64"):
|
||||||
|
|
@ -52,7 +67,7 @@ def json_to_ndarray(json_dict: dict):
|
||||||
|
|
||||||
b64_string: Optional[str] = json_dict.get("base64")
|
b64_string: Optional[str] = json_dict.get("base64")
|
||||||
dtype: Optional[str] = json_dict.get("dtype")
|
dtype: Optional[str] = json_dict.get("dtype")
|
||||||
shape: Optional[List[int]] = json_dict.get("shape")
|
shape: Optional[Tuple[int, ...]] = json_dict.get("shape")
|
||||||
|
|
||||||
if b64_string and dtype and shape:
|
if b64_string and dtype and shape:
|
||||||
return deserialise_array_b64(b64_string, dtype, shape)
|
return deserialise_array_b64(b64_string, dtype, shape)
|
||||||
|
|
|
||||||
38
poetry.lock
generated
38
poetry.lock
generated
|
|
@ -278,7 +278,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importlib-metadata"
|
name = "importlib-metadata"
|
||||||
version = "3.1.0"
|
version = "3.1.1"
|
||||||
description = "Read metadata from Python packages"
|
description = "Read metadata from Python packages"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
|
@ -288,8 +288,8 @@ python-versions = ">=3.6"
|
||||||
zipp = ">=0.5"
|
zipp = ">=0.5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx", "rst.linker"]
|
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||||
testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"]
|
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
|
|
@ -336,7 +336,7 @@ i18n = ["Babel (>=0.8)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "labthings"
|
name = "labthings"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
description = "Python implementation of LabThings, based on the Flask microframework"
|
description = "Python implementation of LabThings, based on the Flask microframework"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
|
@ -421,6 +421,25 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy-stubs"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = ""
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
develop = false
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
numpy = ">=1.16.0"
|
||||||
|
typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/numpy/numpy-stubs.git"
|
||||||
|
reference = "master"
|
||||||
|
resolved_reference = "c49d2d6875971a669a166ea93ef998911af283a1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opencv-python-headless"
|
name = "opencv-python-headless"
|
||||||
version = "4.4.0.44"
|
version = "4.4.0.44"
|
||||||
|
|
@ -948,7 +967,7 @@ rpi = ["RPi.GPIO"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.7.3"
|
python-versions = "^3.7.3"
|
||||||
content-hash = "ec95ce987cd855cbcbdc4d7689dcbee7ca1f0154a60f5dddc3cecc02c92ddc75"
|
content-hash = "2c0faa244099ec3433edfc2cb9321f5de8bd5c0c32e135cae23f91e717cb07d7"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
|
|
@ -1082,8 +1101,8 @@ imagesize = [
|
||||||
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
|
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
|
||||||
]
|
]
|
||||||
importlib-metadata = [
|
importlib-metadata = [
|
||||||
{file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"},
|
{file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"},
|
||||||
{file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"},
|
{file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"},
|
||||||
]
|
]
|
||||||
iniconfig = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
|
|
@ -1102,8 +1121,8 @@ jinja2 = [
|
||||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||||
]
|
]
|
||||||
labthings = [
|
labthings = [
|
||||||
{file = "labthings-1.2.1-py3-none-any.whl", hash = "sha256:0b4bf4b7348cab8638bc9ec6028a48197f201410e352c866d131618a9877acbc"},
|
{file = "labthings-1.2.2-py3-none-any.whl", hash = "sha256:0f4fb76feed5ce9213d471f97700c4b52d9563f940954e1efb644889d3381d2d"},
|
||||||
{file = "labthings-1.2.1.tar.gz", hash = "sha256:f5f0fe319ef7aa2697e06f5969eb47dc834e7981c3726eab29472ee4f34c01ea"},
|
{file = "labthings-1.2.2.tar.gz", hash = "sha256:62a8c5bceea25883a7f329a8e19bd95d211d4fe8ac7eb39ee2a2bd51befa4bd5"},
|
||||||
]
|
]
|
||||||
lazy-object-proxy = [
|
lazy-object-proxy = [
|
||||||
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
||||||
|
|
@ -1219,6 +1238,7 @@ numpy = [
|
||||||
{file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"},
|
{file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"},
|
||||||
{file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"},
|
{file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"},
|
||||||
]
|
]
|
||||||
|
numpy-stubs = []
|
||||||
opencv-python-headless = [
|
opencv-python-headless = [
|
||||||
{file = "opencv-python-headless-4.4.0.44.tar.gz", hash = "sha256:6eefcacfb9b2da305277e1a93c7bf074dcd10b7aa154a0c963ded08fc0ffc02e"},
|
{file = "opencv-python-headless-4.4.0.44.tar.gz", hash = "sha256:6eefcacfb9b2da305277e1a93c7bf074dcd10b7aa154a0c963ded08fc0ffc02e"},
|
||||||
{file = "opencv_python_headless-4.4.0.44-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:0896413b35b4b64acae42b84f740a458e0520d51f806946adf185e710a2ab300"},
|
{file = "opencv_python_headless-4.4.0.44-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:0896413b35b4b64acae42b84f740a458e0520d51f806946adf185e710a2ab300"},
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,11 @@ expiringdict = "^1.2.1"
|
||||||
camera-stage-mapping = "0.1.4"
|
camera-stage-mapping = "0.1.4"
|
||||||
picamerax = ">=20.9.1"
|
picamerax = ">=20.9.1"
|
||||||
pyyaml = "^5.3.1"
|
pyyaml = "^5.3.1"
|
||||||
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
|
|
||||||
pytest-cov = "^2.10.1"
|
pytest-cov = "^2.10.1"
|
||||||
piexif = "^1.1.3"
|
piexif = "^1.1.3"
|
||||||
labthings = "1.2.1"
|
labthings = "1.2.2"
|
||||||
|
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
|
||||||
|
numpy-stubs = {git = "https://github.com/numpy/numpy-stubs.git"} # Needed for Numpy < 1.20
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
rpi = ["RPi.GPIO"]
|
rpi = ["RPi.GPIO"]
|
||||||
|
|
@ -66,7 +67,7 @@ poethepoet = "^0.9.0"
|
||||||
freezegun = "^1.0.0"
|
freezegun = "^1.0.0"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
exclude = '(\.eggs|\.git|\.venv|node_modules/)'
|
exclude = '(\.eggs|\.git|\.venv|\venv|node_modules/)'
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue