Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Laser-Based Focus display to show results of running laser autofocus #126

Merged
merged 7 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from configparser import ConfigParser
import json
import csv
from control.utils import SpotDetectionMode
import squid.logging
from enum import Enum, auto

Expand Down Expand Up @@ -259,6 +258,23 @@ def from_string(cls, mode_str: str) -> "ZStageConfig":
return mapping[mode_str.lower()]


class SpotDetectionMode(Enum):
"""Specifies which spot to detect when multiple spots are present.

SINGLE: Expect and detect single spot
DUAL_RIGHT: In dual-spot case, use rightmost spot
DUAL_LEFT: In dual-spot case, use leftmost spot
MULTI_RIGHT: In multi-spot case, use rightmost spot
MULTI_SECOND_RIGHT: In multi-spot case, use spot immediately left of rightmost spot
"""

SINGLE = "single"
DUAL_RIGHT = "dual_right"
DUAL_LEFT = "dual_left"
MULTI_RIGHT = "multi_right"
MULTI_SECOND_RIGHT = "multi_second_right"


PRINT_CAMERA_FPS = True

###########################################################
Expand Down
66 changes: 54 additions & 12 deletions software/control/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3017,7 +3017,6 @@ def __init__(
liveController=None,
contrastManager=None,
window_title="",
draw_crosshairs=False,
show_LUT=False,
autoLevels=False,
):
Expand Down Expand Up @@ -3074,7 +3073,6 @@ def __init__(
self.ptRect2 = None
self.DrawCirc = False
self.centroid = None
self.DrawCrossHairs = False
self.image_offset = np.array([0, 0])

## Layout
Expand Down Expand Up @@ -3159,6 +3157,43 @@ def display_image(self, image):

self.graphics_widget.img.updateImage()

def mark_spot(self, image: np.ndarray, x: float, y: float):
"""Mark the detected laserspot location on the image.

Args:
image: Image to mark
x: x-coordinate of the spot
y: y-coordinate of the spot

Returns:
Image with marked spot
"""
# Draw a green crosshair at the specified x,y coordinates
crosshair_size = 10 # Size of crosshair lines in pixels
crosshair_color = (0, 255, 0) # Green in BGR format
crosshair_thickness = 1
x = int(round(x))
y = int(round(y))

# Convert grayscale to BGR
marked_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)

# Draw horizontal line
cv2.line(marked_image,
(x - crosshair_size, y),
(x + crosshair_size, y),
crosshair_color,
crosshair_thickness)

# Draw vertical line
cv2.line(marked_image,
(x, y - crosshair_size),
(x, y + crosshair_size),
crosshair_color,
crosshair_thickness)

self.display_image(marked_image)

def update_contrast_limits(self):
if self.show_LUT and self.contrastManager and self.contrastManager.acquisition_dtype:
min_val, max_val = self.LUTWidget.region.getRegion()
Expand Down Expand Up @@ -4589,6 +4624,7 @@ class LaserAutofocusController(QObject):

image_to_display = Signal(np.ndarray)
signal_displacement_um = Signal(float)
signal_cross_correlation = Signal(float)
signal_piezo_position_update = Signal() # Signal to emit piezo position updates

def __init__(
Expand Down Expand Up @@ -4869,7 +4905,9 @@ def move_to_target(self, target_um: float) -> bool:
self._move_z(um_to_move)

# Verify using cross-correlation that spot is in same location as reference
if not self._verify_spot_alignment():
cc_result, correlation = self._verify_spot_alignment()
self.signal_cross_correlation.emit(correlation)
if not cc_result:
self._log.warning("Cross correlation check failed - spots not well aligned")
# move back to the current position
self._move_z(-um_to_move)
Expand Down Expand Up @@ -4970,7 +5008,7 @@ def on_objective_changed(self) -> None:
self.has_reference = False
self.load_cached_configuration()

def _verify_spot_alignment(self) -> bool:
def _verify_spot_alignment(self) -> Tuple[bool, float]:
"""Verify laser spot alignment using cross-correlation with reference image.

Captures current laser spot image and compares it with the reference image
Expand Down Expand Up @@ -5025,7 +5063,7 @@ def _verify_spot_alignment(self) -> bool:
self._log.warning("Cross correlation check failed - spots not well aligned")
return False

return True
return True, correlation

def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]:
"""Get the centroid location of the laser spot.
Expand Down Expand Up @@ -5061,11 +5099,17 @@ def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]:
self.image = image # store for debugging # TODO: add to return instead of storing

# calculate centroid
result = utils.find_spot_location(image, mode=self.laser_af_properties.spot_detection_mode)
spot_detection_params = {
"y_window": self.laser_af_properties.y_window,
"x_window": self.laser_af_properties.x_window,
"peak_width": self.laser_af_properties.min_peak_width,
"peak_distance": self.laser_af_properties.min_peak_distance,
"peak_prominence": self.laser_af_properties.min_peak_prominence,
"spot_spacing": self.laser_af_properties.spot_spacing
}
result = utils.find_spot_location(image, mode=self.laser_af_properties.spot_detection_mode, params=spot_detection_params)
if result is None:
self._log.warning(
f"No spot detected in frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}"
)
self._log.warning(f"No spot detected in frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}")
continue

x, y = result
Expand All @@ -5074,9 +5118,7 @@ def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]:
successful_detections += 1

except Exception as e:
self._log.error(
f"Error processing frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}: {str(e)}"
)
self._log.error(f"Error processing frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}: {str(e)}")
continue

