background detect and better focus handling
The primary change here is that there's now an option to skip autofocus if the background detect plugin says the current image is background. I have also overhauled the way it picks the next Z position based on Joe's code; instead of just using the last point, it will pick the closest point where we have a successful autofocus recorded. This will usually be the last point, except for raster scans where it neatly reproduces the behaviour of the old code but without needing to treat it as a special case (when we jump back to the start of a line, it will use the z position of the start of the previous line, rather than the end of the previous line). This does represent a minor change to previous behaviour, but it should not break anything that isn't already broken, i.e. it might cause slightly odd behaviour if autofocus gives random results - but probably indistinguishable from the current behaviour.
This commit is contained in:
parent
fe3af0fd29
commit
333b52aa9b
1 changed files with 78 additions and 22 deletions
|
|
@ -15,6 +15,7 @@ from labthings import (
|
|||
)
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
import numpy as np
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
|
||||
|
|
@ -95,6 +96,30 @@ def construct_grid(
|
|||
return arr
|
||||
|
||||
|
||||
def closest_point_in_xy(current_position: XyCoordinate, points: List[XyzCoordinate]):
|
||||
"""Find the closest point in a list
|
||||
|
||||
Given a 2D position, find the 3D position that's closest in XY and return it.
|
||||
In the event of a tie, the most recent (i.e. latest in the list) is returned.
|
||||
|
||||
If the list is empty, we return None
|
||||
"""
|
||||
if len(points) < 1:
|
||||
return None
|
||||
points_2d = np.asarray(points)[:, :2]
|
||||
|
||||
squared_distances = np.sum((points_2d - current_position)**2, axis=1)
|
||||
# We reverse the distances before searching, as argmin will return the first
|
||||
# point in the event of there being multiple points with the same minimum,
|
||||
# and we want to pick the last one.
|
||||
reverse_min_index = np.argmin(squared_distances[::-1])
|
||||
# of course, now we must convert the index to be the right way round
|
||||
min_index = len(points) - 1 - reverse_min_index
|
||||
if min_index != len(points) - 1:
|
||||
logging.info(f"Using point {min_index} of {len(points)} for focus.")
|
||||
return points[min_index]
|
||||
|
||||
|
||||
### Capturing
|
||||
|
||||
|
||||
|
|
@ -175,6 +200,7 @@ class ScanExtension(BaseExtension):
|
|||
metadata: Optional[dict] = None,
|
||||
annotations: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
detect_empty_fields_and_skip_autofocus: bool = False
|
||||
):
|
||||
metadata = metadata or {}
|
||||
annotations = annotations or {}
|
||||
|
|
@ -182,16 +208,23 @@ class ScanExtension(BaseExtension):
|
|||
|
||||
start = time.time()
|
||||
|
||||
# Store initial position
|
||||
initial_position = microscope.stage.position
|
||||
# Construct an x-y grid (worry about z later)
|
||||
x_y_grid = construct_grid(
|
||||
initial_position[:2], stride_size[:2], grid[:2], style=style
|
||||
) # This is a list of lists.
|
||||
|
||||
# Keep task progress
|
||||
self._images_to_be_captured = reduce((lambda x, y: x * y), grid)
|
||||
# NB the number of points is found from the number of elements in
|
||||
# x_y_grid; this is not guaranteed to be the same in every row.
|
||||
self._images_to_be_captured = sum([len(line) for line in x_y_grid])
|
||||
self._images_captured_so_far = 0
|
||||
|
||||
# Generate a basename if none given
|
||||
if not basename:
|
||||
basename = generate_basename()
|
||||
|
||||
# Store initial position
|
||||
initial_position = microscope.stage.position
|
||||
|
||||
# Add dataset metadata
|
||||
dataset_d = {
|
||||
|
|
@ -217,32 +250,38 @@ class ScanExtension(BaseExtension):
|
|||
else:
|
||||
autofocus_enabled = False
|
||||
|
||||
# Construct an x-y grid (worry about z later)
|
||||
x_y_grid = construct_grid(
|
||||
initial_position[:2], stride_size[:2], grid[:2], style=style
|
||||
)
|
||||
if detect_empty_fields_and_skip_autofocus:
|
||||
# Check for the background detect extension if we need it, raise an error now if it's missing.
|
||||
background_detect_extension = find_extension("org.openflexure.background-detect")
|
||||
if not background_detect_extension:
|
||||
raise RuntimeError("Detecting background fields requires the background detect extension and it was not found.")
|
||||
|
||||
# Keep the initial Z position the same as our current position
|
||||
initial_z = initial_position[2]
|
||||
next_z = initial_z # Save this value for use in raster scans
|
||||
focused_positions: List[XyzCoordinate] = [] # Positions where we found sharp images
|
||||
|
||||
# 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":
|
||||
next_z = initial_z # Reset z position at start of each new row
|
||||
logging.debug("Returning to initial z position")
|
||||
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
|
||||
# Set the next z position based on the closest point that was in focus.
|
||||
# For a snake/spiral scan, this should always be the last point, unless
|
||||
# it's skipped for some reason. In a raster scan, this should be the last
|
||||
# point, except when we're at the start of a row when it will be the first
|
||||
# point of the preceding row.
|
||||
closest_focused_point = closest_point_in_xy(x_y, focused_positions)
|
||||
next_z = closest_focused_point[2] if closest_focused_point else initial_position[2]
|
||||
# Move to new grid position
|
||||
logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
|
||||
microscope.stage.move_abs((x_y[0], x_y[1], next_z))
|
||||
# Refocus
|
||||
if autofocus_enabled:
|
||||
|
||||
# Check if the current field looks empty, and skip autofocus if it does
|
||||
skip_autofocus = False
|
||||
if detect_empty_fields_and_skip_autofocus:
|
||||
verdict = background_detect_extension.grab_and_classify_image()
|
||||
logging.debug(f"Background detection verdict: {verdict}")
|
||||
if verdict["classification"] == "background":
|
||||
skip_autofocus = True
|
||||
logging.info(f"Detected an empty field at {x_y}, skipping autofocus.")
|
||||
|
||||
if autofocus_enabled and not skip_autofocus:
|
||||
if fast_autofocus:
|
||||
# Run fast autofocus. Client should provide dz ~ 2000
|
||||
autofocus_extension.fast_autofocus(microscope, dz=autofocus_dz)
|
||||
|
|
@ -254,6 +293,21 @@ class ScanExtension(BaseExtension):
|
|||
)
|
||||
logging.debug("Finished autofocus")
|
||||
time.sleep(1)
|
||||
autofocus_accepted = True
|
||||
here = microscope.stage.position
|
||||
# Check if we've moved worryingly far in Z
|
||||
if closest_focused_point:
|
||||
lateral_move = np.sqrt(np.sum((np.array(here)[:2] - np.array(closest_focused_point)[:2])**2))
|
||||
axial_move = np.abs(here[2] - closest_focused_point[2])
|
||||
if axial_move > lateral_move * 0.4:
|
||||
autofocus_accepted = False
|
||||
logging.warn(
|
||||
f"During a scan, there was a large axial jump from {closest_focused_point}"
|
||||
f" to {here}. This may mean autofocus has failed."
|
||||
)
|
||||
# Append the (current, i.e. focused) position to the list
|
||||
if autofocus_accepted:
|
||||
focused_positions.append(here)
|
||||
|
||||
# If we're not doing a z-stack, just capture
|
||||
if grid[2] <= 1:
|
||||
|
|
@ -373,6 +427,7 @@ class TileScanArgs(FullCaptureArgs):
|
|||
stride_size = fields.List(
|
||||
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
||||
)
|
||||
detect_empty_fields_and_skip_autofocus = fields.Boolean(missing=False)
|
||||
|
||||
|
||||
class TileScanAPI(ActionView):
|
||||
|
|
@ -418,4 +473,5 @@ class TileScanAPI(ActionView):
|
|||
fast_autofocus=args.get("fast_autofocus"),
|
||||
annotations=args.get("annotations"),
|
||||
tags=args.get("tags"),
|
||||
detect_empty_fields_and_skip_autofocus=args.get("detect_empty_fields_and_skip_autofocus"),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue