From eecafb203a4bc9f1a547e10b16fabf056bb2583d Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Mon, 2 Dec 2024 20:17:15 -0800 Subject: [PATCH 1/9] flexible scan grid --- software/control/core.py | 179 ++----- software/control/gui_hcs.py | 62 +-- software/control/widgets.py | 955 ++++++++++-------------------------- 3 files changed, 308 insertions(+), 888 deletions(-) diff --git a/software/control/core.py b/software/control/core.py index f0c314b9..cbf0ecd1 100644 --- a/software/control/core.py +++ b/software/control/core.py @@ -42,6 +42,7 @@ import numpy as np import pandas as pd import scipy.signal +from scipy import interpolate import cv2 import imageio as iio @@ -1803,12 +1804,7 @@ def run_single_time_point(self): # init z parameters, z range self.initialize_z_stack() - if self.coordinate_dict is not None: - print("coordinate acquisition") - self.run_coordinate_acquisition(current_path) - else: - print("grid acquisition") - self.run_grid_acquisition(current_path) + self.run_coordinate_acquisition(current_path) # finished region scan self.coordinates_pd.to_csv(os.path.join(current_path,'coordinates.csv'),index=False,header=True) @@ -1847,13 +1843,10 @@ def initialize_coordinates_dataframe(self): base_columns = ['z_level', 'x (mm)', 'y (mm)', 'z (um)', 'time'] piezo_column = ['z_piezo (um)'] if self.use_piezo else [] - if IS_HCS: - if self.coordinate_dict is not None: - self.coordinates_pd = pd.DataFrame(columns=['region', 'fov'] + base_columns + piezo_column) - else: - self.coordinates_pd = pd.DataFrame(columns=['region', 'i', 'j'] + base_columns + piezo_column) + if self.coordinate_dict is not None: + self.coordinates_pd = pd.DataFrame(columns=['region', 'fov'] + base_columns + piezo_column) else: - self.coordinates_pd = pd.DataFrame(columns=['i', 'j'] + base_columns + piezo_column) + self.coordinates_pd = pd.DataFrame(columns=['region', 'i', 'j'] + base_columns + piezo_column) def update_coordinates_dataframe(self, region_id, z_level, fov=None, i=None, j=None): base_data = { @@ -1889,17 +1882,6 @@ def update_coordinates_dataframe(self, region_id, z_level, fov=None, i=None, j=N self.coordinates_pd = pd.concat([self.coordinates_pd, new_row], ignore_index=True) - def calculate_grid_indices(self, i, j): - # Ensure that i/y-indexing is always top to bottom - sgn_i = -1 if self.deltaY >= 0 else 1 - sgn_i = -sgn_i if INVERTED_OBJECTIVE else sgn_i - sgn_j = self.x_scan_direction if self.deltaX >= 0 else -self.x_scan_direction - - real_i = self.NY-1-i if sgn_i == -1 else i - real_j = self.NX-1-j if sgn_j == -1 else j - - return sgn_i, sgn_j, real_i, real_j - def move_to_coordinate(self, coordinate_mm): print("moving to coordinate", coordinate_mm) @@ -1935,60 +1917,6 @@ def move_to_z_level(self, z_mm): self.wait_till_operation_is_completed() time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) - def run_grid_acquisition(self, current_path): - n_regions = len(self.scan_coordinates_mm) - - for region_id in range(n_regions): - self.signal_acquisition_progress.emit(region_id + 1, n_regions, self.time_point) - coordinate_mm = self.scan_coordinates_mm[region_id] - - self.x_scan_direction = 1 - self.dx_usteps = 0 # accumulated x displacement - self.dy_usteps = 0 # accumulated y displacement - - if self.use_scan_coordinates: - # Calculate grid size - grid_size_x_mm = (self.NX - 1) * self.deltaX - grid_size_y_mm = (self.NY - 1) * self.deltaY - - # Calculate top-left corner position - start_x = coordinate_mm[0] - grid_size_x_mm / 2 - start_y = coordinate_mm[1] - grid_size_y_mm / 2 - if len(coordinate_mm) == 3: - self.move_to_coordinate([start_x, start_y, coordinate_mm[2]]) - else: - self.move_to_coordinate([start_x, start_y]) - - self.wait_till_operation_is_completed() - - self.num_fovs = self.NX * self.NY - len(self.multiPointController.scanCoordinates.grid_skip_positions) - self.total_scans = self.num_fovs * self.NZ * len(self.selected_configurations) - fov_count = 0 # count fovs for progress - - for i in range(self.NY): - self.af_fov_count = 0 # for AF, so that AF at the beginning of each new row - - for j in range(self.NX): - sgn_i, sgn_j, real_i, real_j = self.calculate_grid_indices(i, j) - - if not self.multiPointController.scanCoordinates or (real_i, real_j) not in self.multiPointController.scanCoordinates.grid_skip_positions: - self.acquire_at_position(region_id, current_path, fov_count, i=real_i, j=real_j) - fov_count += 1 - - if self.multiPointController.abort_acqusition_requested: - self.handle_acquisition_abort(current_path, region_id) - return - - if j < self.NX - 1: - self.move_to_next_x_position() - - if i < self.NY - 1: - self.move_to_next_y_position() - - self.x_scan_direction = -self.x_scan_direction - - self.finish_grid_scan(n_regions, region_id) - def run_coordinate_acquisition(self, current_path): n_regions = len(self.scan_coordinates_mm) @@ -2451,18 +2379,6 @@ def handle_acquisition_abort(self, current_path, region_id=0): self.coordinates_pd.to_csv(os.path.join(current_path,'coordinates.csv'),index=False,header=True) self.navigationController.enable_joystick_button_action = True - def move_to_next_x_position(self): - self.navigationController.move_x_usteps(self.x_scan_direction*self.deltaX_usteps) - self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_X/1000) - self.dx_usteps = self.dx_usteps + self.x_scan_direction*self.deltaX_usteps - - def move_to_next_y_position(self): - self.navigationController.move_y_usteps(self.deltaY_usteps) - self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Y/1000) - self.dy_usteps = self.dy_usteps + self.deltaY_usteps - def move_z_for_stack(self): if self.use_piezo: self.z_piezo_um += self.deltaZ*1000 @@ -2510,40 +2426,6 @@ def move_z_back_after_stack(self): self.wait_till_operation_is_completed() self.dz_usteps = self.dz_usteps - self.deltaZ_usteps*(self.NZ-1) - def finish_grid_scan(self, n_regions, region_id): - print("moving slide back") - if SHOW_TILED_PREVIEW and IS_HCS: - self.navigationController.keep_scan_begin_position(self.navigationController.x_pos_mm, self.navigationController.y_pos_mm) - - if n_regions == 1: - # only move to the start position if there's only one region in the scan - if self.NY > 1: - # move y back - self.navigationController.move_y_usteps(-self.deltaY_usteps*(self.NY-1)) - self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Y/1000) - self.dy_usteps = self.dy_usteps - self.deltaY_usteps*(self.NY-1) - - if SHOW_TILED_PREVIEW and not IS_HCS: - self.navigationController.keep_scan_begin_position(self.navigationController.x_pos_mm, self.navigationController.y_pos_mm) - - # move x back at the end of the scan - if self.x_scan_direction == -1: - self.navigationController.move_x_usteps(-self.deltaX_usteps*(self.NX-1)) - self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_X/1000) - - # move z back - if self.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*self.navigationController.z_microstepping) - self.navigationController.move_z_to_usteps(self.z_pos - STAGE_MOVEMENT_SIGN_Z*_usteps_to_clear_backlash) - self.wait_till_operation_is_completed() - self.navigationController.move_z_usteps(_usteps_to_clear_backlash) - self.wait_till_operation_is_completed() - else: - self.navigationController.microcontroller.move_z_to_usteps(self.z_pos) - self.wait_till_operation_is_completed() - def update_tiled_preview(self, current_round_images, i, j, k): if SHOW_TILED_PREVIEW and 'BF LED matrix full' in current_round_images: # initialize the variable @@ -2760,34 +2642,35 @@ def run_acquisition(self, location_list=None, coordinate_dict=None): print('start multipoint') if coordinate_dict is not None: - print('Using coordinate-based acquisition') + print('Using coordinate-based acquisition') # always total_points = sum(len(coords) for coords in coordinate_dict) self.coordinate_dict = coordinate_dict self.location_list = None self.use_scan_coordinates = False self.scan_coordinates_mm = location_list self.scan_coordinates_name = list(coordinate_dict.keys()) # list(coordinate_dict.keys()) if not wellplate - elif location_list is not None: - print('Using location list acquisition') - self.coordinate_dict = None - self.location_list = location_list - self.use_scan_coordinates = True - self.scan_coordinates_mm = location_list - self.scan_coordinates_name = [f'R{i}' for i in range(len(location_list))] - else: - print(f"t_c_z_y_x: {self.Nt}_{len(self.selected_configurations)}_{self.NZ}_{self.NY}_{self.NX}") - self.coordinate_dict = None - self.location_list = None - if self.scanCoordinates is not None and self.scanCoordinates.get_selected_wells(): - print('Using well plate scan') - self.use_scan_coordinates = True - self.scan_coordinates_mm = self.scanCoordinates.coordinates_mm - self.scan_coordinates_name = self.scanCoordinates.name - else: - print('Using current location') - self.use_scan_coordinates = False - self.scan_coordinates_mm = [(self.navigationController.x_pos_mm, self.navigationController.y_pos_mm)] - self.scan_coordinates_name = ['ROI'] + + # elif location_list is not None: + # print('Using location list acquisition') + # self.coordinate_dict = None + # self.location_list = location_list + # self.use_scan_coordinates = True + # self.scan_coordinates_mm = location_list + # self.scan_coordinates_name = [f'R{i}' for i in range(len(location_list))] + # else: + # print(f"t_c_z_y_x: {self.Nt}_{len(self.selected_configurations)}_{self.NZ}_{self.NY}_{self.NX}") + # self.coordinate_dict = None + # self.location_list = None + # if self.scanCoordinates is not None and self.scanCoordinates.get_selected_wells(): + # print('Using well plate scan') + # self.use_scan_coordinates = True + # self.scan_coordinates_mm = self.scanCoordinates.coordinates_mm + # self.scan_coordinates_name = self.scanCoordinates.name + # else: + # print('Using current location') + # self.use_scan_coordinates = False + # self.scan_coordinates_mm = [(self.navigationController.x_pos_mm, self.navigationController.y_pos_mm)] + # self.scan_coordinates_name = ['ROI'] print("num regions:",len(self.scan_coordinates_mm)) print("region ids:", self.scan_coordinates_name) @@ -4153,7 +4036,7 @@ def __init__(self): self.wellplate_offset_y_mm = WELLPLATE_OFFSET_Y_mm self.well_spacing_mm = WELL_SPACING_MM self.well_size_mm = WELL_SIZE_MM - self.grid_skip_positions = [] + # self.grid_skip_positions = [] # remove def _index_to_row(self,index): index += 1 @@ -4206,6 +4089,10 @@ def get_selected_wells(self): _increasing = not _increasing return len(selected_wells) # if wells selected + def get_approx_well_from_coordinate(self, x_mm, y_mm): + pass + # to implement later + class LaserAutofocusController(QObject): diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index e890075b..a90e2cd3 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -423,8 +423,7 @@ def loadWidgets(self): else: self.setupImageDisplayTabs() - self.multiPointWidget = widgets.MultiPointWidget(self.multipointController, self.configurationManager) - self.multiPointWidget2 = widgets.MultiPointWidget2(self.navigationController, self.navigationViewer, self.multipointController, self.configurationManager, scanCoordinates=None) + self.multiPointWidget2 = widgets.MultiPointWidget2(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, scanCoordinates=None) self.multiPointWidgetGrid = widgets.MultiPointWidgetGrid(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, self.scanCoordinates, self.napariMosaicDisplayWidget) self.sampleSettingsWidget = widgets.SampleSettingsWidget(self.objectivesWidget, self.wellplateFormatWidget) @@ -633,13 +632,8 @@ def makeConnections(self): else: self.navigationController.signal_joystick_button_pressed.connect(self.autofocusController.autofocus) - self.multiPointWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart) - if ENABLE_STITCHER: self.multipointController.signal_stitcher.connect(self.startStitcher) - self.multiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) - self.multiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.multiPointWidget.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) if ENABLE_FLEXIBLE_MULTIPOINT: self.multiPointWidget2.signal_acquisition_started.connect(self.toggleAcquisitionStart) @@ -662,12 +656,14 @@ def makeConnections(self): self.liveControlWidget.update_camera_settings() self.slidePositionController.signal_slide_loading_position_reached.connect(self.navigationWidget.slot_slide_loading_position_reached) - self.slidePositionController.signal_slide_loading_position_reached.connect(self.multiPointWidget.disable_the_start_aquisition_button) + #self.slidePositionController.signal_slide_loading_position_reached.connect(self.multiPointWidget.disable_the_start_aquisition_button) self.slidePositionController.signal_slide_scanning_position_reached.connect(self.navigationWidget.slot_slide_scanning_position_reached) - self.slidePositionController.signal_slide_scanning_position_reached.connect(self.multiPointWidget.enable_the_start_aquisition_button) + #self.slidePositionController.signal_slide_scanning_position_reached.connect(self.multiPointWidget.enable_the_start_aquisition_button) self.slidePositionController.signal_clear_slide.connect(self.navigationViewer.clear_slide) self.navigationViewer.signal_coordinates_clicked.connect(self.navigationController.move_from_click_mosaic) self.objectivesWidget.signal_objective_changed.connect(self.navigationViewer.on_objective_changed) + if ENABLE_FLEXIBLE_MULTIPOINT: + self.objectivesWidget.signal_objective_changed.connect(self.multiPointWidget2.update_fov_positions) self.navigationController.xyPos.connect(self.navigationViewer.update_current_location) self.multipointController.signal_register_current_fov.connect(self.navigationViewer.register_fov) self.multipointController.signal_current_configuration.connect(self.liveControlWidget.set_microscope_mode) @@ -705,7 +701,7 @@ def makeConnections(self): self.wellplateFormatWidget.signalWellplateSettings.connect(lambda format_, *args: self.onWellplateChanged(format_)) self.wellSelectionWidget.signal_wellSelectedPos.connect(self.navigationController.move_to) - self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) + #self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) if ENABLE_SCAN_GRID: self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidgetGrid.set_well_coordinates) self.objectivesWidget.signal_objective_changed.connect(self.multiPointWidgetGrid.update_coordinates) @@ -769,9 +765,7 @@ def makeNapariConnections(self): if USE_NAPARI_FOR_MULTIPOINT: self.napari_connections['napariMultiChannelWidget'] = [ (self.multipointController.napari_layers_init, self.napariMultiChannelWidget.initLayers), - (self.multipointController.napari_layers_update, self.napariMultiChannelWidget.updateLayers), - (self.multiPointWidget.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), - (self.multiPointWidget.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) + (self.multipointController.napari_layers_update, self.napariMultiChannelWidget.updateLayers) ] if ENABLE_FLEXIBLE_MULTIPOINT: @@ -793,8 +787,6 @@ def makeNapariConnections(self): self.napari_connections['napariTiledDisplayWidget'] = [ (self.multipointController.napari_layers_init, self.napariTiledDisplayWidget.initLayers), (self.multipointController.napari_layers_update, self.napariTiledDisplayWidget.updateLayers), - (self.multiPointWidget.signal_acquisition_channels, self.napariTiledDisplayWidget.initChannels), - (self.multiPointWidget.signal_acquisition_shape, self.napariTiledDisplayWidget.initLayersShape), (self.napariTiledDisplayWidget.signal_coordinates_clicked, self.navigationController.scan_preview_move_from_click) ] @@ -815,8 +807,6 @@ def makeNapariConnections(self): if USE_NAPARI_FOR_MOSAIC_DISPLAY: self.napari_connections['napariMosaicDisplayWidget'] = [ (self.multipointController.napari_mosaic_update, self.napariMosaicDisplayWidget.updateMosaic), - (self.multiPointWidget.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), - (self.multiPointWidget.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape), (self.napariMosaicDisplayWidget.signal_coordinates_clicked, self.navigationController.move_from_click_mosaic), (self.napariMosaicDisplayWidget.signal_update_viewer, self.navigationViewer.update_slide) @@ -887,19 +877,22 @@ def openLedMatrixSettings(self): def onTabChanged(self, index): acquisitionWidget = self.recordTabWidget.widget(index) - is_multipoint = (index == self.recordTabWidget.indexOf(self.multiPointWidget)) + is_flexible = (index == self.recordTabWidget.indexOf(self.multiPointWidget2)) is_scan_grid = (index == self.recordTabWidget.indexOf(self.multiPointWidgetGrid)) if ENABLE_SCAN_GRID else False - self.toggleWellSelector((is_multipoint or is_scan_grid) and self.wellSelectionWidget.format != 'glass slide') + self.toggleWellSelector(is_scan_grid and self.wellSelectionWidget.format != 'glass slide') if is_scan_grid: + self.navigationViewer.clear_overlay() self.wellSelectionWidget.onSelectionChanged() else: self.multiPointWidgetGrid.clear_regions() - try: - if ENABLE_STITCHER: - self.toggleStitcherWidget(acquisitionWidget.checkbox_stitchOutput.isChecked()) - acquisitionWidget.emit_selected_channels() - except AttributeError: - pass + + if is_flexible: + self.multiPointWidget2.update_fov_positions() + + if ENABLE_STITCHER: + self.toggleStitcherWidget(acquisitionWidget.checkbox_stitchOutput.isChecked()) + acquisitionWidget.emit_selected_channels() + def resizeCurrentTab(self, tabWidget): current_widget = tabWidget.currentWidget() @@ -951,9 +944,9 @@ def setupSlidePositionController(self, is_for_wellplate): def connectSlidePositionController(self): self.slidePositionController.signal_slide_loading_position_reached.connect(self.navigationWidget.slot_slide_loading_position_reached) - self.slidePositionController.signal_slide_loading_position_reached.connect(self.multiPointWidget.disable_the_start_aquisition_button) + #self.slidePositionController.signal_slide_loading_position_reached.connect(self.multiPointWidget.disable_the_start_aquisition_button) self.slidePositionController.signal_slide_scanning_position_reached.connect(self.navigationWidget.slot_slide_scanning_position_reached) - self.slidePositionController.signal_slide_scanning_position_reached.connect(self.multiPointWidget.enable_the_start_aquisition_button) + #self.slidePositionController.signal_slide_scanning_position_reached.connect(self.multiPointWidget.enable_the_start_aquisition_button) self.slidePositionController.signal_clear_slide.connect(self.navigationViewer.clear_slide) if SHOW_NAVIGATION_BAR: self.navigationBarWidget.replace_slide_controller(self.slidePositionController) @@ -970,7 +963,7 @@ def replaceWellSelectionWidget(self, new_widget): self.dock_wellSelection.addWidget(self.wellSelectionWidget) def connectWellSelectionWidget(self): - self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) + #self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) self.wellSelectionWidget.signal_wellSelectedPos.connect(self.navigationController.move_to) self.wellplateFormatWidget.signalWellplateSettings.connect(self.wellSelectionWidget.updateWellplateSettings) if ENABLE_SCAN_GRID: @@ -990,16 +983,13 @@ def toggleAcquisitionStart(self, acquisition_started): self.recordTabWidget.setTabEnabled(index, not acquisition_started or index == current_index) if acquisition_started: self.liveControlWidget.toggle_autolevel(not acquisition_started) - is_multipoint = (current_index == self.recordTabWidget.indexOf(self.multiPointWidget)) + is_scan_grid = (current_index == self.recordTabWidget.indexOf(self.multiPointWidgetGrid)) if ENABLE_SCAN_GRID else False - if (is_multipoint or is_scan_grid) and self.wellSelectionWidget.format != 'glass slide': + if is_scan_grid and self.wellSelectionWidget.format != 'glass slide': self.toggleWellSelector(not acquisition_started) - if is_scan_grid: - self.navigationViewer.on_acquisition_start(acquisition_started) - self.multiPointWidgetGrid.display_progress_bar(acquisition_started) - if is_multipoint: - self.navigationViewer.on_acquisition_start(acquisition_started) - self.multiPointWidget2.display_progress_bar(acquisition_started) + + self.recordTabWidget.currentWidget().display_progress_bar(acquisition_started) + self.navigationViewer.on_acquisition_start(acquisition_started) def toggleStitcherWidget(self, checked): if checked: diff --git a/software/control/widgets.py b/software/control/widgets.py index 5e4c211a..c2254dfd 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -23,6 +23,7 @@ import itertools import numpy as np from scipy.spatial import Delaunay +from scipy import interpolate import shutil from control._def import * from PIL import Image, ImageDraw, ImageFont @@ -1997,341 +1998,20 @@ def display_stats(self, stats): row+=1 -class MultiPointWidget(QFrame): - - signal_acquisition_started = Signal(bool) - signal_acquisition_channels = Signal(list) - signal_acquisition_z_levels = Signal(int) - signal_acquisition_shape = Signal(int, int, int, float, float, float) - signal_stitcher_widget = Signal(bool) - - def __init__(self, multipointController, configurationManager = None, main=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.multipointController = multipointController - self.configurationManager = configurationManager - self.well_selected = False - self.base_path_is_set = False - self.add_components() - self.setFrameStyle(QFrame.Panel | QFrame.Raised) - - def add_components(self): - - self.btn_setSavingDir = QPushButton('Browse') - self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) - - self.lineEdit_savingDir = QLineEdit() - self.lineEdit_savingDir.setReadOnly(True) - self.lineEdit_savingDir.setText('Choose a base saving directory') - - self.lineEdit_savingDir.setText(DEFAULT_SAVING_PATH) - self.multipointController.set_base_path(DEFAULT_SAVING_PATH) - self.base_path_is_set = True - - self.lineEdit_experimentID = QLineEdit() - - self.entry_deltaX = QDoubleSpinBox() - self.entry_deltaX.setMinimum(0) - self.entry_deltaX.setMaximum(5) - self.entry_deltaX.setSingleStep(0.1) - self.entry_deltaX.setValue(Acquisition.DX) - self.entry_deltaX.setDecimals(3) - self.entry_deltaX.setSuffix(' mm') - self.entry_deltaX.setKeyboardTracking(False) - - self.entry_NX = QSpinBox() - self.entry_NX.setMinimum(1) - self.entry_NX.setMaximum(50) - self.entry_NX.setSingleStep(1) - self.entry_NX.setValue(Acquisition.NX) - self.entry_NX.setKeyboardTracking(False) - - self.entry_deltaY = QDoubleSpinBox() - self.entry_deltaY.setMinimum(0) - self.entry_deltaY.setMaximum(5) - self.entry_deltaY.setSingleStep(0.1) - self.entry_deltaY.setValue(Acquisition.DX) - self.entry_deltaY.setDecimals(3) - self.entry_deltaY.setSuffix(' mm') - self.entry_deltaY.setKeyboardTracking(False) - - self.entry_NY = QSpinBox() - self.entry_NY.setMinimum(1) - self.entry_NY.setMaximum(50) - self.entry_NY.setSingleStep(1) - self.entry_NY.setValue(Acquisition.NY) - self.entry_NY.setKeyboardTracking(False) - - self.entry_deltaZ = QDoubleSpinBox() - self.entry_deltaZ.setMinimum(0) - self.entry_deltaZ.setMaximum(1000) - self.entry_deltaZ.setSingleStep(0.2) - self.entry_deltaZ.setValue(Acquisition.DZ) - self.entry_deltaZ.setDecimals(3) - self.entry_deltaZ.setSuffix(' μm') - self.entry_deltaZ.setKeyboardTracking(False) - - self.entry_NZ = QSpinBox() - self.entry_NZ.setMinimum(1) - self.entry_NZ.setMaximum(2000) - self.entry_NZ.setSingleStep(1) - self.entry_NZ.setValue(1) - self.entry_NZ.setKeyboardTracking(False) - - self.entry_dt = QDoubleSpinBox() - self.entry_dt.setMinimum(0) - self.entry_dt.setMaximum(12*3600) - self.entry_dt.setSingleStep(1) - self.entry_dt.setValue(0) - self.entry_dt.setSuffix(' s') - self.entry_dt.setKeyboardTracking(False) - - self.entry_Nt = QSpinBox() - self.entry_Nt.setMinimum(1) - self.entry_Nt.setMaximum(5000) # @@@ to be changed - self.entry_Nt.setSingleStep(1) - self.entry_Nt.setValue(1) - self.entry_Nt.setKeyboardTracking(False) - - self.list_configurations = QListWidget() - for microscope_configuration in self.configurationManager.configurations: - self.list_configurations.addItems([microscope_configuration.name]) - self.list_configurations.setSelectionMode(QAbstractItemView.MultiSelection) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum - - self.checkbox_withAutofocus = QCheckBox('Contrast AF') - self.checkbox_withAutofocus.setChecked(MULTIPOINT_CONTRAST_AUTOFOCUS_ENABLE_BY_DEFAULT) - self.multipointController.set_af_flag(MULTIPOINT_CONTRAST_AUTOFOCUS_ENABLE_BY_DEFAULT) - - self.checkbox_genFocusMap = QCheckBox('Focus Map') - self.checkbox_genFocusMap.setChecked(False) - - self.checkbox_withReflectionAutofocus = QCheckBox('Reflection AF') - self.checkbox_withReflectionAutofocus.setChecked(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) - - self.checkbox_stitchOutput = QCheckBox('Stitch Scans') - self.checkbox_stitchOutput.setChecked(False) - - self.multipointController.set_reflection_af_flag(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) - - self.btn_startAcquisition = QPushButton('Start\n Acquisition ') - self.btn_startAcquisition.setStyleSheet("background-color: #C2C2FF") - self.btn_startAcquisition.setCheckable(True) - self.btn_startAcquisition.setChecked(False) - - # layout - grid_line0 = QGridLayout() - grid_line0.addWidget(QLabel('Saving Path')) - grid_line0.addWidget(self.lineEdit_savingDir, 0,1) - grid_line0.addWidget(self.btn_setSavingDir, 0,2) - - grid_line1 = QGridLayout() - grid_line1.addWidget(QLabel('Experiment ID'), 0,0) - grid_line1.addWidget(self.lineEdit_experimentID,0,1) - - grid_line2 = QGridLayout() - grid_line2.addWidget(QLabel('dx'), 0,0) - grid_line2.addWidget(self.entry_deltaX, 0,1) - grid_line2.addWidget(QLabel('Nx'), 0,3) - grid_line2.addWidget(self.entry_NX, 0,4) - grid_line2.addWidget(QLabel('dy'), 0,6) - grid_line2.addWidget(self.entry_deltaY, 0,7) - grid_line2.addWidget(QLabel('Ny'), 0,9) - grid_line2.addWidget(self.entry_NY, 0,10) - - grid_line2.addWidget(QLabel('dz'), 1,0) - grid_line2.addWidget(self.entry_deltaZ, 1,1) - grid_line2.addWidget(QLabel('Nz'), 1,3) - grid_line2.addWidget(self.entry_NZ, 1,4) - grid_line2.addWidget(QLabel('dt'), 1,6) - grid_line2.addWidget(self.entry_dt, 1,7) - grid_line2.addWidget(QLabel('Nt'), 1,9) - grid_line2.addWidget(self.entry_Nt, 1,10) - - grid_line2.setColumnStretch(2, 1) - grid_line2.setColumnStretch(5, 1) - grid_line2.setColumnStretch(8, 1) - - grid_af = QGridLayout() - grid_af.addItem(QSpacerItem(7, 1, QSizePolicy.Fixed, QSizePolicy.Minimum), 0, 0) - grid_af.addWidget(self.checkbox_withAutofocus,0,1) - if SUPPORT_LASER_AUTOFOCUS: - grid_af.addWidget(self.checkbox_withReflectionAutofocus,1,1) - grid_af.addWidget(self.checkbox_genFocusMap,2,1) - if ENABLE_STITCHER: - grid_af.addWidget(self.checkbox_stitchOutput,3,1) - grid_af.addItem(QSpacerItem(6, 1, QSizePolicy.Fixed, QSizePolicy.Minimum), 0, 2) - - grid_line3 = QHBoxLayout() - grid_line3.addWidget(self.list_configurations, 2) - # grid_line3.addWidget(self.checkbox_withAutofocus) - grid_line3.addLayout(grid_af, 1) - grid_line3.addWidget(self.btn_startAcquisition, 1) - - self.grid = QGridLayout() - self.grid.addLayout(grid_line0,0,0) - self.grid.addLayout(grid_line1,1,0) - self.grid.addLayout(grid_line2,2,0) - self.grid.addLayout(grid_line3,3,0) - self.setLayout(self.grid) - - # add and display a timer - to be implemented - # self.timer = QTimer() - - # connections - self.entry_deltaX.valueChanged.connect(self.set_deltaX) - self.entry_deltaY.valueChanged.connect(self.set_deltaY) - self.entry_deltaZ.valueChanged.connect(self.set_deltaZ) - self.entry_dt.valueChanged.connect(self.multipointController.set_deltat) - self.entry_NX.valueChanged.connect(self.multipointController.set_NX) - self.entry_NY.valueChanged.connect(self.multipointController.set_NY) - self.entry_NZ.valueChanged.connect(self.multipointController.set_NZ) - self.entry_NZ.valueChanged.connect(self.signal_acquisition_z_levels.emit) - self.entry_Nt.valueChanged.connect(self.multipointController.set_Nt) - self.checkbox_withAutofocus.stateChanged.connect(self.multipointController.set_af_flag) - self.checkbox_withReflectionAutofocus.stateChanged.connect(self.multipointController.set_reflection_af_flag) - self.checkbox_genFocusMap.stateChanged.connect(self.multipointController.set_gen_focus_map_flag) - self.checkbox_stitchOutput.toggled.connect(self.display_stitcher_widget) - self.btn_setSavingDir.clicked.connect(self.set_saving_dir) - self.btn_startAcquisition.clicked.connect(self.toggle_acquisition) - self.multipointController.acquisitionFinished.connect(self.acquisition_is_finished) - self.list_configurations.itemSelectionChanged.connect(self.emit_selected_channels) - - def set_deltaX(self,value): - mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_X() - deltaX = round(value/mm_per_ustep)*mm_per_ustep - self.entry_deltaX.setValue(deltaX) - self.multipointController.set_deltaX(deltaX) - - def set_deltaY(self,value): - mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_Y() - deltaY = round(value/mm_per_ustep)*mm_per_ustep - self.entry_deltaY.setValue(deltaY) - self.multipointController.set_deltaY(deltaY) - - def set_deltaZ(self,value): - mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_Z() - deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 - self.entry_deltaZ.setValue(deltaZ) - self.multipointController.set_deltaZ(deltaZ) - - def set_saving_dir(self): - dialog = QFileDialog() - save_dir_base = dialog.getExistingDirectory(None, "Select Folder") - self.multipointController.set_base_path(save_dir_base) - self.lineEdit_savingDir.setText(save_dir_base) - self.base_path_is_set = True - - def set_well_selected(self, selected): - self.well_selected = selected - - def emit_selected_channels(self): - selected_channels = [item.text() for item in self.list_configurations.selectedItems()] - print(selected_channels) - self.signal_acquisition_channels.emit(selected_channels) - - def toggle_acquisition(self,pressed): - if self.base_path_is_set == False: - self.btn_startAcquisition.setChecked(False) - msg = QMessageBox() - msg.setText("Please choose base saving directory first") - msg.exec_() - return - if self.well_selected == False and self.multipointController.scanCoordinates.format != 0: - self.btn_startAcquisition.setChecked(False) - msg = QMessageBox() - msg.setText("Please select a well to scan first") - msg.exec_() - return - if not self.list_configurations.selectedItems(): # no channel selected - self.btn_startAcquisition.setChecked(False) - msg = QMessageBox() - msg.setText("Please select at least one imaging channel first") - msg.exec_() - return - if pressed: - # @@@ to do: add a widgetManger to enable and disable widget - # @@@ to do: emit signal to widgetManager to disable other widgets - self.setEnabled_all(False) - - # set parameters - if self.multipointController.scanCoordinates is not None: - self.multipointController.scanCoordinates.grid_skip_positions = [] - self.multipointController.set_deltaX(self.entry_deltaX.value()) - self.multipointController.set_deltaY(self.entry_deltaY.value()) - self.multipointController.set_deltaZ(self.entry_deltaZ.value()) - self.multipointController.set_deltat(self.entry_dt.value()) - self.multipointController.set_NX(self.entry_NX.value()) - self.multipointController.set_NY(self.entry_NY.value()) - self.multipointController.set_NZ(self.entry_NZ.value()) - self.multipointController.set_Nt(self.entry_Nt.value()) - self.multipointController.set_af_flag(self.checkbox_withAutofocus.isChecked()) - self.multipointController.set_reflection_af_flag(self.checkbox_withReflectionAutofocus.isChecked()) - self.multipointController.set_base_path(self.lineEdit_savingDir.text()) - self.multipointController.set_selected_configurations((item.text() for item in self.list_configurations.selectedItems())) - self.multipointController.start_new_experiment(self.lineEdit_experimentID.text()) - - # emit acquisition data - self.signal_acquisition_started.emit(True) - self.signal_acquisition_shape.emit(self.entry_NX.value(), - self.entry_NY.value(), - self.entry_NZ.value(), - self.entry_deltaX.value(), - self.entry_deltaY.value(), - self.entry_deltaZ.value()) - - self.multipointController.run_acquisition() - else: - self.multipointController.request_abort_aquisition() - self.setEnabled_all(True) - - def acquisition_is_finished(self): - self.signal_acquisition_started.emit(False) - self.btn_startAcquisition.setChecked(False) - self.setEnabled_all(True) - - def setEnabled_all(self,enabled,exclude_btn_startAcquisition=True): - self.btn_setSavingDir.setEnabled(enabled) - self.lineEdit_savingDir.setEnabled(enabled) - self.lineEdit_experimentID.setEnabled(enabled) - self.entry_deltaX.setEnabled(enabled) - self.entry_NX.setEnabled(enabled) - self.entry_deltaY.setEnabled(enabled) - self.entry_NY.setEnabled(enabled) - self.entry_deltaZ.setEnabled(enabled) - self.entry_NZ.setEnabled(enabled) - self.entry_dt.setEnabled(enabled) - self.entry_Nt.setEnabled(enabled) - self.list_configurations.setEnabled(enabled) - self.checkbox_withAutofocus.setEnabled(enabled) - self.checkbox_withReflectionAutofocus.setEnabled(enabled) - self.checkbox_genFocusMap.setEnabled(enabled) - self.checkbox_stitchOutput.setEnabled(enabled) - if exclude_btn_startAcquisition is not True: - self.btn_startAcquisition.setEnabled(enabled) - - def display_stitcher_widget(self, checked): - self.signal_stitcher_widget.emit(checked) - - def disable_the_start_aquisition_button(self): - self.btn_startAcquisition.setEnabled(False) - - def enable_the_start_aquisition_button(self): - self.btn_startAcquisition.setEnabled(True) - - class MultiPointWidget2(QFrame): signal_acquisition_started = Signal(bool) signal_acquisition_channels = Signal(list) signal_acquisition_z_levels = Signal(int) - signal_acquisition_shape = Signal(int, int, int, float, float, float) + signal_acquisition_shape = Signal(int, float) signal_stitcher_widget = Signal(bool) - def __init__(self, navigationController, navigationViewer, multipointController, configurationManager = None, main=None, scanCoordinates=None, *args, **kwargs): + def __init__(self, navigationController, navigationViewer, multipointController, objectiveStore, configurationManager = None, main=None, scanCoordinates=None, *args, **kwargs): super().__init__(*args, **kwargs) self.last_used_locations = None self.last_used_location_ids = None self.multipointController = multipointController + self.objectiveStore = objectiveStore self.configurationManager = configurationManager self.navigationController = navigationController self.navigationViewer = navigationViewer @@ -2339,6 +2019,8 @@ def __init__(self, navigationController, navigationViewer, multipointController, self.base_path_is_set = False self.location_list = np.empty((0, 3), dtype=float) self.location_ids = np.empty((0,), dtype='= 0: + # Get the region ID + region_id = self.location_ids[index] + print("Before Removal:") + print(f"Location IDs: {self.location_ids}") + print(f"Region FOV Coordinates Dict Keys: {list(self.region_fov_coordinates_dict.keys())}") + + print(f"Removing Region: {region_id}") + # Remove overlays + if region_id in self.region_fov_coordinates_dict: + for coord in self.region_fov_coordinates_dict[region_id]: + self.navigationViewer.deregister_fov_to_image(coord[0], coord[1]) + del self.region_fov_coordinates_dict[region_id] + + # Remove from data structures + self.location_list = np.delete(self.location_list, index, axis=0) + self.location_ids = np.delete(self.location_ids, index) + if region_id in self.region_coordinates: + del self.region_coordinates[region_id] + + # Update remaining IDs + for i in range(index, len(self.location_ids)): + old_id = self.location_ids[i] + new_id = f'R{i}' + self.location_ids[i] = new_id + self.region_coordinates[new_id] = self.region_coordinates.pop(old_id) + self.region_fov_coordinates_dict[new_id] = self.region_fov_coordinates_dict.pop(old_id) + + # Update UI + self.table_location_list.setItem(i, 3, QTableWidgetItem(new_id)) + location_str = f"x:{self.location_list[i, 0]} mm y:{self.location_list[i, 1]} mm z:{self.location_list[i, 2] * 1000} μm" + self.dropdown_location_list.setItemText(i, location_str) + + # Update UI + self.dropdown_location_list.removeItem(index) + self.table_location_list.removeRow(index) + + print("After Removal:") + print(f"Location IDs: {self.location_ids}") + print(f"Region FOV Coordinates Dict Keys: {list(self.region_fov_coordinates_dict.keys())}") + + # Clear overlay if no locations remain + if len(self.location_list) == 0: + self.navigationViewer.clear_overlay() def create_point_id(self): self.scanCoordinates.get_selected_wells() @@ -3015,21 +2785,6 @@ def create_point_id(self): new_id = f'{name}-0' return new_id - def remove_location(self): - index = self.dropdown_location_list.currentIndex() - if index >=0: - self.dropdown_location_list.removeItem(index) - self.table_location_list.removeRow(index) - x = self.location_list[index,0] - y = self.location_list[index,1] - z = self.location_list[index,2] - self.navigationViewer.deregister_fov_to_image(x,y) - self.location_list = np.delete(self.location_list, index, axis=0) - self.location_ids = np.delete(self.location_ids, index, axis=0) - if len(self.location_list) == 0: - self.navigationViewer.clear_slide() - print(self.location_list) - def next(self): index = self.dropdown_location_list.currentIndex() # max_index = self.dropdown_location_list.count() - 1 @@ -3057,11 +2812,15 @@ def previous(self): def clear(self): self.location_list = np.empty((0, 3), dtype=float) - self.location_ids = np.empty((0,), dtype=str) + self.location_ids = np.empty((0,), dtype=' 1 else scan_size_mm - - else: - # Use grid-based acquisition - if self.scanCoordinates.format == 'glass slide' or len(self.region_coordinates) == 0: - x = self.navigationController.x_pos_mm - y = self.navigationController.y_pos_mm - z = self.navigationController.z_pos_mm - self.region_coordinates['current'] = [x, y, z] - steps, step_size_mm = self.create_scan_grid( + if len(self.region_coordinates) == 0: + # Use current location if no regions added + x = self.navigationController.x_pos_mm + y = self.navigationController.y_pos_mm + z = self.navigationController.z_pos_mm + self.region_coordinates['current'] = [x, y, z] + scan_coordinates = self.create_region_coordinates( self.objectiveStore, + x, y, scan_size_mm=scan_size_mm, overlap_percent=overlap_percent, shape=shape ) - Nx = Ny = steps - dx_mm = dy_mm = step_size_mm + self.region_fov_coordinates_dict['current'] = scan_coordinates - # Set up multipoint controller - self.multipointController.set_NX(Nx) - self.multipointController.set_NY(Ny) - self.multipointController.set_deltaX(dx_mm) - self.multipointController.set_deltaY(dy_mm) + # Calculate total number of positions for signal emission # not needed ever + total_positions = sum(len(coords) for coords in self.region_fov_coordinates_dict.values()) + Nx = Ny = int(math.sqrt(total_positions)) + dx_mm = dy_mm = scan_size_mm / (Nx - 1) if Nx > 1 else scan_size_mm if self.checkbox_set_z_range.isChecked(): # Set Z-range (convert from μm to mm) @@ -4144,17 +3874,11 @@ def toggle_acquisition(self, pressed): # Emit signals self.signal_acquisition_started.emit(True) - self.signal_acquisition_shape.emit(Nx, Ny, self.entry_NZ.value(), - dx_mm, dy_mm, self.entry_deltaZ.value()) + self.signal_acquisition_shape.emit(self.entry_NZ.value(), self.entry_deltaZ.value()) # Start acquisition - if self.use_coordinate_acquisition: - self.multipointController.run_acquisition(location_list=self.region_coordinates, coordinate_dict=self.region_fov_coordinates_dict) - else: - if self.scanCoordinates.format == 'glass slide': - self.multipointController.run_acquisition(location_list=list(self.region_coordinates.values())) # glass slide - else: - self.multipointController.run_acquisition() # wellplate + self.multipointController.run_acquisition(location_list=self.region_coordinates, coordinate_dict=self.region_fov_coordinates_dict) + else: self.multipointController.request_abort_aquisition() self.setEnabled_all(True) @@ -4953,7 +4677,7 @@ def customizeViewer(self): if hasattr(self.viewer.window._qt_viewer, 'layerButtons'): self.viewer.window._qt_viewer.layerButtons.hide() - def initLayersShape(self, Nx, Ny, Nz, dx, dy, dz): + def initLayersShape(self, Nz, dz): pixel_size_um = self.objectiveStore.get_pixel_size() if self.Nz != Nz or self.dz_um != dz or self.pixel_size_um != pixel_size_um: self.acquisition_initialized = False @@ -5107,187 +4831,6 @@ def activate(self): self.viewer.window.activate() -class NapariTiledDisplayWidget(QWidget): - - signal_coordinates_clicked = Signal(int, int, int, int, int, int, float, float) - - def __init__(self, objectiveStore, contrastManager, parent=None): - super().__init__(parent) - # Initialize placeholders for the acquisition parameters - self.objectiveStore = objectiveStore - self.contrastManager = contrastManager - self.downsample_factor = PRVIEW_DOWNSAMPLE_FACTOR - self.image_width = 0 - self.image_height = 0 - self.dtype = np.uint8 - self.channels = set() - self.Nx = 1 - self.Ny = 1 - self.Nz = 1 - self.dz_um = 1 - self.pixel_size_um = 1 - self.layers_initialized = False - self.acquisition_initialized = False - self.viewer_scale_initialized = False - self.initNapariViewer() - - def initNapariViewer(self): - self.viewer = napari.Viewer(show=False) #, ndisplay=3) - self.viewerWidget = self.viewer.window._qt_window - self.viewer.dims.axis_labels = ['Z-axis', 'Y-axis', 'X-axis'] - self.layout = QVBoxLayout() - self.layout.addWidget(self.viewerWidget) - self.setLayout(self.layout) - self.customizeViewer() - - def customizeViewer(self): - # Hide the status bar (which includes the activity button) - if hasattr(self.viewer.window, '_status_bar'): - self.viewer.window._status_bar.hide() - - # Hide the layer buttons - if hasattr(self.viewer.window._qt_viewer, 'layerButtons'): - self.viewer.window._qt_viewer.layerButtons.hide() - - def initLayersShape(self, Nx, Ny, Nz, dx, dy, dz): - self.acquisition_initialized = False - self.Nx = Nx - self.Ny = Ny - self.Nz = Nz - self.dx_mm = dx - self.dy_mm = dy - self.dz_um = dz if Nz > 1 and dz != 0 else 1.0 - pixel_size_um = self.objectiveStore.get_pixel_size() - self.pixel_size_um = pixel_size_um * self.downsample_factor - - def initChannels(self, channels): - self.channels = set(channels) - - def extractWavelength(self, name): - # Split the string and find the wavelength number immediately after "Fluorescence" - parts = name.split() - if 'Fluorescence' in parts: - index = parts.index('Fluorescence') + 1 - if index < len(parts): - return parts[index].split()[0] # Assuming '488 nm Ex' and taking '488' - for color in ['R', 'G', 'B']: - if color in parts or f"full_{color}" in parts: - return color - return None - - def generateColormap(self, channel_info): - """Convert a HEX value to a normalized RGB tuple.""" - c0 = (0, 0, 0) - c1 = (((channel_info['hex'] >> 16) & 0xFF) / 255, # Normalize the Red component - ((channel_info['hex'] >> 8) & 0xFF) / 255, # Normalize the Green component - (channel_info['hex'] & 0xFF) / 255) # Normalize the Blue component - return Colormap(colors=[c0, c1], controls=[0, 1], name=channel_info['name']) - - def initLayers(self, image_height, image_width, image_dtype): - """Initializes the full canvas for each channel based on the acquisition parameters.""" - if self.acquisition_initialized: - for layer in list(self.viewer.layers): - if layer.name not in self.channels: - self.viewer.layers.remove(layer) - else: - self.viewer.layers.clear() - self.acquisition_initialized = True - - self.image_width = image_width // self.downsample_factor - self.image_height = image_height // self.downsample_factor - self.dtype = np.dtype(image_dtype) - self.layers_initialized = True - self.resetView() - self.viewer_scale_initialized = False - - def updateLayers(self, image, i, j, k, channel_name): - """Updates the appropriate slice of the canvas with the new image data.""" - if i == -1 or j == -1: - print("no tiled preview for coordinate acquisition") - return - - # Check if the layer exists and has a different dtype - if self.dtype != image.dtype: - # Remove the existing layer - self.layers_initialized = False - self.acquisition_initialized = False - - if not self.layers_initialized: - self.initLayers(image.shape[0], image.shape[1], image.dtype) - - rgb = len(image.shape) == 3 # Check if image is RGB based on shape - if channel_name not in self.viewer.layers: - self.channels.add(channel_name) - if rgb: - color = None # No colormap for RGB images - canvas = np.zeros((self.Nz, self.Ny * self.image_height, self.Nx * self.image_width, 3), dtype=self.dtype) - else: - channel_info = CHANNEL_COLORS_MAP.get(self.extractWavelength(channel_name), {'hex': 0xFFFFFF, 'name': 'gray'}) - if channel_info['name'] in AVAILABLE_COLORMAPS: - color = AVAILABLE_COLORMAPS[channel_info['name']] - else: - color = self.generateColormap(channel_info) - canvas = np.zeros((self.Nz, self.Ny * self.image_height, self.Nx * self.image_width), dtype=self.dtype) - - limits = self.getContrastLimits(self.dtype) - layer = self.viewer.add_image(canvas, name=channel_name, visible=True, rgb=rgb, - colormap=color, contrast_limits=limits, blending='additive', - scale=(self.dz_um, self.pixel_size_um, self.pixel_size_um)) - # print(f"tiled display - dz_um:{self.dz_um}, pixel_y_um:{self.pixel_size_um}, pixel_x_um:{self.pixel_size_um}") - layer.contrast_limits = self.contrastManager.get_limits(channel_name) - layer.events.contrast_limits.connect(self.signalContrastLimits) - layer.mouse_double_click_callbacks.append(self.onDoubleClick) - - image = cv2.resize(image, (self.image_width, self.image_height), interpolation=cv2.INTER_AREA) - - if not self.viewer_scale_initialized: - self.resetView() - self.viewer_scale_initialized = True - self.viewer.dims.set_point(0, k * self.dz_um) - layer = self.viewer.layers[channel_name] - layer.contrast_limits = self.contrastManager.get_limits(channel_name) - layer_data = layer.data - y_slice = slice(i * self.image_height, (i + 1) * self.image_height) - x_slice = slice(j * self.image_width, (j + 1) * self.image_width) - if rgb: - layer_data[k, y_slice, x_slice, :] = image - else: - layer_data[k, y_slice, x_slice] = image - layer.data = layer_data - layer.refresh() - - def signalContrastLimits(self, event): - layer = event.source - min_val, max_val = map(float, layer.contrast_limits) - self.contrastManager.update_limits(layer.name, min_val, max_val) - - def getContrastLimits(self, dtype): - return self.contrastManager.get_default_limits() - - def onDoubleClick(self, layer, event): - """Handle double-click events and emit centered coordinates if within the data range.""" - coords = layer.world_to_data(event.position) - layer_shape = layer.data.shape[0:3] if len(layer.data.shape) >= 4 else layer.data.shape - - if coords is not None and (0 <= int(coords[-1]) < layer_shape[-1] and (0 <= int(coords[-2]) < layer_shape[-2])): - x_centered = int(coords[-1] - layer_shape[-1] / 2) - y_centered = int(coords[-2] - layer_shape[-2] / 2) - # Emit the centered coordinates and dimensions of the layer's data array - self.signal_coordinates_clicked.emit(x_centered, y_centered, - layer_shape[-1], layer_shape[-2], - self.Nx, self.Ny, - self.dx_mm, self.dy_mm) - - def resetView(self): - self.viewer.reset_view() - for layer in self.viewer.layers: - layer.refresh() - - def activate(self): - print("ACTIVATING NAPARI TILED DISPLAY WIDGET") - self.viewer.window.activate() - - class NapariMosaicDisplayWidget(QWidget): signal_coordinates_clicked = Signal(float, float) # x, y in mm @@ -5403,7 +4946,7 @@ def update_shape_layer_position(self, prev_top_left, new_top_left): def initChannels(self, channels): self.channels = set(channels) - def initLayersShape(self, Nx, Ny, Nz, dx, dy, dz): + def initLayersShape(self, Nz, dz): self.Nz = 1 self.dz_um = dz From 79ab266f952ee2d7249fd4e62540eb83510375be Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Tue, 3 Dec 2024 15:20:50 -0800 Subject: [PATCH 2/9] cleanup --- software/control/core.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/software/control/core.py b/software/control/core.py index cbf0ecd1..6fcebf78 100644 --- a/software/control/core.py +++ b/software/control/core.py @@ -2649,28 +2649,9 @@ def run_acquisition(self, location_list=None, coordinate_dict=None): self.use_scan_coordinates = False self.scan_coordinates_mm = location_list self.scan_coordinates_name = list(coordinate_dict.keys()) # list(coordinate_dict.keys()) if not wellplate - - # elif location_list is not None: - # print('Using location list acquisition') - # self.coordinate_dict = None - # self.location_list = location_list - # self.use_scan_coordinates = True - # self.scan_coordinates_mm = location_list - # self.scan_coordinates_name = [f'R{i}' for i in range(len(location_list))] - # else: - # print(f"t_c_z_y_x: {self.Nt}_{len(self.selected_configurations)}_{self.NZ}_{self.NY}_{self.NX}") - # self.coordinate_dict = None - # self.location_list = None - # if self.scanCoordinates is not None and self.scanCoordinates.get_selected_wells(): - # print('Using well plate scan') - # self.use_scan_coordinates = True - # self.scan_coordinates_mm = self.scanCoordinates.coordinates_mm - # self.scan_coordinates_name = self.scanCoordinates.name - # else: - # print('Using current location') - # self.use_scan_coordinates = False - # self.scan_coordinates_mm = [(self.navigationController.x_pos_mm, self.navigationController.y_pos_mm)] - # self.scan_coordinates_name = ['ROI'] + else: + print("obsolete functionailty. use coordinate acquisition instead of grid acquisition") + return print("num regions:",len(self.scan_coordinates_mm)) print("region ids:", self.scan_coordinates_name) @@ -4036,7 +4017,6 @@ def __init__(self): self.wellplate_offset_y_mm = WELLPLATE_OFFSET_Y_mm self.well_spacing_mm = WELL_SPACING_MM self.well_size_mm = WELL_SIZE_MM - # self.grid_skip_positions = [] # remove def _index_to_row(self,index): index += 1 From 1ec84c6fcbc5eb5b2ca7f4d62c4d7683ddfb1d69 Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Tue, 3 Dec 2024 16:39:14 -0800 Subject: [PATCH 3/9] layout flexible fixed --- software/control/core.py | 5 - software/control/widgets.py | 199 +++++++++++++++++++++--------------- 2 files changed, 118 insertions(+), 86 deletions(-) diff --git a/software/control/core.py b/software/control/core.py index 6fcebf78..a4f498a0 100644 --- a/software/control/core.py +++ b/software/control/core.py @@ -42,7 +42,6 @@ import numpy as np import pandas as pd import scipy.signal -from scipy import interpolate import cv2 import imageio as iio @@ -4069,10 +4068,6 @@ def get_selected_wells(self): _increasing = not _increasing return len(selected_wells) # if wells selected - def get_approx_well_from_coordinate(self, x_mm, y_mm): - pass - # to implement later - class LaserAutofocusController(QObject): diff --git a/software/control/widgets.py b/software/control/widgets.py index c2254dfd..f13b9352 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -23,7 +23,6 @@ import itertools import numpy as np from scipy.spatial import Delaunay -from scipy import interpolate import shutil from control._def import * from PIL import Image, ImageDraw, ImageFont @@ -2107,15 +2106,6 @@ def add_components(self): self.entry_NZ.setValue(1) self.entry_NZ.setKeyboardTracking(False) - max_entry_width = max(self.entry_NX.sizeHint().width(), - self.entry_NY.sizeHint().width(), - self.entry_NZ.sizeHint().width()) - - # Apply the fixed width to Nx and Ny - self.entry_NX.setFixedWidth(max_entry_width) - self.entry_NY.setFixedWidth(max_entry_width) - self.entry_NZ.setFixedWidth(max_entry_width) - self.entry_dt = QDoubleSpinBox() self.entry_dt.setMinimum(0) self.entry_dt.setMaximum(12*3600) @@ -2127,17 +2117,24 @@ def add_components(self): self.entry_Nt = QSpinBox() self.entry_Nt.setMinimum(1) - self.entry_Nt.setMaximum(5000) # @@@ to be changed + self.entry_Nt.setMaximum(10000) # @@@ to be changed self.entry_Nt.setSingleStep(1) self.entry_Nt.setValue(1) self.entry_Nt.setKeyboardTracking(False) # Calculate a consistent width - max_width = max(self.entry_deltaZ.sizeHint().width(), self.entry_dt.sizeHint().width()) - - # Apply the consistent width - self.entry_deltaZ.setFixedWidth(max_width) - self.entry_dt.setFixedWidth(max_width) + max_delta_width = max(self.entry_deltaZ.sizeHint().width(), self.entry_dt.sizeHint().width()) + self.entry_deltaZ.setFixedWidth(max_delta_width) + self.entry_dt.setFixedWidth(max_delta_width) + + max_num_width = max(self.entry_NX.sizeHint().width(), + self.entry_NY.sizeHint().width(), + self.entry_NZ.sizeHint().width(), + self.entry_Nt.sizeHint().width()) + self.entry_NX.setFixedWidth(max_num_width) + self.entry_NY.setFixedWidth(max_num_width) + self.entry_NZ.setFixedWidth(max_num_width) + self.entry_Nt.setFixedWidth(max_num_width) self.list_configurations = QListWidget() for microscope_configuration in self.configurationManager.configurations: @@ -2226,39 +2223,61 @@ def add_components(self): grid_line1.addWidget(self.btn_import_locations, 2, 0, 1, 4) grid_line1.addWidget(self.btn_export_locations, 2, 4, 1, 4) - grid_line2 = QGridLayout() - # Add first half (Nx and Ny) - grid_line2.addWidget(QLabel('Nx'), 3, 0, Qt.AlignRight) - grid_line2.addWidget(self.entry_NX, 3, 1) - grid_line2.addWidget(QLabel('Ny'), 3, 2, Qt.AlignRight) - grid_line2.addWidget(self.entry_NY, 3, 3) - - # Add second half (FOV Overlap) - grid_line2.addWidget(QLabel('FOV Overlap'), 3, 4, 1, 2, Qt.AlignRight) - grid_line2.addWidget(self.entry_overlap, 3, 6, 1, 2) - - grid_line2.addWidget(QLabel('dz'), 4, 0, Qt.AlignRight) - grid_line2.addWidget(self.entry_deltaZ, 4, 1) - grid_line2.addWidget(QLabel('Nz'), 4, 2, Qt.AlignRight) - grid_line2.addWidget(self.entry_NZ, 4, 3) - - grid_line2.addWidget(QLabel('dt'), 4, 4, Qt.AlignRight) - grid_line2.addWidget(self.entry_dt, 4, 5) - grid_line2.addWidget(QLabel('Nt'), 4, 6, Qt.AlignRight) - grid_line2.addWidget(self.entry_Nt, 4, 7) + # Create spacer items + EDGE_SPACING = 4 # Adjust this value as needed + edge_spacer = QSpacerItem(EDGE_SPACING, 0, QSizePolicy.Fixed, QSizePolicy.Minimum) + + # Create first row layouts + xy_half = QHBoxLayout() + xy_half.addWidget(QLabel('Nx')) + xy_half.addWidget(self.entry_NX) + xy_half.addStretch(1) + xy_half.addWidget(QLabel('Ny')) + xy_half.addWidget(self.entry_NY) + xy_half.addSpacerItem(edge_spacer) + + overlap_half = QHBoxLayout() + overlap_half.addSpacerItem(edge_spacer) + overlap_half.addWidget(QLabel('FOV Overlap'), alignment=Qt.AlignRight) + overlap_half.addWidget(self.entry_overlap) + + # Create second row layouts + dz_half = QHBoxLayout() + dz_half.addWidget(QLabel('dz')) + dz_half.addWidget(self.entry_deltaZ) + dz_half.addStretch(1) + dz_half.addWidget(QLabel('Nz')) + dz_half.addWidget(self.entry_NZ) + dz_half.addSpacerItem(edge_spacer) + + dt_half = QHBoxLayout() + dt_half.addSpacerItem(edge_spacer) + dt_half.addWidget(QLabel('dt')) + dt_half.addWidget(self.entry_dt) + dt_half.addStretch(1) + dt_half.addWidget(QLabel('Nt')) + dt_half.addWidget(self.entry_Nt) + + # Add the layouts to grid_line1 + grid_line1.addLayout(xy_half, 3, 0, 1, 4) + grid_line1.addLayout(overlap_half, 3, 4, 1, 4) + grid_line1.addLayout(dz_half, 4, 0, 1, 4) + grid_line1.addLayout(dt_half, 4, 4, 1, 4) self.z_min_layout = QHBoxLayout() - self.z_min_layout.addWidget(QLabel('Z-min')) + self.z_min_layout.addWidget(self.set_minZ_button) + self.z_min_layout.addWidget(QLabel('Z-min'), Qt.AlignRight) self.z_min_layout.addWidget(self.entry_minZ) - self.z_min_layout.addWidget(self.set_minZ_button, Qt.AlignLeft) + self.z_min_layout.addSpacerItem(edge_spacer) self.z_max_layout = QHBoxLayout() - self.z_max_layout.addWidget(QLabel('Z-max')) + self.z_max_layout.addSpacerItem(edge_spacer) + self.z_max_layout.addWidget(self.set_maxZ_button) + self.z_max_layout.addWidget(QLabel('Z-max'), Qt.AlignRight) self.z_max_layout.addWidget(self.entry_maxZ) - self.z_max_layout.addWidget(self.set_maxZ_button, Qt.AlignLeft) - grid_line2.addLayout(self.z_min_layout, 5, 0, 1, 4) # hide this in toggle - grid_line2.addLayout(self.z_max_layout, 5, 4, 1, 4) # hide this in toggle + grid_line1.addLayout(self.z_min_layout, 5, 0, 1, 4) # hide this in toggle + grid_line1.addLayout(self.z_max_layout, 5, 4, 1, 4) # hide this in toggle grid_af = QVBoxLayout() grid_af.addWidget(self.checkbox_withAutofocus) @@ -2271,23 +2290,38 @@ def add_components(self): if ENABLE_STITCHER: grid_af.addWidget(self.checkbox_stitchOutput) - grid_line2.addWidget(self.list_configurations,6,0,1,4) - grid_line2.addLayout(grid_af,6,4,1,2, Qt.AlignCenter) - grid_line2.addWidget(self.btn_startAcquisition,6,6,1,2) + grid_config = QHBoxLayout() + grid_config.addWidget(self.list_configurations) + grid_config.addSpacerItem(edge_spacer) + + grid_acquisition = QHBoxLayout() + grid_acquisition.addSpacerItem(edge_spacer) + grid_acquisition.addLayout(grid_af) + grid_acquisition.addWidget(self.btn_startAcquisition) + + grid_line1.addLayout(grid_config,6,0,3,4) + grid_line1.addLayout(grid_acquisition,6,4,3,4) - #set column stetch of 0-3 equal to 4-7 - #set column stetch of 0-1 equal to 2-3 # Columns 0-3: Combined stretch factor = 4 - grid_line2.setColumnStretch(0, 1) - grid_line2.setColumnStretch(1, 1) - grid_line2.setColumnStretch(2, 1) - grid_line2.setColumnStretch(3, 1) + grid_line1.setColumnStretch(0, 1) + grid_line1.setColumnStretch(1, 1) + grid_line1.setColumnStretch(2, 1) + grid_line1.setColumnStretch(3, 1) # Columns 4-7: Combined stretch factor = 4 - grid_line2.setColumnStretch(4, 1) - grid_line2.setColumnStretch(5, 1) - grid_line2.setColumnStretch(6, 1) - grid_line2.setColumnStretch(7, 1) + grid_line1.setColumnStretch(4, 1) + grid_line1.setColumnStretch(5, 1) + grid_line1.setColumnStretch(6, 1) + grid_line1.setColumnStretch(7, 1) + + grid_line1.setRowStretch(0, 0) # Location list row + grid_line1.setRowStretch(1, 0) # Button row + grid_line1.setRowStretch(2, 0) # Import/Export buttons + grid_line1.setRowStretch(3, 0) # Nx/Ny and overlap row + grid_line1.setRowStretch(4, 0) # dz/Nz and dt/Nt row + grid_line1.setRowStretch(5, 0) # Z-range row + grid_line1.setRowStretch(6, 1) # Configuration/AF row - allow this to stretch + grid_line1.setRowStretch(7, 0) # Last row # Row : Progress Bar row_progress_layout = QHBoxLayout() @@ -2298,8 +2332,6 @@ def add_components(self): self.grid = QVBoxLayout() self.grid.addLayout(grid_line0) self.grid.addLayout(grid_line1) - self.grid.addLayout(grid_line2) - #self.grid.addLayout(grid_line3,4,0) self.grid.addLayout(row_progress_layout) self.setLayout(self.grid) @@ -2694,27 +2726,31 @@ def enable_the_start_aquisition_button(self): self.btn_startAcquisition.setEnabled(True) def add_location(self): - x = round(self.navigationController.x_pos_mm, 3) - y = round(self.navigationController.y_pos_mm, 3) - z = round(self.navigationController.z_pos_mm, 3) - name = f'R{len(self.location_ids)}' # ID based on current length of location_ids - - # Check for duplicates - if not np.any(np.all(self.location_list[:, :2] == [x, y], axis=1)): - # Update location data + # Get raw positions without rounding + x = self.navigationController.x_pos_mm + y = self.navigationController.y_pos_mm + z = self.navigationController.z_pos_mm + name = f'R{len(self.location_ids)}' + + # Check for duplicates using rounded values for comparison + if not np.any(np.all(self.location_list[:, :2] == [round(x,3), round(y,3)], axis=1)): + # Store actual values in location_list self.location_list = np.vstack((self.location_list, [[x, y, z]])) self.location_ids = np.append(self.location_ids, name) - # Update UI - location_str = f"x:{x} mm y:{y} mm z:{z*1000} μm" + # Display rounded values in UI + location_str = f"x:{round(x,3)} mm y:{round(y,3)} mm z:{round(z*1000,1)} μm" self.dropdown_location_list.addItem(location_str) - self.table_location_list.insertRow(self.table_location_list.rowCount()) - self.table_location_list.setItem(self.table_location_list.rowCount() - 1, 0, QTableWidgetItem(str(x))) - self.table_location_list.setItem(self.table_location_list.rowCount() - 1, 1, QTableWidgetItem(str(y))) - self.table_location_list.setItem(self.table_location_list.rowCount() - 1, 2, QTableWidgetItem(str(z * 1000))) - self.table_location_list.setItem(self.table_location_list.rowCount() - 1, 3, QTableWidgetItem(name)) - # Create region coordinates and update overlays + # Update table with rounded display values + row = self.table_location_list.rowCount() + self.table_location_list.insertRow(row) + self.table_location_list.setItem(row, 0, QTableWidgetItem(str(round(x,3)))) + self.table_location_list.setItem(row, 1, QTableWidgetItem(str(round(y,3)))) + self.table_location_list.setItem(row, 2, QTableWidgetItem(str(round(z*1000,1)))) + self.table_location_list.setItem(row, 3, QTableWidgetItem(name)) + + # Store actual values in region coordinates self.region_coordinates[name] = [x, y, z] scan_coordinates = self.create_region_coordinates(x, y, overlap_percent=self.entry_overlap.value()) self.region_fov_coordinates_dict[name] = scan_coordinates @@ -2723,7 +2759,6 @@ def add_location(self): else: print("Duplicate location not added.") - def remove_location(self): index = self.dropdown_location_list.currentIndex() if index >= 0: @@ -2733,8 +2768,7 @@ def remove_location(self): print(f"Location IDs: {self.location_ids}") print(f"Region FOV Coordinates Dict Keys: {list(self.region_fov_coordinates_dict.keys())}") - print(f"Removing Region: {region_id}") - # Remove overlays + # Remove overlays using actual stored coordinates if region_id in self.region_fov_coordinates_dict: for coord in self.region_fov_coordinates_dict[region_id]: self.navigationViewer.deregister_fov_to_image(coord[0], coord[1]) @@ -2746,17 +2780,20 @@ def remove_location(self): if region_id in self.region_coordinates: del self.region_coordinates[region_id] - # Update remaining IDs + # Update remaining IDs and UI for i in range(index, len(self.location_ids)): old_id = self.location_ids[i] new_id = f'R{i}' self.location_ids[i] = new_id + + # Update dictionaries self.region_coordinates[new_id] = self.region_coordinates.pop(old_id) self.region_fov_coordinates_dict[new_id] = self.region_fov_coordinates_dict.pop(old_id) - # Update UI + # Update UI with rounded display values self.table_location_list.setItem(i, 3, QTableWidgetItem(new_id)) - location_str = f"x:{self.location_list[i, 0]} mm y:{self.location_list[i, 1]} mm z:{self.location_list[i, 2] * 1000} μm" + x, y, z = self.location_list[i] + location_str = f"x:{round(x,3)} mm y:{round(y,3)} mm z:{round(z*1000,1)} μm" self.dropdown_location_list.setItemText(i, location_str) # Update UI @@ -3151,20 +3188,20 @@ def add_components(self): # Z-min self.z_min_layout = QHBoxLayout() + self.z_min_layout.addWidget(self.set_minZ_button) min_label = QLabel('Z-min') min_label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) self.z_min_layout.addWidget(min_label) self.z_min_layout.addWidget(self.entry_minZ) - self.z_min_layout.addWidget(self.set_minZ_button, Qt.AlignLeft) grid.addLayout(self.z_min_layout, 1, 0) # Z-max self.z_max_layout = QHBoxLayout() + self.z_max_layout.addWidget(self.set_maxZ_button) max_label = QLabel('Z-max') max_label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter) self.z_max_layout.addWidget(max_label) self.z_max_layout.addWidget(self.entry_maxZ) - self.z_max_layout.addWidget(self.set_maxZ_button, Qt.AlignLeft) grid.addLayout(self.z_max_layout, 1, 2) w = max(min_label.sizeHint().width(), max_label.sizeHint().width()) From c0dd22da4a4be1c89823a36209f7098b888b9d5d Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Tue, 3 Dec 2024 17:04:06 -0800 Subject: [PATCH 4/9] flexible and wellplate multipoint rename and cleanup --- software/control/_def.py | 2 +- software/control/core.py | 6 +- software/control/gui_hcs.py | 107 ++++++++++++++++++------------------ software/control/widgets.py | 4 +- 4 files changed, 60 insertions(+), 59 deletions(-) diff --git a/software/control/_def.py b/software/control/_def.py index c7a70db6..f557efcc 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -429,7 +429,7 @@ class SOFTWARE_POS_LIMIT: DEFAULT_MULTIPOINT_NY=1 ENABLE_FLEXIBLE_MULTIPOINT = True -ENABLE_SCAN_GRID = True +ENABLE_WELLPLATE_MULTIPOINT = True ENABLE_RECORDING = False CAMERA_SN = {'ch 1':'SN1','ch 2': 'SN2'} # for multiple cameras, to be overwritten in the configuration file diff --git a/software/control/core.py b/software/control/core.py index a4f498a0..f14d1a97 100644 --- a/software/control/core.py +++ b/software/control/core.py @@ -2072,15 +2072,15 @@ def perform_autofocus(self, region_id): self.scan_coordinates_mm[region_id][2] = self.navigationController.z_pos_mm # update the coordinate in the widget if self.coordinate_dict is not None: - self.microscope.multiPointWidgetGrid.update_region_z_level(region_id, self.navigationController.z_pos_mm) + self.microscope.wellplateMultiPointWidget.update_region_z_level(region_id, self.navigationController.z_pos_mm) elif self.multiPointController.location_list is not None: try: - self.microscope.multiPointWidget2._update_z(region_id, self.navigationController.z_pos_mm) + self.microscope.flexibleMultiPointWidget._update_z(region_id, self.navigationController.z_pos_mm) except: print("failed update flexible widget z") pass try: - self.microscope.multiPointWidgetGrid.update_region_z_level(region_id, self.navigationController.z_pos_mm) + self.microscope.wellplateMultiPointWidget.update_region_z_level(region_id, self.navigationController.z_pos_mm) except: print("failed update grid widget z") pass diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index a90e2cd3..77d41a1d 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -114,9 +114,9 @@ def __init__(self, is_simulation=False, live_only_mode=False, *args, **kwargs): if HOMING_ENABLED_X and HOMING_ENABLED_Y and HOMING_ENABLED_Z: self.navigationController.move_to_cached_position() self.waitForMicrocontroller() - if ENABLE_SCAN_GRID: - self.multiPointWidgetGrid.init_z() - self.multiPointWidget2.init_z() + if ENABLE_WELLPLATE_MULTIPOINT: + self.wellplateMultiPointWidget.init_z() + self.flexibleMultiPointWidget.init_z() # Create the menu bar menubar = self.menuBar() @@ -423,8 +423,8 @@ def loadWidgets(self): else: self.setupImageDisplayTabs() - self.multiPointWidget2 = widgets.MultiPointWidget2(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, scanCoordinates=None) - self.multiPointWidgetGrid = widgets.MultiPointWidgetGrid(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, self.scanCoordinates, self.napariMosaicDisplayWidget) + self.flexibleMultiPointWidget = widgets.FlexibleMultiPointWidget(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, scanCoordinates=None) + self.wellplateMultiPointWidget = widgets.WellplateMultiPointWidget(self.navigationController, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, self.scanCoordinates, self.napariMosaicDisplayWidget) self.sampleSettingsWidget = widgets.SampleSettingsWidget(self.objectivesWidget, self.wellplateFormatWidget) if ENABLE_TRACKING: @@ -503,10 +503,10 @@ def setupImageDisplayTabs(self): self.imageDisplayTabs.addTab(laserfocus_dockArea, "Laser-Based Focus") def setupRecordTabWidget(self): - if ENABLE_SCAN_GRID: - self.recordTabWidget.addTab(self.multiPointWidgetGrid, "Wellplate Multipoint") + if ENABLE_WELLPLATE_MULTIPOINT: + self.recordTabWidget.addTab(self.wellplateMultiPointWidget, "Wellplate Multipoint") if ENABLE_FLEXIBLE_MULTIPOINT: - self.recordTabWidget.addTab(self.multiPointWidget2, "Flexible Multipoint") + self.recordTabWidget.addTab(self.flexibleMultiPointWidget, "Flexible Multipoint") if ENABLE_TRACKING: self.recordTabWidget.addTab(self.trackingControlWidget, "Tracking") if ENABLE_RECORDING: @@ -636,18 +636,18 @@ def makeConnections(self): self.multipointController.signal_stitcher.connect(self.startStitcher) if ENABLE_FLEXIBLE_MULTIPOINT: - self.multiPointWidget2.signal_acquisition_started.connect(self.toggleAcquisitionStart) + self.flexibleMultiPointWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart) if ENABLE_STITCHER: - self.multiPointWidget2.signal_stitcher_widget.connect(self.toggleStitcherWidget) - self.multiPointWidget2.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.multiPointWidget2.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.flexibleMultiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) + self.flexibleMultiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) + self.flexibleMultiPointWidget.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) - if ENABLE_SCAN_GRID: - self.multiPointWidgetGrid.signal_acquisition_started.connect(self.toggleAcquisitionStart) + if ENABLE_WELLPLATE_MULTIPOINT: + self.wellplateMultiPointWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart) if ENABLE_STITCHER: - self.multiPointWidgetGrid.signal_stitcher_widget.connect(self.toggleStitcherWidget) - self.multiPointWidgetGrid.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.multiPointWidgetGrid.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.wellplateMultiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) + self.wellplateMultiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) + self.wellplateMultiPointWidget.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) self.liveControlWidget.signal_newExposureTime.connect(self.cameraSettingWidget.set_exposure_time) self.liveControlWidget.signal_newAnalogGain.connect(self.cameraSettingWidget.set_analog_gain) @@ -663,12 +663,12 @@ def makeConnections(self): self.navigationViewer.signal_coordinates_clicked.connect(self.navigationController.move_from_click_mosaic) self.objectivesWidget.signal_objective_changed.connect(self.navigationViewer.on_objective_changed) if ENABLE_FLEXIBLE_MULTIPOINT: - self.objectivesWidget.signal_objective_changed.connect(self.multiPointWidget2.update_fov_positions) + self.objectivesWidget.signal_objective_changed.connect(self.flexibleMultiPointWidget.update_fov_positions) self.navigationController.xyPos.connect(self.navigationViewer.update_current_location) self.multipointController.signal_register_current_fov.connect(self.navigationViewer.register_fov) self.multipointController.signal_current_configuration.connect(self.liveControlWidget.set_microscope_mode) self.multipointController.signal_z_piezo_um.connect(self.piezoWidget.update_displacement_um_display) - self.multiPointWidgetGrid.signal_z_stacking.connect(self.multipointController.set_z_stacking_config) + self.wellplateMultiPointWidget.signal_z_stacking.connect(self.multipointController.set_z_stacking_config) self.recordTabWidget.currentChanged.connect(self.onTabChanged) if not self.live_only_mode: @@ -702,10 +702,10 @@ def makeConnections(self): self.wellSelectionWidget.signal_wellSelectedPos.connect(self.navigationController.move_to) #self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) - if ENABLE_SCAN_GRID: - self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidgetGrid.set_well_coordinates) - self.objectivesWidget.signal_objective_changed.connect(self.multiPointWidgetGrid.update_coordinates) - self.multiPointWidgetGrid.signal_update_navigation_viewer.connect(self.navigationViewer.update_current_location) + if ENABLE_WELLPLATE_MULTIPOINT: + self.wellSelectionWidget.signal_wellSelected.connect(self.wellplateMultiPointWidget.set_well_coordinates) + self.objectivesWidget.signal_objective_changed.connect(self.wellplateMultiPointWidget.update_coordinates) + self.wellplateMultiPointWidget.signal_update_navigation_viewer.connect(self.navigationViewer.update_current_location) if SUPPORT_LASER_AUTOFOCUS: self.liveControlWidget_focus_camera.signal_newExposureTime.connect(self.cameraSettingWidget_focus_camera.set_exposure_time) @@ -770,14 +770,14 @@ def makeNapariConnections(self): if ENABLE_FLEXIBLE_MULTIPOINT: self.napari_connections['napariMultiChannelWidget'].extend([ - (self.multiPointWidget2.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), - (self.multiPointWidget2.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) + (self.flexibleMultiPointWidget.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), + (self.flexibleMultiPointWidget.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) ]) - if ENABLE_SCAN_GRID: + if ENABLE_WELLPLATE_MULTIPOINT: self.napari_connections['napariMultiChannelWidget'].extend([ - (self.multiPointWidgetGrid.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), - (self.multiPointWidgetGrid.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) + (self.wellplateMultiPointWidget.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), + (self.wellplateMultiPointWidget.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) ]) else: self.multipointController.image_to_display_multi.connect(self.imageArrayDisplayWindow.display_image) @@ -793,14 +793,14 @@ def makeNapariConnections(self): if ENABLE_FLEXIBLE_MULTIPOINT: self.napari_connections['napariTiledDisplayWidget'].extend([ - (self.multiPointWidget2.signal_acquisition_channels, self.napariTiledDisplayWidget.initChannels), - (self.multiPointWidget2.signal_acquisition_shape, self.napariTiledDisplayWidget.initLayersShape) + (self.flexibleMultiPointWidget.signal_acquisition_channels, self.napariTiledDisplayWidget.initChannels), + (self.flexibleMultiPointWidget.signal_acquisition_shape, self.napariTiledDisplayWidget.initLayersShape) ]) - if ENABLE_SCAN_GRID: + if ENABLE_WELLPLATE_MULTIPOINT: self.napari_connections['napariTiledDisplayWidget'].extend([ - (self.multiPointWidgetGrid.signal_acquisition_channels, self.napariTiledDisplayWidget.initChannels), - (self.multiPointWidgetGrid.signal_acquisition_shape, self.napariTiledDisplayWidget.initLayersShape) + (self.wellplateMultiPointWidget.signal_acquisition_channels, self.napariTiledDisplayWidget.initChannels), + (self.wellplateMultiPointWidget.signal_acquisition_shape, self.napariTiledDisplayWidget.initLayersShape) ]) # Setup mosaic display widget connections @@ -814,16 +814,16 @@ def makeNapariConnections(self): if ENABLE_FLEXIBLE_MULTIPOINT: self.napari_connections['napariMosaicDisplayWidget'].extend([ - (self.multiPointWidget2.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), - (self.multiPointWidget2.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape) + (self.flexibleMultiPointWidget.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), + (self.flexibleMultiPointWidget.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape) ]) - if ENABLE_SCAN_GRID: + if ENABLE_WELLPLATE_MULTIPOINT: self.napari_connections['napariMosaicDisplayWidget'].extend([ - (self.multiPointWidgetGrid.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), - (self.multiPointWidgetGrid.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape), - (self.multiPointWidgetGrid.signal_draw_shape, self.napariMosaicDisplayWidget.enable_shape_drawing), - (self.napariMosaicDisplayWidget.signal_shape_drawn, self.multiPointWidgetGrid.update_manual_shape) + (self.wellplateMultiPointWidget.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), + (self.wellplateMultiPointWidget.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape), + (self.wellplateMultiPointWidget.signal_draw_shape, self.napariMosaicDisplayWidget.enable_shape_drawing), + (self.napariMosaicDisplayWidget.signal_shape_drawn, self.wellplateMultiPointWidget.update_manual_shape) ]) # Make initial connections @@ -877,17 +877,18 @@ def openLedMatrixSettings(self): def onTabChanged(self, index): acquisitionWidget = self.recordTabWidget.widget(index) - is_flexible = (index == self.recordTabWidget.indexOf(self.multiPointWidget2)) - is_scan_grid = (index == self.recordTabWidget.indexOf(self.multiPointWidgetGrid)) if ENABLE_SCAN_GRID else False + is_flexible = (index == self.recordTabWidget.indexOf(self.flexibleMultiPointWidget)) + is_scan_grid = (index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) if ENABLE_WELLPLATE_MULTIPOINT else False self.toggleWellSelector(is_scan_grid and self.wellSelectionWidget.format != 'glass slide') + if is_scan_grid: self.navigationViewer.clear_overlay() self.wellSelectionWidget.onSelectionChanged() else: - self.multiPointWidgetGrid.clear_regions() + self.wellplateMultiPointWidget.clear_regions() if is_flexible: - self.multiPointWidget2.update_fov_positions() + self.flexibleMultiPointWidget.update_fov_positions() if ENABLE_STITCHER: self.toggleStitcherWidget(acquisitionWidget.checkbox_stitchOutput.isChecked()) @@ -930,10 +931,10 @@ def onWellplateChanged(self, format_): self.connectWellSelectionWidget() if ENABLE_FLEXIBLE_MULTIPOINT: - self.multiPointWidget2.clear_only_location_list() - if ENABLE_SCAN_GRID: - self.multiPointWidgetGrid.clear_regions() - self.multiPointWidgetGrid.set_default_scan_size() + self.flexibleMultiPointWidget.clear_only_location_list() + if ENABLE_WELLPLATE_MULTIPOINT: + self.wellplateMultiPointWidget.clear_regions() + self.wellplateMultiPointWidget.set_default_scan_size() self.wellSelectionWidget.onSelectionChanged() def setupSlidePositionController(self, is_for_wellplate): @@ -966,8 +967,8 @@ def connectWellSelectionWidget(self): #self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidget.set_well_selected) self.wellSelectionWidget.signal_wellSelectedPos.connect(self.navigationController.move_to) self.wellplateFormatWidget.signalWellplateSettings.connect(self.wellSelectionWidget.updateWellplateSettings) - if ENABLE_SCAN_GRID: - self.wellSelectionWidget.signal_wellSelected.connect(self.multiPointWidgetGrid.set_well_coordinates) + if ENABLE_WELLPLATE_MULTIPOINT: + self.wellSelectionWidget.signal_wellSelected.connect(self.wellplateMultiPointWidget.set_well_coordinates) def toggleWellSelector(self, show): if USE_NAPARI_WELL_SELECTION and not self.performance_mode and not self.live_only_mode: @@ -984,7 +985,7 @@ def toggleAcquisitionStart(self, acquisition_started): if acquisition_started: self.liveControlWidget.toggle_autolevel(not acquisition_started) - is_scan_grid = (current_index == self.recordTabWidget.indexOf(self.multiPointWidgetGrid)) if ENABLE_SCAN_GRID else False + is_scan_grid = (current_index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) if ENABLE_WELLPLATE_MULTIPOINT else False if is_scan_grid and self.wellSelectionWidget.format != 'glass slide': self.toggleWellSelector(not acquisition_started) @@ -1007,11 +1008,11 @@ def startStitcher(self, acquisition_path): use_registration = self.stitcherWidget.useRegistrationCheck.isChecked() registration_channel = self.stitcherWidget.registrationChannelCombo.currentText() registration_z_level = self.stitcherWidget.registrationZCombo.value() - overlap_percent = self.multiPointWidgetGrid.entry_overlap.value() + overlap_percent = self.wellplateMultiPointWidget.entry_overlap.value() output_name = acquisitionWidget.lineEdit_experimentID.text() or "stitched" output_format = ".ome.zarr" if self.stitcherWidget.outputFormatCombo.currentText() == "OME-ZARR" else ".ome.tiff" - stitcher_class = stitcher.CoordinateStitcher if self.recordTabWidget.currentIndex() == self.recordTabWidget.indexOf(self.multiPointWidgetGrid) else stitcher.Stitcher + stitcher_class = stitcher.CoordinateStitcher if self.recordTabWidget.currentIndex() == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget) else stitcher.Stitcher self.stitcherThread = stitcher_class( input_folder=acquisition_path, output_name=output_name, diff --git a/software/control/widgets.py b/software/control/widgets.py index f13b9352..71ec4cb6 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1997,7 +1997,7 @@ def display_stats(self, stats): row+=1 -class MultiPointWidget2(QFrame): +class FlexibleMultiPointWidget(QFrame): signal_acquisition_started = Signal(bool) signal_acquisition_channels = Signal(list) @@ -2959,7 +2959,7 @@ def import_location_list(self): print(self.location_list) -class MultiPointWidgetGrid(QFrame): +class WellplateMultiPointWidget(QFrame): signal_acquisition_started = Signal(bool) signal_acquisition_channels = Signal(list) From f5cebbdcb2257d2802178101d8f996fbd18b41f8 Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Tue, 3 Dec 2024 21:28:31 -0800 Subject: [PATCH 5/9] USE_OVERLAP_FOR_FLEXIBLE: dx, dy vs overlap --- software/control/_def.py | 1 + software/control/widgets.py | 188 +++++++++++++++++++++++++++--------- 2 files changed, 143 insertions(+), 46 deletions(-) diff --git a/software/control/_def.py b/software/control/_def.py index f557efcc..fe3c5ca0 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -429,6 +429,7 @@ class SOFTWARE_POS_LIMIT: DEFAULT_MULTIPOINT_NY=1 ENABLE_FLEXIBLE_MULTIPOINT = True +USE_OVERLAP_FOR_FLEXIBLE = False ENABLE_WELLPLATE_MULTIPOINT = True ENABLE_RECORDING = False diff --git a/software/control/widgets.py b/software/control/widgets.py index 71ec4cb6..2f07ec39 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -2020,6 +2020,7 @@ def __init__(self, navigationController, navigationViewer, multipointController, self.location_ids = np.empty((0,), dtype=' Date: Thu, 5 Dec 2024 15:46:08 -0800 Subject: [PATCH 6/9] readd legacy multipointwidget --- software/control/widgets.py | 322 ++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/software/control/widgets.py b/software/control/widgets.py index 2f07ec39..2e1128d5 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1997,6 +1997,328 @@ def display_stats(self, stats): row+=1 +class MultiPointWidget(QFrame): + + signal_acquisition_started = Signal(bool) + signal_acquisition_channels = Signal(list) + signal_acquisition_z_levels = Signal(int) + signal_acquisition_shape = Signal(int, int, int, float, float, float) + signal_stitcher_widget = Signal(bool) + + def __init__(self, multipointController, configurationManager = None, main=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.multipointController = multipointController + self.configurationManager = configurationManager + self.well_selected = False + self.base_path_is_set = False + self.add_components() + self.setFrameStyle(QFrame.Panel | QFrame.Raised) + + def add_components(self): + + self.btn_setSavingDir = QPushButton('Browse') + self.btn_setSavingDir.setDefault(False) + self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + + self.lineEdit_savingDir = QLineEdit() + self.lineEdit_savingDir.setReadOnly(True) + self.lineEdit_savingDir.setText('Choose a base saving directory') + + self.lineEdit_savingDir.setText(DEFAULT_SAVING_PATH) + self.multipointController.set_base_path(DEFAULT_SAVING_PATH) + self.base_path_is_set = True + + self.lineEdit_experimentID = QLineEdit() + + self.entry_deltaX = QDoubleSpinBox() + self.entry_deltaX.setMinimum(0) + self.entry_deltaX.setMaximum(5) + self.entry_deltaX.setSingleStep(0.1) + self.entry_deltaX.setValue(Acquisition.DX) + self.entry_deltaX.setDecimals(3) + self.entry_deltaX.setSuffix(' mm') + self.entry_deltaX.setKeyboardTracking(False) + + self.entry_NX = QSpinBox() + self.entry_NX.setMinimum(1) + self.entry_NX.setMaximum(50) + self.entry_NX.setSingleStep(1) + self.entry_NX.setValue(Acquisition.NX) + self.entry_NX.setKeyboardTracking(False) + + self.entry_deltaY = QDoubleSpinBox() + self.entry_deltaY.setMinimum(0) + self.entry_deltaY.setMaximum(5) + self.entry_deltaY.setSingleStep(0.1) + self.entry_deltaY.setValue(Acquisition.DX) + self.entry_deltaY.setDecimals(3) + self.entry_deltaY.setSuffix(' mm') + self.entry_deltaY.setKeyboardTracking(False) + + self.entry_NY = QSpinBox() + self.entry_NY.setMinimum(1) + self.entry_NY.setMaximum(50) + self.entry_NY.setSingleStep(1) + self.entry_NY.setValue(Acquisition.NY) + self.entry_NY.setKeyboardTracking(False) + + self.entry_deltaZ = QDoubleSpinBox() + self.entry_deltaZ.setMinimum(0) + self.entry_deltaZ.setMaximum(1000) + self.entry_deltaZ.setSingleStep(0.2) + self.entry_deltaZ.setValue(Acquisition.DZ) + self.entry_deltaZ.setDecimals(3) + self.entry_deltaZ.setSuffix(' μm') + self.entry_deltaZ.setKeyboardTracking(False) + + self.entry_NZ = QSpinBox() + self.entry_NZ.setMinimum(1) + self.entry_NZ.setMaximum(2000) + self.entry_NZ.setSingleStep(1) + self.entry_NZ.setValue(1) + self.entry_NZ.setKeyboardTracking(False) + + self.entry_dt = QDoubleSpinBox() + self.entry_dt.setMinimum(0) + self.entry_dt.setMaximum(12*3600) + self.entry_dt.setSingleStep(1) + self.entry_dt.setValue(0) + self.entry_dt.setSuffix(' s') + self.entry_dt.setKeyboardTracking(False) + + self.entry_Nt = QSpinBox() + self.entry_Nt.setMinimum(1) + self.entry_Nt.setMaximum(5000) # @@@ to be changed + self.entry_Nt.setSingleStep(1) + self.entry_Nt.setValue(1) + self.entry_Nt.setKeyboardTracking(False) + + self.list_configurations = QListWidget() + for microscope_configuration in self.configurationManager.configurations: + self.list_configurations.addItems([microscope_configuration.name]) + self.list_configurations.setSelectionMode(QAbstractItemView.MultiSelection) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum + + self.checkbox_withAutofocus = QCheckBox('Contrast AF') + self.checkbox_withAutofocus.setChecked(MULTIPOINT_CONTRAST_AUTOFOCUS_ENABLE_BY_DEFAULT) + self.multipointController.set_af_flag(MULTIPOINT_CONTRAST_AUTOFOCUS_ENABLE_BY_DEFAULT) + + self.checkbox_genFocusMap = QCheckBox('Focus Map') + self.checkbox_genFocusMap.setChecked(False) + + self.checkbox_withReflectionAutofocus = QCheckBox('Reflection AF') + self.checkbox_withReflectionAutofocus.setChecked(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) + + self.checkbox_stitchOutput = QCheckBox('Stitch Scans') + self.checkbox_stitchOutput.setChecked(False) + + self.multipointController.set_reflection_af_flag(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) + + self.btn_startAcquisition = QPushButton('Start\n Acquisition ') + self.btn_startAcquisition.setStyleSheet("background-color: #C2C2FF") + self.btn_startAcquisition.setCheckable(True) + self.btn_startAcquisition.setChecked(False) + + # layout + grid_line0 = QGridLayout() + grid_line0.addWidget(QLabel('Saving Path')) + grid_line0.addWidget(self.lineEdit_savingDir, 0,1) + grid_line0.addWidget(self.btn_setSavingDir, 0,2) + + grid_line1 = QGridLayout() + grid_line1.addWidget(QLabel('Experiment ID'), 0,0) + grid_line1.addWidget(self.lineEdit_experimentID,0,1) + + grid_line2 = QGridLayout() + grid_line2.addWidget(QLabel('dx'), 0,0) + grid_line2.addWidget(self.entry_deltaX, 0,1) + grid_line2.addWidget(QLabel('Nx'), 0,3) + grid_line2.addWidget(self.entry_NX, 0,4) + grid_line2.addWidget(QLabel('dy'), 0,6) + grid_line2.addWidget(self.entry_deltaY, 0,7) + grid_line2.addWidget(QLabel('Ny'), 0,9) + grid_line2.addWidget(self.entry_NY, 0,10) + + grid_line2.addWidget(QLabel('dz'), 1,0) + grid_line2.addWidget(self.entry_deltaZ, 1,1) + grid_line2.addWidget(QLabel('Nz'), 1,3) + grid_line2.addWidget(self.entry_NZ, 1,4) + grid_line2.addWidget(QLabel('dt'), 1,6) + grid_line2.addWidget(self.entry_dt, 1,7) + grid_line2.addWidget(QLabel('Nt'), 1,9) + grid_line2.addWidget(self.entry_Nt, 1,10) + + grid_line2.setColumnStretch(2, 1) + grid_line2.setColumnStretch(5, 1) + grid_line2.setColumnStretch(8, 1) + + grid_af = QGridLayout() + grid_af.addItem(QSpacerItem(7, 1, QSizePolicy.Fixed, QSizePolicy.Minimum), 0, 0) + grid_af.addWidget(self.checkbox_withAutofocus,0,1) + if SUPPORT_LASER_AUTOFOCUS: + grid_af.addWidget(self.checkbox_withReflectionAutofocus,1,1) + grid_af.addWidget(self.checkbox_genFocusMap,2,1) + if ENABLE_STITCHER: + grid_af.addWidget(self.checkbox_stitchOutput,3,1) + grid_af.addItem(QSpacerItem(6, 1, QSizePolicy.Fixed, QSizePolicy.Minimum), 0, 2) + + grid_line3 = QHBoxLayout() + grid_line3.addWidget(self.list_configurations, 2) + # grid_line3.addWidget(self.checkbox_withAutofocus) + grid_line3.addLayout(grid_af, 1) + grid_line3.addWidget(self.btn_startAcquisition, 1) + + self.grid = QGridLayout() + self.grid.addLayout(grid_line0,0,0) + self.grid.addLayout(grid_line1,1,0) + self.grid.addLayout(grid_line2,2,0) + self.grid.addLayout(grid_line3,3,0) + self.setLayout(self.grid) + + # add and display a timer - to be implemented + # self.timer = QTimer() + + # connections + self.entry_deltaX.valueChanged.connect(self.set_deltaX) + self.entry_deltaY.valueChanged.connect(self.set_deltaY) + self.entry_deltaZ.valueChanged.connect(self.set_deltaZ) + self.entry_dt.valueChanged.connect(self.multipointController.set_deltat) + self.entry_NX.valueChanged.connect(self.multipointController.set_NX) + self.entry_NY.valueChanged.connect(self.multipointController.set_NY) + self.entry_NZ.valueChanged.connect(self.multipointController.set_NZ) + self.entry_NZ.valueChanged.connect(self.signal_acquisition_z_levels.emit) + self.entry_Nt.valueChanged.connect(self.multipointController.set_Nt) + self.checkbox_withAutofocus.stateChanged.connect(self.multipointController.set_af_flag) + self.checkbox_withReflectionAutofocus.stateChanged.connect(self.multipointController.set_reflection_af_flag) + self.checkbox_genFocusMap.stateChanged.connect(self.multipointController.set_gen_focus_map_flag) + self.checkbox_stitchOutput.toggled.connect(self.display_stitcher_widget) + self.btn_setSavingDir.clicked.connect(self.set_saving_dir) + self.btn_startAcquisition.clicked.connect(self.toggle_acquisition) + self.multipointController.acquisitionFinished.connect(self.acquisition_is_finished) + self.list_configurations.itemSelectionChanged.connect(self.emit_selected_channels) + + def set_deltaX(self,value): + mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_X() + deltaX = round(value/mm_per_ustep)*mm_per_ustep + self.entry_deltaX.setValue(deltaX) + self.multipointController.set_deltaX(deltaX) + + def set_deltaY(self,value): + mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_Y() + deltaY = round(value/mm_per_ustep)*mm_per_ustep + self.entry_deltaY.setValue(deltaY) + self.multipointController.set_deltaY(deltaY) + + def set_deltaZ(self,value): + mm_per_ustep = self.multipointController.navigationController.get_mm_per_ustep_Z() + deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 + self.entry_deltaZ.setValue(deltaZ) + self.multipointController.set_deltaZ(deltaZ) + + def set_saving_dir(self): + dialog = QFileDialog() + save_dir_base = dialog.getExistingDirectory(None, "Select Folder") + self.multipointController.set_base_path(save_dir_base) + self.lineEdit_savingDir.setText(save_dir_base) + self.base_path_is_set = True + + def set_well_selected(self, selected): + self.well_selected = selected + + def emit_selected_channels(self): + selected_channels = [item.text() for item in self.list_configurations.selectedItems()] + print(selected_channels) + self.signal_acquisition_channels.emit(selected_channels) + + def toggle_acquisition(self,pressed): + if self.base_path_is_set == False: + self.btn_startAcquisition.setChecked(False) + msg = QMessageBox() + msg.setText("Please choose base saving directory first") + msg.exec_() + return + if self.well_selected == False and self.multipointController.scanCoordinates.format != 0: + self.btn_startAcquisition.setChecked(False) + msg = QMessageBox() + msg.setText("Please select a well to scan first") + msg.exec_() + return + if not self.list_configurations.selectedItems(): # no channel selected + self.btn_startAcquisition.setChecked(False) + msg = QMessageBox() + msg.setText("Please select at least one imaging channel first") + msg.exec_() + return + if pressed: + # @@@ to do: add a widgetManger to enable and disable widget + # @@@ to do: emit signal to widgetManager to disable other widgets + self.setEnabled_all(False) + + # set parameters + if self.multipointController.scanCoordinates is not None: + self.multipointController.scanCoordinates.grid_skip_positions = [] + self.multipointController.set_deltaX(self.entry_deltaX.value()) + self.multipointController.set_deltaY(self.entry_deltaY.value()) + self.multipointController.set_deltaZ(self.entry_deltaZ.value()) + self.multipointController.set_deltat(self.entry_dt.value()) + self.multipointController.set_NX(self.entry_NX.value()) + self.multipointController.set_NY(self.entry_NY.value()) + self.multipointController.set_NZ(self.entry_NZ.value()) + self.multipointController.set_Nt(self.entry_Nt.value()) + self.multipointController.set_af_flag(self.checkbox_withAutofocus.isChecked()) + self.multipointController.set_reflection_af_flag(self.checkbox_withReflectionAutofocus.isChecked()) + self.multipointController.set_base_path(self.lineEdit_savingDir.text()) + self.multipointController.set_selected_configurations((item.text() for item in self.list_configurations.selectedItems())) + self.multipointController.start_new_experiment(self.lineEdit_experimentID.text()) + + # emit acquisition data + self.signal_acquisition_started.emit(True) + self.signal_acquisition_shape.emit(self.entry_NX.value(), + self.entry_NY.value(), + self.entry_NZ.value(), + self.entry_deltaX.value(), + self.entry_deltaY.value(), + self.entry_deltaZ.value()) + + self.multipointController.run_acquisition() + else: + self.multipointController.request_abort_aquisition() + self.setEnabled_all(True) + + def acquisition_is_finished(self): + self.signal_acquisition_started.emit(False) + self.btn_startAcquisition.setChecked(False) + self.setEnabled_all(True) + + def setEnabled_all(self,enabled,exclude_btn_startAcquisition=True): + self.btn_setSavingDir.setEnabled(enabled) + self.lineEdit_savingDir.setEnabled(enabled) + self.lineEdit_experimentID.setEnabled(enabled) + self.entry_deltaX.setEnabled(enabled) + self.entry_NX.setEnabled(enabled) + self.entry_deltaY.setEnabled(enabled) + self.entry_NY.setEnabled(enabled) + self.entry_deltaZ.setEnabled(enabled) + self.entry_NZ.setEnabled(enabled) + self.entry_dt.setEnabled(enabled) + self.entry_Nt.setEnabled(enabled) + self.list_configurations.setEnabled(enabled) + self.checkbox_withAutofocus.setEnabled(enabled) + self.checkbox_withReflectionAutofocus.setEnabled(enabled) + self.checkbox_genFocusMap.setEnabled(enabled) + self.checkbox_stitchOutput.setEnabled(enabled) + if exclude_btn_startAcquisition is not True: + self.btn_startAcquisition.setEnabled(enabled) + + def display_stitcher_widget(self, checked): + self.signal_stitcher_widget.emit(checked) + + def disable_the_start_aquisition_button(self): + self.btn_startAcquisition.setEnabled(False) + + def enable_the_start_aquisition_button(self): + self.btn_startAcquisition.setEnabled(True) + + class FlexibleMultiPointWidget(QFrame): signal_acquisition_started = Signal(bool) From e7011e3b9e5ff2e017c530dfe5ef36dd9c45c9c9 Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Thu, 5 Dec 2024 15:55:21 -0800 Subject: [PATCH 7/9] re-add legacy multipointwidget and tileddisplaywidget --- software/control/widgets.py | 181 ++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/software/control/widgets.py b/software/control/widgets.py index 2e1128d5..5f0fe83f 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -5286,6 +5286,187 @@ def activate(self): self.viewer.window.activate() +class NapariTiledDisplayWidget(QWidget): + + signal_coordinates_clicked = Signal(int, int, int, int, int, int, float, float) + + def __init__(self, objectiveStore, contrastManager, parent=None): + super().__init__(parent) + # Initialize placeholders for the acquisition parameters + self.objectiveStore = objectiveStore + self.contrastManager = contrastManager + self.downsample_factor = PRVIEW_DOWNSAMPLE_FACTOR + self.image_width = 0 + self.image_height = 0 + self.dtype = np.uint8 + self.channels = set() + self.Nx = 1 + self.Ny = 1 + self.Nz = 1 + self.dz_um = 1 + self.pixel_size_um = 1 + self.layers_initialized = False + self.acquisition_initialized = False + self.viewer_scale_initialized = False + self.initNapariViewer() + + def initNapariViewer(self): + self.viewer = napari.Viewer(show=False) #, ndisplay=3) + self.viewerWidget = self.viewer.window._qt_window + self.viewer.dims.axis_labels = ['Z-axis', 'Y-axis', 'X-axis'] + self.layout = QVBoxLayout() + self.layout.addWidget(self.viewerWidget) + self.setLayout(self.layout) + self.customizeViewer() + + def customizeViewer(self): + # Hide the status bar (which includes the activity button) + if hasattr(self.viewer.window, '_status_bar'): + self.viewer.window._status_bar.hide() + + # Hide the layer buttons + if hasattr(self.viewer.window._qt_viewer, 'layerButtons'): + self.viewer.window._qt_viewer.layerButtons.hide() + + def initLayersShape(self, Nx, Ny, Nz, dx, dy, dz): + self.acquisition_initialized = False + self.Nx = Nx + self.Ny = Ny + self.Nz = Nz + self.dx_mm = dx + self.dy_mm = dy + self.dz_um = dz if Nz > 1 and dz != 0 else 1.0 + pixel_size_um = self.objectiveStore.get_pixel_size() + self.pixel_size_um = pixel_size_um * self.downsample_factor + + def initChannels(self, channels): + self.channels = set(channels) + + def extractWavelength(self, name): + # Split the string and find the wavelength number immediately after "Fluorescence" + parts = name.split() + if 'Fluorescence' in parts: + index = parts.index('Fluorescence') + 1 + if index < len(parts): + return parts[index].split()[0] # Assuming '488 nm Ex' and taking '488' + for color in ['R', 'G', 'B']: + if color in parts or f"full_{color}" in parts: + return color + return None + + def generateColormap(self, channel_info): + """Convert a HEX value to a normalized RGB tuple.""" + c0 = (0, 0, 0) + c1 = (((channel_info['hex'] >> 16) & 0xFF) / 255, # Normalize the Red component + ((channel_info['hex'] >> 8) & 0xFF) / 255, # Normalize the Green component + (channel_info['hex'] & 0xFF) / 255) # Normalize the Blue component + return Colormap(colors=[c0, c1], controls=[0, 1], name=channel_info['name']) + + def initLayers(self, image_height, image_width, image_dtype): + """Initializes the full canvas for each channel based on the acquisition parameters.""" + if self.acquisition_initialized: + for layer in list(self.viewer.layers): + if layer.name not in self.channels: + self.viewer.layers.remove(layer) + else: + self.viewer.layers.clear() + self.acquisition_initialized = True + + self.image_width = image_width // self.downsample_factor + self.image_height = image_height // self.downsample_factor + self.dtype = np.dtype(image_dtype) + self.layers_initialized = True + self.resetView() + self.viewer_scale_initialized = False + + def updateLayers(self, image, i, j, k, channel_name): + """Updates the appropriate slice of the canvas with the new image data.""" + if i == -1 or j == -1: + print("no tiled preview for coordinate acquisition") + return + + # Check if the layer exists and has a different dtype + if self.dtype != image.dtype: + # Remove the existing layer + self.layers_initialized = False + self.acquisition_initialized = False + + if not self.layers_initialized: + self.initLayers(image.shape[0], image.shape[1], image.dtype) + + rgb = len(image.shape) == 3 # Check if image is RGB based on shape + if channel_name not in self.viewer.layers: + self.channels.add(channel_name) + if rgb: + color = None # No colormap for RGB images + canvas = np.zeros((self.Nz, self.Ny * self.image_height, self.Nx * self.image_width, 3), dtype=self.dtype) + else: + channel_info = CHANNEL_COLORS_MAP.get(self.extractWavelength(channel_name), {'hex': 0xFFFFFF, 'name': 'gray'}) + if channel_info['name'] in AVAILABLE_COLORMAPS: + color = AVAILABLE_COLORMAPS[channel_info['name']] + else: + color = self.generateColormap(channel_info) + canvas = np.zeros((self.Nz, self.Ny * self.image_height, self.Nx * self.image_width), dtype=self.dtype) + + limits = self.getContrastLimits(self.dtype) + layer = self.viewer.add_image(canvas, name=channel_name, visible=True, rgb=rgb, + colormap=color, contrast_limits=limits, blending='additive', + scale=(self.dz_um, self.pixel_size_um, self.pixel_size_um)) + # print(f"tiled display - dz_um:{self.dz_um}, pixel_y_um:{self.pixel_size_um}, pixel_x_um:{self.pixel_size_um}") + layer.contrast_limits = self.contrastManager.get_limits(channel_name) + layer.events.contrast_limits.connect(self.signalContrastLimits) + layer.mouse_double_click_callbacks.append(self.onDoubleClick) + + image = cv2.resize(image, (self.image_width, self.image_height), interpolation=cv2.INTER_AREA) + + if not self.viewer_scale_initialized: + self.resetView() + self.viewer_scale_initialized = True + self.viewer.dims.set_point(0, k * self.dz_um) + layer = self.viewer.layers[channel_name] + layer.contrast_limits = self.contrastManager.get_limits(channel_name) + layer_data = layer.data + y_slice = slice(i * self.image_height, (i + 1) * self.image_height) + x_slice = slice(j * self.image_width, (j + 1) * self.image_width) + if rgb: + layer_data[k, y_slice, x_slice, :] = image + else: + layer_data[k, y_slice, x_slice] = image + layer.data = layer_data + layer.refresh() + + def signalContrastLimits(self, event): + layer = event.source + min_val, max_val = map(float, layer.contrast_limits) + self.contrastManager.update_limits(layer.name, min_val, max_val) + + def getContrastLimits(self, dtype): + return self.contrastManager.get_default_limits() + + def onDoubleClick(self, layer, event): + """Handle double-click events and emit centered coordinates if within the data range.""" + coords = layer.world_to_data(event.position) + layer_shape = layer.data.shape[0:3] if len(layer.data.shape) >= 4 else layer.data.shape + + if coords is not None and (0 <= int(coords[-1]) < layer_shape[-1] and (0 <= int(coords[-2]) < layer_shape[-2])): + x_centered = int(coords[-1] - layer_shape[-1] / 2) + y_centered = int(coords[-2] - layer_shape[-2] / 2) + # Emit the centered coordinates and dimensions of the layer's data array + self.signal_coordinates_clicked.emit(x_centered, y_centered, + layer_shape[-1], layer_shape[-2], + self.Nx, self.Ny, + self.dx_mm, self.dy_mm) + + def resetView(self): + self.viewer.reset_view() + for layer in self.viewer.layers: + layer.refresh() + + def activate(self): + print("ACTIVATING NAPARI TILED DISPLAY WIDGET") + self.viewer.window.activate() + + class NapariMosaicDisplayWidget(QWidget): signal_coordinates_clicked = Signal(float, float) # x, y in mm From 07b7e8e1030832042e61a568b302d5663ccdd1ef Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Thu, 5 Dec 2024 16:25:46 -0800 Subject: [PATCH 8/9] flexible signals comments --- software/control/gui_hcs.py | 4 ++-- software/control/widgets.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 77d41a1d..02548a24 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -640,14 +640,14 @@ def makeConnections(self): if ENABLE_STITCHER: self.flexibleMultiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) self.flexibleMultiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.flexibleMultiPointWidget.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.flexibleMultiPointWidget.signal_stitcher_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) if ENABLE_WELLPLATE_MULTIPOINT: self.wellplateMultiPointWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart) if ENABLE_STITCHER: self.wellplateMultiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) self.wellplateMultiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.wellplateMultiPointWidget.signal_acquisition_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.wellplateMultiPointWidget.signal_stitcher_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) self.liveControlWidget.signal_newExposureTime.connect(self.cameraSettingWidget.set_exposure_time) self.liveControlWidget.signal_newAnalogGain.connect(self.cameraSettingWidget.set_analog_gain) diff --git a/software/control/widgets.py b/software/control/widgets.py index 5f0fe83f..0719d664 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -2321,11 +2321,11 @@ def enable_the_start_aquisition_button(self): class FlexibleMultiPointWidget(QFrame): - signal_acquisition_started = Signal(bool) - signal_acquisition_channels = Signal(list) - signal_acquisition_z_levels = Signal(int) - signal_acquisition_shape = Signal(int, float) - signal_stitcher_widget = Signal(bool) + signal_acquisition_started = Signal(bool) # true = started, false = finished + signal_acquisition_channels = Signal(list) # list channels + signal_acquisition_shape = Signal(int, float) # Nz, dz + signal_stitcher_z_levels = Signal(int) # live Nz + signal_stitcher_widget = Signal(bool) # signal start stitcher def __init__(self, navigationController, navigationViewer, multipointController, objectiveStore, configurationManager = None, main=None, scanCoordinates=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -2721,7 +2721,7 @@ def add_components(self): self.entry_NX.valueChanged.connect(self.multipointController.set_NX) self.entry_NY.valueChanged.connect(self.multipointController.set_NY) self.entry_NZ.valueChanged.connect(self.multipointController.set_NZ) - self.entry_NZ.valueChanged.connect(self.signal_acquisition_z_levels.emit) + self.entry_NZ.valueChanged.connect(self.signal_stitcher_z_levels.emit) self.entry_Nt.valueChanged.connect(self.multipointController.set_Nt) self.checkbox_genFocusMap.toggled.connect(self.multipointController.set_gen_focus_map_flag) self.checkbox_withAutofocus.toggled.connect(self.multipointController.set_af_flag) @@ -3381,7 +3381,7 @@ class WellplateMultiPointWidget(QFrame): signal_acquisition_started = Signal(bool) signal_acquisition_channels = Signal(list) - signal_acquisition_z_levels = Signal(int) + signal_stitcher_z_levels = Signal(int) signal_acquisition_shape = Signal(int, float) signal_update_navigation_viewer = Signal() signal_stitcher_widget = Signal(bool) @@ -3693,7 +3693,7 @@ def add_components(self): self.combobox_z_stack.currentIndexChanged.connect(self.signal_z_stacking.emit) if not self.performance_mode: self.napariMosaicWidget.signal_layers_initialized.connect(self.enable_manual_ROI) - self.entry_NZ.valueChanged.connect(self.signal_acquisition_z_levels.emit) + self.entry_NZ.valueChanged.connect(self.signal_stitcher_z_levels.emit) def enable_manual_ROI(self, enable): self.combobox_shape.model().item(2).setEnabled(enable) From c2f36d448ae648a83e3fe16467fff1fe5a482f6c Mon Sep 17 00:00:00 2001 From: Soham Mukherjee Date: Fri, 6 Dec 2024 00:23:32 -0800 Subject: [PATCH 9/9] comments for location_list and coordinate_dict, will edit this logic in new branch --- software/control/core.py | 2 ++ software/control/widgets.py | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/software/control/core.py b/software/control/core.py index f14d1a97..7a854248 100644 --- a/software/control/core.py +++ b/software/control/core.py @@ -2638,6 +2638,8 @@ def set_selected_configurations(self, selected_configurations_name): self.selected_configurations.append(next((config for config in self.configurationManager.configurations if config.name == configuration_name))) def run_acquisition(self, location_list=None, coordinate_dict=None): + # location_list dict -> {key: region_id, value: center coordinate (x,y,z)} + # coordinate_dict dict -> {key: region_id, value: fov coordinates list [(x0,y0,z0), (x1,y1,z1), ... ]} print('start multipoint') if coordinate_dict is not None: diff --git a/software/control/widgets.py b/software/control/widgets.py index 0719d664..44d26f37 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -2340,12 +2340,12 @@ def __init__(self, navigationController, navigationViewer, multipointController, self.base_path_is_set = False self.location_list = np.empty((0, 3), dtype=float) self.location_ids = np.empty((0,), dtype='