Renamed LabThing plugins to extensions
This commit is contained in:
parent
d96e188d16
commit
2a245185a6
10 changed files with 89 additions and 89 deletions
367
openflexure_microscope/api/default_extensions/autofocus.py
Normal file
367
openflexure_microscope/api/default_extensions/autofocus.py
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
|
||||
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||
|
||||
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
|
||||
from openflexure_microscope.utilities import set_properties
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
import threading
|
||||
import logging
|
||||
from scipy import ndimage
|
||||
from contextlib import contextmanager
|
||||
|
||||
### Autofocus utilities
|
||||
|
||||
|
||||
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]]
|
||||
)
|
||||
|
||||
|
||||
### Autofocus extension
|
||||
|
||||
|
||||
def measure_sharpness(microscope, metric_fn=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))
|
||||
|
||||
|
||||
def autofocus(microscope, 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 = microscope.camera
|
||||
stage = 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(measure_sharpness(microscope, metric_fn))
|
||||
|
||||
newposition = positions[np.argmax(sharpnesses)]
|
||||
stage.move_rel([0, 0, newposition - stage.position[2]])
|
||||
|
||||
return positions, sharpnesses
|
||||
|
||||
|
||||
@contextmanager
|
||||
def monitor_sharpness(microscope):
|
||||
m = JPEGSharpnessMonitor(microscope)
|
||||
m.start()
|
||||
try:
|
||||
yield m
|
||||
finally:
|
||||
m.stop()
|
||||
|
||||
|
||||
def move_and_find_focus(microscope, dz):
|
||||
"""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 fast_autofocus(microscope, dz=2000, backlash=None):
|
||||
"""Perform a down-up-down-up autofocus"""
|
||||
with monitor_sharpness(microscope) 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(
|
||||
microscope, 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 monitor_sharpness(microscope) as m, microscope.camera.lock:
|
||||
# Ensure the MJPEG stream has started
|
||||
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()
|
||||
|
||||
|
||||
class MeasureSharpnessAPI(Resource):
|
||||
def post(self):
|
||||
microscope = find_device("openflexure_microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to measure sharpness.")
|
||||
|
||||
return jsonify({"sharpness": measure_sharpness(microscope)})
|
||||
|
||||
|
||||
class AutofocusAPI(Resource):
|
||||
"""
|
||||
Run a standard autofocus
|
||||
"""
|
||||
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
microscope = find_device("openflexure_microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to autofocus.")
|
||||
|
||||
# Figure out the range of z values to use
|
||||
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
|
||||
|
||||
if microscope.has_real_stage():
|
||||
logging.info("Running autofocus...")
|
||||
task = taskify(autofocus)(microscope, dz)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
return jsonify(task.state), 201
|
||||
|
||||
else:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
||||
|
||||
class FastAutofocusAPI(Resource):
|
||||
"""
|
||||
Run a fast autofocus
|
||||
"""
|
||||
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
microscope = find_device("openflexure_microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to autofocus.")
|
||||
|
||||
# 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 microscope.has_real_stage():
|
||||
logging.info("Running autofocus...")
|
||||
task = taskify(fast_autofocus)(microscope, dz, backlash=backlash)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
return jsonify(task.state), 201
|
||||
|
||||
else:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
||||
|
||||
autofocus_extension_v2 = BaseExtension("autofocus")
|
||||
|
||||
autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus")
|
||||
autofocus_extension_v2.add_method(autofocus, "autofocus")
|
||||
|
||||
autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness")
|
||||
|
||||
autofocus_extension_v2.add_view(AutofocusAPI, "/autofocus")
|
||||
autofocus_extension_v2.register_action(AutofocusAPI)
|
||||
|
||||
autofocus_extension_v2.add_view(FastAutofocusAPI, "/fast_autofocus")
|
||||
autofocus_extension_v2.register_action(FastAutofocusAPI)
|
||||
404
openflexure_microscope/api/default_extensions/scan.py
Normal file
404
openflexure_microscope/api/default_extensions/scan.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import numpy as np
|
||||
import itertools
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Tuple
|
||||
from functools import reduce
|
||||
|
||||
from openflexure_microscope.camera.base import generate_basename
|
||||
from openflexure_microscope.common.flask_labthings.find import find_device, find_extension
|
||||
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
|
||||
|
||||
from openflexure_microscope.devel import (
|
||||
JsonResponse,
|
||||
request,
|
||||
jsonify,
|
||||
taskify,
|
||||
abort,
|
||||
update_task_progress,
|
||||
update_task_data,
|
||||
)
|
||||
|
||||
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||
import time
|
||||
|
||||
|
||||
### Grid construction
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
### Progress
|
||||
|
||||
_images_to_be_captured: int = 1
|
||||
_images_captured_so_far: int = 0
|
||||
|
||||
|
||||
def progress():
|
||||
progress = (_images_captured_so_far / _images_to_be_captured) * 100
|
||||
logging.info(progress)
|
||||
return progress
|
||||
|
||||
|
||||
### Capturing
|
||||
|
||||
|
||||
def capture(
|
||||
microscope,
|
||||
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, *microscope.stage.position)
|
||||
folder = "SCAN_{}".format(basename)
|
||||
|
||||
# Create output object
|
||||
output = microscope.camera.new_image(
|
||||
temporary=temporary, filename=filename, folder=folder
|
||||
)
|
||||
|
||||
# Capture
|
||||
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(microscope.metadata, system=True)
|
||||
|
||||
# Insert custom metadata
|
||||
output.put_metadata(metadata)
|
||||
|
||||
# Insert custom tags
|
||||
output.put_tags(tags)
|
||||
|
||||
|
||||
### Scanning
|
||||
|
||||
|
||||
def tile(
|
||||
microscope,
|
||||
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 = [],
|
||||
):
|
||||
global _images_to_be_captured
|
||||
global _images_captured_so_far
|
||||
|
||||
# Keep task progress
|
||||
# TODO: Make this line not nasty
|
||||
_images_to_be_captured = reduce((lambda x, y: x * y), grid)
|
||||
_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 = 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
|
||||
autofocus_extension = find_extension("autofocus")
|
||||
if (
|
||||
autofocus_dz
|
||||
and autofocus_extension
|
||||
and microscope.has_real_stage()
|
||||
and microscope.has_real_camera()
|
||||
):
|
||||
autofocus_enabled = True
|
||||
else:
|
||||
autofocus_enabled = 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")
|
||||
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]))
|
||||
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
|
||||
# Refocus
|
||||
if autofocus_enabled:
|
||||
if fast_autofocus:
|
||||
autofocus_extension.fast_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")
|
||||
autofocus_extension.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:
|
||||
capture(
|
||||
microscope,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary=temporary,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
tags=tags,
|
||||
)
|
||||
# Update task progress
|
||||
_images_captured_so_far += 1
|
||||
update_task_progress(progress())
|
||||
else:
|
||||
logging.debug("Entering z-stack")
|
||||
stack(
|
||||
microscope=microscope,
|
||||
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 = 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))
|
||||
microscope.stage.move_abs(initial_position)
|
||||
|
||||
|
||||
def stack(
|
||||
microscope,
|
||||
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 = [],
|
||||
):
|
||||
global _images_captured_so_far
|
||||
|
||||
# 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 = microscope.stage.position
|
||||
|
||||
with microscope.lock:
|
||||
# Move to center scan
|
||||
if center:
|
||||
logging.debug("Moving to starting position")
|
||||
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
|
||||
|
||||
for i in range(steps):
|
||||
time.sleep(0.1)
|
||||
logging.debug("Capturing...")
|
||||
capture(
|
||||
microscope,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary=temporary,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
tags=tags,
|
||||
)
|
||||
# Update task progress
|
||||
_images_captured_so_far += 1
|
||||
update_task_progress(progress())
|
||||
|
||||
if i != steps - 1:
|
||||
logging.debug("Moving z by {}".format(step_size))
|
||||
microscope.stage.move_rel([0, 0, step_size])
|
||||
if return_to_start:
|
||||
logging.debug("Returning to {}".format(initial_position))
|
||||
microscope.stage.move_abs(initial_position)
|
||||
|
||||
|
||||
### Web views
|
||||
|
||||
|
||||
class TileScanAPI(Resource):
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
microscope = find_device("openflexure_microscope")
|
||||
|
||||
if not microscope:
|
||||
abort(503, "No microscope connected. Unable to autofocus.")
|
||||
|
||||
# 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(tile)(
|
||||
microscope,
|
||||
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
|
||||
|
||||
|
||||
scan_extension_v2 = BaseExtension("scan")
|
||||
|
||||
scan_extension_v2.add_view(TileScanAPI, "/tile")
|
||||
scan_extension_v2.register_action(TileScanAPI)
|
||||
Loading…
Add table
Add a link
Reference in a new issue