Static type analysis
This commit is contained in:
parent
3aebb8bead
commit
7866ec0f47
63 changed files with 1825 additions and 2722 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
196
openflexure_microscope/api/static/package-lock.json
generated
196
openflexure_microscope/api/static/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
:time="item.time"
|
||||
:tags="item.tags"
|
||||
:type="item.type"
|
||||
:captures="item.captures"
|
||||
:thumbnail="item.thumbnail"
|
||||
@selectFolder="selectFolder"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import subprocess
|
|||
from labthings.views import ActionView
|
||||
|
||||
|
||||
def is_raspberrypi():
|
||||
def is_raspberrypi() -> bool:
|
||||
"""
|
||||
Checks if Raspberry Pi.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue