Remove old plugins

This commit is contained in:
Joel Collins 2020-01-14 16:49:30 +00:00
parent bd7cea0fdf
commit 94aac61925
22 changed files with 0 additions and 1438 deletions

View file

@ -1,2 +0,0 @@
__all__ = ["AutofocusPlugin"]
from .plugin import AutofocusPlugin

View file

@ -1,64 +0,0 @@
import numpy as np
import logging
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
taskify,
abort,
)
class MeasureSharpnessAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
return jsonify({"sharpness": self.plugin.measure_sharpness()})
class AutofocusAPI(MicroscopeViewPlugin):
"""
Run a standard autofocus
"""
def post(self):
payload = JsonResponse(request)
# Figure out the range of z values to use
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
if self.microscope.has_real_stage():
logging.info("Running autofocus...")
task = taskify(self.plugin.autofocus)(dz)
# return a handle on the autofocus task
return jsonify(task.state), 201
else:
abort(503, "No stage connected. Unable to autofocus.")
class FastAutofocusAPI(MicroscopeViewPlugin):
"""
Run a fast autofocus
"""
def post(self):
payload = JsonResponse(request)
# Figure out the parameters to use
dz = payload.param("dz", default=2000, convert=int)
backlash = payload.param("backlash", default=0, convert=int)
if backlash < 0:
backlash = 0
if self.microscope.has_real_stage():
logging.info("Running autofocus...")
task = taskify(self.plugin.fast_autofocus)(dz, backlash=backlash)
# return a handle on the autofocus task
return jsonify(task.state), 201
else:
abort(503, "No stage connected. Unable to autofocus.")

View file

@ -1,129 +0,0 @@
import time
import numpy as np
import threading
import logging
from scipy import ndimage
class JPEGSharpnessMonitor:
def __init__(self, microscope, timeout=60):
self.microscope = microscope
self.camera = microscope.camera
self.stage = microscope.stage
self.jpeg_sizes = []
self.jpeg_times = []
self.stage_positions = []
self.stage_times = []
self.stop_event = threading.Event()
self.timeout = timeout
self.keep_alive()
self.background_thread = None
def is_alive(self):
if self.background_thread is None:
return False
else:
return self.background_thread.is_alive()
def should_stop(self):
import time
return time.time() - self.kept_alive > self.timeout
def keep_alive(self):
import time
self.kept_alive = time.time()
def start(self):
"Start monitoring sharpness by looking at JPEG size"
self.background_thread = threading.Thread(target=self._measure_jpegs)
self.background_thread.start()
return self
def stop(self):
"Stop the background thread"
self.stop_event.set()
self.background_thread.join()
def _measure_jpegs(self):
"Function that runs in a background thread to record sharpness"
logging.info("Starting sharpness measurement in background thread")
self.keep_alive()
while not self.stop_event.is_set() and not self.should_stop():
self.jpeg_sizes.append(self.jpeg_size())
self.jpeg_times.append(time.time())
if self.stop_event.is_set():
logging.info("Cleanly stopped sharpness measurement in background thread")
if self.should_stop():
logging.info("Sharpness measurement timed out and has stopped")
def jpeg_size(self):
"""Return the size of a frame from the MJPEG stream"""
return len(self.camera.get_frame())
def focus_rel(self, dz, backlash=False, **kwargs):
self.keep_alive()
self.stage_times.append(time.time())
self.stage_positions.append(self.stage.position)
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
self.stage_times.append(time.time())
self.stage_positions.append(self.stage.position)
i = len(self.stage_positions) - 2
return i, self.stage_positions[-1][2]
def move_data(self, istart, istop=None):
"Extract sharpness as a function of (interpolated) z"
global np, logging
if istop is None:
istop = istart + 2
jpeg_times = np.array(self.jpeg_times)
jpeg_sizes = np.array(self.jpeg_sizes)
stage_times = np.array(self.stage_times)[istart:istop]
stage_zs = np.array(self.stage_positions)[istart:istop, 2]
start = np.argmax(jpeg_times > stage_times[0])
stop = np.argmax(jpeg_times > stage_times[1])
if stop < 1:
stop = len(jpeg_times)
logging.debug("changing stop to {}".format(stop))
jpeg_times = jpeg_times[start:stop]
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):
"""Return the z position of the sharpest image on a given move"""
jt, jz, js = self.move_data(index)
return jz[np.argmax(js)]
def data_dict(self):
"""Return the gathered data as a single convenient dictionary"""
data = {}
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
data[k] = getattr(self, k)
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):
"""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):
"""Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2)
n = 20
edge = np.array([[-1] * n + [1] * n])
return np.sum(
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
)

View file

@ -1,168 +0,0 @@
import time
import logging
import numpy as np
from contextlib import contextmanager
from openflexure_microscope.utilities import set_properties
from .focus_utils import sharpness_sum_lap2, JPEGSharpnessMonitor
from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI
from openflexure_microscope.devel import MicroscopePlugin
class AutofocusPlugin(MicroscopePlugin):
"""
Basic autofocus plugin
"""
api_views = {
"/measure_sharpness": MeasureSharpnessAPI,
"/autofocus": AutofocusAPI,
"/fast_autofocus": FastAutofocusAPI,
}
### SLOW AUTOFOCUS
def autofocus(self, dz, settle=0.5, metric_fn=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
evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values)
"""
camera = self.microscope.camera
stage = self.microscope.stage
with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses = []
positions = []
camera.annotate_text = ""
for _ in stage.scan_z(dz, return_to_start=False):
positions.append(stage.position[2])
time.sleep(settle)
sharpnesses.append(self.measure_sharpness(metric_fn))
newposition = positions[np.argmax(sharpnesses)]
stage.move_rel([0, 0, newposition - stage.position[2]])
return positions, sharpnesses
def measure_sharpness(self, metric_fn=sharpness_sum_lap2):
"""Measure the sharpness of the camera's current view."""
return metric_fn(self.microscope.camera.array(use_video_port=True))
### FAST AUTOFOCUS
@contextmanager
def monitor_sharpness(self):
m = JPEGSharpnessMonitor(self.microscope)
m.start()
try:
yield m
finally:
m.stop()
def move_and_find_focus(self, dz):
"""Make a relative Z move and return the peak sharpness position"""
with self.monitor_sharpness() as m:
m.focus_rel(dz)
return m.sharpest_z_on_move(0)
def fast_autofocus(self, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus"""
with self.monitor_sharpness() as m:
i, z = m.focus_rel(-dz / 2)
i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i)
if backlash is None:
i, z = m.focus_rel(
-dz
) # move all the way to the start so it's consistent
else:
i, z = m.focus_rel(fz - z - backlash)
m.focus_rel(fz - z)
return m.data_dict()
def fast_up_down_up_autofocus(
self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150
):
"""Autofocus by measuring on the way down, and moving back up with feedback.
This autofocus method is very efficient, as it only passes the peak once.
The sequence of moves it performs is:
1. Move to the top of the range `dz/2` (can be disabled)
2. Move down by `dz` while monitoring JPEG size to find the focus.
3. Move back up to the `target_z` position, relative to the sharpest image.
4. Measure the sharpness, and compare against the curve recorded in (2) to \\
estimate how much further we need to go. Make this move, to reach our \\
target position.
Moving back to the target position in two steps allows us to correct for
backlash, by using the sharpness-vs-z curve as a rough encoder for Z.
Parameters:
dz: number of steps over which to scan (optional, default 2000)
target_z: we aim to finish at this position, relative to focus. This may
be useful if, for example, you want to acquire a stack of images in Z.
It is optional, and the default value of 0 will finish at the focus.
initial_move_up: (optional, default True) set this to `False` to move down
from the starting position. Mostly useful if you're able to combine
the initial move with something else, e.g. moving to the next scan point.
mini_backlash: (optional, default 50) is a small extra move made in step
3 to help counteract backlash. It should be small enough that you
would always expect there to be greater backlash than this. Too small
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
may cause you to overshoot, which is a problem.
"""
with self.monitor_sharpness() as m, self.microscope.camera.lock:
# Ensure the MJPEG stream has started
self.microscope.camera.start_stream_recording()
df = dz # TODO: refactor so I actually use dz in the code below!
if initial_move_up:
m.focus_rel(df / 2)
# move down
i, z = m.focus_rel(-df)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
jt, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)]
target_s = np.interp(
[best_z + target_z], jz[::-1], js[::-1]
) # NB jz is decreasing
# now move to the start of the z stack
i, z = m.focus_rel(
best_z + target_z - z + mini_backlash
) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve
current_js = m.jpeg_size()
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:]
inow = np.argmax(
js < current_js
) # use the curve we recorded to estimate our position
# TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
correction_move = best_z + target_z - jz[inow]
logging.debug(
"Fast autofocus scan: correcting backlash by moving {} steps".format(
correction_move
)
)
m.focus_rel(correction_move)
return m.data_dict()

View file

@ -1,2 +0,0 @@
__all__ = ["Plugin"]
from .plugin import AutocalibrationPlugin

View file

@ -1,57 +0,0 @@
from openflexure_microscope.devel import (
MicroscopePlugin,
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
taskify,
)
import logging
from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings
class RecalibrateAPIView(MicroscopeViewPlugin):
def post(self):
logging.info("Starting microscope recalibration...")
task = taskify(self.plugin.recalibrate)()
# Return a handle on the autofocus task
return jsonify(task.state), 201
class AutocalibrationPlugin(MicroscopePlugin):
"""
Auto-calibration plugin
"""
api_views = {"/recalibrate": RecalibrateAPIView}
def recalibrate(self):
"""Reset the camera's settings.
This generates new gains, exposure time, and lens shading
table such that the background is as uniform as possible
with a gray level of 230. It takes a little while to run.
"""
scamera = self.microscope.camera
with scamera.lock:
assert not scamera.status[
"record_active"
], "Can't recalibrate while recording!"
streaming = scamera.status["stream_active"]
if streaming:
logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480))
old_resolution = scamera.camera.resolution
try:
scamera.camera.resolution = (640, 480)
auto_expose_and_freeze_settings(scamera.camera)
recalibrate_camera(scamera.camera)
finally:
scamera.camera.resolution = old_resolution
self.microscope.save_settings()
if streaming:
logging.info("Restarting stream after recalibration")
scamera.start_stream_recording()

View file

@ -1,191 +0,0 @@
import numpy as np
import time
from picamera import PiCamera
from picamera.array import PiRGBArray, PiBayerArray
def rgb_image(camera, resize=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):
"""Return a flat (i.e. unity gain) lens shading table.
This is mostly useful because it makes it easy to get the size
of the array correct. NB if you are not using the forked picamera
library (with lens shading table support) it will raise an error.
"""
if not hasattr(PiCamera, "lens_shading_table"):
raise ImportError(
"This program requires the forked picamera library with lens shading support"
)
return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32
def adjust_exposure_to_setpoint(camera, setpoint):
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
for i in range(3):
print(".", end="")
camera.shutter_speed = int(
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
)
time.sleep(1)
print("done")
def auto_expose_and_freeze_settings(camera):
"""Freeze the settings after auto-exposing to white illumination"""
print("Allowing the camera to auto-expose")
camera.awb_mode = "auto"
camera.exposure_mode = "auto"
camera.iso = (
0
) # This is important, if it's on a fixed ISO, gain might not set properly.
for i in range(6):
print(".", end="")
time.sleep(0.5)
print("done")
print("Freezing the camera settings...")
camera.shutter_speed = camera.exposure_speed
print("Shutter speed = {}".format(camera.shutter_speed))
camera.exposure_mode = "off"
print("Auto exposure disabled")
g = camera.awb_gains
camera.awb_mode = "off"
camera.awb_gains = g
print("Auto white balance disabled, gains are {}".format(g))
print(
"Analogue gain: {}, Digital gain: {}".format(
camera.analog_gain, camera.digital_gain
)
)
adjust_exposure_to_setpoint(camera, 215)
def channels_from_bayer_array(bayer_array):
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern = [(i // 2, i % 2) for i in range(4)]
channels = np.zeros(
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
dtype=bayer_array.dtype,
)
for i, offset in enumerate(bayer_pattern):
# We simplify life by dealing with only one channel at a time.
channels[i, :, :] = np.sum(
bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2
)
return channels
def lst_from_channels(channels):
"""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.
print("Generating a lens shading table at {}x{}".format(*lst_resolution))
lens_shading = 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
ls_channel = lens_shading[i, :, :]
lw, lh = ls_channel.shape
# The lens shading table is rounded **up** in size to 1/64th of the size of
# the image. Rather than handle edge images separately, I'm just going to
# pad the image by copying edge pixels, so that it is exactly 32 times the
# size of the lens shading table (NB 32 not 64 because each channel is only
# half the size of the full image - remember the Bayer pattern... This
# should give results very close to 6by9's solution, albeit considerably
# less computationally efficient!
padded_image_channel = np.pad(
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
) # Pad image to the right and bottom
print(
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
iw, ih, lw * 32, lh * 32, padded_image_channel.shape
)
)
# Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be!
box = 3 # We average together a square of this side length for each pixel.
# NB this isn't quite what 6by9's program does - it averages 3 pixels
# horizontally, but not vertically.
for dx in np.arange(box) - box // 2:
for dy in np.arange(box) - box // 2:
ls_channel[:, :] += (
padded_image_channel[16 + dx :: 32, 16 + dy :: 32] - 64
)
ls_channel /= box ** 2
# The original C code written by 6by9 normalises to the central 64 pixels in each channel.
# ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4])
# I have had better results just normalising to the maximum:
ls_channel /= np.max(ls_channel)
# NB the central pixel should now be *approximately* 1.0 (may not be exactly
# due to different averaging widths between the normalisation & shading table)
# For most sensible lenses I'd expect that 1.0 is the maximum value.
# NB ls_channel should be a "view" of the whole lens shading array, so we don't
# need to update the big array here.
# What we actually want to calculate is the gains needed to compensate for the
# lens shading - that's 1/lens_shading_table_float as we currently have it.
gains = 32.0 / lens_shading # 32 is unity gain
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
lens_shading_table = gains.astype(np.uint8)
return lens_shading_table[::-1, :, :].copy()
def recalibrate_camera(camera):
"""Reset the lens shading table and exposure settings.
This method first resets to a flat lens shading table, then auto-exposes,
then generates a new lens shading table to make the current view uniform.
It should be run when the camera is looking at a uniform white scene.
NB the only parameter ``camera`` is a ``PiCamera`` instance and **not** a
``StreamingCamera``.
"""
camera.lens_shading_table = flat_lens_shading_table(camera)
_ = rgb_image(camera) # for some reason the camera won't work unless I do this!
with PiBayerArray(camera) as a:
camera.capture(a, format="jpeg", bayer=True)
raw_image = a.array.copy()
# Now we need to calculate a lens shading table that would make this flat.
# raw_image is a 3D array, with full resolution and 3 colour channels. No
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
# channels, 1/2 for green because there's twice as many green pixels).
channels = channels_from_bayer_array(raw_image)
lens_shading_table = lst_from_channels(channels)
camera.lens_shading_table = lens_shading_table
_ = rgb_image(camera)
# Fix the AWB gains so the image is neutral
channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0)
old_gains = camera.awb_gains
camera.awb_gains = (
channel_means[1] / channel_means[0] * old_gains[0],
channel_means[1] / channel_means[2] * old_gains[1],
)
time.sleep(1)
# Ensure the background is bright but not saturated
adjust_exposure_to_setpoint(camera, 230)
if __name__ == "__main__":
with PiCamera() as camera:
camera.start_preview()
time.sleep(3)
print("Recalibrating...")
recalibrate_camera(camera)
print("Done.")
time.sleep(2)

View file

@ -1,2 +0,0 @@
__all__ = ["ScanPlugin"]
from .plugin import ScanPlugin

View file

@ -1,63 +0,0 @@
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
abort,
taskify,
)
import logging
class TileScanAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
# Get params
filename = payload.param("filename")
temporary = payload.param("temporary", default=False, convert=bool)
step_size = payload.param("step_size", default=[2000, 1500, 100], convert=list)
step_size = [int(i) for i in step_size]
grid = payload.param("grid", default=[3, 3, 5], convert=list)
grid = [int(i) for i in grid]
style = payload.param("style", default="raster", convert=str)
autofocus_dz = payload.param("autofocus_dz", default=50, convert=int)
fast_autofocus = payload.param("fast_autofocus", default=False, convert=bool)
use_video_port = payload.param("use_video_port", default=True, convert=bool)
resize = payload.param("size", default=None)
if resize:
if ("width" in resize) and ("height" in resize):
resize = (
int(resize["width"]),
int(resize["height"]),
) # Convert dict to tuple
else:
abort(404)
bayer = payload.param("bayer", default=False, convert=bool)
metadata = payload.param("metadata", default={}, convert=dict)
tags = payload.param("tags", default=[], convert=list)
logging.info("Running tile scan...")
task = taskify(self.plugin.tile)(
basename=filename,
temporary=temporary,
step_size=step_size,
grid=grid,
style=style,
autofocus_dz=autofocus_dz,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
fast_autofocus=fast_autofocus,
metadata=metadata,
tags=tags,
)
# return a handle on the scan task
return jsonify(task.state), 201

View file

@ -1,328 +0,0 @@
import time
from typing import Tuple
from functools import reduce
import uuid
import itertools
import logging
from openflexure_microscope.camera.base import generate_basename
from openflexure_microscope.devel import (
MicroscopePlugin,
update_task_progress,
update_task_data,
)
from .api import TileScanAPI
def construct_grid(initial, step_sizes, n_steps, style="raster"):
"""
Given an initial position, step sizes, and number of steps,
construct a 2-dimensional list of scan x-y positions.
"""
arr = []
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)]
# Append coordinate array to position grid
arr[i].append(tuple(coord))
# Style modifiers
if style == "snake":
for i, line in enumerate(arr):
if i % 2 != 0:
line.reverse()
return arr
def flatten_grid(grid):
"""
Convert a 3D list of scan positions into a flat list
of sequential positions.
"""
grid = list(itertools.chain(*grid))
return grid
class ScanPlugin(MicroscopePlugin):
"""
Stack and tile plugin
"""
api_views = {"/tile": TileScanAPI}
def __init__(self):
MicroscopePlugin.__init__(self)
self.images_to_be_captured: int = 1
update_task_data({"images_to_be_captured": self.images_to_be_captured})
@property
def progress(self):
progress = (self.images_captured_so_far / self.images_to_be_captured) * 100
logging.info(progress)
return progress
def capture(
self,
basename,
scan_id,
temporary: bool = False,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
metadata: dict = {},
tags: list = [],
):
# Construct a tile filename
filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position)
folder = "SCAN_{}".format(basename)
# Create output object
output = self.microscope.camera.new_image(
temporary=temporary, filename=filename, folder=folder
)
# Capture
self.microscope.camera.capture(
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
)
# Affix metadata
if "scan" not in tags:
tags.append("scan")
# Inject system metadata
output.put_metadata(self.microscope.metadata, system=True)
# Insert custom metadata
output.put_metadata(metadata)
# Insert custom tags
output.put_tags(tags)
def tile(
self,
basename: str = None,
temporary: bool = False,
step_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5],
style="raster",
autofocus_dz: int = 50,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
fast_autofocus=False,
metadata: dict = {},
tags: list = [],
):
# Keep task progress
# TODO: Make this line not nasty
self.images_to_be_captured = reduce((lambda x, y: x * y), grid)
self.images_captured_so_far = 0
# Generate a basename if none given
if not basename:
basename = generate_basename()
# Generate a stack ID
scan_id = uuid.uuid4()
# Store initial position
initial_position = self.microscope.stage.position
# Add scan metadata
if "time" not in metadata:
metadata["time"] = generate_basename()
metadata.update(
{
"scan_id": scan_id,
"basename": basename,
"scan_parameters": {
"step_size": step_size,
"grid": grid,
"style": style,
"autofocus_dz": autofocus_dz,
},
}
)
# Check if autofocus is enabled
if (
autofocus_dz
and hasattr(self.microscope.plugins, "default_autofocus")
and self.microscope.has_real_stage()
and self.microscope.has_real_camera()
):
autofocus_enabled = True
else:
autofocus_enabled = False
if fast_autofocus and not hasattr(
self.microscope.plugins.default_autofocus, "monitor_sharpness"
):
logging.warning(
"Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?"
)
fast_autofocus = False
z_stack_dz = (
grid[2] * step_size[2] if grid[2] > 1 else 0
) # shorthand for Z stack range
# Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(
initial_position, step_size[:2], grid[:2], style=style
)
# Keep the initial Z position the same as our current position
next_z = initial_position[2]
if fast_autofocus: # If fast autofocus is enabled, make
next_z += autofocus_dz / 2 # sure we start from the top of the range
initial_z = next_z # Save this value for use in raster scans
# Now step through each point in the x-y coordinate array
for line in x_y_grid:
# If rastering, rather than snake (or eventually spiral)
# Return focus to initial position
if style == "raster":
next_z = initial_z
logging.debug("Returning to initial z position")
self.microscope.stage.move_abs(
[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 {}".format([x_y[0], x_y[1], next_z]))
self.microscope.stage.move_abs([x_y[0], x_y[1], next_z])
# Refocus
if autofocus_enabled:
if fast_autofocus:
self.microscope.plugins.default_autofocus.fast_up_down_up_autofocus(
dz=autofocus_dz,
target_z=-z_stack_dz / 2.0, # Finish below the focus
initial_move_up=False, # We're already at the top of the scan
)
# TODO: save the focus data for future reference? Use it for diagnostics?
else:
logging.debug("Running autofocus")
self.microscope.plugins.default_autofocus.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
)
logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove
# If we're not doing a z-stack, just capture
if grid[2] <= 1:
self.capture(
basename,
scan_id,
temporary=temporary,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
tags=tags,
)
# Update task progress
self.images_captured_so_far += 1
update_task_progress(self.progress)
else:
logging.debug("Entering z-stack")
self.stack(
basename=basename,
temporary=temporary,
scan_id=scan_id,
step_size=step_size[2],
steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
return_to_start=not fast_autofocus,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
tags=tags,
)
# Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = self.microscope.stage.position[2]
if fast_autofocus:
next_z += (
autofocus_dz / 2
) # Fast autofocus requires us to start at the top of the range
if grid[2] > 1:
next_z -= int(
grid[2] / 2.0 * step_size[2]
) # Z stacking means we're higher up to start with
logging.debug("Returning to {}".format(initial_position))
self.microscope.stage.move_abs(initial_position)
def stack(
self,
basename: str = None,
temporary: bool = False,
scan_id: str = None,
step_size: int = 100,
steps: int = 5,
center: bool = True,
return_to_start: bool = True,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
metadata: dict = {},
tags: list = [],
):
# Generate a basename if none given
if not basename:
basename = generate_basename()
# Generate a stack ID
if not scan_id:
scan_id = uuid.uuid4()
# Add scan metadata
if not "time" in metadata:
metadata["time"] = generate_basename()
# Store initial position
initial_position = self.microscope.stage.position
with self.microscope.lock:
# Move to center scan
if center:
logging.debug("Moving to starting position")
self.microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
for i in range(steps):
time.sleep(0.1)
logging.debug("Capturing...")
self.capture(
basename,
scan_id,
temporary=temporary,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
tags=tags,
)
# Update task progress
self.images_captured_so_far += 1
update_task_progress(self.progress)
if i != steps - 1:
logging.debug("Moving z by {}".format(step_size))
self.microscope.stage.move_rel([0, 0, step_size])
if return_to_start:
logging.debug("Returning to {}".format(initial_position))
self.microscope.stage.move_abs(initial_position)

View file

@ -1 +0,0 @@
from .plugin import ZipBuilderPlugin

View file

@ -1,138 +0,0 @@
from openflexure_microscope.devel import (
MicroscopePlugin,
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
taskify,
update_task_progress,
)
from flask import send_file, abort
import uuid
import os
import zipfile
import tempfile
import logging
class ZipBuilderAPIView(MicroscopeViewPlugin):
def post(self):
ids = list(JsonResponse(request).json)
task = taskify(self.plugin.build_zip_from_capture_ids)(ids)
# Return a handle on the autofocus task
return jsonify(task.state), 201
class ZipListAPIView(MicroscopeViewPlugin):
def get(self):
return jsonify(self.plugin.session_zips)
class ZipGetterAPIView(MicroscopeViewPlugin):
def get(self, session_id):
if not session_id in self.plugin.session_zips:
return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}")
return send_file(
self.plugin.zip_from_id(session_id).name,
mimetype="application/zip",
as_attachment=True,
attachment_filename=f"{session_id}.zip",
)
def delete(self, session_id):
if not session_id in self.plugin.session_zips:
return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}")
fp = self.plugin.zip_from_id(session_id)
logging.debug(fp.name)
fp.close()
os.unlink(fp.name)
assert not os.path.exists(fp.name)
del self.plugin.session_zips[session_id]
return jsonify({"return": session_id})
class ZipBuilderPlugin(MicroscopePlugin):
"""
ZIP-builder plugin
"""
def __init__(self):
super().__init__()
self.session_zips = {}
self.add_view("/get/<string:session_id>", ZipGetterAPIView)
self.add_view("/get", ZipListAPIView)
self.add_view("/build", ZipBuilderAPIView)
def build_zip_from_capture_ids(self, capture_id_list):
logging.debug(capture_id_list)
# Get array of captures from IDs
capture_list = [
self.microscope.camera.image_from_id(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]
# Get size (in bytes) of each capture
capture_sizes = [
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
# If more than 1GB
if data_size_megabytes > 1000:
# Throw exception
raise Exception(
"Zip data cannot exceed 1GB. Please transfer data manually."
)
# Number of files to add (used for task progress)
n_files = len(capture_id_list)
# Create temporary file
fp = tempfile.NamedTemporaryFile(delete=False)
# Open temp file as a ZIP file
with zipfile.ZipFile(fp, "w") as zipObj:
for index, capture_obj in enumerate(capture_list):
# Add to ZIP file if it exists
file_path = capture_obj.file
rel_path = os.path.relpath(
file_path, self.microscope.camera.paths["default"]
)
zipObj.write(file_path, arcname=rel_path)
# Update task progress
update_task_progress(int((index / n_files) * 100))
session_id = uuid.uuid4()
# self.session_zips[session_id] = fp
self.session_zips[session_id] = {
"id": session_id,
"fp": fp,
"data_size": data_size_megabytes,
"zip_size": os.path.getsize(fp.name) * 1e-6,
}
return self.session_zips[session_id]
def zip_from_id(self, session_id):
return self.session_zips[session_id]["fp"]

View file

@ -1,2 +0,0 @@
from .plugin import DynamicExamplePlugin
from . import api

View file

@ -1,24 +0,0 @@
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
taskify,
)
import logging
class DoAPI(MicroscopeViewPlugin):
"""
A dynamic example API plugin
"""
def get(self):
values = {"val_int": self.plugin.val_int}
return jsonify(values)
def post(self):
self.plugin.val_int += 1
return jsonify({"response": "completed"})

View file

@ -1,52 +0,0 @@
import random
import time
import os
import json
from openflexure_microscope.devel import MicroscopePlugin, update_task_progress
from .api import DoAPI
class DynamicExamplePlugin(MicroscopePlugin):
"""
An example plugin using a comprehensive form
"""
api_views = {"/do": DoAPI}
def __init__(self):
MicroscopePlugin.__init__(self)
self.val_int = 0
self.val_str = "Hello"
self.set_gui(self.dynamic_form)
def dynamic_form(self):
return {
"id": "test-plugin",
"icon": "pets",
"forms": [
{
"name": "Simple request",
"isCollapsible": False,
"isTask": False,
"selfUpdate": True,
"route": "/do",
"submitLabel": "Do things",
"schema": [
{
"fieldType": "numberInput",
"name": "val_int",
"label": "Number value",
"minValue": 0,
},
{
"fieldType": "htmlBlock",
"name": "html_block",
"content": f"<i>Value is: </i><br>{self.val_int}.",
},
],
}
],
}

View file

@ -1,2 +0,0 @@
from .plugin import ExamplePlugin
from . import api

View file

@ -1,60 +0,0 @@
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
taskify,
)
import logging
class DoAPI(MicroscopeViewPlugin):
"""
A simple example API plugin
"""
def get(self):
return jsonify(self.plugin.get_values_dict())
def post(self):
# Get payload JSON
payload = JsonResponse(request)
# Extract a values from the JSON payload.
val_int = payload.param("val_int", default=None, convert=int)
val_str = payload.param("val_str", default=None, convert=str)
val_radio = payload.param("val_radio", default=None)
val_check = payload.param("val_check", default=None)
val_select = payload.param("val_select", default=None)
val_disposable = payload.param("val_disposable", default=None)
self.plugin.set_values(
val_int, val_str, val_radio, val_check, val_select, val_disposable
)
print(self.plugin.get_values_dict())
return jsonify({"response": "completed"})
class TaskAPI(MicroscopeViewPlugin):
"""
A task example API plugin
"""
def get(self):
return jsonify({"run_time": self.plugin.run_time})
def post(self):
# Get payload JSON
payload = JsonResponse(request)
# Extract a values from the JSON payload.
val_int = payload.param("run_time", default=5, convert=int)
logging.info("Running task...")
task = taskify(self.plugin.generate_random_numbers_for_a_while)(val_int)
# return a handle on the autofocus task
return jsonify(task.state), 201

View file

@ -1,73 +0,0 @@
{
"id": "test-plugin",
"icon": "pets",
"forms": [{
"name": "Simple request",
"isCollapsible": false,
"isTask": false,
"selfUpdate": true,
"route": "/do",
"submitLabel": "Do things",
"schema": [
[{
"fieldType": "numberInput",
"name": "val_int",
"label": "Number value",
"minValue": 0
},
{
"fieldType": "textInput",
"placeholder": "Some string",
"label": "String value",
"name": "val_str"
}
],
[{
"fieldType": "radioList",
"name": "val_radio",
"label": "Radio value",
"options": ["First", "Second", "Third"]
},
{
"fieldType": "checkList",
"name": "val_check",
"label": "Checklist values",
"options": ["Foo", "Bar", "Baz"]
}
],
{
"fieldType": "htmlBlock",
"name": "html_block",
"content": "<i>This is a block of HTML in a plugin!</i><br>I can do paragraph breaks and stuff."
},
{
"fieldType": "selectList",
"name": "val_select",
"multi": false,
"label": "Some selection",
"options": ["Most", "Average", "Least"]
},
{
"fieldType": "textInput",
"label": "Non-persistent string",
"default": "A default value",
"name": "val_disposable"
}
]
},
{
"name": "Task form",
"isTask": true,
"selfUpdate": true,
"route": "/task",
"submitLabel": "Start task",
"schema": [{
"fieldType": "numberInput",
"name": "run_time",
"label": "Run time (seconds)",
"minValue": 1
}]
}
]
}

View file

@ -1,80 +0,0 @@
import random
import time
import os
import json
from openflexure_microscope.devel import MicroscopePlugin, update_task_progress
from .api import DoAPI, TaskAPI
HERE = os.path.dirname(os.path.realpath(__file__))
FORM_PATH = os.path.join(HERE, "forms.json")
class ExamplePlugin(MicroscopePlugin):
"""
An example plugin using a comprehensive form
"""
global FORM_PATH
with open(FORM_PATH, "r") as sc:
api_form = json.load(sc)
api_views = {"/do": DoAPI, "/task": TaskAPI}
def __init__(self):
MicroscopePlugin.__init__(self)
self.val_int = 10
self.val_str = "Hello"
self.val_radio = "First"
self.val_check = ["Foo", "Bar"]
self.val_select = "Most"
self.val_unused = "I'm an unused string, here to confuse the form parsing"
self.run_time = 5
def set_values(
self, val_int, val_str, val_radio, val_check, val_select, val_disposable
):
"""
Demonstrate a plugin with form
"""
if val_int:
self.val_int = int(val_int)
if val_str:
self.val_str = str(val_str)
if val_radio:
self.val_radio = val_radio
if val_check is not None:
print(val_check)
self.val_check = val_check if (type(val_check) is list) else [val_check]
if val_select:
self.val_select = val_select
if val_disposable:
print("DISPOSABLE VALUE: {}".format(val_disposable))
def get_values_dict(self):
return {
"val_int": self.val_int,
"val_str": self.val_str,
"val_radio": self.val_radio,
"val_check": self.val_check,
"val_select": self.val_select,
"val_unused": self.val_unused,
}
def generate_random_numbers_for_a_while(self, run_time: int):
self.run_time = run_time
vals = []
for t in range(run_time):
vals.append(random.random())
time.sleep(1)
# Update task progress (if running as a task)
percent_complete = int(((t + 1) / run_time) * 100)
update_task_progress(percent_complete)
return vals