Blackened everything

This commit is contained in:
Joel Collins 2019-09-15 14:17:52 +01:00
parent e213647217
commit 5966ce29be
57 changed files with 1938 additions and 1414 deletions

View file

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

View file

@ -1,18 +1,24 @@
import numpy as np
import logging
from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
)
class MeasureSharpnessAPI(MicroscopeViewPlugin):
def post(self):
payload = JsonResponse(request)
return jsonify({'sharpness': self.plugin.measure_sharpness()})
return jsonify({"sharpness": self.plugin.measure_sharpness()})
class AutofocusAPI(MicroscopeViewPlugin):
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)
@ -22,10 +28,11 @@ class AutofocusAPI(MicroscopeViewPlugin):
# return a handle on the autofocus task
return jsonify(task.state), 202
class FastAutofocusAPI(MicroscopeViewPlugin):
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)
@ -33,7 +40,9 @@ class FastAutofocusAPI(MicroscopeViewPlugin):
backlash = 0
logging.info("Running autofocus...")
task = self.microscope.task.start(self.plugin.fast_autofocus, dz, backlash=backlash)
task = self.microscope.task.start(
self.plugin.fast_autofocus, dz, backlash=backlash
)
# return a handle on the autofocus task
return jsonify(task.state), 202
return jsonify(task.state), 202

View file

@ -4,7 +4,8 @@ import threading
import logging
from scipy import ndimage
class JPEGSharpnessMonitor():
class JPEGSharpnessMonitor:
def __init__(self, microscope, timeout=60):
self.microscope = microscope
self.camera = microscope.camera
@ -17,32 +18,34 @@ class JPEGSharpnessMonitor():
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")
@ -59,7 +62,6 @@ class JPEGSharpnessMonitor():
"""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())
@ -69,7 +71,7 @@ class JPEGSharpnessMonitor():
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
@ -92,18 +94,21 @@ class JPEGSharpnessMonitor():
"""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']:
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), ...]
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):
@ -111,12 +116,14 @@ def sharpness_sum_lap2(rgb_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)
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]])
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

@ -10,15 +10,16 @@ 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,
"/measure_sharpness": MeasureSharpnessAPI,
"/autofocus": AutofocusAPI,
"/fast_autofocus": FastAutofocusAPI,
}
### SLOW AUTOFOCUS
@ -70,21 +71,24 @@ class AutofocusPlugin(MicroscopePlugin):
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 / 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
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):
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.
@ -124,32 +128,41 @@ class AutofocusPlugin(MicroscopePlugin):
# 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!
df = dz # TODO: refactor so I actually use dz in the code below!
if initial_move_up:
m.focus_rel(df/2)
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
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
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
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
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))
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 +1,2 @@
__all__ = ['Plugin']
__all__ = ["Plugin"]
from .plugin import Plugin

View file

