Blackened everything
This commit is contained in:
parent
e213647217
commit
5966ce29be
57 changed files with 1938 additions and 1414 deletions
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['AutofocusPlugin']
|
||||
from .plugin import AutofocusPlugin
|
||||
__all__ = ["AutofocusPlugin"]
|
||||
from .plugin import AutofocusPlugin
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['Plugin']
|
||||
__all__ = ["Plugin"]
|
||||
from .plugin import Plugin
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__all__ = ['ScanPlugin']
|
||||
__all__ = ["ScanPlugin"]
|
||||
from .plugin import ScanPlugin
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue