Overhauled autofocus tracker

This commit is contained in:
Joel Collins 2020-10-22 16:00:52 +01:00
parent 68748a26c5
commit 3c63d6a240

View file

@ -1,7 +1,7 @@
import logging import logging
import time import time
from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from threading import Event, Thread
import numpy as np import numpy as np
from labthings import current_action, fields, find_component from labthings import current_action, fields, find_component
@ -14,80 +14,100 @@ from openflexure_microscope.utilities import set_properties
### Autofocus utilities ### Autofocus utilities
# Class to store a frames metadata
TrackerFrame = namedtuple("TrackerFrame", ["size", "time"])
class FrameTracker(object):
"""
A file-like object used to analyse MJPEG frames.
Instead of analysing a load of real MJPEG frames
after they've been stored in a BytesIO stream,
we tell the camera to write frames to this class instead.
We then do analysis as the frames are written, and discard the heavier
image data.
"""
def __init__(self):
# Array of TrackerFrame objects
self.frames = []
self.last = None
def write(self, s):
# Store the incoming frame metadata
# Discards the actual frame image data
# NOTE: This could be used for smarter processing
# E.g. different sharpness metrics
frame = TrackerFrame(size=len(s), time=time.time())
self.frames.append(frame)
self.last = frame
def flush(self):
# Called at end of recording
pass
class JPEGSharpnessMonitor: class JPEGSharpnessMonitor:
def __init__(self, microscope, timeout=60): def __init__(self, microscope):
self.microscope = microscope self.microscope = microscope
self.camera = microscope.camera self.camera = microscope.camera
self.stage = microscope.stage self.stage = microscope.stage
self.jpeg_sizes = []
self.jpeg_times = [] self.recording_start_time = None
self.stage_positions = [] self.stage_positions = []
self.stage_times = [] self.stage_times = []
self.stop_event = Event() self.jpeg_times = []
self.timeout = timeout self.jpeg_sizes = []
self.keep_alive()
self.background_thread = None
def is_alive(self): # Splitter port 3 is reserved for autofocus
if self.background_thread is None: self.splitter_port = 3
return False self.stream = FrameTracker()
else:
return self.background_thread.is_alive()
def should_stop(self): def stop_recording(self):
return time.time() - self.kept_alive > self.timeout logging.info("Stopping recording for autofocus...")
self.camera.camera.stop_recording(splitter_port=self.splitter_port)
def keep_alive(self): logging.info("Stopped recording for autofocus!")
self.kept_alive = time.time()
def start(self): def start(self):
"Start monitoring sharpness by looking at JPEG size" # Log the recording start time
if not self.camera.stream_active: self.recording_start_time = time.time()
logging.warning( # Start the stream recording
"Autofocus sharpness monitor was started but the camera isn't streaming. Attempting to start the stream..." self.camera.camera.start_recording(
) self.stream,
self.camera.start_stream_recording() format="mjpeg",
self.background_thread = Thread(target=self._measure_jpegs) quality=100, # Use best quality (we don't store the frames anyway!)
self.background_thread.start() bitrate=-1, # RWB: disable bitrate control
return self # (bitrate control makes JPEG size less good as a focus
# metric)
splitter_port=self.splitter_port,
)
logging.info("Started recording for autofocus")
def stop(self): def stop(self):
"Stop the background thread" self.stop_recording()
self.stop_event.set() logging.info("Stopped recording for autofocus")
logging.info("Joining JPEG thread")
self.background_thread.join()
def _measure_jpegs(self):
"Function that runs in a background thread to record sharpness"
logging.debug("Starting sharpness measurement in background thread")
self.keep_alive()
logging.debug("_measure_jpegs stop_event: %s", self.stop_event.is_set())
while not self.stop_event.is_set() and not self.should_stop():
size_now = self.jpeg_size()
time_now = time.time()
self.jpeg_sizes.append(size_now)
self.jpeg_times.append(time_now)
logging.info("Exited JPEG measure loop")
if self.stop_event.is_set():
logging.debug("Cleanly stopped sharpness measurement in background thread")
if self.should_stop():
logging.debug("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): def focus_rel(self, dz, backlash=False, **kwargs):
self.keep_alive() # Store the start time and position
# Start time and position
self.stage_times.append(time.time()) self.stage_times.append(time.time())
self.stage_positions.append(self.stage.position) self.stage_positions.append(self.stage.position)
# Main move # Main move
self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs)
# End time and position
# Store the end time and position
self.stage_times.append(time.time()) self.stage_times.append(time.time())
self.stage_positions.append(self.stage.position) self.stage_positions.append(self.stage.position)
# Retrieve frame data
for frame in self.stream.frames:
# Make timestamp absolute Unix time
self.jpeg_times.append(frame.time)
self.jpeg_sizes.append(frame.size)
# Index of the data for this movement # Index of the data for this movement
data_index = len(self.stage_positions) - 2 data_index = len(self.stage_positions) - 2
# Final z position after move # Final z position after move
@ -309,7 +329,7 @@ def fast_up_down_up_autofocus(
# We've deliberately undershot - figure out how much further we should move based on the curve # We've deliberately undershot - figure out how much further we should move based on the curve
logging.debug("Calculate remining movement") logging.debug("Calculate remining movement")
current_js = m.jpeg_size() current_js = m.stream.last.size
imax = np.argmax(js) # we want to crop out just the bit below the peak imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:] jz = jz[imax:]