Static type analysis

This commit is contained in:
Joel Collins 2020-11-30 13:36:45 +00:00
parent 3aebb8bead
commit 7866ec0f47
63 changed files with 1825 additions and 2722 deletions

View file

@ -39,36 +39,44 @@ from openflexure_microscope.paths import (
logs_file_path,
)
# Custom RotatingFileHandler subclass
class CustomRotatingFileHandler(logging.handlers.RotatingFileHandler):
"""
A custom class for a rotating file log handler, with defaults we like.
1MB per file, maximum of 5 historic files.
Non-propagating logs (so we can separate access logs from error logs)
Optional debugging level.
"""
def __init__(self, filename: str, debug: bool = False) -> None:
super().__init__(filename, maxBytes=1_000_000, backupCount=5)
# Set formatter
self.setFormatter(
logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
)
# Never propagate
self.propagate = False
# Conditionally enable debugging
if debug:
self.setLevel(logging.DEBUG)
# Log files
ROOT_LOGFILE = logs_file_path("openflexure_microscope.log")
ACCESS_LOGFILE = logs_file_path("openflexure_microscope.access.log")
# Basic log format
formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
# Our WSGI server uses Werkzeug, so use that for the access log
access_log = logging.getLogger("werkzeug")
# Block the access logs from propagating up to the root logger
access_log.propagate = False
# Create error log file handler
fh = logging.handlers.RotatingFileHandler(
ROOT_LOGFILE, maxBytes=1_000_000, backupCount=5
)
fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG)
fh.propagate = False
fh = CustomRotatingFileHandler(ROOT_LOGFILE, debug=debug_app)
# Create access log file handler
afh = logging.handlers.RotatingFileHandler(
ACCESS_LOGFILE, maxBytes=1_000_000, backupCount=5
)
afh.setFormatter(formatter)
afh.setLevel(logging.DEBUG)
afh.propagate = False
afh = CustomRotatingFileHandler(ACCESS_LOGFILE, debug=debug_app)
# Add file handler to root logger
root_log.addHandler(fh)
access_log.addHandler(afh)

View file