@ -1,4 +1,10 @@
from openflexure_microscope.devel import MicroscopePlugin, MicroscopeViewPlugin, JsonResponse, request, jsonify
from openflexure_microscope.devel import (
MicroscopePlugin,
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
)
import logging
@ -19,9 +25,7 @@ class Plugin(MicroscopePlugin):
A set of default plugins
"""
api_views = {
'/recalibrate': RecalibrateAPIView,
}
api_views = {"/recalibrate": RecalibrateAPIView}
def recalibrate(self):
"""Reset the camera's settings.
@ -32,8 +36,10 @@ class Plugin(MicroscopePlugin):
"""
scamera = self.microscope.camera
with scamera.lock:
assert not scamera.state['record_active'], "Can't recalibrate while recording!"
streaming = scamera.state['stream_active']
assert not scamera.state[
"record_active"
], "Can't recalibrate while recording!"
streaming = scamera.state["stream_active"]
if streaming:
logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480))

View file

@ -4,10 +4,11 @@ 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)
camera.capture(output, format="rgb", resize=resize, **kwargs)
return output.array
@ -19,7 +20,9 @@ def flat_lens_shading_table(camera):
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")
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
@ -28,7 +31,9 @@ def adjust_exposure_to_setpoint(camera, 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)))
camera.shutter_speed = int(
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
)
time.sleep(1)
print("done")
@ -38,7 +43,9 @@ def auto_expose_and_freeze_settings(camera):
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.
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)
@ -53,17 +60,26 @@ def auto_expose_and_freeze_settings(camera):
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))
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)
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)
channels[i, :, :] = np.sum(
bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2
)
return channels
@ -86,25 +102,27 @@ def lst_from_channels(channels):
# 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
# 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))
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
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:
@ -114,10 +132,10 @@ def lst_from_channels(channels):
# 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
# 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 = 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)
@ -145,7 +163,7 @@ def recalibrate_camera(camera):
# 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)
channels = channels_from_bayer_array(raw_image)
lens_shading_table = lst_from_channels(channels)
camera.lens_shading_table = lens_shading_table
@ -154,8 +172,10 @@ def recalibrate_camera(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])
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)

View file

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

View file

@ -1,36 +1,46 @@
from openflexure_microscope.devel import MicroscopeViewPlugin, JsonResponse, request, jsonify, abort
from openflexure_microscope.devel import (
MicroscopeViewPlugin,
JsonResponse,
request,
jsonify,
abort,
)
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)
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 = 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 = 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)
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)
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
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)
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 = self.microscope.task.start(
@ -46,7 +56,7 @@ class TileScanAPI(MicroscopeViewPlugin):
bayer=bayer,
fast_autofocus=fast_autofocus,
metadata=metadata,
tags=tags
tags=tags,
)
# return a handle on the autofocus task

View file

@ -11,29 +11,31 @@ from openflexure_microscope.devel import MicroscopePlugin
from .api import TileScanAPI
def construct_grid(initial, step_sizes, n_steps, style='raster'):
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
for i in range(n_steps[0]): # x axis
arr.append([])
for j in range(n_steps[1]): # y axis
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)]
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':
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
@ -43,24 +45,25 @@ def flatten_grid(grid):
grid = list(itertools.chain(*grid))
return grid
class ScanPlugin(MicroscopePlugin):
"""
Stack and tile plugin
"""
api_views = {
'/tile': TileScanAPI,
}
api_views = {"/tile": TileScanAPI}
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 = []):
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)
@ -68,47 +71,46 @@ class ScanPlugin(MicroscopePlugin):
# Create output object
output = self.microscope.camera.new_image(
write_to_file=True,
temporary=temporary,
filename=filename,
folder=folder)
write_to_file=True, temporary=temporary, filename=filename, folder=folder
)
# Capture
self.microscope.camera.capture(
output,
use_video_port=use_video_port,
resize=resize,
bayer=bayer)
output, use_video_port=use_video_port, resize=resize, bayer=bayer
)
# Affix metadata
if 'scan' not in tags:
tags.append('scan')
if "scan" not in tags:
tags.append("scan")
metadata.update({
'position': self.microscope.state['stage']['position'],
'scan_id': scan_id,
'basename': basename,
'microscope_id': self.microscope.id,
'microscope_name': self.microscope.name
})
metadata.update(
{
"position": self.microscope.state["stage"]["position"],
"scan_id": scan_id,
"basename": basename,
"microscope_id": self.microscope.id,
"microscope_name": self.microscope.name,
}
)
output.put_metadata(metadata)
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 = []):
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 = [],
):
# Generate a basename if none given
if not basename:
@ -121,42 +123,47 @@ class ScanPlugin(MicroscopePlugin):
initial_position = self.microscope.stage.position
# Add scan metadata
if not 'time' in metadata:
metadata['time'] = generate_basename()
if not "time" in metadata:
metadata["time"] = generate_basename()
# Check if autofocus is enabled
if autofocus_dz and hasattr(self.microscope.plugin, 'default_autofocus'):
if autofocus_dz and hasattr(self.microscope.plugin, "default_autofocus"):
autofocus_enabled = True
else:
autofocus_enabled = False
if fast_autofocus and not hasattr(self.microscope.plugin.default_autofocus, 'monitor_sharpness'):
logging.error("Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?")
if fast_autofocus and not hasattr(
self.microscope.plugin.default_autofocus, "monitor_sharpness"
):
logging.error(
"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
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
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
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':
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
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
@ -166,20 +173,21 @@ class ScanPlugin(MicroscopePlugin):
if autofocus_enabled:
if fast_autofocus:
self.microscope.plugin.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?
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.plugin.default_autofocus.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz))
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):
if grid[2] <= 1:
self.capture(
basename,
scan_id,
@ -188,7 +196,7 @@ class ScanPlugin(MicroscopePlugin):
resize=resize,
bayer=bayer,
metadata=metadata,
tags=tags
tags=tags,
)
else:
logging.debug("Entering z-stack")
@ -198,38 +206,43 @@ class ScanPlugin(MicroscopePlugin):
scan_id=scan_id,
step_size=step_size[2],
steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
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
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
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
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 = []):
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:
@ -240,13 +253,12 @@ class ScanPlugin(MicroscopePlugin):
scan_id = uuid.uuid4().hex
# Add scan metadata
if not 'time' in metadata:
metadata['time'] = generate_basename()
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:
@ -264,7 +276,7 @@ class ScanPlugin(MicroscopePlugin):
resize=resize,
bayer=bayer,
metadata=metadata,
tags=tags
tags=tags,
)
if i != steps - 1: