Changes based on review, mostly enums and moved stack params

This commit is contained in:
Joe Knapper 2026-02-19 15:05:37 +00:00 committed by Julian Stirling
parent 37e3450670
commit 331ceb705d
2 changed files with 54 additions and 19 deletions

View file

@ -11,6 +11,7 @@ import logging
import os import os
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from types import TracebackType from types import TracebackType
from typing import Literal, Mapping, Optional, Self, Sequence from typing import Literal, Mapping, Optional, Self, Sequence
@ -33,11 +34,25 @@ class NotStreamingError(RuntimeError):
"""No images captured from stream. The camera is almost certainly not streaming.""" """No images captured from stream. The camera is almost certainly not streaming."""
class SharpnessMethod(str, Enum):
"""The possible SharpnessMethods for autofocus."""
jpeg = "jpeg"
class AutofocusParams(BaseModel): class AutofocusParams(BaseModel):
"""A class for running autofocus routines.""" """A class for running autofocus routines."""
dz: int dz: int
sharpness_method: str = "jpeg" sharpness_method: SharpnessMethod = SharpnessMethod.jpeg
class StackOrigin(str, Enum):
"""The position of the current location in the stack."""
START = "start"
CENTER = "center"
END = "end"
class StackParams(BaseModel): class StackParams(BaseModel):
@ -52,15 +67,28 @@ class StackParams(BaseModel):
settling_time: float = 0.3 settling_time: float = 0.3
"""Time (in seconds) between moving and capturing an image""" """Time (in seconds) between moving and capturing an image"""
backlash_correction: int = 250
"""
Distance (in steps) to overshoot a move and then undo, to account for backlash
"""
origin: StackOrigin = StackOrigin.START
"""Where the stack is positioned relative to the current z position."""
class SmartStackParams(StackParams): class SmartStackParams(StackParams):
"""A class for holding smart stack parameters, and returning computed ones.""" """A class for holding smart stack parameters, and returning computed ones."""
min_images_to_test: int min_images_to_test: int
backlash_correction: int = 250 save_on_failure: bool = False
"""Whether to save an image even if no focus was found."""
check_turning_points: bool = True
""" """
Distance (in steps) to overshoot a move and then undo, to account for backlash Whether to check the number of turning points in
the sharpnesses of the images in the stack is exactly 1.
(May fail with thick samples)
""" """
stack_height_limit: int = 15 stack_height_limit: int = 15
@ -468,8 +496,6 @@ class AutofocusThing(lt.Thing):
stack_parameters: SmartStackParams, stack_parameters: SmartStackParams,
capture_parameters: CaptureParams, capture_parameters: CaptureParams,
autofocus_parameters: AutofocusParams, autofocus_parameters: AutofocusParams,
save_on_failure: bool = False,
check_turning_points: bool = True,
) -> tuple[bool, int]: ) -> tuple[bool, int]:
"""Run a smart stack. """Run a smart stack.
@ -481,10 +507,6 @@ class AutofocusThing(lt.Thing):
:param stack_parameters: A SmartStackParams object containing the required :param stack_parameters: A SmartStackParams object containing the required
parameters to run a stack. parameters to run a stack.
:param save_on_failure: Whether to save an image even if no focus was found.
:param check_turning_points: Whether to check the number of turning points in
the sharpnesses of the images in the stack is exactly 1. (May fail with
thick samples)
:returns: A tuple containing: :returns: A tuple containing:
@ -496,13 +518,10 @@ class AutofocusThing(lt.Thing):
attempt += 1 attempt += 1
success, captures, sharpest_id = self.smart_z_stack( success, captures, sharpest_id = self.smart_z_stack(
stack_parameters=stack_parameters, stack_parameters=stack_parameters,
check_turning_points=check_turning_points, check_turning_points=stack_parameters.check_turning_points,
) )
if success: if success or attempt >= stack_parameters.max_attempts:
break
if attempt >= stack_parameters.max_attempts:
break break
# The z position of the first images in the previous attempt. # The z position of the first images in the previous attempt.
@ -516,7 +535,7 @@ class AutofocusThing(lt.Thing):
# Save stack_parameters.image_to_save images centred on the sharpest capture. # Save stack_parameters.image_to_save images centred on the sharpest capture.
# If the smart_stack failed the exact number of images saved may not be # If the smart_stack failed the exact number of images saved may not be
# stack_parameters.image_to_save # stack_parameters.image_to_save
if success or save_on_failure: if success or stack_parameters.save_on_failure:
self.save_stack( self.save_stack(
sharpest_id=sharpest_id, sharpest_id=sharpest_id,
captures=captures, captures=captures,
@ -747,6 +766,24 @@ class AutofocusThing(lt.Thing):
captures: list[CaptureInfo] = [] captures: list[CaptureInfo] = []
z_positions: list[int] = [] z_positions: list[int] = []
total_range = stack_parameters.stack_dz * (stack_parameters.images_to_save - 1)
# Determine starting offset based on stack origin
if stack_parameters.origin == StackOrigin.CENTER:
target_offset = -total_range // 2
elif stack_parameters.origin == StackOrigin.END:
target_offset = -total_range
else:
target_offset = 0
# Apply backlash correction: overshoot and move back
# Overshoot past the starting position
self._stage.move_relative(
z=target_offset - stack_parameters.backlash_correction
)
# Move back to the exact starting point
self._stage.move_relative(z=stack_parameters.backlash_correction)
# Capture images_to_save images # Capture images_to_save images
for _ in range(stack_parameters.images_to_save): for _ in range(stack_parameters.images_to_save):
time.sleep(stack_parameters.settling_time) time.sleep(stack_parameters.settling_time)

View file

@ -481,6 +481,7 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
stack_dz=self.stack_dz, stack_dz=self.stack_dz,
images_to_save=self.stack_images_to_save, images_to_save=self.stack_images_to_save,
min_images_to_test=self.stack_min_images_to_test, min_images_to_test=self.stack_min_images_to_test,
save_on_failure=not self.skip_background,
) )
def pre_scan_routine(self, settings: HistoScanSettingsModel) -> None: def pre_scan_routine(self, settings: HistoScanSettingsModel) -> None:
@ -539,17 +540,14 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
self.logger.info(msg) self.logger.info(msg)
return False, None return False, None
save_on_failure = not settings.skip_background
focus_height: Optional[int] focus_height: Optional[int]
focused, focus_height = self._autofocus.run_smart_stack( focused, focus_height = self._autofocus.run_smart_stack(
stack_parameters=settings.smart_stack_params, stack_parameters=settings.smart_stack_params,
capture_parameters=settings.capture_params, capture_parameters=settings.capture_params,
autofocus_parameters=settings.autofocus_params, autofocus_parameters=settings.autofocus_params,
save_on_failure=save_on_failure,
) )
# An image was captured if we are focussed or we are not skipping background. # An image was captured if we are focussed or we are not skipping background.
imaged = focused or save_on_failure imaged = focused or settings.smart_stack_params.save_on_failure
if not imaged: if not imaged:
msg = f"Stack failed at {xyz_pos}. Treating as background." msg = f"Stack failed at {xyz_pos}. Treating as background."