From b4487a72836f1f5ab3c45457ed561735025bfe87 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sun, 23 Feb 2025 23:50:03 -0800 Subject: [PATCH 1/6] pass in parameters for get_laser_spot_centroid and find_spot_location --- software/control/_def.py | 18 ++++++- software/control/core/core.py | 92 +++++++++++++++++++++++++++++------ software/control/utils.py | 40 ++++++--------- 3 files changed, 109 insertions(+), 41 deletions(-) diff --git a/software/control/_def.py b/software/control/_def.py index 86596292..c6816f77 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -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 @@ -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 ########################################################### diff --git a/software/control/core/core.py b/software/control/core/core.py index 6dbd1c8a..f8911efc 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -4698,7 +4698,18 @@ def initialize_auto(self) -> bool: self.microcontroller.turn_on_AF_laser() self.microcontroller.wait_till_operation_is_completed() - result = self._get_laser_spot_centroid() + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence + } + result = self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) if result is None: self._log.error("Failed to find laser spot during initialization") self.microcontroller.turn_off_AF_laser() @@ -4748,7 +4759,18 @@ def _calibrate_pixel_to_um(self) -> bool: self.stage.move_z(-1.5 * self.laser_af_properties.pixel_to_um_calibration_distance / 1000) self.stage.move_z(self.laser_af_properties.pixel_to_um_calibration_distance / 1000) - result = self._get_laser_spot_centroid() + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence + } + result = self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) if result is None: self._log.error("Failed to find laser spot during calibration (position 1)") self.microcontroller.turn_off_AF_laser() @@ -4760,7 +4782,11 @@ def _calibrate_pixel_to_um(self) -> bool: self._move_z(self.laser_af_properties.pixel_to_um_calibration_distance) time.sleep(MULTIPOINT_PIEZO_DELAY_MS / 1000) - result = self._get_laser_spot_centroid() + result = self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) if result is None: self._log.error("Failed to find laser spot during calibration (position 2)") self.microcontroller.turn_off_AF_laser() @@ -4817,7 +4843,18 @@ def measure_displacement(self) -> float: self.microcontroller.wait_till_operation_is_completed() # get laser spot location - result = self._get_laser_spot_centroid() + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence + } + result = self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) # turn off the laser self.microcontroller.turn_off_AF_laser() @@ -4908,7 +4945,18 @@ def set_reference(self) -> bool: self.microcontroller.wait_till_operation_is_completed() # get laser spot location and image - result = self._get_laser_spot_centroid() + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence + } + result = self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) reference_image = self.image # turn off the laser @@ -4971,7 +5019,18 @@ def _verify_spot_alignment(self) -> bool: self.camera.send_trigger() current_image = self.camera.read_frame() """ - self._get_laser_spot_centroid() + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence + } + self.get_laser_spot_centroid( + averaging_n=self.laser_af_properties.laser_af_averaging_n, + spot_detection_mode=self.laser_af_properties.spot_detection_mode, + spot_detection_params=spot_detection_params + ) current_image = self.image self.microcontroller.turn_off_AF_laser() @@ -5005,7 +5064,12 @@ def _verify_spot_alignment(self) -> bool: return True - def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]: + def get_laser_spot_centroid( + self, + averaging_n: int, + spot_detection_mode: SpotDetectionMode, + spot_detection_params: dict + ) -> Optional[Tuple[float, float]]: """Get the centroid location of the laser spot. Averages multiple measurements to improve accuracy. The number of measurements @@ -5021,7 +5085,7 @@ def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]: tmp_x = 0 tmp_y = 0 - for i in range(self.laser_af_properties.laser_af_averaging_n): + for i in range(averaging_n): try: # send camera trigger if self.liveController.trigger_mode == TriggerMode.SOFTWARE: @@ -5033,17 +5097,15 @@ def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]: # read camera frame image = self.camera.read_frame() if image is None: - self._log.warning(f"Failed to read frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}") + self._log.warning(f"Failed to read frame {i+1}/{averaging_n}") continue 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) + result = utils.find_spot_location(image, mode=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}/{averaging_n}") continue x, y = result @@ -5052,9 +5114,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}/{averaging_n}: {str(e)}") continue # optionally display the image diff --git a/software/control/utils.py b/software/control/utils.py index 06b4f773..0e8d6913 100644 --- a/software/control/utils.py +++ b/software/control/utils.py @@ -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") @@ -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, @@ -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: From ff68b924ee71cf29c24fd074027e4493ff7b2002 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 24 Feb 2025 12:50:17 -0800 Subject: [PATCH 2/6] show crosshair on laser spot --- software/control/core/core.py | 133 +++++++++++++++------------------- software/control/gui_hcs.py | 4 +- software/control/widgets.py | 97 ++++++++++++++++--------- 3 files changed, 124 insertions(+), 110 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index f8911efc..45b7e213 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3165,6 +3165,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() @@ -4698,18 +4735,7 @@ def initialize_auto(self) -> bool: self.microcontroller.turn_on_AF_laser() self.microcontroller.wait_till_operation_is_completed() - spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence - } - result = self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + result = self._get_laser_spot_centroid() if result is None: self._log.error("Failed to find laser spot during initialization") self.microcontroller.turn_off_AF_laser() @@ -4759,18 +4785,7 @@ def _calibrate_pixel_to_um(self) -> bool: self.stage.move_z(-1.5 * self.laser_af_properties.pixel_to_um_calibration_distance / 1000) self.stage.move_z(self.laser_af_properties.pixel_to_um_calibration_distance / 1000) - spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence - } - result = self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + result = self._get_laser_spot_centroid() if result is None: self._log.error("Failed to find laser spot during calibration (position 1)") self.microcontroller.turn_off_AF_laser() @@ -4782,11 +4797,7 @@ def _calibrate_pixel_to_um(self) -> bool: self._move_z(self.laser_af_properties.pixel_to_um_calibration_distance) time.sleep(MULTIPOINT_PIEZO_DELAY_MS / 1000) - result = self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + result = self._get_laser_spot_centroid() if result is None: self._log.error("Failed to find laser spot during calibration (position 2)") self.microcontroller.turn_off_AF_laser() @@ -4843,18 +4854,7 @@ def measure_displacement(self) -> float: self.microcontroller.wait_till_operation_is_completed() # get laser spot location - spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence - } - result = self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + result = self._get_laser_spot_centroid() # turn off the laser self.microcontroller.turn_off_AF_laser() @@ -4945,18 +4945,7 @@ def set_reference(self) -> bool: self.microcontroller.wait_till_operation_is_completed() # get laser spot location and image - spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence - } - result = self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + result = self._get_laser_spot_centroid() reference_image = self.image # turn off the laser @@ -5019,18 +5008,7 @@ def _verify_spot_alignment(self) -> bool: self.camera.send_trigger() current_image = self.camera.read_frame() """ - spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence - } - self.get_laser_spot_centroid( - averaging_n=self.laser_af_properties.laser_af_averaging_n, - spot_detection_mode=self.laser_af_properties.spot_detection_mode, - spot_detection_params=spot_detection_params - ) + self._get_laser_spot_centroid() current_image = self.image self.microcontroller.turn_off_AF_laser() @@ -5064,12 +5042,7 @@ def _verify_spot_alignment(self) -> bool: return True - def get_laser_spot_centroid( - self, - averaging_n: int, - spot_detection_mode: SpotDetectionMode, - spot_detection_params: dict - ) -> Optional[Tuple[float, float]]: + def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]: """Get the centroid location of the laser spot. Averages multiple measurements to improve accuracy. The number of measurements @@ -5085,7 +5058,7 @@ def get_laser_spot_centroid( tmp_x = 0 tmp_y = 0 - for i in range(averaging_n): + for i in range(self.laser_af_properties.laser_af_averaging_n): try: # send camera trigger if self.liveController.trigger_mode == TriggerMode.SOFTWARE: @@ -5097,15 +5070,23 @@ def get_laser_spot_centroid( # read camera frame image = self.camera.read_frame() if image is None: - self._log.warning(f"Failed to read frame {i+1}/{averaging_n}") + self._log.warning(f"Failed to read frame {i+1}/{self.laser_af_properties.laser_af_averaging_n}") continue self.image = image # store for debugging # TODO: add to return instead of storing # calculate centroid - result = utils.find_spot_location(image, mode=spot_detection_mode, params=spot_detection_params) + spot_detection_params = { + "y_window": self.laser_af_properties.laser_af_y_window, + "x_window": self.laser_af_properties.laser_af_x_window, + "peak_width": self.laser_af_properties.laser_af_min_peak_width, + "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, + "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence, + "spot_spacing": self.laser_af_properties.laser_af_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}/{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 @@ -5114,7 +5095,7 @@ def get_laser_spot_centroid( successful_detections += 1 except Exception as e: - self._log.error(f"Error processing frame {i+1}/{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 diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 3be89164..044af1fa 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -1007,7 +1007,9 @@ 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() ) diff --git a/software/control/widgets.py b/software/control/widgets.py index c06895c5..ee75d546 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -351,6 +351,7 @@ class LaserAutofocusSettingWidget(QWidget): signal_newExposureTime = Signal(float) signal_newAnalogGain = Signal(float) signal_apply_settings = Signal() + signal_laser_spot_location = Signal(np.ndarray, float, float) def __init__(self, streamHandler, liveController, laserAutofocusController, stretch=True): super().__init__() @@ -418,45 +419,34 @@ def init_ui(self): # Create spinboxes for numerical parameters self.spinboxes = {} - # Averaging + # Add non-spot detection related spinboxes self._add_spinbox(settings_layout, "Laser AF Averaging N:", "laser_af_averaging_n", 1, 100, 0) - - # Displacement window self._add_spinbox( settings_layout, "Displacement Success Window (μm):", "displacement_success_window_um", 0.1, 10.0, 2 ) - - # Spot crop size self._add_spinbox(settings_layout, "Spot Crop Size (pixels):", "spot_crop_size", 1, 500, 0) - - # Correlation threshold self._add_spinbox(settings_layout, "Correlation Threshold:", "correlation_threshold", 0.1, 1.0, 2) - - # Calibration distance self._add_spinbox( settings_layout, "Calibration Distance (μm):", "pixel_to_um_calibration_distance", 0.1, 20.0, 2 ) - - # AF Range self._add_spinbox(settings_layout, "Laser AF Range (μm):", "laser_af_range", 1, 1000, 1) - # Y window - self._add_spinbox(settings_layout, "Y Window (pixels):", "y_window", 1, 500, 0) - - # X window - self._add_spinbox(settings_layout, "X Window (pixels):", "x_window", 1, 500, 0) - - # Min peak width - self._add_spinbox(settings_layout, "Min Peak Width:", "min_peak_width", 1, 100, 1) - - # Min peak distance - self._add_spinbox(settings_layout, "Min Peak Distance:", "min_peak_distance", 1, 100, 1) - - # Min peak prominence - self._add_spinbox(settings_layout, "Min Peak Prominence:", "min_peak_prominence", 0.01, 1.0, 2) - - # Spot spacing - self._add_spinbox(settings_layout, "Spot Spacing (pixels):", "spot_spacing", 1, 1000, 1) + # Create spot detection group + spot_detection_group = QFrame() + spot_detection_group.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + spot_detection_group.setAutoFillBackground(True) + palette = spot_detection_group.palette() + palette.setColor(spot_detection_group.backgroundRole(), QColor("#ffffff")) + spot_detection_group.setPalette(palette) + spot_detection_layout = QVBoxLayout() + + # Add spot detection related spinboxes + self._add_spinbox(spot_detection_layout, "Y Window (pixels):", "y_window", 1, 500, 0) + self._add_spinbox(spot_detection_layout, "X Window (pixels):", "x_window", 1, 500, 0) + self._add_spinbox(spot_detection_layout, "Min Peak Width:", "min_peak_width", 1, 100, 1) + self._add_spinbox(spot_detection_layout, "Min Peak Distance:", "min_peak_distance", 1, 100, 1) + self._add_spinbox(spot_detection_layout, "Min Peak Prominence:", "min_peak_prominence", 0.01, 1.0, 2) + self._add_spinbox(spot_detection_layout, "Spot Spacing (pixels):", "spot_spacing", 1, 1000, 1) # Spot detection mode combo box spot_mode_layout = QHBoxLayout() @@ -469,12 +459,18 @@ def init_ui(self): ) self.spot_mode_combo.setCurrentIndex(current_index) spot_mode_layout.addWidget(self.spot_mode_combo) + spot_detection_layout.addLayout(spot_mode_layout) + + # Add Run Spot Detection button + self.run_spot_detection_button = QPushButton("Run Spot Detection") + self.run_spot_detection_button.setEnabled(False) # Disabled by default + spot_detection_layout.addWidget(self.run_spot_detection_button) + + spot_detection_group.setLayout(spot_detection_layout) + settings_layout.addWidget(spot_detection_group) # Apply button self.apply_button = QPushButton("Apply and Initialize") - - # Add settings controls - settings_layout.addLayout(spot_mode_layout) settings_layout.addWidget(self.apply_button) settings_group.setLayout(settings_layout) @@ -491,6 +487,7 @@ def init_ui(self): self.exposure_spinbox.valueChanged.connect(self.update_exposure_time) self.analog_gain_spinbox.valueChanged.connect(self.update_analog_gain) self.apply_button.clicked.connect(self.apply_settings) + self.run_spot_detection_button.clicked.connect(self.run_spot_detection) def _add_spinbox( self, layout, label: str, property_name: str, min_val: float, max_val: float, decimals: int @@ -516,9 +513,11 @@ def toggle_live(self, pressed): if pressed: self.liveController.start_live() self.btn_live.setText("Stop Live") + self.run_spot_detection_button.setEnabled(False) else: self.liveController.stop_live() self.btn_live.setText("Start Live") + self.run_spot_detection_button.setEnabled(True) def update_exposure_time(self, value): self.signal_newExposureTime.emit(value) @@ -564,9 +563,8 @@ def apply_settings(self): self.laserAutofocusController.set_laser_af_properties(updates) self.laserAutofocusController.initialize_auto() self.signal_apply_settings.emit() - self.update_calibration_label() - def update_calibration_label(self): + # Show calibration result # Clear previous calibration label if it exists if hasattr(self, "calibration_label"): self.calibration_label.deleteLater() @@ -578,6 +576,39 @@ def update_calibration_label(self): ) self.layout().addWidget(self.calibration_label) + def run_spot_detection(self): + """Run spot detection with current settings and emit results""" + params = { + 'y_window': int(self.spinboxes['y_window'].value()), + 'x_window': int(self.spinboxes['x_window'].value()), + 'min_peak_width': self.spinboxes['min_peak_width'].value(), + 'min_peak_distance': self.spinboxes['min_peak_distance'].value(), + 'min_peak_prominence': self.spinboxes['min_peak_prominence'].value(), + 'spot_spacing': self.spinboxes['spot_spacing'].value() + } + mode = self.spot_mode_combo.currentData() + + # Get current frame from live controller + frame = self.liveController.camera.current_frame + if frame is not None: + result = utils.find_spot_location( + frame, + mode=mode, + params=params + ) + if result is not None: + x, y = result + self.signal_laser_spot_location.emit(frame, x, y) + else: + # Show error message + # Clear previous error label if it exists + if hasattr(self, "spot_detection_error_label"): + self.spot_detection_error_label.deleteLater() + + # Create and add new error label + self.spot_detection_error_label = QLabel("Spot detection failed!") + self.layout().addWidget(self.spot_detection_error_label) + class SpinningDiskConfocalWidget(QWidget): def __init__(self, xlight, config_manager=None): From 7261614f50becd542349ce08f138f2626108fb68 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 24 Feb 2025 13:24:38 -0800 Subject: [PATCH 3/6] toggle laser af characterization mode --- software/control/core/core.py | 12 ++++++------ software/control/widgets.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 45b7e213..538156fc 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -5077,12 +5077,12 @@ def _get_laser_spot_centroid(self) -> Optional[Tuple[float, float]]: # calculate centroid spot_detection_params = { - "y_window": self.laser_af_properties.laser_af_y_window, - "x_window": self.laser_af_properties.laser_af_x_window, - "peak_width": self.laser_af_properties.laser_af_min_peak_width, - "peak_distance": self.laser_af_properties.laser_af_min_peak_distance, - "peak_prominence": self.laser_af_properties.laser_af_min_peak_prominence, - "spot_spacing": self.laser_af_properties.laser_af_spot_spacing + "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: diff --git a/software/control/widgets.py b/software/control/widgets.py index ee75d546..6e128449 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -474,9 +474,18 @@ def init_ui(self): settings_layout.addWidget(self.apply_button) settings_group.setLayout(settings_layout) + # Add Laser AF Characterization Mode checkbox + characterization_group = QFrame() + characterization_layout = QHBoxLayout() + self.characterization_checkbox = QCheckBox("Laser AF Characterization Mode") + self.characterization_checkbox.setChecked(False) + characterization_layout.addWidget(self.characterization_checkbox) + characterization_group.setLayout(characterization_layout) + # Add to main layout layout.addWidget(live_group) layout.addWidget(settings_group) + layout.addWidget(characterization_group) self.setLayout(layout) if not self.stretch: @@ -488,6 +497,7 @@ def init_ui(self): self.analog_gain_spinbox.valueChanged.connect(self.update_analog_gain) self.apply_button.clicked.connect(self.apply_settings) self.run_spot_detection_button.clicked.connect(self.run_spot_detection) + self.characterization_checkbox.stateChanged.connect(self.toggle_characterization_mode) def _add_spinbox( self, layout, label: str, property_name: str, min_val: float, max_val: float, decimals: int @@ -518,6 +528,10 @@ def toggle_live(self, pressed): self.liveController.stop_live() self.btn_live.setText("Start Live") self.run_spot_detection_button.setEnabled(True) + + def toggle_characterization_mode(self, state): + global LASER_AF_CHARACTERIZATION_MODE + LASER_AF_CHARACTERIZATION_MODE = state def update_exposure_time(self, value): self.signal_newExposureTime.emit(value) From e59469b26fab710a32a17509163f6c585e3e3b40 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 24 Feb 2025 13:43:57 -0800 Subject: [PATCH 4/6] show cross-correlation result --- software/control/core/core.py | 9 ++++++--- software/control/gui_hcs.py | 3 +++ software/control/widgets.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 538156fc..54452dfd 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -4632,6 +4632,7 @@ class LaserAutofocusController(QObject): image_to_display = Signal(np.ndarray) signal_displacement_um = Signal(float) + signal_cross_correlation = Signal(float) def __init__( self, @@ -4897,7 +4898,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) @@ -4989,7 +4992,7 @@ def on_objective_changed(self) -> None: self.is_initialized = 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 @@ -5040,7 +5043,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. diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 044af1fa..f6930ad1 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -1016,6 +1016,9 @@ def connect_objective_changed_laser_af(): 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 diff --git a/software/control/widgets.py b/software/control/widgets.py index 6e128449..0f609888 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -622,6 +622,17 @@ def run_spot_detection(self): # Create and add new error label self.spot_detection_error_label = QLabel("Spot detection failed!") self.layout().addWidget(self.spot_detection_error_label) + + def show_cross_correlation_result(self, value): + """Show cross-correlation value from validating laser af images""" + # Clear previous correlation label if it exists + if hasattr(self, "correlation_label"): + self.correlation_label.deleteLater() + + # Create and add new correlation label + self.correlation_label = QLabel() + self.correlation_label.setText(f"Cross-correlation: {value:.3f}") + self.layout().addWidget(self.correlation_label) class SpinningDiskConfocalWidget(QWidget): From ae2cdf0c8e1b8eea99d911642277924c156422eb Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 24 Feb 2025 13:53:54 -0800 Subject: [PATCH 5/6] remove unused crosshair flag --- software/control/core/core.py | 2 -- software/control/gui_hcs.py | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 54452dfd..b31b6236 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3023,7 +3023,6 @@ def __init__( liveController=None, contrastManager=None, window_title="", - draw_crosshairs=False, show_LUT=False, autoLevels=False, ): @@ -3080,7 +3079,6 @@ def __init__( self.ptRect2 = None self.DrawCirc = False self.centroid = None - self.DrawCrossHairs = False self.image_offset = np.array([0, 0]) ## Layout diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index f6930ad1..f6fec14f 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -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( @@ -641,18 +641,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 @@ -713,12 +713,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") From 0fb8debdb1a4489b37fad201fab2462d738501ef Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 24 Feb 2025 13:56:37 -0800 Subject: [PATCH 6/6] remove unused import --- software/main_hcs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/software/main_hcs.py b/software/main_hcs.py index 65ec35bf..088d4b34 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -22,7 +22,6 @@ from control.widgets import ConfigEditorBackwardsCompatible from control._def import CACHED_CONFIG_FILE_PATH from control._def import USE_TERMINAL_CONSOLE -from control._def import SUPPORT_LASER_AUTOFOCUS import control.utils