@ -1,6 +1,7 @@
import logging
import time
from contextlib import contextmanager
from typing import Callable, List, Optional, Tuple
import numpy as np
from labthings import current_action, fields, find_component
@ -8,24 +9,27 @@ from labthings.extensions import BaseExtension
from labthings.views import ActionView, View
from scipy import ndimage
from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.devel import abort
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.utilities import set_properties
### Autofocus utilities
class JPEGSharpnessMonitor:
def __init__(self, microscope):
self.microscope = microscope
self.camera = microscope.camera
self.stage = microscope.stage
def __init__(self, microscope: Microscope):
self.microscope: Microscope = microscope
self.camera: BaseCamera = microscope.camera
self.stage: BaseStage = microscope.stage
self.recording_start_time = None
self.recording_start_time: Optional[float] = None
self.stage_positions = []
self.stage_times = []
self.jpeg_times = []
self.jpeg_sizes = []
self.stage_positions: List[Tuple[int, int, int]] = []
self.stage_times: List[float] = []
self.jpeg_times: List[float] = []
self.jpeg_sizes: List[int] = []
def start(self):
# Log the recording start time
@ -35,14 +39,14 @@ class JPEGSharpnessMonitor:
self.camera.stream.stop_tracking()
self.camera.stream.reset_tracking()
def focus_rel(self, dz, backlash=False, **kwargs):
def focus_rel(self, dz: int, backlash: bool = False, **kwargs):
# Store the start time and position
self.camera.stream.start_tracking()
self.stage_times.append(time.time())
self.stage_positions.append(self.stage.position)
# Main move
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
self.stage.move_rel((0, 0, dz), backlash=backlash, **kwargs)
# Store the end time and position
self.camera.stream.stop_tracking()
@ -63,7 +67,7 @@ class JPEGSharpnessMonitor:
final_z_position = self.stage_positions[-1][2]
return data_index, final_z_position
def move_data(self, istart, istop=None):
def move_data(self, istart: int, istop: Optional[int] = None):
"""Extract sharpness as a function of (interpolated) z"""
if istop is None:
istop = istart + 2
@ -88,7 +92,7 @@ class JPEGSharpnessMonitor:
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
def sharpest_z_on_move(self, index):
def sharpest_z_on_move(self, index: int):
"""Return the z position of the sharpest image on a given move"""
_, jz, js = self.move_data(index)
if len(js) == 0:
@ -105,23 +109,14 @@ class JPEGSharpnessMonitor:
return data
def decimate_to(shape, image):
"""Decimate an image to reduce its size if it's too big."""
decimation = np.max(
np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape))
)
return image[:: int(decimation), :: int(decimation), ...]
def sharpness_sum_lap2(rgb_image):
def sharpness_sum_lap2(rgb_image: np.ndarray):
"""Return an image sharpness metric: sum(laplacian(image)**")"""
# image_bw=np.mean(decimate_to((1000,1000), rgb_image),2)
image_bw = np.mean(rgb_image, 2)
image_lap = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float) ** 4)
def sharpness_edge(image):
def sharpness_edge(image: np.ndarray):
"""Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2)
n = 20
@ -134,13 +129,23 @@ def sharpness_edge(image):
### Autofocus extension
def measure_sharpness(microscope, metric_fn=sharpness_sum_lap2):
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2):
"""Measure the sharpness of the camera's current view."""
if hasattr(microscope.camera, "array"):
return metric_fn(microscope.camera.array(use_video_port=True))
if hasattr(microscope.camera, "array") and callable(
getattr(microscope.camera, "array")
):
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
def autofocus(
microscope: Microscope,
dz: List[int],
settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2,
):
"""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
@ -154,7 +159,10 @@ def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses = []
positions = []
camera.annotate_text = ""
# Some cameras may not have annotate_text. Reset if it does
if getattr(camera, "annotate_text"):
setattr(camera, "annotate_text", "")
for _ in stage.scan_z(dz, return_to_start=False):
if current_action() and current_action().stopped:
@ -164,13 +172,13 @@ def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
sharpnesses.append(measure_sharpness(microscope, metric_fn))
newposition = positions[np.argmax(sharpnesses)]
stage.move_rel([0, 0, newposition - stage.position[2]])
stage.move_rel((0, 0, newposition - stage.position[2]))
return positions, sharpnesses
@contextmanager
def monitor_sharpness(microscope):
def monitor_sharpness(microscope: Microscope):
m = JPEGSharpnessMonitor(microscope)
m.start()
try:
@ -179,21 +187,21 @@ def monitor_sharpness(microscope):
m.stop()
def move_and_find_focus(microscope, dz):
def move_and_find_focus(microscope: Microscope, dz: int):
"""Make a relative Z move and return the peak sharpness position"""
with monitor_sharpness(microscope) as m:
m.focus_rel(dz)
return m.sharpest_z_on_move(0)
def move_and_measure(microscope, dz):
def move_and_measure(microscope: Microscope, dz: int):
"""Make a relative Z move and return the sharpness data"""
with monitor_sharpness(microscope) as m:
m.focus_rel(dz)
return m.data_dict()
def fast_autofocus(microscope, dz=2000):
def fast_autofocus(microscope: Microscope, dz: int = 2000):
"""Perform a down-up-down-up autofocus"""
with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m:
@ -216,7 +224,11 @@ def fast_autofocus(microscope, dz=2000):
def fast_up_down_up_autofocus(
microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=25
microscope: Microscope,
dz: int = 2000,
target_z: int = 0,
initial_move_up: bool = True,
mini_backlash: int = 25,
):
"""Autofocus by measuring on the way down, and moving back up with feedback.
@ -256,7 +268,7 @@ def fast_up_down_up_autofocus(
with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m:
# Ensure the MJPEG stream has started
microscope.camera.start_stream_recording()
microscope.camera.start_stream()
logging.debug("Initial move")
if initial_move_up:

View file

@ -1,5 +1,6 @@
import logging
import os
from typing import Dict, List, Optional, Tuple
import psutil
from flask import abort
@ -9,32 +10,43 @@ from labthings.marshalling import use_args
from labthings.views import PropertyView, View
from openflexure_microscope.api.utilities.gui import build_gui
from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH
from openflexure_microscope.captures.capture_manager import (
BASE_CAPTURE_PATH,
CaptureManager,
)
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.paths import check_rw, settings_file_path
AS_SETTINGS_PATH = settings_file_path("autostorage_settings.json")
def get_partitions():
def get_partitions() -> List[str]:
return [disk.mountpoint for disk in psutil.disk_partitions() if "rw" in disk.opts]
def get_permissive_partitions():
def get_permissive_partitions() -> List[str]:
return [partition for partition in get_partitions() if check_rw(partition)]
def get_permissive_locations():
def get_permissive_locations() -> List[Tuple[str, str]]:
return [
(partition, os.path.join(partition, "openflexure", "data", "micrographs"))
for partition in get_permissive_partitions()
]
def get_current_location(capture_manager):
return capture_manager.paths.get("default")
def get_current_location(capture_manager: Optional[CaptureManager]) -> str:
if capture_manager:
return capture_manager.paths.get("default", BASE_CAPTURE_PATH)
return BASE_CAPTURE_PATH
def set_current_location(capture_manager, location: str):
def set_current_location(capture_manager: Optional[CaptureManager], location: str):
if not capture_manager:
logging.warning(
"Cannot set_current_location of a missing capture_manager. Skipping."
)
return
if not os.path.isdir(location):
os.makedirs(location)
logging.debug("Updating location...")
@ -44,12 +56,12 @@ def set_current_location(capture_manager, location: str):
logging.debug("Capture location changed successfully.")
def get_default_location():
def get_default_location() -> str:
return BASE_CAPTURE_PATH
def get_all_locations():
locations = {}
def get_all_locations() -> Dict[str, str]:
locations: Dict[str, str] = {}
# If default is not already listed (e.g. if it's currently set)
if get_default_location() not in locations.values():
locations["Default"] = get_default_location()
@ -71,29 +83,23 @@ def get_all_locations():
return {k: v for k, v in locations.items() if v}
class CaptureStorageLocation:
def __init__(self, mountpoint):
pass
class AutostorageExtension(BaseExtension):
def __init__(self):
BaseExtension.__init__(
self,
super().__init__(
"org.openflexure.autostorage",
version="2.0.0",
description="Handle switching capture storage devices",
)
# We'll store a reference to a CaptureManager object, who's capture paths will be modified
self.capture_manager = None
self.capture_manager: Optional[CaptureManager] = None
self.initial_location = get_default_location()
self.initial_location: str = get_default_location()
# Register the on_microscope function to run when the microscope is attached
self.on_component("org.openflexure.microscope", self.on_microscope)
def on_microscope(self, microscope_obj):
def on_microscope(self, microscope_obj: Microscope):
"""Function to automatically call when the parent LabThing has a microscope attached."""
logging.debug("Autostorage extension found microscope %s", microscope_obj)
if hasattr(microscope_obj, "captures"):
@ -110,10 +116,15 @@ class AutostorageExtension(BaseExtension):
self.check_location(self.initial_location)
logging.debug(self.get_locations())
else:
raise RuntimeError(
"Attached a microscope with no `captures` capture manager. Skipping extension."
)
def check_location(self, location=None):
def check_location(self, location: Optional[str] = None) -> bool:
location = location or get_current_location(self.capture_manager)
if not location:
location = get_current_location(self.capture_manager)
return False
# If preferred path does not exist, or cannot be written to
if not (os.path.isdir(location) and check_rw(location)):
logging.error(
@ -122,8 +133,9 @@ class AutostorageExtension(BaseExtension):
)
# Reset the storage location to default
set_current_location(self.capture_manager, get_default_location())
return True
def get_locations(self):
def get_locations(self) -> Dict[str, str]:
if self.capture_manager:
locations = get_all_locations()
@ -135,7 +147,7 @@ class AutostorageExtension(BaseExtension):
else:
return {}
def get_preferred_key(self):
def get_preferred_key(self) -> Optional[str]:
current = get_current_location(self.capture_manager)
locations = self.get_locations()
@ -146,22 +158,28 @@ class AutostorageExtension(BaseExtension):
"Multiple path matches found. Weird, but carrying on using zeroth."
)
return matches[0]
if matches:
return matches[0]
else:
logging.warning("No matches found. Skipping.")
return None
def set_preferred_key(self, new_path_key: str):
if not new_path_key in self.get_locations().keys():
raise KeyError(f"No location named {new_path_key}")
location = self.get_locations().get(new_path_key)
set_current_location(self.capture_manager, location)
if location:
set_current_location(self.capture_manager, location)
def key_to_title(self, path_key: str):
def key_to_title(self, path_key: Optional[str]) -> Optional[str]:
if not path_key:
return None
if not path_key in self.get_locations().keys():
raise KeyError(f"No location named {path_key}")
return f"{path_key} ({self.get_locations().get(path_key)})"
def title_to_key(self, path_title: str):
def title_to_key(self, path_title: str) -> str:
matches = []
for loc_key in self.get_locations().keys():
if path_title.startswith(loc_key):
@ -174,10 +192,15 @@ class AutostorageExtension(BaseExtension):
return matches[0]
def get_titles(self):
return [self.key_to_title(key) for key in self.get_locations().keys()]
def get_titles(self) -> List[str]:
titles: List[str] = []
for key in self.get_locations().keys():
title: Optional[str] = self.key_to_title(key)
if title:
titles.append(title)
return titles
def get_preferred_title(self):
def get_preferred_title(self) -> Optional[str]:
return self.key_to_title(self.get_preferred_key())

View file

@ -1,14 +1,15 @@
import logging
from contextlib import contextmanager
# Type hinting
from typing import Tuple
import picamerax
from flask import abort
from labthings import find_component
from labthings.extensions import BaseExtension
from labthings.views import ActionView
from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.microscope import Microscope
from .recalibrate_utils import (
auto_expose_and_freeze_settings,
flat_lens_shading_table,
@ -17,7 +18,7 @@ from .recalibrate_utils import (
@contextmanager
def pause_stream(scamera, resolution: Tuple[int, int] = None):
def pause_stream(scamera: BaseCamera):
"""This context manager locks a streaming camera, and pauses the stream.
The stream is re-enabled, with the original resolution, once the with
@ -28,20 +29,20 @@ def pause_stream(scamera, resolution: Tuple[int, int] = None):
not scamera.record_active
), "We can't pause the camera's video stream while a recording is in progress."
streaming = scamera.stream_active
old_resolution = scamera.camera.resolution
old_resolution = scamera.stream_resolution
if streaming:
logging.info("Stopping stream in pause_stream context manager")
scamera.stop_stream_recording(resolution=resolution)
scamera.stop_stream()
try:
yield scamera
finally:
scamera.camera.resolution = old_resolution
scamera.stream_resolution = old_resolution
if streaming:
logging.info("Restarting stream in pause_stream context manager")
scamera.start_stream_recording()
scamera.start_stream()
def recalibrate(microscope):
def recalibrate(microscope: Microscope):
"""Reset the camera's settings.
This generates new gains, exposure time, and lens shading
@ -49,11 +50,15 @@ def recalibrate(microscope):
with a gray level of 230. It takes a little while to run.
"""
with pause_stream(microscope.camera) as scamera:
auto_expose_and_freeze_settings(
scamera.camera
) # scamera.camera is the PiCamera object
recalibrate_camera(scamera.camera)
microscope.save_settings()
if hasattr(scamera, "picamera"):
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera")
auto_expose_and_freeze_settings(picamera_obj)
recalibrate_camera(picamera_obj)
microscope.save_settings()
else:
raise RuntimeError(
"Recalibrate can only be used with a Raspberry Pi camera"
)
class RecalibrateView(ActionView):

View file

@ -1,19 +1,21 @@
import logging
import time
from fractions import Fraction
from typing import List, Optional, Tuple
import numpy as np
from picamerax import PiCamera
from picamerax.array import PiBayerArray, PiRGBArray
def rgb_image(camera, resize=None, **kwargs):
def rgb_image(camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs):
"""Capture an image and return an RGB numpy array"""
with PiRGBArray(camera, size=resize) as output:
camera.capture(output, format="rgb", resize=resize, **kwargs)
return output.array
def flat_lens_shading_table(camera):
def flat_lens_shading_table(camera: PiCamera):
"""Return a flat (i.e. unity gain) lens shading table.
This is mostly useful because it makes it easy to get the size
@ -24,14 +26,11 @@ def flat_lens_shading_table(camera):
raise ImportError(
"This program requires the forked picamera library with lens shading support"
)
return (
# pylint: disable=W0212
np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8)
+ 32
)
# pylint: disable=protected-access
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
def adjust_exposure_to_setpoint(camera, setpoint):
def adjust_exposure_to_setpoint(camera: PiCamera, setpoint: int):
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
for _ in range(3):
@ -43,7 +42,7 @@ def adjust_exposure_to_setpoint(camera, setpoint):
print("done")
def auto_expose_and_freeze_settings(camera):
def auto_expose_and_freeze_settings(camera: PiCamera):
"""Freeze the settings after auto-exposing to white illumination"""
logging.info("Allowing the camera to auto-expose")
if "greyworld" in camera.AWB_MODES:
@ -66,7 +65,7 @@ def auto_expose_and_freeze_settings(camera):
logging.info("Shutter speed = %s", (camera.shutter_speed))
camera.exposure_mode = "off"
logging.info("Auto exposure disabled")
g = camera.awb_gains
g: Tuple[Fraction, Fraction] = camera.awb_gains
camera.awb_mode = "off"
camera.awb_gains = g
logging.info("Auto white balance disabled, gains are %s", (g))
@ -76,10 +75,10 @@ def auto_expose_and_freeze_settings(camera):
adjust_exposure_to_setpoint(camera, 215)
def channels_from_bayer_array(bayer_array):
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern = [(i // 2, i % 2) for i in range(4)]
channels = np.zeros(
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
channels: np.ndarray = np.zeros(
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
dtype=bayer_array.dtype,
)
@ -92,14 +91,19 @@ def channels_from_bayer_array(bayer_array):
return channels
def lst_from_channels(channels):
def lst_from_channels(channels: np.ndarray) -> np.ndarray:
"""Given the 4 Bayer colour channels from a white image, generate a LST."""
full_resolution = np.array(channels.shape[1:]) * 2 # channels have been binned
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
lst_resolution = [(r // 64) + 1 for r in full_resolution]
# NB the size of the LST is 1/64th of the image, but rounded UP.
full_resolution: np.ndarray = np.array(
channels.shape[1:]
) * 2 # channels have been binned
# NOTE: the size of the LST is 1/64th of the image, but rounded UP.
lst_resolution: List[int] = [(r // 64) + 1 for r in full_resolution]
logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float)
lens_shading: np.ndarray = np.zeros(
[channels.shape[0]] + lst_resolution, dtype=np.float
)
for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :]
iw, ih = image_channel.shape
@ -153,7 +157,7 @@ def lst_from_channels(channels):
return lens_shading_table[::-1, :, :].copy()
def recalibrate_camera(camera):
def recalibrate_camera(camera: PiCamera):
"""Reset the lens shading table and exposure settings.
This method first resets to a flat lens shading table, then auto-exposes,

View file

@ -3,7 +3,7 @@ import logging
import time
import uuid
from functools import reduce
from typing import Tuple
from typing import Dict, List, Optional, Tuple
from labthings import (
current_action,
@ -14,20 +14,32 @@ from labthings import (
)
from labthings.extensions import BaseExtension
from labthings.views import ActionView
from typing_extensions import Literal
from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
from openflexure_microscope.captures.capture_manager import generate_basename
from openflexure_microscope.devel import abort
from openflexure_microscope.microscope import Microscope
# Type alias for convenience
XyCoordinate = Tuple[int, int]
XyzCoordinate = Tuple[int, int, int]
### Grid construction
def construct_grid(initial, step_sizes, n_steps, style="raster"):
def construct_grid(
initial: XyCoordinate,
step_sizes: XyCoordinate,
n_steps: XyCoordinate,
style: Literal["raster", "snake", "spiral"] = "raster",
):
"""
Given an initial position, step sizes, and number of steps,
construct a 2-dimensional list of scan x-y positions.
"""
arr = []
arr: List[List[XyCoordinate]] = [] # 2D array of coordinates
if style == "spiral":
# deal with the centre image immediately
@ -35,31 +47,48 @@ def construct_grid(initial, step_sizes, n_steps, style="raster"):
arr.append([initial])
# for spiral, n_steps is the number of shells, and so only requires n_steps[0]
for i in range(2, n_steps[0] + 1):
arr.append([])
arr.append([]) # Append new shell holder
side_length = (2 * i) - 1
# iteratively generate the next location to append
coord = [coord[ax] + [-1, 1][ax] * step_sizes[ax] for ax in range(2)]
# Iteratively generate the next location to append
# Start coordinate of the shell
# We create a copy of coord so that the new value of coord doesn't depend on itself
# Otherwise we create a generator, not a tuple, which makes type checking angry
last_coordinate: XyCoordinate = coord
coord = (
last_coordinate[0] + [-1, 1][0] * step_sizes[0],
last_coordinate[1] + [-1, 1][1] * step_sizes[1],
)
for direction in ([1, 0], [0, -1], [-1, 0], [0, 1]):
for _ in range(side_length - 1):
coord = [
coord[ax] + direction[ax] * step_sizes[ax] for ax in range(2)
]
arr[i - 1].append(tuple(coord))
last_coordinate = coord
coord = (
last_coordinate[0] + direction[0] * step_sizes[0],
last_coordinate[1] + direction[1] * step_sizes[1],
)
arr[i - 1].append(coord)
# If raster or snake
else:
for i in range(n_steps[0]): # x axis
arr.append([])
for j in range(n_steps[1]): # y axis
# Create a coordinate array
coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)]
# Create a coordinate tuple
coord = (
initial[0] + [i, j][0] * step_sizes[0],
initial[1] + [i, j][1] * step_sizes[1],
)
# Append coordinate array to position grid
arr[i].append(tuple(coord))
arr[i].append(coord)
# Style modifiers
if style == "snake":
# For each line (row) in the coordinate array
for i, line in enumerate(arr):
# If it's an odd row
if i % 2 != 0:
# Reverse the list of coordinates
line.reverse()
return arr
@ -76,16 +105,17 @@ class ScanExtension(BaseExtension):
def capture(
self,
microscope,
basename,
microscope: Microscope,
basename: Optional[str],
namemode: str = "coordinates",
temporary: bool = False,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
resize: Optional[Tuple[int, int]] = None,
bayer: bool = False,
metadata: dict = None,
annotations: dict = None,
tags: list = None,
metadata: Optional[dict] = None,
annotations: Optional[Dict[str, str]] = None,
tags: Optional[List[str]] = None,
dataset: Optional[Dict[str, str]] = None,
):
metadata = metadata or {}
annotations = annotations or {}
@ -113,6 +143,7 @@ class ScanExtension(BaseExtension):
bayer=bayer,
annotations=annotations,
tags=tags,
dataset=dataset,
metadata=metadata,
cache_key=folder,
)
@ -125,21 +156,21 @@ class ScanExtension(BaseExtension):
### Scanning
def tile(
self,
microscope,
basename: str = None,
microscope: Microscope,
basename: Optional[str] = None,
namemode: str = "coordinates",
temporary: bool = False,
stride_size: list = (2000, 1500, 100),
grid: list = (3, 3, 5),
stride_size: XyzCoordinate = (2000, 1500, 100),
grid: XyzCoordinate = (3, 3, 5),
style="raster",
autofocus_dz: int = 50,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
resize: Optional[Tuple[int, int]] = None,
bayer: bool = False,
fast_autofocus=False,
metadata: dict = None,
annotations: dict = None,
tags: list = None,
fast_autofocus: bool = False,
metadata: Optional[dict] = None,
annotations: Optional[Dict[str, str]] = None,
tags: Optional[List[str]] = None,
):
metadata = metadata or {}
annotations = annotations or {}
@ -160,16 +191,14 @@ class ScanExtension(BaseExtension):
# Add dataset metadata
dataset_d = {
"dataset": {
"id": uuid.uuid4(),
"type": "xyzScan",
"name": basename,
"acquisitionDate": datetime.datetime.now().isoformat(),
"strideSize": stride_size,
"grid": grid,
"style": style,
"autofocusDz": autofocus_dz,
}
"id": uuid.uuid4(),
"type": "xyzScan",
"name": basename,
"acquisitionDate": datetime.datetime.now().isoformat(),
"strideSize": stride_size,
"grid": grid,
"style": style,
"autofocusDz": autofocus_dz,
}
# Check if autofocus is enabled
@ -186,7 +215,7 @@ class ScanExtension(BaseExtension):
# Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(
initial_position, stride_size[:2], grid[:2], style=style
initial_position[:2], stride_size[:2], grid[:2], style=style
)
# Keep the initial Z position the same as our current position
@ -201,13 +230,13 @@ class ScanExtension(BaseExtension):
next_z = initial_z # Reset z position at start of each new row
logging.debug("Returning to initial z position")
microscope.stage.move_abs(
[line[0][0], line[0][1], next_z]
(line[0][0], line[0][1], next_z)
) # RWB: I think this line is redundant
for x_y in line:
# Move to new grid position without changing z
logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
microscope.stage.move_abs((x_y[0], x_y[1], next_z))
# Refocus
if autofocus_enabled:
if fast_autofocus:
@ -232,7 +261,7 @@ class ScanExtension(BaseExtension):
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=dataset_d,
dataset=dataset_d,
annotations=annotations,
tags=tags,
)
@ -251,7 +280,7 @@ class ScanExtension(BaseExtension):
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=dataset_d,
dataset=dataset_d,
annotations=annotations,
tags=tags,
)
@ -268,19 +297,20 @@ class ScanExtension(BaseExtension):
def stack(
self,
microscope,
basename: str = None,
microscope: Microscope,
basename: Optional[str] = None,
namemode: str = "coordinates",
temporary: bool = False,
step_size: int = 100,
steps: int = 5,
return_to_start: bool = True,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
resize: Optional[Tuple[int, int]] = None,
bayer: bool = False,
metadata: dict = None,
annotations: dict = None,
tags: list = None,
metadata: Optional[dict] = None,
annotations: Optional[Dict[str, str]] = None,
dataset: Optional[Dict[str, str]] = None,
tags: Optional[List[str]] = None,
):
metadata = metadata or {}
annotations = annotations or {}
@ -293,7 +323,7 @@ class ScanExtension(BaseExtension):
with microscope.lock:
# Move to center scan
logging.debug("Moving to z-stack starting position")
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
microscope.stage.move_rel((0, 0, int((-step_size * steps) / 2)))
logging.debug("Starting scan from position %s", microscope.stage.position)
for i in range(steps):
@ -309,6 +339,7 @@ class ScanExtension(BaseExtension):
bayer=bayer,
metadata=metadata,
annotations=annotations,
dataset=dataset,
tags=tags,
)
# Update task progress
@ -319,7 +350,7 @@ class ScanExtension(BaseExtension):
if i != steps - 1:
logging.debug("Moving z by %s", (step_size))
microscope.stage.move_rel([0, 0, step_size])
microscope.stage.move_rel((0, 0, step_size))
if return_to_start:
logging.debug("Returning to %s", (initial_position))
microscope.stage.move_abs(initial_position)

View file

@ -3,6 +3,7 @@ import os
import tempfile
import uuid
import zipfile
from typing import IO, List, Optional
from flask import abort, send_file, url_for
from labthings import fields, find_component, update_action_progress
@ -11,6 +12,9 @@ from labthings.schema import Schema, pre_dump
from labthings.utilities import description_from_view
from labthings.views import ActionView, PropertyView, View
from openflexure_microscope.captures import CaptureObject
from openflexure_microscope.microscope import Microscope
class ZipObjectSchema(Schema):
id = fields.String()
@ -32,11 +36,13 @@ class ZipObjectSchema(Schema):
class ZipObjectDescription:
def __init__(self, id_, file_pointer, data_size=None):
self.id = id_
self.fp = file_pointer
self.data_size = data_size
self.zip_size = os.path.getsize(self.fp.name) * 1e-6
def __init__(
self, id_: str, file_pointer: IO[bytes], data_size: Optional[float] = None
):
self.id: str = id_
self.fp: IO[bytes] = file_pointer
self.data_size: Optional[float] = data_size
self.zip_size: float = os.path.getsize(self.fp.name) * 1e-6
def close(self):
logging.debug(self.fp.name)
@ -60,22 +66,26 @@ class ZipManager:
self.session_zips = {}
def build_zip_from_capture_ids(self, microscope, capture_id_list):
def build_zip_from_capture_ids(
self, microscope: Microscope, capture_id_list: List[str]
):
logging.debug(capture_id_list)
# Get array of captures from IDs
capture_list = [
optional_capture_list: List[Optional[CaptureObject]] = [
microscope.captures.images.get(capture_id) for capture_id in capture_id_list
]
# Remove Nones from list (missing/invalid captures)
capture_list = [capture for capture in capture_list if capture]
capture_list: List[CaptureObject] = [
capture for capture in optional_capture_list if capture
]
# Get size (in bytes) of each capture
capture_sizes = [
capture_sizes: List[float] = [
os.path.getsize(capture_obj.file) for capture_obj in capture_list
]
# Calculate size of input data in megabytes
data_size_megabytes = sum(capture_sizes) * 1e-6
data_size_megabytes: float = sum(capture_sizes) * 1e-6
# If more than 1GB
if data_size_megabytes > 1000:
@ -85,10 +95,10 @@ class ZipManager:
)
# Number of files to add (used for task progress)
n_files = len(capture_id_list)
n_files: int = len(capture_id_list)
# Create temporary file
fp = tempfile.NamedTemporaryFile(delete=False)
fp: IO[bytes] = tempfile.NamedTemporaryFile(delete=False)
# Open temp file as a ZIP file
with zipfile.ZipFile(fp, "w") as zipObj:
@ -102,8 +112,8 @@ class ZipManager:
# Update task progress
update_action_progress(int((index / n_files) * 100))
session_id = str(uuid.uuid4())
session_description = ZipObjectDescription(
session_id: str = str(uuid.uuid4())
session_description: ZipObjectDescription = ZipObjectDescription(
session_id, fp, data_size=data_size_megabytes
)
self.session_zips[session_id] = session_description
@ -113,7 +123,7 @@ class ZipManager:
def marshaled_build_zip_from_capture_ids(self, *args, **kwargs):
return ZipObjectSchema().dump(self.build_zip_from_capture_ids(*args, **kwargs))
def zip_fp_from_id(self, session_id):
def zip_fp_from_id(self, session_id: str):
return self.session_zips[session_id].fp
def __del__(self):

View file

@ -16,7 +16,7 @@ class SleepFor(ActionView):
args = {"time": fields.Float(description="Time to sleep, in seconds", example=0.5)}
def post(self, args):
sleep_time = args.get("time")
sleep_time: int = args.get("time", 0)
logging.info("Going to sleep for %s...", sleep_time)
start = time.time()
time.sleep(sleep_time)

View file

@ -2917,6 +2917,16 @@
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"browserslist": {
"version": "4.14.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
@ -2962,12 +2972,36 @@
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
"dev": true
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"electron-to-chromium": {
"version": "1.3.592",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
"dev": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true,
"optional": true
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -3001,6 +3035,35 @@
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@ -3134,6 +3197,16 @@
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"terser-webpack-plugin": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@ -3160,6 +3233,31 @@
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.0",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz",
"integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
}
}
},
@ -13759,104 +13857,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-rc.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.2.tgz",
"integrity": "sha512-cz8GK4dgIf1UTC+do80pGvh8BHcCRHLIQVHV9ONVQ8wtoqS9t/+H02rKcQP+TVNg7khgLyQV2+8eHUq7/AFq3g==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",

View file

@ -80,6 +80,10 @@ export default {
type: String,
required: true
},
captures: {
type: Array,
required: true
},
tags: {
type: Array,
required: false,
@ -92,7 +96,7 @@ export default {
computed: {
allURLs: function() {
var urls = [];
for (var capture of this.scanState.captures) {
for (var capture of this.captures) {
urls.push(capture.links.self.href);
}
return urls;
@ -115,6 +119,7 @@ export default {
deleteAll: function() {
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
console.log("Delete finished")
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
});

View file

@ -105,6 +105,7 @@
:time="item.time"
:tags="item.tags"
:type="item.type"
:captures="item.captures"
:thumbnail="item.thumbnail"
@selectFolder="selectFolder"
/>

View file

@ -1,38 +1,51 @@
import errno
import logging
import os
from typing import Union
from flask import Blueprint, current_app, url_for
from werkzeug.exceptions import BadRequest
from flask import Blueprint, Flask, current_app, url_for
from flask.views import View
from . import gui
__all__ = [
"gui",
"view_class_from_endpoint",
"blueprint_for_module",
"get_bool",
"list_routes",
"create_file",
"init_default_extensions",
]
def view_class_from_endpoint(endpoint: str):
def view_class_from_endpoint(endpoint: str) -> View:
return current_app.view_functions[endpoint].view_class
def blueprint_for_module(module_name, api_ver=2, suffix=""):
def blueprint_for_module(module_name: str, api_ver: int = 2, suffix: str = ""):
return Blueprint(
blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix),
module_name,
)
def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
def blueprint_name_for_module(module_name: str, api_ver: int = 2, suffix: str = ""):
bp_name = module_name.split(".")[-1]
return f"v{api_ver}_{bp_name}_blueprint{suffix}"
def get_bool(get_arg):
def get_bool(get_arg: Union[bool, str]):
"""Convert GET request argument string to a Python bool"""
if get_arg == "true" or get_arg == "True" or get_arg == "1":
if isinstance(get_arg, bool):
return get_arg
elif get_arg == "true" or get_arg == "True" or get_arg == "1":
return True
else:
return False
def list_routes(app):
def list_routes(app: Flask):
output = {}
for rule in app.url_map.iter_rules():
@ -49,7 +62,7 @@ def list_routes(app):
return output
def create_file(config_path):
def create_file(config_path: str):
if not os.path.exists(os.path.dirname(config_path)):
try:
os.makedirs(os.path.dirname(config_path))
@ -58,7 +71,7 @@ def create_file(config_path):
raise
def init_default_extensions(extension_dir):
def init_default_extensions(extension_dir: str):
os.makedirs(extension_dir, exist_ok=True)
default_ext_path = os.path.join(extension_dir, "defaults.py")

View file

@ -1,6 +1,9 @@
import copy
import logging
from functools import wraps
from typing import Callable, Union
from labthings.extensions import BaseExtension
def clean_rule(rule: str):
@ -9,13 +12,12 @@ def clean_rule(rule: str):
return f"{rule}"
def build_gui_from_dict(gui_description, extension_object):
def build_gui_from_dict(gui_description: dict, extension_object: BaseExtension):
# Make a working copy of GUI description
api_gui = copy.deepcopy(gui_description)
# Grab the extensions rules dictionary
# NOTE: The LabThings extension object should probably have a non-private
# way to get the rules dictionary. For now we need to ignore pylint W0212
ext_rules = extension_object._rules # pylint: disable=W0212
# TODO: Public property should be added to LabThings
ext_rules = extension_object._rules # pylint: disable=protected-access
# Expand shorthand routes into full relative URLs
if "forms" in gui_description and isinstance(api_gui["forms"], list):
@ -35,7 +37,7 @@ def build_gui_from_dict(gui_description, extension_object):
return api_gui
def build_gui_from_func(func, extension_object):
def build_gui_from_func(func: Callable, extension_object: BaseExtension):
@wraps(func)
def wrapped(*args, **kwargs):
return build_gui_from_dict(func(*args, **kwargs), extension_object)
@ -43,7 +45,7 @@ def build_gui_from_func(func, extension_object):
return wrapped
def build_gui(gui_description, extension_object):
def build_gui(gui_description: Union[dict, Callable], extension_object: BaseExtension):
# If given a function that generates a GUI dictionary
if callable(gui_description):
# Wrap in the route expander

View file

@ -1,5 +1,6 @@
import io
import logging
from typing import Dict, Optional, Tuple
from flask import send_file
from labthings import Schema, fields, find_component
@ -42,12 +43,14 @@ class CaptureAPI(ActionView):
"""
microscope = find_component("org.openflexure.microscope")
resize = args.get("resize", None)
if resize:
resize = (
int(resize["width"]),
int(resize["height"]),
resize_dict: Optional[Dict[str, int]] = args.get("resize", None)
if resize_dict:
resize: Optional[Tuple[int, int]] = (
int(resize_dict["width"]),
int(resize_dict["height"]),
) # Convert dict to tuple
else:
resize = None
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
with microscope.camera.lock:
@ -76,12 +79,14 @@ class RAMCaptureAPI(ActionView):
"""
microscope = find_component("org.openflexure.microscope")
resize = args.get("resize", None)
if resize:
resize = (
int(resize["width"]),
int(resize["height"]),
resize_dict: Optional[Dict[str, int]] = args.get("resize", None)
if resize_dict:
resize: Optional[Tuple[int, int]] = (
int(resize_dict["width"]),
int(resize_dict["height"]),
) # Convert dict to tuple
else:
resize = None
# Open a BytesIO stream to be destroyed once request has returned
with microscope.camera.lock, io.BytesIO() as stream:
@ -113,15 +118,18 @@ class GPUPreviewStartAPI(ActionView):
"""
microscope = find_component("org.openflexure.microscope")
window = args.get("window")
logging.debug(window)
# Get window argument from request
window_arg = args.get("window")
logging.debug(window_arg)
if len(window) != 4:
fullscreen = True
window = None
else:
# Default to no window
fullscreen: bool = True
window: Optional[Tuple[int, int, int, int]] = None
# If request argument is well formed, use that
if len(window_arg) == 4:
fullscreen = False
window = [int(w) for w in window]
window = (int(w) for w in window_arg)
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
return microscope.state

View file

@ -1,4 +1,5 @@
import logging
from typing import List, Tuple
from labthings import fields, find_component
from labthings.views import ActionView
@ -24,11 +25,11 @@ class MoveStageAPI(ActionView):
# 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 = axes_to_array(args, ["x", "y", "z"])
target_position: List[int] = axes_to_array(args, ["x", "y", "z"])
logging.debug("TARGET: %s", (target_position))
position = [
position: Tuple[int, int, int] = (
target_position[i] - microscope.stage.position[i] for i in range(3)
]
)
logging.debug("DELTA: %s", (position))
else:

View file

@ -4,7 +4,7 @@ import subprocess
from labthings.views import ActionView
def is_raspberrypi():
def is_raspberrypi() -> bool:
"""
Checks if Raspberry Pi.
"""

View file

@ -34,17 +34,18 @@ class LSTImageProperty(PropertyView):
logging.warning("PiCamera.lens_shading_table returned as None")
return ("", 204)
else:
# Get the LST Numpy array from the camera's memoryview
lst = np.asarray(microscope.camera.camera.lens_shading_table)
# R next to G1
# Create array of R next to G1
top = np.concatenate(lst[:2], axis=1)
# G2 next to B
# Create array of G2 next to B
bottom = np.concatenate(lst[2:], axis=1)
# Combined into a 2x2 grid of greyscale images
# Create combined array of 2x2 grid of greyscale images
all_channels = np.concatenate((top, bottom), axis=0)
# Create a PNG
img_bytes = BytesIO()
img_bytes: BytesIO = BytesIO()
Image.fromarray(np.uint8(all_channels), "L").save(img_bytes, "PNG")
img_bytes.seek(0)
# Return the image
fname = f"lst-{datetime.date.today().isoformat()}.png"
fname: str = f"lst-{datetime.date.today().isoformat()}.png"
return send_file(img_bytes, as_attachment=True, attachment_filename=fname)

View file

@ -1,3 +1,7 @@
from io import BytesIO
from typing import List, Optional, Union
from uuid import UUID
from flask import abort, redirect, request, send_file, url_for
from labthings import Schema, fields, find_component
from labthings.marshalling import marshal_with, use_args
@ -6,6 +10,7 @@ from labthings.views import PropertyView, View
from marshmallow import pre_dump
from openflexure_microscope.api.utilities import get_bool
from openflexure_microscope.captures import CaptureObject
# SCHEMAS
@ -61,10 +66,10 @@ class CaptureSchema(ImageSchema):
links = fields.Dict()
@pre_dump
def generate_links(self, data, **_):
def generate_links(self, data: Union[dict, CaptureObject], **_):
if isinstance(data, dict):
capture_id = data.get("id")
capture_name = data.get("name")
capture_id: Optional[Union[str, UUID]] = data.get("id")
capture_name: Optional[str] = data.get("name")
else:
capture_id = data.id
capture_name = data.name
@ -102,7 +107,7 @@ class CaptureSchema(ImageSchema):
if isinstance(data, dict):
data["links"] = links
else:
data.links = links
setattr(data, "links", links)
return data
@ -129,7 +134,7 @@ class CaptureList(PropertyView):
List all image captures
"""
microscope = find_component("org.openflexure.microscope")
image_list = microscope.captures.images.values()
image_list: List[CaptureObject] = microscope.captures.images.values()
return image_list
@ -142,7 +147,7 @@ class CaptureView(View):
Description of a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
@ -154,7 +159,7 @@ class CaptureView(View):
Delete a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
@ -171,17 +176,17 @@ class CaptureDownload(View):
tags = ["captures"]
responses = {200: {"content_type": "image/jpeg"}}
def get(self, id_, filename):
def get(self, id_, filename: Optional[str]):
"""
Image data for a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
thumbnail = get_bool(request.args.get("thumbnail"))
thumbnail: bool = get_bool(request.args.get("thumbnail", ""))
# If no filename is specified, redirect to the capture's currently set filename
if not filename:
@ -189,7 +194,7 @@ class CaptureDownload(View):
url_for(
"DownloadAPI",
id=id_,
filename=capture_obj.filename,
filename=capture_obj.name,
thumbnail=thumbnail,
),
code=307,
@ -197,10 +202,14 @@ class CaptureDownload(View):
# Download the image data using the requested filename
if thumbnail:
img = capture_obj.thumbnail
img: Optional[BytesIO] = capture_obj.thumbnail
else:
img = capture_obj.data
# If we can't get any data, return 404
if not img:
return abort(404) # 404 Not Found
return send_file(img, mimetype="image/jpeg")
@ -212,7 +221,7 @@ class CaptureTags(View):
Get tags associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
@ -225,7 +234,7 @@ class CaptureTags(View):
Add tags to a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
@ -240,7 +249,7 @@ class CaptureTags(View):
Delete tags from a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found
@ -259,7 +268,7 @@ class CaptureAnnotations(View):
Get annotations associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.captures.images.get(id_)
capture_obj: Optional[CaptureObject] = microscope.captures.images.get(id_)
if not capture_obj:
return abort(404) # 404 Not Found

View file

@ -1,4 +1,5 @@
import logging
from typing import Any, List
from flask import abort
from labthings import fields, find_component
@ -35,29 +36,29 @@ class NestedSettingsProperty(View):
tags = ["properties"]
responses = {404: {"description": "Settings key cannot be found"}}
def get(self, route):
def get(self, route: str):
"""
Show a nested section of the current microscope settings
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
keys: List[str] = route.split("/")
try:
value = get_by_path(microscope.read_settings(), keys)
value: Any = get_by_path(microscope.read_settings(), keys)
except KeyError:
return abort(404)
return value
@use_args(fields.Dict())
def put(self, args, route):
def put(self, args: Any, route: str):
"""
Update a nested section of the current microscope settings
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
keys: List[str] = route.split("/")
dictionary = create_from_path(keys)
dictionary: dict = create_from_path(keys)
set_by_path(dictionary, keys, args)
microscope.update_settings(dictionary)
@ -84,10 +85,10 @@ class NestedStateProperty(View):
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
keys: List[str] = route.split("/")
try:
value = get_by_path(microscope.state, keys)
value: Any = get_by_path(microscope.state, keys)
except KeyError:
return abort(404)
@ -112,10 +113,10 @@ class NestedConfigurationProperty(View):
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
keys: List[str] = route.split("/")
try:
value = get_by_path(microscope.configuration, keys)
value: Any = get_by_path(microscope.configuration, keys)
except KeyError:
return abort(404)

View file

@ -7,7 +7,7 @@ def gen(camera):
"""Video streaming generator function."""
while True:
# the obtained frame is a jpeg
frame = camera.stream.getframe()
frame: bytes = camera.stream.getframe()
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")