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

@ -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):