Merge branch 'fast-autofocus-in-scanning' into 'master'

Fast autofocus in scanning

See merge request openflexure/openflexure-microscope-server!7
This commit is contained in:
Joel Collins 2019-04-09 06:13:32 +00:00
commit 49afbf3e8f
3 changed files with 109 additions and 25 deletions

View file

@ -55,13 +55,9 @@ class AutofocusPlugin(MicroscopePlugin):
### FAST AUTOFOCUS
#JPEGSharpnessMonitor = JPEGSharpnessMonitor # make the class available
def sharpness_monitor(self):
return JPEGSharpnessMonitor(self.microscope)
@contextmanager
def monitor_sharpness(self):
m = self.sharpness_monitor()
m = JPEGSharpnessMonitor(self.microscope)
m.start()
try:
yield m
@ -86,4 +82,63 @@ class AutofocusPlugin(MicroscopePlugin):
else:
i, z = m.focus_rel(fz - z - backlash)
m.focus_rel(fz - z)
return m.data_dict()
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:
df = dz #TODO: refactor so I actually use dz in the code below!
if initial_move_up:
m.focus_rel(df/2)
# move down
i, z = m.focus_rel(-df)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
jt, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)]
target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing
# now move to the start of the z stack
i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve
current_js = m.jpeg_size()
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:]
inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position
# TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
correction_move = best_z + target_z - jz[inow]
logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move))
m.focus_rel(correction_move)
return m.data_dict()

View file

@ -19,6 +19,7 @@ class TileScanAPI(MicroscopeViewPlugin):
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)
@ -43,6 +44,7 @@ class TileScanAPI(MicroscopeViewPlugin):
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
fast_autofocus=fast_autofocus,
metadata=metadata,
tags=tags
)

View file

@ -101,6 +101,7 @@ class ScanPlugin(MicroscopePlugin):
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
fast_autofocus = False,
metadata: dict = {},
tags: list = []):
@ -124,6 +125,11 @@ class ScanPlugin(MicroscopePlugin):
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?")
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,
@ -132,27 +138,40 @@ class ScanPlugin(MicroscopePlugin):
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':
# Return focus to initial position
next_z = initial_z
logging.debug("Returning to initial z position")
self.microscope.stage.move_abs([line[0][0], line[0][1], initial_position[2]])
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:
current_position = self.microscope.stage.position
# Move to new grid position without changing z
logging.debug("Moving to step {}".format([x_y[0], x_y[1], current_position[2]]))
self.microscope.stage.move_abs([x_y[0], x_y[1], current_position[2]])
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:
# TODO: Better autofocus
logging.debug("Running autofocus")
self.microscope.plugin.default_autofocus.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz))
logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove
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?
else:
logging.debug("Running autofocus")
self.microscope.plugin.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):
@ -172,13 +191,20 @@ class ScanPlugin(MicroscopePlugin):
scan_id=scan_id,
step_size=step_size[2],
steps=grid[2],
center=True,
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)
@ -190,6 +216,7 @@ class ScanPlugin(MicroscopePlugin):
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,
@ -211,12 +238,12 @@ class ScanPlugin(MicroscopePlugin):
# Store initial position
initial_position = self.microscope.stage.position
# Move to center scan
if center:
logging.debug("Moving to starting position")
self.microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
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)
@ -234,6 +261,6 @@ class ScanPlugin(MicroscopePlugin):
if i != steps - 1:
logging.debug("Moving z by {}".format(step_size))
self.microscope.stage.move_rel([0, 0, step_size])
logging.debug("Returning to {}".format(initial_position))
self.microscope.stage.move_abs(initial_position)
if return_to_start:
logging.debug("Returning to {}".format(initial_position))
self.microscope.stage.move_abs(initial_position)