# optionally display the image
Expand Down
19 changes: 12 additions & 7 deletions software/control/gui_hcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def loadObjects(self, is_simulation):
for_displacement_measurement=True,
)
self.imageDisplayWindow_focus = core.ImageDisplayWindow(
draw_crosshairs=True, show_LUT=False, autoLevels=False
show_LUT=False, autoLevels=False
)
self.displacementMeasurementController = core_displacement_measurement.DisplacementMeasurementController()
self.laserAutofocusController = core.LaserAutofocusController(
Expand Down Expand Up @@ -644,18 +644,18 @@ def loadWidgets(self):
self.laserAutofocusControlWidget: widgets.LaserAutofocusControlWidget = widgets.LaserAutofocusControlWidget(
self.laserAutofocusController
)
self.imageDisplayWindow_focus = core.ImageDisplayWindow(draw_crosshairs=True)
self.imageDisplayWindow_focus = core.ImageDisplayWindow()

self.imageDisplayTabs = QTabWidget()
if self.live_only_mode:
if ENABLE_TRACKING:
self.imageDisplayWindow = core.ImageDisplayWindow(
self.liveController, self.contrastManager, draw_crosshairs=True
self.liveController, self.contrastManager
)
self.imageDisplayWindow.show_ROI_selector()
else:
self.imageDisplayWindow = core.ImageDisplayWindow(
self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True
self.liveController, self.contrastManager, show_LUT=True, autoLevels=True
)
self.imageDisplayTabs = self.imageDisplayWindow.widget
self.napariMosaicDisplayWidget = None
Expand Down Expand Up @@ -716,12 +716,12 @@ def setupImageDisplayTabs(self):
else:
if ENABLE_TRACKING:
self.imageDisplayWindow = core.ImageDisplayWindow(
self.liveController, self.contrastManager, draw_crosshairs=True
self.liveController, self.contrastManager
)
self.imageDisplayWindow.show_ROI_selector()
else:
self.imageDisplayWindow = core.ImageDisplayWindow(
self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True
self.liveController, self.contrastManager, show_LUT=True, autoLevels=True
)
self.imageDisplayTabs.addTab(self.imageDisplayWindow.widget, "Live View")

Expand Down Expand Up @@ -1011,13 +1011,18 @@ def connect_objective_changed_laser_af():
self.laserAutofocusSettingWidget.signal_apply_settings.connect(
self.laserAutofocusControlWidget.update_init_state
)

self.laserAutofocusSettingWidget.signal_laser_spot_location.connect(
self.imageDisplayWindow_focus.mark_spot
)
self.laserAutofocusSettingWidget.update_exposure_time(
self.laserAutofocusSettingWidget.exposure_spinbox.value()
)
self.laserAutofocusSettingWidget.update_analog_gain(
self.laserAutofocusSettingWidget.analog_gain_spinbox.value()
)
self.laserAutofocusController.signal_cross_correlation.connect(
self.laserAutofocusSettingWidget.show_cross_correlation_result
)

self.streamHandler_focus_camera.signal_new_frame_received.connect(
self.liveController_focus_camera.on_new_frame
Expand Down
40 changes: 16 additions & 24 deletions software/control/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@
from scipy import signal
import os
from typing import Optional, Tuple
from enum import Enum, auto

from control._def import (
LASER_AF_Y_WINDOW,
LASER_AF_X_WINDOW,
LASER_AF_MIN_PEAK_WIDTH,
LASER_AF_MIN_PEAK_DISTANCE,
LASER_AF_MIN_PEAK_PROMINENCE,
LASER_AF_SPOT_SPACING,
SpotDetectionMode,
)
import squid.logging

_log = squid.logging.get_logger("control.utils")
Expand Down Expand Up @@ -174,23 +183,6 @@ def ensure_directory_exists(raw_string_path: str):
path.mkdir(parents=True, exist_ok=True)


class SpotDetectionMode(Enum):
"""Specifies which spot to detect when multiple spots are present.

SINGLE: Expect and detect single spot
DUAL_RIGHT: In dual-spot case, use rightmost spot
DUAL_LEFT: In dual-spot case, use leftmost spot
MULTI_RIGHT: In multi-spot case, use rightmost spot
MULTI_SECOND_RIGHT: In multi-spot case, use spot immediately left of rightmost spot
"""

SINGLE = "single"
DUAL_RIGHT = "dual_right"
DUAL_LEFT = "dual_left"
MULTI_RIGHT = "multi_right"
MULTI_SECOND_RIGHT = "multi_second_right"


def find_spot_location(
image: np.ndarray,
mode: SpotDetectionMode = SpotDetectionMode.SINGLE,
Expand Down Expand Up @@ -224,13 +216,13 @@ def find_spot_location(

# Default parameters
default_params = {
"y_window": 96, # Half-height of y-axis crop
"x_window": 20, # Half-width of centroid window
"min_peak_width": 10, # Minimum width of peaks
"min_peak_distance": 10, # Minimum distance between peaks
"min_peak_prominence": 0.25, # Minimum peak prominence
"y_window": LASER_AF_Y_WINDOW, # Half-height of y-axis crop
"x_window": LASER_AF_X_WINDOW, # Half-width of centroid window
"min_peak_width": LASER_AF_MIN_PEAK_WIDTH, # Minimum width of peaks
"min_peak_distance": LASER_AF_MIN_PEAK_DISTANCE, # Minimum distance between peaks
"min_peak_prominence": LASER_AF_MIN_PEAK_PROMINENCE, # Minimum peak prominence
"intensity_threshold": 0.1, # Threshold for intensity filtering
"spot_spacing": 100, # Expected spacing between spots
"spot_spacing": LASER_AF_SPOT_SPACING, # Expected spacing between spots
}

if params is not None:
Expand Down
Loading
Loading