173 lines
6.9 KiB
Python
173 lines
6.9 KiB
Python
import time
|
|
import logging
|
|
import numpy as np
|
|
from contextlib import contextmanager
|
|
|
|
from openflexure_microscope.plugins import MicroscopePlugin
|
|
from openflexure_microscope.utilities import set_properties
|
|
|
|
from .focus_utils import sharpness_sum_lap2, JPEGSharpnessMonitor
|
|
from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI
|
|
|
|
API_SCHEMA = {
|
|
'icon': 'center_focus_strong',
|
|
'requireConnection': True,
|
|
'forms': [
|
|
{
|
|
'route': '/fast_autofocus',
|
|
'schema': [
|
|
{
|
|
'fieldType': "htmlBlock",
|
|
'name': "heading",
|
|
'content': "<b>This plugin was generated from JSON</b>"
|
|
},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
class AutofocusPlugin(MicroscopePlugin):
|
|
"""
|
|
Basic autofocus plugin
|
|
"""
|
|
|
|
api_views = {
|
|
'/measure_sharpness': MeasureSharpnessAPI,
|
|
'/autofocus': AutofocusAPI,
|
|
'/fast_autofocus': FastAutofocusAPI,
|
|
}
|
|
|
|
api_schema = API_SCHEMA
|
|
|
|
### SLOW AUTOFOCUS
|
|
|
|
def autofocus(self, dz, settle=0.5, metric_fn=sharpness_sum_lap2):
|
|
"""Perform a simple autofocus routine.
|
|
The stage is moved to z positions (relative to current position) in dz,
|
|
and at each position an image is captured and the sharpness function
|
|
evaulated. We then move back to the position where the sharpness was
|
|
highest. No interpolation is performed.
|
|
dz is assumed to be in ascending order (starting at -ve values)
|
|
"""
|
|
camera = self.microscope.camera
|
|
stage = self.microscope.stage
|
|
|
|
with set_properties(stage, backlash=256), stage.lock, camera.lock:
|
|
sharpnesses = []
|
|
positions = []
|
|
camera.annotate_text = ""
|
|
|
|
for _ in stage.scan_z(dz, return_to_start=False):
|
|
positions.append(stage.position[2])
|
|
time.sleep(settle)
|
|
sharpnesses.append(self.measure_sharpness(metric_fn))
|
|
|
|
newposition = positions[np.argmax(sharpnesses)]
|
|
stage.move_rel([0, 0, newposition - stage.position[2]])
|
|
|
|
return positions, sharpnesses
|
|
|
|
def measure_sharpness(self, metric_fn=sharpness_sum_lap2):
|
|
"""Measure the sharpness of the camera's current view."""
|
|
return metric_fn(self.microscope.camera.array(use_video_port=True))
|
|
|
|
### FAST AUTOFOCUS
|
|
|
|
@contextmanager
|
|
def monitor_sharpness(self):
|
|
m = JPEGSharpnessMonitor(self.microscope)
|
|
m.start()
|
|
try:
|
|
yield m
|
|
finally:
|
|
m.stop()
|
|
|
|
def move_and_find_focus(self, dz):
|
|
"""Make a relative Z move and return the peak sharpness position"""
|
|
with self.monitor_sharpness() as m:
|
|
m.focus_rel(dz)
|
|
return m.sharpest_z_on_move(0)
|
|
|
|
|
|
def fast_autofocus(self, dz=2000, backlash=None):
|
|
"""Perform a down-up-down-up autofocus"""
|
|
with self.monitor_sharpness() as m:
|
|
i, z = m.focus_rel(-dz/2)
|
|
i, z = m.focus_rel(dz)
|
|
fz = m.sharpest_z_on_move(i)
|
|
if backlash is None:
|
|
i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent
|
|
else:
|
|
i, z = m.focus_rel(fz - z - backlash)
|
|
m.focus_rel(fz - z)
|
|
return m.data_dict()
|
|
|
|
def fast_up_down_up_autofocus(self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150):
|
|
"""Autofocus by measuring on the way down, and moving back up with feedback.
|
|
|
|
This autofocus method is very efficient, as it only passes the peak once.
|
|
The sequence of moves it performs is:
|
|
|
|
1. Move to the top of the range `dz/2` (can be disabled)
|
|
|
|
2. Move down by `dz` while monitoring JPEG size to find the focus.
|
|
|
|
3. Move back up to the `target_z` position, relative to the sharpest image.
|
|
|
|
4. Measure the sharpness, and compare against the curve recorded in (2) to \\
|
|
estimate how much further we need to go. Make this move, to reach our \\
|
|
target position.
|
|
|
|
Moving back to the target position in two steps allows us to correct for
|
|
backlash, by using the sharpness-vs-z curve as a rough encoder for Z.
|
|
|
|
Parameters:
|
|
dz: number of steps over which to scan (optional, default 2000)
|
|
|
|
target_z: we aim to finish at this position, relative to focus. This may
|
|
be useful if, for example, you want to acquire a stack of images in Z.
|
|
It is optional, and the default value of 0 will finish at the focus.
|
|
|
|
initial_move_up: (optional, default True) set this to `False` to move down
|
|
from the starting position. Mostly useful if you're able to combine
|
|
the initial move with something else, e.g. moving to the next scan point.
|
|
|
|
mini_backlash: (optional, default 50) is a small extra move made in step
|
|
3 to help counteract backlash. It should be small enough that you
|
|
would always expect there to be greater backlash than this. Too small
|
|
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
|
|
may cause you to overshoot, which is a problem.
|
|
"""
|
|
with self.monitor_sharpness() as m, self.microscope.camera.lock:
|
|
# Ensure the MJPEG stream has started
|
|
self.microscope.camera.start_stream_recording()
|
|
|
|
df = dz #TODO: refactor so I actually use dz in the code below!
|
|
if initial_move_up:
|
|
m.focus_rel(df/2)
|
|
# move down
|
|
i, z = m.focus_rel(-df)
|
|
# now inspect where the sharpest point is, and estimate the sharpness
|
|
# (JPEG size) that we should find at the start of the Z stack
|
|
jt, jz, js = m.move_data(i)
|
|
best_z = jz[np.argmax(js)]
|
|
target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing
|
|
|
|
# now move to the start of the z stack
|
|
i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack
|
|
|
|
# We've deliberately undershot - figure out how much further we should move based on the curve
|
|
current_js = m.jpeg_size()
|
|
imax = np.argmax(js) # we want to crop out just the bit below the peak
|
|
js = js[imax:] # NB z is in DECREASING order
|
|
jz = jz[imax:]
|
|
inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position
|
|
# TODO: fancy interpolation stuff
|
|
|
|
# So, the Z position corresponding to our current sharpness value is zs[inow]
|
|
# That means we should move forwards, by best_z - zs[inow]
|
|
correction_move = best_z + target_z - jz[inow]
|
|
logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move))
|
|
m.focus_rel(correction_move)
|
|
return m.data_dict()
|
|
|