# mesoSPIM MainWindow
import tifffile
import logging
import time
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt, QTimer
from PyQt5.uic import loadUi
from ..config.demo_config import plugins
''' Disabled taskbar button progress display due to problems with Anaconda default'''
# if sys.platform == 'win32':
# from PyQt5.QtWinExtras import QWinTaskbarButton
from .mesoSPIM_CameraWindow import mesoSPIM_CameraWindow
from .mesoSPIM_AcquisitionManagerWindow import mesoSPIM_AcquisitionManagerWindow
from .mesoSPIM_Optimizer import mesoSPIM_Optimizer
from .WebcamWindow import WebcamWindow
from .mesoSPIM_ContrastWindow import mesoSPIM_ContrastWindow
from .mesoSPIM_ScriptWindow import mesoSPIM_ScriptWindow # do not delete this line, it is actually used in exec()
from .mesoSPIM_TileViewWindow import mesoSPIM_TileViewWindow
from .mesoSPIM_State import mesoSPIM_StateSingleton
from .mesoSPIM_Core import mesoSPIM_Core
from .devices.joysticks.mesoSPIM_JoystickHandlers import mesoSPIM_JoystickHandler
from .utils.utility_functions import log_cpu_core
logger = logging.getLogger(__name__)
# discontinued to increase performance. Not actually useful for the user.
# class LogDisplayHandler(QtCore.QObject, logging.Handler):
# """ Handler class to display log in a TextDisplay widget. A thread-safe version, callable from non-GUI threads."""
# new_record = QtCore.pyqtSignal(object)
# def __init__(self, parent):
# super().__init__(parent)
# super(logging.Handler).__init__()
# formatter = Formatter('%(levelname)s:%(module)s:%(funcName)s:%(message)s')
# self.setFormatter(formatter)
# def emit(self, record):
# msg = self.format(record)
# self.new_record.emit(msg) # <---- emit signal here
# class Formatter(logging.Formatter):
# """ Formatter of the LogDisplayHandler class."""
# def formatException(self, ei):
# result = super(Formatter, self).formatException(ei)
# return result
# def format(self, record):
# s = super(Formatter, self).format(record)
# if record.exc_text:
# s = s.replace('\n', '')
# return s
[docs]
class mesoSPIM_MainWindow(QtWidgets.QMainWindow):
""" Main application window which instantiates worker objects and moves them to a thread. """
# sig_live = QtCore.pyqtSignal()
sig_stop = QtCore.pyqtSignal()
sig_finished = QtCore.pyqtSignal()
sig_enable_gui = QtCore.pyqtSignal(bool)
sig_state_request = QtCore.pyqtSignal(dict)
sig_execute_script = QtCore.pyqtSignal(str)
sig_move_relative = QtCore.pyqtSignal(dict)
sig_move_absolute = QtCore.pyqtSignal(dict)
sig_zero_axes = QtCore.pyqtSignal(list)
sig_unzero_axes = QtCore.pyqtSignal(list)
sig_stop_movement = QtCore.pyqtSignal()
sig_load_sample = QtCore.pyqtSignal()
sig_unload_sample = QtCore.pyqtSignal()
sig_center_sample = QtCore.pyqtSignal()
sig_save_etl_config = QtCore.pyqtSignal()
sig_poke_demo_thread = QtCore.pyqtSignal()
sig_launch_optimizer = QtCore.pyqtSignal(dict)
sig_launch_contrast_window = QtCore.pyqtSignal()
def __init__(self, package_directory, config, title="mesoSPIM Main Window"):
super().__init__()
# Initial housekeeping
self.cfg = config
self.package_directory = package_directory
# Instantiate the one and only mesoSPIM state '''
self.state = mesoSPIM_StateSingleton()
self.state.set_parameters(self.cfg.startup)
# Setting up the user interface windows
loadUi(self.package_directory + '/gui/mesoSPIM_MainWindow.ui', self)
self.setWindowTitle(title)
# Discontinued
# Connect log display widget
# self.log_display_handler = LogDisplayHandler(self)
# self.log_display_handler.new_record.connect(self.LogTextDisplay.appendPlainText)
self.camera_window = mesoSPIM_CameraWindow(self)
self.camera_window.show()
self.acquisition_manager_window = mesoSPIM_AcquisitionManagerWindow(self)
self.acquisition_manager_window.show()
self.tile_view_window = mesoSPIM_TileViewWindow(self)
self.tile_view_window.show()
self.webcam_window = None
self.check_config_file()
self.open_webcam_window()
self.scriptwindow = None
# arrange the windows on the screen, tiled
if hasattr(self.cfg, 'ui_options') and 'window_pos' in self.cfg.ui_options.keys():
window_pos = self.cfg.ui_options['window_pos']
else:
window_pos = (100, 100)
self.move(window_pos[0], window_pos[1])
self.camera_window.move(window_pos[0] + self.width() + 50, window_pos[1])
self.tile_view_window.move(window_pos[0] + self.width() + self.camera_window.width() + 2*50, window_pos[1])
self.acquisition_manager_window.move(window_pos[0], window_pos[1] + self.height() + 50)
if self.webcam_window:
self.webcam_window.move(window_pos[0] + self.width() + self.camera_window.width() + 2*50, window_pos[1] + self.height())
# set up some Acq manager signals
self.acquisition_manager_window.sig_warning.connect(self.display_warning)
self.acquisition_manager_window.sig_move_absolute.connect(self.sig_move_absolute.emit)
# Setting up the threads
logger.debug('Ideal thread count: '+str(int(QtCore.QThread.idealThreadCount())))
self.core_thread = QtCore.QThread()
# Entry point: Work on thread affinity here
self.core = mesoSPIM_Core(self.cfg, self)
self.core.moveToThread(self.core_thread)
self.core.waveformer.moveToThread(self.core_thread)
self.core.serial_worker.moveToThread(self.core_thread) # depending of signal source, some commands are still executed in main (GUI) thread, eg move_relative() from button press
# Get buttons & connections ready
self.initialize_and_connect_menubar()
self.initialize_and_connect_widgets()
# launch ETL menu
self.choose_etl_config()
# Widget list for blockSignals during status updates
self.widgets_to_block = []
self.parent_widgets_to_block = [self.ETLTabWidget, self.ParameterTabWidget, self.ControlGroupBox]
self.create_widget_list(self.parent_widgets_to_block, self.widgets_to_block)
# The signal switchboard, Core -> MainWindow
# Memo: with type=QtCore.Qt.DirectConnection the slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread (Core).
self.core.sig_finished.connect(self.finished)
self.core.sig_position.connect(self.update_position_indicators)
self.core.sig_update_gui_from_state.connect(self.update_gui_from_state)
self.core.sig_status_message.connect(self.display_status_message)
self.core.sig_progress.connect(self.update_progressbars)
self.core.sig_warning.connect(self.display_warning)
self.sig_move_absolute.connect(self.core.move_absolute, type=QtCore.Qt.QueuedConnection)
# Set stages, revolver, filter to initialization positions defined in the config file:
#self.sig_move_to_ini_position.connect(self.core.move_to_initial_positions, type=QtCore.Qt.QueuedConnection)
self.optimizer = None
self.contrast_window = None
# Signal from state to GUI
#self.state.sig_updated.connect(self.update_gui_from_state) # too frequent updates because of stage polling
# The signal switchboard, MainWindow -> Core
self.sig_launch_optimizer.connect(self.launch_optimizer)
self.sig_launch_contrast_window.connect(self.launch_contrast_window)
''' Start the thread '''
self.core_thread.start(QtCore.QThread.HighestPriority)
try:
self.thread().setPriority(QtCore.QThread.HighestPriority)
logger.debug('Main Window Thread priority: '+str(self.thread().priority()))
except:
logger.error(f'Main Window: Printing Thread priority failed.')
logger.debug(f'Main Window: Core thread priority: {self.core_thread.priority()}')
self.acquisition_manager_window.mark_all_current_parameters() # initialize the first dummy raw with the current state
self.joystick = mesoSPIM_JoystickHandler(self)
[docs]
def check_config_file(self):
"""Checks missing blocks in config file and gives suggestions.
Todo: all new config options
"""
gen_msg = "You are using outdated config file, check project github with the most recent template (demo_config.py):"
if not hasattr(self.cfg, 'ui_options'):
spec_msg = "\n - 'ui_options' is missing"
logger.info(gen_msg + spec_msg)
print(gen_msg + spec_msg)
else:
if not ('button_sleep_ms_xyzft' in self.cfg.ui_options.keys()):
spec_msg = "\n - 'button_sleep_ms_xyzft' is missing"
logger.info(gen_msg + spec_msg)
print(gen_msg + spec_msg)
if not hasattr(self.cfg, 'scale_galvo_amp_with_zoom'):
print("INFO: Config file: parameter 'scale_galvo_amp_with_zoom' (True, False) is missing. Default is False.")
self.state['galvo_amp_scale_w_zoom'] = False
else:
self.state['galvo_amp_scale_w_zoom'] = self.cfg.scale_galvo_amp_with_zoom
self.checkBoxScaleWZoom.setChecked(self.state['galvo_amp_scale_w_zoom'])
if 'f_objective_exchange' in self.cfg.stage_parameters.keys():
msg = f"Objective exchange in f-position {self.cfg.stage_parameters['f_objective_exchange']} ('f_objective_exchange' in stage parameters of the config file)."
if self.cfg.stage_parameters['f_min'] <= self.cfg.stage_parameters['f_objective_exchange'] <= self.cfg.stage_parameters['f_max']:
pass
else:
msg = "ERROR: 'f_objective_exchange' is not within the allowed range of 'f_min' and 'f_max'"
logger.error(msg), print(msg)
else:
msg = "Objective exchange in the current f-position. To set the safe f-position for objective exchange, add 'f_objective_exchange' to the stage parameters in the config file."
logger.warning(msg)
print(msg)
[docs]
def open_webcam_window(self):
"""Open USB webcam window using cam ID specified in config file."""
if self.webcam_window is None: # first call
if hasattr(self.cfg, 'ui_options') and ('usb_webcam_ID' in self.cfg.ui_options.keys()):
self.webcam_window = WebcamWindow(self.cfg.ui_options['usb_webcam_ID'])
else: # create a dummy
self.webcam_window = WebcamWindow(None)
else: # open previously closed window
self.webcam_window.show()
[docs]
def open_tile_view_window(self):
self.tile_view_window.show()
def __del__(self):
"""Cleans the threads up after deletion, waits until the threads
have truly finished their life.
Make sure to keep this up to date with the number of threads
"""
try:
self.core_thread.quit()
self.core_thread.wait()
except:
pass
[docs]
def close_app(self):
#self.log_display_handler.flushOnClose = False #discontinued
logger.info('Closing the application')
self.camera_window.close()
self.acquisition_manager_window.close()
if self.optimizer:
self.optimizer.close()
try:
self.webcam_window.close()
except:
pass
if self.contrast_window:
self.contrast_window.close()
self.tile_view_window.close()
if self.scriptwindow:
self.scriptwindow.close()
self.close()
[docs]
def open_tiff(self):
"""Open and display a TIFF file (stack), eg for demo and debugging purposes."""
tiff_path, _ = QtWidgets.QFileDialog.getOpenFileName(None, 'Open TIFF', "./", "TIFF files (*tif; *tiff)")
if tiff_path:
try:
stack = tifffile.imread(tiff_path)
self.camera_window.set_image(stack)
logger.info(f"Loaded TIFF file from {tiff_path}, dimensions {stack.shape}")
except Exception as e:
logger.exception(f"{e}")
else:
logger.info(f"Loaded TIFF file path is None")
[docs]
def get_state_parameter(self, state_parameter):
return self.state[state_parameter]
[docs]
def check_instances(self, widget):
'''
Method to check whether a widget belongs to one of the Qt Classes specified.
Args:
widget (QtWidgets.QWidget): Widget to check
Returns:
return_value (bool): True if widget is in the list, False if not.
'''
if isinstance(widget, (QtWidgets.QSpinBox,
QtWidgets.QDoubleSpinBox,
QtWidgets.QSlider,
QtWidgets.QComboBox,
QtWidgets.QPushButton)):
return True
else:
return False
[docs]
def create_widget_list(self, list, widget_list):
"""
Helper method to recursively loop through all the widgets in a list and their children.
Args:
list (list): List of QtWidgets.QWidget objects
"""
for widget in list:
if list != ([] or None):
if self.check_instances(widget):
widget_list.append(widget)
list = widget.children()
self.create_widget_list(list, widget_list)
else:
return None
[docs]
@QtCore.pyqtSlot(str)
def display_status_message(self, string):
"""
Displays a message in the status bar for a time in ms
"""
self.statusBar().showMessage(string)
[docs]
def pos2str(self, position):
""" Little helper method for converting positions to strings """
return '%.1f' % position
[docs]
@QtCore.pyqtSlot(dict)
def update_position_indicators(self, dict):
for key, pos_dict in dict.items():
if key == 'position':
self.x_position, self.y_position, self.z_position = pos_dict['x_pos'], pos_dict['y_pos'], pos_dict['z_pos']
self.f_position, self.theta_position = pos_dict['f_pos'], pos_dict['theta_pos']
self.X_Position_Indicator.setText(self.pos2str(self.x_position)+' µm')
self.Y_Position_Indicator.setText(self.pos2str(self.y_position)+' µm')
self.Z_Position_Indicator.setText(self.pos2str(self.z_position)+' µm')
self.Focus_Position_Indicator.setText(self.pos2str(self.f_position)+' µm')
self.Rotation_Position_Indicator.setText(self.pos2str(self.theta_position)+'°')
#self.state['position'] = dict['position'] # this must be done in the core thread
[docs]
@QtCore.pyqtSlot(dict)
def update_progressbars(self,dict):
cur_acq = dict['current_acq']
tot_acqs = dict['total_acqs']
cur_image = dict['current_image_in_acq']
images_in_acq = dict['images_in_acq']
tot_images = dict['total_image_count']
image_count = dict['image_counter']
time_passed_string = dict['time_passed_string']
remaining_time_string = dict['remaining_time_string']
fps = self.state['current_framerate']
self.AcquisitionProgressBar.setValue(int((cur_image+1)/images_in_acq*100))
self.TotalProgressBar.setValue(int((image_count+1)/tot_images*100))
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setValue(int((image_count+1)/tot_images*100))
'''
self.AcquisitionProgressBar.setFormat('%p% Image: '+ str(cur_image+1) + '/' + str(images_in_acq) + ' FPS: ' + '{:.1f}'.format(fps))
self.TotalProgressBar.setFormat('%p% Acq: '+ str(cur_acq+1) +\
'/' + str(tot_acqs) +\
' ' + ' Image: '+ str(image_count) +\
'/' + str(tot_images) + ' ' +\
'Time: ' + time_passed_string + \
' Remaining: ' + remaining_time_string)
[docs]
def create_script_window(self):
"""
Creates a script window for the user to input Python code.
"""
self.scriptwindow = mesoSPIM_ScriptWindow(self)
self.scriptwindow.setWindowTitle("Script Window")
self.scriptwindow.show()
self.scriptwindow.sig_execute_script.connect(self.execute_script)
[docs]
def initialize_and_connect_menubar(self):
self.actionExit.triggered.connect(self.close_app)
self.actionOpen_TIFF.triggered.connect(self.open_tiff)
self.actionOpen_Camera_Window.triggered.connect(self.camera_window.show)
self.actionOpen_Webcam_Window.triggered.connect(self.open_webcam_window)
self.actionOpen_Acquisition_Manager.triggered.connect(self.acquisition_manager_window.show)
self.actionOpen_Tile_Overview.triggered.connect(self.tile_view_window.show)
self.actionCascade_windows.triggered.connect(self.cascade_all_windows)
[docs]
def initialize_and_connect_widgets(self):
""" Connecting the menu actions """
self.openScriptEditorButton.clicked.connect(self.create_script_window)
''' Connecting the movement & zero buttons '''
if 'flip_XYZFT_button_polarity' in self.cfg.ui_options.keys():
x_sign = -1 if self.cfg.ui_options['flip_XYZFT_button_polarity'][0] else 1
y_sign = -1 if self.cfg.ui_options['flip_XYZFT_button_polarity'][1] else 1
z_sign = -1 if self.cfg.ui_options['flip_XYZFT_button_polarity'][2] else 1
f_sign = -1 if self.cfg.ui_options['flip_XYZFT_button_polarity'][3] else 1
t_sign = -1 if self.cfg.ui_options['flip_XYZFT_button_polarity'][4] else 1
else:
x_sign, y_sign, z_sign, f_sign, t_sign = 1, 1, 1, 1, 1
logger.warning('flip_XYZFT_button_polarity key not found in config file. Assuming all buttons are positive.')
self.xPlusButton.pressed.connect(lambda: self.move_relative({'x_rel': - x_sign*self.xyzIncrementSpinbox.value()}))
self.xMinusButton.pressed.connect(lambda: self.move_relative({'x_rel': x_sign*self.xyzIncrementSpinbox.value()}))
self.yPlusButton.pressed.connect(lambda: self.move_relative({'y_rel': y_sign*self.xyzIncrementSpinbox.value()}))
self.yMinusButton.pressed.connect(lambda: self.move_relative({'y_rel': - y_sign*self.xyzIncrementSpinbox.value()}))
self.zPlusButton.pressed.connect(lambda: self.move_relative({'z_rel': z_sign*self.xyzIncrementSpinbox.value()}))
self.zMinusButton.pressed.connect(lambda: self.move_relative({'z_rel': - z_sign*self.xyzIncrementSpinbox.value()}))
self.focusPlusButton.pressed.connect(lambda: self.move_relative({'f_rel': f_sign*self.focusIncrementSpinbox.value()}))
self.focusMinusButton.pressed.connect(lambda: self.move_relative({'f_rel': - f_sign*self.focusIncrementSpinbox.value()}))
self.rotPlusButton.pressed.connect(lambda: self.move_relative({'theta_rel': t_sign*self.rotIncrementSpinbox.value()}))
self.rotMinusButton.pressed.connect(lambda: self.move_relative({'theta_rel': - t_sign*self.rotIncrementSpinbox.value()}))
self.xyzrotStopButton.pressed.connect(self.sig_stop_movement.emit)
self.xyZeroButton.clicked.connect(lambda bool: self.sig_zero_axes.emit(['x','y']) if bool is True else self.sig_unzero_axes.emit(['x','y']))
self.zZeroButton.clicked.connect(lambda bool: self.sig_zero_axes.emit(['z']) if bool is True else self.sig_unzero_axes.emit(['z']))
self.focusZeroButton.clicked.connect(lambda bool: self.sig_zero_axes.emit(['f']) if bool is True else self.sig_unzero_axes.emit(['f']))
self.focusAutoButton.clicked.connect(lambda: self.sig_launch_optimizer.emit({'mode': 'focus', 'amplitude': 300}))
self.rotZeroButton.clicked.connect(lambda bool: self.sig_zero_axes.emit(['theta']) if bool is True else self.sig_unzero_axes.emit(['theta']))
self.xyzLoadButton.clicked.connect(self.sig_load_sample.emit)
self.xyzUnloadButton.clicked.connect(self.sig_unload_sample.emit)
self.centerButton.clicked.connect(self.sig_center_sample.emit)
self.launchOptimizerButton.clicked.connect(lambda: self.sig_launch_optimizer.emit({'mode': 'etl_offset', 'amplitude': 0.5}))
self.ContrastWindowButton.clicked.connect(lambda: self.sig_launch_contrast_window.emit())
''' Disabling UI buttons if necessary '''
if hasattr(self.cfg, 'ui_options'):
if self.cfg.ui_options['enable_x_buttons'] is False:
self.enable_move_buttons('x', False)
if self.cfg.ui_options['enable_y_buttons'] is False:
self.enable_move_buttons('y', False)
if self.cfg.ui_options['enable_x_buttons'] is False and self.cfg.ui_options['enable_y_buttons'] is False:
self.xyZeroButton.setEnabled(False)
if self.cfg.ui_options['enable_z_buttons'] is False:
self.enable_move_buttons('z', False)
self.zZeroButton.setEnabled(False)
if self.cfg.ui_options['enable_f_buttons'] is False:
self.enable_move_buttons('f', False)
self.focusZeroButton.setEnabled(False)
if 'enable_f_zero_button' in self.cfg.ui_options.keys():
self.focusZeroButton.setEnabled(self.cfg.ui_options['enable_f_zero_button'])
if self.cfg.ui_options['enable_rotation_buttons'] is False:
self.enable_move_buttons('theta', False)
self.rotZeroButton.setEnabled(False)
if self.cfg.ui_options['enable_loading_buttons'] is False:
self.xyzLoadButton.setEnabled(False)
self.xyzUnloadButton.setEnabled(False)
''' Connecting state-changing buttons '''
self.LiveButton.clicked.connect(self.run_live)
self.SnapButton.clicked.connect(self.run_snap)
self.RunSelectedAcquisitionButton.clicked.connect(self.run_selected_acquisition)
self.RunAcquisitionListButton.clicked.connect(self.run_acquisition_list)
self.StopButton.clicked.connect(lambda: self.sig_state_request.emit({'state':'idle'}))
self.LightsheetSwitchingModeButton.clicked.connect(self.run_lightsheet_alignment_mode)
self.VisualModeButton.clicked.connect(self.run_visual_mode)
self.ETLIncrementSpinBox.valueChanged.connect(self.update_etl_increments)
self.ZeroLeftETLButton.toggled.connect(self.zero_left_etl)
self.ZeroRightETLButton.toggled.connect(self.zero_right_etl)
self.freezeGalvoButton.toggled.connect(self.zero_galvo_amp)
self.ChooseETLcfgButton.clicked.connect(self.choose_etl_config)
self.SaveETLParametersButton.clicked.connect(self.save_etl_config)
self.ChooseSnapFolderButton.clicked.connect(self.choose_snap_folder)
self.SnapFolderIndicator.setText(self.state['snap_folder'])
self.ETLconfigIndicator.setText(self.state['ETL_cfg_file'])
self.ShutterComboBox.currentIndexChanged.connect(self.update_GUI_by_shutter_state)
self.widget_to_state_parameter_assignment=(
(self.FilterComboBox, 'filter',1),
(self.FilterComboBox, 'filter',1),
(self.ZoomComboBox, 'zoom',1),
(self.ShutterComboBox, 'shutterconfig',1),
(self.LaserComboBox, 'laser',1),
(self.LaserIntensitySlider, 'intensity', 1),
(self.LaserIntensitySpinBox, 'intensity', 1),
(self.CameraExposureTimeSpinBox, 'camera_exposure_time',1000),
#(self.CameraLineIntervalSpinBox,'camera_line_interval',1000000),
(self.CameraTriggerDelaySpinBox,'camera_delay_%',1),
(self.CameraTriggerPulseLengthSpinBox, 'camera_pulse_%',1),
(self.SweeptimeSpinBox,'sweeptime',1000),
(self.LeftLaserPulseDelaySpinBox,'laser_l_delay_%',1),
(self.RightLaserPulseDelaySpinBox,'laser_r_delay_%',1),
(self.LeftLaserPulseLengthSpinBox,'laser_l_pulse_%',1),
(self.RightLaserPulseLengthSpinBox,'laser_r_pulse_%',1),
(self.LeftLaserPulseMaxAmplitudeSpinBox,'laser_l_max_amplitude_%',1),
(self.RightLaserPulseMaxAmplitudeSpinBox,'laser_r_max_amplitude_%',1),
(self.GalvoFrequencySpinBox,'galvo_r_frequency',1),
(self.GalvoFrequencySpinBox,'galvo_l_frequency',1),
(self.LeftGalvoAmplitudeSpinBox,'galvo_l_amplitude',1),
#(self.LeftGalvoAmplitudeSpinBox,'galvo_r_amplitude',1),
(self.LeftGalvoPhaseSpinBox,'galvo_l_phase',1),
(self.RightGalvoPhaseSpinBox,'galvo_r_phase',1),
(self.LeftGalvoOffsetSpinBox, 'galvo_l_offset',1),
(self.RightGalvoOffsetSpinBox, 'galvo_r_offset',1),
(self.LeftETLOffsetSpinBox,'etl_l_offset',1),
(self.RightETLOffsetSpinBox,'etl_r_offset',1),
(self.LeftETLAmplitudeSpinBox,'etl_l_amplitude',1),
(self.RightETLAmplitudeSpinBox,'etl_r_amplitude',1),
(self.LeftETLDelaySpinBox,'etl_l_delay_%',1),
(self.RightETLDelaySpinBox,'etl_r_delay_%',1),
(self.LeftETLRampRisingSpinBox,'etl_l_ramp_rising_%',1),
(self.RightETLRampRisingSpinBox, 'etl_r_ramp_rising_%',1),
(self.LeftETLRampFallingSpinBox, 'etl_l_ramp_falling_%',1),
(self.RightETLRampFallingSpinBox, 'etl_r_ramp_falling_%',1),
)
for widget, state_parameter, conversion_factor in self.widget_to_state_parameter_assignment:
self.connect_widget_to_state_parameter(widget, state_parameter, conversion_factor)
''' Connecting the microscope controls '''
''' List for subsampling factors - comboboxes need a list of strings'''
subsampling_list = [str(i) for i in self.cfg.camera_parameters['subsampling']]
self.connect_combobox_to_state_parameter(self.FilterComboBox,self.cfg.filterdict.keys(),'filter')
self.connect_combobox_to_state_parameter(self.ZoomComboBox,self.cfg.zoomdict.keys(),'zoom')
self.connect_combobox_to_state_parameter(self.ShutterComboBox,self.cfg.shutteroptions,'shutterconfig')
self.connect_combobox_to_state_parameter(self.LaserComboBox,self.cfg.laserdict.keys(),'laser')
# self.connect_combobox_to_state_parameter(self.CameraSensorModeComboBox,['ASLM','Area'],'camera_sensor_mode')
self.connect_combobox_to_state_parameter(self.LiveSubSamplingComboBox,subsampling_list,'camera_display_live_subsampling', int_conversion = True)
self.connect_combobox_to_state_parameter(self.AcquisitionSubSamplingComboBox,subsampling_list,'camera_display_acquisition_subsampling', int_conversion = True)
# self.connect_combobox_to_state_parameter(self.CameraSensorModeComboBox,['ASLM','Area'],'camera_sensor_mode')
self.connect_combobox_to_state_parameter(self.BinningComboBox, self.cfg.binning_dict.keys(),'camera_binning')
self.checkBoxScaleWZoom.stateChanged.connect(self.scale_galvo_amp_w_zoom)
self.LaserIntensitySlider.valueChanged.connect(self.set_laser_intensity)
self.LaserIntensitySpinBox.valueChanged.connect(self.set_laser_intensity)
self.LaserIntensitySlider.setValue(self.cfg.startup['intensity'])
[docs]
def enable_move_buttons(self, axis='x', state=True):
if axis == 'x':
self.xPlusButton.setEnabled(state)
self.xMinusButton.setEnabled(state)
elif axis == 'y':
self.yPlusButton.setEnabled(state)
self.yMinusButton.setEnabled(state)
elif axis == 'z':
self.zPlusButton.setEnabled(state)
self.zMinusButton.setEnabled(state)
elif axis == 'f':
self.focusPlusButton.setEnabled(state)
self.focusMinusButton.setEnabled(state)
elif axis == 'theta':
self.rotPlusButton.setEnabled(state)
self.rotMinusButton.setEnabled(state)
else:
raise ValueError(f'axis = {axis} is unknown.')
[docs]
@QtCore.pyqtSlot(dict)
def move_relative(self, pos_dict):
assert len(pos_dict) == 1, f"Position dictionary expects only one entry, got {pos_dict}"
key, value = list(pos_dict.keys())[0], list(pos_dict.values())[0]
#self.sig_move_relative.emit(pos_dict)
self.core.serial_worker.move_relative(pos_dict) # direct call to ensure execution in main thread during live mode and avoid conflicts with core thread (stage freezing)
if hasattr(self.cfg, 'ui_options') and ('button_sleep_ms_xyzft' in self.cfg.ui_options.keys()):
axis = key[:-4]
index = ['x', 'y', 'z', 'f', 'theta'].index(axis)
sleep_ms = self.cfg.ui_options['button_sleep_ms_xyzft'][index]
if sleep_ms > 0:
self.enable_move_buttons(axis, False)
QtCore.QTimer().singleShot(sleep_ms, lambda: self.enable_move_buttons(axis, True))
else:
pass
[docs]
@QtCore.pyqtSlot(int)
def set_laser_intensity(self, value):
self.sig_state_request.emit({'intensity': value})
self.LaserIntensitySlider.setValue(value)
self.LaserIntensitySpinBox.setValue(value)
[docs]
@QtCore.pyqtSlot()
def scale_galvo_amp_w_zoom(self):
self.state['galvo_amp_scale_w_zoom'] = self.checkBoxScaleWZoom.isChecked()
[docs]
@QtCore.pyqtSlot()
def update_GUI_by_shutter_state(self):
''' Disables controls for the opposite ETL to avoid overriding parameters '''
if self.ShutterComboBox.currentText() == 'Left':
self.LeftETLOffsetSpinBox.setEnabled(True)
self.LeftETLAmplitudeSpinBox.setEnabled(True)
self.ZeroLeftETLButton.setEnabled(True)
self.RightETLOffsetSpinBox.setEnabled(False)
self.RightETLAmplitudeSpinBox.setEnabled(False)
self.ZeroRightETLButton.setEnabled(False)
elif self.ShutterComboBox.currentText() == 'Right':
self.RightETLOffsetSpinBox.setEnabled(True)
self.RightETLAmplitudeSpinBox.setEnabled(True)
self.ZeroRightETLButton.setEnabled(True)
self.LeftETLOffsetSpinBox.setEnabled(False)
self.LeftETLAmplitudeSpinBox.setEnabled(False)
self.ZeroLeftETLButton.setEnabled(False)
else: # In case of "Both" (or if something completely different is in the config file)
self.RightETLOffsetSpinBox.setEnabled(True)
self.RightETLAmplitudeSpinBox.setEnabled(True)
self.ZeroRightETLButton.setEnabled(True)
self.LeftETLOffsetSpinBox.setEnabled(True)
self.LeftETLAmplitudeSpinBox.setEnabled(True)
self.ZeroLeftETLButton.setEnabled(True)
[docs]
def connect_widget_to_state_parameter(self, widget, state_parameter, conversion_factor):
'''
Helper method to (currently) connect spinboxes
'''
if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)):
self.connect_spinbox_to_state_parameter(widget, state_parameter, conversion_factor)
[docs]
def connect_combobox_to_state_parameter(self, combobox, option_list, state_parameter, int_conversion = False):
'''
Helper method to connect and initialize a combobox from the config
Args:
combobox (QtWidgets.QComboBox): Combobox in the GUI to be connected
option_list (list): List of selection options
state_parameter (str): State parameter (has to exist in the config)
'''
combobox.addItems(option_list)
if not int_conversion:
self.sig_state_request.emit({state_parameter: self.cfg.startup[state_parameter]}) # force update of the state
combobox.setCurrentText(self.cfg.startup[state_parameter])
combobox.currentTextChanged.connect(lambda currentText: self.sig_state_request.emit({state_parameter : currentText}), type=QtCore.Qt.QueuedConnection) # Execute in the Core (receiver) thread
else:
self.sig_state_request.emit({state_parameter: int(self.cfg.startup[state_parameter])}) # force update of the state
combobox.setCurrentText(str(self.cfg.startup[state_parameter]))
combobox.currentTextChanged.connect(lambda currentParameter: self.sig_state_request.emit({state_parameter : int(currentParameter)}), type=QtCore.Qt.QueuedConnection) # Execute in the Core (receiver) thread
[docs]
def connect_spinbox_to_state_parameter(self, spinbox, state_parameter, conversion_factor=1):
'''
Helper method to connect and initialize a spinbox from the config
Args:
spinbox (QtWidgets.QSpinBox or QtWidgets.QDoubleSpinbox): Spinbox in
the GUI to be connected
state_parameter (str): State parameter (has to exist in the config)
conversion_factor (float): Conversion factor. If the config is in
seconds, the spinbox displays ms:
conversion_factor = 1000. If the config is
in seconds and the spinbox displays
microseconds: conversion_factor = 1000000
'''
spinbox.valueChanged.connect(lambda new_value: self.spinbox_to_state_parameter(new_value, spinbox, state_parameter, conversion_factor))
[docs]
def spinbox_to_state_parameter(self, new_value, spinbox, state_parameter, conversion_factor):
self.sig_state_request.emit({state_parameter : new_value/conversion_factor})
self.slow_down_spinbox(spinbox)
[docs]
def slow_down_spinbox(self, spinbox):
spinbox.setReadOnly(True) # Disable the spinbox to allow state take change
QTimer.singleShot(250, lambda: spinbox.setReadOnly(False)) # Re-enable after 250 ms to prevent looping between state->GUI and GUI->state updates
[docs]
@QtCore.pyqtSlot(str)
def execute_script(self, script):
self.sig_execute_script.emit(script)
[docs]
def update_widget_from_state(self, widget, state_parameter_string, conversion_factor):
if isinstance(widget, QtWidgets.QComboBox):
widget.setCurrentText(self.state[state_parameter_string])
elif isinstance(widget, (QtWidgets.QSlider, QtWidgets.QSpinBox)):
widget.setValue(int(self.state[state_parameter_string]*conversion_factor))
elif isinstance(widget, (QtWidgets.QDoubleSpinBox)):
widget.setValue(float(self.state[state_parameter_string]*conversion_factor))
[docs]
@QtCore.pyqtSlot()
def update_gui_from_state(self):
for widget, state_parameter, conversion_factor in self.widget_to_state_parameter_assignment:
self.update_widget_from_state(widget, state_parameter, conversion_factor)
self.acquisition_manager_window.set_selected_row(self.state['selected_row'])
logger.debug('GUI updated from state')
[docs]
def run_snap(self):
self.sig_state_request.emit({'state':'snap'})
self.set_progressbars_to_busy()
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
[docs]
def run_live(self):
self.sig_state_request.emit({'state':'live'})
logger.debug('Thread name during live: '+ QtCore.QThread.currentThread().objectName())
self.sig_poke_demo_thread.emit()
self.set_progressbars_to_busy()
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
[docs]
def run_selected_acquisition(self):
row = self.acquisition_manager_window.get_first_selected_row()
if row is None:
self.display_warning('No row selected - stopping!')
else:
# print('selected row:', row)
self.state['selected_row'] = row
self.sig_state_request.emit({'state':'run_selected_acquisition'})
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
self.enable_gui(False)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setVisible(True)
'''
[docs]
def run_acquisition_list(self):
self.state['selected_row'] = -1
self.sig_state_request.emit({'state':'run_acquisition_list'})
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
self.enable_gui(False)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setVisible(True)
'''
[docs]
def run_lightsheet_alignment_mode(self):
self.sig_state_request.emit({'state':'lightsheet_alignment_mode'})
self.set_progressbars_to_busy()
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setVisible(False)
'''
[docs]
def run_visual_mode(self):
self.sig_state_request.emit({'state':'visual_mode'})
self.set_progressbars_to_busy()
self.enable_mode_control_buttons(False)
self.enable_stop_button(True)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setVisible(False)
'''
[docs]
@QtCore.pyqtSlot(int)
def run_timepoint(self, timepoint):
self.acquisition_manager_window.append_time_index_to_filenames(timepoint)
self.run_acquisition_list()
[docs]
@QtCore.pyqtSlot(dict)
def launch_optimizer(self, ini_dict=None):
self.sig_move_relative.emit({'f_rel': 5}) # a hack to fix Galil stage coupling, between F and X/Y stages.
time.sleep(0.1)
self.sig_move_relative.emit({'f_rel': -5}) # a hack to fix Galil stage coupling, between F and X/Y stages.
if not self.optimizer:
self.optimizer = mesoSPIM_Optimizer(self)
self.optimizer.set_parameters(ini_dict)
else:
self.optimizer.set_parameters(ini_dict)
self.optimizer.show()
[docs]
@QtCore.pyqtSlot()
def launch_contrast_window(self):
if not self.contrast_window:
self.contrast_window = mesoSPIM_ContrastWindow(self)
self.core.camera_worker.sig_camera_frame.connect(self.contrast_window.set_image)
else:
self.contrast_window.active = True
self.contrast_window.show()
[docs]
def enable_stop_button(self, boolean):
self.StopButton.setEnabled(boolean)
[docs]
def enable_gui(self, boolean):
self.TabWidget.setEnabled(boolean)
self.ControlGroupBox.setEnabled(boolean)
self.sig_enable_gui.emit(boolean)
[docs]
def enable_mode_control_buttons(self, boolean):
self.LiveButton.setEnabled(boolean)
self.SnapButton.setEnabled(boolean)
self.RunSelectedAcquisitionButton.setEnabled(boolean)
self.RunAcquisitionListButton.setEnabled(boolean)
self.VisualModeButton.setEnabled(boolean)
self.LightsheetSwitchingModeButton.setEnabled(boolean)
[docs]
def finished(self):
self.enable_stop_button(False)
self.enable_mode_control_buttons(True)
self.enable_gui(True)
self.set_progressbars_to_standard()
[docs]
def set_progressbars_to_busy(self):
'''If min and max of a progress bar are 0, it shows a "busy" indicator'''
self.AcquisitionProgressBar.setMinimum(0)
self.AcquisitionProgressBar.setMaximum(0)
self.TotalProgressBar.setMinimum(0)
self.TotalProgressBar.setMaximum(0)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setVisible(False)
'''
[docs]
def set_progressbars_to_standard(self):
self.AcquisitionProgressBar.setMinimum(0)
self.AcquisitionProgressBar.setMaximum(100)
self.TotalProgressBar.setMinimum(0)
self.TotalProgressBar.setMaximum(100)
''' Disabled taskbar button progress display due to problems with Anaconda default
if sys.platform == 'win32':
self.win_taskbar_button.progress().setValue(0)
self.win_taskbar_button.progress().setVisible(False)
'''
[docs]
def update_etl_increments(self):
increment = self.ETLIncrementSpinBox.value()
self.LeftETLOffsetSpinBox.setSingleStep(increment)
self.RightETLOffsetSpinBox.setSingleStep(increment)
self.LeftETLAmplitudeSpinBox.setSingleStep(increment)
self.RightETLAmplitudeSpinBox.setSingleStep(increment)
[docs]
def zero_left_etl(self):
''' Zeros the amplitude of the left ETL for faster alignment '''
if self.ZeroLeftETLButton.isChecked():
self.ETL_L_amp_backup = self.LeftETLAmplitudeSpinBox.value()
self.LeftETLAmplitudeSpinBox.setValue(0)
self.LeftETLAmplitudeSpinBox.setEnabled(False)
self.SaveETLParametersButton.setEnabled(False)
self.ChooseETLcfgButton.setEnabled(False)
if self.ShutterComboBox.currentText() == 'Both':
self.RightETLOffsetSpinBox.setEnabled(False)
self.RightETLAmplitudeSpinBox.setEnabled(False)
self.ZeroRightETLButton.setEnabled(False)
else:
self.LeftETLAmplitudeSpinBox.setValue(self.ETL_L_amp_backup)
self.LeftETLAmplitudeSpinBox.setEnabled(True)
self.SaveETLParametersButton.setEnabled(True)
self.ChooseETLcfgButton.setEnabled(True)
if self.ShutterComboBox.currentText() == 'Both':
self.RightETLOffsetSpinBox.setEnabled(True)
self.RightETLAmplitudeSpinBox.setEnabled(True)
self.ZeroRightETLButton.setEnabled(True)
[docs]
def zero_right_etl(self):
''' Zeros the amplitude of the right ETL for faster alignment '''
if self.ZeroRightETLButton.isChecked():
self.ETL_R_amp_backup = self.RightETLAmplitudeSpinBox.value()
self.RightETLAmplitudeSpinBox.setValue(0)
self.RightETLAmplitudeSpinBox.setEnabled(False)
self.SaveETLParametersButton.setEnabled(False)
self.ChooseETLcfgButton.setEnabled(False)
if self.ShutterComboBox.currentText() == 'Both':
self.LeftETLOffsetSpinBox.setEnabled(False)
self.LeftETLAmplitudeSpinBox.setEnabled(False)
self.ZeroLeftETLButton.setEnabled(False)
else:
self.RightETLAmplitudeSpinBox.setValue(self.ETL_R_amp_backup)
self.RightETLAmplitudeSpinBox.setEnabled(True)
self.SaveETLParametersButton.setEnabled(True)
self.ChooseETLcfgButton.setEnabled(True)
if self.ShutterComboBox.currentText() == 'Both':
self.LeftETLOffsetSpinBox.setEnabled(True)
self.LeftETLAmplitudeSpinBox.setEnabled(True)
self.ZeroLeftETLButton.setEnabled(True)
[docs]
def zero_galvo_amp(self):
'''Set the amplitude of both galvos to zero, or back to where it was, depending on button state'''
if self.freezeGalvoButton.isChecked():
self.galvo_amp_backup = self.LeftGalvoAmplitudeSpinBox.value()
self.state['galvo_l_amplitude'] = 0
#self.state['galvo_r_amplitude'] = 0
self.freezeGalvoButton.setText('Unfreeze galvos')
else:
self.state['galvo_l_amplitude'] = self.galvo_amp_backup
#self.state['galvo_r_amplitude'] = self.galvo_amp_backup
self.freezeGalvoButton.setText('Freeze galvos')
self.sig_state_request.emit({'galvo_l_amplitude': self.state['galvo_l_amplitude']})
self.update_gui_from_state()
#self.sig_state_request.emit({'galvo_r_amplitude': self.state['galvo_r_amplitude']})
[docs]
def choose_etl_config(self):
''' File dialog for choosing the config file
'''
path , _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open ETL config file, specific for immersion medium and stationary cuvette size',
self.state['ETL_cfg_file'], filter='CSV file (*.csv)')
''' To avoid crashes, only set the cfg file when a file has been selected:'''
if path:
self.state['ETL_cfg_file'] = path
self.ETLconfigIndicator.setText(path)
logger.info(f'Main Window: Chose ETL Config File: {path}')
self.sig_state_request.emit({'ETL_cfg_file' : path})
else:
logger.debug(f'Choose ETL Config File cancelled')
[docs]
def save_etl_config(self):
''' Save current ETL parameters into config '''
self.sig_save_etl_config.emit()
[docs]
def display_warning(self, string):
warning = QtWidgets.QMessageBox.warning(None,'mesoSPIM Warning', string, QtWidgets.QMessageBox.Ok)
[docs]
def choose_snap_folder(self):
path = QtWidgets.QFileDialog.getExistingDirectory(self, 'Open csv File', self.state['snap_folder'])
if path:
self.state['snap_folder'] = path
self.SnapFolderIndicator.setText(path)
print('Chosen Snap Folder:', path)
[docs]
def cascade_all_windows(self):
if hasattr(self.cfg, 'ui_options') and 'window_pos' in self.cfg.ui_options.keys():
window_pos = self.cfg.ui_options['window_pos']
else:
window_pos = (100, 100)
self.move(window_pos[0], window_pos[1])
self.camera_window.move(window_pos[0] + 100, window_pos[1] + 100)
self.acquisition_manager_window.move(window_pos[0] + 200, window_pos[1] + 200)
if self.webcam_window:
self.webcam_window.move(window_pos[0] + 300, window_pos[1] + 300)