Remove old plugins
This commit is contained in:
parent
bd7cea0fdf
commit
94aac61925
22 changed files with 0 additions and 1438 deletions
|
|
@ -1,2 +0,0 @@
|
|||
__all__ = ["AutofocusPlugin"]
|
||||
from .plugin import AutofocusPlugin
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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]]
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
__all__ = ["Plugin"]
|
||||
from .plugin import AutocalibrationPlugin
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
__all__ = ["ScanPlugin"]
|
||||
from .plugin import ScanPlugin
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
from .plugin import ZipBuilderPlugin
|
||||
|
|
@ -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"]
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from .plugin import DynamicExamplePlugin
|
||||
from . import api
|
||||
|
|
@ -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"})
|
||||
|
|
@ -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}.",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from .plugin import ExamplePlugin
|
||||
from . import api
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue