diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 4ca3e7041..e80830734 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -7,6 +7,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # NOTE(imo): This 3.13 python version is just for black to be able to use the use_pyproject option. We + # don't actually require 3.13 in our code! + - uses: actions/setup-python@v5 + with: + python-version: "3.13" - uses: psf/black@stable with: - options: "--line-length=120" + options: "--config software/pyproject.toml" + src: "./software/" diff --git a/software/control/NL5.py b/software/control/NL5.py index bacd6471b..c50cbf7ae 100644 --- a/software/control/NL5.py +++ b/software/control/NL5.py @@ -1,21 +1,22 @@ import control.RCM_API as RCM_API import json + class NL5: - + def __init__(self): self.rcm = RCM_API.RCM_API() self.rcm.initialize_device(simulated=False) self.load_settings() - def set_scan_amplitude(self,amplitude): + def set_scan_amplitude(self, amplitude): self.scan_amplitude = amplitude - self.rcm.set_float_parameter(self.rcm.AMPLITUDE_X,amplitude) + self.rcm.set_float_parameter(self.rcm.AMPLITUDE_X, amplitude) - def set_offset_x(self,offset_x): + def set_offset_x(self, offset_x): self.offset_x = offset_x - self.rcm.set_float_parameter(self.rcm.OFFSET_SCAN_X,offset_x) + self.rcm.set_float_parameter(self.rcm.OFFSET_SCAN_X, offset_x) def start_acquisition(self): ret = self.rcm.start_acquisition() @@ -35,33 +36,33 @@ def set_bypass(self, enabled): def set_active_channel(self, channel): self.active_channel = channel for i in range(1, 5): - self.rcm.set_integer_parameter(getattr(self.rcm, f'LASER_{i}_SELECTED'), 1 if i == channel else 0) + self.rcm.set_integer_parameter(getattr(self.rcm, f"LASER_{i}_SELECTED"), 1 if i == channel else 0) - def set_laser_power(self,channel,power): - self.rcm.set_integer_parameter(getattr(self.rcm,f'LASER_{channel}_POWER'),power) + def set_laser_power(self, channel, power): + self.rcm.set_integer_parameter(getattr(self.rcm, f"LASER_{channel}_POWER"), power) def set_bypass_offset(self, offset): self.bypass_offset = offset - self.rcm.set_float_parameter(self.rcm.BYPASS_OFFSET,offset) + self.rcm.set_float_parameter(self.rcm.BYPASS_OFFSET, offset) - def set_line_speed(self,speed,save_setting=False): + def set_line_speed(self, speed, save_setting=False): self.line_speed = speed - self.rcm.set_integer_parameter(self.rcm.LINE_FREQUENCY,speed) # speed in mrad/s + self.rcm.set_integer_parameter(self.rcm.LINE_FREQUENCY, speed) # speed in mrad/s if save_setting: self.save_settings() - def set_fov_x(self,fov_x): + def set_fov_x(self, fov_x): self.fov_x = fov_x - self.rcm.set_integer_parameter(self.rcm.FIELD_OF_VIEW_X,fov_x) + self.rcm.set_integer_parameter(self.rcm.FIELD_OF_VIEW_X, fov_x) self.save_settings() - def set_exposure_delay(self,exposure_delay_ms): + def set_exposure_delay(self, exposure_delay_ms): self.exposure_delay_ms = exposure_delay_ms - self.rcm.set_integer_parameter(self.rcm.EXPOSURE_DELAY,exposure_delay_ms) + self.rcm.set_integer_parameter(self.rcm.EXPOSURE_DELAY, exposure_delay_ms) def load_settings(self): try: - with open('NL5_settings.json', 'r') as file: + with open("NL5_settings.json", "r") as file: settings = json.load(file) self.scan_amplitude = settings.get("scan_amplitude", 70.0) self.offset_x = settings.get("offset_x", 0.0) @@ -77,7 +78,7 @@ def load_settings(self): self.exposure_delay_ms = 30 self.line_speed = 3000 self.fov_x = 2048 - + def save_settings(self): settings = { "scan_amplitude": self.scan_amplitude, @@ -85,9 +86,9 @@ def save_settings(self): "bypass_offset": self.bypass_offset, "fov_x": self.fov_x, "exposure_delay_ms": self.exposure_delay_ms, - "line_speed": self.line_speed + "line_speed": self.line_speed, } - with open('NL5_settings.json', 'w') as file: + with open("NL5_settings.json", "w") as file: json.dump(settings, file) @@ -96,11 +97,11 @@ class NL5_Simulation: def __init__(self): self.load_settings() - def set_scan_amplitude(self,amplitude): + def set_scan_amplitude(self, amplitude): self.scan_amplitude = amplitude pass - def set_offset_x(self,offset_x): + def set_offset_x(self, offset_x): self.offset_x = offset_x pass @@ -119,29 +120,29 @@ def set_bypass(self, enabled): def set_active_channel(self, channel): pass - def set_laser_power(self,channel,power): + def set_laser_power(self, channel, power): pass def set_bypass_offset(self, offset): self.bypass_offset = offset pass - def set_line_speed(self,speed, save_setting = False): + def set_line_speed(self, speed, save_setting=False): self.line_speed = speed if save_setting: self.save_settings() - def set_fov_x(self,fov_x): + def set_fov_x(self, fov_x): self.fov_x = fov_x self.save_settings() - def set_exposure_delay(self,exposure_delay_ms): + def set_exposure_delay(self, exposure_delay_ms): self.exposure_delay_ms = exposure_delay_ms pass def load_settings(self): try: - with open('NL5_settings.json', 'r') as file: + with open("NL5_settings.json", "r") as file: settings = json.load(file) self.scan_amplitude = settings.get("scan_amplitude", 70.0) self.offset_x = settings.get("offset_x", 0.0) @@ -157,7 +158,7 @@ def load_settings(self): self.exposure_delay_ms = 30 self.line_speed = 3000 self.fov_x = 2048 - + def save_settings(self): settings = { "scan_amplitude": self.scan_amplitude, @@ -165,7 +166,7 @@ def save_settings(self): "bypass_offset": self.bypass_offset, "fov_x": self.fov_x, "exposure_delay_ms": self.exposure_delay_ms, - "line_speed": self.line_speed + "line_speed": self.line_speed, } - with open('NL5_settings.json', 'w') as file: - json.dump(settings, file) \ No newline at end of file + with open("NL5_settings.json", "w") as file: + json.dump(settings, file) diff --git a/software/control/NL5Widget.py b/software/control/NL5Widget.py index 996269282..3d1933d49 100644 --- a/software/control/NL5Widget.py +++ b/software/control/NL5Widget.py @@ -1,5 +1,18 @@ import sys -from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout, QLabel, QDoubleSpinBox, QSpinBox, QPushButton, QCheckBox, QDialog, QDialogButtonBox +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QGridLayout, + QHBoxLayout, + QVBoxLayout, + QLabel, + QDoubleSpinBox, + QSpinBox, + QPushButton, + QCheckBox, + QDialog, + QDialogButtonBox, +) from PyQt5.QtCore import Qt @@ -8,35 +21,35 @@ def __init__(self, nl5): super().__init__() self.nl5 = nl5 self.setWindowTitle("NL5 Settings") - + layout = QGridLayout() - + # Scan amplitude control layout.addWidget(QLabel("Scan Amplitude"), 0, 0) self.scan_amplitude_input = QDoubleSpinBox() self.scan_amplitude_input.setValue(self.nl5.scan_amplitude) layout.addWidget(self.scan_amplitude_input, 0, 1) - + # Offset X control layout.addWidget(QLabel("Offset X"), 1, 0) self.offset_x_input = QDoubleSpinBox() self.offset_x_input.setMinimum(-30) self.offset_x_input.setValue(self.nl5.offset_x) layout.addWidget(self.offset_x_input, 1, 1) - + # Bypass offset control layout.addWidget(QLabel("Bypass Offset"), 2, 0) self.bypass_offset_input = QDoubleSpinBox() self.bypass_offset_input.setMinimum(-30) self.bypass_offset_input.setValue(self.nl5.bypass_offset) layout.addWidget(self.bypass_offset_input, 2, 1) - + # Dialog buttons buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons, 3, 0, 1, 2) - + self.setLayout(layout) def accept(self): @@ -50,9 +63,9 @@ def accept(self): class NL5Widget(QWidget): def __init__(self, nl5): super().__init__() - + self.nl5 = nl5 - + # Create layout layout1 = QHBoxLayout() layout2 = QHBoxLayout() @@ -61,7 +74,7 @@ def __init__(self, nl5): layout1.addWidget(QLabel("Exposure Delay")) self.exposure_delay_input = QSpinBox() self.exposure_delay_input.setValue(self.nl5.exposure_delay_ms) - self.exposure_delay_input.setSuffix(' ms') + self.exposure_delay_input.setSuffix(" ms") self.exposure_delay_input.valueChanged.connect(self.update_exposure_delay) layout1.addWidget(self.exposure_delay_input) layout1.addStretch() @@ -71,19 +84,18 @@ def __init__(self, nl5): self.line_speed_input = QSpinBox() self.line_speed_input.setMaximum(20000) self.line_speed_input.setValue(self.nl5.line_speed) - self.line_speed_input.setSuffix(' mrad/s') + self.line_speed_input.setSuffix(" mrad/s") self.line_speed_input.valueChanged.connect(self.update_line_speed) layout1.addWidget(self.line_speed_input) layout1.addStretch() - # FOV X control layout1.addWidget(QLabel("FOV")) # layout1.addWidget(QLabel("FOV X")) self.fov_x_input = QSpinBox() self.fov_x_input.setMaximum(4000) self.fov_x_input.setValue(self.nl5.fov_x) - self.fov_x_input.setSuffix(' px') + self.fov_x_input.setSuffix(" px") self.fov_x_input.valueChanged.connect(self.update_fov_x) layout1.addWidget(self.fov_x_input) @@ -92,7 +104,7 @@ def __init__(self, nl5): self.bypass_button.setCheckable(True) self.bypass_button.toggled.connect(self.update_bypass) layout2.addWidget(self.bypass_button) - + # Start acquisition button self.start_acquisition_button = QPushButton("Start Acquisition") self.start_acquisition_button.clicked.connect(self.nl5.start_acquisition) @@ -107,31 +119,32 @@ def __init__(self, nl5): layout.addLayout(layout1) layout.addLayout(layout2) self.setLayout(layout) - + def show_settings_dialog(self): dialog = NL5SettingsDialog(self.nl5) dialog.exec_() - + def update_bypass(self, checked): self.nl5.set_bypass(checked) self.start_acquisition_button.setEnabled(not checked) - + def update_exposure_delay(self, value): self.nl5.set_exposure_delay(value) - + def update_line_speed(self, value): self.nl5.set_line_speed(value) - + def update_fov_x(self, value): self.nl5.set_fov_x(value) if __name__ == "__main__": app = QApplication(sys.argv) - + import NL5 + nl5 = NL5.NL5() widget = NL5Widget(nl5) widget.show() - - sys.exit(app.exec_()) \ No newline at end of file + + sys.exit(app.exec_()) diff --git a/software/control/_def.py b/software/control/_def.py index 6fc42df50..6e5d471d6 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -55,31 +55,33 @@ def populate_class_from_dict(myclass, options): REMEMBER TO ENCLOSE PROPERTY NAMES IN LISTS IN DOUBLE QUOTES """ for key, value in options: - if key.startswith('_') and key.endswith('options'): + if key.startswith("_") and key.endswith("options"): continue actualkey = key.upper() actualvalue = conf_attribute_reader(value) setattr(myclass, actualkey, actualvalue) + class TriggerMode: - SOFTWARE = 'Software Trigger' - HARDWARE = 'Hardware Trigger' - CONTINUOUS = 'Continuous Acquisition' + SOFTWARE = "Software Trigger" + HARDWARE = "Hardware Trigger" + CONTINUOUS = "Continuous Acquisition" + class Acquisition: CROP_WIDTH = 3000 CROP_HEIGHT = 3000 NUMBER_OF_FOVS_PER_AF = 3 - IMAGE_FORMAT = 'bmp' + IMAGE_FORMAT = "bmp" IMAGE_DISPLAY_SCALING_FACTOR = 0.3 PSEUDO_COLOR = False MERGE_CHANNELS = False PSEUDO_COLOR_MAP = { - "405": {"hex": 0x0000FF}, # blue - "488": {"hex": 0x00FF00}, # green - "561": {"hex": 0xFFCF00}, # yellow - "638": {"hex": 0xFF0000}, # red - "730": {"hex": 0x770000} # dark red + "405": {"hex": 0x0000FF}, # blue + "488": {"hex": 0x00FF00}, # green + "561": {"hex": 0xFFCF00}, # yellow + "638": {"hex": 0xFF0000}, # red + "730": {"hex": 0x770000}, # dark red } DX = 0.9 DY = 0.9 @@ -87,16 +89,20 @@ class Acquisition: NX = 1 NY = 1 + class PosUpdate: INTERVAL_MS = 25 + class MicrocontrollerDef: MSG_LENGTH = 24 CMD_LENGTH = 8 N_BYTES_POS = 4 + USE_SEPARATE_MCU_FOR_DAC = False + class MCU_PINS: PWM1 = 5 PWM2 = 4 @@ -115,6 +121,7 @@ class MCU_PINS: PWM16 = 25 AF_LASER = 15 + class CMD_SET: MOVE_X = 0 MOVE_Y = 1 @@ -153,20 +160,24 @@ class CMD_SET: INITIALIZE = 254 RESET = 255 + class CMD_SET2: ANALOG_WRITE_DAC8050X = 0 SET_CAMERA_TRIGGER_FREQUENCY = 1 START_CAMERA_TRIGGERING = 2 STOP_CAMERA_TRIGGERING = 3 + BIT_POS_JOYSTICK_BUTTON = 0 BIT_POS_SWITCH = 1 + class HOME_OR_ZERO: - HOME_NEGATIVE = 1 # motor moves along the negative direction (MCU coordinates) - HOME_POSITIVE = 0 # motor moves along the negative direction (MCU coordinates) + HOME_NEGATIVE = 1 # motor moves along the negative direction (MCU coordinates) + HOME_POSITIVE = 0 # motor moves along the negative direction (MCU coordinates) ZERO = 2 + class AXIS: X = 0 Y = 1 @@ -175,6 +186,7 @@ class AXIS: XY = 4 W = 5 + class LIMIT_CODE: X_POSITIVE = 0 X_NEGATIVE = 1 @@ -183,13 +195,14 @@ class LIMIT_CODE: Z_POSITIVE = 4 Z_NEGATIVE = 5 + class LIMIT_SWITCH_POLARITY: ACTIVE_LOW = 0 ACTIVE_HIGH = 1 DISABLED = 2 - X_HOME= 1 - Y_HOME= 1 - Z_HOME= 2 + X_HOME = 1 + Y_HOME = 1 + Z_HOME = 2 class ILLUMINATION_CODE: @@ -207,9 +220,11 @@ class ILLUMINATION_CODE: ILLUMINATION_SOURCE_561NM = 14 ILLUMINATION_SOURCE_730NM = 15 + class VOLUMETRIC_IMAGING: NUM_PLANES_PER_VOLUME = 20 + class CMD_EXECUTION_STATUS: COMPLETED_WITHOUT_ERRORS = 0 IN_PROGRESS = 1 @@ -218,19 +233,21 @@ class CMD_EXECUTION_STATUS: CMD_EXECUTION_ERROR = 4 ERROR_CODE_EMPTYING_THE_FLUDIIC_LINE_FAILED = 100 + class CAMERA_CONFIG: ROI_OFFSET_X_DEFAULT = 0 ROI_OFFSET_Y_DEFAULT = 0 ROI_WIDTH_DEFAULT = 3104 ROI_HEIGHT_DEFAULT = 2084 + PRINT_CAMERA_FPS = True ########################################################### #### machine specific configurations - to be overridden ### ########################################################### ROTATE_IMAGE_ANGLE = None -FLIP_IMAGE = None # 'Horizontal', 'Vertical', 'Both' +FLIP_IMAGE = None # 'Horizontal', 'Vertical', 'Both' CAMERA_REVERSE_X = False CAMERA_REVERSE_Y = False @@ -281,14 +298,14 @@ class CAMERA_CONFIG: SCREW_PITCH_X_MM = 1 SCREW_PITCH_Y_MM = 1 -SCREW_PITCH_Z_MM = 0.012*25.4 +SCREW_PITCH_Z_MM = 0.012 * 25.4 SCREW_PITCH_W_MM = 1 MICROSTEPPING_DEFAULT_X = 8 MICROSTEPPING_DEFAULT_Y = 8 MICROSTEPPING_DEFAULT_Z = 8 MICROSTEPPING_DEFAULT_W = 64 -MICROSTEPPING_DEFAULT_THETA = 8 # not used, to be removed +MICROSTEPPING_DEFAULT_THETA = 8 # not used, to be removed X_MOTOR_RMS_CURRENT_mA = 490 Y_MOTOR_RMS_CURRENT_mA = 490 @@ -317,25 +334,25 @@ class CAMERA_CONFIG: HAS_ENCODER_W = False # enable PID control -ENABLE_PID_X = False -ENABLE_PID_Y = False -ENABLE_PID_Z = False -ENABLE_PID_W = False +ENABLE_PID_X = False +ENABLE_PID_Y = False +ENABLE_PID_Z = False +ENABLE_PID_W = False # PID arguments -PID_P_X = int(1<<12) +PID_P_X = int(1 << 12) PID_I_X = int(0) PID_D_X = int(0) -PID_P_Y = int(1<<12) +PID_P_Y = int(1 << 12) PID_I_Y = int(0) PID_D_Y = int(0) -PID_P_Z = int(1<<12) +PID_P_Z = int(1 << 12) PID_I_Z = int(0) PID_D_Z = int(1) -PID_P_W = int(1<<12) +PID_P_W = int(1 << 12) PID_I_W = int(1) PID_D_W = int(1) @@ -367,7 +384,8 @@ class CAMERA_CONFIG: DEFAULT_SAVING_PATH = str(Path.home()) + "/Downloads" -DEFAULT_PIXEL_FORMAT = 'MONO12' +DEFAULT_PIXEL_FORMAT = "MONO12" + class PLATE_READER: NUMBER_OF_ROWS = 8 @@ -377,41 +395,58 @@ class PLATE_READER: OFFSET_COLUMN_1_MM = 20 OFFSET_ROW_A_MM = 20 -DEFAULT_DISPLAY_CROP = 100 # value ranges from 1 to 100 - image display crop size -CAMERA_PIXEL_SIZE_UM = {'IMX290':2.9,'IMX178':2.4,'IMX226':1.85,'IMX250':3.45,'IMX252':3.45,'IMX273':3.45,'IMX264':3.45,'IMX265':3.45,'IMX571':3.76,'PYTHON300':4.8} +DEFAULT_DISPLAY_CROP = 100 # value ranges from 1 to 100 - image display crop size + +CAMERA_PIXEL_SIZE_UM = { + "IMX290": 2.9, + "IMX178": 2.4, + "IMX226": 1.85, + "IMX250": 3.45, + "IMX252": 3.45, + "IMX273": 3.45, + "IMX264": 3.45, + "IMX265": 3.45, + "IMX571": 3.76, + "PYTHON300": 4.8, +} TUBE_LENS_MM = 50 -CAMERA_SENSOR = 'IMX226' -TRACKERS = ['csrt', 'kcf', 'mil', 'tld', 'medianflow','mosse','daSiamRPN'] -DEFAULT_TRACKER = 'csrt' +CAMERA_SENSOR = "IMX226" +TRACKERS = ["csrt", "kcf", "mil", "tld", "medianflow", "mosse", "daSiamRPN"] +DEFAULT_TRACKER = "csrt" ENABLE_TRACKING = False -TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS = False # set to true when doing multimodal acquisition +TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS = False # set to true when doing multimodal acquisition if ENABLE_TRACKING: DEFAULT_DISPLAY_CROP = 100 + class AF: STOP_THRESHOLD = 0.85 CROP_WIDTH = 800 CROP_HEIGHT = 800 + class Tracking: - SEARCH_AREA_RATIO = 10 #@@@ check - CROPPED_IMG_RATIO = 10 #@@@ check + SEARCH_AREA_RATIO = 10 # @@@ check + CROPPED_IMG_RATIO = 10 # @@@ check BBOX_SCALE_FACTOR = 1.2 DEFAULT_TRACKER = "csrt" INIT_METHODS = ["roi"] DEFAULT_INIT_METHOD = "roi" + SHOW_DAC_CONTROL = False + class SLIDE_POSITION: LOADING_X_MM = 30 LOADING_Y_MM = 55 SCANNING_X_MM = 3 SCANNING_Y_MM = 3 + class OUTPUT_GAINS: REFDIV = False CHANNEL0_GAIN = False @@ -423,9 +458,11 @@ class OUTPUT_GAINS: CHANNEL6_GAIN = False CHANNEL7_GAIN = True + SLIDE_POTISION_SWITCHING_TIMEOUT_LIMIT_S = 10 SLIDE_POTISION_SWITCHING_HOME_EVERYTIME = False + class SOFTWARE_POS_LIMIT: X_POSITIVE = 56 X_NEGATIVE = -0.5 @@ -434,59 +471,58 @@ class SOFTWARE_POS_LIMIT: Z_POSITIVE = 7 Z_NEGATIVE = 0.05 + SHOW_AUTOLEVEL_BTN = False AUTOLEVEL_DEFAULT_SETTING = False -MULTIPOINT_AUTOFOCUS_CHANNEL = 'BF LED matrix full' +MULTIPOINT_AUTOFOCUS_CHANNEL = "BF LED matrix full" # MULTIPOINT_AUTOFOCUS_CHANNEL = 'BF LED matrix left half' MULTIPOINT_AUTOFOCUS_ENABLE_BY_DEFAULT = False -MULTIPOINT_BF_SAVING_OPTION = 'Raw' +MULTIPOINT_BF_SAVING_OPTION = "Raw" # MULTIPOINT_BF_SAVING_OPTION = 'RGB2GRAY' # MULTIPOINT_BF_SAVING_OPTION = 'Green Channel Only' -DEFAULT_MULTIPOINT_NX=1 -DEFAULT_MULTIPOINT_NY=1 +DEFAULT_MULTIPOINT_NX = 1 +DEFAULT_MULTIPOINT_NY = 1 ENABLE_FLEXIBLE_MULTIPOINT = True USE_OVERLAP_FOR_FLEXIBLE = 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 +CAMERA_SN = {"ch 1": "SN1", "ch 2": "SN2"} # for multiple cameras, to be overwritten in the configuration file ENABLE_STROBE_OUTPUT = False -ACQUISITION_PATTERN = 'S-Pattern' # 'S-Pattern', 'Unidirectional' -FOV_PATTERN = 'Unidirectional' # 'S-Pattern', 'Unidirectional' +ACQUISITION_PATTERN = "S-Pattern" # 'S-Pattern', 'Unidirectional' +FOV_PATTERN = "Unidirectional" # 'S-Pattern', 'Unidirectional' -Z_STACKING_CONFIG = 'FROM BOTTOM' # 'FROM BOTTOM', 'FROM TOP' -Z_STACKING_CONFIG_MAP = { - 0: 'FROM BOTTOM', - 1: 'FROM CENTER', - 2: 'FROM TOP' -} +Z_STACKING_CONFIG = "FROM BOTTOM" # 'FROM BOTTOM', 'FROM TOP' +Z_STACKING_CONFIG_MAP = {0: "FROM BOTTOM", 1: "FROM CENTER", 2: "FROM TOP"} DEFAULT_Z_POS_MM = 2 -WELLPLATE_OFFSET_X_mm = 0 # x offset adjustment for using different plates -WELLPLATE_OFFSET_Y_mm = 0 # y offset adjustment for using different plates +WELLPLATE_OFFSET_X_mm = 0 # x offset adjustment for using different plates +WELLPLATE_OFFSET_Y_mm = 0 # y offset adjustment for using different plates # for USB spectrometer N_SPECTRUM_PER_POINT = 5 # focus measure operator -FOCUS_MEASURE_OPERATOR = 'LAPE' # 'GLVA' # LAPE has worked well for bright field images; GLVA works well for darkfield/fluorescence +FOCUS_MEASURE_OPERATOR = ( + "LAPE" # 'GLVA' # LAPE has worked well for bright field images; GLVA works well for darkfield/fluorescence +) # controller version -CONTROLLER_VERSION = 'Arduino Due' # 'Teensy' +CONTROLLER_VERSION = "Arduino Due" # 'Teensy' -#How to read Spinnaker nodemaps, options are INDIVIDUAL or VALUE -CHOSEN_READ = 'INDIVIDUAL' +# How to read Spinnaker nodemaps, options are INDIVIDUAL or VALUE +CHOSEN_READ = "INDIVIDUAL" # laser autofocus SUPPORT_LASER_AUTOFOCUS = True -MAIN_CAMERA_MODEL = 'MER2-1220-32U3M' -FOCUS_CAMERA_MODEL = 'MER2-630-60U3M' +MAIN_CAMERA_MODEL = "MER2-1220-32U3M" +FOCUS_CAMERA_MODEL = "MER2-630-60U3M" FOCUS_CAMERA_EXPOSURE_TIME_MS = 2 FOCUS_CAMERA_ANALOG_GAIN = 0 LASER_AF_AVERAGING_N = 5 @@ -532,11 +568,11 @@ class SOFTWARE_POS_LIMIT: # Spinning disk confocal integration ENABLE_SPINNING_DISK_CONFOCAL = False USE_LDI_SERIAL_CONTROL = False -LDI_INTENSITY_MODE = 'PC' -LDI_SHUTTER_MODE = 'PC' +LDI_INTENSITY_MODE = "PC" +LDI_SHUTTER_MODE = "PC" USE_CELESTA_ETHENET_CONTROL = False -XLIGHT_EMISSION_FILTER_MAPPING = {405:1,470:2,555:3,640:4,730:5} +XLIGHT_EMISSION_FILTER_MAPPING = {405: 1, 470: 2, 555: 3, 640: 4, 730: 5} XLIGHT_SERIAL_NUMBER = "B00031BE" XLIGHT_SLEEP_TIME_FOR_WHEEL = 0.25 XLIGHT_VALIDATE_WHEEL_POS = False @@ -545,16 +581,11 @@ class SOFTWARE_POS_LIMIT: ENABLE_NL5 = False ENABLE_CELLX = False CELLX_SN = None -CELLX_MODULATION = 'EXT Digital' +CELLX_MODULATION = "EXT Digital" NL5_USE_AOUT = False NL5_USE_DOUT = True NL5_TRIGGER_PIN = 2 -NL5_WAVENLENGTH_MAP = { - 405: 1, - 470: 2, 488: 2, - 545: 3, 555: 3, 561: 3, - 637: 4, 638: 4, 640: 4 -} +NL5_WAVENLENGTH_MAP = {405: 1, 470: 2, 488: 2, 545: 3, 555: 3, 561: 3, 637: 4, 638: 4, 640: 4} # Laser AF characterization mode LASER_AF_CHARACTERIZATION_MODE = False @@ -576,8 +607,8 @@ class SOFTWARE_POS_LIMIT: SCIMICROSCOPY_LED_ARRAY_SN = None SCIMICROSCOPY_LED_ARRAY_DISTANCE = 50 SCIMICROSCOPY_LED_ARRAY_DEFAULT_NA = 0.8 -SCIMICROSCOPY_LED_ARRAY_DEFAULT_COLOR = [1,1,1] -SCIMICROSCOPY_LED_ARRAY_TURN_ON_DELAY = 0.03 # time to wait before trigger the camera (in seconds) +SCIMICROSCOPY_LED_ARRAY_DEFAULT_COLOR = [1, 1, 1] +SCIMICROSCOPY_LED_ARRAY_TURN_ON_DELAY = 0.03 # time to wait before trigger the camera (in seconds) # Navigation Settings ENABLE_CLICK_TO_MOVE_BY_DEFAULT = True @@ -595,7 +626,7 @@ class SOFTWARE_POS_LIMIT: "730": {"hex": 0x770000, "name": "dark red"}, "R": {"hex": 0xFF0000, "name": "red"}, "G": {"hex": 0x1FFF00, "name": "green"}, - "B": {"hex": 0x3300FF, "name": "blue"} + "B": {"hex": 0x3300FF, "name": "blue"}, } # Emission filter wheel @@ -603,7 +634,7 @@ class SOFTWARE_POS_LIMIT: ZABER_EMISSION_FILTER_WHEEL_DELAY_MS = 70 ZABER_EMISSION_FILTER_WHEEL_BLOCKING_CALL = False USE_OPTOSPIN_EMISSION_FILTER_WHEEL = False -FILTER_CONTROLLER_SERIAL_NUMBER = 'A10NG007' +FILTER_CONTROLLER_SERIAL_NUMBER = "A10NG007" OPTOSPIN_EMISSION_FILTER_WHEEL_SPEED_HZ = 50 OPTOSPIN_EMISSION_FILTER_WHEEL_DELAY_MS = 70 OPTOSPIN_EMISSION_FILTER_WHEEL_TTL_TRIGGER = False @@ -624,62 +655,65 @@ class SOFTWARE_POS_LIMIT: DISPLAY_TOUPCAMER_BLACKLEVEL_SETTINGS = False DEFAULT_BLACKLEVEL_VALUE = 3 + def read_objectives_csv(file_path): objectives = {} - with open(file_path, 'r') as csvfile: + with open(file_path, "r") as csvfile: reader = csv.DictReader(csvfile) for row in reader: - objectives[row['name']] = { - 'magnification': float(row['magnification']), - 'NA': float(row['NA']), - 'tube_lens_f_mm': float(row['tube_lens_f_mm']) + objectives[row["name"]] = { + "magnification": float(row["magnification"]), + "NA": float(row["NA"]), + "tube_lens_f_mm": float(row["tube_lens_f_mm"]), } return objectives + def read_sample_formats_csv(file_path): sample_formats = {} - with open(file_path, 'r') as csvfile: + with open(file_path, "r") as csvfile: reader = csv.DictReader(csvfile) for row in reader: - format_ = str(row['format']) + format_ = str(row["format"]) format_key = f"{format_} well plate" if format_.isdigit() else format_ sample_formats[format_key] = { - 'a1_x_mm': float(row['a1_x_mm']), - 'a1_y_mm': float(row['a1_y_mm']), - 'a1_x_pixel': int(row['a1_x_pixel']), - 'a1_y_pixel': int(row['a1_y_pixel']), - 'well_size_mm': float(row['well_size_mm']), - 'well_spacing_mm': float(row['well_spacing_mm']), - 'number_of_skip': int(row['number_of_skip']), - 'rows': int(row['rows']), - 'cols': int(row['cols']) + "a1_x_mm": float(row["a1_x_mm"]), + "a1_y_mm": float(row["a1_y_mm"]), + "a1_x_pixel": int(row["a1_x_pixel"]), + "a1_y_pixel": int(row["a1_y_pixel"]), + "well_size_mm": float(row["well_size_mm"]), + "well_spacing_mm": float(row["well_spacing_mm"]), + "number_of_skip": int(row["number_of_skip"]), + "rows": int(row["rows"]), + "cols": int(row["cols"]), } return sample_formats + def load_formats(): """Load formats, prioritizing cache for sample formats.""" - cache_path = 'cache' - default_path = 'objective_and_sample_formats' + cache_path = "cache" + default_path = "objective_and_sample_formats" # Load objectives (from default location) - objectives = read_objectives_csv(os.path.join(default_path, 'objectives.csv')) + objectives = read_objectives_csv(os.path.join(default_path, "objectives.csv")) # Try cache first for sample formats, fall back to default if not found - cached_formats_path = os.path.join(cache_path, 'sample_formats.csv') - default_formats_path = os.path.join(default_path, 'sample_formats.csv') + cached_formats_path = os.path.join(cache_path, "sample_formats.csv") + default_formats_path = os.path.join(default_path, "sample_formats.csv") if os.path.exists(cached_formats_path): - print('Using cached sample formats') + print("Using cached sample formats") sample_formats = read_sample_formats_csv(cached_formats_path) else: - print('Using default sample formats') + print("Using default sample formats") sample_formats = read_sample_formats_csv(default_formats_path) return objectives, sample_formats -OBJECTIVES_CSV_PATH = 'objectives.csv' -SAMPLE_FORMATS_CSV_PATH = 'sample_formats.csv' +OBJECTIVES_CSV_PATH = "objectives.csv" +SAMPLE_FORMATS_CSV_PATH = "sample_formats.csv" OBJECTIVES, WELLPLATE_FORMAT_SETTINGS = load_formats() @@ -689,7 +723,7 @@ def load_formats(): CACHED_CONFIG_FILE_PATH = None # Piezo configuration items -Z_MOTOR_CONFIG = "STEPPER" # "STEPPER", "STEPPER + PIEZO", "PIEZO", "LINEAR" +Z_MOTOR_CONFIG = "STEPPER" # "STEPPER", "STEPPER + PIEZO", "PIEZO", "LINEAR" ENABLE_OBJECTIVE_PIEZO = "PIEZO" in Z_MOTOR_CONFIG # the value of OBJECTIVE_PIEZO_CONTROL_VOLTAGE_RANGE is 2.5 or 5 @@ -707,24 +741,24 @@ def load_formats(): AWB_RATIOS_B = 1.4141 try: - with open("cache/config_file_path.txt", 'r') as file: + with open("cache/config_file_path.txt", "r") as file: for line in file: CACHED_CONFIG_FILE_PATH = line break except FileNotFoundError: CACHED_CONFIG_FILE_PATH = None -config_files = glob.glob('.' + '/' + 'configuration*.ini') +config_files = glob.glob("." + "/" + "configuration*.ini") if config_files: if len(config_files) > 1: if CACHED_CONFIG_FILE_PATH in config_files: - log.info(f'defaulting to last cached config file at \'{CACHED_CONFIG_FILE_PATH}\'') + log.info(f"defaulting to last cached config file at '{CACHED_CONFIG_FILE_PATH}'") config_files = [CACHED_CONFIG_FILE_PATH] else: - log.error('multiple machine configuration files found, the program will exit') + log.error("multiple machine configuration files found, the program will exit") sys.exit(1) - log.info('load machine-specific configuration') - #exec(open(config_files[0]).read()) + log.info("load machine-specific configuration") + # exec(open(config_files[0]).read()) cfp = ConfigParser() cfp.read(config_files[0]) var_items = list(locals().keys()) @@ -734,7 +768,7 @@ def load_formats(): varnamelower = var_name.lower() if varnamelower not in cfp.options("GENERAL"): continue - value = cfp.get("GENERAL",varnamelower) + value = cfp.get("GENERAL", varnamelower) actualvalue = conf_attribute_reader(value) locals()[var_name] = actualvalue for classkey in var_items: @@ -748,43 +782,47 @@ def load_formats(): if type(locals()[classkey]) is not type: continue myclass = locals()[classkey] - populate_class_from_dict(myclass,pop_items) - - with open("cache/config_file_path.txt", 'w') as file: + populate_class_from_dict(myclass, pop_items) + + with open("cache/config_file_path.txt", "w") as file: file.write(config_files[0]) CACHED_CONFIG_FILE_PATH = config_files[0] else: - log.warning('configuration*.ini file not found, defaulting to legacy configuration') - config_files = glob.glob('.' + '/' + 'configuration*.txt') + log.warning("configuration*.ini file not found, defaulting to legacy configuration") + config_files = glob.glob("." + "/" + "configuration*.txt") if config_files: if len(config_files) > 1: - log.error('multiple machine configuration files found, the program will exit') + log.error("multiple machine configuration files found, the program will exit") sys.exit(1) - log.info('load machine-specific configuration') + log.info("load machine-specific configuration") exec(open(config_files[0]).read()) else: - log.error('machine-specific configuration not present, the program will exit') + log.error("machine-specific configuration not present, the program will exit") sys.exit(1) try: - with open("cache/objective_and_sample_format.txt", 'r') as f: + with open("cache/objective_and_sample_format.txt", "r") as f: cached_settings = json.load(f) - DEFAULT_OBJECTIVE = cached_settings.get('objective') if cached_settings.get('objective') in OBJECTIVES else '20x' - WELLPLATE_FORMAT = str(cached_settings.get('wellplate_format')) - WELLPLATE_FORMAT = WELLPLATE_FORMAT + ' well plate' if WELLPLATE_FORMAT.isdigit() else WELLPLATE_FORMAT + DEFAULT_OBJECTIVE = ( + cached_settings.get("objective") if cached_settings.get("objective") in OBJECTIVES else "20x" + ) + WELLPLATE_FORMAT = str(cached_settings.get("wellplate_format")) + WELLPLATE_FORMAT = WELLPLATE_FORMAT + " well plate" if WELLPLATE_FORMAT.isdigit() else WELLPLATE_FORMAT if WELLPLATE_FORMAT not in WELLPLATE_FORMAT_SETTINGS: - WELLPLATE_FORMAT = '96 well plate' + WELLPLATE_FORMAT = "96 well plate" except (FileNotFoundError, json.JSONDecodeError): - DEFAULT_OBJECTIVE = '20x' - WELLPLATE_FORMAT = '96 well plate' - -NUMBER_OF_SKIP = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['number_of_skip'] # num rows/cols to skip on wellplate edge -WELL_SIZE_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['well_size_mm'] -WELL_SPACING_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['well_spacing_mm'] -A1_X_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['a1_x_mm'] # measured stage position - to update -A1_Y_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['a1_y_mm'] # measured stage position - to update -A1_X_PIXEL = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['a1_x_pixel'] # coordinate on the png -A1_Y_PIXEL = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]['a1_y_pixel'] # coordinate on the png + DEFAULT_OBJECTIVE = "20x" + WELLPLATE_FORMAT = "96 well plate" + +NUMBER_OF_SKIP = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT][ + "number_of_skip" +] # num rows/cols to skip on wellplate edge +WELL_SIZE_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["well_size_mm"] +WELL_SPACING_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["well_spacing_mm"] +A1_X_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["a1_x_mm"] # measured stage position - to update +A1_Y_MM = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["a1_y_mm"] # measured stage position - to update +A1_X_PIXEL = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["a1_x_pixel"] # coordinate on the png +A1_Y_PIXEL = WELLPLATE_FORMAT_SETTINGS[WELLPLATE_FORMAT]["a1_y_pixel"] # coordinate on the png ########################################################## ##### end of loading machine specific configurations ##### @@ -796,7 +834,7 @@ def load_formats(): # saving path if not (DEFAULT_SAVING_PATH.startswith(str(Path.home()))): - DEFAULT_SAVING_PATH = str(Path.home())+"/"+DEFAULT_SAVING_PATH.strip("/") + DEFAULT_SAVING_PATH = str(Path.home()) + "/" + DEFAULT_SAVING_PATH.strip("/") # limit switch X_HOME_SWITCH_POLARITY = LIMIT_SWITCH_POLARITY.X_HOME @@ -806,7 +844,7 @@ def load_formats(): # home safety margin with (um) unit X_HOME_SAFETY_MARGIN_UM = 50 Y_HOME_SAFETY_MARGIN_UM = 50 -Z_HOME_SAFETY_MARGIN_UM = 600 +Z_HOME_SAFETY_MARGIN_UM = 600 if ENABLE_TRACKING: DEFAULT_DISPLAY_CROP = Tracking.DEFAULT_DISPLAY_CROP diff --git a/software/control/_multipoint_custom_script_entry.py b/software/control/_multipoint_custom_script_entry.py index 5f6ae95bb..4da593bab 100644 --- a/software/control/_multipoint_custom_script_entry.py +++ b/software/control/_multipoint_custom_script_entry.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -15,70 +16,107 @@ from control._def import * import control.utils as utils -def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coordinate_id,coordiante_name,i,j): - - print( 'in custom script; t ' + str(multiPointWorker.time_point) + ', location ' + coordiante_name + ': ' + str(i) + '_' + str(j) ) + +def multipoint_custom_script_entry(multiPointWorker, time_point, current_path, coordinate_id, coordiante_name, i, j): + + print( + "in custom script; t " + + str(multiPointWorker.time_point) + + ", location " + + coordiante_name + + ": " + + str(i) + + "_" + + str(j) + ) # autofocus # if z location is included in the scan coordinates - if multiPointWorker.use_scan_coordinates and multiPointWorker.scan_coordinates_mm.shape[1] == 3 : + if multiPointWorker.use_scan_coordinates and multiPointWorker.scan_coordinates_mm.shape[1] == 3: if multiPointWorker.do_autofocus: - + # autofocus for every FOV in the first scan and update the coordinates if multiPointWorker.time_point == 0: configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() - multiPointWorker.scan_coordinates_mm[coordinate_id,2] = multiPointWorker.navigationController.z_pos_mm + multiPointWorker.scan_coordinates_mm[coordinate_id, 2] = multiPointWorker.navigationController.z_pos_mm # in subsequent scans, autofocus at the first FOV and offset the rest else: if coordinate_id == 0: - z0 = multiPointWorker.scan_coordinates_mm[0,2] + z0 = multiPointWorker.scan_coordinates_mm[0, 2] configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() - multiPointWorker.scan_coordinates_mm[0,2] = multiPointWorker.navigationController.z_pos_mm - offset = multiPointWorker.scan_coordinates_mm[0,2] - z0 - print('offset is ' + str(offset)) - multiPointWorker.scan_coordinates_mm[1:,2] = multiPointWorker.scan_coordinates_mm[1:,2] + offset + multiPointWorker.scan_coordinates_mm[0, 2] = multiPointWorker.navigationController.z_pos_mm + offset = multiPointWorker.scan_coordinates_mm[0, 2] - z0 + print("offset is " + str(offset)) + multiPointWorker.scan_coordinates_mm[1:, 2] = multiPointWorker.scan_coordinates_mm[1:, 2] + offset else: pass - # if z location is not included in the scan coordinates else: if multiPointWorker.do_reflection_af == False: # perform AF only if when not taking z stack or doing z stack from center - if ( (multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == 'FROM CENTER' ) and (multiPointWorker.do_autofocus) and (multiPointWorker.FOV_counter%Acquisition.NUMBER_OF_FOVS_PER_AF==0): - # temporary: replace the above line with the line below to AF every FOV - # if (multiPointWorker.NZ == 1) and (multiPointWorker.do_autofocus): + if ( + ((multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == "FROM CENTER") + and (multiPointWorker.do_autofocus) + and (multiPointWorker.FOV_counter % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) + ): + # temporary: replace the above line with the line below to AF every FOV + # if (multiPointWorker.NZ == 1) and (multiPointWorker.do_autofocus): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() else: - # initialize laser autofocus - if multiPointWorker.reflection_af_initialized==False: + # initialize laser autofocus + if multiPointWorker.reflection_af_initialized == False: # initialize the reflection AF multiPointWorker.microscope.laserAutofocusController.initialize_auto() multiPointWorker.reflection_af_initialized = True # do contrast AF for the first FOV - if multiPointWorker.do_autofocus and ( (multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == 'FROM CENTER' ) : + if multiPointWorker.do_autofocus and ((multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == "FROM CENTER"): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() @@ -86,36 +124,47 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.microscope.laserAutofocusController.set_reference() else: multiPointWorker.microscope.laserAutofocusController.move_to_target(0) - multiPointWorker.microscope.laserAutofocusController.move_to_target(0) # for stepper in open loop mode, repeat the operation to counter backlash + multiPointWorker.microscope.laserAutofocusController.move_to_target( + 0 + ) # for stepper in open loop mode, repeat the operation to counter backlash - if (multiPointWorker.NZ > 1): + if multiPointWorker.NZ > 1: # move to bottom of the z stack - if Z_STACKING_CONFIG == 'FROM CENTER': - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2)) + if Z_STACKING_CONFIG == "FROM CENTER": + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # maneuver for achiving uniform step size and repeatability when using open-loop control multiPointWorker.navigationController.move_z_usteps(-160) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(160) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # z-stack for k in range(multiPointWorker.NZ): - - file_ID = coordiante_name + str(i) + '_' + str(j if multiPointWorker.x_scan_direction==1 else multiPointWorker.NX-1-j) + '_' + str(k) + + file_ID = ( + coordiante_name + + str(i) + + "_" + + str(j if multiPointWorker.x_scan_direction == 1 else multiPointWorker.NX - 1 - j) + + "_" + + str(k) + ) # metadata = dict(x = multiPointWorker.navigationController.x_pos_mm, y = multiPointWorker.navigationController.y_pos_mm, z = multiPointWorker.navigationController.z_pos_mm) # metadata = json.dumps(metadata) # iterate through selected modes for config in multiPointWorker.selected_configurations: - if 'USB Spectrometer' not in config.name: + if "USB Spectrometer" not in config.name: - if time_point%10 != 0: + if time_point % 10 != 0: - if 'Fluorescence' in config.name: + if "Fluorescence" in config.name: # only do fluorescence every 10th timepoint continue @@ -128,65 +177,89 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.wait_till_operation_is_completed() multiPointWorker.camera.send_trigger() elif multiPointWorker.liveController.trigger_mode == TriggerMode.HARDWARE: - multiPointWorker.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=multiPointWorker.camera.exposure_time*1000) + multiPointWorker.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=multiPointWorker.camera.exposure_time * 1000 + ) # read camera frame image = multiPointWorker.camera.read_frame() if image is None: - print('multiPointWorker.camera.read_frame() returned None') + print("multiPointWorker.camera.read_frame() returned None") continue # tunr of the illumination if using software trigger if multiPointWorker.liveController.trigger_mode == TriggerMode.SOFTWARE: multiPointWorker.liveController.turn_off_illumination() # process the image - @@@ to move to camera - image = utils.crop_image(image,multiPointWorker.crop_width,multiPointWorker.crop_height) - image = utils.rotate_and_flip_image(image,rotate_image_angle=multiPointWorker.camera.rotate_image_angle,flip_image=multiPointWorker.camera.flip_image) + image = utils.crop_image(image, multiPointWorker.crop_width, multiPointWorker.crop_height) + image = utils.rotate_and_flip_image( + image, + rotate_image_angle=multiPointWorker.camera.rotate_image_angle, + flip_image=multiPointWorker.camera.flip_image, + ) # multiPointWorker.image_to_display.emit(cv2.resize(image,(round(multiPointWorker.crop_width*multiPointWorker.display_resolution_scaling), round(multiPointWorker.crop_height*multiPointWorker.display_resolution_scaling)),cv2.INTER_LINEAR)) - image_to_display = utils.crop_image(image,round(multiPointWorker.crop_width*multiPointWorker.display_resolution_scaling), round(multiPointWorker.crop_height*multiPointWorker.display_resolution_scaling)) + image_to_display = utils.crop_image( + image, + round(multiPointWorker.crop_width * multiPointWorker.display_resolution_scaling), + round(multiPointWorker.crop_height * multiPointWorker.display_resolution_scaling), + ) multiPointWorker.image_to_display.emit(image_to_display) - multiPointWorker.image_to_display_multi.emit(image_to_display,config.illumination_source) + multiPointWorker.image_to_display_multi.emit(image_to_display, config.illumination_source) if image.dtype == np.uint16: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.tiff') + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + ".tiff" + ) if multiPointWorker.camera.is_color: - if 'BF LED matrix' in config.name: - if MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': - image = image[:,:,1] - iio.imwrite(saving_path,image) + if "BF LED matrix" in config.name: + if MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": + image = image[:, :, 1] + iio.imwrite(saving_path, image) else: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.' + Acquisition.IMAGE_FORMAT) + saving_path = os.path.join( + current_path, + file_ID + "_" + str(config.name).replace(" ", "_") + "." + Acquisition.IMAGE_FORMAT, + ) if multiPointWorker.camera.is_color: - if 'BF LED matrix' in config.name: - if MULTIPOINT_BF_SAVING_OPTION == 'Raw': - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - elif MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': - image = image[:,:,1] + if "BF LED matrix" in config.name: + if MULTIPOINT_BF_SAVING_OPTION == "Raw": + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + elif MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": + image = image[:, :, 1] else: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) QApplication.processEvents() - + else: if multiPointWorker.usb_spectrometer != None: for l in range(N_SPECTRUM_PER_POINT): data = multiPointWorker.usb_spectrometer.read_spectrum() multiPointWorker.spectrum_to_display.emit(data) - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '_' + str(l) + '.csv') - np.savetxt(saving_path,data,delimiter=',') + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + "_" + str(l) + ".csv" + ) + np.savetxt(saving_path, data, delimiter=",") # add the coordinate of the current location - new_row = pd.DataFrame({'i':[i],'j':[multiPointWorker.NX-1-j],'k':[k], - 'x (mm)':[multiPointWorker.navigationController.x_pos_mm], - 'y (mm)':[multiPointWorker.navigationController.y_pos_mm], - 'z (um)':[multiPointWorker.navigationController.z_pos_mm*1000]}, - ) + new_row = pd.DataFrame( + { + "i": [i], + "j": [multiPointWorker.NX - 1 - j], + "k": [k], + "x (mm)": [multiPointWorker.navigationController.x_pos_mm], + "y (mm)": [multiPointWorker.navigationController.y_pos_mm], + "z (um)": [multiPointWorker.navigationController.z_pos_mm * 1000], + }, + ) multiPointWorker.coordinates_pd = pd.concat([multiPointWorker.coordinates_pd, new_row], ignore_index=True) - # register the current fov in the navigationViewer - multiPointWorker.signal_register_current_fov.emit(multiPointWorker.navigationController.x_pos_mm,multiPointWorker.navigationController.y_pos_mm) + # register the current fov in the navigationViewer + multiPointWorker.signal_register_current_fov.emit( + multiPointWorker.navigationController.x_pos_mm, multiPointWorker.navigationController.y_pos_mm + ) # check if the acquisition should be aborted if multiPointWorker.multiPointController.abort_acqusition_requested: @@ -196,15 +269,19 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.navigationController.move_y_usteps(-multiPointWorker.dy_usteps) multiPointWorker.wait_till_operation_is_completed() if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.dz_usteps-_usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.dz_usteps - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() else: multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.dz_usteps) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.coordinates_pd.to_csv(os.path.join(current_path,'coordinates.csv'),index=False,header=True) + multiPointWorker.coordinates_pd.to_csv( + os.path.join(current_path, "coordinates.csv"), index=False, header=True + ) multiPointWorker.navigationController.enable_joystick_button_action = True return @@ -213,35 +290,52 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor if k < multiPointWorker.NZ - 1: multiPointWorker.navigationController.move_z_usteps(multiPointWorker.deltaZ_usteps) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) multiPointWorker.dz_usteps = multiPointWorker.dz_usteps + multiPointWorker.deltaZ_usteps - + if multiPointWorker.NZ > 1: # move z back - if Z_STACKING_CONFIG == 'FROM CENTER': + if Z_STACKING_CONFIG == "FROM CENTER": if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps( -multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2) - _usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() else: - multiPointWorker.navigationController.move_z_usteps( -multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2)) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2) + multiPointWorker.dz_usteps = ( + multiPointWorker.dz_usteps + - multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) else: if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) - _usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() else: - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1)) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + ) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps * ( + multiPointWorker.NZ - 1 + ) # update FOV counter multiPointWorker.FOV_counter = multiPointWorker.FOV_counter + 1 diff --git a/software/control/camera.py b/software/control/camera.py index 99f3071df..a74486364 100644 --- a/software/control/camera.py +++ b/software/control/camera.py @@ -2,13 +2,15 @@ import cv2 import time import numpy as np + try: import control.gxipy as gx except: - print('gxipy import error') + print("gxipy import error") from control._def import * + def get_sn_by_model(model_name): try: device_manager = gx.DeviceManager() @@ -17,13 +19,14 @@ def get_sn_by_model(model_name): device_num = 0 if device_num > 0: for i in range(device_num): - if device_info_list[i]['model_name'] == model_name: - return device_info_list[i]['sn'] - return None # return None if no device with the specified model_name is connected + if device_info_list[i]["model_name"] == model_name: + return device_info_list[i]["sn"] + return None # return None if no device with the specified model_name is connected + class Camera(object): - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None): + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): # many to be purged self.sn = sn @@ -40,7 +43,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image - self.exposure_time = 1 # unit: ms + self.exposure_time = 1 # unit: ms self.analog_gain = 0 self.frame_ID = -1 self.frame_ID_software = -1 @@ -62,22 +65,24 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None self.pixel_size_byte = 1 - # below are values for IMX226 (MER2-1220-32U3M) - to make configurable + # below are values for IMX226 (MER2-1220-32U3M) - to make configurable self.row_period_us = 10 self.row_numbers = 3036 self.exposure_delay_us_8bit = 650 - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) - self.pixel_format = None # use the default pixel format + self.pixel_format = None # use the default pixel format - self.is_live = False # this determines whether a new frame received will be handled in the streamHandler + self.is_live = False # this determines whether a new frame received will be handled in the streamHandler # mainly for discarding the last frame received after stop_live() is called, where illumination is being turned off during exposure - def open(self,index=0): + def open(self, index=0): (device_num, self.device_info_list) = self.device_manager.update_device_list() if device_num == 0: - raise RuntimeError('Could not find any USB camera devices!') + raise RuntimeError("Could not find any USB camera devices!") if self.sn is None: self.device_index = index self.camera = self.device_manager.open_device_by_index(index + 1) @@ -108,7 +113,7 @@ def open(self,index=0): self.OffsetX = self.camera.OffsetX.get() self.OffsetY = self.camera.OffsetY.get() - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -121,7 +126,7 @@ def enable_callback(self): was_streaming = False # enable callback user_param = None - self.camera.register_capture_callback(user_param,self._on_frame_callback) + self.camera.register_capture_callback(user_param, self._on_frame_callback) self.callback_is_enabled = True # resume streaming if it was on if was_streaming: @@ -147,20 +152,20 @@ def disable_callback(self): else: pass - def open_by_sn(self,sn): + def open_by_sn(self, sn): (device_num, self.device_info_list) = self.device_manager.update_device_list() if device_num == 0: - raise RuntimeError('Could not find any USB camera devices!') + raise RuntimeError("Could not find any USB camera devices!") self.camera = self.device_manager.open_device_by_sn(sn) self.is_color = self.camera.PixelColorFilter.is_implemented() self._update_image_improvement_params() - ''' + """ if self.is_color is True: self.camera.register_capture_callback(_on_color_frame_callback) else: self.camera.register_capture_callback(_on_frame_callback) - ''' + """ def close(self): self.camera.close_device() @@ -174,8 +179,8 @@ def close(self): self.last_converted_image = None self.last_numpy_image = None - def set_exposure_time(self,exposure_time): - use_strobe = (self.trigger_mode == TriggerMode.HARDWARE) # true if using hardware trigger + def set_exposure_time(self, exposure_time): + use_strobe = self.trigger_mode == TriggerMode.HARDWARE # true if using hardware trigger if use_strobe == False or self.is_global_shutter: self.exposure_time = exposure_time self.camera.ExposureTime.set(exposure_time * 1000) @@ -183,18 +188,28 @@ def set_exposure_time(self,exposure_time): # set the camera exposure time such that the active exposure time (illumination on time) is the desired value self.exposure_time = exposure_time # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure - camera_exposure_time = self.exposure_delay_us + self.exposure_time*1000 + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + 500 # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure + camera_exposure_time = ( + self.exposure_delay_us + + self.exposure_time * 1000 + + self.row_period_us * self.pixel_size_byte * (self.row_numbers - 1) + + 500 + ) # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure self.camera.ExposureTime.set(camera_exposure_time) def update_camera_exposure_time(self): - use_strobe = (self.trigger_mode == TriggerMode.HARDWARE) # true if using hardware trigger + use_strobe = self.trigger_mode == TriggerMode.HARDWARE # true if using hardware trigger if use_strobe == False or self.is_global_shutter: self.camera.ExposureTime.set(self.exposure_time * 1000) else: - camera_exposure_time = self.exposure_delay_us + self.exposure_time*1000 + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + 500 # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure + camera_exposure_time = ( + self.exposure_delay_us + + self.exposure_time * 1000 + + self.row_period_us * self.pixel_size_byte * (self.row_numbers - 1) + + 500 + ) # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure self.camera.ExposureTime.set(camera_exposure_time) - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): self.analog_gain = analog_gain self.camera.Gain.set(analog_gain) @@ -234,10 +249,10 @@ def get_balance_white_auto(self): def get_is_color(self): return self.is_color - def set_reverse_x(self,value): + def set_reverse_x(self, value): self.camera.ReverseX.set(value) - def set_reverse_y(self,value): + def set_reverse_y(self, value): self.camera.ReverseY.set(value) def start_streaming(self): @@ -248,7 +263,7 @@ def stop_streaming(self): self.camera.stream_off() self.is_streaming = False - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): if self.is_streaming == True: was_streaming = True self.stop_streaming() @@ -256,25 +271,25 @@ def set_pixel_format(self,pixel_format): was_streaming = False if self.camera.PixelFormat.is_implemented() and self.camera.PixelFormat.is_writable(): - if pixel_format == 'MONO8': + if pixel_format == "MONO8": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.MONO8) self.pixel_size_byte = 1 - if pixel_format == 'MONO10': + if pixel_format == "MONO10": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.MONO10) self.pixel_size_byte = 1 - if pixel_format == 'MONO12': + if pixel_format == "MONO12": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.MONO12) self.pixel_size_byte = 2 - if pixel_format == 'MONO14': + if pixel_format == "MONO14": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.MONO14) self.pixel_size_byte = 2 - if pixel_format == 'MONO16': + if pixel_format == "MONO16": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.MONO16) self.pixel_size_byte = 2 - if pixel_format == 'BAYER_RG8': + if pixel_format == "BAYER_RG8": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.BAYER_RG8) self.pixel_size_byte = 1 - if pixel_format == 'BAYER_RG12': + if pixel_format == "BAYER_RG12": self.camera.PixelFormat.set(gx.GxPixelFormatEntry.BAYER_RG12) self.pixel_size_byte = 2 self.pixel_format = pixel_format @@ -282,11 +297,13 @@ def set_pixel_format(self,pixel_format): print("pixel format is not implemented or not writable") if was_streaming: - self.start_streaming() + self.start_streaming() # update the exposure delay and strobe delay - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) def set_continuous_acquisition(self): self.camera.TriggerMode.set(gx.GxSwitchEntry.OFF) @@ -301,7 +318,7 @@ def set_software_triggered_acquisition(self): def set_hardware_triggered_acquisition(self): self.camera.TriggerMode.set(gx.GxSwitchEntry.ON) - self.camera.TriggerSource.set(gx.GxTriggerSourceEntry.LINE2) # LINE0 requires 7 mA min + self.camera.TriggerSource.set(gx.GxTriggerSourceEntry.LINE2) # LINE0 requires 7 mA min # self.camera.TriggerSource.set(gx.GxTriggerActivationEntry.RISING_EDGE) self.frame_ID_offset_hardware_trigger = None self.trigger_mode = TriggerMode.HARDWARE @@ -311,18 +328,18 @@ def send_trigger(self): if self.is_streaming: self.camera.TriggerSoftware.send_command() else: - print('trigger not sent - camera is not streaming') + print("trigger not sent - camera is not streaming") def read_frame(self): raw_image = self.camera.data_stream[self.device_index].get_image() if self.is_color: rgb_image = raw_image.convert("RGB") numpy_image = rgb_image.get_numpy_array() - if self.pixel_format == 'BAYER_RG12': + if self.pixel_format == "BAYER_RG12": numpy_image = numpy_image << 4 else: numpy_image = raw_image.get_numpy_array() - if self.pixel_format == 'MONO12': + if self.pixel_format == "MONO12": numpy_image = numpy_image << 4 # self.current_frame = numpy_image return numpy_image @@ -335,16 +352,16 @@ def _on_frame_callback(self, user_param, raw_image): print("Got an incomplete frame") return if self.image_locked: - print('last image is still being processed, a frame is dropped') + print("last image is still being processed, a frame is dropped") return if self.is_color: rgb_image = raw_image.convert("RGB") numpy_image = rgb_image.get_numpy_array() - if self.pixel_format == 'BAYER_RG12': + if self.pixel_format == "BAYER_RG12": numpy_image = numpy_image << 4 else: numpy_image = raw_image.get_numpy_array() - if self.pixel_format == 'MONO12': + if self.pixel_format == "MONO12": numpy_image = numpy_image << 4 if numpy_image is None: return @@ -360,8 +377,8 @@ def _on_frame_callback(self, user_param, raw_image): # self.frameID = self.frameID + 1 # print(self.frameID) - - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): # stop streaming if streaming is on if self.is_streaming == True: @@ -429,9 +446,10 @@ def set_line3_to_exposure_active(self): self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) self.camera.LineSource.set(gx.GxLineSourceEntry.EXPOSURE_ACTIVE) + class Camera_Simulation(object): - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None): + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): # many to be purged self.sn = sn self.is_global_shutter = is_global_shutter @@ -468,14 +486,16 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None self.pixel_size_byte = 1 - # below are values for IMX226 (MER2-1220-32U3M) - to make configurable + # below are values for IMX226 (MER2-1220-32U3M) - to make configurable self.row_period_us = 10 self.row_numbers = 3036 self.exposure_delay_us_8bit = 650 - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) - self.pixel_format = 'MONO8' + self.pixel_format = "MONO8" self.is_live = False @@ -490,10 +510,10 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.new_image_callback_external = None - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -502,19 +522,19 @@ def enable_callback(self): def disable_callback(self): self.callback_is_enabled = False - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): pass - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): pass def update_camera_exposure_time(self): pass - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): pass def get_awb_ratios(self): @@ -538,7 +558,7 @@ def start_streaming(self): def stop_streaming(self): pass - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): self.pixel_format = pixel_format print(pixel_format) self.frame_ID = 0 @@ -556,19 +576,25 @@ def send_trigger(self): self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - if self.pixel_format == 'MONO8': - self.current_frame = np.random.randint(255,size=(self.Height,self.Width),dtype=np.uint8) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200 - elif self.pixel_format == 'MONO12': - self.current_frame = np.random.randint(4095,size=(self.Height,self.Width),dtype=np.uint16) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200*16 + if self.pixel_format == "MONO8": + self.current_frame = np.random.randint(255, size=(self.Height, self.Width), dtype=np.uint8) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = 200 + elif self.pixel_format == "MONO12": + self.current_frame = np.random.randint(4095, size=(self.Height, self.Width), dtype=np.uint16) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = (200 * 16) self.current_frame = self.current_frame << 4 - elif self.pixel_format == 'MONO16': - self.current_frame = np.random.randint(65535,size=(self.Height,self.Width),dtype=np.uint16) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200*256 + elif self.pixel_format == "MONO16": + self.current_frame = np.random.randint(65535, size=(self.Height, self.Width), dtype=np.uint16) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = (200 * 256) else: - self.current_frame = np.roll(self.current_frame,10,axis=0) - pass + self.current_frame = np.roll(self.current_frame, 10, axis=0) + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None and self.callback_is_enabled: self.new_image_callback_external(self) @@ -579,7 +605,7 @@ def read_frame(self): def _on_frame_callback(self, user_param, raw_image): pass - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): pass def reset_camera_acquisition_counter(self): diff --git a/software/control/camera_TIS.py b/software/control/camera_TIS.py index 94df33ca6..1465403a6 100644 --- a/software/control/camera_TIS.py +++ b/software/control/camera_TIS.py @@ -2,29 +2,32 @@ from collections import namedtuple from time import sleep import sys -import time #@@@ +import time # @@@ import numpy as np from scipy import misc import cv2 import squid.logging + log = squid.logging.get_logger(__name__) try: import gi + gi.require_version("Gst", "1.0") gi.require_version("Tcam", "0.1") from gi.repository import Tcam, Gst, GLib, GObject except ImportError: - log.error('gi import error') + log.error("gi import error") # TODO(imo): Propagate error in some way and handle DeviceInfo = namedtuple("DeviceInfo", "status name identifier connection_type") CameraProperty = namedtuple("CameraProperty", "status value min max default step type flags category group") + class Camera(object): - def __init__(self,sn=None,width=1920,height=1080,framerate=30,color=False): + def __init__(self, sn=None, width=1920, height=1080, framerate=30, color=False): self.log = squid.logging.get_logger(self.__class__.__name__) Gst.init(sys.argv) self.height = height @@ -50,15 +53,27 @@ def __init__(self,sn=None,width=1920,height=1080,framerate=30,color=False): self.callback_was_enabled_before_multipoint = False format = "BGRx" - if(color == False): - format="GRAY8" - - if(framerate == 2500000): - p = 'tcambin serial="%s" name=source ! video/x-raw,format=%s,width=%d,height=%d,framerate=%d/10593' % (sn,format,width,height,framerate,) + if color == False: + format = "GRAY8" + + if framerate == 2500000: + p = 'tcambin serial="%s" name=source ! video/x-raw,format=%s,width=%d,height=%d,framerate=%d/10593' % ( + sn, + format, + width, + height, + framerate, + ) else: - p = 'tcambin serial="%s" name=source ! video/x-raw,format=%s,width=%d,height=%d,framerate=%d/1' % (sn,format,width,height,framerate,) + p = 'tcambin serial="%s" name=source ! video/x-raw,format=%s,width=%d,height=%d,framerate=%d/1' % ( + sn, + format, + width, + height, + framerate, + ) - p += ' ! videoconvert ! appsink name=sink' + p += " ! videoconvert ! appsink name=sink" self.log.info(p) try: @@ -74,35 +89,35 @@ def __init__(self,sn=None,width=1920,height=1080,framerate=30,color=False): # Query a pointer to the appsink, so we can assign the callback function. self.appsink = self.pipeline.get_by_name("sink") - self.appsink.set_property("max-buffers",5) + self.appsink.set_property("max-buffers", 5) self.appsink.set_property("drop", True) self.appsink.set_property("emit-signals", True) - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): - self.appsink.connect('new-sample', self._on_new_buffer) + self.appsink.connect("new-sample", self._on_new_buffer) def disable_callback(self): pass - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): self.stop_streaming() - def set_exposure_time(self,exposure_time): - self._set_property('Exposure Auto',False) - self._set_property('Exposure Time (us)',int(exposure_time*1000)) + def set_exposure_time(self, exposure_time): + self._set_property("Exposure Auto", False) + self._set_property("Exposure Time (us)", int(exposure_time * 1000)) - def set_analog_gain(self,analog_gain): - self._set_property('Gain Auto',False) - self._set_property('Gain',int(analog_gain)) + def set_analog_gain(self, analog_gain): + self._set_property("Gain Auto", False) + self._set_property("Gain", int(analog_gain)) def get_awb_ratios(self): pass @@ -127,15 +142,15 @@ def stop_streaming(self): self.is_streaming = False def set_continuous_acquisition(self): - self._set_property('Trigger Mode', False) + self._set_property("Trigger Mode", False) def set_software_triggered_acquisition(self): pass def set_hardware_triggered_acquisition(self): - self._set_property('Trigger Mode', True) - self._set_property('Trigger Polarity', 'RisingEdge') - self._set_property('Trigger Delay (us)', 0) + self._set_property("Trigger Mode", True) + self._set_property("Trigger Polarity", "RisingEdge") + self._set_property("Trigger Delay (us)", 0) def send_trigger(self): pass @@ -147,19 +162,19 @@ def _on_new_buffer(self, appsink): # Function that is called when a new sample from camera is available self.newsample = True if self.image_locked: - self.log.error('last image is still being processed, a frame is dropped') + self.log.error("last image is still being processed, a frame is dropped") # TODO(imo): Propagate error in some way and handle return if self.samplelocked is False: self.samplelocked = True try: - self.sample = self.appsink.get_property('last-sample') + self.sample = self.appsink.get_property("last-sample") self._gstbuffer_to_opencv() self.samplelocked = False self.newsample = False # gotimage reflects if a new image was triggered self.gotimage = True - self.frame_ID = self.frame_ID + 1 # @@@ read frame ID from the camera + self.frame_ID = self.frame_ID + 1 # @@@ read frame ID from the camera self.timestamp = time.time() if self.new_image_callback_external is not None: self.new_image_callback_external(self) @@ -179,7 +194,7 @@ def _get_property(self, property_name): def _set_property(self, property_name, value): try: - self.log.info('setting ' + property_name + 'to ' + str(value)) + self.log.info("setting " + property_name + "to " + str(value)) self.source.set_tcam_property(property_name, GObject.Value(type(value), value)) except GLib.Error as error: self.log.error(f"Error set Property {property_name}", error) @@ -189,24 +204,26 @@ def _gstbuffer_to_opencv(self): # Sample code from https://gist.github.com/cbenhagen/76b24573fa63e7492fb6#file-gst-appsink-opencv-py-L34 buf = self.sample.get_buffer() caps = self.sample.get_caps() - bpp = 4; - if caps.get_structure(0).get_value('format') == "BGRx": - bpp = 4; + bpp = 4 + if caps.get_structure(0).get_value("format") == "BGRx": + bpp = 4 - if caps.get_structure(0).get_value('format') == "GRAY8": - bpp = 1; + if caps.get_structure(0).get_value("format") == "GRAY8": + bpp = 1 self.current_frame = numpy.ndarray( - (caps.get_structure(0).get_value('height'), - caps.get_structure(0).get_value('width'), - bpp), + (caps.get_structure(0).get_value("height"), caps.get_structure(0).get_value("width"), bpp), buffer=buf.extract_dup(0, buf.get_size()), - dtype=numpy.uint8) - def set_pixel_format(self,format): + dtype=numpy.uint8, + ) + + def set_pixel_format(self, format): pass + + class Camera_Simulation(object): - def __init__(self,sn=None,width=640,height=480,framerate=30,color=False): + def __init__(self, sn=None, width=640, height=480, framerate=30, color=False): self.height = height self.width = width self.sample = None @@ -229,10 +246,10 @@ def __init__(self,sn=None,width=640,height=480,framerate=30,color=False): self.callback_was_enabled_before_autofocus = False self.callback_was_enabled_before_multipoint = False - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -241,16 +258,16 @@ def enable_callback(self): def disable_callback(self): pass - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): pass - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): pass - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): pass def get_awb_ratios(self): @@ -278,11 +295,11 @@ def send_trigger(self): self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - self.current_frame = np.random.randint(255,size=(2000,2000),dtype=np.uint8) - self.current_frame[901:1100,901:1100] = 200 + self.current_frame = np.random.randint(255, size=(2000, 2000), dtype=np.uint8) + self.current_frame[901:1100, 901:1100] = 200 else: - self.current_frame = np.roll(self.current_frame,10,axis=0) - pass + self.current_frame = np.roll(self.current_frame, 10, axis=0) + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None: self.new_image_callback_external(self) @@ -302,5 +319,5 @@ def _set_property(self, PropertyName, value): def _gstbuffer_to_opencv(self): pass - def set_pixel_format(self,format): + def set_pixel_format(self, format): pass diff --git a/software/control/camera_flir.py b/software/control/camera_flir.py index 2cf1531d8..e63df6e97 100644 --- a/software/control/camera_flir.py +++ b/software/control/camera_flir.py @@ -5,22 +5,26 @@ import PySpin from control._def import * + class ReadType: """ Use the following constants to determine whether nodes are read as Value nodes or their individual types. """ - VALUE = 0, + + VALUE = (0,) INDIVIDUAL = 1 + try: - if CHOSEN_READ == 'VALUE': + if CHOSEN_READ == "VALUE": CHOSEN_READ = ReadType.VALUE else: CHOSEN_READ = ReadType.INDIVIDUAL except: CHOSEN_READ = ReadType.INDIVIDUAL + def get_value_node(node): """ Retrieves and prints the display name and value of all node types as value nodes. @@ -54,10 +58,10 @@ def get_value_node(node): # easier to deal with nodes as value nodes rather than their actual # individual types. value = node_value.ToString() - return (name,value) + return (name, value) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) def get_string_node(node): @@ -86,11 +90,12 @@ def get_string_node(node): value = node_string.GetValue() # Print value; 'level' determines the indentation level of output - return(name,value) + return (name, value) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) + def get_integer_node(node): """ @@ -116,11 +121,12 @@ def get_integer_node(node): value = node_integer.GetValue() # Print value - return (name,value) + return (name, value) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) + def get_float_node(node): """ @@ -143,11 +149,11 @@ def get_float_node(node): value = node_float.GetValue() # Print value - return (name,value) + return (name, value) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) def get_boolean_node(node): @@ -171,11 +177,11 @@ def get_boolean_node(node): # Print Boolean value # NOTE: In Python a Boolean will be printed as "True" or "False". - return (name,value) + return (name, value) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) def get_command_node(node): @@ -212,8 +218,8 @@ def get_command_node(node): return (name, tooltip) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) def get_enumeration_node_and_current_entry(node): @@ -251,11 +257,11 @@ def get_enumeration_node_and_current_entry(node): entry_symbolic = node_enum_entry.GetSymbolic() # Print current entry symbolic - return(name, entry_symbolic) + return (name, entry_symbolic) except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - return ('',None) + print("Error: %s" % ex) + return ("", None) def get_category_node_and_all_features(node): @@ -288,10 +294,12 @@ def get_category_node_and_all_features(node): # Ensure node is readable if not PySpin.IsReadable(node_feature): continue - + # Category nodes must be dealt with separately in order to retrieve subnodes recursively. if node_feature.GetPrincipalInterfaceType() == PySpin.intfICategory: - return_dict[PySpin.CCategoryPtr(node_feature).GetName()] = get_category_node_and_all_features(node_feature) + return_dict[PySpin.CCategoryPtr(node_feature).GetName()] = get_category_node_and_all_features( + node_feature + ) # Cast all non-category nodes as value nodes # @@ -300,12 +308,12 @@ def get_category_node_and_all_features(node): # simpler to cast them as value nodes rather than as their individual types. # However, with this increased ease-of-use, functionality is sacrificed. elif CHOSEN_READ == ReadType.VALUE: - node_name, node_value = get_value_node(node_feature) + node_name, node_value = get_value_node(node_feature) return_dict[node_name] = node_value # Cast all non-category nodes as actual types elif CHOSEN_READ == ReadType.INDIVIDUAL: - node_name = '' + node_name = "" node_value = None if node_feature.GetPrincipalInterfaceType() == PySpin.intfIString: node_name, node_value = get_string_node(node_feature) @@ -314,49 +322,51 @@ def get_category_node_and_all_features(node): elif node_feature.GetPrincipalInterfaceType() == PySpin.intfIFloat: node_name, node_value = get_float_node(node_feature) elif node_feature.GetPrincipalInterfaceType() == PySpin.intfIBoolean: - node_name, node_value= get_boolean_node(node_feature) + node_name, node_value = get_boolean_node(node_feature) elif node_feature.GetPrincipalInterfaceType() == PySpin.intfICommand: - node_name, node_value = get_command_node(node_feature) + node_name, node_value = get_command_node(node_feature) elif node_feature.GetPrincipalInterfaceType() == PySpin.intfIEnumeration: node_name, node_value = get_enumeration_node_and_current_entry(node_feature) return_dict[node_name] = node_value except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) - - return return_dict + print("Error: %s" % ex) + + return return_dict def get_device_info(cam): nodemap_tldevice = cam.GetTLDeviceNodeMap() device_info_dict = {} - device_info_dict['TLDevice'] = get_category_node_and_all_features(nodemap_tldevice.GetNode('Root')) + device_info_dict["TLDevice"] = get_category_node_and_all_features(nodemap_tldevice.GetNode("Root")) return device_info_dict + def get_device_info_full(cam, get_genicam=False): device_info_dict = {} nodemap_gentl = cam.GetTLDeviceNodeMap() - device_info_dict['TLDevice'] = get_category_node_and_all_features(nodemap_gentl.GetNode('Root')) + device_info_dict["TLDevice"] = get_category_node_and_all_features(nodemap_gentl.GetNode("Root")) nodemap_tlstream = cam.GetTLStreamNodeMap() - device_info_dict['TLStream'] = get_category_node_and_all_features(nodemap_tlstream.GetNode('Root')) + device_info_dict["TLStream"] = get_category_node_and_all_features(nodemap_tlstream.GetNode("Root")) if get_genicam: cam.Init() nodemap_applayer = cam.GetNodeMap() - device_info_dict['GenICam'] = get_category_node_and_all_features(nodemap_applayer.GetNode('Root')) + device_info_dict["GenICam"] = get_category_node_and_all_features(nodemap_applayer.GetNode("Root")) cam.DeInit() return device_info_dict + def retrieve_all_camera_info(get_genicam=False): system = PySpin.System.GetInstance() cam_list = system.GetCameras() device_num = cam_list.GetSize() return_list = [] if device_num > 0: - for i,cam in enumerate(cam_list): - return_list.append(get_device_info_full(cam,get_genicam=get_genicam)) + for i, cam in enumerate(cam_list): + return_list.append(get_device_info_full(cam, get_genicam=get_genicam)) try: del cam except NameError: @@ -364,7 +374,6 @@ def retrieve_all_camera_info(get_genicam=False): cam_list.Clear() system.ReleaseInstance() return return_list - def get_sn_by_model(model_name): @@ -373,11 +382,11 @@ def get_sn_by_model(model_name): device_num = cam_list.GetSize() sn_to_return = None if device_num > 0: - for i,cam in enumerate(cam_list): + for i, cam in enumerate(cam_list): device_info = get_device_info(cam) try: - if device_info['TLDevice']['DeviceInformation']['DeviceModelName'] == model_name: - sn_to_return = device_info['TLDevice']['DeviceInformation']['DeviceSerialNumber'] + if device_info["TLDevice"]["DeviceInformation"]["DeviceModelName"] == model_name: + sn_to_return = device_info["TLDevice"]["DeviceInformation"]["DeviceSerialNumber"] break except KeyError: pass @@ -389,29 +398,35 @@ def get_sn_by_model(model_name): system.ReleaseInstance() return sn_to_return + class ImageEventHandler(PySpin.ImageEventHandler): - def __init__(self,parent): - super(ImageEventHandler,self).__init__() + def __init__(self, parent): + super(ImageEventHandler, self).__init__() - self.camera = parent #Camera() type object + self.camera = parent # Camera() type object self._processor = PySpin.ImageProcessor() self._processor.SetColorProcessing(PySpin.SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR) def OnImageEvent(self, raw_image): - + if raw_image.IsIncomplete(): - print('Image incomplete with image status %i ...' % raw_image.GetImageStatus()) + print("Image incomplete with image status %i ..." % raw_image.GetImageStatus()) return - elif self.camera.is_color and 'mono' not in self.camera.pixel_format.lower(): - if "10" in self.camera.pixel_format or "12" in self.camera.pixel_format or "14" in self.camera.pixel_format or "16" in self.camera.pixel_format: - rgb_image = self._processor.Convert(raw_image,PySpin.PixelFormat_RGB16) + elif self.camera.is_color and "mono" not in self.camera.pixel_format.lower(): + if ( + "10" in self.camera.pixel_format + or "12" in self.camera.pixel_format + or "14" in self.camera.pixel_format + or "16" in self.camera.pixel_format + ): + rgb_image = self._processor.Convert(raw_image, PySpin.PixelFormat_RGB16) else: - rgb_image = self._processor.Convert(raw_image,PySpin.PixelFormat_RGB8) + rgb_image = self._processor.Convert(raw_image, PySpin.PixelFormat_RGB8) numpy_image = rgb_image.GetNDArray() else: if self.camera.convert_pixel_format: - converted_image = self._processor.Convert(raw_image,self.camera.conversion_pixel_format) + converted_image = self._processor.Convert(raw_image, self.camera.conversion_pixel_format) numpy_image = converted_image.GetNDArray() if self.camera.conversion_pixel_format == PySpin.PixelFormat_Mono12: numpy_image = numpy_image << 4 @@ -421,8 +436,8 @@ def OnImageEvent(self, raw_image): except PySpin.SpinnakerException: converted_image = self.one_frame_post_processor.Convert(raw_image, PySpin.PixelFormat_Mono8) numpy_image = converted_image.GetNDArray() - if self.camera.pixel_format == 'MONO12': - numpy_image = numpy_image <<4 + if self.camera.pixel_format == "MONO12": + numpy_image = numpy_image << 4 self.camera.current_frame = numpy_image self.camera.frame_ID_software = self.camera.frame_ID_software + 1 self.camera.frame_ID = raw_image.GetFrameID() @@ -432,21 +447,21 @@ def OnImageEvent(self, raw_image): self.camera.frame_ID = self.camera.frame_ID - self.camera.frame_ID_offset_hardware_trigger self.camera.timestamp = time.time() self.camera.new_image_callback_external(self.camera) - + class Camera(object): - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None, is_color=False): + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None, is_color=False): self.py_spin_system = PySpin.System.GetInstance() self.camera_list = self.py_spin_system.GetCameras() - self.sn = sn + self.sn = sn self_is_color = is_color # many to be purged self.is_global_shutter = is_global_shutter self.device_info_dict = None self.device_index = 0 - self.camera = None #PySpin CameraPtr type + self.camera = None # PySpin CameraPtr type self.is_color = None self.gamma_lut = None self.contrast_lut = None @@ -457,7 +472,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.convert_pixel_format = False self.one_frame_post_processor.SetColorProcessing(PySpin.SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR) - self.auto_exposure_mode =None + self.auto_exposure_mode = None self.auto_gain_mode = None self.auto_wb_mode = None self.auto_wb_profile = None @@ -465,7 +480,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image - self.exposure_time = 1 # unit: ms + self.exposure_time = 1 # unit: ms self.analog_gain = 0 self.frame_ID = -1 self.frame_ID_software = -1 @@ -487,21 +502,23 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None self.pixel_size_byte = 1 - # below are values for IMX226 (MER2-1220-32U3M) - to make configurable + # below are values for IMX226 (MER2-1220-32U3M) - to make configurable self.row_period_us = 10 self.row_numbers = 3036 self.exposure_delay_us_8bit = 650 - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) - self.pixel_format = None # use the default pixel format + self.pixel_format = None # use the default pixel format - self.is_live = False # this determines whether a new frame received will be handled in the streamHandler + self.is_live = False # this determines whether a new frame received will be handled in the streamHandler self.image_event_handler = ImageEventHandler(self) # mainly for discarding the last frame received after stop_live() is called, where illumination is being turned off during exposure - def open(self,index=0, is_color=None): + def open(self, index=0, is_color=None): if is_color is None: is_color = self.is_color try: @@ -513,61 +530,58 @@ def open(self,index=0, is_color=None): self.camera_list = self.py_spin_system.GetCameras() device_num = self.camera_list.GetSize() if device_num == 0: - raise RuntimeError('Could not find any USB camera devices!') + raise RuntimeError("Could not find any USB camera devices!") if self.sn is None: self.device_index = index self.camera = self.camera_list.GetByIndex(index) else: self.camera = self.camera_list.GetBySerial(str(self.sn)) - self.device_info_dict = get_device_info_full(self.camera, get_genicam=True) - + self.camera.Init() self.nodemap = self.camera.GetNodeMap() - + self.is_color = is_color if self.is_color: - self.set_wb_ratios(2,1,2) - + self.set_wb_ratios(2, 1, 2) # set to highest possible framerate - PySpin.CBooleanPtr(self.nodemap.GetNode('AcquisitionFrameRateEnable')).SetValue(True) + PySpin.CBooleanPtr(self.nodemap.GetNode("AcquisitionFrameRateEnable")).SetValue(True) target_rate = 1000 - for decrement in range(0,1000): + for decrement in range(0, 1000): try: - PySpin.CFloatPtr(self.nodemap.GetNode('AcquisitionFrameRate')).SetValue(target_rate-decrement) + PySpin.CFloatPtr(self.nodemap.GetNode("AcquisitionFrameRate")).SetValue(target_rate - decrement) break except PySpin.SpinnakerException as ex: pass # turn off device throughput limit - node_throughput_limit = PySpin.CIntegerPtr(self.nodemap.GetNode('DeviceLinkThroughputLimit')) + node_throughput_limit = PySpin.CIntegerPtr(self.nodemap.GetNode("DeviceLinkThroughputLimit")) node_throughput_limit.SetValue(node_throughput_limit.GetMax()) - self.Width = PySpin.CIntegerPtr(self.nodemap.GetNode('Width')).GetValue() - self.Height = PySpin.CIntegerPtr(self.nodemap.GetNode('Height')).GetValue() - + self.Width = PySpin.CIntegerPtr(self.nodemap.GetNode("Width")).GetValue() + self.Height = PySpin.CIntegerPtr(self.nodemap.GetNode("Height")).GetValue() + + self.WidthMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode("SensorWidth")).GetValue() + self.HeightMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode("SensorHeight")).GetValue() + + self.set_ROI(0, 0) - self.WidthMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode('SensorWidth')).GetValue() - self.HeightMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode('SensorHeight')).GetValue() - - self.set_ROI(0,0) - - self.WidthMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode('WidthMax')).GetValue() - self.HeightMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode('HeightMax')).GetValue() + self.WidthMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode("WidthMax")).GetValue() + self.HeightMaxAbsolute = PySpin.CIntegerPtr(self.nodemap.GetNode("HeightMax")).GetValue() - self.set_ROI(0,0,self.WidthMaxAbsolute,self.HeightMaxAbsolute) + self.set_ROI(0, 0, self.WidthMaxAbsolute, self.HeightMaxAbsolute) self.WidthMax = self.WidthMaxAbsolute self.HeightMax = self.HeightMaxAbsolute - self.OffsetX = PySpin.CIntegerPtr(self.nodemap.GetNode('OffsetX')).GetValue() - self.OffsetY = PySpin.CIntegerPtr(self.nodemap.GetNode('OffsetY')).GetValue() + self.OffsetX = PySpin.CIntegerPtr(self.nodemap.GetNode("OffsetX")).GetValue() + self.OffsetY = PySpin.CIntegerPtr(self.nodemap.GetNode("OffsetY")).GetValue() # disable gamma - PySpin.CBooleanPtr(self.nodemap.GetNode('GammaEnable')).SetValue(False) + PySpin.CBooleanPtr(self.nodemap.GetNode("GammaEnable")).SetValue(False) - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -583,7 +597,7 @@ def enable_callback(self): self.camera.RegisterEventHandler(self.image_event_handler) self.callback_is_enabled = True except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) + print("Error: %s" % ex) # resume streaming if it was on if was_streaming: self.start_streaming() @@ -603,14 +617,14 @@ def disable_callback(self): self.camera.UnregisterEventHandler(self.image_event_handler) self.callback_is_enabled = False except PySpin.SpinnakerException as ex: - print('Error: %s' % ex) + print("Error: %s" % ex) # resume streaming if it was on if was_streaming: self.start_streaming() else: pass - def open_by_sn(self,sn, is_color=None): + def open_by_sn(self, sn, is_color=None): self.sn = sn self.open(is_color=is_color) @@ -634,42 +648,47 @@ def close(self): self.last_converted_image = None self.last_numpy_image = None - def set_exposure_time(self,exposure_time): ## NOTE: Disables auto-exposure - use_strobe = (self.trigger_mode == TriggerMode.HARDWARE) # true if using hardware trigger + def set_exposure_time(self, exposure_time): ## NOTE: Disables auto-exposure + use_strobe = self.trigger_mode == TriggerMode.HARDWARE # true if using hardware trigger self.nodemap = self.camera.GetNodeMap() - node_auto_exposure = PySpin.CEnumerationPtr(self.nodemap.GetNode('ExposureAuto')) - node_auto_exposure_off = PySpin.CEnumEntryPtr(node_auto_exposure.GetEntryByName('Off')) + node_auto_exposure = PySpin.CEnumerationPtr(self.nodemap.GetNode("ExposureAuto")) + node_auto_exposure_off = PySpin.CEnumEntryPtr(node_auto_exposure.GetEntryByName("Off")) if not PySpin.IsReadable(node_auto_exposure_off) or not PySpin.IsWritable(node_auto_exposure): print("Unable to set exposure manually (cannot disable auto exposure)") return - + if node_auto_exposure.GetIntValue() != node_auto_exposure_off.GetValue(): self.auto_exposure_mode = PySpin.CEnumEntryPtr(node_auto_exposure.GetCurrentEntry()).GetValue() node_auto_exposure.SetIntValue(node_auto_exposure_off.GetValue()) - node_exposure_time = PySpin.CFloatPtr(self.nodemap.GetNode('ExposureTime')) + node_exposure_time = PySpin.CFloatPtr(self.nodemap.GetNode("ExposureTime")) if not PySpin.IsWritable(node_exposure_time): print("Unable to set exposure manually after disabling auto exposure") if use_strobe == False or self.is_global_shutter: self.exposure_time = exposure_time - node_exposure_time.SetValue(exposure_time*1000.0) + node_exposure_time.SetValue(exposure_time * 1000.0) else: # set the camera exposure time such that the active exposure time (illumination on time) is the desired value self.exposure_time = exposure_time # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure - camera_exposure_time = self.exposure_delay_us + self.exposure_time*1000 + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + 500 # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure + camera_exposure_time = ( + self.exposure_delay_us + + self.exposure_time * 1000 + + self.row_period_us * self.pixel_size_byte * (self.row_numbers - 1) + + 500 + ) # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure node_exposure_time.SetValue(camera_exposure_time) def update_camera_exposure_time(self): self.set_exposure_time(self.exposure_time) - def set_analog_gain(self,analog_gain): ## NOTE: Disables auto-gain + def set_analog_gain(self, analog_gain): ## NOTE: Disables auto-gain self.nodemap = self.camera.GetNodeMap() - - node_auto_gain = PySpin.CEnumerationPtr(self.nodemap.GetNode('GainAuto')) - node_auto_gain_off = PySpin.CEnumEntryPtr(node_auto_gain.GetEntryByName('Off')) + + node_auto_gain = PySpin.CEnumerationPtr(self.nodemap.GetNode("GainAuto")) + node_auto_gain_off = PySpin.CEnumEntryPtr(node_auto_gain.GetEntryByName("Off")) if not PySpin.IsReadable(node_auto_gain_off) or not PySpin.IsWritable(node_auto_gain): print("Unable to set gain manually (cannot disable auto gain)") return @@ -678,8 +697,8 @@ def set_analog_gain(self,analog_gain): ## NOTE: Disables auto-gain self.auto_gain_mode = PySpin.CEnumEntryPtr(node_auto_gain.GetCurrentEntry()).GetValue() node_auto_gain.SetIntValue(node_auto_gain_off.GetValue()) - - node_gain = PySpin.CFloatPtr(self.nodemap.GetNode('Gain')) + + node_gain = PySpin.CFloatPtr(self.nodemap.GetNode("Gain")) if not PySpin.IsWritable(node_gain): print("Unable to set gain manually after disabling auto gain") @@ -688,34 +707,39 @@ def set_analog_gain(self,analog_gain): ## NOTE: Disables auto-gain self.analog_gain = analog_gain node_gain.SetValue(analog_gain) - def get_awb_ratios(self): ## NOTE: Enables auto WB, defaults to continuous WB + def get_awb_ratios(self): ## NOTE: Enables auto WB, defaults to continuous WB self.nodemap = self.camera.GetNodeMap() node_balance_white_auto = PySpin.CEnumerationPtr(self.nodemap.GetNode("BalanceWhiteAuto")) - #node_balance_white_auto_options = [PySpin.CEnumEntryPtr(entry).GetName() for entry in node_balance_white_auto.GetEntries()] - #print("WB Auto options: "+str(node_balance_white_auto_options)) + # node_balance_white_auto_options = [PySpin.CEnumEntryPtr(entry).GetName() for entry in node_balance_white_auto.GetEntries()] + # print("WB Auto options: "+str(node_balance_white_auto_options)) node_balance_ratio_select = PySpin.CEnumerationPtr(self.nodemap.GetNode("BalanceRatioSelector")) - #node_balance_ratio_select_options = [PySpin.CEnumEntryPtr(entry).GetName() for entry in node_balance_ratio_select.GetEntries()] - #print("Balance Ratio Select options: "+str(node_balance_ratio_select_options)) + # node_balance_ratio_select_options = [PySpin.CEnumEntryPtr(entry).GetName() for entry in node_balance_ratio_select.GetEntries()] + # print("Balance Ratio Select options: "+str(node_balance_ratio_select_options)) """ node_balance_profile = PySpin.CEnumerationPtr(self.nodemap.GetNode("BalanceWhiteAutoProfile")) node_balance_profile_options= [PySpin.CEnumEntryPtr(entry).GetName() for entry in node_balance_profile.GetEntries()] print("WB Auto Profile options: "+str(node_balance_profile_options)) """ - node_balance_white_auto_off = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntryByName('Off')) + node_balance_white_auto_off = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntryByName("Off")) if not PySpin.IsReadable(node_balance_white_auto) or not PySpin.IsReadable(node_balance_white_auto_off): print("Unable to check if white balance is auto or not") - elif PySpin.IsWritable(node_balance_white_auto) and node_balance_white_auto.GetIntValue() == node_balance_white_auto_off.GetValue(): + elif ( + PySpin.IsWritable(node_balance_white_auto) + and node_balance_white_auto.GetIntValue() == node_balance_white_auto_off.GetValue() + ): if self.auto_wb_mode is not None: node_balance_white_auto.SetIntValue(self.auto_wb_mode) else: - node_balance_white_continuous = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntryByName('Continuous')) + node_balance_white_continuous = PySpin.CEnumEntryPtr( + node_balance_white_auto.GetEntryByName("Continuous") + ) if PySpin.IsReadable(node_balance_white_continuous): node_balance_white_auto.SetIntValue(node_balance_white_continuous.GetValue()) else: print("Cannot turn on auto white balance in continuous mode") - node_balance_white_once = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntry('Once')) + node_balance_white_once = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntry("Once")) if PySpin.IsReadable(node_balance_white_once): node_balance_white_auto.SetIntValue(node_balance_white_once.GetValue()) else: @@ -727,9 +751,14 @@ def get_awb_ratios(self): ## NOTE: Enables auto WB, defaults to continuous WB balance_ratio_green = PySpin.CEnumEntryPtr(node_balance_ratio_select.GetEntryByName("Green")) balance_ratio_blue = PySpin.CEnumEntryPtr(node_balance_ratio_select.GetEntryByName("Blue")) node_balance_ratio = PySpin.CFloatPtr(self.nodemap.GetNode("BalanceRatio")) - if not PySpin.IsWritable(node_balance_ratio_select) or not PySpin.IsReadable(balance_ratio_red) or not PySpin.IsReadable(balance_ratio_green) or not PySpin.IsReadable(balance_ratio_blue): + if ( + not PySpin.IsWritable(node_balance_ratio_select) + or not PySpin.IsReadable(balance_ratio_red) + or not PySpin.IsReadable(balance_ratio_green) + or not PySpin.IsReadable(balance_ratio_blue) + ): print("Unable to move balance ratio selector") - return (0,0,0) + return (0, 0, 0) node_balance_ratio_select.SetIntValue(balance_ratio_red.GetValue()) if not PySpin.IsReadable(node_balance_ratio): @@ -754,12 +783,12 @@ def get_awb_ratios(self): ## NOTE: Enables auto WB, defaults to continuous WB return (awb_r, awb_g, awb_b) - def set_wb_ratios(self, wb_r=None, wb_g=None, wb_b=None): ## NOTE disables auto WB, stores extant - ## auto WB mode if any + def set_wb_ratios(self, wb_r=None, wb_g=None, wb_b=None): ## NOTE disables auto WB, stores extant + ## auto WB mode if any self.nodemap = self.camera.GetNodeMap() node_balance_white_auto = PySpin.CEnumerationPtr(self.nodemap.GetNode("BalanceWhiteAuto")) node_balance_ratio_select = PySpin.CEnumerationPtr(self.nodemap.GetNode("BalanceRatioSelector")) - node_balance_white_auto_off = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntryByName('Off')) + node_balance_white_auto_off = PySpin.CEnumEntryPtr(node_balance_white_auto.GetEntryByName("Off")) if not PySpin.IsReadable(node_balance_white_auto) or not PySpin.IsReadable(node_balance_white_auto_off): print("Unable to check if white balance is auto or not") elif node_balance_white_auto.GetIntValue() != node_balance_white_auto_off.GetValue(): @@ -768,12 +797,17 @@ def set_wb_ratios(self, wb_r=None, wb_g=None, wb_b=None): ## NOTE disables auto node_balance_white_auto.SetIntValue(node_balance_white_auto_off.GetValue()) else: print("Cannot turn off auto WB") - + balance_ratio_red = PySpin.CEnumEntryPtr(node_balance_ratio_select.GetEntryByName("Red")) balance_ratio_green = PySpin.CEnumEntryPtr(node_balance_ratio_select.GetEntryByName("Green")) balance_ratio_blue = PySpin.CEnumEntryPtr(node_balance_ratio_select.GetEntryByName("Blue")) node_balance_ratio = PySpin.CFloatPtr(self.nodemap.GetNode("BalanceRatio")) - if not PySpin.IsWritable(node_balance_ratio_select) or not PySpin.IsReadable(balance_ratio_red) or not PySpin.IsReadable(balance_ratio_green) or not PySpin.IsReadable(balance_ratio_blue): + if ( + not PySpin.IsWritable(node_balance_ratio_select) + or not PySpin.IsReadable(balance_ratio_red) + or not PySpin.IsReadable(balance_ratio_green) + or not PySpin.IsReadable(balance_ratio_blue) + ): print("Unable to move balance ratio selector") return @@ -798,18 +832,18 @@ def set_wb_ratios(self, wb_r=None, wb_g=None, wb_b=None): ## NOTE disables auto if wb_b is not None: node_balance_ratio.SetValue(wb_b) - def set_reverse_x(self,value): + def set_reverse_x(self, value): self.nodemap = self.camera.GetNodeMap() - node_reverse_x = PySpin.CBooleanPtr(self.nodemap.GetNode('ReverseX')) + node_reverse_x = PySpin.CBooleanPtr(self.nodemap.GetNode("ReverseX")) if not PySpin.IsWritable(node_reverse_x): print("Can't write to reverse X node") return else: node_reverse_x.SetValue(bool(value)) - def set_reverse_y(self,value): + def set_reverse_y(self, value): self.nodemap = self.camera.GetNodeMap() - node_reverse_y = PySpin.CBooleanPtr(self.nodemap.GetNode('ReverseY')) + node_reverse_y = PySpin.CBooleanPtr(self.nodemap.GetNode("ReverseY")) if not PySpin.IsWritable(node_reverse_y): print("Can't write to reverse Y node") return @@ -823,7 +857,7 @@ def start_streaming(self): try: self.camera.BeginAcquisition() except PySpin.SpinnakerException as ex: - print("Spinnaker exception: "+str(ex)) + print("Spinnaker exception: " + str(ex)) if self.camera.IsStreaming(): print("Camera is streaming") self.is_streaming = True @@ -833,66 +867,66 @@ def stop_streaming(self): try: self.camera.EndAcquisition() except PySpin.SpinnakerException as ex: - print("Spinnaker exception: "+str(ex)) + print("Spinnaker exception: " + str(ex)) if not self.camera.IsStreaming(): print("Camera is not streaming") self.is_streaming = False - def set_pixel_format(self,pixel_format,convert_if_not_native=False): + def set_pixel_format(self, pixel_format, convert_if_not_native=False): if self.is_streaming == True: was_streaming = True self.stop_streaming() else: was_streaming = False self.nodemap = self.camera.GetNodeMap() - - node_pixel_format = PySpin.CEnumerationPtr(self.nodemap.GetNode('PixelFormat')) - node_adc_bit_depth = PySpin.CEnumerationPtr(self.nodemap.GetNode('AdcBitDepth')) + + node_pixel_format = PySpin.CEnumerationPtr(self.nodemap.GetNode("PixelFormat")) + node_adc_bit_depth = PySpin.CEnumerationPtr(self.nodemap.GetNode("AdcBitDepth")) if PySpin.IsWritable(node_pixel_format) and PySpin.IsWritable(node_adc_bit_depth): - pixel_selection = None + pixel_selection = None pixel_size_byte = None adc_bit_depth = None fallback_pixel_selection = None conversion_pixel_format = None - if pixel_format == 'MONO8': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono8')) + if pixel_format == "MONO8": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono8")) conversion_pixel_format = PySpin.PixelFormat_Mono8 pixel_size_byte = 1 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit10')) - if pixel_format == 'MONO10': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono10')) - fallback_pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono10p')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit10")) + if pixel_format == "MONO10": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono10")) + fallback_pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono10p")) conversion_pixel_format = PySpin.PixelFormat_Mono8 pixel_size_byte = 1 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit10')) - if pixel_format == 'MONO12': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono12')) - fallback_pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono12p')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit10")) + if pixel_format == "MONO12": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono12")) + fallback_pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono12p")) conversion_pixel_format = PySpin.PixelFormat_Mono16 pixel_size_byte = 2 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit12')) - if pixel_format == 'MONO14': # MONO14/16 are aliases of each other, since they both - # do ADC at bit depth 14 - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono16')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit12")) + if pixel_format == "MONO14": # MONO14/16 are aliases of each other, since they both + # do ADC at bit depth 14 + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono16")) conversion_pixel_format = PySpin.PixelFormat_Mono16 pixel_size_byte = 2 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit14')) - if pixel_format == 'MONO16': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('Mono16')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit14")) + if pixel_format == "MONO16": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("Mono16")) conversion_pixel_format = PySpin.PixelFormat_Mono16 pixel_size_byte = 2 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit14')) - if pixel_format == 'BAYER_RG8': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('BayerRG8')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit14")) + if pixel_format == "BAYER_RG8": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("BayerRG8")) conversion_pixel_format = PySpin.PixelFormat_BayerRG8 pixel_size_byte = 1 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit10')) - if pixel_format == 'BAYER_RG12': - pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName('BayerRG12')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit10")) + if pixel_format == "BAYER_RG12": + pixel_selection = PySpin.CEnumEntryPtr(node_pixel_format.GetEntryByName("BayerRG12")) conversion_pixel_format = PySpin.PixelFormat_BayerRG12 pixel_size_byte = 2 - adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName('Bit12')) + adc_bit_depth = PySpin.CEnumEntryPtr(node_adc_bit_depth.GetEntryByName("Bit12")) if pixel_selection is not None and adc_bit_depth is not None: if PySpin.IsReadable(pixel_selection): @@ -917,7 +951,7 @@ def set_pixel_format(self,pixel_format,convert_if_not_native=False): print("Pixel format not available for this camera") if PySpin.IsReadable(adc_bit_depth): node_adc_bit_depth.SetIntValue(adc_bit_depth.GetValue()) - print("Still able to set ADC bit depth to "+adc_bit_depth.GetSymbolic()) + print("Still able to set ADC bit depth to " + adc_bit_depth.GetSymbolic()) else: print("Pixel format not implemented for Squid") @@ -926,16 +960,18 @@ def set_pixel_format(self,pixel_format,convert_if_not_native=False): print("pixel format is not writable") if was_streaming: - self.start_streaming() + self.start_streaming() # update the exposure delay and strobe delay - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) def set_continuous_acquisition(self): self.nodemap = self.camera.GetNodeMap() - node_trigger_mode = PySpin.CEnumerationPtr(self.nodemap.GetNode('TriggerMode')) - node_trigger_mode_off = PySpin.CEnumEntryPtr(node_trigger_mode.GetEntryByName('Off')) + node_trigger_mode = PySpin.CEnumerationPtr(self.nodemap.GetNode("TriggerMode")) + node_trigger_mode_off = PySpin.CEnumEntryPtr(node_trigger_mode.GetEntryByName("Off")) if not PySpin.IsWritable(node_trigger_mode) or not PySpin.IsReadable(node_trigger_mode_off): print("Cannot toggle TriggerMode") return @@ -945,12 +981,12 @@ def set_continuous_acquisition(self): def set_triggered_acquisition_flir(self, source, activation=None): self.nodemap = self.camera.GetNodeMap() - node_trigger_mode = PySpin.CEnumerationPtr(self.nodemap.GetNode('TriggerMode')) - node_trigger_mode_on = PySpin.CEnumEntryPtr(node_trigger_mode.GetEntryByName('On')) + node_trigger_mode = PySpin.CEnumerationPtr(self.nodemap.GetNode("TriggerMode")) + node_trigger_mode_on = PySpin.CEnumEntryPtr(node_trigger_mode.GetEntryByName("On")) if not PySpin.IsWritable(node_trigger_mode) or not PySpin.IsReadable(node_trigger_mode_on): print("Cannot toggle TriggerMode") return - node_trigger_source = PySpin.CEnumerationPtr(self.nodemap.GetNode('TriggerSource')) + node_trigger_source = PySpin.CEnumerationPtr(self.nodemap.GetNode("TriggerSource")) node_trigger_source_option = PySpin.CEnumEntryPtr(node_trigger_source.GetEntryByName(str(source))) node_trigger_mode.SetIntValue(node_trigger_mode_on.GetValue()) @@ -961,22 +997,24 @@ def set_triggered_acquisition_flir(self, source, activation=None): node_trigger_source.SetIntValue(node_trigger_source_option.GetValue()) - if source != "Software" and activation is not None: # Set activation criteria for hardware trigger - node_trigger_activation = PySpin.CEnumerationPtr(self.nodemap.GetNode('TriggerActivation')) - node_trigger_activation_option = PySpin.CEnumEntryPtr(node_trigger_activation.GetEntryByName(str(activation))) - if not PySpin.IsWritable(node_trigger_activation) or not PySpin.IsReadable(node_trigger_activation_option): + if source != "Software" and activation is not None: # Set activation criteria for hardware trigger + node_trigger_activation = PySpin.CEnumerationPtr(self.nodemap.GetNode("TriggerActivation")) + node_trigger_activation_option = PySpin.CEnumEntryPtr( + node_trigger_activation.GetEntryByName(str(activation)) + ) + if not PySpin.IsWritable(node_trigger_activation) or not PySpin.IsReadable(node_trigger_activation_option): print("Cannot set trigger activation mode") return node_trigger_activation.SetIntValue(node_trigger_activation_option.GetValue()) def set_software_triggered_acquisition(self): - self.set_triggered_acquisition_flir(source='Software') + self.set_triggered_acquisition_flir(source="Software") self.trigger_mode = TriggerMode.SOFTWARE self.update_camera_exposure_time() - def set_hardware_triggered_acquisition(self, source='Line2', activation='RisingEdge'): + def set_hardware_triggered_acquisition(self, source="Line2", activation="RisingEdge"): self.set_triggered_acquisition_flir(source=source, activation=activation) self.frame_ID_offset_hardware_trigger = None self.trigger_mode = TriggerMode.HARDWARE @@ -985,39 +1023,44 @@ def set_hardware_triggered_acquisition(self, source='Line2', activation='RisingE def send_trigger(self): if self.is_streaming: self.nodemap = self.camera.GetNodeMap() - node_trigger = PySpin.CCommandPtr(self.nodemap.GetNode('TriggerSoftware')) + node_trigger = PySpin.CCommandPtr(self.nodemap.GetNode("TriggerSoftware")) if not PySpin.IsWritable(node_trigger): - print('Trigger node not writable') + print("Trigger node not writable") return node_trigger.Execute() else: - print('trigger not sent - camera is not streaming') + print("trigger not sent - camera is not streaming") def read_frame(self): if not self.camera.IsStreaming(): print("Cannot read frame, camera not streaming") - return np.zeros((self.Width,self.Height)) + return np.zeros((self.Width, self.Height)) callback_was_enabled = False - if self.callback_is_enabled: # need to disable callback to read stream manually + if self.callback_is_enabled: # need to disable callback to read stream manually callback_was_enabled = True self.disable_callback() raw_image = self.camera.GetNextImage(1000) if raw_image.IsIncomplete(): - print('Image incomplete with image status %d ...' % raw_image.GetImageStatus()) + print("Image incomplete with image status %d ..." % raw_image.GetImageStatus()) raw_image.Release() - return np.zeros((self.Width,self.Height)) - - if self.is_color and 'mono' not in self.pixel_format.lower(): - if "10" in self.pixel_format or "12" in self.pixel_format or "14" in self.pixel_format or "16" in self.pixel_format: - rgb_image = self.one_frame_post_processor.Convert(raw_image,PySpin.PixelFormat_RGB16) + return np.zeros((self.Width, self.Height)) + + if self.is_color and "mono" not in self.pixel_format.lower(): + if ( + "10" in self.pixel_format + or "12" in self.pixel_format + or "14" in self.pixel_format + or "16" in self.pixel_format + ): + rgb_image = self.one_frame_post_processor.Convert(raw_image, PySpin.PixelFormat_RGB16) else: - rgb_image = self.one_frame_post_processor.Convert(raw_image,PySpin.PixelFormat_RGB8) + rgb_image = self.one_frame_post_processor.Convert(raw_image, PySpin.PixelFormat_RGB8) numpy_image = rgb_image.GetNDArray() - if self.pixel_format == 'BAYER_RG12': + if self.pixel_format == "BAYER_RG12": numpy_image = numpy_image << 4 else: if self.convert_pixel_format: - converted_image = self.one_frame_post_processor.Convert(raw_image,self.conversion_pixel_format) + converted_image = self.one_frame_post_processor.Convert(raw_image, self.conversion_pixel_format) numpy_image = converted_image.GetNDArray() if self.conversion_pixel_format == PySpin.PixelFormat_Mono12: numpy_image = numpy_image << 4 @@ -1028,15 +1071,15 @@ def read_frame(self): print("Encountered problem getting ndarray, falling back to conversion to Mono8") converted_image = self.one_frame_post_processor.Convert(raw_image, PySpin.PixelFormat_Mono8) numpy_image = converted_image.GetNDArray() - if self.pixel_format == 'MONO12': - numpy_image = numpy_image <<4 + if self.pixel_format == "MONO12": + numpy_image = numpy_image << 4 # self.current_frame = numpy_image raw_image.Release() - if callback_was_enabled: # reenable callback if it was disabled + if callback_was_enabled: # reenable callback if it was disabled self.enable_callback() return numpy_image - - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): # stop streaming if streaming is on if self.is_streaming == True: @@ -1046,23 +1089,23 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): was_streaming = False self.nodemap = self.camera.GetNodeMap() - node_width = PySpin.CIntegerPtr(self.nodemap.GetNode('Width')) - node_height = PySpin.CIntegerPtr(self.nodemap.GetNode('Height')) - node_width_max = PySpin.CIntegerPtr(self.nodemap.GetNode('WidthMax')) - node_height_max = PySpin.CIntegerPtr(self.nodemap.GetNode('HeightMax')) - node_offset_x = PySpin.CIntegerPtr(self.nodemap.GetNode('OffsetX')) - node_offset_y = PySpin.CIntegerPtr(self.nodemap.GetNode('OffsetY')) + node_width = PySpin.CIntegerPtr(self.nodemap.GetNode("Width")) + node_height = PySpin.CIntegerPtr(self.nodemap.GetNode("Height")) + node_width_max = PySpin.CIntegerPtr(self.nodemap.GetNode("WidthMax")) + node_height_max = PySpin.CIntegerPtr(self.nodemap.GetNode("HeightMax")) + node_offset_x = PySpin.CIntegerPtr(self.nodemap.GetNode("OffsetX")) + node_offset_y = PySpin.CIntegerPtr(self.nodemap.GetNode("OffsetY")) if width is not None: # update the camera setting if PySpin.IsWritable(node_width): node_min = node_width.GetMin() node_inc = node_width.GetInc() - diff = width-node_min - num_incs = diff//node_inc - width = node_min+num_incs*node_inc + diff = width - node_min + num_incs = diff // node_inc + width = node_min + num_incs * node_inc self.Width = width - node_width.SetValue(min(max(int(width),0),node_width_max.GetValue())) + node_width.SetValue(min(max(int(width), 0), node_width_max.GetValue())) else: print("Width is not implemented or not writable") @@ -1071,12 +1114,12 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): if PySpin.IsWritable(node_height): node_min = node_height.GetMin() node_inc = node_height.GetInc() - diff = height-node_min - num_incs = diff//node_inc - height = node_min+num_incs*node_inc + diff = height - node_min + num_incs = diff // node_inc + height = node_min + num_incs * node_inc self.Height = height - node_height.SetValue(min(max(int(height),0),node_height_max.GetValue())) + node_height.SetValue(min(max(int(height), 0), node_height_max.GetValue())) else: print("Height is not implemented or not writable") @@ -1086,69 +1129,68 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): node_min = node_offset_x.GetMin() node_max = node_offset_x.GetMax() node_inc = node_offset_x.GetInc() - diff = offset_x-node_min - num_incs = diff//node_inc - offset_x = node_min+num_incs*node_inc + diff = offset_x - node_min + num_incs = diff // node_inc + offset_x = node_min + num_incs * node_inc self.OffsetX = offset_x node_offset_x.SetValue(min(int(offset_x), node_max)) else: print("OffsetX is not implemented or not writable") - + if offset_y is not None: # update the camera setting if PySpin.IsWritable(node_offset_y): node_min = node_offset_y.GetMin() node_max = node_offset_y.GetMax() node_inc = node_offset_y.GetInc() - diff = offset_y-node_min - num_incs = diff//node_inc - offset_y = node_min+num_incs*node_inc + diff = offset_y - node_min + num_incs = diff // node_inc + offset_y = node_min + num_incs * node_inc self.OffsetY = offset_y node_offset_y.SetValue(min(int(offset_y), node_max)) else: print("OffsetY is not implemented or not writable") - # restart streaming if it was previously on if was_streaming == True: self.start_streaming() def reset_camera_acquisition_counter(self): self.nodemap = self.camera.GetNodeMap() - node_counter_event_source = PySpin.CEnumerationPtr(self.nodemap.GetNode('CounterEventSource')) - node_counter_event_source_line2 = PySpin.CEnumEntryPtr(node_counter_event_source.GetEntryByName('Line2')) + node_counter_event_source = PySpin.CEnumerationPtr(self.nodemap.GetNode("CounterEventSource")) + node_counter_event_source_line2 = PySpin.CEnumEntryPtr(node_counter_event_source.GetEntryByName("Line2")) if PySpin.IsWritable(node_counter_event_source) and PySpin.IsReadable(node_counter_event_source_line2): node_counter_event_source.SetIntValue(node_counter_event_source_line2.GetValue()) else: print("CounterEventSource is not implemented or not writable, or Line 2 is not an option") - node_counter_reset = PySpin.CCommandPtr(self.nodemap.GetNode('CounterReset')) + node_counter_reset = PySpin.CCommandPtr(self.nodemap.GetNode("CounterReset")) if PySpin.IsImplemented(node_counter_reset) and PySpin.IsWritable(node_counter_reset): node_counter_reset.Execute() else: print("CounterReset is not implemented") - def set_line3_to_strobe(self): #FLIR cams don't have the right Line layout for this + def set_line3_to_strobe(self): # FLIR cams don't have the right Line layout for this # self.camera.StrobeSwitch.set(gx.GxSwitchEntry.ON) - #self.nodemap = self.camera.GetNodeMap() - - #node_line_selector = PySpin.CEnumerationPtr(self.nodemap.GetNode('LineSelector')) - - #node_line3 = PySpin.CEnumEntryPtr(node_line_selector.GetEntryByName('Line3')) - - #self.camera.LineSelector.set(gx.GxLineSelectorEntry.LINE3) - #self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) - #self.camera.LineSource.set(gx.GxLineSourceEntry.STROBE) + # self.nodemap = self.camera.GetNodeMap() + + # node_line_selector = PySpin.CEnumerationPtr(self.nodemap.GetNode('LineSelector')) + + # node_line3 = PySpin.CEnumEntryPtr(node_line_selector.GetEntryByName('Line3')) + + # self.camera.LineSelector.set(gx.GxLineSelectorEntry.LINE3) + # self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) + # self.camera.LineSource.set(gx.GxLineSourceEntry.STROBE) pass - - def set_line3_to_exposure_active(self): #BlackFly cam has no output on Line 3 + + def set_line3_to_exposure_active(self): # BlackFly cam has no output on Line 3 # self.camera.StrobeSwitch.set(gx.GxSwitchEntry.ON) - #self.camera.LineSelector.set(gx.GxLineSelectorEntry.LINE3) - #self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) - #self.camera.LineSource.set(gx.GxLineSourceEntry.EXPOSURE_ACTIVE) + # self.camera.LineSelector.set(gx.GxLineSelectorEntry.LINE3) + # self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) + # self.camera.LineSource.set(gx.GxLineSourceEntry.EXPOSURE_ACTIVE) pass def __del__(self): @@ -1160,4 +1202,3 @@ def __del__(self): pass self.camera_list.Clear() self.py_spin_system.ReleaseInstance() - diff --git a/software/control/camera_hamamatsu.py b/software/control/camera_hamamatsu.py index 5ccf74697..3a7e96fbc 100644 --- a/software/control/camera_hamamatsu.py +++ b/software/control/camera_hamamatsu.py @@ -8,11 +8,12 @@ from control.dcamapi4 import * from control._def import * + def get_sn_by_model(model_name): try: _, count = Dcamapi.init() except TypeError: - print('Cannot init Hamamatsu Camera.') + print("Cannot init Hamamatsu Camera.") sys.exit(1) for i in range(count): @@ -20,15 +21,17 @@ def get_sn_by_model(model_name): sn = d.dev_getstring(DCAM_IDSTR.CAMERAID) if sn is not False: Dcamapi.uninit() - print('Hamamatsu Camera ' + sn) + print("Hamamatsu Camera " + sn) return sn - + Dcamapi.uninit() - return None + return None class Camera(object): - def __init__(self,sn=None, resolution=(2304,2304), is_global_shutter=False, rotate_image_angle=None, flip_image=None): + def __init__( + self, sn=None, resolution=(2304, 2304), is_global_shutter=False, rotate_image_angle=None, flip_image=None + ): self.dcam = None self.exposure_time = 1 # ms self.analog_gain = 0 @@ -55,7 +58,7 @@ def __init__(self,sn=None, resolution=(2304,2304), is_global_shutter=False, rota self.GAIN_STEP = 0 self.EXPOSURE_TIME_MS_MIN = 0.017633 self.EXPOSURE_TIME_MS_MAX = 10000.0046 - + self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image self.is_global_shutter = is_global_shutter @@ -66,7 +69,7 @@ def __init__(self,sn=None, resolution=(2304,2304), is_global_shutter=False, rota self.ROI_width = 2304 self.ROI_height = 2304 - self.OffsetX = 0 + self.OffsetX = 0 self.OffsetY = 0 self.Width = 2304 self.Height = 2304 @@ -80,8 +83,8 @@ def open(self, index=0): result = self.dcam.dev_open(index) and result if result: self.calculate_strobe_delay() - print('Hamamatsu Camera opened: ' + str(result)) - + print("Hamamatsu Camera opened: " + str(result)) + def open_by_sn(self, sn): unopened = 0 success, count = Dcamapi.init() @@ -95,14 +98,14 @@ def open_by_sn(self, sn): else: unopened += 1 if unopened == count or not success: - print('Hamamatsu Camera open_by_sn: No camera is opened.') + print("Hamamatsu Camera open_by_sn: No camera is opened.") def close(self): if self.is_streaming: self.stop_streaming() self.disable_callback() result = self.dcam.dev_close() and Dcamapi.uninit() - print('Hamamatsu Camera closed: ' + str(result)) + print("Hamamatsu Camera closed: " + str(result)) def set_callback(self, function): self.new_image_callback_external = function @@ -129,20 +132,20 @@ def _wait_and_callback(self): event = self.dcam.wait_event(DCAMWAIT_CAPEVENT.FRAMEREADY, 1000) if event is not False: self._on_new_frame() - + def _on_new_frame(self): image = self.read_frame(no_wait=True) if image is False: - print('Cannot get new frame from buffer.') + print("Cannot get new frame from buffer.") return if self.image_locked: - print('Last image is still being processed; a frame is dropped') + print("Last image is still being processed; a frame is dropped") return self.current_frame = image self.frame_ID_software += 1 - self.frame_ID += 1 + self.frame_ID += 1 # frame ID for hardware triggered acquisition if self.trigger_mode == TriggerMode.HARDWARE: @@ -152,7 +155,7 @@ def _on_new_frame(self): self.timestamp = time.time() self.new_image_callback_external(self) - + def disable_callback(self): if not self.callback_is_enabled: return @@ -169,7 +172,7 @@ def disable_callback(self): if was_streaming: self.start_streaming() - + def set_analog_gain(self, gain): pass @@ -227,74 +230,74 @@ def set_pixel_format(self, pixel_format): self.pixel_format = pixel_format - if pixel_format == 'MONO8': + if pixel_format == "MONO8": result = self.dcam.prop_setvalue(DCAM_IDPROP.IMAGE_PIXELTYPE, DCAM_PIXELTYPE.MONO8) - elif pixel_format == 'MONO16': + elif pixel_format == "MONO16": result = self.dcam.prop_setvalue(DCAM_IDPROP.IMAGE_PIXELTYPE, DCAM_PIXELTYPE.MONO16) if was_streaming: self.start_streaming() - print('Set pixel format: ' + str(result)) + print("Set pixel format: " + str(result)) def send_trigger(self): if self.is_streaming: if not self.dcam.cap_firetrigger(): - print('trigger not sent - firetrigger failed') + print("trigger not sent - firetrigger failed") else: - print('trigger not sent - camera is not streaming') + print("trigger not sent - camera is not streaming") def read_frame(self, no_wait=False): if no_wait: return self.dcam.buf_getlastframedata() - else: + else: if self.dcam.wait_capevent_frameready(5000) is not False: data = self.dcam.buf_getlastframedata() return data dcamerr = self.dcam.lasterr() if dcamerr.is_timeout(): - print('===: timeout') + print("===: timeout") + + print("-NG: Dcam.wait_event() fails with error {}".format(dcamerr)) - print('-NG: Dcam.wait_event() fails with error {}'.format(dcamerr)) - def start_streaming(self, buffer_frame_num=5): if self.is_streaming: return if self.dcam.buf_alloc(buffer_frame_num): if self.dcam.cap_start(True): self.is_streaming = True - print('Hamamatsu Camera starts streaming') + print("Hamamatsu Camera starts streaming") return else: self.dcam.buf_release() - print('Hamamatsu Camera cannot start streaming') + print("Hamamatsu Camera cannot start streaming") def stop_streaming(self): if self.dcam.cap_stop() and self.dcam.buf_release(): self.is_streaming = False - print('Hamamatsu Camera streaming stopped') + print("Hamamatsu Camera streaming stopped") else: - print('Hamamatsu Camera cannot stop streaming') + print("Hamamatsu Camera cannot stop streaming") - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): if offset_x is not None: - ROI_offset_x = 2*(offset_x//2) + ROI_offset_x = 2 * (offset_x // 2) else: ROI_offset_x = self.ROI_offset_x if offset_y is not None: - ROI_offset_y = 2*(offset_y//2) + ROI_offset_y = 2 * (offset_y // 2) else: ROI_offset_y = self.ROI_offset_y if width is not None: - ROI_width = max(16,2*(width//2)) + ROI_width = max(16, 2 * (width // 2)) else: ROI_width = self.ROI_width if height is not None: - ROI_height = max(16,2*(height//2)) + ROI_height = max(16, 2 * (height // 2)) else: ROI_height = self.ROI_height @@ -319,12 +322,15 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): self.start_streaming() def calculate_strobe_delay(self): - self.strobe_delay_us = int(self.dcam.prop_getvalue(DCAM_IDPROP.INTERNAL_LINEINTERVAL) * 1000000 * 2304 + self.dcam.prop_getvalue(DCAM_IDPROP.TRIGGERDELAY) *1000000) # s to us + self.strobe_delay_us = int( + self.dcam.prop_getvalue(DCAM_IDPROP.INTERNAL_LINEINTERVAL) * 1000000 * 2304 + + self.dcam.prop_getvalue(DCAM_IDPROP.TRIGGERDELAY) * 1000000 + ) # s to us class Camera_Simulation(object): - - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None): + + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): # many to be purged self.sn = sn self.is_global_shutter = is_global_shutter @@ -360,7 +366,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None - self.pixel_format = 'MONO16' + self.pixel_format = "MONO16" self.is_live = False @@ -373,11 +379,10 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.new_image_callback_external = None - - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -386,16 +391,16 @@ def enable_callback(self): def disable_callback(self): self.callback_is_enabled = False - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): pass - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): pass - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): pass def start_streaming(self): @@ -404,7 +409,7 @@ def start_streaming(self): def stop_streaming(self): pass - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): self.pixel_format = pixel_format print(pixel_format) self.frame_ID = 0 @@ -419,19 +424,19 @@ def set_hardware_triggered_acquisition(self): pass def send_trigger(self): - print('send trigger') + print("send trigger") self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - if self.pixel_format == 'MONO8': - self.current_frame = np.random.randint(255,size=(2000,2000),dtype=np.uint8) - self.current_frame[901:1100,901:1100] = 200 - elif self.pixel_format == 'MONO16': - self.current_frame = np.random.randint(65535,size=(2000,2000),dtype=np.uint16) - self.current_frame[901:1100,901:1100] = 200*256 + if self.pixel_format == "MONO8": + self.current_frame = np.random.randint(255, size=(2000, 2000), dtype=np.uint8) + self.current_frame[901:1100, 901:1100] = 200 + elif self.pixel_format == "MONO16": + self.current_frame = np.random.randint(65535, size=(2000, 2000), dtype=np.uint16) + self.current_frame[901:1100, 901:1100] = 200 * 256 else: - self.current_frame = np.roll(self.current_frame,10,axis=0) - pass + self.current_frame = np.roll(self.current_frame, 10, axis=0) + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None and self.callback_is_enabled: self.new_image_callback_external(self) @@ -442,7 +447,7 @@ def read_frame(self): def _on_frame_callback(self, user_param, raw_image): pass - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): pass def set_line3_to_strobe(self): diff --git a/software/control/camera_ids.py b/software/control/camera_ids.py index f57cb1e0c..2a7386c34 100644 --- a/software/control/camera_ids.py +++ b/software/control/camera_ids.py @@ -9,14 +9,16 @@ from ids_peak import ids_peak_ipl_extension import squid.logging + log = squid.logging.get_logger(__name__) + def get_sn_by_model(model_name): ids_peak.Library.Initialize() device_manager = ids_peak.DeviceManager.Instance() device_manager.Update() if device_manager.Devices().empty(): - log.error('iDS camera not found.') + log.error("iDS camera not found.") # TODO(imo): Propagate error in some way and handle return devices = device_manager.Devices() @@ -25,8 +27,8 @@ def get_sn_by_model(model_name): nodemap = dev.RemoteDevice().NodeMaps()[i] sn = nodemap.FindNode("DeviceSerialNumber").Value() mn = nodemap.FindNode("DeviceModelName").Value() - #if mn == model_name: - #return nodemap.FindNode("DeviceSerialNumber").Value() + # if mn == model_name: + # return nodemap.FindNode("DeviceSerialNumber").Value() log.debug(f"get_sn_by_model: {mn}") return sn @@ -36,7 +38,9 @@ def get_sn_by_model(model_name): class Camera(object): - def __init__(self, sn=None, resolution=(1920,1080), is_global_shutter=False, rotate_image_angle=None, flip_image=None): + def __init__( + self, sn=None, resolution=(1920, 1080), is_global_shutter=False, rotate_image_angle=None, flip_image=None + ): self.log = squid.logging.get_logger(self.__class__.__name__) ids_peak.Library.Initialize() @@ -71,7 +75,7 @@ def __init__(self, sn=None, resolution=(1920,1080), is_global_shutter=False, rot self.GAIN_STEP = 0.01 self.EXPOSURE_TIME_MS_MIN = 0.015 self.EXPOSURE_TIME_MS_MAX = 1999 - + self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image self.is_global_shutter = is_global_shutter @@ -93,18 +97,18 @@ def __init__(self, sn=None, resolution=(1920,1080), is_global_shutter=False, rot def open(self, index=0): self.device_manager.Update() if self.device_manager.Devices().empty(): - self.log.error('iDS camera not found.') + self.log.error("iDS camera not found.") # TODO(imo): Propagate error in some way and handle return self.device = self.device_manager.Devices()[index].OpenDevice(ids_peak.DeviceAccessType_Control) if self.device is None: - self.log.error('Cannot open iDS camera.') + self.log.error("Cannot open iDS camera.") # TODO(imo): Propagate error in some way and handle return self.nodemap = self.device.RemoteDevice().NodeMaps()[0] self._camera_init() - self.log.info('iDS camera opened.') + self.log.info("iDS camera opened.") def open_by_sn(self, sn): self.device_manager.Update() @@ -114,26 +118,28 @@ def open_by_sn(self, sn): if sn == nodemap.FindNode("DeviceSerialNumber").Value(): self.device = dev.OpenDevice(ids_peak.DeviceAccessType_Control) if self.device is None: - self.log.error('Cannot open iDS camera.') + self.log.error("Cannot open iDS camera.") # TODO(imo): Propagate error in some way and handle return self.nodemap = nodemap self._camera_init() - self.log.info(f'iDS camera opened by sn={sn}.') + self.log.info(f"iDS camera opened by sn={sn}.") return - self.log.error('No iDS camera is opened.') + self.log.error("No iDS camera is opened.") # TODO(imo): Propagate error in some way and handle return def _camera_init(self): gain_node = self.nodemap.FindNode("Gain") - self.log.info(f'gain: min={gain_node.Minimum()}, max={gain_node.Maximum()}, increment={gain_node.Increment()}') + self.log.info(f"gain: min={gain_node.Minimum()}, max={gain_node.Maximum()}, increment={gain_node.Increment()}") # initialize software trigger entries = [] for entry in self.nodemap.FindNode("TriggerSelector").Entries(): - if (entry.AccessStatus() != ids_peak.NodeAccessStatus_NotAvailable - and entry.AccessStatus() != ids_peak.NodeAccessStatus_NotImplemented): + if ( + entry.AccessStatus() != ids_peak.NodeAccessStatus_NotAvailable + and entry.AccessStatus() != ids_peak.NodeAccessStatus_NotImplemented + ): entries.append(entry.SymbolicValue()) if len(entries) == 0: @@ -193,16 +199,16 @@ def _on_new_frame(self, buffer): image = self.read_frame(no_wait=True, buffer=buffer) if image is False: # TODO(imo): Propagate error in some way and handle - self.log.error('Cannot get new frame from buffer.') + self.log.error("Cannot get new frame from buffer.") return if self.image_locked: # TODO(imo): Propagate error in some way and handle - self.log.error('Last image is still being processed; a frame is dropped') + self.log.error("Last image is still being processed; a frame is dropped") return self.current_frame = image self.frame_ID_software += 1 - self.frame_ID += 1 + self.frame_ID += 1 # frame ID for hardware triggered acquisition if self.trigger_mode == TriggerMode.HARDWARE: @@ -263,19 +269,19 @@ def set_hardware_triggered_acquisition(self): self.nodemap.FindNode("TriggerSource").SetCurrentEntry("Line0") self.trigger_mode = TriggerMode.HARDWARE - def set_pixel_format(self, pixel_format): + def set_pixel_format(self, pixel_format): self.log.debug(f"Pixel format={pixel_format}") was_streaming = False if self.is_streaming: was_streaming = True self.stop_streaming() try: - if pixel_format == 'MONO10': + if pixel_format == "MONO10": self.nodemap.FindNode("PixelFormat").SetCurrentEntry("Mono10g40IDS") - elif pixel_format == 'MONO12': + elif pixel_format == "MONO12": self.nodemap.FindNode("PixelFormat").SetCurrentEntry("Mono12g24IDS") else: - raise Exception('Wrong pixel format.') + raise Exception("Wrong pixel format.") self.pixel_format = pixel_format if was_streaming: @@ -287,7 +293,7 @@ def send_trigger(self): if self.is_streaming: self.nodemap.FindNode("TriggerSoftware").Execute() self.nodemap.FindNode("TriggerSoftware").WaitUntilDone() - self.log.debug('Trigger sent') + self.log.debug("Trigger sent") def read_frame(self, no_wait=False, buffer=None): if not no_wait: @@ -295,16 +301,18 @@ def read_frame(self, no_wait=False, buffer=None): self.log.debug("Buffered image!") # Convert image and make deep copy - if self.pixel_format == 'MONO10': + if self.pixel_format == "MONO10": output_pixel_format = ids_peak_ipl.PixelFormatName_Mono10 - elif self.pixel_format == 'MONO12': + elif self.pixel_format == "MONO12": output_pixel_format = ids_peak_ipl.PixelFormatName_Mono12 ipl_image = ids_peak_ipl_extension.BufferToImage(buffer) ipl_converted = self.image_converter.Convert(ipl_image, output_pixel_format) numpy_image = ipl_converted.get_numpy_1D().copy() - self.current_frame = np.frombuffer(numpy_image, dtype=np.uint16).reshape(ipl_converted.Height(), ipl_converted.Width()) + self.current_frame = np.frombuffer(numpy_image, dtype=np.uint16).reshape( + ipl_converted.Height(), ipl_converted.Width() + ) self.datastream.QueueBuffer(buffer) @@ -313,7 +321,7 @@ def read_frame(self, no_wait=False, buffer=None): def start_streaming(self, extra_buffer=1): if self.is_streaming: return - + # Allocate image buffer for image acquisition self._revoke_buffer() self._allocate_buffer(extra_buffer) @@ -327,15 +335,13 @@ def start_streaming(self, extra_buffer=1): # while the acquisition is running # NOTE: Re-create the image converter, so old conversion buffers # get freed - input_pixel_format = ids_peak_ipl.PixelFormat( - self.nodemap.FindNode("PixelFormat").CurrentEntry().Value()) - if self.pixel_format == 'MONO10': + input_pixel_format = ids_peak_ipl.PixelFormat(self.nodemap.FindNode("PixelFormat").CurrentEntry().Value()) + if self.pixel_format == "MONO10": output_pixel_format = ids_peak_ipl.PixelFormatName_Mono10 - elif self.pixel_format == 'MONO12': + elif self.pixel_format == "MONO12": output_pixel_format = ids_peak_ipl.PixelFormatName_Mono12 - self.image_converter.PreAllocateConversion( - input_pixel_format, output_pixel_format, self.Width, self.Height) + self.image_converter.PreAllocateConversion(input_pixel_format, output_pixel_format, self.Width, self.Height) self.datastream.StartAcquisition() self.nodemap.FindNode("AcquisitionStart").Execute() @@ -397,10 +403,9 @@ def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): class Camera_Simulation(object): - + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): - self.log = squid.logging.get_logger(self.__class__.__name__ - ) + self.log = squid.logging.get_logger(self.__class__.__name__) # many to be purged self.sn = sn self.is_global_shutter = is_global_shutter @@ -436,7 +441,7 @@ def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, fl self.trigger_mode = None - self.pixel_format = 'MONO12' + self.pixel_format = "MONO12" self.is_live = False @@ -494,15 +499,15 @@ def set_hardware_triggered_acquisition(self): pass def send_trigger(self): - self.log.info('send trigger') + self.log.info("send trigger") self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - self.current_frame = np.random.randint(255, size=(2000,2000), dtype=np.uint8) - self.current_frame[901:1100,901:1100] = 200 + self.current_frame = np.random.randint(255, size=(2000, 2000), dtype=np.uint8) + self.current_frame[901:1100, 901:1100] = 200 else: self.current_frame = np.roll(self.current_frame, 10, axis=0) - pass + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None and self.callback_is_enabled: self.new_image_callback_external(self) diff --git a/software/control/camera_toupcam.py b/software/control/camera_toupcam.py index 311b0f18f..ac6451f21 100644 --- a/software/control/camera_toupcam.py +++ b/software/control/camera_toupcam.py @@ -10,6 +10,7 @@ log = squid.logging.get_logger(__name__) + def get_sn_by_model(model_name): try: device_list = toupcam.Toupcam.EnumV2() @@ -19,7 +20,7 @@ def get_sn_by_model(model_name): for dev in device_list: if dev.displayname == model_name: return dev.id - return None # return None if no device with the specified model_name is connected + return None # return None if no device with the specified model_name is connected class Camera(object): @@ -32,18 +33,20 @@ def _event_callback(nEvent, camera): camera._software_trigger_sent = False def _on_frame_callback(self): - + # check if the last image is still locked if self.image_locked: - self.log.warning('last image is still being processed, a frame is dropped') + self.log.warning("last image is still being processed, a frame is dropped") return # get the image from the camera try: - self.camera.PullImageV2(self.buf, self.pixel_size_byte*8, None) # the second camera is number of bits per pixel - ignored in RAW mode + self.camera.PullImageV2( + self.buf, self.pixel_size_byte * 8, None + ) # the second camera is number of bits per pixel - ignored in RAW mode except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('pull image failed, hr=0x{:x}'.format(ex.hr)) + self.log.error("pull image failed, hr=0x{:x}".format(ex.hr)) # increament frame ID self.frame_ID_software += 1 @@ -51,17 +54,17 @@ def _on_frame_callback(self): self.timestamp = time.time() # right now support the raw format only - if self.data_format == 'RGB': - if self.pixel_format == 'RGB24': + if self.data_format == "RGB": + if self.pixel_format == "RGB24": # TODO(imo): Propagate error in some way and handle - self.log.error('convert buffer to image not yet implemented for the RGB format') + self.log.error("convert buffer to image not yet implemented for the RGB format") return else: if self.pixel_size_byte == 1: - raw_image = np.frombuffer(self.buf, dtype='uint8') + raw_image = np.frombuffer(self.buf, dtype="uint8") elif self.pixel_size_byte == 2: - raw_image = np.frombuffer(self.buf, dtype='uint16') - self.current_frame = raw_image.reshape(self.Height,self.Width) + raw_image = np.frombuffer(self.buf, dtype="uint16") + self.current_frame = raw_image.reshape(self.Height, self.Width) # frame ID for hardware triggered acquisition if self.trigger_mode == TriggerMode.HARDWARE: @@ -77,7 +80,9 @@ def _on_frame_callback(self): def _TDIBWIDTHBYTES(w): return (w * 24 + 31) // 32 * 4 - def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_image_angle=None,flip_image=None): + def __init__( + self, sn=None, resolution=(3104, 2084), is_global_shutter=False, rotate_image_angle=None, flip_image=None + ): self.log = squid.logging.get_logger(self.__class__.__name__) # many to be purged @@ -94,7 +99,7 @@ def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_ self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image - self.exposure_time = 1 # unit: ms + self.exposure_time = 1 # unit: ms self.analog_gain = 0 self.frame_ID = -1 self.frame_ID_software = -1 @@ -121,20 +126,22 @@ def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_ self.trigger_mode = None self.pixel_size_byte = 1 - # below are values for IMX226 (MER2-1220-32U3M) - to make configurable + # below are values for IMX226 (MER2-1220-32U3M) - to make configurable self.row_period_us = 10 self.row_numbers = 3036 self.exposure_delay_us_8bit = 650 - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte # just setting a default value # it would be re-calculate with function calculate_hardware_trigger_arguments - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) - self.pixel_format = None # use the default pixel format + self.pixel_format = None # use the default pixel format # toupcam - self.data_format = 'RAW' + self.data_format = "RAW" self.devices = toupcam.Toupcam.EnumV2() self.image_is_ready = False self._toupcam_pullmode_started = False @@ -144,8 +151,8 @@ def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_ # the balcklevel factor # 8 bits: 1 # 10 bits: 4 - # 12 bits: 16 - # 14 bits: 64 + # 12 bits: 16 + # 14 bits: 64 # 16 bits: 256 self.blacklevel_factor = 1 @@ -160,11 +167,11 @@ def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_ self.terminate_read_temperature_thread = False self.thread_read_temperature = threading.Thread(target=self.check_temperature, daemon=True) - self.brand = 'ToupTek' - + self.brand = "ToupTek" + self.res_list = [] - self.OffsetX = CAMERA_CONFIG.ROI_OFFSET_X_DEFAULT + self.OffsetX = CAMERA_CONFIG.ROI_OFFSET_X_DEFAULT self.OffsetY = CAMERA_CONFIG.ROI_OFFSET_X_DEFAULT self.Width = CAMERA_CONFIG.ROI_WIDTH_DEFAULT self.Height = CAMERA_CONFIG.ROI_HEIGHT_DEFAULT @@ -182,43 +189,50 @@ def __init__(self,sn=None,resolution=(3104,2084),is_global_shutter=False,rotate_ def check_temperature(self): while self.terminate_read_temperature_thread == False: time.sleep(2) - temperature = self.get_temperature() + temperature = self.get_temperature() if self.temperature_reading_callback is not None: try: self.temperature_reading_callback(temperature) except TypeError as ex: - self.log.error("Temperature read callback failed due to error: "+repr(ex)) + self.log.error("Temperature read callback failed due to error: " + repr(ex)) pass - def open(self,index=0): + def open(self, index=0): if len(self.devices) > 0: - self.log.info('{}: flag = {:#x}, preview = {}, still = {}'.format(self.devices[0].displayname, self.devices[0].model.flag, self.devices[0].model.preview, self.devices[0].model.still)) + self.log.info( + "{}: flag = {:#x}, preview = {}, still = {}".format( + self.devices[0].displayname, + self.devices[0].model.flag, + self.devices[0].model.preview, + self.devices[0].model.still, + ) + ) for r in self.devices[index].model.res: - self.log.info('\t = [{} x {}]'.format(r.width, r.height)) + self.log.info("\t = [{} x {}]".format(r.width, r.height)) if self.sn is not None: index = [idx for idx in range(len(self.devices)) if self.devices[idx].id == self.sn][0] - highest_res = (0,0) + highest_res = (0, 0) self.res_list = [] for r in self.devices[index].model.res: - self.res_list.append((r.width,r.height)) + self.res_list.append((r.width, r.height)) if r.width > highest_res[0] or r.height > highest_res[1]: highest_res = (r.width, r.height) self.camera = toupcam.Toupcam.Open(self.devices[index].id) - self.has_fan = ( self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_FAN ) > 0 - self.has_TEC = ( self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_TEC_ONOFF ) > 0 - self.has_low_noise_mode = ( self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_LOW_NOISE ) > 0 + self.has_fan = (self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_FAN) > 0 + self.has_TEC = (self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_TEC_ONOFF) > 0 + self.has_low_noise_mode = (self.devices[index].model.flag & toupcam.TOUPCAM_FLAG_LOW_NOISE) > 0 if self.has_low_noise_mode: - self.camera.put_Option(toupcam.TOUPCAM_OPTION_LOW_NOISE,0) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_LOW_NOISE, 0) # RGB format: The output of every pixel contains 3 componants which stand for R/G/B value respectively. This output is a processed output from the internal color processing engine. # RAW format: In this format, the output is the raw data directly output from the sensor. The RAW format is for the users that want to skip the internal color processing and obtain the raw data for user-specific purpose. With the raw format output enabled, the functions that are related to the internal color processing will not work, such as Toupcam_put_Hue or Toupcam_AwbOnce function and so on - + # set temperature self.set_fan_speed(1) self.set_temperature(20) - self.set_data_format('RAW') - self.set_pixel_format('MONO16') # 'MONO8' + self.set_data_format("RAW") + self.set_pixel_format("MONO16") # 'MONO8' self.set_auto_exposure(False) self.set_blacklevel(DEFAULT_BLACKLEVEL_VALUE) @@ -229,22 +243,22 @@ def open(self,index=0): self.resolution = highest_res # set camera resolution - self.set_resolution(self.resolution[0],self.resolution[1]) # buffer created when setting resolution + self.set_resolution(self.resolution[0], self.resolution[1]) # buffer created when setting resolution self._update_buffer_settings() - + if self.camera: if self.buf: try: self.camera.StartPullModeWithCallback(self._event_callback, self) except toupcam.HRESULTException as ex: - self.log.error('failed to start camera, hr=0x{:x}'.format(ex.hr)) + self.log.error("failed to start camera, hr=0x{:x}".format(ex.hr)) raise ex self._toupcam_pullmode_started = True else: - self.log.error('failed to open camera') + self.log.error("failed to open camera") raise RuntimeError("Couldn't open camera") else: - self.log.error('no camera found') + self.log.error("no camera found") self.is_color = False if self.is_color: @@ -252,7 +266,7 @@ def open(self,index=0): self.thread_read_temperature.start() - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def set_temperature_reading_callback(self, func): @@ -264,7 +278,7 @@ def enable_callback(self): def disable_callback(self): self.callback_is_enabled = False - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): @@ -278,7 +292,7 @@ def close(self): self.last_converted_image = None self.last_numpy_image = None - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): # use_strobe = (self.trigger_mode == TriggerMode.HARDWARE) # true if using hardware trigger # if use_strobe == False or self.is_global_shutter: # self.exposure_time = exposure_time @@ -293,9 +307,9 @@ def set_exposure_time(self,exposure_time): # exposure time in ms if self.trigger_mode == TriggerMode.HARDWARE: - self.camera.put_ExpoTime(int(exposure_time*1000) + int(self.strobe_delay_us)) + self.camera.put_ExpoTime(int(exposure_time * 1000) + int(self.strobe_delay_us)) else: - self.camera.put_ExpoTime(int(exposure_time*1000)) + self.camera.put_ExpoTime(int(exposure_time * 1000)) def update_camera_exposure_time(self): pass @@ -306,13 +320,13 @@ def update_camera_exposure_time(self): # camera_exposure_time = self.exposure_delay_us + self.exposure_time*1000 + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + 500 # add an additional 500 us so that the illumination can fully turn off before rows start to end exposure # self.camera.ExposureTime.set(camera_exposure_time) - def set_analog_gain(self,analog_gain): - analog_gain = min(self.GAIN_MAX,analog_gain) - analog_gain = max(self.GAIN_MIN,analog_gain) + def set_analog_gain(self, analog_gain): + analog_gain = min(self.GAIN_MAX, analog_gain) + analog_gain = max(self.GAIN_MIN, analog_gain) self.analog_gain = analog_gain # gain_min, gain_max, gain_default = self.camera.get_ExpoAGainRange() # remove from set_analog_gain # for touptek cameras gain is 100-10000 (for 1x - 100x) - self.camera.put_ExpoAGain(int(100*(10**(analog_gain/20)))) + self.camera.put_ExpoAGain(int(100 * (10 ** (analog_gain / 20)))) # self.camera.Gain.set(analog_gain) def get_awb_ratios(self): @@ -320,21 +334,21 @@ def get_awb_ratios(self): self.camera.AwbInit() return self.camera.get_WhiteBalanceGain() except toupcam.HRESULTException as ex: - err_type = hresult_checker(ex,'E_NOTIMPL') + err_type = hresult_checker(ex, "E_NOTIMPL") self.log.warning("AWB not implemented") - return (0,0,0) + return (0, 0, 0) def set_wb_ratios(self, wb_r=None, wb_g=None, wb_b=None): try: - camera.put_WhiteBalanceGain(wb_r,wb_g,wb_b) + camera.put_WhiteBalanceGain(wb_r, wb_g, wb_b) except toupcam.HRESULTException as ex: - err_type = hresult_checker(ex,'E_NOTIMPL') + err_type = hresult_checker(ex, "E_NOTIMPL") self.log.warning("White balance not implemented") - def set_reverse_x(self,value): + def set_reverse_x(self, value): pass - def set_reverse_y(self,value): + def set_reverse_y(self, value): pass def start_streaming(self): @@ -343,11 +357,11 @@ def start_streaming(self): self.camera.StartPullModeWithCallback(self._event_callback, self) self._toupcam_pullmode_started = True except toupcam.HRESULTException as ex: - self.log.error('failed to start camera, hr: '+hresult_checker(ex)) + self.log.error("failed to start camera, hr: " + hresult_checker(ex)) self.close() # TODO(imo): Remove sys.exit and propagate+handle. sys.exit(1) - self.log.info('start streaming') + self.log.info("start streaming") self.is_streaming = True def stop_streaming(self): @@ -355,7 +369,7 @@ def stop_streaming(self): self.is_streaming = False self._toupcam_pullmode_started = False - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): was_streaming = False if self.is_streaming: @@ -363,53 +377,53 @@ def set_pixel_format(self,pixel_format): self.stop_streaming() self.pixel_format = pixel_format - if self.data_format == 'RAW': - if pixel_format == 'MONO8': + if self.data_format == "RAW": + if pixel_format == "MONO8": self.pixel_size_byte = 1 self.blacklevel_factor = 1 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,0) - elif pixel_format == 'MONO12': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 0) + elif pixel_format == "MONO12": self.pixel_size_byte = 2 self.blacklevel_factor = 16 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - elif pixel_format == 'MONO14': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + elif pixel_format == "MONO14": self.pixel_size_byte = 2 self.blacklevel_factor = 64 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - elif pixel_format == 'MONO16': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + elif pixel_format == "MONO16": self.pixel_size_byte = 2 self.blacklevel_factor = 256 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) else: # RGB data format - if pixel_format == 'MONO8': + if pixel_format == "MONO8": self.pixel_size_byte = 1 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,0) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,3) # for monochrome camera only - if pixel_format == 'MONO12': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 0) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 3) # for monochrome camera only + if pixel_format == "MONO12": self.pixel_size_byte = 2 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,4) # for monochrome camera only - if pixel_format == 'MONO14': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 4) # for monochrome camera only + if pixel_format == "MONO14": self.pixel_size_byte = 2 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,4) # for monochrome camera only - if pixel_format == 'MONO16': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 4) # for monochrome camera only + if pixel_format == "MONO16": self.pixel_size_byte = 2 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,4) # for monochrome camera only - if pixel_format == 'RGB24': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 4) # for monochrome camera only + if pixel_format == "RGB24": self.pixel_size_byte = 3 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,0) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,0) - if pixel_format == 'RGB32': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 0) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 0) + if pixel_format == "RGB32": self.pixel_size_byte = 4 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,0) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,2) - if pixel_format == 'RGB48': + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 0) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 2) + if pixel_format == "RGB48": self.pixel_size_byte = 6 - self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH,1) - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB,1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_BITDEPTH, 1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RGB, 1) self._update_buffer_settings() @@ -419,36 +433,36 @@ def set_pixel_format(self,pixel_format): if self.reset_strobe_delay is not None: self.reset_strobe_delay() - # It is forbidden to call Toupcam_put_Option with TOUPCAM_OPTION_BITDEPTH in the callback context of + # It is forbidden to call Toupcam_put_Option with TOUPCAM_OPTION_BITDEPTH in the callback context of # PTOUPCAM_EVENT_CALLBACK and PTOUPCAM_DATA_CALLBACK_V3, the return value is E_WRONG_THREAD - def set_auto_exposure(self,enabled): + def set_auto_exposure(self, enabled): try: self.camera.put_AutoExpoEnable(enabled) except toupcam.HRESULTException as ex: - self.log.error("Unable to set auto exposure: "+repr(ex)) + self.log.error("Unable to set auto exposure: " + repr(ex)) # TODO(imo): Propagate error in some way and handle - def set_data_format(self,data_format): + def set_data_format(self, data_format): self.data_format = data_format - if data_format == 'RGB': - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RAW,0) # 0 is RGB mode, 1 is RAW mode - elif data_format == 'RAW': - self.camera.put_Option(toupcam.TOUPCAM_OPTION_RAW,1) # 1 is RAW mode, 0 is RGB mode + if data_format == "RGB": + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RAW, 0) # 0 is RGB mode, 1 is RAW mode + elif data_format == "RAW": + self.camera.put_Option(toupcam.TOUPCAM_OPTION_RAW, 1) # 1 is RAW mode, 0 is RGB mode - def set_resolution(self,width,height): + def set_resolution(self, width, height): was_streaming = False if self.is_streaming: self.stop_streaming() was_streaming = True try: - self.camera.put_Size(width,height) + self.camera.put_Size(width, height) except toupcam.HRESULTException as ex: - err_type = hresult_checker(ex,'E_INVALIDARG','E_BUSY','E_ACCESDENIED', 'E_UNEXPECTED') - if err_type == 'E_INVALIDARG': + err_type = hresult_checker(ex, "E_INVALIDARG", "E_BUSY", "E_ACCESDENIED", "E_UNEXPECTED") + if err_type == "E_INVALIDARG": self.log.error(f"Resolution ({width},{height}) not supported by camera") else: - self.log.error(f"Resolution cannot be set due to error: "+err_type) + self.log.error(f"Resolution cannot be set due to error: " + err_type) # TODO(imo): Propagate error in some way and handle self._update_buffer_settings() if was_streaming: @@ -465,54 +479,54 @@ def _update_buffer_settings(self): self.Height = height # calculate buffer size - if (self.data_format == 'RGB') & (self.pixel_size_byte != 4): + if (self.data_format == "RGB") & (self.pixel_size_byte != 4): bufsize = _TDIBWIDTHBYTES(width * self.pixel_size_byte * 8) * height else: bufsize = width * self.pixel_size_byte * height - self.log.info('image size: {} x {}, bufsize = {}'.format(width, height, bufsize)) + self.log.info("image size: {} x {}, bufsize = {}".format(width, height, bufsize)) # create the buffer self.buf = bytes(bufsize) def get_temperature(self): try: - return self.camera.get_Temperature()/10 + return self.camera.get_Temperature() / 10 except toupcam.HRESULTException as ex: error_type = hresult_checker(ex) - self.log.debug("Could not get temperature, error: "+error_type) + self.log.debug("Could not get temperature, error: " + error_type) # TODO(imo): Returning 0 temp here seems dangerous - probably indicate instead that we don't know the temp return 0 - def set_temperature(self,temperature): + def set_temperature(self, temperature): try: - self.camera.put_Temperature(int(temperature*10)) + self.camera.put_Temperature(int(temperature * 10)) except toupcam.HRESULTException as ex: error_type = hresult_checker(ex) # TODO(imo): Propagate error in some way and handle - self.log.error("Unable to set temperature: "+error_type) + self.log.error("Unable to set temperature: " + error_type) - def set_fan_speed(self,speed): + def set_fan_speed(self, speed): if self.has_fan: try: - self.camera.put_Option(toupcam.TOUPCAM_OPTION_FAN,speed) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_FAN, speed) except toupcam.HRESULTException as ex: error_type = hresult_checker(ex) # TODO(imo): Propagate error in some way and handle - self.log.error("Unable to set fan speed: "+error_type) + self.log.error("Unable to set fan speed: " + error_type) else: pass def set_continuous_acquisition(self): - self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER,0) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, 0) self.trigger_mode = TriggerMode.CONTINUOUS # self.update_camera_exposure_time() def set_software_triggered_acquisition(self): - self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER,1) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, 1) self.trigger_mode = TriggerMode.SOFTWARE # self.update_camera_exposure_time() def set_hardware_triggered_acquisition(self): - self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER,2) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, 2) self.frame_ID_offset_hardware_trigger = None self.trigger_mode = TriggerMode.HARDWARE @@ -533,31 +547,31 @@ def set_hardware_triggered_acquisition(self): self.log.error("Unable to set GPIO1 for trigger ready: " + error_type) def set_trigger_width_mode(self): - self.camera.IoControl(1, toupcam.TOUPCAM_IOCONTROLTYPE_SET_PWMSOURCE, 1) # set PWM source to GPIO0 - self.camera.IoControl(1, toupcam.TOUPCAM_IOCONTROLTYPE_SET_TRIGGERSOURCE, 4) # trigger source to PWM - - def set_gain_mode(self,mode): - if mode == 'LCG': - self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG,0) - elif mode == 'HCG': - self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG,1) - elif mode == 'HDR': - self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG,2) - + self.camera.IoControl(1, toupcam.TOUPCAM_IOCONTROLTYPE_SET_PWMSOURCE, 1) # set PWM source to GPIO0 + self.camera.IoControl(1, toupcam.TOUPCAM_IOCONTROLTYPE_SET_TRIGGERSOURCE, 4) # trigger source to PWM + + def set_gain_mode(self, mode): + if mode == "LCG": + self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG, 0) + elif mode == "HCG": + self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG, 1) + elif mode == "HDR": + self.camera.put_Option(toupcam.TOUPCAM_OPTION_CG, 2) + def send_trigger(self): - if self._last_software_trigger_timestamp!= None: - if (time.time() - self._last_software_trigger_timestamp) > (1.5*self.exposure_time/1000*1.02 + 4): - self.log.warning('last software trigger timed out') + if self._last_software_trigger_timestamp != None: + if (time.time() - self._last_software_trigger_timestamp) > (1.5 * self.exposure_time / 1000 * 1.02 + 4): + self.log.warning("last software trigger timed out") self._software_trigger_sent = False if self.is_streaming and (self._software_trigger_sent == False): self.camera.Trigger(1) self._software_trigger_sent = True self._last_software_trigger_timestamp = time.time() - self.log.debug('>>> trigger sent') + self.log.debug(">>> trigger sent") else: # TODO(imo): Propagate these errors in some way and handle if self.is_streaming == False: - self.logger.error('trigger not sent - camera is not streaming') + self.logger.error("trigger not sent - camera is not streaming") else: pass @@ -568,37 +582,37 @@ def stop_exposure(self): else: pass - def read_frame(self,reset_image_ready_flag=True): + def read_frame(self, reset_image_ready_flag=True): # set reset_image_ready_flag to True when read_frame() is called immediately after triggering the acquisition if reset_image_ready_flag: self.image_is_ready = False timestamp_t0 = time.time() - while (time.time() - timestamp_t0) <= (self.exposure_time/1000)*1.02 + 4: + while (time.time() - timestamp_t0) <= (self.exposure_time / 1000) * 1.02 + 4: time.sleep(0.005) if self.image_is_ready: self.image_is_ready = False return self.current_frame - self.log.error('read frame timed out') + self.log.error("read frame timed out") return None - - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): if offset_x is not None: - ROI_offset_x = 2*(offset_x//2) + ROI_offset_x = 2 * (offset_x // 2) else: ROI_offset_x = self.ROI_offset_x if offset_y is not None: - ROI_offset_y = 2*(offset_y//2) + ROI_offset_y = 2 * (offset_y // 2) else: ROI_offset_y = self.ROI_offset_y if width is not None: - ROI_width = max(16,2*(width//2)) + ROI_width = max(16, 2 * (width // 2)) else: ROI_width = self.ROI_width if height is not None: - ROI_height = max(16,2*(height//2)) + ROI_height = max(16, 2 * (height // 2)) else: ROI_height = self.ROI_height @@ -614,7 +628,7 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): self.OffsetY = 0 self.ROI_height = 0 self.ROI_width = 0 - self.camera.put_Roi(0,0,0,0) + self.camera.put_Roi(0, 0, 0, 0) width, height = self.camera.get_Size() self.Width = width self.Height = height @@ -624,7 +638,7 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): else: try: - self.camera.put_Roi(ROI_offset_x,ROI_offset_y,ROI_width,ROI_height) + self.camera.put_Roi(ROI_offset_x, ROI_offset_y, ROI_width, ROI_height) self.ROI_height = ROI_height self.Height = ROI_height self.ROI_width = ROI_width @@ -636,7 +650,7 @@ def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): self.ROI_offset_y = ROI_offset_y self.OffsetY = ROI_offset_y except toupcam.HRESULTException as ex: - err_type = hresult_checker(ex,'E_INVALIDARG') + err_type = hresult_checker(ex, "E_INVALIDARG") self.log.error("ROI bounds invalid, not changing ROI.") self._update_buffer_settings() if was_streaming: @@ -673,7 +687,7 @@ def set_line3_to_exposure_active(self): # self.camera.LineMode.set(gx.GxLineModeEntry.OUTPUT) # self.camera.LineSource.set(gx.GxLineSourceEntry.EXPOSURE_ACTIVE) pass - + def calculate_hardware_trigger_arguments(self): # use camera arguments such as resolutuon, ROI, exposure time, set max FPS, bandwidth to calculate the trigger delay time resolution_width = 0 @@ -684,7 +698,6 @@ def calculate_hardware_trigger_arguments(self): pixel_bits = self.pixel_size_byte * 8 - line_length = 0 low_noise = 0 @@ -702,22 +715,22 @@ def calculate_hardware_trigger_arguments(self): resolution_width, resolution_height = self.camera.get_Size() except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('get resolution fail, hr=0x{:x}'.format(ex.hr)) + self.log.error("get resolution fail, hr=0x{:x}".format(ex.hr)) xoffset, yoffset, roi_width, roi_height = self.camera.get_Roi() try: - bandwidth = self.camera.get_Option(toupcam.TOUPCAM_OPTION_BANDWIDTH) + bandwidth = self.camera.get_Option(toupcam.TOUPCAM_OPTION_BANDWIDTH) except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('get badwidth fail, hr=0x{:x}'.format(ex.hr)) + self.log.error("get badwidth fail, hr=0x{:x}".format(ex.hr)) if self.has_low_noise_mode: try: low_noise = self.camera.get_Option(toupcam.TOUPCAM_OPTION_LOW_NOISE) except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('get low_noise fail, hr=0x{:x}'.format(ex.hr)) + self.log.error("get low_noise fail, hr=0x{:x}".format(ex.hr)) if resolution_width == 6224 and resolution_height == 4168: if pixel_bits == 8: @@ -747,14 +760,14 @@ def calculate_hardware_trigger_arguments(self): max_framerate = self.camera.get_Option(toupcam.TOUPCAM_OPTION_MAX_PRECISE_FRAMERATE) except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('get max_framerate fail, hr=0x{:x}'.format(ex.hr)) + self.log.error("get max_framerate fail, hr=0x{:x}".format(ex.hr)) # need reset value, because the default value is only 90% of setting value try: - self.camera.put_Option(toupcam.TOUPCAM_OPTION_PRECISE_FRAMERATE, max_framerate ) + self.camera.put_Option(toupcam.TOUPCAM_OPTION_PRECISE_FRAMERATE, max_framerate) except toupcam.HRESULTException as ex: # TODO(imo): Propagate error in some way and handle - self.log.error('put max_framerate fail, hr=0x{:x}'.format(ex.hr)) + self.log.error("put max_framerate fail, hr=0x{:x}".format(ex.hr)) max_framerate = max_framerate / 10.0 @@ -769,13 +782,13 @@ def calculate_hardware_trigger_arguments(self): self.strobe_delay_us = frame_time def set_reset_strobe_delay_function(self, function_body): - self.reset_strobe_delay = function_body + self.reset_strobe_delay = function_body def set_blacklevel(self, blacklevel): try: current_blacklevel = self.camera.get_Option(toupcam.TOUPCAM_OPTION_BLACKLEVEL) except toupcam.HRESULTException as ex: - err_type = hresult_checker(ex,'E_NOTIMPL') + err_type = hresult_checker(ex, "E_NOTIMPL") print("blacklevel not implemented") return @@ -784,12 +797,12 @@ def set_blacklevel(self, blacklevel): try: self.camera.put_Option(toupcam.TOUPCAM_OPTION_BLACKLEVEL, _blacklevel) except toupcam.HRESULTException as ex: - print('put blacklevel fail, hr=0x{:x}'.format(ex.hr)) - + print("put blacklevel fail, hr=0x{:x}".format(ex.hr)) + class Camera_Simulation(object): - - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None): + + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): self.log = squid.logging.get_logger(self.__class__.__name__) # many to be purged self.sn = sn @@ -827,17 +840,19 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None self.pixel_size_byte = 1 - # below are values for IMX226 (MER2-1220-32U3M) - to make configurable + # below are values for IMX226 (MER2-1220-32U3M) - to make configurable self.row_period_us = 10 self.row_numbers = 3036 self.exposure_delay_us_8bit = 650 - self.exposure_delay_us = self.exposure_delay_us_8bit*self.pixel_size_byte + self.exposure_delay_us = self.exposure_delay_us_8bit * self.pixel_size_byte # just setting a default value # it would be re-calculate with function calculate_hardware_trigger_arguments - self.strobe_delay_us = self.exposure_delay_us + self.row_period_us*self.pixel_size_byte*(self.row_numbers-1) + self.strobe_delay_us = self.exposure_delay_us + self.row_period_us * self.pixel_size_byte * ( + self.row_numbers - 1 + ) - self.pixel_format = 'MONO16' + self.pixel_format = "MONO16" self.Width = Acquisition.CROP_WIDTH self.Height = Acquisition.CROP_HEIGHT @@ -846,7 +861,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.OffsetX = 0 self.OffsetY = 0 - self.brand = 'ToupTek' + self.brand = "ToupTek" # when camera arguments changed, call it to update strobe_delay self.reset_strobe_delay = None @@ -854,15 +869,15 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i # the balcklevel factor # 8 bits: 1 # 10 bits: 4 - # 12 bits: 16 - # 14 bits: 64 + # 12 bits: 16 + # 14 bits: 64 # 16 bits: 256 self.blacklevel_factor = 1 - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def set_temperature_reading_callback(self, func): @@ -874,19 +889,19 @@ def enable_callback(self): def disable_callback(self): self.callback_is_enabled = False - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): pass - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): pass def update_camera_exposure_time(self): pass - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): pass def get_awb_ratios(self): @@ -901,7 +916,7 @@ def start_streaming(self): def stop_streaming(self): pass - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): self.pixel_format = pixel_format self.log.debug(f"Pixel format: {pixel_format}") self.frame_ID = 0 @@ -909,10 +924,10 @@ def set_pixel_format(self,pixel_format): def get_temperature(self): return 0 - def set_temperature(self,temperature): + def set_temperature(self, temperature): pass - def set_fan_speed(self,speed): + def set_fan_speed(self, speed): pass def set_continuous_acquisition(self): @@ -924,26 +939,32 @@ def set_software_triggered_acquisition(self): def set_hardware_triggered_acquisition(self): pass - def set_gain_mode(self,mode): + def set_gain_mode(self, mode): pass def send_trigger(self): self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - if self.pixel_format == 'MONO8': - self.current_frame = np.random.randint(255,size=(self.Height,self.Width),dtype=np.uint8) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200 - elif self.pixel_format == 'MONO12': - self.current_frame = np.random.randint(4095,size=(self.Height,self.Width),dtype=np.uint16) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200*16 + if self.pixel_format == "MONO8": + self.current_frame = np.random.randint(255, size=(self.Height, self.Width), dtype=np.uint8) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = 200 + elif self.pixel_format == "MONO12": + self.current_frame = np.random.randint(4095, size=(self.Height, self.Width), dtype=np.uint16) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = (200 * 16) self.current_frame = self.current_frame << 4 - elif self.pixel_format == 'MONO16': - self.current_frame = np.random.randint(65535,size=(self.Height,self.Width),dtype=np.uint16) - self.current_frame[self.Height//2-99:self.Height//2+100,self.Width//2-99:self.Width//2+100] = 200*256 + elif self.pixel_format == "MONO16": + self.current_frame = np.random.randint(65535, size=(self.Height, self.Width), dtype=np.uint16) + self.current_frame[ + self.Height // 2 - 99 : self.Height // 2 + 100, self.Width // 2 - 99 : self.Width // 2 + 100 + ] = (200 * 256) else: - self.current_frame = np.roll(self.current_frame,10,axis=0) - pass + self.current_frame = np.roll(self.current_frame, 10, axis=0) + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None and self.callback_is_enabled: self.new_image_callback_external(self) @@ -960,7 +981,7 @@ def read_frame(self): def _on_frame_callback(self, user_param, raw_image): pass - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): pass def reset_camera_acquisition_counter(self): @@ -977,6 +998,6 @@ def calculate_hardware_trigger_arguments(self): def set_reset_strobe_delay_function(self, function_body): pass - + def set_blacklevel(self, blacklevel): pass diff --git a/software/control/camera_tucsen.py b/software/control/camera_tucsen.py index 8866a66e4..8c92c1606 100644 --- a/software/control/camera_tucsen.py +++ b/software/control/camera_tucsen.py @@ -9,10 +9,11 @@ from control._def import * + def get_sn_by_model(model_name): - TUCAMINIT = TUCAM_INIT(0, './control'.encode('utf-8')) + TUCAMINIT = TUCAM_INIT(0, "./control".encode("utf-8")) TUCAM_Api_Init(pointer(TUCAMINIT)) - + for i in range(TUCAMINIT.uiCamCount): TUCAMOPEN = TUCAM_OPEN(i, 0) TUCAM_Dev_Open(pointer(TUCAMOPEN)) @@ -24,20 +25,22 @@ def get_sn_by_model(model_name): pSN = cast(cSN, c_char_p) TUCAMREGRW = TUCAM_REG_RW(1, pSN, 64) TUCAM_Reg_Read(TUCAMOPEN.hIdxTUCam, TUCAMREGRW) - sn = string_at(pSN).decode('utf-8') + sn = string_at(pSN).decode("utf-8") TUCAM_Dev_Close(TUCAMOPEN.hIdxTUCam) TUCAM_Api_Uninit() return sn TUCAM_Dev_Close(TUCAMOPEN.hIdxTUCam) - + TUCAM_Api_Uninit() return None class Camera(object): - def __init__(self, sn=None, resolution=(6240, 4168), is_global_shutter=False, rotate_image_angle=None, flip_image=None): + def __init__( + self, sn=None, resolution=(6240, 4168), is_global_shutter=False, rotate_image_angle=None, flip_image=None + ): self.log = squid.logging.get_logger(self.__class__.__name__) self.sn = sn @@ -46,11 +49,11 @@ def __init__(self, sn=None, resolution=(6240, 4168), is_global_shutter=False, ro self.rotate_image_angle = rotate_image_angle self.flip_image = flip_image - self.TUCAMINIT = TUCAM_INIT(0, './control'.encode('utf-8')) + self.TUCAMINIT = TUCAM_INIT(0, "./control".encode("utf-8")) self.TUCAMOPEN = TUCAM_OPEN(0, 0) self.trigger_attr = TUCAM_TRIGGER_ATTR() - self.m_frame = None # buffer - + self.m_frame = None # buffer + self.exposure_time = 1 # ms self.analog_gain = 0 self.is_streaming = False @@ -92,16 +95,22 @@ def __init__(self, sn=None, resolution=(6240, 4168), is_global_shutter=False, ro self.WidthMax = 6240 self.HeightMax = 4168 self.binning_options = { - (6240, 4168): 0, (3120, 2084): 1, (2080, 1388): 2, (1560, 1040): 3, - (1248, 832): 4, (1040, 692): 5, (780, 520): 6, (388, 260): 7 + (6240, 4168): 0, + (3120, 2084): 1, + (2080, 1388): 2, + (1560, 1040): 3, + (1248, 832): 4, + (1040, 692): 5, + (780, 520): 6, + (388, 260): 7, } def open(self, index=0): TUCAM_Api_Init(pointer(self.TUCAMINIT)) - self.log.info(f'Connect {self.TUCAMINIT.uiCamCount} camera(s)') - + self.log.info(f"Connect {self.TUCAMINIT.uiCamCount} camera(s)") + if index >= self.TUCAMINIT.uiCamCount: - self.log.error('Camera index out of range') + self.log.error("Camera index out of range") # TODO(imo): Propagate error in some way and handle return @@ -110,16 +119,16 @@ def open(self, index=0): # TODO(imo): Propagate error in some way and handle if self.TUCAMOPEN.hIdxTUCam == 0: - self.log.error('Open Tucsen camera failure!') + self.log.error("Open Tucsen camera failure!") else: - self.log.info('Open Tucsen camera success!') + self.log.info("Open Tucsen camera success!") self.set_temperature(20) self.temperature_reading_thread.start() def open_by_sn(self, sn): TUCAM_Api_Init(pointer(self.TUCAMINIT)) - + for i in range(self.TUCAMINIT.uiCamCount): TUCAMOPEN = TUCAM_OPEN(i, 0) TUCAM_Dev_Open(pointer(TUCAMOPEN)) @@ -130,17 +139,17 @@ def open_by_sn(self, sn): TUCAMREGRW = TUCAM_REG_RW(1, pSN, 64) TUCAM_Reg_Read(TUCAMOPEN.hIdxTUCam, TUCAMREGRW) - if string_at(pSN).decode('utf-8') == sn: + if string_at(pSN).decode("utf-8") == sn: self.TUCAMOPEN = TUCAMOPEN self.set_temperature(20) self.temperature_reading_thread.start() - self.log.info(f'Open the camera success! sn={sn}') + self.log.info(f"Open the camera success! sn={sn}") return else: TUCAM_Dev_Close(TUCAMOPEN.hIdxTUCam) # TODO(imo): Propagate error in some way and handle - self.log.error('No camera with the specified serial number found') + self.log.error("No camera with the specified serial number found") def close(self): self.disable_callback() @@ -149,7 +158,7 @@ def close(self): TUCAM_Buf_Release(self.TUCAMOPEN.hIdxTUCam) TUCAM_Dev_Close(self.TUCAMOPEN.hIdxTUCam) TUCAM_Api_Uninit() - self.log.info('Close Tucsen camera success') + self.log.info("Close Tucsen camera success") def set_callback(self, function): self.new_image_callback_external = function @@ -166,11 +175,13 @@ def enable_callback(self): self.callback_thread.start() self.callback_is_enabled = True - self.log.debug('enable callback') + self.log.debug("enable callback") def _wait_and_callback(self): while not self.stop_waiting: - result = TUCAM_Buf_WaitForFrame(self.TUCAMOPEN.hIdxTUCam, pointer(self.m_frame), int(self.exposure_time + 1000)) + result = TUCAM_Buf_WaitForFrame( + self.TUCAMOPEN.hIdxTUCam, pointer(self.m_frame), int(self.exposure_time + 1000) + ) if result == TUCAMRET.TUCAMRET_SUCCESS: self._on_new_frame(self.m_frame) @@ -181,16 +192,16 @@ def _wait_and_callback(self): def _on_new_frame(self, frame): # TODO(imo): Propagate error in some way and handle if frame is False: - self.log.error('Cannot get new frame from buffer.') + self.log.error("Cannot get new frame from buffer.") return if self.image_locked: - self.log.error('Last image is still being processed; a frame is dropped') + self.log.error("Last image is still being processed; a frame is dropped") return self.current_frame = self._convert_frame_to_numpy(frame) self.frame_ID_software += 1 - self.frame_ID += 1 + self.frame_ID += 1 # frame ID for hardware triggered acquisition if self.trigger_mode == TriggerMode.HARDWARE: @@ -210,14 +221,14 @@ def disable_callback(self): self.stop_waiting = True self.is_streaming = False - if hasattr(self, 'callback_thread'): + if hasattr(self, "callback_thread"): self.callback_thread.join() del self.callback_thread self.callback_is_enabled = False if was_streaming: self.start_streaming() - self.log.debug('disable callback') + self.log.debug("disable callback") def set_temperature_reading_callback(self, func): self.temperature_reading_callback = func @@ -239,7 +250,7 @@ def check_temperature(self): try: self.temperature_reading_callback(temperature) except TypeError as ex: - self.log.error("Temperature read callback failed due to error: "+repr(ex)) + self.log.error("Temperature read callback failed due to error: " + repr(ex)) # TODO(imo): Propagate error in some way and handle pass @@ -259,7 +270,7 @@ def set_resolution(self, width, height): TUCAM_Capa_SetValue(self.TUCAMOPEN.hIdxTUCam, TUCAM_IDCAPA.TUIDC_BINNING_SUM.value, bin_value) except Exception: - self.log.error('Cannot set binning.') + self.log.error("Cannot set binning.") # TODO(imo): Propagate error in some way and handle if was_streaming: @@ -344,7 +355,7 @@ def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): self.ROI_height = roi_attr.nHeight except Exception: - self.log.error('Cannot set ROI.') + self.log.error("Cannot set ROI.") # TODO(imo): Propagate error in some way and handle if was_streaming: @@ -373,7 +384,7 @@ def start_streaming(self): raise Exception("Failed to start capture") self.is_streaming = True - self.log.info('TUCam Camera starts streaming') + self.log.info("TUCam Camera starts streaming") def stop_streaming(self): if not self.is_streaming: @@ -382,15 +393,15 @@ def stop_streaming(self): TUCAM_Cap_Stop(self.TUCAMOPEN.hIdxTUCam) TUCAM_Buf_Release(self.TUCAMOPEN.hIdxTUCam) self.is_streaming = False - self.log.info('TUCam Camera streaming stopped') + self.log.info("TUCam Camera streaming stopped") def read_frame(self): result = TUCAM_Buf_WaitForFrame(self.TUCAMOPEN.hIdxTUCam, pointer(self.m_frame), int(self.exposure_time + 1000)) - if result == TUCAMRET.TUCAMRET_SUCCESS: + if result == TUCAMRET.TUCAMRET_SUCCESS: self.current_frame = self._convert_frame_to_numpy(self.m_frame) TUCAM_Buf_AbortWait(self.TUCAMOPEN.hIdxTUCam) return self.current_frame - + return None def _convert_frame_to_numpy(self, frame): @@ -406,8 +417,8 @@ def _convert_frame_to_numpy(self, frame): class Camera_Simulation(object): - - def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_image=None): + + def __init__(self, sn=None, is_global_shutter=False, rotate_image_angle=None, flip_image=None): self.log = squid.logging.get_logger(self.__class__.__name__) # many to be purged @@ -445,7 +456,7 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.trigger_mode = None - self.pixel_format = 'MONO16' + self.pixel_format = "MONO16" self.is_live = False @@ -458,11 +469,10 @@ def __init__(self,sn=None,is_global_shutter=False,rotate_image_angle=None,flip_i self.new_image_callback_external = None - - def open(self,index=0): + def open(self, index=0): pass - def set_callback(self,function): + def set_callback(self, function): self.new_image_callback_external = function def enable_callback(self): @@ -471,16 +481,16 @@ def enable_callback(self): def disable_callback(self): self.callback_is_enabled = False - def open_by_sn(self,sn): + def open_by_sn(self, sn): pass def close(self): pass - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): pass - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): pass def start_streaming(self): @@ -489,7 +499,7 @@ def start_streaming(self): def stop_streaming(self): pass - def set_pixel_format(self,pixel_format): + def set_pixel_format(self, pixel_format): self.pixel_format = pixel_format self.log.debug(f"Pixel format={pixel_format}") self.frame_ID = 0 @@ -504,19 +514,19 @@ def set_hardware_triggered_acquisition(self): pass def send_trigger(self): - self.log.debug('send trigger') + self.log.debug("send trigger") self.frame_ID = self.frame_ID + 1 self.timestamp = time.time() if self.frame_ID == 1: - if self.pixel_format == 'MONO8': - self.current_frame = np.random.randint(255,size=(2000,2000),dtype=np.uint8) - self.current_frame[901:1100,901:1100] = 200 - elif self.pixel_format == 'MONO16': - self.current_frame = np.random.randint(65535,size=(2000,2000),dtype=np.uint16) - self.current_frame[901:1100,901:1100] = 200*256 + if self.pixel_format == "MONO8": + self.current_frame = np.random.randint(255, size=(2000, 2000), dtype=np.uint8) + self.current_frame[901:1100, 901:1100] = 200 + elif self.pixel_format == "MONO16": + self.current_frame = np.random.randint(65535, size=(2000, 2000), dtype=np.uint16) + self.current_frame[901:1100, 901:1100] = 200 * 256 else: - self.current_frame = np.roll(self.current_frame,10,axis=0) - pass + self.current_frame = np.roll(self.current_frame, 10, axis=0) + pass # self.current_frame = np.random.randint(255,size=(768,1024),dtype=np.uint8) if self.new_image_callback_external is not None and self.callback_is_enabled: self.new_image_callback_external(self) @@ -527,7 +537,7 @@ def read_frame(self): def _on_frame_callback(self, user_param, raw_image): pass - def set_ROI(self,offset_x=None,offset_y=None,width=None,height=None): + def set_ROI(self, offset_x=None, offset_y=None, width=None, height=None): pass def set_line3_to_strobe(self): diff --git a/software/control/celesta.py b/software/control/celesta.py index 771cd29fd..61fe067bd 100755 --- a/software/control/celesta.py +++ b/software/control/celesta.py @@ -4,38 +4,42 @@ revised HL 2/2024 """ + import urllib.request import traceback from squid.abc import LightSource from control.microscope import LightSourceType, IntensityControlMode, ShutterControlMode -def lumencor_httpcommand(command = 'GET IP',ip = '192.168.201.200'): + +def lumencor_httpcommand(command="GET IP", ip="192.168.201.200"): """ Sends commands to the lumencor system via http. Plese find commands here: http://lumencor.com/wp-content/uploads/sites/11/2019/01/57-10018.pdf """ - command_full = r'http://'+ip+'/service/?command='+command.replace(' ','%20') + command_full = r"http://" + ip + "/service/?command=" + command.replace(" ", "%20") with urllib.request.urlopen(command_full) as response: - message = eval(response.read()) # the default is conveniently JSON so eval creates dictionary + message = eval(response.read()) # the default is conveniently JSON so eval creates dictionary return message + class CELESTA(LightSource): """ This controls a lumencor object (default: Celesta) using HTTP. Please connect the provided cat5e, RJ45 ethernet cable between the PC and Lumencor system. """ + def __init__(self, **kwds): """ Connect to the Lumencor system via HTTP and check if you get the right response. """ self.on = False - self.ip = kwds.get('ip', '192.168.201.200') - [self.pmin, self.pmax] = 0,1000 + self.ip = kwds.get("ip", "192.168.201.200") + [self.pmin, self.pmax] = 0, 1000 try: # See if the system returns back the right IP. self.message = self.get_IP() - assert (self.message['message'] == 'A IP '+self.ip) + assert self.message["message"] == "A IP " + self.ip self.n_lasers = self.get_number_lasers() self.live = True except: @@ -47,8 +51,8 @@ def __init__(self, **kwds): [self.pmin, self.pmax] = self.get_intensity_range() self.set_shutter_control_mode(True) for i in range(self.n_lasers): - if (not self.get_shutter_state(i)): - self.set_shutter_state(i,False) + if not self.get_shutter_state(i): + self.set_shutter_state(i, False) self.channel_mappings = { 405: 0, @@ -62,7 +66,7 @@ def __init__(self, **kwds): 640: 5, 730: 6, 735: 6, - 750: 6 + 750: 6, } def initialize(self): @@ -76,29 +80,29 @@ def get_intensity_control_mode(self): def get_number_lasers(self): """Return the number of lasers the current lumencor system can control""" - self.message = lumencor_httpcommand(command ='GET CHMAP', ip=self.ip) - if self.message['message'][0]=='A': - return len(self.message['message'].split(' '))-2 + self.message = lumencor_httpcommand(command="GET CHMAP", ip=self.ip) + if self.message["message"][0] == "A": + return len(self.message["message"].split(" ")) - 2 return 0 - def get_color(self,laser_id): + def get_color(self, laser_id): """Returns the color of the current laser""" - self.message = lumencor_httpcommand(command ='GET CHMAP', ip=self.ip) - colors = self.message['message'].split(' ')[2:] + self.message = lumencor_httpcommand(command="GET CHMAP", ip=self.ip) + colors = self.message["message"].split(" ")[2:] print(colors) return colors[int(laser_id)] def get_IP(self): - self.message = lumencor_httpcommand(command = 'GET IP', ip=self.ip) + self.message = lumencor_httpcommand(command="GET IP", ip=self.ip) return self.message def get_shutter_control_mode(self): """ Return True/False the lasers can be controlled with TTL. """ - self.message = lumencor_httpcommand(command = 'GET TTLENABLE', ip=self.ip) - response = self.message['message'] - if response[-1]=='1': + self.message = lumencor_httpcommand(command="GET TTLENABLE", ip=self.ip) + response = self.message["message"] + if response[-1] == "1": return ShutterControlMode.TTL else: return ShutterControlMode.Software @@ -108,38 +112,38 @@ def set_shutter_control_mode(self, mode): Turn on/off external TTL control mode. """ if mode == ShutterControlMode.TTL: - ttl_enable = '1' + ttl_enable = "1" else: - ttl_enable = '0' - self.message = lumencor_httpcommand(command = 'SET TTLENABLE '+ttl_enable,ip=self.ip) + ttl_enable = "0" + self.message = lumencor_httpcommand(command="SET TTLENABLE " + ttl_enable, ip=self.ip) - def get_shutter_state(self,laser_id): + def get_shutter_state(self, laser_id): """ Return True/False the laser is on/off. """ - self.message = lumencor_httpcommand(command = 'GET CH '+str(laser_id), ip=self.ip) - response = self.message['message'] - self.on = response[-1]=='1' + self.message = lumencor_httpcommand(command="GET CH " + str(laser_id), ip=self.ip) + response = self.message["message"] + self.on = response[-1] == "1" return self.on def get_intensity_range(self): """ Return [minimum power, maximum power]. """ - max_int =1000 # default - self.message = lumencor_httpcommand(command = 'GET MAXINT', ip=self.ip) - if self.message['message'][0]=='A': - max_int = float(self.message['message'].split(' ')[-1]) + max_int = 1000 # default + self.message = lumencor_httpcommand(command="GET MAXINT", ip=self.ip) + if self.message["message"][0] == "A": + max_int = float(self.message["message"].split(" ")[-1]) return [0, max_int] - def get_intensity(self,laser_id): + def get_intensity(self, laser_id): """ Return the current laser power. """ - self.message = lumencor_httpcommand(command = 'GET CHINT '+str(laser_id), ip=self.ip) + self.message = lumencor_httpcommand(command="GET CHINT " + str(laser_id), ip=self.ip) # print(command = 'GET CHINT '+str(laser_id), ip=self.ip) - response = self.message['message'] - power = float(response.split(' ')[-1]) + response = self.message["message"] + power = float(response.split(" ")[-1]) return power def set_shutter_state(self, laser_id, on): @@ -147,10 +151,10 @@ def set_shutter_state(self, laser_id, on): Turn the laser on/off. """ if on: - self.message = lumencor_httpcommand(command = 'SET CH '+str(laser_id)+' 1', ip=self.ip) + self.message = lumencor_httpcommand(command="SET CH " + str(laser_id) + " 1", ip=self.ip) self.on = True else: - self.message = lumencor_httpcommand(command = 'SET CH '+str(laser_id)+' 0', ip=self.ip) + self.message = lumencor_httpcommand(command="SET CH " + str(laser_id) + " 0", ip=self.ip) self.on = False print("Turning On/Off", self.on, self.message) @@ -161,8 +165,10 @@ def set_intensity(self, laser_id, power_in_mw): print("Setting Power", power_in_mw, self.message) if power_in_mw > self.pmax: power_in_mw = self.pmax - self.message = lumencor_httpcommand(command ='SET CHINT '+str(laser_id)+' '+ str(int(power_in_mw)), ip=self.ip) - if self.message['message'][0]=='A': + self.message = lumencor_httpcommand( + command="SET CHINT " + str(laser_id) + " " + str(int(power_in_mw)), ip=self.ip + ) + if self.message["message"][0] == "A": return True return False @@ -172,8 +178,8 @@ def shut_down(self): """ if self.live: for i in range(self.n_lasers): - self.set_intensity(i,0) - self.set_shutter_state(i,False) + self.set_intensity(i, 0) + self.set_shutter_state(i, False) def get_status(self): """ @@ -181,6 +187,7 @@ def get_status(self): """ return self.live + # # The MIT License # diff --git a/software/control/console.py b/software/control/console.py index 63c6be2cb..0d6998edc 100644 --- a/software/control/console.py +++ b/software/control/console.py @@ -1,4 +1,5 @@ import os + # set QT_API environment variable os.environ["QT_API"] = "pyqt5" @@ -14,11 +15,13 @@ import functools import inspect + class QtCompleter: """Custom completer for Qt objects""" + def __init__(self, namespace): self.namespace = namespace - + def complete(self, text, state): if state == 0: if "." in text: @@ -30,7 +33,7 @@ def complete(self, text, state): return self.matches[state] except IndexError: return None - + def global_matches(self, text): """Compute matches when text is a simple name.""" matches = [] @@ -39,14 +42,14 @@ def global_matches(self, text): if word[:n] == text: matches.append(word) return matches - + def attr_matches(self, text): """Match attributes of an object.""" # Split the text on dots - parts = text.split('.') + parts = text.split(".") if not parts: return [] - + # Find the object we're looking for completions on try: obj = self.namespace[parts[0]] @@ -54,23 +57,22 @@ def attr_matches(self, text): if isinstance(obj, GuiProxy): obj = obj.target obj = getattr(obj, part) - + if isinstance(obj, GuiProxy): obj = obj.target except (KeyError, AttributeError): return [] - + # Get the incomplete name we're trying to match incomplete = parts[-1] - + # Get all possible matches matches = [] - + try: # Get standard Python attributes - matches.extend(name for name in dir(obj) - if name.startswith(incomplete)) - + matches.extend(name for name in dir(obj) if name.startswith(incomplete)) + # If it's a QObject, also get Qt properties if isinstance(obj, QObject): meta = obj.metaObject() @@ -79,26 +81,29 @@ def attr_matches(self, text): name = prop.name() if name.startswith(incomplete): matches.append(name) - + # Get methods with their signatures if incomplete: matches.extend( - f"{name}()" for name, member in inspect.getmembers(obj, inspect.ismethod) + f"{name}()" + for name, member in inspect.getmembers(obj, inspect.ismethod) if name.startswith(incomplete) ) - + except Exception as e: print(f"Error during completion: {e}") return [] - + # Make the matches into complete dots if len(parts) > 1: - matches = ['.'.join(parts[:-1] + [m]) for m in matches] - + matches = [".".join(parts[:-1] + [m]) for m in matches] + return matches + class MainThreadCall(QObject): """Helper class to execute functions on the main thread""" + execute_signal = Signal(object, tuple, dict) def __init__(self): @@ -119,18 +124,20 @@ def _execute(self, func, args, kwargs): def __call__(self, func, *args, **kwargs): if QThread.currentThread() is QApplication.instance().thread(): return func(*args, **kwargs) - + self._event.clear() self._result = None self.execute_signal.emit(func, args, kwargs) self._event.wait() - + if isinstance(self._result, Exception): raise self._result return self._result + class GuiProxy: """Proxy class to safely execute GUI operations from other threads""" + def __init__(self, target_object): self.target = target_object self.main_thread_call = MainThreadCall() @@ -138,9 +145,11 @@ def __init__(self, target_object): def __getattr__(self, name): attr = getattr(self.target, name) if callable(attr): + @functools.wraps(attr) def wrapper(*args, **kwargs): return self.main_thread_call(attr, *args, **kwargs) + return wrapper return attr @@ -148,46 +157,53 @@ def __dir__(self): """Support for auto-completion""" return dir(self.target) + class EnhancedInteractiveConsole(code.InteractiveConsole): """Enhanced console with better completion support""" + def __init__(self, locals=None): super().__init__(locals) # Set up readline with our custom completer self.completer = QtCompleter(locals) readline.set_completer(self.completer.complete) - readline.parse_and_bind('tab: complete') - + readline.parse_and_bind("tab: complete") + # Use better completion delimiters - readline.set_completer_delims(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>?') - + readline.set_completer_delims(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>?") + # Set up readline history import os + histfile = os.path.expanduser("~/.pyqt_console_history") try: readline.read_history_file(histfile) except FileNotFoundError: pass import atexit + atexit.register(readline.write_history_file, histfile) + class ConsoleThread(QThread): """Thread for running the interactive console""" + def __init__(self, locals_dict): super().__init__() self.locals_dict = locals_dict self.wrapped_locals = { - key: GuiProxy(value) if isinstance(value, QObject) else value - for key, value in locals_dict.items() + key: GuiProxy(value) if isinstance(value, QObject) else value for key, value in locals_dict.items() } self.console = EnhancedInteractiveConsole(locals=self.wrapped_locals) def run(self): while True: try: - self.console.interact(banner=""" + self.console.interact( + banner=""" Squid Microscope Console ----------------------- Use 'microscope' to access the microscope -""") +""" + ) except SystemExit: break diff --git a/software/control/core/core.py b/software/control/core/core.py index 550407c62..76ff551cf 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -15,6 +15,7 @@ # control from control._def import * + if DO_FLUORESCENCE_RTP: from control.processing_handler import ProcessingHandler from control.processing_pipeline import * @@ -27,7 +28,8 @@ try: from control.multipoint_custom_script_entry_v2 import * - print('custom multipoint script found') + + print("custom multipoint script found") except: pass @@ -50,6 +52,7 @@ import imageio as iio import squid.abc + class ObjectiveStore: def __init__(self, objectives_dict=OBJECTIVES, default_objective=DEFAULT_OBJECTIVE, parent=None): self.objectives_dict = objectives_dict @@ -90,6 +93,7 @@ def get_pixel_binning(self): pixel_binning = 1 return pixel_binning + class StreamHandler(QObject): image_to_display = Signal(np.ndarray) @@ -97,7 +101,9 @@ class StreamHandler(QObject): packet_image_for_tracking = Signal(np.ndarray, int, float) signal_new_frame_received = Signal() - def __init__(self,crop_width=Acquisition.CROP_WIDTH,crop_height=Acquisition.CROP_HEIGHT,display_resolution_scaling=1): + def __init__( + self, crop_width=Acquisition.CROP_WIDTH, crop_height=Acquisition.CROP_HEIGHT, display_resolution_scaling=1 + ): QObject.__init__(self) self.fps_display = 1 self.fps_save = 1 @@ -131,18 +137,18 @@ def start_tracking(self): def stop_tracking(self): self.tracking_flag = False - def set_display_fps(self,fps): + def set_display_fps(self, fps): self.fps_display = fps - def set_save_fps(self,fps): + def set_save_fps(self, fps): self.fps_save = fps - def set_crop(self,crop_width,crop_height): + def set_crop(self, crop_width, crop_height): self.crop_width = crop_width self.crop_height = crop_height def set_display_resolution_scaling(self, display_resolution_scaling): - self.display_resolution_scaling = display_resolution_scaling/100 + self.display_resolution_scaling = display_resolution_scaling / 100 print(self.display_resolution_scaling) def on_new_frame(self, camera): @@ -151,58 +157,66 @@ def on_new_frame(self, camera): camera.image_locked = True self.handler_busy = True - self.signal_new_frame_received.emit() # self.liveController.turn_off_illumination() + self.signal_new_frame_received.emit() # self.liveController.turn_off_illumination() # measure real fps timestamp_now = round(time.time()) if timestamp_now == self.timestamp_last: - self.counter = self.counter+1 + self.counter = self.counter + 1 else: self.timestamp_last = timestamp_now self.fps_real = self.counter self.counter = 0 if PRINT_CAMERA_FPS: - print('real camera fps is ' + str(self.fps_real)) + print("real camera fps is " + str(self.fps_real)) # moved down (so that it does not modify the camera.current_frame, which causes minor problems for simulation) - 1/30/2022 # # rotate and flip - eventually these should be done in the camera # camera.current_frame = utils.rotate_and_flip_image(camera.current_frame,rotate_image_angle=camera.rotate_image_angle,flip_image=camera.flip_image) # crop image - image_cropped = utils.crop_image(camera.current_frame,self.crop_width,self.crop_height) + image_cropped = utils.crop_image(camera.current_frame, self.crop_width, self.crop_height) image_cropped = np.squeeze(image_cropped) # # rotate and flip - moved up (1/10/2022) # image_cropped = utils.rotate_and_flip_image(image_cropped,rotate_image_angle=ROTATE_IMAGE_ANGLE,flip_image=FLIP_IMAGE) # added on 1/30/2022 # @@@ to move to camera - image_cropped = utils.rotate_and_flip_image(image_cropped,rotate_image_angle=camera.rotate_image_angle,flip_image=camera.flip_image) + image_cropped = utils.rotate_and_flip_image( + image_cropped, rotate_image_angle=camera.rotate_image_angle, flip_image=camera.flip_image + ) # send image to display time_now = time.time() - if time_now-self.timestamp_last_display >= 1/self.fps_display: + if time_now - self.timestamp_last_display >= 1 / self.fps_display: # self.image_to_display.emit(cv2.resize(image_cropped,(round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling)),cv2.INTER_LINEAR)) - self.image_to_display.emit(utils.crop_image(image_cropped,round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling))) + self.image_to_display.emit( + utils.crop_image( + image_cropped, + round(self.crop_width * self.display_resolution_scaling), + round(self.crop_height * self.display_resolution_scaling), + ) + ) self.timestamp_last_display = time_now # send image to write - if self.save_image_flag and time_now-self.timestamp_last_save >= 1/self.fps_save: + if self.save_image_flag and time_now - self.timestamp_last_save >= 1 / self.fps_save: if camera.is_color: - image_cropped = cv2.cvtColor(image_cropped,cv2.COLOR_RGB2BGR) - self.packet_image_to_write.emit(image_cropped,camera.frame_ID,camera.timestamp) + image_cropped = cv2.cvtColor(image_cropped, cv2.COLOR_RGB2BGR) + self.packet_image_to_write.emit(image_cropped, camera.frame_ID, camera.timestamp) self.timestamp_last_save = time_now # send image to track - if self.track_flag and time_now-self.timestamp_last_track >= 1/self.fps_track: + if self.track_flag and time_now - self.timestamp_last_track >= 1 / self.fps_track: # track is a blocking operation - it needs to be # @@@ will cropping before emitting the signal lead to speedup? - self.packet_image_for_tracking.emit(image_cropped,camera.frame_ID,camera.timestamp) + self.packet_image_for_tracking.emit(image_cropped, camera.frame_ID, camera.timestamp) self.timestamp_last_track = time_now self.handler_busy = False camera.image_locked = False - ''' + """ def on_new_frame_from_simulation(self,image,frame_ID,timestamp): # check whether image is a local copy or pointer, if a pointer, needs to prevent the image being modified while this function is being executed @@ -228,19 +242,20 @@ def on_new_frame_from_simulation(self,image,frame_ID,timestamp): self.timestamp_last_track = time_now self.handler_busy = False - ''' + """ + class ImageSaver(QObject): stop_recording = Signal() - def __init__(self,image_format=Acquisition.IMAGE_FORMAT): + def __init__(self, image_format=Acquisition.IMAGE_FORMAT): QObject.__init__(self) - self.base_path = './' - self.experiment_ID = '' + self.base_path = "./" + self.experiment_ID = "" self.image_format = image_format self.max_num_image_per_folder = 1000 - self.queue = Queue(10) # max 10 items in the queue + self.queue = Queue(10) # max 10 items in the queue self.image_lock = Lock() self.stop_signal_received = False self.thread = Thread(target=self.process_queue) @@ -256,21 +271,28 @@ def process_queue(self): return # process the queue try: - [image,frame_ID,timestamp] = self.queue.get(timeout=0.1) + [image, frame_ID, timestamp] = self.queue.get(timeout=0.1) self.image_lock.acquire(True) - folder_ID = int(self.counter/self.max_num_image_per_folder) - file_ID = int(self.counter%self.max_num_image_per_folder) + folder_ID = int(self.counter / self.max_num_image_per_folder) + file_ID = int(self.counter % self.max_num_image_per_folder) # create a new folder if file_ID == 0: - os.mkdir(os.path.join(self.base_path,self.experiment_ID,str(folder_ID))) + os.mkdir(os.path.join(self.base_path, self.experiment_ID, str(folder_ID))) if image.dtype == np.uint16: # need to use tiff when saving 16 bit images - saving_path = os.path.join(self.base_path,self.experiment_ID,str(folder_ID),str(file_ID) + '_' + str(frame_ID) + '.tiff') - iio.imwrite(saving_path,image) + saving_path = os.path.join( + self.base_path, self.experiment_ID, str(folder_ID), str(file_ID) + "_" + str(frame_ID) + ".tiff" + ) + iio.imwrite(saving_path, image) else: - saving_path = os.path.join(self.base_path,self.experiment_ID,str(folder_ID),str(file_ID) + '_' + str(frame_ID) + '.' + self.image_format) - cv2.imwrite(saving_path,image) + saving_path = os.path.join( + self.base_path, + self.experiment_ID, + str(folder_ID), + str(file_ID) + "_" + str(frame_ID) + "." + self.image_format, + ) + cv2.imwrite(saving_path, image) self.counter = self.counter + 1 self.queue.task_done() @@ -278,31 +300,33 @@ def process_queue(self): except: pass - def enqueue(self,image,frame_ID,timestamp): + def enqueue(self, image, frame_ID, timestamp): try: - self.queue.put_nowait([image,frame_ID,timestamp]) - if ( self.recording_time_limit>0 ) and ( time.time()-self.recording_start_time >= self.recording_time_limit ): + self.queue.put_nowait([image, frame_ID, timestamp]) + if (self.recording_time_limit > 0) and ( + time.time() - self.recording_start_time >= self.recording_time_limit + ): self.stop_recording.emit() # when using self.queue.put(str_), program can be slowed down despite multithreading because of the block and the GIL except: - print('imageSaver queue is full, image discarded') + print("imageSaver queue is full, image discarded") - def set_base_path(self,path): + def set_base_path(self, path): self.base_path = path - def set_recording_time_limit(self,time_limit): + def set_recording_time_limit(self, time_limit): self.recording_time_limit = time_limit - def start_new_experiment(self,experiment_ID,add_timestamp=True): + def start_new_experiment(self, experiment_ID, add_timestamp=True): if add_timestamp: # generate unique experiment ID - self.experiment_ID = experiment_ID + '_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + self.experiment_ID = experiment_ID + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") else: self.experiment_ID = experiment_ID self.recording_start_time = time.time() # create a new folder try: - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) # to do: save configuration except: pass @@ -316,12 +340,12 @@ def close(self): class ImageSaver_Tracking(QObject): - def __init__(self,base_path,image_format='bmp'): + def __init__(self, base_path, image_format="bmp"): QObject.__init__(self) self.base_path = base_path self.image_format = image_format self.max_num_image_per_folder = 1000 - self.queue = Queue(100) # max 100 items in the queue + self.queue = Queue(100) # max 100 items in the queue self.image_lock = Lock() self.stop_signal_received = False self.thread = Thread(target=self.process_queue) @@ -334,29 +358,37 @@ def process_queue(self): return # process the queue try: - [image,frame_counter,postfix] = self.queue.get(timeout=0.1) + [image, frame_counter, postfix] = self.queue.get(timeout=0.1) self.image_lock.acquire(True) - folder_ID = int(frame_counter/self.max_num_image_per_folder) - file_ID = int(frame_counter%self.max_num_image_per_folder) + folder_ID = int(frame_counter / self.max_num_image_per_folder) + file_ID = int(frame_counter % self.max_num_image_per_folder) # create a new folder if file_ID == 0: - os.mkdir(os.path.join(self.base_path,str(folder_ID))) + os.mkdir(os.path.join(self.base_path, str(folder_ID))) if image.dtype == np.uint16: - saving_path = os.path.join(self.base_path,str(folder_ID),str(file_ID) + '_' + str(frame_counter) + '_' + postfix + '.tiff') - iio.imwrite(saving_path,image) + saving_path = os.path.join( + self.base_path, + str(folder_ID), + str(file_ID) + "_" + str(frame_counter) + "_" + postfix + ".tiff", + ) + iio.imwrite(saving_path, image) else: - saving_path = os.path.join(self.base_path,str(folder_ID),str(file_ID) + '_' + str(frame_counter) + '_' + postfix + '.' + self.image_format) - cv2.imwrite(saving_path,image) + saving_path = os.path.join( + self.base_path, + str(folder_ID), + str(file_ID) + "_" + str(frame_counter) + "_" + postfix + "." + self.image_format, + ) + cv2.imwrite(saving_path, image) self.queue.task_done() self.image_lock.release() except: pass - def enqueue(self,image,frame_counter,postfix): + def enqueue(self, image, frame_counter, postfix): try: - self.queue.put_nowait([image,frame_counter,postfix]) + self.queue.put_nowait([image, frame_counter, postfix]) except: - print('imageSaver queue is full, image discarded') + print("imageSaver queue is full, image discarded") def close(self): self.queue.join() @@ -370,7 +402,7 @@ class ImageDisplay(QObject): def __init__(self): QObject.__init__(self) - self.queue = Queue(10) # max 10 items in the queue + self.queue = Queue(10) # max 10 items in the queue self.image_lock = Lock() self.stop_signal_received = False self.thread = Thread(target=self.process_queue) @@ -383,7 +415,7 @@ def process_queue(self): return # process the queue try: - [image,frame_ID,timestamp] = self.queue.get(timeout=0.1) + [image, frame_ID, timestamp] = self.queue.get(timeout=0.1) self.image_lock.acquire(True) self.image_to_display.emit(image) self.image_lock.release() @@ -392,15 +424,15 @@ def process_queue(self): pass # def enqueue(self,image,frame_ID,timestamp): - def enqueue(self,image): + def enqueue(self, image): try: - self.queue.put_nowait([image,None,None]) + self.queue.put_nowait([image, None, None]) # when using self.queue.put(str_) instead of try + nowait, program can be slowed down despite multithreading because of the block and the GIL pass except: - print('imageDisplay queue is full, image discarded') + print("imageDisplay queue is full, image discarded") - def emit_directly(self,image): + def emit_directly(self, image): self.image_to_display.emit(image) def close(self): @@ -408,8 +440,23 @@ def close(self): self.stop_signal_received = True self.thread.join() + class Configuration: - def __init__(self,mode_id=None,name=None,color=None,camera_sn=None,exposure_time=None,analog_gain=None,illumination_source=None,illumination_intensity=None,z_offset=None,pixel_format=None,_pixel_format_options=None,emission_filter_position=None): + def __init__( + self, + mode_id=None, + name=None, + color=None, + camera_sn=None, + exposure_time=None, + analog_gain=None, + illumination_source=None, + illumination_intensity=None, + z_offset=None, + pixel_format=None, + _pixel_format_options=None, + emission_filter_position=None, + ): self.id = mode_id self.name = name self.color = color @@ -429,23 +476,35 @@ def __init__(self,mode_id=None,name=None,color=None,camera_sn=None,exposure_time class LiveController(QObject): - def __init__(self,camera,microcontroller,configurationManager,illuminationController,parent=None,control_illumination=True,use_internal_timer_for_hardware_trigger=True,for_displacement_measurement=False): + def __init__( + self, + camera, + microcontroller, + configurationManager, + illuminationController, + parent=None, + control_illumination=True, + use_internal_timer_for_hardware_trigger=True, + for_displacement_measurement=False, + ): QObject.__init__(self) self.microscope = parent self.camera = camera self.microcontroller = microcontroller self.configurationManager = configurationManager self.currentConfiguration = None - self.trigger_mode = TriggerMode.SOFTWARE # @@@ change to None + self.trigger_mode = TriggerMode.SOFTWARE # @@@ change to None self.is_live = False self.control_illumination = control_illumination self.illumination_on = False self.illuminationController = illuminationController - self.use_internal_timer_for_hardware_trigger = use_internal_timer_for_hardware_trigger # use QTimer vs timer in the MCU + self.use_internal_timer_for_hardware_trigger = ( + use_internal_timer_for_hardware_trigger # use QTimer vs timer in the MCU + ) self.for_displacement_measurement = for_displacement_measurement - self.fps_trigger = 1; - self.timer_trigger_interval = (1/self.fps_trigger)*1000 + self.fps_trigger = 1 + self.timer_trigger_interval = (1 / self.fps_trigger) * 1000 self.timer_trigger = QTimer() self.timer_trigger.setInterval(int(self.timer_trigger_interval)) @@ -457,7 +516,7 @@ def __init__(self,camera,microcontroller,configurationManager,illuminationContro self.counter = 0 self.timestamp_last = 0 - self.display_resolution_scaling = DEFAULT_DISPLAY_CROP/100 + self.display_resolution_scaling = DEFAULT_DISPLAY_CROP / 100 self.enable_channel_auto_filter_switching = True @@ -466,145 +525,186 @@ def __init__(self,camera,microcontroller,configurationManager,illuminationContro if SUPPORT_SCIMICROSCOPY_LED_ARRAY: # to do: add error handling - self.led_array = serial_peripherals.SciMicroscopyLEDArray(SCIMICROSCOPY_LED_ARRAY_SN,SCIMICROSCOPY_LED_ARRAY_DISTANCE,SCIMICROSCOPY_LED_ARRAY_TURN_ON_DELAY) + self.led_array = serial_peripherals.SciMicroscopyLEDArray( + SCIMICROSCOPY_LED_ARRAY_SN, SCIMICROSCOPY_LED_ARRAY_DISTANCE, SCIMICROSCOPY_LED_ARRAY_TURN_ON_DELAY + ) self.led_array.set_NA(SCIMICROSCOPY_LED_ARRAY_DEFAULT_NA) # illumination control def turn_on_illumination(self): - if self.illuminationController is not None and not 'LED matrix' in self.currentConfiguration.name: - self.illuminationController.turn_on_illumination(int(self.configurationManager.extract_wavelength(self.currentConfiguration.name))) - elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and 'LED matrix' in self.currentConfiguration.name: + if self.illuminationController is not None and not "LED matrix" in self.currentConfiguration.name: + self.illuminationController.turn_on_illumination( + int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)) + ) + elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and "LED matrix" in self.currentConfiguration.name: self.led_array.turn_on_illumination() else: self.microcontroller.turn_on_illumination() self.illumination_on = True def turn_off_illumination(self): - if self.illuminationController is not None and not 'LED matrix' in self.currentConfiguration.name: - self.illuminationController.turn_off_illumination(int(self.configurationManager.extract_wavelength(self.currentConfiguration.name))) - elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and 'LED matrix' in self.currentConfiguration.name: + if self.illuminationController is not None and not "LED matrix" in self.currentConfiguration.name: + self.illuminationController.turn_off_illumination( + int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)) + ) + elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and "LED matrix" in self.currentConfiguration.name: self.led_array.turn_off_illumination() else: self.microcontroller.turn_off_illumination() self.illumination_on = False - def set_illumination(self,illumination_source,intensity,update_channel_settings=True): - if illumination_source < 10: # LED matrix + def set_illumination(self, illumination_source, intensity, update_channel_settings=True): + if illumination_source < 10: # LED matrix if SUPPORT_SCIMICROSCOPY_LED_ARRAY: # set color - if 'BF LED matrix full_R' in self.currentConfiguration.name: - self.led_array.set_color((1,0,0)) - elif 'BF LED matrix full_G' in self.currentConfiguration.name: - self.led_array.set_color((0,1,0)) - elif 'BF LED matrix full_B' in self.currentConfiguration.name: - self.led_array.set_color((0,0,1)) + if "BF LED matrix full_R" in self.currentConfiguration.name: + self.led_array.set_color((1, 0, 0)) + elif "BF LED matrix full_G" in self.currentConfiguration.name: + self.led_array.set_color((0, 1, 0)) + elif "BF LED matrix full_B" in self.currentConfiguration.name: + self.led_array.set_color((0, 0, 1)) else: self.led_array.set_color(SCIMICROSCOPY_LED_ARRAY_DEFAULT_COLOR) # set intensity self.led_array.set_brightness(intensity) # set mode - if 'BF LED matrix left half' in self.currentConfiguration.name: - self.led_array.set_illumination('dpc.l') - if 'BF LED matrix right half' in self.currentConfiguration.name: - self.led_array.set_illumination('dpc.r') - if 'BF LED matrix top half' in self.currentConfiguration.name: - self.led_array.set_illumination('dpc.t') - if 'BF LED matrix bottom half' in self.currentConfiguration.name: - self.led_array.set_illumination('dpc.b') - if 'BF LED matrix full' in self.currentConfiguration.name: - self.led_array.set_illumination('bf') - if 'DF LED matrix' in self.currentConfiguration.name: - self.led_array.set_illumination('df') + if "BF LED matrix left half" in self.currentConfiguration.name: + self.led_array.set_illumination("dpc.l") + if "BF LED matrix right half" in self.currentConfiguration.name: + self.led_array.set_illumination("dpc.r") + if "BF LED matrix top half" in self.currentConfiguration.name: + self.led_array.set_illumination("dpc.t") + if "BF LED matrix bottom half" in self.currentConfiguration.name: + self.led_array.set_illumination("dpc.b") + if "BF LED matrix full" in self.currentConfiguration.name: + self.led_array.set_illumination("bf") + if "DF LED matrix" in self.currentConfiguration.name: + self.led_array.set_illumination("df") else: - if 'BF LED matrix full_R' in self.currentConfiguration.name: - self.microcontroller.set_illumination_led_matrix(illumination_source,r=(intensity/100),g=0,b=0) - elif 'BF LED matrix full_G' in self.currentConfiguration.name: - self.microcontroller.set_illumination_led_matrix(illumination_source,r=0,g=(intensity/100),b=0) - elif 'BF LED matrix full_B' in self.currentConfiguration.name: - self.microcontroller.set_illumination_led_matrix(illumination_source,r=0,g=0,b=(intensity/100)) + if "BF LED matrix full_R" in self.currentConfiguration.name: + self.microcontroller.set_illumination_led_matrix(illumination_source, r=(intensity / 100), g=0, b=0) + elif "BF LED matrix full_G" in self.currentConfiguration.name: + self.microcontroller.set_illumination_led_matrix(illumination_source, r=0, g=(intensity / 100), b=0) + elif "BF LED matrix full_B" in self.currentConfiguration.name: + self.microcontroller.set_illumination_led_matrix(illumination_source, r=0, g=0, b=(intensity / 100)) else: - self.microcontroller.set_illumination_led_matrix(illumination_source,r=(intensity/100)*LED_MATRIX_R_FACTOR,g=(intensity/100)*LED_MATRIX_G_FACTOR,b=(intensity/100)*LED_MATRIX_B_FACTOR) + self.microcontroller.set_illumination_led_matrix( + illumination_source, + r=(intensity / 100) * LED_MATRIX_R_FACTOR, + g=(intensity / 100) * LED_MATRIX_G_FACTOR, + b=(intensity / 100) * LED_MATRIX_B_FACTOR, + ) else: # update illumination if self.illuminationController is not None: - self.illuminationController.set_intensity(int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)), intensity) - elif ENABLE_NL5 and NL5_USE_DOUT and 'Fluorescence' in self.currentConfiguration.name: + self.illuminationController.set_intensity( + int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)), intensity + ) + elif ENABLE_NL5 and NL5_USE_DOUT and "Fluorescence" in self.currentConfiguration.name: wavelength = int(self.currentConfiguration.name[13:16]) self.microscope.nl5.set_active_channel(NL5_WAVENLENGTH_MAP[wavelength]) if NL5_USE_AOUT and update_channel_settings: - self.microscope.nl5.set_laser_power(NL5_WAVENLENGTH_MAP[wavelength],int(intensity)) + self.microscope.nl5.set_laser_power(NL5_WAVENLENGTH_MAP[wavelength], int(intensity)) if ENABLE_CELLX: - self.microscope.cellx.set_laser_power(NL5_WAVENLENGTH_MAP[wavelength],int(intensity)) + self.microscope.cellx.set_laser_power(NL5_WAVENLENGTH_MAP[wavelength], int(intensity)) else: - self.microcontroller.set_illumination(illumination_source,intensity) + self.microcontroller.set_illumination(illumination_source, intensity) # set emission filter position if ENABLE_SPINNING_DISK_CONFOCAL: try: - self.microscope.xlight.set_emission_filter(XLIGHT_EMISSION_FILTER_MAPPING[illumination_source],extraction=False,validate=XLIGHT_VALIDATE_WHEEL_POS) + self.microscope.xlight.set_emission_filter( + XLIGHT_EMISSION_FILTER_MAPPING[illumination_source], + extraction=False, + validate=XLIGHT_VALIDATE_WHEEL_POS, + ) except Exception as e: - print('not setting emission filter position due to ' + str(e)) + print("not setting emission filter position due to " + str(e)) if USE_ZABER_EMISSION_FILTER_WHEEL and self.enable_channel_auto_filter_switching: try: - if self.currentConfiguration.emission_filter_position != self.microscope.emission_filter_wheel.current_index: + if ( + self.currentConfiguration.emission_filter_position + != self.microscope.emission_filter_wheel.current_index + ): if ZABER_EMISSION_FILTER_WHEEL_BLOCKING_CALL: - self.microscope.emission_filter_wheel.set_emission_filter(self.currentConfiguration.emission_filter_position,blocking=True) + self.microscope.emission_filter_wheel.set_emission_filter( + self.currentConfiguration.emission_filter_position, blocking=True + ) else: - self.microscope.emission_filter_wheel.set_emission_filter(self.currentConfiguration.emission_filter_position,blocking=False) + self.microscope.emission_filter_wheel.set_emission_filter( + self.currentConfiguration.emission_filter_position, blocking=False + ) if self.trigger_mode == TriggerMode.SOFTWARE: - time.sleep(ZABER_EMISSION_FILTER_WHEEL_DELAY_MS/1000) + time.sleep(ZABER_EMISSION_FILTER_WHEEL_DELAY_MS / 1000) else: - time.sleep(max(0,ZABER_EMISSION_FILTER_WHEEL_DELAY_MS/1000-self.camera.strobe_delay_us/1e6)) + time.sleep( + max(0, ZABER_EMISSION_FILTER_WHEEL_DELAY_MS / 1000 - self.camera.strobe_delay_us / 1e6) + ) except Exception as e: - print('not setting emission filter position due to ' + str(e)) + print("not setting emission filter position due to " + str(e)) - if USE_OPTOSPIN_EMISSION_FILTER_WHEEL and self.enable_channel_auto_filter_switching and OPTOSPIN_EMISSION_FILTER_WHEEL_TTL_TRIGGER == False: + if ( + USE_OPTOSPIN_EMISSION_FILTER_WHEEL + and self.enable_channel_auto_filter_switching + and OPTOSPIN_EMISSION_FILTER_WHEEL_TTL_TRIGGER == False + ): try: - if self.currentConfiguration.emission_filter_position != self.microscope.emission_filter_wheel.current_index: - self.microscope.emission_filter_wheel.set_emission_filter(self.currentConfiguration.emission_filter_position) + if ( + self.currentConfiguration.emission_filter_position + != self.microscope.emission_filter_wheel.current_index + ): + self.microscope.emission_filter_wheel.set_emission_filter( + self.currentConfiguration.emission_filter_position + ) if self.trigger_mode == TriggerMode.SOFTWARE: - time.sleep(OPTOSPIN_EMISSION_FILTER_WHEEL_DELAY_MS/1000) + time.sleep(OPTOSPIN_EMISSION_FILTER_WHEEL_DELAY_MS / 1000) elif self.trigger_mode == TriggerMode.HARDWARE: - time.sleep(max(0,OPTOSPIN_EMISSION_FILTER_WHEEL_DELAY_MS/1000-self.camera.strobe_delay_us/1e6)) + time.sleep( + max(0, OPTOSPIN_EMISSION_FILTER_WHEEL_DELAY_MS / 1000 - self.camera.strobe_delay_us / 1e6) + ) except Exception as e: - print('not setting emission filter position due to ' + str(e)) + print("not setting emission filter position due to " + str(e)) if USE_SQUID_FILTERWHEEL and self.enable_channel_auto_filter_switching: try: self.microscope.squid_filter_wheel.set_emission(self.currentConfiguration.emission_filter_position) except Exception as e: - print('not setting emission filter position due to ' + str(e)) + print("not setting emission filter position due to " + str(e)) def start_live(self): self.is_live = True self.camera.is_live = True self.camera.start_streaming() - if self.trigger_mode == TriggerMode.SOFTWARE or ( self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger ): - self.camera.enable_callback() # in case it's disabled e.g. by the laser AF controller + if self.trigger_mode == TriggerMode.SOFTWARE or ( + self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger + ): + self.camera.enable_callback() # in case it's disabled e.g. by the laser AF controller self._start_triggerred_acquisition() # if controlling the laser displacement measurement camera if self.for_displacement_measurement: - self.microcontroller.set_pin_level(MCU_PINS.AF_LASER,1) + self.microcontroller.set_pin_level(MCU_PINS.AF_LASER, 1) def stop_live(self): if self.is_live: self.is_live = False self.camera.is_live = False - if hasattr(self.camera,'stop_exposure'): + if hasattr(self.camera, "stop_exposure"): self.camera.stop_exposure() if self.trigger_mode == TriggerMode.SOFTWARE: self._stop_triggerred_acquisition() # self.camera.stop_streaming() # 20210113 this line seems to cause problems when using af with multipoint if self.trigger_mode == TriggerMode.CONTINUOUS: self.camera.stop_streaming() - if ( self.trigger_mode == TriggerMode.SOFTWARE ) or ( self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger ): + if (self.trigger_mode == TriggerMode.SOFTWARE) or ( + self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger + ): self._stop_triggerred_acquisition() if self.control_illumination: self.turn_off_illumination() # if controlling the laser displacement measurement camera if self.for_displacement_measurement: - self.microcontroller.set_pin_level(MCU_PINS.AF_LASER,0) + self.microcontroller.set_pin_level(MCU_PINS.AF_LASER, 0) # software trigger related def trigger_acquisition(self): @@ -616,7 +716,7 @@ def trigger_acquisition(self): # measure real fps timestamp_now = round(time.time()) if timestamp_now == self.timestamp_last: - self.counter = self.counter+1 + self.counter = self.counter + 1 else: self.timestamp_last = timestamp_now self.fps_real = self.counter @@ -627,23 +727,27 @@ def trigger_acquisition(self): if ENABLE_NL5 and NL5_USE_DOUT: self.microscope.nl5.start_acquisition() else: - self.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=self.camera.exposure_time*1000) + self.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000 + ) def _start_triggerred_acquisition(self): self.timer_trigger.start() - def _set_trigger_fps(self,fps_trigger): + def _set_trigger_fps(self, fps_trigger): self.fps_trigger = fps_trigger - self.timer_trigger_interval = (1/self.fps_trigger)*1000 + self.timer_trigger_interval = (1 / self.fps_trigger) * 1000 self.timer_trigger.setInterval(int(self.timer_trigger_interval)) def _stop_triggerred_acquisition(self): self.timer_trigger.stop() # trigger mode and settings - def set_trigger_mode(self,mode): + def set_trigger_mode(self, mode): if mode == TriggerMode.SOFTWARE: - if self.is_live and ( self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger ): + if self.is_live and ( + self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger + ): self._stop_triggerred_acquisition() self.camera.set_software_triggered_acquisition() if self.is_live: @@ -659,18 +763,22 @@ def set_trigger_mode(self,mode): if self.is_live and self.use_internal_timer_for_hardware_trigger: self._start_triggerred_acquisition() if mode == TriggerMode.CONTINUOUS: - if ( self.trigger_mode == TriggerMode.SOFTWARE ) or ( self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger ): + if (self.trigger_mode == TriggerMode.SOFTWARE) or ( + self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger + ): self._stop_triggerred_acquisition() self.camera.set_continuous_acquisition() self.trigger_mode = mode - def set_trigger_fps(self,fps): - if ( self.trigger_mode == TriggerMode.SOFTWARE ) or ( self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger ): + def set_trigger_fps(self, fps): + if (self.trigger_mode == TriggerMode.SOFTWARE) or ( + self.trigger_mode == TriggerMode.HARDWARE and self.use_internal_timer_for_hardware_trigger + ): self._set_trigger_fps(fps) # set microscope mode # @@@ to do: change softwareTriggerGenerator to TriggerGeneratror - def set_microscope_mode(self,configuration): + def set_microscope_mode(self, configuration): self.currentConfiguration = configuration print("setting microscope mode to " + self.currentConfiguration.name) @@ -687,7 +795,9 @@ def set_microscope_mode(self,configuration): # set illumination if self.control_illumination: - self.set_illumination(self.currentConfiguration.illumination_source,self.currentConfiguration.illumination_intensity) + self.set_illumination( + self.currentConfiguration.illumination_source, self.currentConfiguration.illumination_intensity + ) # restart live if self.is_live is True: @@ -705,7 +815,7 @@ def on_new_frame(self): self.turn_off_illumination() def set_display_resolution_scaling(self, display_resolution_scaling): - self.display_resolution_scaling = display_resolution_scaling/100 + self.display_resolution_scaling = display_resolution_scaling / 100 def reset_strobe_arugment(self): # re-calculate the strobe_delay_us value @@ -735,11 +845,11 @@ def move_to_slide_loading_position(self): self.signal_stop_live.emit() # retract z - self.slidePositionController.z_pos = self.stage.get_pos().z_mm # zpos at the beginning of the scan + self.slidePositionController.z_pos = self.stage.get_pos().z_mm # zpos at the beginning of the scan self.stage.move_z_to(OBJECTIVE_RETRACTED_POS_MM, blocking=False) self.stage.wait_for_idle(SLIDE_POTISION_SWITCHING_TIMEOUT_LIMIT_S) - print('z retracted') + print("z retracted") self.slidePositionController.objective_retracted = True # move to position @@ -752,11 +862,12 @@ def move_to_slide_loading_position(self): x_pos_mm=a_large_limit_mm, x_neg_mm=-a_large_limit_mm, y_pos_mm=a_large_limit_mm, - y_neg_mm=-a_large_limit_mm) + y_neg_mm=-a_large_limit_mm, + ) # home for the first time if self.slidePositionController.homing_done == False: - print('running homing first') + print("running homing first") timestamp_start = time.time() # x needs to be at > + 20 mm when homing y self.stage.move_x(20) @@ -774,7 +885,8 @@ def move_to_slide_loading_position(self): x_pos_mm=self.stage.get_config().X_AXIS.MAX_POSITION, x_neg_mm=self.stage.get_config().X_AXIS.MIN_POSITION, y_pos_mm=self.stage.get_config().Y_AXIS.MAX_POSITION, - y_neg_mm=self.stage.get_config().Y_AXIS.MIN_POSITION) + y_neg_mm=self.stage.get_config().Y_AXIS.MIN_POSITION, + ) else: # for glass slide @@ -852,11 +964,13 @@ def move_to_slide_scanning_position(self): # migration, we only compensated for backlash in the case that we were using PID control. Since that # info isn't plumbed through yet (or ever from now on?), we just always compensate now. It doesn't hurt # in the case of not needing it, except that it's a little slower because we need 2 moves. - mm_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units(max(160, 20*self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) - self.stage.move_z_to(self.slidePositionController.z_pos-mm_to_clear_backlash) + mm_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) + self.stage.move_z_to(self.slidePositionController.z_pos - mm_to_clear_backlash) self.stage.move_z_to(self.slidePositionController.z_pos) self.slidePositionController.objective_retracted = False - print('z position restored') + print("z position restored") if was_live: self.signal_resume_live.emit() @@ -892,8 +1006,10 @@ def move_to_slide_loading_position(self): self.slidePositionControlWorker.moveToThread(self.thread) # connect signals and slots self.thread.started.connect(self.slidePositionControlWorker.move_to_slide_loading_position) - self.slidePositionControlWorker.signal_stop_live.connect(self.slot_stop_live,type=Qt.BlockingQueuedConnection) - self.slidePositionControlWorker.signal_resume_live.connect(self.slot_resume_live,type=Qt.BlockingQueuedConnection) + self.slidePositionControlWorker.signal_stop_live.connect(self.slot_stop_live, type=Qt.BlockingQueuedConnection) + self.slidePositionControlWorker.signal_resume_live.connect( + self.slot_resume_live, type=Qt.BlockingQueuedConnection + ) self.slidePositionControlWorker.finished.connect(self.signal_slide_loading_position_reached.emit) self.slidePositionControlWorker.finished.connect(self.slidePositionControlWorker.deleteLater) self.slidePositionControlWorker.finished.connect(self.thread.quit) @@ -903,7 +1019,7 @@ def move_to_slide_loading_position(self): self.thread.start() def move_to_slide_scanning_position(self): - # create a QThread object + # create a QThread object self.thread = QThread() # create a worker object self.slidePositionControlWorker = SlidePositionControlWorker(self, self.stage) @@ -911,15 +1027,17 @@ def move_to_slide_scanning_position(self): self.slidePositionControlWorker.moveToThread(self.thread) # connect signals and slots self.thread.started.connect(self.slidePositionControlWorker.move_to_slide_scanning_position) - self.slidePositionControlWorker.signal_stop_live.connect(self.slot_stop_live,type=Qt.BlockingQueuedConnection) - self.slidePositionControlWorker.signal_resume_live.connect(self.slot_resume_live,type=Qt.BlockingQueuedConnection) + self.slidePositionControlWorker.signal_stop_live.connect(self.slot_stop_live, type=Qt.BlockingQueuedConnection) + self.slidePositionControlWorker.signal_resume_live.connect( + self.slot_resume_live, type=Qt.BlockingQueuedConnection + ) self.slidePositionControlWorker.finished.connect(self.signal_slide_scanning_position_reached.emit) self.slidePositionControlWorker.finished.connect(self.slidePositionControlWorker.deleteLater) self.slidePositionControlWorker.finished.connect(self.thread.quit) self.thread.finished.connect(self.thread.quit) # self.slidePositionControlWorker.finished.connect(self.threadFinished,type=Qt.BlockingQueuedConnection) # start the thread - print('before thread.start()') + print("before thread.start()") self.thread.start() self.signal_clear_slide.emit() @@ -929,13 +1047,14 @@ def slot_stop_live(self): def slot_resume_live(self): self.liveController.start_live() + class AutofocusWorker(QObject): finished = Signal() image_to_display = Signal(np.ndarray) # signal_current_configuration = Signal(Configuration) - def __init__(self,autofocusController): + def __init__(self, autofocusController): QObject.__init__(self) self.autofocusController = autofocusController @@ -961,15 +1080,16 @@ def wait_till_operation_is_completed(self): def run_autofocus(self): # @@@ to add: increase gain, decrease exposure time # @@@ can move the execution into a thread - done 08/21/2021 - focus_measure_vs_z = [0]*self.N + focus_measure_vs_z = [0] * self.N focus_measure_max = 0 - z_af_offset = self.deltaZ*round(self.N/2) + z_af_offset = self.deltaZ * round(self.N / 2) # maneuver for achiving uniform step size and repeatability when using open-loop control # can be moved to the firmware mm_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( - max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) self.stage.move_z(-mm_to_clear_backlash - z_af_offset) self.stage.move_z(mm_to_clear_backlash) @@ -985,12 +1105,14 @@ def run_autofocus(self): self.camera.send_trigger() image = self.camera.read_frame() elif self.liveController.trigger_mode == TriggerMode.HARDWARE: - if 'Fluorescence' in self.liveController.currentConfiguration.name and ENABLE_NL5 and NL5_USE_DOUT: - self.camera.image_is_ready = False # to remove + if "Fluorescence" in self.liveController.currentConfiguration.name and ENABLE_NL5 and NL5_USE_DOUT: + self.camera.image_is_ready = False # to remove self.microscope.nl5.start_acquisition() image = self.camera.read_frame(reset_image_ready_flag=False) else: - self.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=self.camera.exposure_time*1000) + self.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000 + ) image = self.camera.read_frame() if image is None: continue @@ -998,27 +1120,31 @@ def run_autofocus(self): if self.liveController.trigger_mode == TriggerMode.SOFTWARE: self.liveController.turn_off_illumination() - image = utils.crop_image(image,self.crop_width,self.crop_height) - image = utils.rotate_and_flip_image(image,rotate_image_angle=self.camera.rotate_image_angle,flip_image=self.camera.flip_image) + image = utils.crop_image(image, self.crop_width, self.crop_height) + image = utils.rotate_and_flip_image( + image, rotate_image_angle=self.camera.rotate_image_angle, flip_image=self.camera.flip_image + ) self.image_to_display.emit(image) - #image_to_display = utils.crop_image(image,round(self.crop_width* self.liveController.display_resolution_scaling), round(self.crop_height* self.liveController.display_resolution_scaling)) + # image_to_display = utils.crop_image(image,round(self.crop_width* self.liveController.display_resolution_scaling), round(self.crop_height* self.liveController.display_resolution_scaling)) QApplication.processEvents() timestamp_0 = time.time() - focus_measure = utils.calculate_focus_measure(image,FOCUS_MEASURE_OPERATOR) + focus_measure = utils.calculate_focus_measure(image, FOCUS_MEASURE_OPERATOR) timestamp_1 = time.time() - print(' calculating focus measure took ' + str(timestamp_1-timestamp_0) + ' second') + print(" calculating focus measure took " + str(timestamp_1 - timestamp_0) + " second") focus_measure_vs_z[i] = focus_measure - print(i,focus_measure) + print(i, focus_measure) focus_measure_max = max(focus_measure, focus_measure_max) - if focus_measure < focus_measure_max*AF.STOP_THRESHOLD: + if focus_measure < focus_measure_max * AF.STOP_THRESHOLD: break QApplication.processEvents() # maneuver for achiving uniform step size and repeatability when using open-loop control # TODO(imo): The backlash handling should be done at a lower level. For now, do backlash compensation no matter if it makes sense to do or not (it is not harmful if it doesn't make sense) - mm_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units(max(160, 20*self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + mm_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) self.stage.move_z(-mm_to_clear_backlash - steps_moved * self.deltaZ) # determine the in-focus position idx_in_focus = focus_measure_vs_z.index(max(focus_measure_vs_z)) @@ -1028,9 +1154,10 @@ def run_autofocus(self): # move to the calculated in-focus position if idx_in_focus == 0: - print('moved to the bottom end of the AF range') - if idx_in_focus == self.N-1: - print('moved to the top end of the AF range') + print("moved to the bottom end of the AF range") + if idx_in_focus == self.N - 1: + print("moved to the top end of the AF range") + class AutoFocusController(QObject): @@ -1038,7 +1165,7 @@ class AutoFocusController(QObject): autofocusFinished = Signal() image_to_display = Signal(np.ndarray) - def __init__(self,camera, stage: AbstractStage, liveController, microcontroller: Microcontroller): + def __init__(self, camera, stage: AbstractStage, liveController, microcontroller: Microcontroller): QObject.__init__(self) self.camera = camera self.stage = stage @@ -1052,13 +1179,13 @@ def __init__(self,camera, stage: AbstractStage, liveController, microcontroller: self.focus_map_coords = [] self.use_focus_map = False - def set_N(self,N): + def set_N(self, N): self.N = N def set_deltaZ(self, delta_z_um): self.deltaZ = delta_z_um / 1000 - def set_crop(self,crop_width,crop_height): + def set_crop(self, crop_width, crop_height): self.crop_width = crop_width self.crop_height = crop_height @@ -1096,10 +1223,10 @@ def autofocus(self, focus_map_override=False): # create a QThread object try: if self.thread.isRunning(): - print('*** autofocus thread is still running ***') + print("*** autofocus thread is still running ***") self.thread.terminate() self.thread.wait() - print('*** autofocus threaded manually stopped ***') + print("*** autofocus threaded manually stopped ***") except: pass self.thread = QThread() @@ -1129,19 +1256,19 @@ def _on_autofocus_completed(self): # emit the autofocus finished signal to enable the UI self.autofocusFinished.emit() QApplication.processEvents() - print('autofocus finished') + print("autofocus finished") # update the state self.autofocus_in_progress = False - def slot_image_to_display(self,image): + def slot_image_to_display(self, image): self.image_to_display.emit(image) def wait_till_autofocus_has_completed(self): while self.autofocus_in_progress == True: QApplication.processEvents() time.sleep(0.005) - print('autofocus wait has completed, exit wait') + print("autofocus wait has completed, exit wait") def set_focus_map_use(self, enable): if not enable: @@ -1152,9 +1279,9 @@ def set_focus_map_use(self, enable): print("Not enough coordinates (less than 3) for focus map generation, disabling focus map.") self.use_focus_map = False return - x1,y1,_ = self.focus_map_coords[0] - x2,y2,_ = self.focus_map_coords[1] - x3,y3,_ = self.focus_map_coords[2] + x1, y1, _ = self.focus_map_coords[0] + x2, y2, _ = self.focus_map_coords[1] + x3, y3, _ = self.focus_map_coords[2] detT = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) if detT == 0: @@ -1170,23 +1297,23 @@ def clear_focus_map(self): self.focus_map_coords = [] self.set_focus_map_use(False) - def gen_focus_map(self, coord1,coord2,coord3): + def gen_focus_map(self, coord1, coord2, coord3): """ Navigate to 3 coordinates and get your focus-map coordinates by autofocusing there and saving the z-values. :param coord1-3: Tuples of (x,y) values, coordinates in mm. :raise: ValueError if coordinates are all on the same line """ - x1,y1 = coord1 - x2,y2 = coord2 - x3,y3 = coord3 + x1, y1 = coord1 + x2, y2 = coord2 + x3, y3 = coord3 detT = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) if detT == 0: raise ValueError("Your 3 x-y coordinates are linear") self.focus_map_coords = [] - for coord in [coord1,coord2,coord3]: + for coord in [coord1, coord2, coord3]: print(f"Navigating to coordinates ({coord[0]},{coord[1]}) to sample for focus map") self.stage.move_x_to(coord[0]) self.stage.move_y_to(coord[1]) @@ -1213,17 +1340,19 @@ def add_current_coords_to_focus_map(self): y = pos.y_mm z = pos.z_mm if len(self.focus_map_coords) >= 2: - x1,y1,_ = self.focus_map_coords[0] - x2,y2,_ = self.focus_map_coords[1] + x1, y1, _ = self.focus_map_coords[0] + x2, y2, _ = self.focus_map_coords[1] x3 = x y3 = y - detT = (y2-y3) * (x1-x3) + (x3-x2) * (y1-y3) + detT = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) if detT == 0: - raise ValueError("Your 3 x-y coordinates are linear. Navigate to a different coordinate or clear and try again.") + raise ValueError( + "Your 3 x-y coordinates are linear. Navigate to a different coordinate or clear and try again." + ) if len(self.focus_map_coords) >= 3: self.focus_map_coords.pop() - self.focus_map_coords.append((x,y,z)) + self.focus_map_coords.append((x, y, z)) print(f"Added triple ({x},{y},{z}) to focus map") @@ -1232,14 +1361,14 @@ class MultiPointWorker(QObject): finished = Signal() image_to_display = Signal(np.ndarray) spectrum_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) signal_current_configuration = Signal(Configuration) - signal_register_current_fov = Signal(float,float) + signal_register_current_fov = Signal(float, float) signal_detection_stats = Signal(object) signal_update_stats = Signal(object) signal_z_piezo_um = Signal(float) napari_layers_init = Signal(int, int, object) - napari_layers_update = Signal(np.ndarray, float, float, int, str) # image, x_mm, y_mm, k, channel + napari_layers_update = Signal(np.ndarray, float, float, int, str) # image, x_mm, y_mm, k, channel napari_rtp_layers_update = Signal(np.ndarray, str) signal_acquisition_progress = Signal(int, int, int) signal_region_progress = Signal(int, int) @@ -1268,7 +1397,7 @@ def __init__(self, multiPointController): self.deltaZ = self.multiPointController.deltaZ self.dt = self.multiPointController.deltat self.do_autofocus = self.multiPointController.do_autofocus - self.do_reflection_af= self.multiPointController.do_reflection_af + self.do_reflection_af = self.multiPointController.do_reflection_af self.crop_width = self.multiPointController.crop_width self.crop_height = self.multiPointController.crop_height self.display_resolution_scaling = self.multiPointController.display_resolution_scaling @@ -1316,12 +1445,14 @@ def update_stats(self, new_stats): self._log.info("stats", self.count) for k in new_stats.keys(): try: - self.detection_stats[k]+=new_stats[k] + self.detection_stats[k] += new_stats[k] except: self.detection_stats[k] = 0 self.detection_stats[k] += new_stats[k] if "Total RBC" in self.detection_stats and "Total Positives" in self.detection_stats: - self.detection_stats["Positives per 5M RBC"] = 5e6*(self.detection_stats["Total Positives"]/self.detection_stats["Total RBC"]) + self.detection_stats["Positives per 5M RBC"] = 5e6 * ( + self.detection_stats["Total Positives"] / self.detection_stats["Total RBC"] + ) self.signal_detection_stats.emit(self.detection_stats) def update_use_piezo(self, value): @@ -1341,27 +1472,27 @@ def run(self): self.run_single_time_point() self.time_point = self.time_point + 1 - if self.dt == 0: # continous acquisition + if self.dt == 0: # continous acquisition pass else: # timed acquisition # check if the aquisition has taken longer than dt or integer multiples of dt, if so skip the next time point(s) - while time.time() > self.timestamp_acquisition_started + self.time_point*self.dt: - self._log.info('skip time point ' + str(self.time_point+1)) - self.time_point = self.time_point+1 + while time.time() > self.timestamp_acquisition_started + self.time_point * self.dt: + self._log.info("skip time point " + str(self.time_point + 1)) + self.time_point = self.time_point + 1 # check if it has reached Nt if self.time_point == self.Nt: - break # no waiting after taking the last time point + break # no waiting after taking the last time point # wait until it's time to do the next acquisition - while time.time() < self.timestamp_acquisition_started + self.time_point*self.dt: + while time.time() < self.timestamp_acquisition_started + self.time_point * self.dt: if self.multiPointController.abort_acqusition_requested: break time.sleep(0.05) elapsed_time = time.perf_counter_ns() - self.start_time - self._log.info("Time taken for acquisition: " + str(elapsed_time/10**9)) + self._log.info("Time taken for acquisition: " + str(elapsed_time / 10**9)) # End processing using the updated method if DO_FLUORESCENCE_RTP: @@ -1380,10 +1511,10 @@ def run_single_time_point(self): start = time.time() self.microcontroller.enable_joystick(False) - self._log.debug('multipoint acquisition - time point ' + str(self.time_point+1)) + self._log.debug("multipoint acquisition - time point " + str(self.time_point + 1)) # for each time point, create a new folder - current_path = os.path.join(self.base_path,self.experiment_ID,str(self.time_point)) + current_path = os.path.join(self.base_path, self.experiment_ID, str(self.time_point)) os.mkdir(current_path) slide_path = os.path.join(self.base_path, self.experiment_ID) @@ -1397,7 +1528,7 @@ def run_single_time_point(self): 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) + self.coordinates_pd.to_csv(os.path.join(current_path, "coordinates.csv"), index=False, header=True) utils.create_done_file(current_path) # TODO(imo): If anything throws above, we don't re-enable the joystick self.microcontroller.enable_joystick(True) @@ -1407,46 +1538,43 @@ def initialize_z_stack(self): self.count_rtp = 0 # z stacking config - if self.z_stacking_config == 'FROM TOP': + if self.z_stacking_config == "FROM TOP": self.deltaZ = -abs(self.deltaZ) self.move_to_z_level(self.z_range[1]) else: self.move_to_z_level(self.z_range[0]) - self.z_pos = self.stage.get_pos().z_mm # zpos at the beginning of the scan + self.z_pos = self.stage.get_pos().z_mm # zpos at the beginning of the scan # reset piezo to home position if self.use_piezo: self.z_piezo_um = OBJECTIVE_PIEZO_HOME_UM self.microcontroller.set_piezo_um(self.z_piezo_um) # TODO(imo): Not sure the wait comment below is actually correct? Should this wait just be in the set_piezo_um helper? - if self.liveController.trigger_mode == TriggerMode.SOFTWARE: # for hardware trigger, delay is in waiting for the last row to start exposure - time.sleep(MULTIPOINT_PIEZO_DELAY_MS/1000) + if ( + self.liveController.trigger_mode == TriggerMode.SOFTWARE + ): # for hardware trigger, delay is in waiting for the last row to start exposure + time.sleep(MULTIPOINT_PIEZO_DELAY_MS / 1000) if MULTIPOINT_PIEZO_UPDATE_DISPLAY: self.signal_z_piezo_um.emit(self.z_piezo_um) 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 [] - self.coordinates_pd = pd.DataFrame(columns=['region', 'fov'] + base_columns + piezo_column) + base_columns = ["z_level", "x (mm)", "y (mm)", "z (um)", "time"] + piezo_column = ["z_piezo (um)"] if self.use_piezo else [] + self.coordinates_pd = pd.DataFrame(columns=["region", "fov"] + base_columns + piezo_column) def update_coordinates_dataframe(self, region_id, z_level, fov=None): pos = self.stage.get_pos() base_data = { - 'z_level': [z_level], - 'x (mm)': [pos.x_mm], - 'y (mm)': [pos.y_mm], - 'z (um)': [pos.z_mm * 1000], - 'time': [datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f')] + "z_level": [z_level], + "x (mm)": [pos.x_mm], + "y (mm)": [pos.y_mm], + "z (um)": [pos.z_mm * 1000], + "time": [datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f")], } - piezo_data = {'z_piezo (um)': [self.z_piezo_um - OBJECTIVE_PIEZO_HOME_UM]} if self.use_piezo else {} + piezo_data = {"z_piezo (um)": [self.z_piezo_um - OBJECTIVE_PIEZO_HOME_UM]} if self.use_piezo else {} - new_row = pd.DataFrame({ - 'region': [region_id], - 'fov': [fov], - **base_data, - **piezo_data - }) + new_row = pd.DataFrame({"region": [region_id], "fov": [fov], **base_data, **piezo_data}) self.coordinates_pd = pd.concat([self.coordinates_pd, new_row], ignore_index=True) @@ -1454,11 +1582,11 @@ def move_to_coordinate(self, coordinate_mm): print("moving to coordinate", coordinate_mm) x_mm = coordinate_mm[0] self.stage.move_x_to(x_mm) - time.sleep(SCAN_STABILIZATION_TIME_MS_X/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_X / 1000) y_mm = coordinate_mm[1] self.stage.move_y_to(y_mm) - time.sleep(SCAN_STABILIZATION_TIME_MS_Y/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Y / 1000) # check if z is included in the coordinate if len(coordinate_mm) == 3: @@ -1474,7 +1602,9 @@ def move_to_z_level(self, z_mm): # TODO(imo): We used to only do this if in PID control mode, but we don't expose the PID mode settings # yet, so for now just do this for all. # TODO(imo): Ideally this would be done at a lower level, and only if needed. As is we only remove backlash in this specific case (and no other Z moves!) - distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units(max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) self.stage.move_z(-distance_to_clear_backlash) self.stage.move_z(distance_to_clear_backlash) time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) @@ -1501,7 +1631,7 @@ def run_coordinate_acquisition(self, current_path): def acquire_at_position(self, region_id, current_path, fov): if RUN_CUSTOM_MULTIPOINT and "multipoint_custom_script_entry" in globals(): - print('run custom multipoint') + print("run custom multipoint") multipoint_custom_script_entry(self, current_path, region_id, fov) return @@ -1524,8 +1654,8 @@ def acquire_at_position(self, region_id, current_path, fov): # laser af characterization mode if LASER_AF_CHARACTERIZATION_MODE: image = self.microscope.laserAutofocusController.get_image() - saving_path = os.path.join(current_path, file_ID + '_laser af camera' + '.bmp') - iio.imwrite(saving_path,image) + saving_path = os.path.join(current_path, file_ID + "_laser af camera" + ".bmp") + iio.imwrite(saving_path, image) current_round_images = {} # iterate through selected modes @@ -1534,16 +1664,21 @@ def acquire_at_position(self, region_id, current_path, fov): self.handle_z_offset(config, True) # acquire image - if 'USB Spectrometer' not in config.name and 'RGB' not in config.name: + if "USB Spectrometer" not in config.name and "RGB" not in config.name: self.acquire_camera_image(config, file_ID, current_path, current_round_images, z_level) - elif 'RGB' in config.name: + elif "RGB" in config.name: self.acquire_rgb_image(config, file_ID, current_path, current_round_images, z_level) else: self.acquire_spectrometer_data(config, file_ID, current_path, z_level) self.handle_z_offset(config, False) - current_image = (fov * self.NZ * len(self.selected_configurations) + z_level * len(self.selected_configurations) + config_idx + 1) + current_image = ( + fov * self.NZ * len(self.selected_configurations) + + z_level * len(self.selected_configurations) + + config_idx + + 1 + ) self.signal_region_progress.emit(current_image, self.total_scans) # real time processing @@ -1570,22 +1705,37 @@ def acquire_at_position(self, region_id, current_path, fov): def run_real_time_processing(self, current_round_images, z_level): acquired_image_configs = list(current_round_images.keys()) - if 'BF LED matrix left half' in current_round_images and 'BF LED matrix right half' in current_round_images and 'Fluorescence 405 nm Ex' in current_round_images: + if ( + "BF LED matrix left half" in current_round_images + and "BF LED matrix right half" in current_round_images + and "Fluorescence 405 nm Ex" in current_round_images + ): try: print("real time processing", self.count_rtp) - if (self.microscope.model is None) or (self.microscope.device is None) or (self.microscope.classification_th is None) or (self.microscope.dataHandler is None): - raise AttributeError('microscope missing model, device, classification_th, and/or dataHandler') - I_fluorescence = current_round_images['Fluorescence 405 nm Ex'] - I_left = current_round_images['BF LED matrix left half'] - I_right = current_round_images['BF LED matrix right half'] + if ( + (self.microscope.model is None) + or (self.microscope.device is None) + or (self.microscope.classification_th is None) + or (self.microscope.dataHandler is None) + ): + raise AttributeError("microscope missing model, device, classification_th, and/or dataHandler") + I_fluorescence = current_round_images["Fluorescence 405 nm Ex"] + I_left = current_round_images["BF LED matrix left half"] + I_right = current_round_images["BF LED matrix right half"] if len(I_left.shape) == 3: - I_left = cv2.cvtColor(I_left,cv2.COLOR_RGB2GRAY) + I_left = cv2.cvtColor(I_left, cv2.COLOR_RGB2GRAY) if len(I_right.shape) == 3: - I_right = cv2.cvtColor(I_right,cv2.COLOR_RGB2GRAY) - malaria_rtp(I_fluorescence, I_left, I_right, z_level, self, - classification_test_mode=self.microscope.classification_test_mode, - sort_during_multipoint=SORT_DURING_MULTIPOINT, - disp_th_during_multipoint=DISP_TH_DURING_MULTIPOINT) + I_right = cv2.cvtColor(I_right, cv2.COLOR_RGB2GRAY) + malaria_rtp( + I_fluorescence, + I_left, + I_right, + z_level, + self, + classification_test_mode=self.microscope.classification_test_mode, + sort_during_multipoint=SORT_DURING_MULTIPOINT, + disp_th_during_multipoint=DISP_TH_DURING_MULTIPOINT, + ) self.count_rtp += 1 except AttributeError as e: print(repr(e)) @@ -1593,23 +1743,41 @@ def run_real_time_processing(self, current_round_images, z_level): def perform_autofocus(self, region_id, fov): if self.do_reflection_af == False: # contrast-based AF; perform AF only if when not taking z stack or doing z stack from center - if ((self.NZ == 1) or self.z_stacking_config == 'FROM CENTER') and (self.do_autofocus) and (self.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0): + if ( + ((self.NZ == 1) or self.z_stacking_config == "FROM CENTER") + and (self.do_autofocus) + and (self.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) + ): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in self.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in self.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) self.signal_current_configuration.emit(config_AF) - if (self.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) or self.autofocusController.use_focus_map: + if ( + self.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0 + ) or self.autofocusController.use_focus_map: self.autofocusController.autofocus() self.autofocusController.wait_till_autofocus_has_completed() else: # initialize laser autofocus if it has not been done - if self.microscope.laserAutofocusController.is_initialized==False: + if self.microscope.laserAutofocusController.is_initialized == False: self._log.info("init reflection af") # initialize the reflection AF self.microscope.laserAutofocusController.initialize_auto() # do contrast AF for the first FOV (if contrast AF box is checked) - if self.do_autofocus and ((self.NZ == 1) or self.z_stacking_config == 'FROM CENTER'): + if self.do_autofocus and ((self.NZ == 1) or self.z_stacking_config == "FROM CENTER"): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in self.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in self.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) self.signal_current_configuration.emit(config_AF) self.autofocusController.autofocus() self.autofocusController.wait_till_autofocus_has_completed() @@ -1620,34 +1788,39 @@ def perform_autofocus(self, region_id, fov): try: # TODO(imo): We used to have a case here to try to fix backlash by double commanding a position. Now, just double command it whether or not we are using PID since we don't expose that now. But in the future, backlash handing shouldb e done at a lower level (and we can remove the double here) self.microscope.laserAutofocusController.move_to_target(0) - self.microscope.laserAutofocusController.move_to_target(0) # for stepper in open loop mode, repeat the operation to counter backlash. It's harmless if any other case. + self.microscope.laserAutofocusController.move_to_target( + 0 + ) # for stepper in open loop mode, repeat the operation to counter backlash. It's harmless if any other case. except: file_ID = f"{region_id}_focus_camera.bmp" saving_path = os.path.join(self.base_path, self.experiment_ID, str(self.time_point), file_ID) iio.imwrite(saving_path, self.microscope.laserAutofocusController.image) - self._log.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! laser AF failed !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + self._log.error( + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! laser AF failed !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + ) def prepare_z_stack(self): # move to bottom of the z stack - if self.z_stacking_config == 'FROM CENTER': + if self.z_stacking_config == "FROM CENTER": self.stage.move_z(-self.deltaZ * round((self.NZ - 1) / 2.0)) - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # TODO(imo): This is some sort of backlash compensation. We should move this down to the low level, and remove it from here. # maneuver for achiving uniform step size and repeatability when using open-loop control distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( - max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) self.stage.move_z(-distance_to_clear_backlash) self.stage.move_z(distance_to_clear_backlash) - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) def handle_z_offset(self, config, not_offset): if config.z_offset is not None: # perform z offset for config, assume z_offset is in um if config.z_offset != 0.0: direction = 1 if not_offset else -1 self._log.info("Moving Z offset" + str(config.z_offset * direction)) - self.stage.move_z(config.z_offset/1000*direction) + self.stage.move_z(config.z_offset / 1000 * direction) self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) def acquire_camera_image(self, config, file_ID, current_path, current_round_images, k): # update the current configuration @@ -1661,18 +1834,20 @@ def acquire_camera_image(self, config, file_ID, current_path, current_round_imag self.camera.send_trigger() image = self.camera.read_frame() elif self.liveController.trigger_mode == TriggerMode.HARDWARE: - if 'Fluorescence' in config.name and ENABLE_NL5 and NL5_USE_DOUT: - self.camera.image_is_ready = False # to remove + if "Fluorescence" in config.name and ENABLE_NL5 and NL5_USE_DOUT: + self.camera.image_is_ready = False # to remove self.microscope.nl5.start_acquisition() image = self.camera.read_frame(reset_image_ready_flag=False) else: - self.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=self.camera.exposure_time*1000) + self.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000 + ) image = self.camera.read_frame() - else: # continuous acquisition + else: # continuous acquisition image = self.camera.read_frame() if image is None: - self._log.warning('self.camera.read_frame() returned None') + self._log.warning("self.camera.read_frame() returned None") return # turn off the illumination if using software trigger @@ -1680,11 +1855,17 @@ def acquire_camera_image(self, config, file_ID, current_path, current_round_imag self.liveController.turn_off_illumination() # process the image - @@@ to move to camera - image = utils.crop_image(image,self.crop_width,self.crop_height) - image = utils.rotate_and_flip_image(image,rotate_image_angle=self.camera.rotate_image_angle,flip_image=self.camera.flip_image) - image_to_display = utils.crop_image(image,round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling)) + image = utils.crop_image(image, self.crop_width, self.crop_height) + image = utils.rotate_and_flip_image( + image, rotate_image_angle=self.camera.rotate_image_angle, flip_image=self.camera.flip_image + ) + image_to_display = utils.crop_image( + image, + round(self.crop_width * self.display_resolution_scaling), + round(self.crop_height * self.display_resolution_scaling), + ) self.image_to_display.emit(image_to_display) - self.image_to_display_multi.emit(image_to_display,config.illumination_source) + self.image_to_display_multi.emit(image_to_display, config.illumination_source) self.save_image(image, file_ID, config, current_path) self.update_napari(image, config.name, k) @@ -1698,7 +1879,7 @@ def acquire_camera_image(self, config, file_ID, current_path, current_round_imag def acquire_rgb_image(self, config, file_ID, current_path, current_round_images, k): # go through the channels - rgb_channels = ['BF LED matrix full_R', 'BF LED matrix full_G', 'BF LED matrix full_B'] + rgb_channels = ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"] images = {} for config_ in self.configurationManager.configurations: @@ -1715,12 +1896,14 @@ def acquire_rgb_image(self, config, file_ID, current_path, current_round_images, self.camera.send_trigger() elif self.liveController.trigger_mode == TriggerMode.HARDWARE: - self.microcontroller.send_hardware_trigger(control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000) + self.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000 + ) # read camera frame image = self.camera.read_frame() if image is None: - print('self.camera.read_frame() returned None') + print("self.camera.read_frame() returned None") continue # TODO(imo): use illum controller @@ -1730,22 +1913,24 @@ def acquire_rgb_image(self, config, file_ID, current_path, current_round_images, # process the image - @@@ to move to camera image = utils.crop_image(image, self.crop_width, self.crop_height) - image = utils.rotate_and_flip_image(image, rotate_image_angle=self.camera.rotate_image_angle, flip_image=self.camera.flip_image) + image = utils.rotate_and_flip_image( + image, rotate_image_angle=self.camera.rotate_image_angle, flip_image=self.camera.flip_image + ) # add the image to dictionary images[config_.name] = np.copy(image) # Check if the image is RGB or monochrome - i_size = images['BF LED matrix full_R'].shape - i_dtype = images['BF LED matrix full_R'].dtype + i_size = images["BF LED matrix full_R"].shape + i_dtype = images["BF LED matrix full_R"].dtype if len(i_size) == 3: # If already RGB, write and emit individual channels - print('writing R, G, B channels') + print("writing R, G, B channels") self.handle_rgb_channels(images, file_ID, current_path, config, k) else: # If monochrome, reconstruct RGB image - print('constructing RGB image') + print("constructing RGB image") self.construct_rgb_image(images, file_ID, current_path, config, k) def acquire_spectrometer_data(self, config, file_ID, current_path): @@ -1753,21 +1938,25 @@ def acquire_spectrometer_data(self, config, file_ID, current_path): for l in range(N_SPECTRUM_PER_POINT): data = self.usb_spectrometer.read_spectrum() self.spectrum_to_display.emit(data) - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '_' + str(l) + '.csv') - np.savetxt(saving_path,data,delimiter=',') + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + "_" + str(l) + ".csv" + ) + np.savetxt(saving_path, data, delimiter=",") def save_image(self, image, file_ID, config, current_path): if image.dtype == np.uint16: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.tiff') + saving_path = os.path.join(current_path, file_ID + "_" + str(config.name).replace(" ", "_") + ".tiff") else: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.' + Acquisition.IMAGE_FORMAT) + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + "." + Acquisition.IMAGE_FORMAT + ) if self.camera.is_color: - if 'BF LED matrix' in config.name: - if MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': - image = image[:,:,1] + if "BF LED matrix" in config.name: + if MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": + image = image[:, :, 1] if Acquisition.PSEUDO_COLOR: image = self.return_pseudo_colored_image(image, config) @@ -1775,7 +1964,7 @@ def save_image(self, image, file_ID, config, current_path): if Acquisition.MERGE_CHANNELS: self._save_merged_image(image, file_ID, current_path) - iio.imwrite(saving_path,image) + iio.imwrite(saving_path, image) def _save_merged_image(self, image, file_ID, current_path): self.image_count += 1 @@ -1786,32 +1975,30 @@ def _save_merged_image(self, image, file_ID, current_path): if self.image_count == len(self.selected_configurations): if image.dtype == np.uint16: - saving_path = os.path.join(current_path, file_ID + '_merged' + '.tiff') + saving_path = os.path.join(current_path, file_ID + "_merged" + ".tiff") else: - saving_path = os.path.join(current_path, file_ID + '_merged' + '.' + Acquisition.IMAGE_FORMAT) + saving_path = os.path.join(current_path, file_ID + "_merged" + "." + Acquisition.IMAGE_FORMAT) iio.imwrite(saving_path, self.merged_image) self.image_count = 0 return def return_pseudo_colored_image(self, image, config): - if '405 nm' in config.name: + if "405 nm" in config.name: image = self.grayscale_to_rgb(image, Acquisition.PSEUDO_COLOR_MAP["405"]["hex"]) - elif '488 nm' in config.name: + elif "488 nm" in config.name: image = self.grayscale_to_rgb(image, Acquisition.PSEUDO_COLOR_MAP["488"]["hex"]) - elif '561 nm' in config.name: + elif "561 nm" in config.name: image = self.grayscale_to_rgb(image, Acquisition.PSEUDO_COLOR_MAP["561"]["hex"]) - elif '638 nm' in config.name: + elif "638 nm" in config.name: image = self.grayscale_to_rgb(image, Acquisition.PSEUDO_COLOR_MAP["638"]["hex"]) - elif '730 nm' in config.name: + elif "730 nm" in config.name: image = self.grayscale_to_rgb(image, Acquisition.PSEUDO_COLOR_MAP["730"]["hex"]) return image def grayscale_to_rgb(self, image, hex_color): - rgb_ratios = np.array([(hex_color >> 16) & 0xFF, - (hex_color >> 8) & 0xFF, - hex_color & 0xFF]) / 255 + rgb_ratios = np.array([(hex_color >> 16) & 0xFF, (hex_color >> 8) & 0xFF, hex_color & 0xFF]) / 255 rgb = np.stack([image] * 3, axis=-1) * rgb_ratios return rgb.astype(image.dtype) @@ -1821,66 +2008,91 @@ def update_napari(self, image, config_name, k): if not self.init_napari_layers: print("init napari layers") self.init_napari_layers = True - self.napari_layers_init.emit(image.shape[0],image.shape[1], image.dtype) + self.napari_layers_init.emit(image.shape[0], image.shape[1], image.dtype) pos = self.stage.get_pos() self.napari_layers_update.emit(image, pos.x_mm, pos.y_mm, k, config_name) def handle_dpc_generation(self, current_round_images): - keys_to_check = ['BF LED matrix left half', 'BF LED matrix right half', 'BF LED matrix top half', 'BF LED matrix bottom half'] + keys_to_check = [ + "BF LED matrix left half", + "BF LED matrix right half", + "BF LED matrix top half", + "BF LED matrix bottom half", + ] if all(key in current_round_images for key in keys_to_check): # generate dpc # TODO(imo): What's the point of this? Is it just a placeholder? pass def handle_rgb_generation(self, current_round_images, file_ID, current_path, k): - keys_to_check = ['BF LED matrix full_R', 'BF LED matrix full_G', 'BF LED matrix full_B'] + keys_to_check = ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"] if all(key in current_round_images for key in keys_to_check): - print('constructing RGB image') - print(current_round_images['BF LED matrix full_R'].dtype) - size = current_round_images['BF LED matrix full_R'].shape - rgb_image = np.zeros((*size, 3),dtype=current_round_images['BF LED matrix full_R'].dtype) + print("constructing RGB image") + print(current_round_images["BF LED matrix full_R"].dtype) + size = current_round_images["BF LED matrix full_R"].shape + rgb_image = np.zeros((*size, 3), dtype=current_round_images["BF LED matrix full_R"].dtype) print(rgb_image.shape) - rgb_image[:, :, 0] = current_round_images['BF LED matrix full_R'] - rgb_image[:, :, 1] = current_round_images['BF LED matrix full_G'] - rgb_image[:, :, 2] = current_round_images['BF LED matrix full_B'] + rgb_image[:, :, 0] = current_round_images["BF LED matrix full_R"] + rgb_image[:, :, 1] = current_round_images["BF LED matrix full_G"] + rgb_image[:, :, 2] = current_round_images["BF LED matrix full_B"] # TODO(imo): There used to be a "display image" comment here, and then an unused cropped image. Do we need to emit an image here? # write the image if len(rgb_image.shape) == 3: - print('writing RGB image') + print("writing RGB image") if rgb_image.dtype == np.uint16: - iio.imwrite(os.path.join(current_path, file_ID + '_BF_LED_matrix_full_RGB.tiff'), rgb_image) + iio.imwrite(os.path.join(current_path, file_ID + "_BF_LED_matrix_full_RGB.tiff"), rgb_image) else: - iio.imwrite(os.path.join(current_path, file_ID + '_BF_LED_matrix_full_RGB.' + Acquisition.IMAGE_FORMAT),rgb_image) + iio.imwrite( + os.path.join(current_path, file_ID + "_BF_LED_matrix_full_RGB." + Acquisition.IMAGE_FORMAT), + rgb_image, + ) def handle_rgb_channels(self, images, file_ID, current_path, config, k): - for channel in ['BF LED matrix full_R', 'BF LED matrix full_G', 'BF LED matrix full_B']: - image_to_display = utils.crop_image(images[channel], round(self.crop_width * self.display_resolution_scaling), round(self.crop_height * self.display_resolution_scaling)) + for channel in ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"]: + image_to_display = utils.crop_image( + images[channel], + round(self.crop_width * self.display_resolution_scaling), + round(self.crop_height * self.display_resolution_scaling), + ) self.image_to_display.emit(image_to_display) self.image_to_display_multi.emit(image_to_display, config.illumination_source) self.update_napari(images[channel], channel, k) - file_name = file_ID + '_' + channel.replace(' ', '_') + ('.tiff' if images[channel].dtype == np.uint16 else '.' + Acquisition.IMAGE_FORMAT) + file_name = ( + file_ID + + "_" + + channel.replace(" ", "_") + + (".tiff" if images[channel].dtype == np.uint16 else "." + Acquisition.IMAGE_FORMAT) + ) iio.imwrite(os.path.join(current_path, file_name), images[channel]) def construct_rgb_image(self, images, file_ID, current_path, config, k): - rgb_image = np.zeros((*images['BF LED matrix full_R'].shape, 3), dtype=images['BF LED matrix full_R'].dtype) - rgb_image[:, :, 0] = images['BF LED matrix full_R'] - rgb_image[:, :, 1] = images['BF LED matrix full_G'] - rgb_image[:, :, 2] = images['BF LED matrix full_B'] + rgb_image = np.zeros((*images["BF LED matrix full_R"].shape, 3), dtype=images["BF LED matrix full_R"].dtype) + rgb_image[:, :, 0] = images["BF LED matrix full_R"] + rgb_image[:, :, 1] = images["BF LED matrix full_G"] + rgb_image[:, :, 2] = images["BF LED matrix full_B"] # send image to display - image_to_display = utils.crop_image(rgb_image, round(self.crop_width * self.display_resolution_scaling), round(self.crop_height * self.display_resolution_scaling)) + image_to_display = utils.crop_image( + rgb_image, + round(self.crop_width * self.display_resolution_scaling), + round(self.crop_height * self.display_resolution_scaling), + ) self.image_to_display.emit(image_to_display) self.image_to_display_multi.emit(image_to_display, config.illumination_source) self.update_napari(rgb_image, config.name, k) # write the RGB image - print('writing RGB image') - file_name = file_ID + '_BF_LED_matrix_full_RGB' + ('.tiff' if rgb_image.dtype == np.uint16 else '.' + Acquisition.IMAGE_FORMAT) + print("writing RGB image") + file_name = ( + file_ID + + "_BF_LED_matrix_full_RGB" + + (".tiff" if rgb_image.dtype == np.uint16 else "." + Acquisition.IMAGE_FORMAT) + ) iio.imwrite(os.path.join(current_path, file_name), rgb_image) def handle_acquisition_abort(self, current_path, region_id=0): @@ -1889,31 +2101,37 @@ def handle_acquisition_abort(self, current_path, region_id=0): self.move_to_coordinate(region_center) # Save coordinates.csv - self.coordinates_pd.to_csv(os.path.join(current_path, 'coordinates.csv'), index=False, header=True) + self.coordinates_pd.to_csv(os.path.join(current_path, "coordinates.csv"), index=False, header=True) self.microcontroller.enable_joystick(True) def move_z_for_stack(self): if self.use_piezo: - self.z_piezo_um += self.deltaZ*1000 + self.z_piezo_um += self.deltaZ * 1000 self.microcontroller.set_piezo_um(self.z_piezo_um) - if self.liveController.trigger_mode == TriggerMode.SOFTWARE: # for hardware trigger, delay is in waiting for the last row to start exposure - time.sleep(MULTIPOINT_PIEZO_DELAY_MS/1000) + if ( + self.liveController.trigger_mode == TriggerMode.SOFTWARE + ): # for hardware trigger, delay is in waiting for the last row to start exposure + time.sleep(MULTIPOINT_PIEZO_DELAY_MS / 1000) if MULTIPOINT_PIEZO_UPDATE_DISPLAY: self.signal_z_piezo_um.emit(self.z_piezo_um) else: self.stage.move_z(self.deltaZ) - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) def move_z_back_after_stack(self): if self.use_piezo: self.z_piezo_um = OBJECTIVE_PIEZO_HOME_UM self.microcontroller.set_piezo_um(self.z_piezo_um) - if self.liveController.trigger_mode == TriggerMode.SOFTWARE: # for hardware trigger, delay is in waiting for the last row to start exposure - time.sleep(MULTIPOINT_PIEZO_DELAY_MS/1000) + if ( + self.liveController.trigger_mode == TriggerMode.SOFTWARE + ): # for hardware trigger, delay is in waiting for the last row to start exposure + time.sleep(MULTIPOINT_PIEZO_DELAY_MS / 1000) if MULTIPOINT_PIEZO_UPDATE_DISPLAY: self.signal_z_piezo_um.emit(self.z_piezo_um) else: - distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units(max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) if self.z_stacking_config == "FROM CENTER": rel_z_to_start = -self.deltaZ * (self.NZ - 1) + self.deltaZ * round((self.NZ - 1) / 2) else: @@ -1928,20 +2146,31 @@ class MultiPointController(QObject): acquisitionFinished = Signal() image_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) spectrum_to_display = Signal(np.ndarray) signal_current_configuration = Signal(Configuration) - signal_register_current_fov = Signal(float,float) + signal_register_current_fov = Signal(float, float) detection_stats = Signal(object) signal_stitcher = Signal(str) napari_rtp_layers_update = Signal(np.ndarray, str) napari_layers_init = Signal(int, int, object) - napari_layers_update = Signal(np.ndarray, float, float, int, str) # image, x_mm, y_mm, k, channel + napari_layers_update = Signal(np.ndarray, float, float, int, str) # image, x_mm, y_mm, k, channel signal_z_piezo_um = Signal(float) signal_acquisition_progress = Signal(int, int, int) signal_region_progress = Signal(int, int) - def __init__(self, camera, stage: AbstractStage, microcontroller: Microcontroller, liveController, autofocusController, configurationManager, usb_spectrometer=None, scanCoordinates=None, parent=None): + def __init__( + self, + camera, + stage: AbstractStage, + microcontroller: Microcontroller, + liveController, + autofocusController, + configurationManager, + usb_spectrometer=None, + scanCoordinates=None, + parent=None, + ): QObject.__init__(self) self.camera = camera @@ -1959,7 +2188,7 @@ def __init__(self, camera, stage: AbstractStage, microcontroller: Microcontrolle self.deltaX = Acquisition.DX self.deltaY = Acquisition.DY # TODO(imo): Switch all to consistent mm units - self.deltaZ = Acquisition.DZ/1000 + self.deltaZ = Acquisition.DZ / 1000 self.deltat = 0 self.do_autofocus = False self.do_reflection_af = False @@ -1974,7 +2203,7 @@ def __init__(self, camera, stage: AbstractStage, microcontroller: Microcontrolle self.counter = 0 self.experiment_ID = None self.base_path = None - self.use_piezo = False # MULTIPOINT_USE_PIEZO_FOR_ZSTACKS + self.use_piezo = False # MULTIPOINT_USE_PIEZO_FOR_ZSTACKS self.selected_configurations = [] self.usb_spectrometer = usb_spectrometer self.scanCoordinates = scanCoordinates @@ -1985,7 +2214,7 @@ def __init__(self, camera, stage: AbstractStage, microcontroller: Microcontrolle self.start_time = 0 self.old_images_per_page = 1 z_mm_current = self.stage.get_pos().z_mm - self.z_range = [z_mm_current, z_mm_current + self.deltaZ * (self.NZ - 1)] # [start_mm, end_mm] + self.z_range = [z_mm_current, z_mm_current + self.deltaZ * (self.NZ - 1)] # [start_mm, end_mm] try: if self.parent is not None: @@ -1997,7 +2226,7 @@ def __init__(self, camera, stage: AbstractStage, microcontroller: Microcontrolle def set_use_piezo(self, checked): print("Use Piezo:", checked) self.use_piezo = checked - if hasattr(self, 'multiPointWorker'): + if hasattr(self, "multiPointWorker"): self.multiPointWorker.update_use_piezo(checked) def set_z_stacking_config(self, z_stacking_config_index): @@ -2008,34 +2237,34 @@ def set_z_stacking_config(self, z_stacking_config_index): def set_z_range(self, minZ, maxZ): self.z_range = [minZ, maxZ] - def set_NX(self,N): + def set_NX(self, N): self.NX = N - def set_NY(self,N): + def set_NY(self, N): self.NY = N - def set_NZ(self,N): + def set_NZ(self, N): self.NZ = N - def set_Nt(self,N): + def set_Nt(self, N): self.Nt = N - def set_deltaX(self,delta): + def set_deltaX(self, delta): self.deltaX = delta - def set_deltaY(self,delta): + def set_deltaY(self, delta): self.deltaY = delta - def set_deltaZ(self,delta_um): - self.deltaZ = delta_um/1000 + def set_deltaZ(self, delta_um): + self.deltaZ = delta_um / 1000 - def set_deltat(self,delta): + def set_deltat(self, delta): self.deltat = delta - def set_af_flag(self,flag): + def set_af_flag(self, flag): self.do_autofocus = flag - def set_reflection_af_flag(self,flag): + def set_reflection_af_flag(self, flag): self.do_reflection_af = flag def set_gen_focus_map_flag(self, flag): @@ -2053,69 +2282,80 @@ def set_fluorescence_rtp_flag(self, flag): self.do_fluorescence_rtp = flag def set_focus_map(self, focusMap): - self.focus_map = focusMap # None if dont use focusMap + self.focus_map = focusMap # None if dont use focusMap - def set_crop(self,crop_width, crop_height): + def set_crop(self, crop_width, crop_height): self.crop_width = crop_width self.crop_height = crop_height - def set_base_path(self,path): + def set_base_path(self, path): self.base_path = path - def start_new_experiment(self,experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment + def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment # generate unique experiment ID - self.experiment_ID = experiment_ID.replace(' ','_') + '_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + self.experiment_ID = experiment_ID.replace(" ", "_") + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") self.recording_start_time = time.time() # create a new folder - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) # TODO(imo): If the config has changed since boot, is this still the correct config? configManagerThrowaway = ConfigurationManager(self.configurationManager.config_filename) - configManagerThrowaway.write_configuration_selected(self.selected_configurations,os.path.join(self.base_path,self.experiment_ID)+"/configurations.xml") # save the configuration for the experiment + configManagerThrowaway.write_configuration_selected( + self.selected_configurations, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + ) # save the configuration for the experiment # Prepare acquisition parameters acquisition_parameters = { - 'dx(mm)': self.deltaX, 'Nx': self.NX, - 'dy(mm)': self.deltaY, 'Ny': self.NY, - 'dz(um)': self.deltaZ * 1000 if self.deltaZ != 0 else 1, 'Nz': self.NZ, - 'dt(s)': self.deltat, 'Nt': self.Nt, - 'with AF': self.do_autofocus, 'with reflection AF': self.do_reflection_af, + "dx(mm)": self.deltaX, + "Nx": self.NX, + "dy(mm)": self.deltaY, + "Ny": self.NY, + "dz(um)": self.deltaZ * 1000 if self.deltaZ != 0 else 1, + "Nz": self.NZ, + "dt(s)": self.deltat, + "Nt": self.Nt, + "with AF": self.do_autofocus, + "with reflection AF": self.do_reflection_af, } - try: # write objective data if it is available + try: # write objective data if it is available current_objective = self.parent.objectiveStore.current_objective objective_info = self.parent.objectiveStore.objectives_dict.get(current_objective, {}) - acquisition_parameters['objective'] = {} + acquisition_parameters["objective"] = {} for k in objective_info.keys(): - acquisition_parameters['objective'][k]=objective_info[k] - acquisition_parameters['objective']['name']=current_objective + acquisition_parameters["objective"][k] = objective_info[k] + acquisition_parameters["objective"]["name"] = current_objective except: try: objective_info = OBJECTIVES[DEFAULT_OBJECTIVE] - acquisition_parameters['objective'] = {} + acquisition_parameters["objective"] = {} for k in objective_info.keys(): - acquisition_parameters['objective'][k] = objective_info[k] - acquisition_parameters['objective']['name']=DEFAULT_OBJECTIVE + acquisition_parameters["objective"][k] = objective_info[k] + acquisition_parameters["objective"]["name"] = DEFAULT_OBJECTIVE except: pass # TODO: USE OBJECTIVE STORE DATA - acquisition_parameters['sensor_pixel_size_um'] = CAMERA_PIXEL_SIZE_UM[CAMERA_SENSOR] - acquisition_parameters['tube_lens_mm'] = TUBE_LENS_MM - f = open(os.path.join(self.base_path,self.experiment_ID)+"/acquisition parameters.json","w") + acquisition_parameters["sensor_pixel_size_um"] = CAMERA_PIXEL_SIZE_UM[CAMERA_SENSOR] + acquisition_parameters["tube_lens_mm"] = TUBE_LENS_MM + f = open(os.path.join(self.base_path, self.experiment_ID) + "/acquisition parameters.json", "w") f.write(json.dumps(acquisition_parameters)) f.close() def set_selected_configurations(self, selected_configurations_name): self.selected_configurations = [] for configuration_name in selected_configurations_name: - self.selected_configurations.append(next((config for config in self.configurationManager.configurations if config.name == configuration_name))) + self.selected_configurations.append( + next( + (config for config in self.configurationManager.configurations if config.name == configuration_name) + ) + ) def run_acquisition(self): - print('start multipoint') + print("start multipoint") self.scan_region_coords_mm = list(self.scanCoordinates.region_centers.values()) self.scan_region_names = list(self.scanCoordinates.region_centers.keys()) self.scan_region_fov_coords_mm = self.scanCoordinates.region_fov_coordinates print("num fovs:", sum(len(coords) for coords in self.scan_region_fov_coords_mm)) - print("num regions:",len(self.scan_region_coords_mm)) + print("num regions:", len(self.scan_region_coords_mm)) print("region ids:", self.scan_region_names) print("region centers:", self.scan_region_coords_mm) @@ -2125,7 +2365,7 @@ def run_acquisition(self): # stop live if self.liveController.is_live: self.liveController_was_live_before_multipoint = True - self.liveController.stop_live() # @@@ to do: also uncheck the live button + self.liveController.stop_live() # @@@ to do: also uncheck the live button else: self.liveController_was_live_before_multipoint = False @@ -2150,7 +2390,12 @@ def run_acquisition(self): elif self.parent is not None and not self.parent.live_only_mode: configs = [config.name for config in self.selected_configurations] print(configs) - if DO_FLUORESCENCE_RTP and 'BF LED matrix left half' in configs and 'BF LED matrix right half' in configs and 'Fluorescence 405 nm Ex' in configs: + if ( + DO_FLUORESCENCE_RTP + and "BF LED matrix left half" in configs + and "BF LED matrix right half" in configs + and "Fluorescence 405 nm Ex" in configs + ): self.parent.recordTabWidget.setCurrentWidget(self.parent.statsDisplayWidget) if USE_NAPARI_FOR_MULTIPOINT: self.parent.imageDisplayTabs.setCurrentWidget(self.parent.napariRTPWidget) @@ -2185,8 +2430,8 @@ def run_acquisition(self): bounds = self.scanCoordinates.get_scan_bounds() if not bounds: return - x_min, x_max = bounds['x'] - y_min, y_max = bounds['y'] + x_min, x_max = bounds["x"] + y_min, y_max = bounds["y"] # Calculate scan dimensions and center x_span = abs(x_max - x_min) @@ -2264,7 +2509,9 @@ def run_acquisition(self): self.multiPointWorker.image_to_display.connect(self.slot_image_to_display) self.multiPointWorker.image_to_display_multi.connect(self.slot_image_to_display_multi) self.multiPointWorker.spectrum_to_display.connect(self.slot_spectrum_to_display) - self.multiPointWorker.signal_current_configuration.connect(self.slot_current_configuration,type=Qt.BlockingQueuedConnection) + self.multiPointWorker.signal_current_configuration.connect( + self.slot_current_configuration, type=Qt.BlockingQueuedConnection + ) self.multiPointWorker.signal_register_current_fov.connect(self.slot_register_current_fov) self.multiPointWorker.napari_layers_init.connect(self.slot_napari_layers_init) self.multiPointWorker.napari_rtp_layers_update.connect(self.slot_napari_rtp_layers_update) @@ -2282,8 +2529,8 @@ def _on_acquisition_completed(self): # restore the previous selected mode if self.gen_focus_map: self.autofocusController.clear_focus_map() - for x,y,z in self.focus_map_storage: - self.autofocusController.focus_map_coords.append((x,y,z)) + for x, y, z in self.focus_map_storage: + self.autofocusController.focus_map_coords.append((x, y, z)) self.autofocusController.use_focus_map = self.already_using_fmap self.signal_current_configuration.emit(self.configuration_before_running_multipoint) @@ -2304,15 +2551,15 @@ def _on_acquisition_completed(self): if self.parent is not None: try: # self.parent.dataHandler.set_number_of_images_per_page(self.old_images_per_page) - self.parent.dataHandler.sort('Sort by prediction score') + self.parent.dataHandler.sort("Sort by prediction score") self.parent.dataHandler.signal_populate_page0.emit() except: pass print("total time for acquisition + processing + reset:", time.time() - self.recording_start_time) - utils.create_done_file(os.path.join(self.base_path,self.experiment_ID)) + utils.create_done_file(os.path.join(self.base_path, self.experiment_ID)) self.acquisitionFinished.emit() if not self.abort_acqusition_requested: - self.signal_stitcher.emit(os.path.join(self.base_path,self.experiment_ID)) + self.signal_stitcher.emit(os.path.join(self.base_path, self.experiment_ID)) QApplication.processEvents() def request_abort_aquisition(self): @@ -2321,20 +2568,20 @@ def request_abort_aquisition(self): def slot_detection_stats(self, stats): self.detection_stats.emit(stats) - def slot_image_to_display(self,image): + def slot_image_to_display(self, image): self.image_to_display.emit(image) - def slot_spectrum_to_display(self,data): + def slot_spectrum_to_display(self, data): self.spectrum_to_display.emit(data) - def slot_image_to_display_multi(self,image,illumination_source): - self.image_to_display_multi.emit(image,illumination_source) + def slot_image_to_display_multi(self, image, illumination_source): + self.image_to_display_multi.emit(image, illumination_source) - def slot_current_configuration(self,configuration): + def slot_current_configuration(self, configuration): self.signal_current_configuration.emit(configuration) - def slot_register_current_fov(self,x_mm,y_mm): - self.signal_register_current_fov.emit(x_mm,y_mm) + def slot_register_current_fov(self, x_mm, y_mm): + self.signal_register_current_fov.emit(x_mm, y_mm) def slot_napari_rtp_layers_update(self, image, channel): self.napari_rtp_layers_update.emit(image, channel) @@ -2359,10 +2606,19 @@ class TrackingController(QObject): signal_tracking_stopped = Signal() image_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) signal_current_configuration = Signal(Configuration) - def __init__(self, camera, microcontroller: Microcontroller, stage: AbstractStage, configurationManager, liveController: LiveController, autofocusController, imageDisplayWindow): + def __init__( + self, + camera, + microcontroller: Microcontroller, + stage: AbstractStage, + configurationManager, + liveController: LiveController, + autofocusController, + imageDisplayWindow, + ): QObject.__init__(self) self.camera = camera self.microcontroller = microcontroller @@ -2394,13 +2650,13 @@ def __init__(self, camera, microcontroller: Microcontroller, stage: AbstractStag def start_tracking(self): # save pre-tracking configuration - print('start tracking') + print("start tracking") self.configuration_before_running_tracking = self.liveController.currentConfiguration # stop live if self.liveController.is_live: self.was_live_before_tracking = True - self.liveController.stop_live() # @@@ to do: also uncheck the live button + self.liveController.stop_live() # @@@ to do: also uncheck the live button else: self.was_live_before_tracking = False @@ -2419,10 +2675,10 @@ def start_tracking(self): # create a QThread object try: if self.thread.isRunning(): - print('*** previous tracking thread is still running ***') + print("*** previous tracking thread is still running ***") self.thread.terminate() self.thread.wait() - print('*** previous tracking threaded manually stopped ***') + print("*** previous tracking threaded manually stopped ***") except: pass self.thread = QThread() @@ -2437,7 +2693,9 @@ def start_tracking(self): self.trackingWorker.finished.connect(self.thread.quit) self.trackingWorker.image_to_display.connect(self.slot_image_to_display) self.trackingWorker.image_to_display_multi.connect(self.slot_image_to_display_multi) - self.trackingWorker.signal_current_configuration.connect(self.slot_current_configuration,type=Qt.BlockingQueuedConnection) + self.trackingWorker.signal_current_configuration.connect( + self.slot_current_configuration, type=Qt.BlockingQueuedConnection + ) # self.thread.finished.connect(self.thread.deleteLater) self.thread.finished.connect(self.thread.quit) # start the thread @@ -2464,67 +2722,73 @@ def _on_tracking_stopped(self): self.signal_tracking_stopped.emit() QApplication.processEvents() - def start_new_experiment(self,experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment + def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment # generate unique experiment ID - self.experiment_ID = experiment_ID + '_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + self.experiment_ID = experiment_ID + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") self.recording_start_time = time.time() # create a new folder try: - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) - self.configurationManager.write_configuration(os.path.join(self.base_path,self.experiment_ID)+"/configurations.xml") # save the configuration for the experiment + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) + self.configurationManager.write_configuration( + os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + ) # save the configuration for the experiment except: - print('error in making a new folder') + print("error in making a new folder") pass def set_selected_configurations(self, selected_configurations_name): self.selected_configurations = [] for configuration_name in selected_configurations_name: - self.selected_configurations.append(next((config for config in self.configurationManager.configurations if config.name == configuration_name))) + self.selected_configurations.append( + next( + (config for config in self.configurationManager.configurations if config.name == configuration_name) + ) + ) - def toggle_stage_tracking(self,state): + def toggle_stage_tracking(self, state): self.flag_stage_tracking_enabled = state > 0 - print('set stage tracking enabled to ' + str(self.flag_stage_tracking_enabled)) + print("set stage tracking enabled to " + str(self.flag_stage_tracking_enabled)) - def toggel_enable_af(self,state): + def toggel_enable_af(self, state): self.flag_AF_enabled = state > 0 - print('set af enabled to ' + str(self.flag_AF_enabled)) + print("set af enabled to " + str(self.flag_AF_enabled)) - def toggel_save_images(self,state): + def toggel_save_images(self, state): self.flag_save_image = state > 0 - print('set save images to ' + str(self.flag_save_image)) + print("set save images to " + str(self.flag_save_image)) - def set_base_path(self,path): + def set_base_path(self, path): self.base_path = path def stop_tracking(self): self.flag_stop_tracking_requested = True - print('stop tracking requested') + print("stop tracking requested") - def slot_image_to_display(self,image): + def slot_image_to_display(self, image): self.image_to_display.emit(image) - def slot_image_to_display_multi(self,image,illumination_source): - self.image_to_display_multi.emit(image,illumination_source) + def slot_image_to_display_multi(self, image, illumination_source): + self.image_to_display_multi.emit(image, illumination_source) - def slot_current_configuration(self,configuration): + def slot_current_configuration(self, configuration): self.signal_current_configuration.emit(configuration) def update_pixel_size(self, pixel_size_um): self.pixel_size_um = pixel_size_um - def update_tracker_selection(self,tracker_str): + def update_tracker_selection(self, tracker_str): self.tracker.update_tracker_type(tracker_str) - def set_tracking_time_interval(self,time_interval): + def set_tracking_time_interval(self, time_interval): self.tracking_time_interval_s = time_interval - def update_image_resizing_factor(self,image_resizing_factor): + def update_image_resizing_factor(self, image_resizing_factor): self.image_resizing_factor = image_resizing_factor - print('update tracking image resizing factor to ' + str(self.image_resizing_factor)) - self.pixel_size_um_scaled = self.pixel_size_um/self.image_resizing_factor + print("update tracking image resizing factor to " + str(self.image_resizing_factor)) + self.pixel_size_um_scaled = self.pixel_size_um / self.image_resizing_factor # PID-based tracking - ''' + """ def on_new_frame(self,image,frame_ID,timestamp): # initialize the tracker when a new track is started if self.tracking_frame_counter == 0: @@ -2558,13 +2822,14 @@ def on_new_frame(self,image,frame_ID,timestamp): def start_a_new_track(self): self.tracking_frame_counter = 0 - ''' + """ + class TrackingWorker(QObject): finished = Signal() image_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) signal_current_configuration = Signal(Configuration) def __init__(self, trackingController: TrackingController): @@ -2589,7 +2854,9 @@ def __init__(self, trackingController: TrackingController): self.number_of_selected_configurations = len(self.selected_configurations) - self.image_saver = ImageSaver_Tracking(base_path=os.path.join(self.base_path,self.experiment_ID),image_format='bmp') + self.image_saver = ImageSaver_Tracking( + base_path=os.path.join(self.base_path, self.experiment_ID), image_format="bmp" + ) def run(self): @@ -2597,14 +2864,16 @@ def run(self): t0 = time.time() # save metadata - self.txt_file = open( os.path.join(self.base_path,self.experiment_ID,"metadata.txt"), "w+") - self.txt_file.write('t0: ' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + '\n') - self.txt_file.write('objective: ' + self.trackingController.objective + '\n') + self.txt_file = open(os.path.join(self.base_path, self.experiment_ID, "metadata.txt"), "w+") + self.txt_file.write("t0: " + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") + "\n") + self.txt_file.write("objective: " + self.trackingController.objective + "\n") self.txt_file.close() # create a file for logging - self.csv_file = open( os.path.join(self.base_path,self.experiment_ID,"track.csv"), "w+") - self.csv_file.write('dt (s), x_stage (mm), y_stage (mm), z_stage (mm), x_image (mm), y_image(mm), image_filename\n') + self.csv_file = open(os.path.join(self.base_path, self.experiment_ID, "track.csv"), "w+") + self.csv_file.write( + "dt (s), x_stage (mm), y_stage (mm), z_stage (mm), x_image (mm), y_image(mm), image_filename\n" + ) # reset tracker self.tracker.reset() @@ -2615,7 +2884,7 @@ def run(self): # tracking loop while not self.trackingController.flag_stop_tracking_requested: - print('tracking_frame_counter: ' + str(tracking_frame_counter) ) + print("tracking_frame_counter: " + str(tracking_frame_counter)) if tracking_frame_counter == 0: is_first_frame = True else: @@ -2631,10 +2900,10 @@ def run(self): # do autofocus if self.trackingController.flag_AF_enabled and tracking_frame_counter > 1: # do autofocus - print('>>> autofocus') + print(">>> autofocus") self.autofocusController.autofocus() self.autofocusController.wait_till_autofocus_has_completed() - print('>>> autofocus completed') + print(">>> autofocus completed") # get current position pos = self.stage.get_pos() @@ -2645,20 +2914,20 @@ def run(self): self.signal_current_configuration.emit(config) # TODO(imo): replace with illumination controller self.microcontroller.wait_till_operation_is_completed() - self.liveController.turn_on_illumination() # keep illumination on for single configuration acqusition + self.liveController.turn_on_illumination() # keep illumination on for single configuration acqusition self.microcontroller.wait_till_operation_is_completed() t = time.time() self.camera.send_trigger() image = self.camera.read_frame() if self.number_of_selected_configurations > 1: - self.liveController.turn_off_illumination() # keep illumination on for single configuration acqusition + self.liveController.turn_off_illumination() # keep illumination on for single configuration acqusition # image crop, rotation and flip - image = utils.crop_image(image,self.crop_width,self.crop_height) + image = utils.crop_image(image, self.crop_width, self.crop_height) image = np.squeeze(image) - image = utils.rotate_and_flip_image(image,rotate_image_angle=ROTATE_IMAGE_ANGLE,flip_image=FLIP_IMAGE) + image = utils.rotate_and_flip_image(image, rotate_image_angle=ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE) # get image size image_shape = image.shape - image_center = np.array([image_shape[1]*0.5,image_shape[0]*0.5]) + image_center = np.array([image_shape[1] * 0.5, image_shape[0] * 0.5]) # image the rest configurations for config_ in self.selected_configurations[1:]: @@ -2672,25 +2941,33 @@ def run(self): image_ = self.camera.read_frame() # TODO(imo): use illumination controller self.liveController.turn_off_illumination() - image_ = utils.crop_image(image_,self.crop_width,self.crop_height) + image_ = utils.crop_image(image_, self.crop_width, self.crop_height) image_ = np.squeeze(image_) - image_ = utils.rotate_and_flip_image(image_,rotate_image_angle=ROTATE_IMAGE_ANGLE,flip_image=FLIP_IMAGE) + image_ = utils.rotate_and_flip_image( + image_, rotate_image_angle=ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE + ) # display image - image_to_display_ = utils.crop_image(image_,round(self.crop_width*self.liveController.display_resolution_scaling), round(self.crop_height*self.liveController.display_resolution_scaling)) - self.image_to_display_multi.emit(image_to_display_,config_.illumination_source) + image_to_display_ = utils.crop_image( + image_, + round(self.crop_width * self.liveController.display_resolution_scaling), + round(self.crop_height * self.liveController.display_resolution_scaling), + ) + self.image_to_display_multi.emit(image_to_display_, config_.illumination_source) # save image if self.trackingController.flag_save_image: if self.camera.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - self.image_saver.enqueue(image_,tracking_frame_counter,str(config_.name)) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + self.image_saver.enqueue(image_, tracking_frame_counter, str(config_.name)) # track - object_found, centroid,rect_pts = self.tracker.track(image, None, is_first_frame = is_first_frame) + object_found, centroid, rect_pts = self.tracker.track(image, None, is_first_frame=is_first_frame) if not object_found: - print('tracker: object not found') + print("tracker: object not found") break in_plane_position_error_pixel = image_center - centroid - in_plane_position_error_mm = in_plane_position_error_pixel*self.trackingController.pixel_size_um_scaled/1000 + in_plane_position_error_mm = ( + in_plane_position_error_pixel * self.trackingController.pixel_size_um_scaled / 1000 + ) x_error_mm = in_plane_position_error_mm[0] y_error_mm = in_plane_position_error_mm[1] @@ -2706,15 +2983,30 @@ def run(self): # save image if self.trackingController.flag_save_image: - self.image_saver.enqueue(image,tracking_frame_counter,str(config.name)) + self.image_saver.enqueue(image, tracking_frame_counter, str(config.name)) # save position data - self.csv_file.write(str(t)+','+str(pos.x_mm)+','+str(pos.y_mm)+','+str(pos.z_mm)+','+str(x_error_mm)+','+str(y_error_mm)+','+str(tracking_frame_counter)+'\n') - if tracking_frame_counter%100 == 0: + self.csv_file.write( + str(t) + + "," + + str(pos.x_mm) + + "," + + str(pos.y_mm) + + "," + + str(pos.z_mm) + + "," + + str(x_error_mm) + + "," + + str(y_error_mm) + + "," + + str(tracking_frame_counter) + + "\n" + ) + if tracking_frame_counter % 100 == 0: self.csv_file.flush() # wait till tracking interval has elapsed - while(time.time() - timestamp_last_frame < self.trackingController.tracking_time_interval_s): + while time.time() - timestamp_last_frame < self.trackingController.tracking_time_interval_s: time.sleep(0.005) # increament counter @@ -2730,7 +3022,15 @@ class ImageDisplayWindow(QMainWindow): image_click_coordinates = Signal(int, int, int, int) - def __init__(self, liveController=None, contrastManager=None, window_title='', draw_crosshairs=False, show_LUT=False, autoLevels=False): + def __init__( + self, + liveController=None, + contrastManager=None, + window_title="", + draw_crosshairs=False, + show_LUT=False, + autoLevels=False, + ): super().__init__() self.liveController = liveController self.contrastManager = contrastManager @@ -2742,7 +3042,7 @@ def __init__(self, liveController=None, contrastManager=None, window_title='', d self.autoLevels = autoLevels # interpret image data as row-major instead of col-major - pg.setConfigOptions(imageAxisOrder='row-major') + pg.setConfigOptions(imageAxisOrder="row-major") self.graphics_widget = pg.GraphicsLayoutWidget() self.graphics_widget.view = self.graphics_widget.addViewBox() @@ -2755,23 +3055,23 @@ def __init__(self, liveController=None, contrastManager=None, window_title='', d if self.show_LUT: self.graphics_widget.view = pg.ImageView() self.graphics_widget.img = self.graphics_widget.view.getImageItem() - self.graphics_widget.img.setBorder('w') + self.graphics_widget.img.setBorder("w") self.graphics_widget.view.ui.roiBtn.hide() self.graphics_widget.view.ui.menuBtn.hide() self.LUTWidget = self.graphics_widget.view.getHistogramWidget() self.LUTWidget.region.sigRegionChanged.connect(self.update_contrast_limits) self.LUTWidget.region.sigRegionChangeFinished.connect(self.update_contrast_limits) else: - self.graphics_widget.img = pg.ImageItem(border='w') + self.graphics_widget.img = pg.ImageItem(border="w") self.graphics_widget.view.addItem(self.graphics_widget.img) ## Create ROI - self.roi_pos = (500,500) - self.roi_size = (500,500) + self.roi_pos = (500, 500) + self.roi_size = (500, 500) self.ROI = pg.ROI(self.roi_pos, self.roi_size, scaleSnap=True, translateSnap=True) self.ROI.setZValue(10) - self.ROI.addScaleHandle((0,0), (1,1)) - self.ROI.addScaleHandle((1,1), (0,0)) + self.ROI.addScaleHandle((0, 0), (1, 1)) + self.ROI.addScaleHandle((1, 1), (0, 0)) self.graphics_widget.view.addItem(self.ROI) self.ROI.hide() self.ROI.sigRegionChanged.connect(self.update_ROI) @@ -2798,9 +3098,9 @@ def __init__(self, liveController=None, contrastManager=None, window_title='', d # set window size desktopWidget = QDesktopWidget() - width = min(desktopWidget.height()*0.9,1000) + width = min(desktopWidget.height() * 0.9, 1000) height = width - self.setFixedSize(int(width),int(height)) + self.setFixedSize(int(width), int(height)) # Connect mouse click handler if self.show_LUT: @@ -2824,11 +3124,11 @@ def handle_mouse_click(self, evt): return if self.is_within_image(image_coord): - x_pixel_centered = int(image_coord.x() - self.graphics_widget.img.width()/2) - y_pixel_centered = int(image_coord.y() - self.graphics_widget.img.height()/2) - self.image_click_coordinates.emit(x_pixel_centered, y_pixel_centered, - self.graphics_widget.img.width(), - self.graphics_widget.img.height()) + x_pixel_centered = int(image_coord.x() - self.graphics_widget.img.width() / 2) + y_pixel_centered = int(image_coord.y() - self.graphics_widget.img.height() / 2) + self.image_click_coordinates.emit( + x_pixel_centered, y_pixel_centered, self.graphics_widget.img.width(), self.graphics_widget.img.height() + ) def is_within_image(self, coordinates): try: @@ -2844,7 +3144,7 @@ def display_image(self, image): image = np.copy(image) self.image_height, self.image_width = image.shape[:2] if self.draw_rectangle: - cv2.rectangle(image, self.ptRect1, self.ptRect2, (255,255,255), 4) + cv2.rectangle(image, self.ptRect1, self.ptRect2, (255, 255, 255), 4) self.draw_rectangle = False info = np.iinfo(image.dtype) if np.issubdtype(image.dtype, np.integer) else np.finfo(image.dtype) @@ -2852,7 +3152,9 @@ def display_image(self, image): if self.liveController is not None and self.contrastManager is not None: channel_name = self.liveController.currentConfiguration.name - if self.contrastManager.acquisition_dtype != None and self.contrastManager.acquisition_dtype != np.dtype(image.dtype): + if self.contrastManager.acquisition_dtype != None and self.contrastManager.acquisition_dtype != np.dtype( + image.dtype + ): self.contrastManager.scale_contrast_limits(np.dtype(image.dtype)) min_val, max_val = self.contrastManager.get_limits(channel_name, image.dtype) @@ -2883,12 +3185,12 @@ def hide_ROI_selector(self): self.ROI.hide() def get_roi(self): - return self.roi_pos,self.roi_size + return self.roi_pos, self.roi_size - def update_bounding_box(self,pts): - self.draw_rectangle=True - self.ptRect1=(pts[0][0],pts[0][1]) - self.ptRect2=(pts[1][0],pts[1][1]) + def update_bounding_box(self, pts): + self.draw_rectangle = True + self.ptRect1 = (pts[0][0], pts[0][1]) + self.ptRect2 = (pts[1][0], pts[1][1]) def get_roi_bounding_box(self): self.update_ROI() @@ -2898,16 +3200,16 @@ def get_roi_bounding_box(self): ymin = max(0, self.roi_pos[1]) return np.array([xmin, ymin, width, height]) - def set_autolevel(self,enabled): + def set_autolevel(self, enabled): self.autoLevels = enabled - print('set autolevel to ' + str(enabled)) + print("set autolevel to " + str(enabled)) class NavigationViewer(QFrame): signal_coordinates_clicked = Signal(float, float) # Will emit x_mm, y_mm when clicked - def __init__(self, objectivestore, sample = 'glass slide', invertX = False, *args, **kwargs): + def __init__(self, objectivestore, sample="glass slide", invertX=False, *args, **kwargs): super().__init__(*args, **kwargs) self.setFrameStyle(QFrame.Panel | QFrame.Raised) self.sample = sample @@ -2926,27 +3228,27 @@ def __init__(self, objectivestore, sample = 'glass slide', invertX = False, *arg self.x_mm = None self.y_mm = None self.image_paths = { - 'glass slide': 'images/slide carrier_828x662.png', - '4 glass slide': 'images/4 slide carrier_1509x1010.png', - '6 well plate': 'images/6 well plate_1509x1010.png', - '12 well plate': 'images/12 well plate_1509x1010.png', - '24 well plate': 'images/24 well plate_1509x1010.png', - '96 well plate': 'images/96 well plate_1509x1010.png', - '384 well plate': 'images/384 well plate_1509x1010.png', - '1536 well plate': 'images/1536 well plate_1509x1010.png' + "glass slide": "images/slide carrier_828x662.png", + "4 glass slide": "images/4 slide carrier_1509x1010.png", + "6 well plate": "images/6 well plate_1509x1010.png", + "12 well plate": "images/12 well plate_1509x1010.png", + "24 well plate": "images/24 well plate_1509x1010.png", + "96 well plate": "images/96 well plate_1509x1010.png", + "384 well plate": "images/384 well plate_1509x1010.png", + "1536 well plate": "images/1536 well plate_1509x1010.png", } print("navigation viewer:", sample) self.init_ui(invertX) - self.load_background_image(self.image_paths.get(sample, 'images/slide carrier_828x662.png')) + self.load_background_image(self.image_paths.get(sample, "images/slide carrier_828x662.png")) self.create_layers() self.update_display_properties(sample) # self.update_display() def init_ui(self, invertX): # interpret image data as row-major instead of col-major - pg.setConfigOptions(imageAxisOrder='row-major') + pg.setConfigOptions(imageAxisOrder="row-major") self.graphics_widget = pg.GraphicsLayoutWidget() self.graphics_widget.setBackground("w") @@ -2963,8 +3265,8 @@ def load_background_image(self, image_path): self.view.clear() self.background_image = cv2.imread(image_path) if self.background_image is None: - #raise ValueError(f"Failed to load image from {image_path}") - self.background_image = cv2.imread(self.image_paths.get('glass slide')) + # raise ValueError(f"Failed to load image from {image_path}") + self.background_image = cv2.imread(self.image_paths.get("glass slide")) if len(self.background_image.shape) == 2: # Grayscale image self.background_image = cv2.cvtColor(self.background_image, cv2.COLOR_GRAY2RGBA) @@ -2994,15 +3296,15 @@ def create_layers(self): self.background_item.setZValue(-1) # Background layer at the bottom self.scan_overlay_item.setZValue(0) # Scan overlay in the middle self.fov_overlay_item.setZValue(1) # FOV overlay next - self.focus_point_overlay_item.setZValue(2) # # Focus points on top + self.focus_point_overlay_item.setZValue(2) # # Focus points on top def update_display_properties(self, sample): - if sample == 'glass slide': + if sample == "glass slide": self.location_update_threshold_mm = 0.2 self.mm_per_pixel = 0.1453 self.origin_x_pixel = 200 self.origin_y_pixel = 120 - elif sample == '4 glass slide': + elif sample == "4 glass slide": self.location_update_threshold_mm = 0.2 self.mm_per_pixel = 0.084665 self.origin_x_pixel = 50 @@ -3010,8 +3312,8 @@ def update_display_properties(self, sample): else: self.location_update_threshold_mm = 0.05 self.mm_per_pixel = 0.084665 - self.origin_x_pixel = self.a1_x_pixel - (self.a1_x_mm)/self.mm_per_pixel - self.origin_y_pixel = self.a1_y_pixel - (self.a1_y_mm)/self.mm_per_pixel + self.origin_x_pixel = self.a1_x_pixel - (self.a1_x_mm) / self.mm_per_pixel + self.origin_y_pixel = self.a1_y_pixel - (self.a1_y_mm) / self.mm_per_pixel self.update_fov_size() def update_fov_size(self): @@ -3023,15 +3325,27 @@ def on_objective_changed(self): self.update_fov_size() self.draw_current_fov(self.x_mm, self.y_mm) - def update_wellplate_settings(self, sample_format, a1_x_mm, a1_y_mm, a1_x_pixel, a1_y_pixel, well_size_mm, well_spacing_mm, number_of_skip, rows, cols): + def update_wellplate_settings( + self, + sample_format, + a1_x_mm, + a1_y_mm, + a1_x_pixel, + a1_y_pixel, + well_size_mm, + well_spacing_mm, + number_of_skip, + rows, + cols, + ): if isinstance(sample_format, QVariant): sample_format = sample_format.value() - if sample_format == 'glass slide': + if sample_format == "glass slide": if IS_HCS: - sample = '4 glass slide' + sample = "4 glass slide" else: - sample = 'glass slide' + sample = "glass slide" else: sample = sample_format @@ -3050,13 +3364,13 @@ def update_wellplate_settings(self, sample_format, a1_x_mm, a1_y_mm, a1_x_pixel, image_path = self.image_paths.get(sample) if image_path is None or not os.path.exists(image_path): # Look for a custom wellplate image - custom_image_path = os.path.join('images', self.sample + '.png') + custom_image_path = os.path.join("images", self.sample + ".png") print(custom_image_path) if os.path.exists(custom_image_path): image_path = custom_image_path else: print(f"Warning: Image not found for {sample}. Using default image.") - image_path = self.image_paths.get('glass slide') # Use a default image + image_path = self.image_paths.get("glass slide") # Use a default image self.load_background_image(image_path) self.create_layers() @@ -3076,36 +3390,48 @@ def draw_fov_current_location(self, pos: squid.abc.Pos): self.y_mm = y_mm def get_FOV_pixel_coordinates(self, x_mm, y_mm): - if self.sample == 'glass slide': + if self.sample == "glass slide": current_FOV_top_left = ( - round(self.origin_x_pixel + x_mm/self.mm_per_pixel - self.fov_size_mm/2/self.mm_per_pixel), - round(self.image_height - (self.origin_y_pixel + y_mm/self.mm_per_pixel) - self.fov_size_mm/2/self.mm_per_pixel) + round(self.origin_x_pixel + x_mm / self.mm_per_pixel - self.fov_size_mm / 2 / self.mm_per_pixel), + round( + self.image_height + - (self.origin_y_pixel + y_mm / self.mm_per_pixel) + - self.fov_size_mm / 2 / self.mm_per_pixel + ), ) current_FOV_bottom_right = ( - round(self.origin_x_pixel + x_mm/self.mm_per_pixel + self.fov_size_mm/2/self.mm_per_pixel), - round(self.image_height - (self.origin_y_pixel + y_mm/self.mm_per_pixel) + self.fov_size_mm/2/self.mm_per_pixel) + round(self.origin_x_pixel + x_mm / self.mm_per_pixel + self.fov_size_mm / 2 / self.mm_per_pixel), + round( + self.image_height + - (self.origin_y_pixel + y_mm / self.mm_per_pixel) + + self.fov_size_mm / 2 / self.mm_per_pixel + ), ) else: current_FOV_top_left = ( - round(self.origin_x_pixel + x_mm/self.mm_per_pixel - self.fov_size_mm/2/self.mm_per_pixel), - round((self.origin_y_pixel + y_mm/self.mm_per_pixel) - self.fov_size_mm/2/self.mm_per_pixel) + round(self.origin_x_pixel + x_mm / self.mm_per_pixel - self.fov_size_mm / 2 / self.mm_per_pixel), + round((self.origin_y_pixel + y_mm / self.mm_per_pixel) - self.fov_size_mm / 2 / self.mm_per_pixel), ) current_FOV_bottom_right = ( - round(self.origin_x_pixel + x_mm/self.mm_per_pixel + self.fov_size_mm/2/self.mm_per_pixel), - round((self.origin_y_pixel + y_mm/self.mm_per_pixel) + self.fov_size_mm/2/self.mm_per_pixel) + round(self.origin_x_pixel + x_mm / self.mm_per_pixel + self.fov_size_mm / 2 / self.mm_per_pixel), + round((self.origin_y_pixel + y_mm / self.mm_per_pixel) + self.fov_size_mm / 2 / self.mm_per_pixel), ) return current_FOV_top_left, current_FOV_bottom_right def draw_current_fov(self, x_mm, y_mm): self.fov_overlay.fill(0) current_FOV_top_left, current_FOV_bottom_right = self.get_FOV_pixel_coordinates(x_mm, y_mm) - cv2.rectangle(self.fov_overlay, current_FOV_top_left, current_FOV_bottom_right, (255, 0, 0, 255), self.box_line_thickness) + cv2.rectangle( + self.fov_overlay, current_FOV_top_left, current_FOV_bottom_right, (255, 0, 0, 255), self.box_line_thickness + ) self.fov_overlay_item.setImage(self.fov_overlay) def register_fov(self, x_mm, y_mm): color = (0, 0, 255, 255) # Blue RGBA current_FOV_top_left, current_FOV_bottom_right = self.get_FOV_pixel_coordinates(x_mm, y_mm) - cv2.rectangle(self.background_image, current_FOV_top_left, current_FOV_bottom_right, color, self.box_line_thickness) + cv2.rectangle( + self.background_image, current_FOV_top_left, current_FOV_bottom_right, color, self.box_line_thickness + ) self.background_item.setImage(self.background_image) def register_fov_to_image(self, x_mm, y_mm): @@ -3116,7 +3442,9 @@ def register_fov_to_image(self, x_mm, y_mm): def deregister_fov_to_image(self, x_mm, y_mm): current_FOV_top_left, current_FOV_bottom_right = self.get_FOV_pixel_coordinates(x_mm, y_mm) - cv2.rectangle(self.scan_overlay, current_FOV_top_left, current_FOV_bottom_right, (0, 0, 0, 0), self.box_line_thickness) + cv2.rectangle( + self.scan_overlay, current_FOV_top_left, current_FOV_bottom_right, (0, 0, 0, 0), self.box_line_thickness + ) self.scan_overlay_item.setImage(self.scan_overlay) def register_focus_point(self, x_mm, y_mm): @@ -3167,7 +3495,7 @@ def handle_mouse_click(self, evt): class ImageArrayDisplayWindow(QMainWindow): - def __init__(self, window_title=''): + def __init__(self, window_title=""): super().__init__() self.setWindowTitle(window_title) self.setWindowFlags(self.windowFlags() | Qt.CustomizeWindowHint) @@ -3175,33 +3503,33 @@ def __init__(self, window_title=''): self.widget = QWidget() # interpret image data as row-major instead of col-major - pg.setConfigOptions(imageAxisOrder='row-major') + pg.setConfigOptions(imageAxisOrder="row-major") self.graphics_widget_1 = pg.GraphicsLayoutWidget() self.graphics_widget_1.view = self.graphics_widget_1.addViewBox() self.graphics_widget_1.view.setAspectLocked(True) - self.graphics_widget_1.img = pg.ImageItem(border='w') + self.graphics_widget_1.img = pg.ImageItem(border="w") self.graphics_widget_1.view.addItem(self.graphics_widget_1.img) self.graphics_widget_1.view.invertY() self.graphics_widget_2 = pg.GraphicsLayoutWidget() self.graphics_widget_2.view = self.graphics_widget_2.addViewBox() self.graphics_widget_2.view.setAspectLocked(True) - self.graphics_widget_2.img = pg.ImageItem(border='w') + self.graphics_widget_2.img = pg.ImageItem(border="w") self.graphics_widget_2.view.addItem(self.graphics_widget_2.img) self.graphics_widget_2.view.invertY() self.graphics_widget_3 = pg.GraphicsLayoutWidget() self.graphics_widget_3.view = self.graphics_widget_3.addViewBox() self.graphics_widget_3.view.setAspectLocked(True) - self.graphics_widget_3.img = pg.ImageItem(border='w') + self.graphics_widget_3.img = pg.ImageItem(border="w") self.graphics_widget_3.view.addItem(self.graphics_widget_3.img) self.graphics_widget_3.view.invertY() self.graphics_widget_4 = pg.GraphicsLayoutWidget() self.graphics_widget_4.view = self.graphics_widget_4.addViewBox() self.graphics_widget_4.view.setAspectLocked(True) - self.graphics_widget_4.img = pg.ImageItem(border='w') + self.graphics_widget_4.img = pg.ImageItem(border="w") self.graphics_widget_4.view.addItem(self.graphics_widget_4.img) self.graphics_widget_4.view.invertY() ## Layout @@ -3214,24 +3542,24 @@ def __init__(self, window_title=''): self.setCentralWidget(self.widget) # set window size - desktopWidget = QDesktopWidget(); - width = min(desktopWidget.height()*0.9,1000) #@@@TO MOVE@@@# + desktopWidget = QDesktopWidget() + width = min(desktopWidget.height() * 0.9, 1000) # @@@TO MOVE@@@# height = width - self.setFixedSize(int(width),int(height)) + self.setFixedSize(int(width), int(height)) - def display_image(self,image,illumination_source): + def display_image(self, image, illumination_source): if illumination_source < 11: - self.graphics_widget_1.img.setImage(image,autoLevels=False) + self.graphics_widget_1.img.setImage(image, autoLevels=False) elif illumination_source == 11: - self.graphics_widget_2.img.setImage(image,autoLevels=False) + self.graphics_widget_2.img.setImage(image, autoLevels=False) elif illumination_source == 12: - self.graphics_widget_3.img.setImage(image,autoLevels=False) + self.graphics_widget_3.img.setImage(image, autoLevels=False) elif illumination_source == 13: - self.graphics_widget_4.img.setImage(image,autoLevels=False) + self.graphics_widget_4.img.setImage(image, autoLevels=False) class ConfigurationManager(QObject): - def __init__(self,filename="channel_configurations.xml"): + def __init__(self, filename="channel_configurations.xml"): QObject.__init__(self) self.config_filename = filename self.configurations = [] @@ -3240,47 +3568,49 @@ def __init__(self,filename="channel_configurations.xml"): def save_configurations(self): self.write_configuration(self.config_filename) - def write_configuration(self,filename): + def write_configuration(self, filename): self.config_xml_tree.write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) def read_configurations(self): - if(os.path.isfile(self.config_filename)==False): + if os.path.isfile(self.config_filename) == False: utils_config.generate_default_configuration(self.config_filename) - print('genenrate default config files') + print("genenrate default config files") self.config_xml_tree = etree.parse(self.config_filename) self.config_xml_tree_root = self.config_xml_tree.getroot() self.num_configurations = 0 - for mode in self.config_xml_tree_root.iter('mode'): + for mode in self.config_xml_tree_root.iter("mode"): self.num_configurations += 1 self.configurations.append( Configuration( - mode_id = mode.get('ID'), - name = mode.get('Name'), - color = self.get_channel_color(mode.get('Name')), - exposure_time = float(mode.get('ExposureTime')), - analog_gain = float(mode.get('AnalogGain')), - illumination_source = int(mode.get('IlluminationSource')), - illumination_intensity = float(mode.get('IlluminationIntensity')), - camera_sn = mode.get('CameraSN'), - z_offset = float(mode.get('ZOffset')), - pixel_format = mode.get('PixelFormat'), - _pixel_format_options = mode.get('_PixelFormat_options'), - emission_filter_position = int(mode.get('EmissionFilterPosition', 1)) + mode_id=mode.get("ID"), + name=mode.get("Name"), + color=self.get_channel_color(mode.get("Name")), + exposure_time=float(mode.get("ExposureTime")), + analog_gain=float(mode.get("AnalogGain")), + illumination_source=int(mode.get("IlluminationSource")), + illumination_intensity=float(mode.get("IlluminationIntensity")), + camera_sn=mode.get("CameraSN"), + z_offset=float(mode.get("ZOffset")), + pixel_format=mode.get("PixelFormat"), + _pixel_format_options=mode.get("_PixelFormat_options"), + emission_filter_position=int(mode.get("EmissionFilterPosition", 1)), ) ) - def update_configuration(self,configuration_id,attribute_name,new_value): + def update_configuration(self, configuration_id, attribute_name, new_value): conf_list = self.config_xml_tree_root.xpath("//mode[contains(@ID," + "'" + str(configuration_id) + "')]") mode_to_update = conf_list[0] - mode_to_update.set(attribute_name,str(new_value)) + mode_to_update.set(attribute_name, str(new_value)) self.save_configurations() def update_configuration_without_writing(self, configuration_id, attribute_name, new_value): conf_list = self.config_xml_tree_root.xpath("//mode[contains(@ID," + "'" + str(configuration_id) + "')]") mode_to_update = conf_list[0] - mode_to_update.set(attribute_name,str(new_value)) + mode_to_update.set(attribute_name, str(new_value)) - def write_configuration_selected(self,selected_configurations,filename): # to be only used with a throwaway instance + def write_configuration_selected( + self, selected_configurations, filename + ): # to be only used with a throwaway instance for conf in self.configurations: self.update_configuration_without_writing(conf.id, "Selected", 0) for conf in selected_configurations: @@ -3290,17 +3620,17 @@ def write_configuration_selected(self,selected_configurations,filename): # to be self.update_configuration_without_writing(conf.id, "Selected", 0) def get_channel_color(self, channel): - channel_info = CHANNEL_COLORS_MAP.get(self.extract_wavelength(channel), {'hex': 0xFFFFFF, 'name': 'gray'}) - return channel_info['hex'] + channel_info = CHANNEL_COLORS_MAP.get(self.extract_wavelength(channel), {"hex": 0xFFFFFF, "name": "gray"}) + return channel_info["hex"] def extract_wavelength(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 "Fluorescence" in parts: + index = parts.index("Fluorescence") + 1 if index < len(parts): return parts[index].split()[0] # Assuming 'Fluorescence 488 nm Ex' and taking '488' - for color in ['R', 'G', 'B']: + for color in ["R", "G", "B"]: if color in parts or "full_" + color in parts: return color return None @@ -3341,8 +3671,12 @@ def get_scaled_limits(self, channel, target_dtype): source_info = np.iinfo(self.acquisition_dtype) target_info = np.iinfo(target_dtype) - scaled_min = (min_val - source_info.min) / (source_info.max - source_info.min) * (target_info.max - target_info.min) + target_info.min - scaled_max = (max_val - source_info.min) / (source_info.max - source_info.min) * (target_info.max - target_info.min) + target_info.min + scaled_min = (min_val - source_info.min) / (source_info.max - source_info.min) * ( + target_info.max - target_info.min + ) + target_info.min + scaled_max = (max_val - source_info.min) / (source_info.max - source_info.min) * ( + target_info.max - target_info.min + ) + target_info.min return scaled_min, scaled_max @@ -3385,7 +3719,9 @@ def __init__(self, objectiveStore, navigationViewer, stage: AbstractStage): def add_well_selector(self, well_selector): self.well_selector = well_selector - def update_wellplate_settings(self, format_, a1_x_mm, a1_y_mm, a1_x_pixel, a1_y_pixel, size_mm, spacing_mm, number_of_skip): + def update_wellplate_settings( + self, format_, a1_x_mm, a1_y_mm, a1_x_pixel, a1_y_pixel, size_mm, spacing_mm, number_of_skip + ): self.format = format_ self.a1_x_mm = a1_x_mm self.a1_y_mm = a1_y_mm @@ -3395,19 +3731,19 @@ def update_wellplate_settings(self, format_, a1_x_mm, a1_y_mm, a1_x_pixel, a1_y_ self.well_spacing_mm = spacing_mm self.number_of_skip = number_of_skip - def _index_to_row(self,index): + def _index_to_row(self, index): index += 1 row = "" while index > 0: index -= 1 - row = chr(index % 26 + ord('A')) + row + row = chr(index % 26 + ord("A")) + row index //= 26 return row def get_selected_wells(self): # get selected wells from the widget print("getting selected wells for acquisition") - if not self.well_selector or self.format == 'glass slide': + if not self.well_selector or self.format == "glass slide": return None selected_wells = np.array(self.well_selector.get_selected_cells()) @@ -3417,32 +3753,32 @@ def get_selected_wells(self): if len(selected_wells) == 0: return well_centers # populate the coordinates - rows = np.unique(selected_wells[:,0]) + rows = np.unique(selected_wells[:, 0]) _increasing = True for row in rows: - items = selected_wells[selected_wells[:,0]==row] - columns = items[:,1] + items = selected_wells[selected_wells[:, 0] == row] + columns = items[:, 1] columns = np.sort(columns) if _increasing == False: columns = np.flip(columns) for column in columns: x_mm = self.a1_x_mm + (column * self.well_spacing_mm) + self.wellplate_offset_x_mm y_mm = self.a1_y_mm + (row * self.well_spacing_mm) + self.wellplate_offset_y_mm - well_id = self._index_to_row(row) + str(column+1) - well_centers[well_id] = (x_mm,y_mm) + well_id = self._index_to_row(row) + str(column + 1) + well_centers[well_id] = (x_mm, y_mm) _increasing = not _increasing return well_centers def set_live_scan_coordinates(self, x_mm, y_mm, scan_size_mm, overlap_percent, shape): - if shape != 'Manual' and self.format == 'glass slide': + if shape != "Manual" and self.format == "glass slide": if self.region_centers: self.clear_regions() - self.add_region('current', x_mm, y_mm, scan_size_mm, overlap_percent, shape) + self.add_region("current", x_mm, y_mm, scan_size_mm, overlap_percent, shape) def set_well_coordinates(self, scan_size_mm, overlap_percent, shape): new_region_centers = self.get_selected_wells() - if self.format == 'glass slide': + if self.format == "glass slide": pos = self.stage.get_pos() self.set_live_scan_coordinates(pos.x_mm, pos.y_mm, scan_size_mm, overlap_percent, shape) @@ -3468,9 +3804,9 @@ def set_manual_coordinates(self, manual_shapes, overlap_percent): scan_coordinates = self.add_manual_region(shape_coords, overlap_percent) if scan_coordinates: if len(manual_shapes) <= 1: - region_name = f'manual' + region_name = f"manual" else: - region_name = f'manual{i}' + region_name = f"manual{i}" center = np.mean(shape_coords, axis=0) self.region_centers[region_name] = [center[0], center[1]] self.region_fov_coordinates[region_name] = scan_coordinates @@ -3481,19 +3817,21 @@ def set_manual_coordinates(self, manual_shapes, overlap_percent): else: print("No Manual ROI found") - def add_region(self, well_id, center_x, center_y, scan_size_mm, overlap_percent=10, shape='Square'): + def add_region(self, well_id, center_x, center_y, scan_size_mm, overlap_percent=10, shape="Square"): """add region based on user inputs""" pixel_size_um = self.objectiveStore.get_pixel_size() fov_size_mm = (pixel_size_um / 1000) * Acquisition.CROP_WIDTH step_size_mm = fov_size_mm * (1 - overlap_percent / 100) steps = math.floor(scan_size_mm / step_size_mm) - if shape == 'Circle': + if shape == "Circle": tile_diagonal = math.sqrt(2) * fov_size_mm if steps % 2 == 1: # for odd steps actual_scan_size_mm = (steps - 1) * step_size_mm + tile_diagonal else: # for even steps - actual_scan_size_mm = math.sqrt(((steps - 1) * step_size_mm + fov_size_mm)**2 + (step_size_mm + fov_size_mm)**2) + actual_scan_size_mm = math.sqrt( + ((steps - 1) * step_size_mm + fov_size_mm) ** 2 + (step_size_mm + fov_size_mm) ** 2 + ) if actual_scan_size_mm > scan_size_mm: actual_scan_size_mm -= step_size_mm @@ -3515,22 +3853,24 @@ def add_region(self, well_id, center_x, center_y, scan_size_mm, overlap_percent= y = center_y + (i - half_steps) * step_size_mm for j in range(steps): x = center_x + (j - half_steps) * step_size_mm - if shape == 'Square' or (shape == 'Circle' and self._is_in_circle(x, y, center_x, center_y, radius_squared, fov_size_mm_half)): + if shape == "Square" or ( + shape == "Circle" and self._is_in_circle(x, y, center_x, center_y, radius_squared, fov_size_mm_half) + ): if self.validate_coordinates(x, y): row.append((x, y)) self.navigationViewer.register_fov_to_image(x, y) - if self.fov_pattern == 'S-Pattern' and i % 2 == 1: + if self.fov_pattern == "S-Pattern" and i % 2 == 1: row.reverse() scan_coordinates.extend(row) - if not scan_coordinates and shape == 'Circle': + if not scan_coordinates and shape == "Circle": if self.validate_coordinates(center_x, center_y): scan_coordinates.append((center_x, center_y)) self.navigationViewer.register_fov_to_image(center_x, center_y) self.region_centers[well_id] = [float(center_x), float(center_y), float(self.stage.get_pos().z_mm)] - self.region_fov_coordinates[well_id] = scan_coordinates + self.region_fov_coordinates[well_id] = scan_coordinates self.signal_scan_coordinates_updated.emit() print(f"Added Region: {well_id}") @@ -3556,7 +3896,7 @@ def clear_regions(self): def add_flexible_region(self, region_id, center_x, center_y, center_z, Nx, Ny, overlap_percent=10): """Convert grid parameters NX, NY to FOV coordinates based on overlap""" fov_size_mm = (self.objectiveStore.get_pixel_size() / 1000) * Acquisition.CROP_WIDTH - step_size_mm = fov_size_mm * (1 - overlap_percent/100) + step_size_mm = fov_size_mm * (1 - overlap_percent / 100) # Calculate total grid size grid_width_mm = (Nx - 1) * step_size_mm @@ -3565,14 +3905,14 @@ def add_flexible_region(self, region_id, center_x, center_y, center_z, Nx, Ny, o scan_coordinates = [] for i in range(Ny): row = [] - y = center_y - grid_height_mm/2 + i * step_size_mm + y = center_y - grid_height_mm / 2 + i * step_size_mm for j in range(Nx): - x = center_x - grid_width_mm/2 + j * step_size_mm + x = center_x - grid_width_mm / 2 + j * step_size_mm if self.validate_coordinates(x, y): row.append((x, y)) self.navigationViewer.register_fov_to_image(x, y) - if self.fov_pattern == 'S-Pattern' and i % 2 == 1: # reverse even rows + if self.fov_pattern == "S-Pattern" and i % 2 == 1: # reverse even rows row.reverse() scan_coordinates.extend(row) @@ -3591,10 +3931,8 @@ def add_flexible_region_with_step_size(self, region_id, center_x, center_y, cent grid_height_mm = (Ny - 1) * dy # Pre-calculate step sizes and ranges - x_steps = [center_x - grid_width_mm/2 + j * dx - for j in range(Nx)] - y_steps = [center_y - grid_height_mm/2 + i * dy - for i in range(Ny)] + x_steps = [center_x - grid_width_mm / 2 + j * dx for j in range(Nx)] + y_steps = [center_y - grid_height_mm / 2 + i * dy for i in range(Ny)] scan_coordinates = [] for i, y in enumerate(y_steps): @@ -3665,7 +4003,7 @@ def add_manual_region(self, shape_coords, overlap_percent): sorted_points = valid_points[sorted_indices] # Apply S-Pattern if needed - if self.fov_pattern == 'S-Pattern': + if self.fov_pattern == "S-Pattern": unique_y = np.unique(sorted_points[:, 1]) for i in range(1, len(unique_y), 2): mask = sorted_points[:, 1] == unique_y[i] @@ -3698,9 +4036,9 @@ def _is_in_circle(self, x, y, center_x, center_y, radius_squared, fov_size_mm_ha (x - fov_size_mm_half, y - fov_size_mm_half), (x + fov_size_mm_half, y - fov_size_mm_half), (x - fov_size_mm_half, y + fov_size_mm_half), - (x + fov_size_mm_half, y + fov_size_mm_half) + (x + fov_size_mm_half, y + fov_size_mm_half), ] - return all((cx - center_x)**2 + (cy - center_y)**2 <= radius_squared for cx, cy in corners) + return all((cx - center_x) ** 2 + (cy - center_y) ** 2 <= radius_squared for cx, cy in corners) def has_regions(self): """Check if any regions exist""" @@ -3711,8 +4049,10 @@ def validate_region(self, region_id): return region_id in self.region_centers and region_id in self.region_fov_coordinates def validate_coordinates(self, x, y): - return (SOFTWARE_POS_LIMIT.X_NEGATIVE <= x <= SOFTWARE_POS_LIMIT.X_POSITIVE and - SOFTWARE_POS_LIMIT.Y_NEGATIVE <= y <= SOFTWARE_POS_LIMIT.Y_POSITIVE) + return ( + SOFTWARE_POS_LIMIT.X_NEGATIVE <= x <= SOFTWARE_POS_LIMIT.X_POSITIVE + and SOFTWARE_POS_LIMIT.Y_NEGATIVE <= y <= SOFTWARE_POS_LIMIT.Y_POSITIVE + ) def sort_coordinates(self): print(f"Acquisition pattern: {self.acquisition_pattern}") @@ -3722,7 +4062,7 @@ def sort_coordinates(self): def sort_key(item): key, coord = item - if 'manual' in key: + if "manual" in key: return (0, coord[1], coord[0]) # Manual coords: sort by y, then x else: row, col = key[0], int(key[1:]) @@ -3730,9 +4070,9 @@ def sort_key(item): sorted_items = sorted(self.region_centers.items(), key=sort_key) - if self.acquisition_pattern == 'S-Pattern': + if self.acquisition_pattern == "S-Pattern": # Group by row and reverse alternate rows - rows = itertools.groupby(sorted_items, key=lambda x: x[1][1] if 'manual' in x[0] else x[0][0]) + rows = itertools.groupby(sorted_items, key=lambda x: x[1][1] if "manual" in x[0] else x[0][0]) sorted_items = [] for i, (_, group) in enumerate(rows): row = list(group) @@ -3742,9 +4082,9 @@ def sort_key(item): # Update dictionaries efficiently self.region_centers = {k: v for k, v in sorted_items} - self.region_fov_coordinates = {k: self.region_fov_coordinates[k] - for k, _ in sorted_items - if k in self.region_fov_coordinates} + self.region_fov_coordinates = { + k: self.region_fov_coordinates[k] for k, _ in sorted_items if k in self.region_fov_coordinates + } def get_region_bounds(self, region_id): """Get region boundaries""" @@ -3752,10 +4092,10 @@ def get_region_bounds(self, region_id): return None fovs = np.array(self.region_fov_coordinates[region_id]) return { - 'min_x': np.min(fovs[:,0]), - 'max_x': np.max(fovs[:,0]), - 'min_y': np.min(fovs[:,1]), - 'max_y': np.max(fovs[:,1]) + "min_x": np.min(fovs[:, 0]), + "max_x": np.max(fovs[:, 0]), + "min_y": np.min(fovs[:, 1]), + "max_y": np.max(fovs[:, 1]), } def get_scan_bounds(self): @@ -3763,32 +4103,29 @@ def get_scan_bounds(self): if not self.has_regions(): return None - min_x = float('inf') - max_x = float('-inf') - min_y = float('inf') - max_y = float('-inf') + min_x = float("inf") + max_x = float("-inf") + min_y = float("inf") + max_y = float("-inf") # Find global bounds across all regions for region_id in self.region_fov_coordinates.keys(): bounds = self.get_region_bounds(region_id) if bounds: - min_x = min(min_x, bounds['min_x']) - max_x = max(max_x, bounds['max_x']) - min_y = min(min_y, bounds['min_y']) - max_y = max(max_y, bounds['max_y']) + min_x = min(min_x, bounds["min_x"]) + max_x = max(max_x, bounds["max_x"]) + min_y = min(min_y, bounds["min_y"]) + max_y = max(max_y, bounds["max_y"]) - if min_x == float('inf'): + if min_x == float("inf"): return None # Add margin around bounds (5% of larger dimension) width = max_x - min_x height = max_y - min_y - margin = max(width, height) * 0.00 # 0.05 + margin = max(width, height) * 0.00 # 0.05 - return { - 'x': (min_x - margin, max_x + margin), - 'y': (min_y - margin, max_y + margin) - } + return {"x": (min_x - margin, max_x + margin), "y": (min_y - margin, max_y + margin)} def update_fov_z_level(self, region_id, fov, new_z): """Update z-level for a specific FOV and its region center""" @@ -3814,13 +4151,15 @@ def update_fov_z_level(self, region_id, fov, new_z): from scipy.interpolate import SmoothBivariateSpline, RBFInterpolator + + class FocusMap: """Handles fitting and interpolation of slide surfaces through measured focus points""" def __init__(self, smoothing_factor=0.1): self.smoothing_factor = smoothing_factor self.surface_fit = None - self.method = 'spline' # can be 'spline' or 'rbf' + self.method = "spline" # can be 'spline' or 'rbf' self.is_fitted = False self.points = None @@ -3830,7 +4169,7 @@ def set_method(self, method): Args: method (str): Either 'spline' or 'rbf' (Radial Basis Function) """ - if method not in ['spline', 'rbf']: + if method not in ["spline", "rbf"]: raise ValueError("Method must be either 'spline' or 'rbf'") self.method = method self.is_fitted = False @@ -3848,21 +4187,18 @@ def fit(self, points): raise ValueError("Need at least 4 points to fit surface") self.points = np.array(points) - x = self.points[:,0] - y = self.points[:,1] - z = self.points[:,2] + x = self.points[:, 0] + y = self.points[:, 1] + z = self.points[:, 2] - if self.method == 'spline': + if self.method == "spline": try: self.surface_fit = SmoothBivariateSpline( - x, y, z, - kx=3, # cubic spline in x - ky=3, # cubic spline in y - s=self.smoothing_factor + x, y, z, kx=3, ky=3, s=self.smoothing_factor # cubic spline in x # cubic spline in y ) except Exception as e: print(f"Spline fitting failed: {str(e)}, falling back to RBF") - self.method = 'rbf' + self.method = "rbf" self._fit_rbf(x, y, z) else: self._fit_rbf(x, y, z) @@ -3874,11 +4210,7 @@ def fit(self, points): def _fit_rbf(self, x, y, z): """Fit using Radial Basis Function interpolation""" xy = np.column_stack((x, y)) - self.surface_fit = RBFInterpolator( - xy, z, - kernel='thin_plate_spline', - epsilon=self.smoothing_factor - ) + self.surface_fit = RBFInterpolator(xy, z, kernel="thin_plate_spline", epsilon=self.smoothing_factor) def interpolate(self, x, y): """Get interpolated Z value at given (x,y) coordinates @@ -3894,14 +4226,14 @@ def interpolate(self, x, y): raise RuntimeError("Must fit surface before interpolating") if np.isscalar(x) and np.isscalar(y): - if self.method == 'spline': + if self.method == "spline": return float(self.surface_fit.ev(x, y)) else: return float(self.surface_fit([[x, y]])) else: x = np.asarray(x) y = np.asarray(y) - if self.method == 'spline': + if self.method == "spline": return self.surface_fit.ev(x, y) else: xy = np.column_stack((x.ravel(), y.ravel())) @@ -3943,7 +4275,16 @@ class LaserAutofocusController(QObject): image_to_display = Signal(np.ndarray) signal_displacement_um = Signal(float) - def __init__(self, microcontroller: Microcontroller, camera, liveController, stage: AbstractStage, has_two_interfaces=True, use_glass_top=True, look_for_cache=True): + def __init__( + self, + microcontroller: Microcontroller, + camera, + liveController, + stage: AbstractStage, + has_two_interfaces=True, + use_glass_top=True, + look_for_cache=True, + ): QObject.__init__(self) self.microcontroller = microcontroller self.camera = camera @@ -3958,13 +4299,13 @@ def __init__(self, microcontroller: Microcontroller, camera, liveController, sta self.x_width = 3088 self.y_width = 2064 - self.has_two_interfaces = has_two_interfaces # e.g. air-glass and glass water, set to false when (1) using oil immersion (2) using 1 mm thick slide (3) using metal coated slide or Si wafer + self.has_two_interfaces = has_two_interfaces # e.g. air-glass and glass water, set to false when (1) using oil immersion (2) using 1 mm thick slide (3) using metal coated slide or Si wafer self.use_glass_top = use_glass_top - self.spot_spacing_pixels = None # spacing between the spots from the two interfaces (unit: pixel) + self.spot_spacing_pixels = None # spacing between the spots from the two interfaces (unit: pixel) self.look_for_cache = look_for_cache - self.image = None # for saving the focus camera image for debugging when centroid cannot be found + self.image = None # for saving the focus camera image for debugging when centroid cannot be found if look_for_cache: cache_path = "cache/laser_af_reference_plane.txt" @@ -3978,27 +4319,29 @@ def __init__(self, microcontroller: Microcontroller, camera, liveController, sta height = int(value_list[3]) pixel_to_um = float(value_list[4]) x_reference = float(value_list[5]) - self.initialize_manual(x_offset,y_offset,width,height,pixel_to_um,x_reference) + self.initialize_manual(x_offset, y_offset, width, height, pixel_to_um, x_reference) break - except (FileNotFoundError, ValueError,IndexError) as e: + except (FileNotFoundError, ValueError, IndexError) as e: print("Unable to read laser AF state cache, exception below:") print(e) pass def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, write_to_cache=True): - cache_string = ",".join([str(x_offset),str(y_offset), str(width),str(height), str(pixel_to_um), str(x_reference)]) + cache_string = ",".join( + [str(x_offset), str(y_offset), str(width), str(height), str(pixel_to_um), str(x_reference)] + ) if write_to_cache: cache_path = Path("cache/laser_af_reference_plane.txt") cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text(cache_string) # x_reference is relative to the full sensor self.pixel_to_um = pixel_to_um - self.x_offset = int((x_offset//8)*8) - self.y_offset = int((y_offset//2)*2) - self.width = int((width//8)*8) - self.height = int((height//2)*2) - self.x_reference = x_reference - self.x_offset # self.x_reference is relative to the cropped region - self.camera.set_ROI(self.x_offset,self.y_offset,self.width,self.height) + self.x_offset = int((x_offset // 8) * 8) + self.y_offset = int((y_offset // 2) * 2) + self.width = int((width // 8) * 8) + self.height = int((height // 2) * 2) + self.x_reference = x_reference - self.x_offset # self.x_reference is relative to the cropped region + self.camera.set_ROI(self.x_offset, self.y_offset, self.width, self.height) self.is_initialized = True def initialize_auto(self): @@ -4007,8 +4350,8 @@ def initialize_auto(self): # then calculate the convert factor # set camera to use full sensor - self.camera.set_ROI(0,0,None,None) # set offset first - self.camera.set_ROI(0,0,3088,2064) + self.camera.set_ROI(0, 0, None, None) # set offset first + self.camera.set_ROI(0, 0, 3088, 2064) # update camera settings self.camera.set_exposure_time(FOCUS_CAMERA_EXPOSURE_TIME_MS) self.camera.set_analog_gain(FOCUS_CAMERA_ANALOG_GAIN) @@ -4018,15 +4361,15 @@ def initialize_auto(self): self.microcontroller.wait_till_operation_is_completed() # get laser spot location - x,y = self._get_laser_spot_centroid() + x, y = self._get_laser_spot_centroid() # turn off the laser self.microcontroller.turn_off_AF_laser() self.microcontroller.wait_till_operation_is_completed() - x_offset = x - LASER_AF_CROP_WIDTH/2 - y_offset = y - LASER_AF_CROP_HEIGHT/2 - print('laser spot location on the full sensor is (' + str(int(x)) + ',' + str(int(y)) + ')') + x_offset = x - LASER_AF_CROP_WIDTH / 2 + y_offset = y - LASER_AF_CROP_HEIGHT / 2 + print("laser spot location on the full sensor is (" + str(int(x)) + "," + str(int(y)) + ")") # set camera crop self.initialize_manual(x_offset, y_offset, LASER_AF_CROP_WIDTH, LASER_AF_CROP_HEIGHT, 1, x) @@ -4041,26 +4384,26 @@ def initialize_auto(self): time.sleep(0.02) # measure - x0,y0 = self._get_laser_spot_centroid() + x0, y0 = self._get_laser_spot_centroid() # move z to 6 um self.stage.move_z(0.006) time.sleep(0.02) # measure - x1,y1 = self._get_laser_spot_centroid() + x1, y1 = self._get_laser_spot_centroid() # turn off laser self.microcontroller.turn_off_AF_laser() self.microcontroller.wait_till_operation_is_completed() - if x1-x0 == 0: + if x1 - x0 == 0: # for simulation self.pixel_to_um = 0.4 else: # calculate the conversion factor - self.pixel_to_um = 6.0/(x1-x0) - print('pixel to um conversion factor is ' + str(self.pixel_to_um) + ' um/pixel') + self.pixel_to_um = 6.0 / (x1 - x0) + print("pixel to um conversion factor is " + str(self.pixel_to_um) + " um/pixel") # set reference self.x_reference = x1 @@ -4082,13 +4425,15 @@ def initialize_auto(self): width = int(value_list[2]) height = int(value_list[3]) pixel_to_um = self.pixel_to_um - x_reference = self.x_reference+self.x_offset + x_reference = self.x_reference + self.x_offset break - cache_string = ",".join([str(x_offset),str(y_offset), str(width),str(height), str(pixel_to_um), str(x_reference)]) + cache_string = ",".join( + [str(x_offset), str(y_offset), str(width), str(height), str(pixel_to_um), str(x_reference)] + ) cache_path = Path("cache/laser_af_reference_plane.txt") cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text(cache_string) - except (FileNotFoundError, ValueError,IndexError) as e: + except (FileNotFoundError, ValueError, IndexError) as e: print("Unable to read laser AF state cache, exception below:") print(e) pass @@ -4098,21 +4443,23 @@ def measure_displacement(self): self.microcontroller.turn_on_AF_laser() self.microcontroller.wait_till_operation_is_completed() # get laser spot location - x,y = self._get_laser_spot_centroid() + x, y = self._get_laser_spot_centroid() # turn off the laser self.microcontroller.turn_off_AF_laser() self.microcontroller.wait_till_operation_is_completed() # calculate displacement - displacement_um = (x - self.x_reference)*self.pixel_to_um + displacement_um = (x - self.x_reference) * self.pixel_to_um self.signal_displacement_um.emit(displacement_um) return displacement_um - def move_to_target(self,target_um): + def move_to_target(self, target_um): current_displacement_um = self.measure_displacement() print("Laser AF displacement: ", current_displacement_um) if abs(current_displacement_um) > LASER_AF_RANGE: - print(f'Warning: Measured displacement ({current_displacement_um:.1f} μm) is unreasonably large, using previous z position') + print( + f"Warning: Measured displacement ({current_displacement_um:.1f} μm) is unreasonably large, using previous z position" + ) um_to_move = 0 else: um_to_move = target_um - current_displacement_um @@ -4127,39 +4474,41 @@ def set_reference(self): self.microcontroller.turn_on_AF_laser() self.microcontroller.wait_till_operation_is_completed() # get laser spot location - x,y = self._get_laser_spot_centroid() + x, y = self._get_laser_spot_centroid() # turn off the laser self.microcontroller.turn_off_AF_laser() self.microcontroller.wait_till_operation_is_completed() self.x_reference = x self.signal_displacement_um.emit(0) - def _caculate_centroid(self,image): + def _caculate_centroid(self, image): if self.has_two_interfaces == False: - h,w = image.shape - x,y = np.meshgrid(range(w),range(h)) + h, w = image.shape + x, y = np.meshgrid(range(w), range(h)) I = image.astype(float) I = I - np.amin(I) - I[I/np.amax(I)<0.2] = 0 - x = np.sum(x*I)/np.sum(I) - y = np.sum(y*I)/np.sum(I) - return x,y + I[I / np.amax(I) < 0.2] = 0 + x = np.sum(x * I) / np.sum(I) + y = np.sum(y * I) / np.sum(I) + return x, y else: I = image # get the y position of the spots - tmp = np.sum(I,axis=1) + tmp = np.sum(I, axis=1) y0 = np.argmax(tmp) # crop along the y axis - I = I[y0-96:y0+96,:] + I = I[y0 - 96 : y0 + 96, :] # signal along x - tmp = np.sum(I,axis=0) + tmp = np.sum(I, axis=0) # find peaks - peak_locations,_ = scipy.signal.find_peaks(tmp,distance=100) + peak_locations, _ = scipy.signal.find_peaks(tmp, distance=100) idx = np.argsort(tmp[peak_locations]) peak_0_location = peak_locations[idx[-1]] - peak_1_location = peak_locations[idx[-2]] # for air-glass-water, the smaller peak corresponds to the glass-water interface - self.spot_spacing_pixels = peak_1_location-peak_0_location - ''' + peak_1_location = peak_locations[ + idx[-2] + ] # for air-glass-water, the smaller peak corresponds to the glass-water interface + self.spot_spacing_pixels = peak_1_location - peak_0_location + """ # find peaks - alternative if self.spot_spacing_pixels is not None: peak_locations,_ = scipy.signal.find_peaks(tmp,distance=100) @@ -4170,24 +4519,24 @@ def _caculate_centroid(self,image): else: peak_0_location = np.argmax(tmp) peak_1_location = peak_0_location + self.spot_spacing_pixels - ''' + """ # choose which surface to use if self.use_glass_top: x1 = peak_1_location else: x1 = peak_0_location # find centroid - h,w = I.shape - x,y = np.meshgrid(range(w),range(h)) - I = I[:,max(0,x1-64):min(w-1,x1+64)] - x = x[:,max(0,x1-64):min(w-1,x1+64)] - y = y[:,max(0,x1-64):min(w-1,x1+64)] + h, w = I.shape + x, y = np.meshgrid(range(w), range(h)) + I = I[:, max(0, x1 - 64) : min(w - 1, x1 + 64)] + x = x[:, max(0, x1 - 64) : min(w - 1, x1 + 64)] + y = y[:, max(0, x1 - 64) : min(w - 1, x1 + 64)] I = I.astype(float) I = I - np.amin(I) - I[I/np.amax(I)<0.1] = 0 - x1 = np.sum(x*I)/np.sum(I) - y1 = np.sum(y*I)/np.sum(I) - return x1,y0-96+y1 + I[I / np.amax(I) < 0.1] = 0 + x1 = np.sum(x * I) / np.sum(I) + y1 = np.sum(y * I) / np.sum(I) + return x1, y0 - 96 + y1 def _get_laser_spot_centroid(self): # disable camera callback @@ -4200,7 +4549,7 @@ def _get_laser_spot_centroid(self): self.camera.send_trigger() elif self.liveController.trigger_mode == TriggerMode.HARDWARE: # self.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=self.camera.exposure_time*1000) - pass # to edit + pass # to edit # read camera frame image = self.camera.read_frame() self.image = image @@ -4208,17 +4557,17 @@ def _get_laser_spot_centroid(self): if LASER_AF_DISPLAY_SPOT_IMAGE: self.image_to_display.emit(image) # calculate centroid - x,y = self._caculate_centroid(image) + x, y = self._caculate_centroid(image) tmp_x = tmp_x + x tmp_y = tmp_y + y - x = tmp_x/LASER_AF_AVERAGING_N - y = tmp_y/LASER_AF_AVERAGING_N - return x,y + x = tmp_x / LASER_AF_AVERAGING_N + y = tmp_y / LASER_AF_AVERAGING_N + return x, y def get_image(self): # turn on the laser self.microcontroller.turn_on_AF_laser() - self.microcontroller.wait_till_operation_is_completed() # send trigger, grab image and display image + self.microcontroller.wait_till_operation_is_completed() # send trigger, grab image and display image self.camera.send_trigger() image = self.camera.read_frame() self.image_to_display.emit(image) diff --git a/software/control/core_PDAF.py b/software/control/core_PDAF.py index 86f75dfd3..2f6588095 100644 --- a/software/control/core_PDAF.py +++ b/software/control/core_PDAF.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -21,16 +22,17 @@ import cv2 from datetime import datetime -import skimage # pip3 install -U scikit-image +import skimage # pip3 install -U scikit-image import skimage.registration + class PDAFController(QObject): # input: stream from camera 1, stream from camera 2 # input: from internal_states shared variables # output: amount of defocus, which may be read by or emitted to focusTrackingController (that manages focus tracking on/off, PID coefficients) - def __init__(self,internal_states): + def __init__(self, internal_states): QObject.__init__(self) self.coefficient_shift2defocus = 1 self.registration_upsample_factor = 5 @@ -39,21 +41,21 @@ def __init__(self,internal_states): self.locked = False self.shared_variables = internal_states - def register_image_from_camera_1(self,image): - if(self.locked==True): + def register_image_from_camera_1(self, image): + if self.locked == True: return self.image1 = np.copy(image) self.image1_received = True - if(self.image2_received): + if self.image2_received: self.calculate_defocus() - def register_image_from_camera_2(self,image): - if(self.locked==True): + def register_image_from_camera_2(self, image): + if self.locked == True: return self.image2 = np.copy(image) - self.image2 = np.fliplr(self.image2) # can be flipud depending on camera orientation + self.image2 = np.fliplr(self.image2) # can be flipud depending on camera orientation self.image2_received = True - if(self.image1_received): + if self.image1_received: self.calculate_defocus() def calculate_defocus(self): @@ -61,36 +63,45 @@ def calculate_defocus(self): # cropping parameters self.x = self.shared_variables.x self.y = self.shared_variables.y - self.w = self.shared_variables.w*2 # double check which dimension to multiply + self.w = self.shared_variables.w * 2 # double check which dimension to multiply self.h = self.shared_variables.h # crop - self.image1 = self.image1[(self.y-int(self.h/2)):(self.y+int(self.h/2)),(self.x-int(self.w/2)):(self.x+int(self.w/2))] - self.image2 = self.image2[(self.y-int(self.h/2)):(self.y+int(self.h/2)),(self.x-int(self.w/2)):(self.x+int(self.w/2))] # additional offsets may need to be added + self.image1 = self.image1[ + (self.y - int(self.h / 2)) : (self.y + int(self.h / 2)), + (self.x - int(self.w / 2)) : (self.x + int(self.w / 2)), + ] + self.image2 = self.image2[ + (self.y - int(self.h / 2)) : (self.y + int(self.h / 2)), + (self.x - int(self.w / 2)) : (self.x + int(self.w / 2)), + ] # additional offsets may need to be added shift = self._compute_shift_from_image_pair() - self.defocus = shift*self.coefficient_shift2defocus + self.defocus = shift * self.coefficient_shift2defocus self.image1_received = False self.image2_received = False self.locked = False def _compute_shift_from_image_pair(self): # method 1: calculate 2D cross correlation -> find peak or centroid - ''' + """ I1 = np.array(self.image1,dtype=np.int) I2 = np.array(self.image2,dtype=np.int) I1 = I1 - np.mean(I1) I2 = I2 - np.mean(I2) xcorr = cv2.filter2D(I1,cv2.CV_32F,I2) cv2.imshow('xcorr',np.array(255*xcorr/np.max(xcorr),dtype=np.uint8)) - cv2.waitKey(15) - ''' + cv2.waitKey(15) + """ # method 2: use skimage.registration.phase_cross_correlation - shifts,error,phasediff = skimage.registration.phase_cross_correlation(self.image1,self.image2,upsample_factor=self.registration_upsample_factor,space='real') - print(shifts) # for debugging - return shifts[0] # can be shifts[1] - depending on camera orientation + shifts, error, phasediff = skimage.registration.phase_cross_correlation( + self.image1, self.image2, upsample_factor=self.registration_upsample_factor, space="real" + ) + print(shifts) # for debugging + return shifts[0] # can be shifts[1] - depending on camera orientation def close(self): pass + class TwoCamerasPDAFCalibrationController(QObject): acquisitionFinished = Signal() @@ -100,7 +111,9 @@ class TwoCamerasPDAFCalibrationController(QObject): z_pos = Signal(float) - def __init__(self,camera1,camera2,navigationController,liveController1,liveController2,configurationManager=None): + def __init__( + self, camera1, camera2, navigationController, liveController1, liveController2, configurationManager=None + ): QObject.__init__(self) self.camera1 = camera1 @@ -111,8 +124,8 @@ def __init__(self,camera1,camera2,navigationController,liveController1,liveContr self.configurationManager = configurationManager self.NZ = 1 self.Nt = 1 - self.deltaZ = Acquisition.DZ/1000 - self.deltaZ_usteps = round((Acquisition.DZ/1000)*Motion.STEPS_PER_MM_Z) + self.deltaZ = Acquisition.DZ / 1000 + self.deltaZ_usteps = round((Acquisition.DZ / 1000) * Motion.STEPS_PER_MM_Z) self.crop_width = Acquisition.CROP_WIDTH self.crop_height = Acquisition.CROP_HEIGHT self.display_resolution_scaling = Acquisition.IMAGE_DISPLAY_SCALING_FACTOR @@ -120,63 +133,79 @@ def __init__(self,camera1,camera2,navigationController,liveController1,liveContr self.experiment_ID = None self.base_path = None - def set_NX(self,N): + def set_NX(self, N): self.NX = N - def set_NY(self,N): + + def set_NY(self, N): self.NY = N - def set_NZ(self,N): + + def set_NZ(self, N): self.NZ = N - def set_Nt(self,N): + + def set_Nt(self, N): self.Nt = N - def set_deltaX(self,delta): + + def set_deltaX(self, delta): self.deltaX = delta - self.deltaX_usteps = round(delta*Motion.STEPS_PER_MM_XY) - def set_deltaY(self,delta): + self.deltaX_usteps = round(delta * Motion.STEPS_PER_MM_XY) + + def set_deltaY(self, delta): self.deltaY = delta - self.deltaY_usteps = round(delta*Motion.STEPS_PER_MM_XY) - def set_deltaZ(self,delta_um): - self.deltaZ = delta_um/1000 - self.deltaZ_usteps = round((delta_um/1000)*Motion.STEPS_PER_MM_Z) - def set_deltat(self,delta): + self.deltaY_usteps = round(delta * Motion.STEPS_PER_MM_XY) + + def set_deltaZ(self, delta_um): + self.deltaZ = delta_um / 1000 + self.deltaZ_usteps = round((delta_um / 1000) * Motion.STEPS_PER_MM_Z) + + def set_deltat(self, delta): self.deltat = delta - def set_af_flag(self,flag): + + def set_af_flag(self, flag): self.do_autofocus = flag - def set_crop(self,crop_width,height): + def set_crop(self, crop_width, height): self.crop_width = crop_width self.crop_height = crop_height - def set_base_path(self,path): + + def set_base_path(self, path): self.base_path = path - def start_new_experiment(self,experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment + + def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment # generate unique experiment ID - self.experiment_ID = experiment_ID + '_' + datetime.now().strftime('%Y-%m-%d %H-%M-%S.%f') + self.experiment_ID = experiment_ID + "_" + datetime.now().strftime("%Y-%m-%d %H-%M-%S.%f") self.recording_start_time = time.time() # create a new folder try: - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) if self.configurationManager: - self.configurationManager.write_configuration(os.path.join(self.base_path,self.experiment_ID)+"/configurations.xml") # save the configuration for the experiment + self.configurationManager.write_configuration( + os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + ) # save the configuration for the experiment except: pass def set_selected_configurations(self, selected_configurations_name): self.selected_configurations = [] for configuration_name in selected_configurations_name: - self.selected_configurations.append(next((config for config in self.configurationManager.configurations if config.name == configuration_name))) - - def run_acquisition(self): # @@@ to do: change name to run_experiment - print('start multipoint') - + self.selected_configurations.append( + next( + (config for config in self.configurationManager.configurations if config.name == configuration_name) + ) + ) + + def run_acquisition(self): # @@@ to do: change name to run_experiment + print("start multipoint") + # stop live if self.liveController1.is_live: self.liveController1.was_live_before_multipoint = True - self.liveController1.stop_live() # @@@ to do: also uncheck the live button + self.liveController1.stop_live() # @@@ to do: also uncheck the live button else: self.liveController1.was_live_before_multipoint = False # stop live if self.liveController2.is_live: self.liveController2.was_live_before_multipoint = True - self.liveController2.stop_live() # @@@ to do: also uncheck the live button + self.liveController2.stop_live() # @@@ to do: also uncheck the live button else: self.liveController2.was_live_before_multipoint = False @@ -185,7 +214,7 @@ def run_acquisition(self): # @@@ to do: change name to run_experiment self.camera1.callback_was_enabled_before_multipoint = True self.camera1.stop_streaming() self.camera1.disable_callback() - self.camera1.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class + self.camera1.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class else: self.camera1.callback_was_enabled_before_multipoint = False # disable callback @@ -193,7 +222,7 @@ def run_acquisition(self): # @@@ to do: change name to run_experiment self.camera2.callback_was_enabled_before_multipoint = True self.camera2.stop_streaming() self.camera2.disable_callback() - self.camera2.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class + self.camera2.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class else: self.camera2.callback_was_enabled_before_multipoint = False @@ -224,9 +253,9 @@ def run_acquisition(self): # @@@ to do: change name to run_experiment def _run_multipoint_single(self): # for each time point, create a new folder - current_path = os.path.join(self.base_path,self.experiment_ID,str(self.time_point)) + current_path = os.path.join(self.base_path, self.experiment_ID, str(self.time_point)) os.mkdir(current_path) - + # z-stack for k in range(self.NZ): file_ID = str(k) @@ -234,50 +263,70 @@ def _run_multipoint_single(self): # iterate through selected modes for config in self.selected_configurations: self.signal_current_configuration.emit(config) - self.camera1.send_trigger() + self.camera1.send_trigger() image = self.camera1.read_frame() - image = utils.crop_image(image,self.crop_width,self.crop_height) - saving_path = os.path.join(current_path, 'camera1_' + file_ID + str(config.name) + '.' + Acquisition.IMAGE_FORMAT) - image_to_display = utils.crop_image(image,round(self.crop_width*self.liveController1.display_resolution_scaling), round(self.crop_height*self.liveController1.display_resolution_scaling)) + image = utils.crop_image(image, self.crop_width, self.crop_height) + saving_path = os.path.join( + current_path, "camera1_" + file_ID + str(config.name) + "." + Acquisition.IMAGE_FORMAT + ) + image_to_display = utils.crop_image( + image, + round(self.crop_width * self.liveController1.display_resolution_scaling), + round(self.crop_height * self.liveController1.display_resolution_scaling), + ) self.image_to_display_camera1.emit(image_to_display) if self.camera1.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) - self.camera2.send_trigger() + self.camera2.send_trigger() image = self.camera2.read_frame() - image = utils.crop_image(image,self.crop_width,self.crop_height) - saving_path = os.path.join(current_path, 'camera2_' + file_ID + str(config.name) + '.' + Acquisition.IMAGE_FORMAT) - image_to_display = utils.crop_image(image,round(self.crop_width*self.liveController2.display_resolution_scaling), round(self.crop_height*self.liveController2.display_resolution_scaling)) + image = utils.crop_image(image, self.crop_width, self.crop_height) + saving_path = os.path.join( + current_path, "camera2_" + file_ID + str(config.name) + "." + Acquisition.IMAGE_FORMAT + ) + image_to_display = utils.crop_image( + image, + round(self.crop_width * self.liveController2.display_resolution_scaling), + round(self.crop_height * self.liveController2.display_resolution_scaling), + ) self.image_to_display_camera2.emit(image_to_display) if self.camera2.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) QApplication.processEvents() else: - self.camera1.send_trigger() + self.camera1.send_trigger() image = self.camera1.read_frame() - image = utils.crop_image(image,self.crop_width,self.crop_height) - saving_path = os.path.join(current_path, 'camera1_' + file_ID + '.' + Acquisition.IMAGE_FORMAT) - image_to_display = utils.crop_image(image,round(self.crop_width*self.liveController1.display_resolution_scaling), round(self.crop_height*self.liveController1.display_resolution_scaling)) + image = utils.crop_image(image, self.crop_width, self.crop_height) + saving_path = os.path.join(current_path, "camera1_" + file_ID + "." + Acquisition.IMAGE_FORMAT) + image_to_display = utils.crop_image( + image, + round(self.crop_width * self.liveController1.display_resolution_scaling), + round(self.crop_height * self.liveController1.display_resolution_scaling), + ) self.image_to_display_camera1.emit(image_to_display) if self.camera1.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) - self.camera2.send_trigger() + self.camera2.send_trigger() image = self.camera2.read_frame() - image = utils.crop_image(image,self.crop_width,self.crop_height) - saving_path = os.path.join(current_path, 'camera2_' + file_ID + '.' + Acquisition.IMAGE_FORMAT) - image_to_display = utils.crop_image(image,round(self.crop_width*self.liveController2.display_resolution_scaling), round(self.crop_height*self.liveController2.display_resolution_scaling)) + image = utils.crop_image(image, self.crop_width, self.crop_height) + saving_path = os.path.join(current_path, "camera2_" + file_ID + "." + Acquisition.IMAGE_FORMAT) + image_to_display = utils.crop_image( + image, + round(self.crop_width * self.liveController2.display_resolution_scaling), + round(self.crop_height * self.liveController2.display_resolution_scaling), + ) self.image_to_display_camera2.emit(image_to_display) if self.camera2.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) QApplication.processEvents() # move z if k < self.NZ - 1: self.navigationController.move_z_usteps(self.deltaZ_usteps) - + # move z back - self.navigationController.move_z_usteps(-self.deltaZ_usteps*(self.NZ-1)) + self.navigationController.move_z_usteps(-self.deltaZ_usteps * (self.NZ - 1)) diff --git a/software/control/core_displacement_measurement.py b/software/control/core_displacement_measurement.py index b04a38ccd..c86794d3b 100644 --- a/software/control/core_displacement_measurement.py +++ b/software/control/core_displacement_measurement.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -15,12 +16,13 @@ import numpy as np import cv2 + class DisplacementMeasurementController(QObject): signal_readings = Signal(list) - signal_plots = Signal(np.ndarray,np.ndarray) + signal_plots = Signal(np.ndarray, np.ndarray) - def __init__(self, x_offset = 0, y_offset = 0, x_scaling = 1, y_scaling = 1, N_average=1, N=10000): + def __init__(self, x_offset=0, y_offset=0, x_scaling=1, y_scaling=1, N_average=1, N=10000): QObject.__init__(self) self.x_offset = x_offset @@ -28,42 +30,42 @@ def __init__(self, x_offset = 0, y_offset = 0, x_scaling = 1, y_scaling = 1, N_a self.x_scaling = x_scaling self.y_scaling = y_scaling self.N_average = N_average - self.N = N # length of array to emit + self.N = N # length of array to emit self.t_array = np.array([]) self.x_array = np.array([]) self.y_array = np.array([]) - def update_measurement(self,image): + def update_measurement(self, image): t = time.time() - if len(image.shape)==3: - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) + if len(image.shape) == 3: + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) - h,w = image.shape - x,y = np.meshgrid(range(w),range(h)) + h, w = image.shape + x, y = np.meshgrid(range(w), range(h)) I = image.astype(float) I = I - np.amin(I) - I[I/np.amax(I)<0.2] = 0 - x = np.sum(x*I)/np.sum(I) - y = np.sum(y*I)/np.sum(I) - + I[I / np.amax(I) < 0.2] = 0 + x = np.sum(x * I) / np.sum(I) + y = np.sum(y * I) / np.sum(I) + x = x - self.x_offset y = y - self.y_offset - x = x*self.x_scaling - y = y*self.y_scaling + x = x * self.x_scaling + y = y * self.y_scaling - self.t_array = np.append(self.t_array,t) - self.x_array = np.append(self.x_array,x) - self.y_array = np.append(self.y_array,y) + self.t_array = np.append(self.t_array, t) + self.x_array = np.append(self.x_array, x) + self.y_array = np.append(self.y_array, y) - self.signal_plots.emit(self.t_array[-self.N:], np.vstack((self.x_array[-self.N:],self.y_array[-self.N:]))) - self.signal_readings.emit([np.mean(self.x_array[-self.N_average:]),np.mean(self.y_array[-self.N_average:])]) + self.signal_plots.emit(self.t_array[-self.N :], np.vstack((self.x_array[-self.N :], self.y_array[-self.N :]))) + self.signal_readings.emit([np.mean(self.x_array[-self.N_average :]), np.mean(self.y_array[-self.N_average :])]) - def update_settings(self,x_offset,y_offset,x_scaling,y_scaling,N_average,N): + def update_settings(self, x_offset, y_offset, x_scaling, y_scaling, N_average, N): self.N = N self.N_average = N_average self.x_offset = x_offset self.y_offset = y_offset self.x_scaling = x_scaling - self.y_scaling = y_scaling \ No newline at end of file + self.y_scaling = y_scaling diff --git a/software/control/core_platereader.py b/software/control/core_platereader.py index 7185b5ab6..1b0bc2cb9 100644 --- a/software/control/core_platereader.py +++ b/software/control/core_platereader.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -27,14 +28,15 @@ import math + class PlateReadingWorker(QObject): finished = Signal() image_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) signal_current_configuration = Signal(Configuration) - def __init__(self,plateReadingController): + def __init__(self, plateReadingController): QObject.__init__(self) self.plateReadingController = plateReadingController @@ -81,13 +83,13 @@ def run(self): self.run_single_time_point() self.time_point = self.time_point + 1 # check if the aquisition has taken longer than dt or integer multiples of dt, if so skip the next time point(s) - while time.time() > self.timestamp_acquisition_started + self.time_point*self.dt: - print('skip time point ' + str(self.time_point+1)) - self.time_point = self.time_point+1 + while time.time() > self.timestamp_acquisition_started + self.time_point * self.dt: + print("skip time point " + str(self.time_point + 1)) + self.time_point = self.time_point + 1 if self.time_point == self.Nt: - break # no waiting after taking the last time point + break # no waiting after taking the last time point # wait until it's time to do the next acquisition - while time.time() < self.timestamp_acquisition_started + self.time_point*self.dt: + while time.time() < self.timestamp_acquisition_started + self.time_point * self.dt: time.sleep(0.05) self.plateReaderNavigationController.is_scanning = False self.finished.emit() @@ -99,10 +101,10 @@ def wait_till_operation_is_completed(self): def run_single_time_point(self): self.FOV_counter = 0 column_counter = 0 - print('multipoint acquisition - time point ' + str(self.time_point+1)) - + print("multipoint acquisition - time point " + str(self.time_point + 1)) + # for each time point, create a new folder - current_path = os.path.join(self.base_path,self.experiment_ID,str(self.time_point)) + current_path = os.path.join(self.base_path, self.experiment_ID, str(self.time_point)) os.mkdir(current_path) # run homing @@ -110,43 +112,53 @@ def run_single_time_point(self): self.wait_till_operation_is_completed() # row scan direction - row_scan_direction = 1 # 1: A -> H, 0: H -> A + row_scan_direction = 1 # 1: A -> H, 0: H -> A # go through columns for column in self.selected_columns: - + # increament counter column_counter = column_counter + 1 - + # move to the current column - self.plateReaderNavigationController.moveto_column(column-1) + self.plateReaderNavigationController.moveto_column(column - 1) self.wait_till_operation_is_completed() - - ''' + + """ # row homing if column_counter > 1: self.plateReaderNavigationController.home_y() self.wait_till_operation_is_completed() - ''' - + """ + # go through rows for row in range(PLATE_READER.NUMBER_OF_ROWS): - if row_scan_direction == 0: # reverse scan: - row = PLATE_READER.NUMBER_OF_ROWS - 1 -row + if row_scan_direction == 0: # reverse scan: + row = PLATE_READER.NUMBER_OF_ROWS - 1 - row - row_str = chr(ord('A')+row) + row_str = chr(ord("A") + row) file_ID = row_str + str(column) # move to the selected row self.plateReaderNavigationController.moveto_row(row) self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Y/1000) - + time.sleep(SCAN_STABILIZATION_TIME_MS_Y / 1000) + # AF - if (self.NZ == 1) and (self.do_autofocus) and (self.FOV_counter%Acquisition.NUMBER_OF_FOVS_PER_AF==0): - configuration_name_AF = 'BF LED matrix full' - config_AF = next((config for config in self.configurationManager.configurations if config.name == configuration_name_AF)) + if ( + (self.NZ == 1) + and (self.do_autofocus) + and (self.FOV_counter % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) + ): + configuration_name_AF = "BF LED matrix full" + config_AF = next( + ( + config + for config in self.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) self.signal_current_configuration.emit(config_AF) self.autofocusController.autofocus() self.autofocusController.wait_till_autofocus_has_completed() @@ -154,15 +166,15 @@ def run_single_time_point(self): # z stack for k in range(self.NZ): - if(self.NZ > 1): + if self.NZ > 1: # update file ID - file_ID = file_ID + '_' + str(k) + file_ID = file_ID + "_" + str(k) # maneuver for achiving uniform step size and repeatability when using open-loop control self.plateReaderNavigationController.move_z_usteps(80) self.wait_till_operation_is_completed() self.plateReaderNavigationController.move_z_usteps(-80) self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # iterate through selected modes for config in self.selected_configurations: @@ -170,31 +182,33 @@ def run_single_time_point(self): self.wait_till_operation_is_completed() self.liveController.turn_on_illumination() self.wait_till_operation_is_completed() - self.camera.send_trigger() + self.camera.send_trigger() image = self.camera.read_frame() self.liveController.turn_off_illumination() - image = utils.crop_image(image,self.crop_width,self.crop_height) - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name) + '.' + Acquisition.IMAGE_FORMAT) + image = utils.crop_image(image, self.crop_width, self.crop_height) + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name) + "." + Acquisition.IMAGE_FORMAT + ) # self.image_to_display.emit(cv2.resize(image,(round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling)),cv2.INTER_LINEAR)) # image_to_display = utils.crop_image(image,round(self.crop_width*self.liveController.display_resolution_scaling), round(self.crop_height*self.liveController.display_resolution_scaling)) - image_to_display = utils.crop_image(image,round(self.crop_width), round(self.crop_height)) + image_to_display = utils.crop_image(image, round(self.crop_width), round(self.crop_height)) self.image_to_display.emit(image_to_display) - self.image_to_display_multi.emit(image_to_display,config.illumination_source) + self.image_to_display_multi.emit(image_to_display, config.illumination_source) if self.camera.is_color: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) QApplication.processEvents() - if(self.NZ > 1): + if self.NZ > 1: # move z if k < self.NZ - 1: self.plateReaderNavigationController.move_z_usteps(self.deltaZ_usteps) self.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) if self.NZ > 1: # move z back - self.plateReaderNavigationController.move_z_usteps(-self.deltaZ_usteps*(self.NZ-1)) + self.plateReaderNavigationController.move_z_usteps(-self.deltaZ_usteps * (self.NZ - 1)) self.wait_till_operation_is_completed() if self.abort_acquisition_requested: @@ -203,18 +217,21 @@ def run_single_time_point(self): # update row scan direction row_scan_direction = 1 - row_scan_direction + class PlateReadingController(QObject): acquisitionFinished = Signal() image_to_display = Signal(np.ndarray) - image_to_display_multi = Signal(np.ndarray,int) + image_to_display_multi = Signal(np.ndarray, int) signal_current_configuration = Signal(Configuration) - def __init__(self,camera,plateReaderNavigationController,liveController,autofocusController,configurationManager): + def __init__( + self, camera, plateReaderNavigationController, liveController, autofocusController, configurationManager + ): QObject.__init__(self) self.camera = camera - self.microcontroller = plateReaderNavigationController.microcontroller # to move to gui for transparency + self.microcontroller = plateReaderNavigationController.microcontroller # to move to gui for transparency self.plateReaderNavigationController = plateReaderNavigationController self.liveController = liveController self.autofocusController = autofocusController @@ -223,15 +240,15 @@ def __init__(self,camera,plateReaderNavigationController,liveController,autofocu self.NY = 1 self.NZ = 1 self.Nt = 1 - mm_per_ustep_X = SCREW_PITCH_X_MM/(self.plateReaderNavigationController.x_microstepping*FULLSTEPS_PER_REV_X) - mm_per_ustep_Y = SCREW_PITCH_Y_MM/(self.plateReaderNavigationController.y_microstepping*FULLSTEPS_PER_REV_Y) - mm_per_ustep_Z = SCREW_PITCH_Z_MM/(self.plateReaderNavigationController.z_microstepping*FULLSTEPS_PER_REV_Z) + mm_per_ustep_X = SCREW_PITCH_X_MM / (self.plateReaderNavigationController.x_microstepping * FULLSTEPS_PER_REV_X) + mm_per_ustep_Y = SCREW_PITCH_Y_MM / (self.plateReaderNavigationController.y_microstepping * FULLSTEPS_PER_REV_Y) + mm_per_ustep_Z = SCREW_PITCH_Z_MM / (self.plateReaderNavigationController.z_microstepping * FULLSTEPS_PER_REV_Z) self.deltaX = Acquisition.DX - self.deltaX_usteps = round(self.deltaX/mm_per_ustep_X) + self.deltaX_usteps = round(self.deltaX / mm_per_ustep_X) self.deltaY = Acquisition.DY - self.deltaY_usteps = round(self.deltaY/mm_per_ustep_Y) - self.deltaZ = Acquisition.DZ/1000 - self.deltaZ_usteps = round(self.deltaZ/mm_per_ustep_Z) + self.deltaY_usteps = round(self.deltaY / mm_per_ustep_Y) + self.deltaZ = Acquisition.DZ / 1000 + self.deltaZ_usteps = round(self.deltaZ / mm_per_ustep_Z) self.deltat = 0 self.do_autofocus = False self.crop_width = Acquisition.CROP_WIDTH @@ -243,58 +260,64 @@ def __init__(self,camera,plateReaderNavigationController,liveController,autofocu self.selected_configurations = [] self.selected_columns = [] - def set_NZ(self,N): + def set_NZ(self, N): self.NZ = N - - def set_Nt(self,N): + + def set_Nt(self, N): self.Nt = N - - def set_deltaZ(self,delta_um): - mm_per_ustep_Z = SCREW_PITCH_Z_MM/(self.plateReaderNavigationController.z_microstepping*FULLSTEPS_PER_REV_Z) - self.deltaZ = delta_um/1000 - self.deltaZ_usteps = round((delta_um/1000)/mm_per_ustep_Z) - - def set_deltat(self,delta): + + def set_deltaZ(self, delta_um): + mm_per_ustep_Z = SCREW_PITCH_Z_MM / (self.plateReaderNavigationController.z_microstepping * FULLSTEPS_PER_REV_Z) + self.deltaZ = delta_um / 1000 + self.deltaZ_usteps = round((delta_um / 1000) / mm_per_ustep_Z) + + def set_deltat(self, delta): self.deltat = delta - - def set_af_flag(self,flag): + + def set_af_flag(self, flag): self.do_autofocus = flag - def set_crop(self,crop_width,height): + def set_crop(self, crop_width, height): self.crop_width = crop_width self.crop_height = crop_height - def set_base_path(self,path): + def set_base_path(self, path): self.base_path = path - def start_new_experiment(self,experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment + def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment # generate unique experiment ID - self.experiment_ID = experiment_ID + '_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + self.experiment_ID = experiment_ID + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") self.recording_start_time = time.time() # create a new folder try: - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) - self.configurationManager.write_configuration(os.path.join(self.base_path,self.experiment_ID)+"/configurations.xml") # save the configuration for the experiment + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) + self.configurationManager.write_configuration( + os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + ) # save the configuration for the experiment except: pass def set_selected_configurations(self, selected_configurations_name): self.selected_configurations = [] for configuration_name in selected_configurations_name: - self.selected_configurations.append(next((config for config in self.configurationManager.configurations if config.name == configuration_name))) - - def set_selected_columns(self,selected_columns): + self.selected_configurations.append( + next( + (config for config in self.configurationManager.configurations if config.name == configuration_name) + ) + ) + + def set_selected_columns(self, selected_columns): selected_columns.sort() self.selected_columns = selected_columns - def run_acquisition(self): # @@@ to do: change name to run_experiment - print('start plate reading') + def run_acquisition(self): # @@@ to do: change name to run_experiment + print("start plate reading") # save the current microscope configuration self.configuration_before_running_multipoint = self.liveController.currentConfiguration # stop live if self.liveController.is_live: self.liveController.was_live_before_multipoint = True - self.liveController.stop_live() # @@@ to do: also uncheck the live button + self.liveController.stop_live() # @@@ to do: also uncheck the live button else: self.liveController.was_live_before_multipoint = False # disable callback @@ -302,7 +325,7 @@ def run_acquisition(self): # @@@ to do: change name to run_experiment self.camera.callback_was_enabled_before_multipoint = True self.camera.stop_streaming() self.camera.disable_callback() - self.camera.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class + self.camera.start_streaming() # @@@ to do: absorb stop/start streaming into enable/disable callback - add a flag is_streaming to the camera class else: self.camera.callback_was_enabled_before_multipoint = False @@ -321,7 +344,9 @@ def run_acquisition(self): # @@@ to do: change name to run_experiment self.plateReadingWorker.finished.connect(self.thread.quit) self.plateReadingWorker.image_to_display.connect(self.slot_image_to_display) self.plateReadingWorker.image_to_display_multi.connect(self.slot_image_to_display_multi) - self.plateReadingWorker.signal_current_configuration.connect(self.slot_current_configuration,type=Qt.BlockingQueuedConnection) + self.plateReadingWorker.signal_current_configuration.connect( + self.slot_current_configuration, type=Qt.BlockingQueuedConnection + ) self.thread.finished.connect(self.thread.deleteLater) # start the thread self.thread.start() @@ -339,20 +364,20 @@ def _on_acquisition_completed(self): self.camera.enable_callback() self.camera.start_streaming() self.camera.callback_was_enabled_before_multipoint = False - + # re-enable live if it's previously on if self.liveController.was_live_before_multipoint: self.liveController.start_live() - + # emit the acquisition finished signal to enable the UI self.acquisitionFinished.emit() QApplication.processEvents() - def slot_image_to_display(self,image): + def slot_image_to_display(self, image): self.image_to_display.emit(image) - def slot_image_to_display_multi(self,image,illumination_source): - self.image_to_display_multi.emit(image,illumination_source) + def slot_image_to_display_multi(self, image, illumination_source): + self.image_to_display_multi.emit(image, illumination_source) - def slot_current_configuration(self,configuration): + def slot_current_configuration(self, configuration): self.signal_current_configuration.emit(configuration) diff --git a/software/control/core_usbspectrometer.py b/software/control/core_usbspectrometer.py index 44659bb8b..9b8ed0e74 100644 --- a/software/control/core_usbspectrometer.py +++ b/software/control/core_usbspectrometer.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -28,6 +29,7 @@ import json import pandas as pd + class SpectrumStreamHandler(QObject): spectrum_to_display = Signal(np.ndarray) @@ -54,10 +56,10 @@ def start_recording(self): def stop_recording(self): self.save_spectrum_flag = False - def set_display_fps(self,fps): + def set_display_fps(self, fps): self.fps_display = fps - def set_save_fps(self,fps): + def set_save_fps(self, fps): self.fps_save = fps def on_new_measurement(self, data): @@ -65,32 +67,33 @@ def on_new_measurement(self, data): # measure real fps timestamp_now = round(time.time()) if timestamp_now == self.timestamp_last: - self.counter = self.counter+1 + self.counter = self.counter + 1 else: self.timestamp_last = timestamp_now self.fps_real = self.counter self.counter = 0 - print('real spectrometer fps is ' + str(self.fps_real)) + print("real spectrometer fps is " + str(self.fps_real)) # send image to display time_now = time.time() - if time_now-self.timestamp_last_display >= 1/self.fps_display: + if time_now - self.timestamp_last_display >= 1 / self.fps_display: self.spectrum_to_display.emit(data) self.timestamp_last_display = time_now # send image to write - if self.save_spectrum_flag and time_now-self.timestamp_last_save >= 1/self.fps_save: + if self.save_spectrum_flag and time_now - self.timestamp_last_save >= 1 / self.fps_save: self.spectrum_to_write.emit(data) self.timestamp_last_save = time_now + class SpectrumSaver(QObject): stop_recording = Signal() def __init__(self): QObject.__init__(self) - self.base_path = './' - self.experiment_ID = '' + self.base_path = "./" + self.experiment_ID = "" self.max_num_file_per_folder = 1000 - self.queue = Queue(10) # max 10 items in the queue + self.queue = Queue(10) # max 10 items in the queue self.stop_signal_received = False self.thread = Thread(target=self.process_queue) self.thread.start() @@ -106,45 +109,47 @@ def process_queue(self): # process the queue try: data = self.queue.get(timeout=0.1) - folder_ID = int(self.counter/self.max_num_file_per_folder) - file_ID = int(self.counter%self.max_num_file_per_folder) + folder_ID = int(self.counter / self.max_num_file_per_folder) + file_ID = int(self.counter % self.max_num_file_per_folder) # create a new folder if file_ID == 0: - os.mkdir(os.path.join(self.base_path,self.experiment_ID,str(folder_ID))) + os.mkdir(os.path.join(self.base_path, self.experiment_ID, str(folder_ID))) - saving_path = os.path.join(self.base_path,self.experiment_ID,str(folder_ID),str(file_ID) + '.csv') - np.savetxt(saving_path,data,delimiter=',') + saving_path = os.path.join(self.base_path, self.experiment_ID, str(folder_ID), str(file_ID) + ".csv") + np.savetxt(saving_path, data, delimiter=",") self.counter = self.counter + 1 self.queue.task_done() except: pass - - def enqueue(self,data): + + def enqueue(self, data): try: self.queue.put_nowait(data) - if ( self.recording_time_limit>0 ) and ( time.time()-self.recording_start_time >= self.recording_time_limit ): + if (self.recording_time_limit > 0) and ( + time.time() - self.recording_start_time >= self.recording_time_limit + ): self.stop_recording.emit() # when using self.queue.put(str_), program can be slowed down despite multithreading because of the block and the GIL except: - print('imageSaver queue is full, image discarded') + print("imageSaver queue is full, image discarded") - def set_base_path(self,path): + def set_base_path(self, path): self.base_path = path - def set_recording_time_limit(self,time_limit): + def set_recording_time_limit(self, time_limit): self.recording_time_limit = time_limit - def start_new_experiment(self,experiment_ID,add_timestamp=True): + def start_new_experiment(self, experiment_ID, add_timestamp=True): if add_timestamp: # generate unique experiment ID - self.experiment_ID = experiment_ID + '_spectrum_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') + self.experiment_ID = experiment_ID + "_spectrum_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") else: self.experiment_ID = experiment_ID self.recording_start_time = time.time() # create a new folder try: - os.mkdir(os.path.join(self.base_path,self.experiment_ID)) + os.mkdir(os.path.join(self.base_path, self.experiment_ID)) # to do: save configuration except: pass diff --git a/software/control/core_volumetric_imaging.py b/software/control/core_volumetric_imaging.py index 7a417bf4d..bb6f07fa4 100644 --- a/software/control/core_volumetric_imaging.py +++ b/software/control/core_volumetric_imaging.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -33,7 +34,9 @@ class StreamHandler(QObject): packet_image_for_array_display = Signal(np.ndarray, int) signal_new_frame_received = Signal() - def __init__(self,crop_width=Acquisition.CROP_WIDTH,crop_height=Acquisition.CROP_HEIGHT,display_resolution_scaling=0.5): + def __init__( + self, crop_width=Acquisition.CROP_WIDTH, crop_height=Acquisition.CROP_HEIGHT, display_resolution_scaling=0.5 + ): QObject.__init__(self) self.fps_display = 1 self.fps_save = 1 @@ -67,62 +70,71 @@ def start_tracking(self): def stop_tracking(self): self.tracking_flag = False - def set_display_fps(self,fps): + def set_display_fps(self, fps): self.fps_display = fps - def set_save_fps(self,fps): + def set_save_fps(self, fps): self.fps_save = fps - def set_crop(self,crop_width,height): + def set_crop(self, crop_width, height): self.crop_width = crop_width self.crop_height = crop_height def set_display_resolution_scaling(self, display_resolution_scaling): - self.display_resolution_scaling = display_resolution_scaling/100 + self.display_resolution_scaling = display_resolution_scaling / 100 print(self.display_resolution_scaling) def on_new_frame(self, camera): camera.image_locked = True self.handler_busy = True - self.signal_new_frame_received.emit() # self.liveController.turn_off_illumination() + self.signal_new_frame_received.emit() # self.liveController.turn_off_illumination() # measure real fps timestamp_now = round(time.time()) if timestamp_now == self.timestamp_last: - self.counter = self.counter+1 + self.counter = self.counter + 1 else: self.timestamp_last = timestamp_now self.fps_real = self.counter self.counter = 0 - print('real camera fps is ' + str(self.fps_real)) + print("real camera fps is " + str(self.fps_real)) # crop image - image_cropped = utils.crop_image(camera.current_frame,self.crop_width,self.crop_height) + image_cropped = utils.crop_image(camera.current_frame, self.crop_width, self.crop_height) image_cropped = np.squeeze(image_cropped) # send image to display time_now = time.time() - if time_now-self.timestamp_last_display >= 1/self.fps_display: + if time_now - self.timestamp_last_display >= 1 / self.fps_display: # self.image_to_display.emit(cv2.resize(image_cropped,(round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling)),cv2.INTER_LINEAR)) - self.image_to_display.emit(utils.crop_image(image_cropped,round(self.crop_width*self.display_resolution_scaling), round(self.crop_height*self.display_resolution_scaling))) + self.image_to_display.emit( + utils.crop_image( + image_cropped, + round(self.crop_width * self.display_resolution_scaling), + round(self.crop_height * self.display_resolution_scaling), + ) + ) self.timestamp_last_display = time_now # send image to array display - self.packet_image_for_array_display.emit(image_cropped,(camera.frame_ID - camera.frame_ID_offset_hardware_trigger - 1) % VOLUMETRIC_IMAGING.NUM_PLANES_PER_VOLUME) + self.packet_image_for_array_display.emit( + image_cropped, + (camera.frame_ID - camera.frame_ID_offset_hardware_trigger - 1) % VOLUMETRIC_IMAGING.NUM_PLANES_PER_VOLUME, + ) # send image to write - if self.save_image_flag and time_now-self.timestamp_last_save >= 1/self.fps_save: + if self.save_image_flag and time_now - self.timestamp_last_save >= 1 / self.fps_save: if camera.is_color: - image_cropped = cv2.cvtColor(image_cropped,cv2.COLOR_RGB2BGR) - self.packet_image_to_write.emit(image_cropped,camera.frame_ID,camera.timestamp) + image_cropped = cv2.cvtColor(image_cropped, cv2.COLOR_RGB2BGR) + self.packet_image_to_write.emit(image_cropped, camera.frame_ID, camera.timestamp) self.timestamp_last_save = time_now # send image to track - if self.track_flag and time_now-self.timestamp_last_track >= 1/self.fps_track: + if self.track_flag and time_now - self.timestamp_last_track >= 1 / self.fps_track: # track is a blocking operation - it needs to be # @@@ will cropping before emitting the signal lead to speedup? - self.packet_image_for_tracking.emit(image_cropped,camera.frame_ID,camera.timestamp) + self.packet_image_for_tracking.emit(image_cropped, camera.frame_ID, camera.timestamp) self.timestamp_last_track = time_now self.handler_busy = False @@ -131,7 +143,7 @@ def on_new_frame(self, camera): class ImageArrayDisplayWindow(QMainWindow): - def __init__(self, window_title=''): + def __init__(self, window_title=""): super().__init__() self.setWindowTitle(window_title) self.setWindowFlags(self.windowFlags() | Qt.CustomizeWindowHint) @@ -139,13 +151,13 @@ def __init__(self, window_title=''): self.widget = QWidget() # interpret image data as row-major instead of col-major - pg.setConfigOptions(imageAxisOrder='row-major') + pg.setConfigOptions(imageAxisOrder="row-major") self.sub_windows = [] for i in range(9): self.sub_windows.append(pg.GraphicsLayoutWidget()) self.sub_windows[i].view = self.sub_windows[i].addViewBox(enableMouse=True) - self.sub_windows[i].img = pg.ImageItem(border='w') + self.sub_windows[i].img = pg.ImageItem(border="w") self.sub_windows[i].view.setAspectLocked(True) self.sub_windows[i].view.addItem(self.sub_windows[i].img) @@ -154,22 +166,22 @@ def __init__(self, window_title=''): layout.addWidget(self.sub_windows[0], 0, 0) layout.addWidget(self.sub_windows[1], 0, 1) layout.addWidget(self.sub_windows[2], 0, 2) - layout.addWidget(self.sub_windows[3], 1, 0) - layout.addWidget(self.sub_windows[4], 1, 1) - layout.addWidget(self.sub_windows[5], 1, 2) - layout.addWidget(self.sub_windows[6], 2, 0) - layout.addWidget(self.sub_windows[7], 2, 1) - layout.addWidget(self.sub_windows[8], 2, 2) + layout.addWidget(self.sub_windows[3], 1, 0) + layout.addWidget(self.sub_windows[4], 1, 1) + layout.addWidget(self.sub_windows[5], 1, 2) + layout.addWidget(self.sub_windows[6], 2, 0) + layout.addWidget(self.sub_windows[7], 2, 1) + layout.addWidget(self.sub_windows[8], 2, 2) self.widget.setLayout(layout) self.setCentralWidget(self.widget) # set window size - desktopWidget = QDesktopWidget(); - width = min(desktopWidget.height()*0.9,1000) #@@@TO MOVE@@@# + desktopWidget = QDesktopWidget() + width = min(desktopWidget.height() * 0.9, 1000) # @@@TO MOVE@@@# height = width - self.setFixedSize(width,height) + self.setFixedSize(width, height) - def display_image(self,image,i): + def display_image(self, image, i): if i < 9: - self.sub_windows[i].img.setImage(image,autoLevels=False) + self.sub_windows[i].img.setImage(image, autoLevels=False) self.sub_windows[i].view.autoRange(padding=0) diff --git a/software/control/filterwheel.py b/software/control/filterwheel.py index a2255d8ba..575fbf350 100644 --- a/software/control/filterwheel.py +++ b/software/control/filterwheel.py @@ -3,6 +3,7 @@ from control._def import * + class SquidFilterWheelWrapper: def __init__(self, microcontroller): @@ -17,11 +18,15 @@ def __init__(self, microcontroller): if HAS_ENCODER_W: self.microcontroller.set_pid_arguments(SQUID_FILTERWHEEL_MOTORSLOTINDEX, PID_P_W, PID_I_W, PID_D_W) - self.microcontroller.configure_stage_pid(SQUID_FILTERWHEEL_MOTORSLOTINDEX, SQUID_FILTERWHEEL_TRANSITIONS_PER_REVOLUTION, ENCODER_FLIP_DIR_W) + self.microcontroller.configure_stage_pid( + SQUID_FILTERWHEEL_MOTORSLOTINDEX, SQUID_FILTERWHEEL_TRANSITIONS_PER_REVOLUTION, ENCODER_FLIP_DIR_W + ) self.microcontroller.turn_on_stage_pid(SQUID_FILTERWHEEL_MOTORSLOTINDEX, ENABLE_PID_W) - def move_w(self,delta): - self.microcontroller.move_w_usteps(int(delta/(SCREW_PITCH_W_MM/(MICROSTEPPING_DEFAULT_W * FULLSTEPS_PER_REV_W)))) + def move_w(self, delta): + self.microcontroller.move_w_usteps( + int(delta / (SCREW_PITCH_W_MM / (MICROSTEPPING_DEFAULT_W * FULLSTEPS_PER_REV_W))) + ) def homing(self): self.microcontroller.home_w() @@ -30,12 +35,12 @@ def homing(self): self.move_w(SQUID_FILTERWHEEL_OFFSET) self.w_pos_index = SQUID_FILTERWHEEL_MIN_INDEX - + def next_position(self): if self.w_pos_index < SQUID_FILTERWHEEL_MAX_INDEX: self.move_w(SCREW_PITCH_W_MM / (SQUID_FILTERWHEEL_MAX_INDEX - SQUID_FILTERWHEEL_MIN_INDEX + 1)) self.microcontroller.wait_till_operation_is_completed() - self.w_pos_index += 1 + self.w_pos_index += 1 def previous_position(self): if self.w_pos_index > SQUID_FILTERWHEEL_MIN_INDEX: @@ -48,9 +53,13 @@ def set_emission(self, pos): Set the emission filter to the specified position. pos from 1 to 8 """ - if pos in range(SQUID_FILTERWHEEL_MIN_INDEX, SQUID_FILTERWHEEL_MAX_INDEX + 1): + if pos in range(SQUID_FILTERWHEEL_MIN_INDEX, SQUID_FILTERWHEEL_MAX_INDEX + 1): if pos != self.w_pos_index: - self.move_w((pos - self.w_pos_index) * SCREW_PITCH_W_MM / (SQUID_FILTERWHEEL_MAX_INDEX - SQUID_FILTERWHEEL_MIN_INDEX + 1)) + self.move_w( + (pos - self.w_pos_index) + * SCREW_PITCH_W_MM + / (SQUID_FILTERWHEEL_MAX_INDEX - SQUID_FILTERWHEEL_MIN_INDEX + 1) + ) self.microcontroller.wait_till_operation_is_completed() self.w_pos_index = pos diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index f07ceba7a..ac024c552 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -1,5 +1,6 @@ # set QT_API environment variable import os + os.environ["QT_API"] = "pyqt5" import serial import time @@ -90,7 +91,8 @@ if SUPPORT_LASER_AUTOFOCUS: import control.core_displacement_measurement as core_displacement_measurement -SINGLE_WINDOW = True # set to False if use separate windows for display and control +SINGLE_WINDOW = True # set to False if use separate windows for display and control + class MovementUpdater(QObject): position_after_move = Signal(squid.abc.Pos) @@ -114,7 +116,11 @@ def do_update(self): abs_delta_x = abs(self.previous_pos.x_mm - pos.x_mm) abs_delta_y = abs(self.previous_pos.y_mm - pos.y_mm) - if abs_delta_y < self.movement_threshhold_mm and abs_delta_x < self.movement_threshhold_mm and not self.stage.get_state().busy: + if ( + abs_delta_y < self.movement_threshhold_mm + and abs_delta_x < self.movement_threshhold_mm + and not self.stage.get_state().busy + ): # In here, send all the signals that must be sent once per stop of movement. AKA once per arriving at a # new position for a while. self.sent_after_stopped = True @@ -187,34 +193,93 @@ def loadObjects(self, is_simulation): # Common object initialization self.objectiveStore = core.ObjectiveStore(parent=self) - self.configurationManager = core.ConfigurationManager(filename='./channel_configurations.xml') + self.configurationManager = core.ConfigurationManager(filename="./channel_configurations.xml") self.contrastManager = core.ContrastManager() - self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP/100) - self.liveController = core.LiveController(self.camera, self.microcontroller, self.configurationManager, self.illuminationController, parent=self) - self.stage: squid.abc.AbstractStage = squid.stage.cephla.CephlaStage(microcontroller = self.microcontroller, stage_config = squid.config.get_stage_config()) - self.slidePositionController = core.SlidePositionController(self.stage, self.liveController, is_for_wellplate=True) - self.autofocusController = core.AutoFocusController(self.camera, self.stage, self.liveController, self.microcontroller) - self.slidePositionController = core.SlidePositionController(self.stage, self.liveController, is_for_wellplate=True) - self.autofocusController = core.AutoFocusController(self.camera, self.stage, self.liveController, self.microcontroller) + self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) + self.liveController = core.LiveController( + self.camera, self.microcontroller, self.configurationManager, self.illuminationController, parent=self + ) + self.stage: squid.abc.AbstractStage = squid.stage.cephla.CephlaStage( + microcontroller=self.microcontroller, stage_config=squid.config.get_stage_config() + ) + self.slidePositionController = core.SlidePositionController( + self.stage, self.liveController, is_for_wellplate=True + ) + self.autofocusController = core.AutoFocusController( + self.camera, self.stage, self.liveController, self.microcontroller + ) + self.slidePositionController = core.SlidePositionController( + self.stage, self.liveController, is_for_wellplate=True + ) + self.autofocusController = core.AutoFocusController( + self.camera, self.stage, self.liveController, self.microcontroller + ) self.imageSaver = core.ImageSaver() self.imageDisplay = core.ImageDisplay() if ENABLE_TRACKING: - self.trackingController = core.TrackingController(self.camera, self.microcontroller, self.stage, self.configurationManager, self.liveController, self.autofocusController, self.imageDisplayWindow) - if WELLPLATE_FORMAT == 'glass slide': - self.navigationViewer = core.NavigationViewer(self.objectiveStore, sample='4 glass slide') + self.trackingController = core.TrackingController( + self.camera, + self.microcontroller, + self.stage, + self.configurationManager, + self.liveController, + self.autofocusController, + self.imageDisplayWindow, + ) + if WELLPLATE_FORMAT == "glass slide": + self.navigationViewer = core.NavigationViewer(self.objectiveStore, sample="4 glass slide") else: self.navigationViewer = core.NavigationViewer(self.objectiveStore, sample=WELLPLATE_FORMAT) - self.scanCoordinates = core.ScanCoordinates(objectiveStore=self.objectiveStore, navigationViewer=self.navigationViewer, stage=self.stage) - self.multipointController = core.MultiPointController(self.camera, self.stage, self.microcontroller, self.liveController, self.autofocusController, self.configurationManager, scanCoordinates=self.scanCoordinates, parent=self) + self.scanCoordinates = core.ScanCoordinates( + objectiveStore=self.objectiveStore, navigationViewer=self.navigationViewer, stage=self.stage + ) + self.multipointController = core.MultiPointController( + self.camera, + self.stage, + self.microcontroller, + self.liveController, + self.autofocusController, + self.configurationManager, + scanCoordinates=self.scanCoordinates, + parent=self, + ) if SUPPORT_LASER_AUTOFOCUS: - self.configurationManager_focus_camera = core.ConfigurationManager(filename='./focus_camera_configurations.xml') + self.configurationManager_focus_camera = core.ConfigurationManager( + filename="./focus_camera_configurations.xml" + ) self.streamHandler_focus_camera = core.StreamHandler() - self.liveController_focus_camera = core.LiveController(self.camera_focus,self.microcontroller,self.configurationManager_focus_camera, self, control_illumination=False,for_displacement_measurement=True) - self.multipointController = core.MultiPointController(self.camera,self.stage,self.microcontroller,self.liveController,self.autofocusController,self.configurationManager,scanCoordinates=self.scanCoordinates,parent=self) - self.imageDisplayWindow_focus = core.ImageDisplayWindow(draw_crosshairs=True, show_LUT=False, autoLevels=False) + self.liveController_focus_camera = core.LiveController( + self.camera_focus, + self.microcontroller, + self.configurationManager_focus_camera, + self, + control_illumination=False, + for_displacement_measurement=True, + ) + self.multipointController = core.MultiPointController( + self.camera, + self.stage, + self.microcontroller, + self.liveController, + self.autofocusController, + self.configurationManager, + scanCoordinates=self.scanCoordinates, + parent=self, + ) + self.imageDisplayWindow_focus = core.ImageDisplayWindow( + draw_crosshairs=True, show_LUT=False, autoLevels=False + ) self.displacementMeasurementController = core_displacement_measurement.DisplacementMeasurementController() - self.laserAutofocusController = core.LaserAutofocusController(self.microcontroller,self.camera_focus,self.liveController_focus_camera,self.stage,has_two_interfaces=HAS_TWO_INTERFACES,use_glass_top=USE_GLASS_TOP,look_for_cache=False) + self.laserAutofocusController = core.LaserAutofocusController( + self.microcontroller, + self.camera_focus, + self.liveController_focus_camera, + self.stage, + has_two_interfaces=HAS_TWO_INTERFACES, + use_glass_top=USE_GLASS_TOP, + look_for_cache=False, + ) if USE_SQUID_FILTERWHEEL: self.squid_filter_wheel = filterwheel.SquidFilterWheelWrapper(self.microcontroller) @@ -227,6 +292,7 @@ def loadSimulationObjects(self): self.xlight = serial_peripherals.XLight_Simulation() if ENABLE_NL5: import control.NL5 as NL5 + self.nl5 = NL5.NL5_Simulation() if ENABLE_CELLX: self.cellx = serial_peripherals.CellX_Simulation() @@ -234,13 +300,17 @@ def loadSimulationObjects(self): self.camera_focus = camera_fc.Camera_Simulation() if USE_LDI_SERIAL_CONTROL: self.ldi = serial_peripherals.LDI_Simulation() - self.illuminationController = control.microscope.IlluminationController(self.microcontroller, self.ldi.intensity_mode, self.ldi.shutter_mode, LightSourceType.LDI, self.ldi) + self.illuminationController = control.microscope.IlluminationController( + self.microcontroller, self.ldi.intensity_mode, self.ldi.shutter_mode, LightSourceType.LDI, self.ldi + ) self.camera = camera.Camera_Simulation(rotate_image_angle=ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE) self.camera.set_pixel_format(DEFAULT_PIXEL_FORMAT) if USE_ZABER_EMISSION_FILTER_WHEEL: - self.emission_filter_wheel = serial_peripherals.FilterController_Simulation(115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE) + self.emission_filter_wheel = serial_peripherals.FilterController_Simulation( + 115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE + ) if USE_OPTOSPIN_EMISSION_FILTER_WHEEL: - self.emission_filter_wheel = serial_peripherals.Optospin_Simulation(SN=None) + self.emission_filter_wheel = serial_peripherals.Optospin_Simulation(SN=None) if USE_SQUID_FILTERWHEEL: self.squid_filter_wheel = filterwheel.SquidFilterWheelWrapper_Simulation(None) @@ -262,6 +332,7 @@ def loadHardwareObjects(self): if ENABLE_NL5: try: import control.NL5 as NL5 + self.nl5 = NL5.NL5() except Exception: self.log.error("Error initializing NL5") @@ -270,7 +341,7 @@ def loadHardwareObjects(self): if ENABLE_CELLX: try: self.cellx = serial_peripherals.CellX(CELLX_SN) - for channel in [1,2,3,4]: + for channel in [1, 2, 3, 4]: self.cellx.set_modulation(channel, CELLX_MODULATION) self.cellx.turn_on(channel) except Exception: @@ -280,7 +351,9 @@ def loadHardwareObjects(self): if USE_LDI_SERIAL_CONTROL: try: self.ldi = serial_peripherals.LDI() - self.illuminationController = control.microscope.IlluminationController(self.microcontroller, self.ldi.intensity_mode, self.ldi.shutter_mode, LightSourceType.LDI, self.ldi) + self.illuminationController = control.microscope.IlluminationController( + self.microcontroller, self.ldi.intensity_mode, self.ldi.shutter_mode, LightSourceType.LDI, self.ldi + ) except Exception: self.log.error("Error initializing LDI") raise @@ -288,8 +361,15 @@ def loadHardwareObjects(self): if USE_CELESTA_ETHENET_CONTROL: try: import control.celesta + self.celesta = control.celesta.CELESTA() - self.illuminationController = control.microscope.IlluminationController(self.microcontroller, IntensityControlMode.Software, ShutterControlMode.TTL, LightSourceType.CELESTA, self.celesta) + self.illuminationController = control.microscope.IlluminationController( + self.microcontroller, + IntensityControlMode.Software, + ShutterControlMode.TTL, + LightSourceType.CELESTA, + self.celesta, + ) except Exception: self.log.error("Error initializing CELESTA") raise @@ -299,7 +379,7 @@ def loadHardwareObjects(self): sn_camera_focus = camera_fc.get_sn_by_model(FOCUS_CAMERA_MODEL) self.camera_focus = camera_fc.Camera(sn=sn_camera_focus) self.camera_focus.open() - self.camera_focus.set_pixel_format('MONO8') + self.camera_focus.set_pixel_format("MONO8") except Exception: self.log.error(f"Error initializing Laser Autofocus Camera") raise @@ -315,7 +395,9 @@ def loadHardwareObjects(self): if USE_ZABER_EMISSION_FILTER_WHEEL: try: - self.emission_filter_wheel = serial_peripherals.FilterController(FILTER_CONTROLLER_SERIAL_NUMBER, 115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE) + self.emission_filter_wheel = serial_peripherals.FilterController( + FILTER_CONTROLLER_SERIAL_NUMBER, 115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE + ) except Exception: self.log.error("Error initializing Zaber Emission Filter Wheel") raise @@ -359,9 +441,9 @@ def setupHardware(self): self.stage.move_y(20) if ENABLE_OBJECTIVE_PIEZO: - OUTPUT_GAINS.CHANNEL7_GAIN = (OBJECTIVE_PIEZO_CONTROL_VOLTAGE_RANGE == 5) + OUTPUT_GAINS.CHANNEL7_GAIN = OBJECTIVE_PIEZO_CONTROL_VOLTAGE_RANGE == 5 div = 1 if OUTPUT_GAINS.REFDIV else 0 - gains = sum(getattr(OUTPUT_GAINS, f'CHANNEL{i}_GAIN') << i for i in range(8)) + gains = sum(getattr(OUTPUT_GAINS, f"CHANNEL{i}_GAIN") << i for i in range(8)) self.microcontroller.configure_dac80508_refdiv_and_gain(div, gains) self.microcontroller.set_dac80508_scaling_factor_for_illumination(ILLUMINATION_INTENSITY_FACTOR) except TimeoutError as e: @@ -378,7 +460,7 @@ def setupHardware(self): self.camera.set_reset_strobe_delay_function(self.liveController.reset_strobe_arugment) if SUPPORT_LASER_AUTOFOCUS: - self.camera_focus.set_software_triggered_acquisition() #self.camera.set_continuous_acquisition() + self.camera_focus.set_software_triggered_acquisition() # self.camera.set_continuous_acquisition() self.camera_focus.set_callback(self.streamHandler_focus_camera.on_new_frame) self.camera_focus.enable_callback() self.camera_focus.start_streaming() @@ -400,67 +482,137 @@ def loadWidgets(self): self.spinningDiskConfocalWidget = widgets.SpinningDiskConfocalWidget(self.xlight, self.configurationManager) if ENABLE_NL5: import control.NL5Widget as NL5Widget + self.nl5Wdiget = NL5Widget.NL5Widget(self.nl5) if CAMERA_TYPE == "Toupcam": - self.cameraSettingWidget = widgets.CameraSettingsWidget(self.camera, include_gain_exposure_time=False, include_camera_temperature_setting=True, include_camera_auto_wb_setting=False) + self.cameraSettingWidget = widgets.CameraSettingsWidget( + self.camera, + include_gain_exposure_time=False, + include_camera_temperature_setting=True, + include_camera_auto_wb_setting=False, + ) else: - self.cameraSettingWidget = widgets.CameraSettingsWidget(self.camera, include_gain_exposure_time=False, include_camera_temperature_setting=False, include_camera_auto_wb_setting=True) - self.liveControlWidget = widgets.LiveControlWidget(self.streamHandler, self.liveController, self.configurationManager, show_display_options=True, show_autolevel=True, autolevel=True) - self.navigationWidget = widgets.NavigationWidget(self.stage, self.slidePositionController, widget_configuration=f'{WELLPLATE_FORMAT} well plate') + self.cameraSettingWidget = widgets.CameraSettingsWidget( + self.camera, + include_gain_exposure_time=False, + include_camera_temperature_setting=False, + include_camera_auto_wb_setting=True, + ) + self.liveControlWidget = widgets.LiveControlWidget( + self.streamHandler, + self.liveController, + self.configurationManager, + show_display_options=True, + show_autolevel=True, + autolevel=True, + ) + self.navigationWidget = widgets.NavigationWidget( + self.stage, self.slidePositionController, widget_configuration=f"{WELLPLATE_FORMAT} well plate" + ) self.dacControlWidget = widgets.DACControWidget(self.microcontroller) self.autofocusWidget = widgets.AutoFocusWidget(self.autofocusController) self.piezoWidget = widgets.PiezoWidget(self.microcontroller) self.objectivesWidget = widgets.ObjectivesWidget(self.objectiveStore) if USE_ZABER_EMISSION_FILTER_WHEEL or USE_OPTOSPIN_EMISSION_FILTER_WHEEL: - self.filterControllerWidget = widgets.FilterControllerWidget(self.emission_filter_wheel, self.liveController) + self.filterControllerWidget = widgets.FilterControllerWidget( + self.emission_filter_wheel, self.liveController + ) if USE_SQUID_FILTERWHEEL: self.squidFilterWidget = widgets.SquidFilterWidget(self) self.recordingControlWidget = widgets.RecordingWidget(self.streamHandler, self.imageSaver) - self.wellplateFormatWidget = widgets.WellplateFormatWidget(self.stage, self.navigationViewer, self.streamHandler, self.liveController) - if WELLPLATE_FORMAT != '1536 well plate': + self.wellplateFormatWidget = widgets.WellplateFormatWidget( + self.stage, self.navigationViewer, self.streamHandler, self.liveController + ) + if WELLPLATE_FORMAT != "1536 well plate": self.wellSelectionWidget = widgets.WellSelectionWidget(WELLPLATE_FORMAT, self.wellplateFormatWidget) else: self.wellSelectionWidget = widgets.Well1536SelectionWidget() self.scanCoordinates.add_well_selector(self.wellSelectionWidget) - self.focusMapWidget = widgets.FocusMapWidget(self.stage, self.navigationViewer, self.scanCoordinates, core.FocusMap()) + self.focusMapWidget = widgets.FocusMapWidget( + self.stage, self.navigationViewer, self.scanCoordinates, core.FocusMap() + ) if SUPPORT_LASER_AUTOFOCUS: if FOCUS_CAMERA_TYPE == "Toupcam": - self.cameraSettingWidget_focus_camera = widgets.CameraSettingsWidget(self.camera_focus, include_gain_exposure_time = False, include_camera_temperature_setting = True, include_camera_auto_wb_setting = False) + self.cameraSettingWidget_focus_camera = widgets.CameraSettingsWidget( + self.camera_focus, + include_gain_exposure_time=False, + include_camera_temperature_setting=True, + include_camera_auto_wb_setting=False, + ) else: - self.cameraSettingWidget_focus_camera = widgets.CameraSettingsWidget(self.camera_focus, include_gain_exposure_time = False, include_camera_temperature_setting = False, include_camera_auto_wb_setting = True) - self.liveControlWidget_focus_camera = widgets.LiveControlWidget(self.streamHandler_focus_camera,self.liveController_focus_camera,self.configurationManager_focus_camera, stretch=False) #,show_display_options=True) - self.waveformDisplay = widgets.WaveformDisplay(N=1000,include_x=True,include_y=False) - self.displacementMeasurementWidget = widgets.DisplacementMeasurementWidget(self.displacementMeasurementController,self.waveformDisplay) + self.cameraSettingWidget_focus_camera = widgets.CameraSettingsWidget( + self.camera_focus, + include_gain_exposure_time=False, + include_camera_temperature_setting=False, + include_camera_auto_wb_setting=True, + ) + self.liveControlWidget_focus_camera = widgets.LiveControlWidget( + self.streamHandler_focus_camera, + self.liveController_focus_camera, + self.configurationManager_focus_camera, + stretch=False, + ) # ,show_display_options=True) + self.waveformDisplay = widgets.WaveformDisplay(N=1000, include_x=True, include_y=False) + self.displacementMeasurementWidget = widgets.DisplacementMeasurementWidget( + self.displacementMeasurementController, self.waveformDisplay + ) self.laserAutofocusControlWidget = widgets.LaserAutofocusControlWidget(self.laserAutofocusController) self.imageDisplayWindow_focus = core.ImageDisplayWindow(draw_crosshairs=True) self.waveformDisplay = widgets.WaveformDisplay(N=1000, include_x=True, include_y=False) - self.displacementMeasurementWidget = widgets.DisplacementMeasurementWidget(self.displacementMeasurementController, self.waveformDisplay) + self.displacementMeasurementWidget = widgets.DisplacementMeasurementWidget( + self.displacementMeasurementController, self.waveformDisplay + ) self.laserAutofocusControlWidget = widgets.LaserAutofocusControlWidget(self.laserAutofocusController) self.imageDisplayTabs = QTabWidget() if self.live_only_mode: if ENABLE_TRACKING: - self.imageDisplayWindow = core.ImageDisplayWindow(self.liveController, self.contrastManager, draw_crosshairs=True) + self.imageDisplayWindow = core.ImageDisplayWindow( + self.liveController, self.contrastManager, draw_crosshairs=True + ) self.imageDisplayWindow.show_ROI_selector() else: - self.imageDisplayWindow = core.ImageDisplayWindow(self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True) + self.imageDisplayWindow = core.ImageDisplayWindow( + self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True + ) self.imageDisplayTabs = self.imageDisplayWindow.widget self.napariMosaicDisplayWidget = None else: self.setupImageDisplayTabs() - self.flexibleMultiPointWidget = widgets.FlexibleMultiPointWidget(self.stage, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, self.scanCoordinates, self.focusMapWidget) - self.wellplateMultiPointWidget = widgets.WellplateMultiPointWidget(self.stage, self.navigationViewer, self.multipointController, self.objectiveStore, self.configurationManager, self.scanCoordinates, self.focusMapWidget, self.napariMosaicDisplayWidget) + self.flexibleMultiPointWidget = widgets.FlexibleMultiPointWidget( + self.stage, + self.navigationViewer, + self.multipointController, + self.objectiveStore, + self.configurationManager, + self.scanCoordinates, + self.focusMapWidget, + ) + self.wellplateMultiPointWidget = widgets.WellplateMultiPointWidget( + self.stage, + self.navigationViewer, + self.multipointController, + self.objectiveStore, + self.configurationManager, + self.scanCoordinates, + self.focusMapWidget, + self.napariMosaicDisplayWidget, + ) self.sampleSettingsWidget = widgets.SampleSettingsWidget(self.objectivesWidget, self.wellplateFormatWidget) if ENABLE_TRACKING: - self.trackingControlWidget = widgets.TrackingControllerWidget(self.trackingController, self.configurationManager, show_configurations=TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS) + self.trackingControlWidget = widgets.TrackingControllerWidget( + self.trackingController, + self.configurationManager, + show_configurations=TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS, + ) if ENABLE_STITCHER: self.stitcherWidget = widgets.StitcherWidget(self.configurationManager, self.contrastManager) @@ -472,46 +624,61 @@ def loadWidgets(self): def setupImageDisplayTabs(self): if USE_NAPARI_FOR_LIVE_VIEW: - self.napariLiveWidget = widgets.NapariLiveWidget(self.streamHandler, self.liveController, self.stage, self.configurationManager, self.contrastManager, self.wellSelectionWidget) + self.napariLiveWidget = widgets.NapariLiveWidget( + self.streamHandler, + self.liveController, + self.stage, + self.configurationManager, + self.contrastManager, + self.wellSelectionWidget, + ) self.imageDisplayTabs.addTab(self.napariLiveWidget, "Live View") else: if ENABLE_TRACKING: - self.imageDisplayWindow = core.ImageDisplayWindow(self.liveController, self.contrastManager, draw_crosshairs=True) + self.imageDisplayWindow = core.ImageDisplayWindow( + self.liveController, self.contrastManager, draw_crosshairs=True + ) self.imageDisplayWindow.show_ROI_selector() else: - self.imageDisplayWindow = core.ImageDisplayWindow(self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True) + self.imageDisplayWindow = core.ImageDisplayWindow( + self.liveController, self.contrastManager, draw_crosshairs=True, show_LUT=True, autoLevels=True + ) self.imageDisplayTabs.addTab(self.imageDisplayWindow.widget, "Live View") if not self.live_only_mode: if USE_NAPARI_FOR_MULTIPOINT: - self.napariMultiChannelWidget = widgets.NapariMultiChannelWidget(self.objectiveStore, self.contrastManager) + self.napariMultiChannelWidget = widgets.NapariMultiChannelWidget( + self.objectiveStore, self.contrastManager + ) self.imageDisplayTabs.addTab(self.napariMultiChannelWidget, "Multichannel Acquisition") else: self.imageArrayDisplayWindow = core.ImageArrayDisplayWindow() self.imageDisplayTabs.addTab(self.imageArrayDisplayWindow.widget, "Multichannel Acquisition") if USE_NAPARI_FOR_MOSAIC_DISPLAY: - self.napariMosaicDisplayWidget = widgets.NapariMosaicDisplayWidget(self.objectiveStore, self.contrastManager) + self.napariMosaicDisplayWidget = widgets.NapariMosaicDisplayWidget( + self.objectiveStore, self.contrastManager + ) self.imageDisplayTabs.addTab(self.napariMosaicDisplayWidget, "Mosaic View") if SUPPORT_LASER_AUTOFOCUS: - dock_laserfocus_image_display = dock.Dock('Focus Camera Image Display', autoOrientation=False) + dock_laserfocus_image_display = dock.Dock("Focus Camera Image Display", autoOrientation=False) dock_laserfocus_image_display.showTitleBar() dock_laserfocus_image_display.addWidget(self.imageDisplayWindow_focus.widget) dock_laserfocus_image_display.setStretch(x=100, y=100) - dock_laserfocus_liveController = dock.Dock('Focus Camera Controller', autoOrientation=False) + dock_laserfocus_liveController = dock.Dock("Focus Camera Controller", autoOrientation=False) dock_laserfocus_liveController.showTitleBar() dock_laserfocus_liveController.addWidget(self.liveControlWidget_focus_camera) dock_laserfocus_liveController.setStretch(x=100, y=100) dock_laserfocus_liveController.setFixedWidth(self.liveControlWidget_focus_camera.minimumSizeHint().width()) - dock_waveform = dock.Dock('Displacement Measurement', autoOrientation=False) + dock_waveform = dock.Dock("Displacement Measurement", autoOrientation=False) dock_waveform.showTitleBar() dock_waveform.addWidget(self.waveformDisplay) dock_waveform.setStretch(x=100, y=40) - dock_displayMeasurement = dock.Dock('Displacement Measurement Control', autoOrientation=False) + dock_displayMeasurement = dock.Dock("Displacement Measurement Control", autoOrientation=False) dock_displayMeasurement.showTitleBar() dock_displayMeasurement.addWidget(self.displacementMeasurementWidget) dock_displayMeasurement.setStretch(x=100, y=40) @@ -519,10 +686,12 @@ def setupImageDisplayTabs(self): laserfocus_dockArea = dock.DockArea() laserfocus_dockArea.addDock(dock_laserfocus_image_display) - laserfocus_dockArea.addDock(dock_laserfocus_liveController, 'right', relativeTo=dock_laserfocus_image_display) + laserfocus_dockArea.addDock( + dock_laserfocus_liveController, "right", relativeTo=dock_laserfocus_image_display + ) if SHOW_LEGACY_DISPLACEMENT_MEASUREMENT_WINDOWS: - laserfocus_dockArea.addDock(dock_waveform, 'bottom', relativeTo=dock_laserfocus_liveController) - laserfocus_dockArea.addDock(dock_displayMeasurement, 'bottom', relativeTo=dock_waveform) + laserfocus_dockArea.addDock(dock_waveform, "bottom", relativeTo=dock_laserfocus_liveController) + laserfocus_dockArea.addDock(dock_displayMeasurement, "bottom", relativeTo=dock_waveform) self.imageDisplayTabs.addTab(laserfocus_dockArea, "Laser-Based Focus") @@ -550,8 +719,8 @@ def setupCameraTabWidget(self): if USE_ZABER_EMISSION_FILTER_WHEEL or USE_OPTOSPIN_EMISSION_FILTER_WHEEL: self.cameraTabWidget.addTab(self.filterControllerWidget, "Emission Filter") if USE_SQUID_FILTERWHEEL: - self.cameraTabWidget.addTab(self.squidFilterWidget,"Squid Filter") - self.cameraTabWidget.addTab(self.cameraSettingWidget, 'Camera') + self.cameraTabWidget.addTab(self.squidFilterWidget, "Squid Filter") + self.cameraTabWidget.addTab(self.cameraSettingWidget, "Camera") self.cameraTabWidget.addTab(self.autofocusWidget, "Contrast AF") if SUPPORT_LASER_AUTOFOCUS: self.cameraTabWidget.addTab(self.laserAutofocusControlWidget, "Laser AF") @@ -601,24 +770,24 @@ def setupLayout(self): def setupSingleWindowLayout(self): main_dockArea = dock.DockArea() - dock_display = dock.Dock('Image Display', autoOrientation=False) + dock_display = dock.Dock("Image Display", autoOrientation=False) dock_display.showTitleBar() dock_display.addWidget(self.imageDisplayTabs) dock_display.setStretch(x=100, y=100) main_dockArea.addDock(dock_display) - self.dock_wellSelection = dock.Dock('Well Selector', autoOrientation=False) + self.dock_wellSelection = dock.Dock("Well Selector", autoOrientation=False) self.dock_wellSelection.showTitleBar() if not USE_NAPARI_WELL_SELECTION or self.live_only_mode: self.dock_wellSelection.addWidget(self.wellSelectionWidget) self.dock_wellSelection.setFixedHeight(self.dock_wellSelection.minimumSizeHint().height()) - main_dockArea.addDock(self.dock_wellSelection, 'bottom') + main_dockArea.addDock(self.dock_wellSelection, "bottom") - dock_controlPanel = dock.Dock('Controls', autoOrientation=False) + dock_controlPanel = dock.Dock("Controls", autoOrientation=False) dock_controlPanel.addWidget(self.centralWidget) dock_controlPanel.setStretch(x=1, y=None) dock_controlPanel.setFixedWidth(dock_controlPanel.minimumSizeHint().width()) - main_dockArea.addDock(dock_controlPanel, 'right') + main_dockArea.addDock(dock_controlPanel, "right") self.setCentralWidget(main_dockArea) desktopWidget = QDesktopWidget() @@ -650,15 +819,23 @@ def makeConnections(self): self.flexibleMultiPointWidget.signal_acquisition_started.connect(self.toggleAcquisitionStart) if ENABLE_STITCHER: self.flexibleMultiPointWidget.signal_stitcher_widget.connect(self.toggleStitcherWidget) - self.flexibleMultiPointWidget.signal_acquisition_channels.connect(self.stitcherWidget.updateRegistrationChannels) - self.flexibleMultiPointWidget.signal_stitcher_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.flexibleMultiPointWidget.signal_acquisition_channels.connect( + self.stitcherWidget.updateRegistrationChannels + ) + 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_stitcher_z_levels.connect(self.stitcherWidget.updateRegistrationZLevels) + self.wellplateMultiPointWidget.signal_acquisition_channels.connect( + self.stitcherWidget.updateRegistrationChannels + ) + 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) @@ -674,7 +851,7 @@ def makeConnections(self): self.objectivesWidget.signal_objective_changed.connect(self.flexibleMultiPointWidget.update_fov_positions) # TODO(imo): Fix position updates after removal of navigation controller self.movement_updater.position_after_move.connect(self.navigationViewer.draw_fov_current_location) - if WELLPLATE_FORMAT == 'glass slide': + if WELLPLATE_FORMAT == "glass slide": # TODO(imo): This well place logic is duplicated below in onWellPlateChanged. We should change it to only exist in 1 location. self.movement_updater.sent_after_stopped.connect(self.wellplateMultiPointWidget.set_live_scan_coordinates) self.is_live_scan_grid_on = True @@ -688,9 +865,15 @@ def makeConnections(self): if USE_NAPARI_FOR_LIVE_VIEW and not self.live_only_mode: self.multipointController.signal_current_configuration.connect(self.napariLiveWidget.set_microscope_mode) - self.autofocusController.image_to_display.connect(lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=True)) - self.streamHandler.image_to_display.connect(lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False)) - self.multipointController.image_to_display.connect(lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False)) + self.autofocusController.image_to_display.connect( + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=True) + ) + self.streamHandler.image_to_display.connect( + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False) + ) + self.multipointController.image_to_display.connect( + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False) + ) self.napariLiveWidget.signal_coordinates_clicked.connect(self.move_from_click_image) self.liveControlWidget.signal_live_configuration.connect(self.napariLiveWidget.set_live_configuration) @@ -711,7 +894,9 @@ def makeConnections(self): self.wellplateFormatWidget.signalWellplateSettings.connect(self.navigationViewer.update_wellplate_settings) self.wellplateFormatWidget.signalWellplateSettings.connect(self.scanCoordinates.update_wellplate_settings) self.wellplateFormatWidget.signalWellplateSettings.connect(self.wellSelectionWidget.onWellplateChanged) - self.wellplateFormatWidget.signalWellplateSettings.connect(lambda format_, *args: self.onWellplateChanged(format_)) + self.wellplateFormatWidget.signalWellplateSettings.connect( + lambda format_, *args: self.onWellplateChanged(format_) + ) self.wellSelectionWidget.signal_wellSelectedPos.connect(self.move_to_mm) if ENABLE_WELLPLATE_MULTIPOINT: @@ -719,16 +904,26 @@ def makeConnections(self): self.objectivesWidget.signal_objective_changed.connect(self.wellplateMultiPointWidget.update_coordinates) if SUPPORT_LASER_AUTOFOCUS: - self.liveControlWidget_focus_camera.signal_newExposureTime.connect(self.cameraSettingWidget_focus_camera.set_exposure_time) - self.liveControlWidget_focus_camera.signal_newAnalogGain.connect(self.cameraSettingWidget_focus_camera.set_analog_gain) + self.liveControlWidget_focus_camera.signal_newExposureTime.connect( + self.cameraSettingWidget_focus_camera.set_exposure_time + ) + self.liveControlWidget_focus_camera.signal_newAnalogGain.connect( + self.cameraSettingWidget_focus_camera.set_analog_gain + ) self.liveControlWidget_focus_camera.update_camera_settings() - self.streamHandler_focus_camera.signal_new_frame_received.connect(self.liveController_focus_camera.on_new_frame) + self.streamHandler_focus_camera.signal_new_frame_received.connect( + self.liveController_focus_camera.on_new_frame + ) self.streamHandler_focus_camera.image_to_display.connect(self.imageDisplayWindow_focus.display_image) - self.streamHandler_focus_camera.image_to_display.connect(self.displacementMeasurementController.update_measurement) + self.streamHandler_focus_camera.image_to_display.connect( + self.displacementMeasurementController.update_measurement + ) self.displacementMeasurementController.signal_plots.connect(self.waveformDisplay.plot) - self.displacementMeasurementController.signal_readings.connect(self.displacementMeasurementWidget.display_readings) + self.displacementMeasurementController.signal_readings.connect( + self.displacementMeasurementWidget.display_readings + ) self.laserAutofocusController.image_to_display.connect(self.imageDisplayWindow_focus.display_image) self.camera.set_callback(self.streamHandler.on_new_frame) @@ -746,31 +941,39 @@ def setup_movement_updater(self): def makeNapariConnections(self): """Initialize all Napari connections in one place""" self.napari_connections = { - 'napariLiveWidget': [], - 'napariMultiChannelWidget': [], - 'napariMosaicDisplayWidget': [] + "napariLiveWidget": [], + "napariMultiChannelWidget": [], + "napariMosaicDisplayWidget": [], } # Setup live view connections if USE_NAPARI_FOR_LIVE_VIEW and not self.live_only_mode: - self.napari_connections['napariLiveWidget'] = [ + self.napari_connections["napariLiveWidget"] = [ (self.multipointController.signal_current_configuration, self.napariLiveWidget.set_microscope_mode), - (self.autofocusController.image_to_display, - lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=True)), - (self.streamHandler.image_to_display, - lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False)), - (self.multipointController.image_to_display, - lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False)), + ( + self.autofocusController.image_to_display, + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=True), + ), + ( + self.streamHandler.image_to_display, + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False), + ), + ( + self.multipointController.image_to_display, + lambda image: self.napariLiveWidget.updateLiveLayer(image, from_autofocus=False), + ), (self.napariLiveWidget.signal_coordinates_clicked, self.move_from_click_image), - (self.liveControlWidget.signal_live_configuration, self.napariLiveWidget.set_live_configuration) + (self.liveControlWidget.signal_live_configuration, self.napariLiveWidget.set_live_configuration), ] if USE_NAPARI_FOR_LIVE_CONTROL: - self.napari_connections['napariLiveWidget'].extend([ - (self.napariLiveWidget.signal_newExposureTime, self.cameraSettingWidget.set_exposure_time), - (self.napariLiveWidget.signal_newAnalogGain, self.cameraSettingWidget.set_analog_gain), - (self.napariLiveWidget.signal_autoLevelSetting, self.imageDisplayWindow.set_autolevel) - ]) + self.napari_connections["napariLiveWidget"].extend( + [ + (self.napariLiveWidget.signal_newExposureTime, self.cameraSettingWidget.set_exposure_time), + (self.napariLiveWidget.signal_newAnalogGain, self.cameraSettingWidget.set_analog_gain), + (self.napariLiveWidget.signal_autoLevelSetting, self.imageDisplayWindow.set_autolevel), + ] + ) else: # Non-Napari display connections self.streamHandler.image_to_display.connect(self.imageDisplay.enqueue) @@ -783,46 +986,84 @@ def makeNapariConnections(self): if not self.live_only_mode: # Setup multichannel widget connections if USE_NAPARI_FOR_MULTIPOINT: - self.napari_connections['napariMultiChannelWidget'] = [ + self.napari_connections["napariMultiChannelWidget"] = [ (self.multipointController.napari_layers_init, self.napariMultiChannelWidget.initLayers), - (self.multipointController.napari_layers_update, self.napariMultiChannelWidget.updateLayers) + (self.multipointController.napari_layers_update, self.napariMultiChannelWidget.updateLayers), ] if ENABLE_FLEXIBLE_MULTIPOINT: - self.napari_connections['napariMultiChannelWidget'].extend([ - (self.flexibleMultiPointWidget.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), - (self.flexibleMultiPointWidget.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) - ]) + self.napari_connections["napariMultiChannelWidget"].extend( + [ + ( + self.flexibleMultiPointWidget.signal_acquisition_channels, + self.napariMultiChannelWidget.initChannels, + ), + ( + self.flexibleMultiPointWidget.signal_acquisition_shape, + self.napariMultiChannelWidget.initLayersShape, + ), + ] + ) if ENABLE_WELLPLATE_MULTIPOINT: - self.napari_connections['napariMultiChannelWidget'].extend([ - (self.wellplateMultiPointWidget.signal_acquisition_channels, self.napariMultiChannelWidget.initChannels), - (self.wellplateMultiPointWidget.signal_acquisition_shape, self.napariMultiChannelWidget.initLayersShape) - ]) + self.napari_connections["napariMultiChannelWidget"].extend( + [ + ( + 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) # Setup mosaic display widget connections if USE_NAPARI_FOR_MOSAIC_DISPLAY: - self.napari_connections['napariMosaicDisplayWidget'] = [ + self.napari_connections["napariMosaicDisplayWidget"] = [ (self.multipointController.napari_layers_update, self.napariMosaicDisplayWidget.updateMosaic), (self.napariMosaicDisplayWidget.signal_coordinates_clicked, self.move_from_click_mm), - (self.napariMosaicDisplayWidget.signal_clear_viewer, self.navigationViewer.clear_slide) + (self.napariMosaicDisplayWidget.signal_clear_viewer, self.navigationViewer.clear_slide), ] if ENABLE_FLEXIBLE_MULTIPOINT: - self.napari_connections['napariMosaicDisplayWidget'].extend([ - (self.flexibleMultiPointWidget.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), - (self.flexibleMultiPointWidget.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape) - ]) + self.napari_connections["napariMosaicDisplayWidget"].extend( + [ + ( + self.flexibleMultiPointWidget.signal_acquisition_channels, + self.napariMosaicDisplayWidget.initChannels, + ), + ( + self.flexibleMultiPointWidget.signal_acquisition_shape, + self.napariMosaicDisplayWidget.initLayersShape, + ), + ] + ) if ENABLE_WELLPLATE_MULTIPOINT: - self.napari_connections['napariMosaicDisplayWidget'].extend([ - (self.wellplateMultiPointWidget.signal_acquisition_channels, self.napariMosaicDisplayWidget.initChannels), - (self.wellplateMultiPointWidget.signal_acquisition_shape, self.napariMosaicDisplayWidget.initLayersShape), - (self.wellplateMultiPointWidget.signal_manual_shape_mode, self.napariMosaicDisplayWidget.enable_shape_drawing), - (self.napariMosaicDisplayWidget.signal_shape_drawn, self.wellplateMultiPointWidget.update_manual_shape) - ]) + self.napari_connections["napariMosaicDisplayWidget"].extend( + [ + ( + self.wellplateMultiPointWidget.signal_acquisition_channels, + self.napariMosaicDisplayWidget.initChannels, + ), + ( + self.wellplateMultiPointWidget.signal_acquisition_shape, + self.napariMosaicDisplayWidget.initLayersShape, + ), + ( + self.wellplateMultiPointWidget.signal_manual_shape_mode, + self.napariMosaicDisplayWidget.enable_shape_drawing, + ), + ( + self.napariMosaicDisplayWidget.signal_shape_drawn, + self.wellplateMultiPointWidget.update_manual_shape, + ), + ] + ) # Make initial connections self.updateNapariConnections() @@ -830,7 +1071,7 @@ def makeNapariConnections(self): def updateNapariConnections(self): # Update Napari connections based on performance mode. Live widget connections are preserved for widget_name, connections in self.napari_connections.items(): - if widget_name != 'napariLiveWidget': # Always keep the live widget connected + if widget_name != "napariLiveWidget": # Always keep the live widget connected widget = getattr(self, widget_name, None) if widget: for signal, slot in connections: @@ -874,12 +1115,20 @@ def openLedMatrixSettings(self): dialog.exec_() def onTabChanged(self, index): - is_flexible_acquisition = (index == self.recordTabWidget.indexOf(self.flexibleMultiPointWidget)) if ENABLE_FLEXIBLE_MULTIPOINT else False - is_wellplate_acquisition = (index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) if ENABLE_WELLPLATE_MULTIPOINT else False + is_flexible_acquisition = ( + (index == self.recordTabWidget.indexOf(self.flexibleMultiPointWidget)) + if ENABLE_FLEXIBLE_MULTIPOINT + else False + ) + is_wellplate_acquisition = ( + (index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) + if ENABLE_WELLPLATE_MULTIPOINT + else False + ) self.scanCoordinates.clear_regions() if is_wellplate_acquisition: - if self.wellplateMultiPointWidget.combobox_shape.currentText() == 'Manual': + if self.wellplateMultiPointWidget.combobox_shape.currentText() == "Manual": # trigger manual shape update if self.wellplateMultiPointWidget.shapes_mm: self.wellplateMultiPointWidget.update_manual_shape(self.wellplateMultiPointWidget.shapes_mm) @@ -890,7 +1139,7 @@ def onTabChanged(self, index): # trigger flexible regions update self.flexibleMultiPointWidget.update_fov_positions() - self.toggleWellSelector(is_wellplate_acquisition and self.wellSelectionWidget.format != 'glass slide') + self.toggleWellSelector(is_wellplate_acquisition and self.wellSelectionWidget.format != "glass slide") acquisitionWidget = self.recordTabWidget.widget(index) if ENABLE_STITCHER: self.toggleStitcherWidget(acquisitionWidget.checkbox_stitchOutput.isChecked()) @@ -907,7 +1156,7 @@ def resizeCurrentTab(self, tabWidget): def onDisplayTabChanged(self, index): current_widget = self.imageDisplayTabs.widget(index) - if hasattr(current_widget, 'viewer'): + if hasattr(current_widget, "viewer"): current_widget.activate() def onWellplateChanged(self, format_): @@ -915,55 +1164,73 @@ def onWellplateChanged(self, format_): format_ = format_.value() # TODO(imo): Not sure why glass slide is so special here? It seems like it's just a "1 well plate". Also why is the objective forced to inverted for all non-glass slide, and not inverted for glass slide? - if format_ == 'glass slide': + if format_ == "glass slide": self.toggleWellSelector(False) self.multipointController.inverted_objective = False - if not self.is_live_scan_grid_on: # connect live scan grid for glass slide - self.movement_updater.position_after_move.connect(self.wellplateMultiPointWidget.update_live_coordinates) + if not self.is_live_scan_grid_on: # connect live scan grid for glass slide + self.movement_updater.position_after_move.connect( + self.wellplateMultiPointWidget.update_live_coordinates + ) self.is_live_scan_grid_on = True self.log.debug("live scan grid connected.") self.setupSlidePositionController(is_for_wellplate=False) else: self.toggleWellSelector(True) self.multipointController.inverted_objective = True - if self.is_live_scan_grid_on: # disconnect live scan grid for wellplate - self.movement_updater.position_after_move.disconnect(self.wellplateMultiPointWidget.update_live_coordinates) + if self.is_live_scan_grid_on: # disconnect live scan grid for wellplate + self.movement_updater.position_after_move.disconnect( + self.wellplateMultiPointWidget.update_live_coordinates + ) self.is_live_scan_grid_on = False self.log.debug("live scan grid disconnected.") self.setupSlidePositionController(is_for_wellplate=True) # replace and reconnect new well selector - if format_ == '1536 well plate': + if format_ == "1536 well plate": self.replaceWellSelectionWidget(widgets.Well1536SelectionWidget()) elif isinstance(self.wellSelectionWidget, widgets.Well1536SelectionWidget): self.replaceWellSelectionWidget(widgets.WellSelectionWidget(format_, self.wellplateFormatWidget)) self.connectWellSelectionWidget() - if ENABLE_FLEXIBLE_MULTIPOINT: # clear regions + if ENABLE_FLEXIBLE_MULTIPOINT: # clear regions self.flexibleMultiPointWidget.clear_only_location_list() - if ENABLE_WELLPLATE_MULTIPOINT: # reset regions onto new wellplate with default size/shape + if ENABLE_WELLPLATE_MULTIPOINT: # reset regions onto new wellplate with default size/shape self.scanCoordinates.clear_regions() self.wellplateMultiPointWidget.set_default_scan_size() def setupSlidePositionController(self, is_for_wellplate): self.slidePositionController.setParent(None) self.slidePositionController.deleteLater() - self.slidePositionController = core.SlidePositionController(self.stage, self.liveController, is_for_wellplate=is_for_wellplate) + self.slidePositionController = core.SlidePositionController( + self.stage, self.liveController, is_for_wellplate=is_for_wellplate + ) self.connectSlidePositionController() self.navigationWidget.replace_slide_controller(self.slidePositionController) 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.navigationWidget.slot_slide_loading_position_reached + ) if ENABLE_FLEXIBLE_MULTIPOINT: - self.slidePositionController.signal_slide_loading_position_reached.connect(self.flexibleMultiPointWidget.disable_the_start_aquisition_button) + self.slidePositionController.signal_slide_loading_position_reached.connect( + self.flexibleMultiPointWidget.disable_the_start_aquisition_button + ) if ENABLE_WELLPLATE_MULTIPOINT: - self.slidePositionController.signal_slide_loading_position_reached.connect(self.wellplateMultiPointWidget.disable_the_start_aquisition_button) + self.slidePositionController.signal_slide_loading_position_reached.connect( + self.wellplateMultiPointWidget.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.navigationWidget.slot_slide_scanning_position_reached + ) if ENABLE_FLEXIBLE_MULTIPOINT: - self.slidePositionController.signal_slide_scanning_position_reached.connect(self.flexibleMultiPointWidget.enable_the_start_aquisition_button) + self.slidePositionController.signal_slide_scanning_position_reached.connect( + self.flexibleMultiPointWidget.enable_the_start_aquisition_button + ) if ENABLE_WELLPLATE_MULTIPOINT: - self.slidePositionController.signal_slide_scanning_position_reached.connect(self.wellplateMultiPointWidget.enable_the_start_aquisition_button) + self.slidePositionController.signal_slide_scanning_position_reached.connect( + self.wellplateMultiPointWidget.enable_the_start_aquisition_button + ) self.slidePositionController.signal_clear_slide.connect(self.navigationViewer.clear_slide) @@ -993,13 +1260,17 @@ def toggleWellSelector(self, show): def toggleAcquisitionStart(self, acquisition_started): if acquisition_started: self.log.info("STARTING ACQUISITION") - if self.is_live_scan_grid_on: # disconnect live scan grid during acquisition - self.movement_updater.position_after_move.disconnect(self.wellplateMultiPointWidget.update_live_coordinates) + if self.is_live_scan_grid_on: # disconnect live scan grid during acquisition + self.movement_updater.position_after_move.disconnect( + self.wellplateMultiPointWidget.update_live_coordinates + ) self.is_live_scan_grid_on = False else: self.log.info("FINISHED ACQUISITION") if not self.is_live_scan_grid_on: # reconnect live scan grid if was on before acqusition - self.movement_updater.position_after_move.connect(self.wellplateMultiPointWidget.update_live_coordinates) + self.movement_updater.position_after_move.connect( + self.wellplateMultiPointWidget.update_live_coordinates + ) self.is_live_scan_grid_on = True # click to move off during acquisition @@ -1015,8 +1286,12 @@ def toggleAcquisitionStart(self, acquisition_started): self.liveControlWidget.toggle_autolevel(not acquisition_started) # hide well selector during acquisition - is_wellplate_acquisition = (current_index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) if ENABLE_WELLPLATE_MULTIPOINT else False - if is_wellplate_acquisition and self.wellSelectionWidget.format != 'glass slide': + is_wellplate_acquisition = ( + (current_index == self.recordTabWidget.indexOf(self.wellplateMultiPointWidget)) + if ENABLE_WELLPLATE_MULTIPOINT + else False + ) + if is_wellplate_acquisition and self.wellSelectionWidget.format != "glass slide": self.toggleWellSelector(not acquisition_started) # display acquisition progress bar during acquisition @@ -1040,9 +1315,15 @@ def startStitcher(self, acquisition_path): registration_z_level = self.stitcherWidget.registrationZCombo.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" + 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.wellplateMultiPointWidget) 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, @@ -1051,7 +1332,7 @@ def startStitcher(self, acquisition_path): overlap_percent=overlap_percent, use_registration=use_registration, registration_channel=registration_channel, - registration_z_level=registration_z_level + registration_z_level=registration_z_level, ) self.stitcherWidget.setStitcherThread(self.stitcherThread) @@ -1075,7 +1356,9 @@ def move_from_click_image(self, click_x, click_y, image_width, image_height): delta_x = pixel_sign_x * pixel_size_um * click_x / 1000.0 delta_y = pixel_sign_y * pixel_size_um * click_y / 1000.0 - self.log.debug(f"Click to move enabled, click at {click_x=}, {click_y=} results in relative move of {delta_x=} [mm], {delta_y=} [mm]") + self.log.debug( + f"Click to move enabled, click at {click_x=}, {click_y=} results in relative move of {delta_x=} [mm], {delta_y=} [mm]" + ) self.stage.move_x(delta_x) self.stage.move_y(delta_y) else: @@ -1123,7 +1406,7 @@ def closeEvent(self, event): self.microcontroller.turn_off_all_pid() if ENABLE_CELLX: - for channel in [1,2,3,4]: + for channel in [1, 2, 3, 4]: self.cellx.turn_off(channel) self.cellx.close() diff --git a/software/control/microcontroller.py b/software/control/microcontroller.py index 684d83957..92690c721 100644 --- a/software/control/microcontroller.py +++ b/software/control/microcontroller.py @@ -14,10 +14,11 @@ # add user to the dialout group to avoid the need to use sudo -# done (7/20/2021) - remove the time.sleep in all functions (except for __init__) to +# done (7/20/2021) - remove the time.sleep in all functions (except for __init__) to # make all callable functions nonblocking, instead, user should check use is_busy() to # check if the microcontroller has finished executing the more recent command + # to do (7/28/2021) - add functions for configuring the stepper motors class CommandAborted(RuntimeError): """ @@ -27,6 +28,7 @@ class CommandAborted(RuntimeError): This does mean that if you don't check for command completion, you may miss these errors! """ + def __init__(self, command_id, reason): super().__init__(reason) self.command_id = command_id @@ -46,11 +48,13 @@ def response_bytes_for(command_id, execution_status, x, y, z, theta, joystick_bu - reserved (4 bytes) - CRC (1 byte) """ - crc_calculator = CrcCalculator(Crc8.CCITT,table_based=True) + crc_calculator = CrcCalculator(Crc8.CCITT, table_based=True) button_state = joystick_button << BIT_POS_JOYSTICK_BUTTON | switch << BIT_POS_SWITCH - reserved_state = 0 # This is just filler for the 4 reserved bytes. - response = bytearray(struct.pack(">BBiiiiBi", command_id, execution_status, x, y, z, theta, button_state, reserved_state)) + reserved_state = 0 # This is just filler for the 4 reserved bytes. + response = bytearray( + struct.pack(">BBiiiiBi", command_id, execution_status, x, y, z, theta, button_state, reserved_state) + ) response.append(crc_calculator.calculate_checksum(response)) return response @@ -110,15 +114,18 @@ def respond_to(self, write_bytes): self.x = 0 self.y = 0 - self.response_buffer.extend(SimSerial.response_bytes_for( - write_bytes[0], - CMD_EXECUTION_STATUS.COMPLETED_WITHOUT_ERRORS, - self.x, - self.y, - self.z, - self.theta, - self.joystick_button, - self.switch)) + self.response_buffer.extend( + SimSerial.response_bytes_for( + write_bytes[0], + CMD_EXECUTION_STATUS.COMPLETED_WITHOUT_ERRORS, + self.x, + self.y, + self.z, + self.theta, + self.joystick_button, + self.switch, + ) + ) self.in_waiting = len(self.response_buffer) @@ -148,22 +155,22 @@ class Microcontroller: LAST_COMMAND_ACK_TIMEOUT = 0.5 MAX_RETRY_COUNT = 5 - def __init__(self, version='Arduino Due', sn=None, existing_serial=None, reset_and_initialize=True): + def __init__(self, version="Arduino Due", sn=None, existing_serial=None, reset_and_initialize=True): self.log = squid.logging.get_logger(self.__class__.__name__) self.tx_buffer_length = MicrocontrollerDef.CMD_LENGTH self.rx_buffer_length = MicrocontrollerDef.MSG_LENGTH self._cmd_id = 0 - self._cmd_id_mcu = None # command id of mcu's last received command + self._cmd_id_mcu = None # command id of mcu's last received command self._cmd_execution_status = None self.mcu_cmd_execution_in_progress = False - self.x_pos = 0 # unit: microstep or encoder resolution - self.y_pos = 0 # unit: microstep or encoder resolution - self.z_pos = 0 # unit: microstep or encoder resolution - self.w_pos = 0 # unit: microstep or encoder resolution - self.theta_pos = 0 # unit: microstep or encoder resolution + self.x_pos = 0 # unit: microstep or encoder resolution + self.y_pos = 0 # unit: microstep or encoder resolution + self.z_pos = 0 # unit: microstep or encoder resolution + self.w_pos = 0 # unit: microstep or encoder resolution + self.theta_pos = 0 # unit: microstep or encoder resolution self.button_and_switch_state = 0 self.joystick_button_pressed = 0 # This is used to keep track of whether or not we should emit joystick events to the joystick listeners, @@ -183,7 +190,7 @@ def __init__(self, version='Arduino Due', sn=None, existing_serial=None, reset_a self.last_command_send_timestamp = time.time() self.last_command_aborted_error = None - self.crc_calculator = CrcCalculator(Crc8.CCITT,table_based=True) + self.crc_calculator = CrcCalculator(Crc8.CCITT, table_based=True) self.retry = 0 self.log.debug("connecting to controller based on " + version) @@ -191,23 +198,29 @@ def __init__(self, version='Arduino Due', sn=None, existing_serial=None, reset_a if existing_serial: self.serial = existing_serial else: - if version =='Arduino Due': - controller_ports = [p.device for p in serial.tools.list_ports.comports() if 'Arduino Due' == p.description] # autodetect - based on Deepak's code + if version == "Arduino Due": + controller_ports = [ + p.device for p in serial.tools.list_ports.comports() if "Arduino Due" == p.description + ] # autodetect - based on Deepak's code else: if sn is not None: - controller_ports = [ p.device for p in serial.tools.list_ports.comports() if sn == p.serial_number] + controller_ports = [p.device for p in serial.tools.list_ports.comports() if sn == p.serial_number] else: - if sys.platform == 'win32': - controller_ports = [ p.device for p in serial.tools.list_ports.comports() if p.manufacturer == 'Microsoft'] + if sys.platform == "win32": + controller_ports = [ + p.device for p in serial.tools.list_ports.comports() if p.manufacturer == "Microsoft" + ] else: - controller_ports = [ p.device for p in serial.tools.list_ports.comports() if p.manufacturer == 'Teensyduino'] + controller_ports = [ + p.device for p in serial.tools.list_ports.comports() if p.manufacturer == "Teensyduino" + ] if not controller_ports: raise IOError("no controller found") if len(controller_ports) > 1: self.log.warning("multiple controller found - using the first") - self.serial = serial.Serial(controller_ports[0],2000000) + self.serial = serial.Serial(controller_ports[0], 2000000) self.log.debug("controller connected") self.new_packet_callback_external = None @@ -228,7 +241,7 @@ def __init__(self, version='Arduino Due', sn=None, existing_serial=None, reset_a if USE_SQUID_FILTERWHEEL: self.configure_squidfilter() time.sleep(0.5) - + def close(self): self.terminate_reading_received_packet_thread = True self.thread_read_received_packet.join() @@ -248,7 +261,9 @@ def remove_joystick_button_listener(self, id_to_remove): self.log.debug(f"Removing joystick button listener id={id_to_remove} at idx={idx}") del self.joystick_event_listeners[idx] except ValueError as e: - self.log.warning(f"Asked to remove joystick button listener {id_to_remove}, but it is not a known listener id") + self.log.warning( + f"Asked to remove joystick button listener {id_to_remove}, but it is not a known listener id" + ) def enable_joystick(self, enabled: bool): self.joystick_listener_events_enabled = enabled @@ -275,7 +290,7 @@ def init_filter_wheel(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.INITFILTERWHEEL self.send_command(cmd) - print('initialize filter wheel') # debug + print("initialize filter wheel") # debug def turn_on_illumination(self): cmd = bytearray(self.tx_buffer_length) @@ -287,32 +302,32 @@ def turn_off_illumination(self): cmd[1] = CMD_SET.TURN_OFF_ILLUMINATION self.send_command(cmd) - def set_illumination(self,illumination_source,intensity): + def set_illumination(self, illumination_source, intensity): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_ILLUMINATION cmd[2] = illumination_source - cmd[3] = int((intensity/100)*65535) >> 8 - cmd[4] = int((intensity/100)*65535) & 0xff + cmd[3] = int((intensity / 100) * 65535) >> 8 + cmd[4] = int((intensity / 100) * 65535) & 0xFF self.send_command(cmd) - def set_illumination_led_matrix(self,illumination_source,r,g,b): + def set_illumination_led_matrix(self, illumination_source, r, g, b): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_ILLUMINATION_LED_MATRIX cmd[2] = illumination_source - cmd[3] = min(int(g*255),255) - cmd[4] = min(int(r*255),255) - cmd[5] = min(int(b*255),255) + cmd[3] = min(int(g * 255), 255) + cmd[4] = min(int(r * 255), 255) + cmd[5] = min(int(b * 255), 255) self.send_command(cmd) - def send_hardware_trigger(self,control_illumination=False,illumination_on_time_us=0,trigger_output_ch=0): + def send_hardware_trigger(self, control_illumination=False, illumination_on_time_us=0, trigger_output_ch=0): illumination_on_time_us = int(illumination_on_time_us) cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SEND_HARDWARE_TRIGGER - cmd[2] = (control_illumination<<7) + trigger_output_ch # MSB: whether illumination is controlled + cmd[2] = (control_illumination << 7) + trigger_output_ch # MSB: whether illumination is controlled cmd[3] = illumination_on_time_us >> 24 - cmd[4] = (illumination_on_time_us >> 16) & 0xff - cmd[5] = (illumination_on_time_us >> 8) & 0xff - cmd[6] = illumination_on_time_us & 0xff + cmd[4] = (illumination_on_time_us >> 16) & 0xFF + cmd[5] = (illumination_on_time_us >> 8) & 0xFF + cmd[6] = illumination_on_time_us & 0xFF self.send_command(cmd) def set_strobe_delay_us(self, strobe_delay_us, camera_channel=0): @@ -320,9 +335,9 @@ def set_strobe_delay_us(self, strobe_delay_us, camera_channel=0): cmd[1] = CMD_SET.SET_STROBE_DELAY cmd[2] = camera_channel cmd[3] = strobe_delay_us >> 24 - cmd[4] = (strobe_delay_us >> 16) & 0xff - cmd[5] = (strobe_delay_us >> 8) & 0xff - cmd[6] = strobe_delay_us & 0xff + cmd[4] = (strobe_delay_us >> 16) & 0xFF + cmd[5] = (strobe_delay_us >> 8) & 0xFF + cmd[6] = strobe_delay_us & 0xFF self.send_command(cmd) def set_axis_enable_disable(self, axis, status): @@ -336,16 +351,16 @@ def _move_axis_usteps(self, usteps, axis_command_code, axis_direction_sign): direction = axis_direction_sign * np.sign(usteps) n_microsteps_abs = abs(usteps) # if n_microsteps_abs exceed the max value that can be sent in one go - while n_microsteps_abs >= (2 ** 32) / 2: - n_microsteps_partial_abs = (2 ** 32) / 2 - 1 + while n_microsteps_abs >= (2**32) / 2: + n_microsteps_partial_abs = (2**32) / 2 - 1 n_microsteps_partial = direction * n_microsteps_partial_abs payload = self._int_to_payload(n_microsteps_partial, 4) cmd = bytearray(self.tx_buffer_length) cmd[1] = axis_command_code cmd[2] = payload >> 24 - cmd[3] = (payload >> 16) & 0xff - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + cmd[3] = (payload >> 16) & 0xFF + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF # TODO(imo): Since this issues multiple commands, there's no way to check for and abort failed # ones mid-move. self.send_command(cmd) @@ -355,122 +370,122 @@ def _move_axis_usteps(self, usteps, axis_command_code, axis_direction_sign): cmd = bytearray(self.tx_buffer_length) cmd[1] = axis_command_code cmd[2] = payload >> 24 - cmd[3] = (payload >> 16) & 0xff - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + cmd[3] = (payload >> 16) & 0xFF + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF self.send_command(cmd) - def move_x_usteps(self,usteps): + def move_x_usteps(self, usteps): self._move_axis_usteps(usteps, CMD_SET.MOVE_X, STAGE_MOVEMENT_SIGN_X) - def move_x_to_usteps(self,usteps): - payload = self._int_to_payload(usteps,4) + def move_x_to_usteps(self, usteps): + payload = self._int_to_payload(usteps, 4) cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.MOVETO_X cmd[2] = payload >> 24 - cmd[3] = (payload >> 16) & 0xff - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + cmd[3] = (payload >> 16) & 0xFF + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF self.send_command(cmd) - def move_y_usteps(self,usteps): + def move_y_usteps(self, usteps): self._move_axis_usteps(usteps, CMD_SET.MOVE_Y, STAGE_MOVEMENT_SIGN_Y) - - def move_y_to_usteps(self,usteps): - payload = self._int_to_payload(usteps,4) + + def move_y_to_usteps(self, usteps): + payload = self._int_to_payload(usteps, 4) cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.MOVETO_Y cmd[2] = payload >> 24 - cmd[3] = (payload >> 16) & 0xff - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + cmd[3] = (payload >> 16) & 0xFF + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF self.send_command(cmd) - def move_z_usteps(self,usteps): + def move_z_usteps(self, usteps): self._move_axis_usteps(usteps, CMD_SET.MOVE_Z, STAGE_MOVEMENT_SIGN_Z) - def move_z_to_usteps(self,usteps): - payload = self._int_to_payload(usteps,4) + def move_z_to_usteps(self, usteps): + payload = self._int_to_payload(usteps, 4) cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.MOVETO_Z cmd[2] = payload >> 24 - cmd[3] = (payload >> 16) & 0xff - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + cmd[3] = (payload >> 16) & 0xFF + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF self.send_command(cmd) - def move_theta_usteps(self,usteps): + def move_theta_usteps(self, usteps): self._move_axis_usteps(usteps, CMD_SET.MOVE_THETA, STAGE_MOVEMENT_SIGN_THETA) - - def move_w_usteps(self,usteps): + + def move_w_usteps(self, usteps): self._move_axis_usteps(usteps, CMD_SET.MOVE_W, STAGE_MOVEMENT_SIGN_W) - def set_off_set_velocity_x(self,off_set_velocity): + def set_off_set_velocity_x(self, off_set_velocity): # off_set_velocity is in mm/s cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_OFFSET_VELOCITY cmd[2] = AXIS.X - off_set_velocity = off_set_velocity*1000000 - payload = self._int_to_payload(off_set_velocity,4) + off_set_velocity = off_set_velocity * 1000000 + payload = self._int_to_payload(off_set_velocity, 4) cmd[3] = payload >> 24 - cmd[4] = (payload >> 16) & 0xff - cmd[5] = (payload >> 8) & 0xff - cmd[6] = payload & 0xff + cmd[4] = (payload >> 16) & 0xFF + cmd[5] = (payload >> 8) & 0xFF + cmd[6] = payload & 0xFF self.send_command(cmd) - def set_off_set_velocity_y(self,off_set_velocity): + def set_off_set_velocity_y(self, off_set_velocity): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_OFFSET_VELOCITY cmd[2] = AXIS.Y - off_set_velocity = off_set_velocity*1000000 - payload = self._int_to_payload(off_set_velocity,4) + off_set_velocity = off_set_velocity * 1000000 + payload = self._int_to_payload(off_set_velocity, 4) cmd[3] = payload >> 24 - cmd[4] = (payload >> 16) & 0xff - cmd[5] = (payload >> 8) & 0xff - cmd[6] = payload & 0xff + cmd[4] = (payload >> 16) & 0xFF + cmd[5] = (payload >> 8) & 0xFF + cmd[6] = payload & 0xFF self.send_command(cmd) def home_x(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = AXIS.X - cmd[3] = int((STAGE_MOVEMENT_SIGN_X+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_X + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def home_y(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = AXIS.Y - cmd[3] = int((STAGE_MOVEMENT_SIGN_Y+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_Y + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def home_z(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = AXIS.Z - cmd[3] = int((STAGE_MOVEMENT_SIGN_Z+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_Z + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def home_theta(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = 3 - cmd[3] = int((STAGE_MOVEMENT_SIGN_THETA+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_THETA + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def home_xy(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = AXIS.XY - cmd[3] = int((STAGE_MOVEMENT_SIGN_X+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 - cmd[4] = int((STAGE_MOVEMENT_SIGN_Y+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_X + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[4] = int((STAGE_MOVEMENT_SIGN_Y + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def home_w(self): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.HOME_OR_ZERO cmd[2] = AXIS.W - cmd[3] = int((STAGE_MOVEMENT_SIGN_W+1)/2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 + cmd[3] = int((STAGE_MOVEMENT_SIGN_W + 1) / 2) # "move backward" if SIGN is 1, "move forward" if SIGN is -1 self.send_command(cmd) def zero_x(self): @@ -513,9 +528,9 @@ def configure_stage_pid(self, axis, transitions_per_revolution, flip_direction=F cmd[1] = CMD_SET.CONFIGURE_STAGE_PID cmd[2] = axis cmd[3] = int(flip_direction) - payload = self._int_to_payload(transitions_per_revolution,2) - cmd[4] = (payload >> 8) & 0xff - cmd[5] = payload & 0xff + payload = self._int_to_payload(transitions_per_revolution, 2) + cmd[4] = (payload >> 8) & 0xFF + cmd[5] = payload & 0xFF self.send_command(cmd) def turn_on_stage_pid(self, axis): @@ -540,25 +555,25 @@ def set_pid_arguments(self, axis, pid_p, pid_i, pid_d): cmd[1] = CMD_SET.SET_PID_ARGUMENTS cmd[2] = int(axis) - cmd[3] = (int(pid_p) >> 8) & 0xff - cmd[4] = int(pid_p) & 0xff + cmd[3] = (int(pid_p) >> 8) & 0xFF + cmd[4] = int(pid_p) & 0xFF cmd[5] = int(pid_i) cmd[6] = int(pid_d) self.send_command(cmd) - def set_lim(self,limit_code,usteps): + def set_lim(self, limit_code, usteps): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_LIM cmd[2] = limit_code - payload = self._int_to_payload(usteps,4) + payload = self._int_to_payload(usteps, 4) cmd[3] = payload >> 24 - cmd[4] = (payload >> 16) & 0xff - cmd[5] = (payload >> 8) & 0xff - cmd[6] = payload & 0xff + cmd[4] = (payload >> 16) & 0xFF + cmd[5] = (payload >> 8) & 0xFF + cmd[6] = payload & 0xFF self.send_command(cmd) - def set_limit_switch_polarity(self,axis,polarity): + def set_limit_switch_polarity(self, axis, polarity): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_LIM_SWITCH_POLARITY cmd[2] = axis @@ -572,11 +587,11 @@ def set_home_safety_margin(self, axis, margin): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_HOME_SAFETY_MERGIN cmd[2] = axis - cmd[3] = (margin >> 8) & 0xff - cmd[4] = (margin) & 0xff + cmd[3] = (margin >> 8) & 0xFF + cmd[4] = (margin) & 0xFF self.send_command(cmd) - def configure_motor_driver(self,axis,microstepping,current_rms,I_hold): + def configure_motor_driver(self, axis, microstepping, current_rms, I_hold): # current_rms in mA # I_hold 0.0-1.0 cmd = bytearray(self.tx_buffer_length) @@ -585,63 +600,63 @@ def configure_motor_driver(self,axis,microstepping,current_rms,I_hold): if microstepping == 1: cmd[3] = 0 elif microstepping == 256: - cmd[3] = 255 # max of uint8 is 255 - will be changed to 255 after received by the MCU + cmd[3] = 255 # max of uint8 is 255 - will be changed to 255 after received by the MCU else: cmd[3] = microstepping cmd[4] = current_rms >> 8 - cmd[5] = current_rms & 0xff - cmd[6] = int(I_hold*255) + cmd[5] = current_rms & 0xFF + cmd[6] = int(I_hold * 255) self.send_command(cmd) - def set_max_velocity_acceleration(self,axis,velocity,acceleration): + def set_max_velocity_acceleration(self, axis, velocity, acceleration): # velocity: max 65535/100 mm/s # acceleration: max 65535/10 mm/s^2 cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_MAX_VELOCITY_ACCELERATION cmd[2] = axis - cmd[3] = int(velocity*100) >> 8 - cmd[4] = int(velocity*100) & 0xff - cmd[5] = int(acceleration*10) >> 8 - cmd[6] = int(acceleration*10) & 0xff + cmd[3] = int(velocity * 100) >> 8 + cmd[4] = int(velocity * 100) & 0xFF + cmd[5] = int(acceleration * 10) >> 8 + cmd[6] = int(acceleration * 10) & 0xFF self.send_command(cmd) - def set_leadscrew_pitch(self,axis,pitch_mm): + def set_leadscrew_pitch(self, axis, pitch_mm): # pitch: max 65535/1000 = 65.535 (mm) cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_LEAD_SCREW_PITCH cmd[2] = axis - cmd[3] = int(pitch_mm*1000) >> 8 - cmd[4] = int(pitch_mm*1000) & 0xff + cmd[3] = int(pitch_mm * 1000) >> 8 + cmd[4] = int(pitch_mm * 1000) & 0xFF self.send_command(cmd) def configure_actuators(self): # lead screw pitch - self.set_leadscrew_pitch(AXIS.X,SCREW_PITCH_X_MM) + self.set_leadscrew_pitch(AXIS.X, SCREW_PITCH_X_MM) self.wait_till_operation_is_completed() - self.set_leadscrew_pitch(AXIS.Y,SCREW_PITCH_Y_MM) + self.set_leadscrew_pitch(AXIS.Y, SCREW_PITCH_Y_MM) self.wait_till_operation_is_completed() - self.set_leadscrew_pitch(AXIS.Z,SCREW_PITCH_Z_MM) + self.set_leadscrew_pitch(AXIS.Z, SCREW_PITCH_Z_MM) self.wait_till_operation_is_completed() # stepper driver (microstepping,rms current and I_hold) - self.configure_motor_driver(AXIS.X,MICROSTEPPING_DEFAULT_X,X_MOTOR_RMS_CURRENT_mA,X_MOTOR_I_HOLD) + self.configure_motor_driver(AXIS.X, MICROSTEPPING_DEFAULT_X, X_MOTOR_RMS_CURRENT_mA, X_MOTOR_I_HOLD) self.wait_till_operation_is_completed() - self.configure_motor_driver(AXIS.Y,MICROSTEPPING_DEFAULT_Y,Y_MOTOR_RMS_CURRENT_mA,Y_MOTOR_I_HOLD) + self.configure_motor_driver(AXIS.Y, MICROSTEPPING_DEFAULT_Y, Y_MOTOR_RMS_CURRENT_mA, Y_MOTOR_I_HOLD) self.wait_till_operation_is_completed() - self.configure_motor_driver(AXIS.Z,MICROSTEPPING_DEFAULT_Z,Z_MOTOR_RMS_CURRENT_mA,Z_MOTOR_I_HOLD) + self.configure_motor_driver(AXIS.Z, MICROSTEPPING_DEFAULT_Z, Z_MOTOR_RMS_CURRENT_mA, Z_MOTOR_I_HOLD) self.wait_till_operation_is_completed() # max velocity and acceleration - self.set_max_velocity_acceleration(AXIS.X,MAX_VELOCITY_X_mm,MAX_ACCELERATION_X_mm) + self.set_max_velocity_acceleration(AXIS.X, MAX_VELOCITY_X_mm, MAX_ACCELERATION_X_mm) self.wait_till_operation_is_completed() - self.set_max_velocity_acceleration(AXIS.Y,MAX_VELOCITY_Y_mm,MAX_ACCELERATION_Y_mm) + self.set_max_velocity_acceleration(AXIS.Y, MAX_VELOCITY_Y_mm, MAX_ACCELERATION_Y_mm) self.wait_till_operation_is_completed() - self.set_max_velocity_acceleration(AXIS.Z,MAX_VELOCITY_Z_mm,MAX_ACCELERATION_Z_mm) + self.set_max_velocity_acceleration(AXIS.Z, MAX_VELOCITY_Z_mm, MAX_ACCELERATION_Z_mm) self.wait_till_operation_is_completed() # home switch - self.set_limit_switch_polarity(AXIS.X,X_HOME_SWITCH_POLARITY) + self.set_limit_switch_polarity(AXIS.X, X_HOME_SWITCH_POLARITY) self.wait_till_operation_is_completed() - self.set_limit_switch_polarity(AXIS.Y,Y_HOME_SWITCH_POLARITY) + self.set_limit_switch_polarity(AXIS.Y, Y_HOME_SWITCH_POLARITY) self.wait_till_operation_is_completed() - self.set_limit_switch_polarity(AXIS.Z,Z_HOME_SWITCH_POLARITY) + self.set_limit_switch_polarity(AXIS.Z, Z_HOME_SWITCH_POLARITY) self.wait_till_operation_is_completed() # home safety margin self.set_home_safety_margin(AXIS.X, int(X_HOME_SAFETY_MARGIN_UM)) @@ -652,11 +667,11 @@ def configure_actuators(self): self.wait_till_operation_is_completed() def configure_squidfilter(self): - self.set_leadscrew_pitch(AXIS.W,SCREW_PITCH_W_MM) + self.set_leadscrew_pitch(AXIS.W, SCREW_PITCH_W_MM) self.wait_till_operation_is_completed() - self.configure_motor_driver(AXIS.W,MICROSTEPPING_DEFAULT_W,W_MOTOR_RMS_CURRENT_mA,W_MOTOR_I_HOLD) + self.configure_motor_driver(AXIS.W, MICROSTEPPING_DEFAULT_W, W_MOTOR_RMS_CURRENT_mA, W_MOTOR_I_HOLD) self.wait_till_operation_is_completed() - self.set_max_velocity_acceleration(AXIS.W,MAX_VELOCITY_W_mm,MAX_ACCELERATION_W_mm) + self.set_max_velocity_acceleration(AXIS.W, MAX_VELOCITY_W_mm, MAX_ACCELERATION_W_mm) self.wait_till_operation_is_completed() def ack_joystick_button_pressed(self): @@ -664,12 +679,12 @@ def ack_joystick_button_pressed(self): cmd[1] = CMD_SET.ACK_JOYSTICK_BUTTON_PRESSED self.send_command(cmd) - def analog_write_onboard_DAC(self,dac,value): + def analog_write_onboard_DAC(self, dac, value): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.ANALOG_WRITE_ONBOARD_DAC cmd[2] = dac - cmd[3] = (value >> 8) & 0xff - cmd[4] = value & 0xff + cmd[3] = (value >> 8) & 0xFF + cmd[4] = value & 0xFF self.send_command(cmd) def set_piezo_um(self, z_piezo_um): @@ -684,7 +699,7 @@ def configure_dac80508_refdiv_and_gain(self, div, gains): cmd[3] = gains self.send_command(cmd) - def set_pin_level(self,pin,level): + def set_pin_level(self, pin, level): cmd = bytearray(self.tx_buffer_length) cmd[1] = CMD_SET.SET_PIN_LEVEL cmd[2] = pin @@ -692,13 +707,13 @@ def set_pin_level(self,pin,level): self.send_command(cmd) def turn_on_AF_laser(self): - self.set_pin_level(MCU_PINS.AF_LASER,1) + self.set_pin_level(MCU_PINS.AF_LASER, 1) def turn_off_AF_laser(self): - self.set_pin_level(MCU_PINS.AF_LASER,0) + self.set_pin_level(MCU_PINS.AF_LASER, 0) - def send_command(self,command): - self._cmd_id = (self._cmd_id + 1)%256 + def send_command(self, command): + self._cmd_id = (self._cmd_id + 1) % 256 command[0] = self._cmd_id command[-1] = self.crc_calculator.calculate_checksum(command[:-1]) self.serial.write(command) @@ -708,7 +723,9 @@ def send_command(self,command): self.retry = 0 if self.last_command_aborted_error is not None: - self.log.warning("Last command aborted and not cleared before new command sent!", self.last_command_aborted_error) + self.log.warning( + "Last command aborted and not cleared before new command sent!", self.last_command_aborted_error + ) self.last_command_aborted_error = None def abort_current_command(self, reason): @@ -737,25 +754,25 @@ def resend_last_command(self): def read_received_packet(self): while self.terminate_reading_received_packet_thread == False: # wait to receive data - if self.serial.in_waiting==0 or self.serial.in_waiting % self.rx_buffer_length != 0: + if self.serial.in_waiting == 0 or self.serial.in_waiting % self.rx_buffer_length != 0: # Sleep a negligible amount of time just to give other threads time to run. Otherwise, # we run the rise of spinning forever here and not letting progress happen elsewhere. time.sleep(0.0001) continue - + # get rid of old data num_bytes_in_rx_buffer = self.serial.in_waiting if num_bytes_in_rx_buffer > self.rx_buffer_length: - for i in range(num_bytes_in_rx_buffer-self.rx_buffer_length): + for i in range(num_bytes_in_rx_buffer - self.rx_buffer_length): self.serial.read() - + # read the buffer - msg=[] + msg = [] for i in range(self.rx_buffer_length): msg.append(ord(self.serial.read())) # parse the message - ''' + """ - command ID (1 byte) - execution status (1 byte) - X pos (4 bytes) @@ -765,30 +782,52 @@ def read_received_packet(self): - buttons and switches (1 byte) - reserved (4 bytes) - CRC (1 byte) - ''' + """ self._cmd_id_mcu = msg[0] self._cmd_execution_status = msg[1] - if (self._cmd_id_mcu == self._cmd_id) and (self._cmd_execution_status == CMD_EXECUTION_STATUS.COMPLETED_WITHOUT_ERRORS): + if (self._cmd_id_mcu == self._cmd_id) and ( + self._cmd_execution_status == CMD_EXECUTION_STATUS.COMPLETED_WITHOUT_ERRORS + ): if self.mcu_cmd_execution_in_progress: self.mcu_cmd_execution_in_progress = False self.log.debug("mcu command " + str(self._cmd_id) + " complete") - elif self.mcu_cmd_execution_in_progress and self._cmd_id_mcu != self._cmd_id and time.time() - self.last_command_send_timestamp > self.LAST_COMMAND_ACK_TIMEOUT and self.last_command is not None: + elif ( + self.mcu_cmd_execution_in_progress + and self._cmd_id_mcu != self._cmd_id + and time.time() - self.last_command_send_timestamp > self.LAST_COMMAND_ACK_TIMEOUT + and self.last_command is not None + ): if self.retry > self.MAX_RETRY_COUNT: - self.abort_current_command(reason=f"Command timed out without an ack after {self.LAST_COMMAND_ACK_TIMEOUT} [s], and {self.retry} retries") + self.abort_current_command( + reason=f"Command timed out without an ack after {self.LAST_COMMAND_ACK_TIMEOUT} [s], and {self.retry} retries" + ) else: - self.log.debug(f"command timed out without an ack after {self.LAST_COMMAND_ACK_TIMEOUT} [s], resending command") + self.log.debug( + f"command timed out without an ack after {self.LAST_COMMAND_ACK_TIMEOUT} [s], resending command" + ) self.resend_last_command() - elif self.mcu_cmd_execution_in_progress and self._cmd_execution_status == CMD_EXECUTION_STATUS.CMD_CHECKSUM_ERROR: + elif ( + self.mcu_cmd_execution_in_progress + and self._cmd_execution_status == CMD_EXECUTION_STATUS.CMD_CHECKSUM_ERROR + ): if self.retry > self.MAX_RETRY_COUNT: self.abort_current_command(reason=f"Checksum error and 10 retries for {self._cmd_id}") else: self.log.error("cmd checksum error, resending command") self.resend_last_command() - self.x_pos = self._payload_to_int(msg[2:6],MicrocontrollerDef.N_BYTES_POS) # unit: microstep or encoder resolution - self.y_pos = self._payload_to_int(msg[6:10],MicrocontrollerDef.N_BYTES_POS) # unit: microstep or encoder resolution - self.z_pos = self._payload_to_int(msg[10:14],MicrocontrollerDef.N_BYTES_POS) # unit: microstep or encoder resolution - self.theta_pos = self._payload_to_int(msg[14:18],MicrocontrollerDef.N_BYTES_POS) # unit: microstep or encoder resolution + self.x_pos = self._payload_to_int( + msg[2:6], MicrocontrollerDef.N_BYTES_POS + ) # unit: microstep or encoder resolution + self.y_pos = self._payload_to_int( + msg[6:10], MicrocontrollerDef.N_BYTES_POS + ) # unit: microstep or encoder resolution + self.z_pos = self._payload_to_int( + msg[10:14], MicrocontrollerDef.N_BYTES_POS + ) # unit: microstep or encoder resolution + self.theta_pos = self._payload_to_int( + msg[14:18], MicrocontrollerDef.N_BYTES_POS + ) # unit: microstep or encoder resolution self.button_and_switch_state = msg[18] # joystick button @@ -796,7 +835,7 @@ def read_received_packet(self): joystick_button_pressed = tmp > 0 if self.joystick_button_pressed != joystick_button_pressed: if self.joystick_listener_events_enabled: - for (_, listener_fn) in self.joystick_event_listeners: + for _, listener_fn in self.joystick_event_listeners: listener_fn(joystick_button_pressed) # The microcontroller wants us to send an ack back only when we see a False -> True @@ -821,7 +860,7 @@ def get_button_and_switch_state(self): def is_busy(self): return self.mcu_cmd_execution_in_progress - def set_callback(self,function): + def set_callback(self, function): self.new_packet_callback_external = function def wait_till_operation_is_completed(self, timeout_limit_s=5): @@ -839,22 +878,22 @@ def wait_till_operation_is_completed(self, timeout_limit_s=5): raise self.last_command_aborted_error @staticmethod - def _int_to_payload(signed_int,number_of_bytes): + def _int_to_payload(signed_int, number_of_bytes): if signed_int >= 0: payload = signed_int else: - payload = 2**(8*number_of_bytes) + signed_int # find two's completement + payload = 2 ** (8 * number_of_bytes) + signed_int # find two's completement return payload @staticmethod - def _payload_to_int(payload,number_of_bytes): + def _payload_to_int(payload, number_of_bytes): signed = 0 for i in range(number_of_bytes): - signed = signed + int(payload[i])*(256**(number_of_bytes-1-i)) - if signed >= 256**number_of_bytes/2: + signed = signed + int(payload[i]) * (256 ** (number_of_bytes - 1 - i)) + if signed >= 256**number_of_bytes / 2: signed = signed - 256**number_of_bytes return signed - + def set_dac80508_scaling_factor_for_illumination(self, illumination_intensity_factor): if illumination_intensity_factor > 1: illumination_intensity_factor = 1 diff --git a/software/control/microscope.py b/software/control/microscope.py index ad206081d..7bfe73137 100644 --- a/software/control/microscope.py +++ b/software/control/microscope.py @@ -45,7 +45,7 @@ def initialize_camera(self, is_simulation): else: sn_camera_main = camera.get_sn_by_model(MAIN_CAMERA_MODEL) self.camera = camera.Camera(sn=sn_camera_main, rotate_image_angle=ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE) - + self.camera.open() self.camera.set_pixel_format(DEFAULT_PIXEL_FORMAT) self.camera.set_software_triggered_acquisition() @@ -59,22 +59,26 @@ def initialize_microcontroller(self, is_simulation): self.home_x_and_y_separately = False def initialize_core_components(self): - self.configurationManager = core.ConfigurationManager(filename='./channel_configurations.xml') + self.configurationManager = core.ConfigurationManager(filename="./channel_configurations.xml") self.objectiveStore = core.ObjectiveStore() - self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP/100) + self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) self.liveController = core.LiveController(self.camera, self.microcontroller, self.configurationManager, self) - self.autofocusController = core.AutoFocusController(self.camera, self.stage, self.liveController, self.microcontroller) - self.slidePositionController = core.SlidePositionController(self.stage,self.liveController) + self.autofocusController = core.AutoFocusController( + self.camera, self.stage, self.liveController, self.microcontroller + ) + self.slidePositionController = core.SlidePositionController(self.stage, self.liveController) def initialize_peripherals(self): if USE_ZABER_EMISSION_FILTER_WHEEL: - self.emission_filter_wheel = serial_peripherals.FilterController(FILTER_CONTROLLER_SERIAL_NUMBER, 115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE) + self.emission_filter_wheel = serial_peripherals.FilterController( + FILTER_CONTROLLER_SERIAL_NUMBER, 115200, 8, serial.PARITY_NONE, serial.STOPBITS_ONE + ) self.emission_filter_wheel.start_homing() elif USE_OPTOSPIN_EMISSION_FILTER_WHEEL: self.emission_filter_wheel = serial_peripherals.Optospin(SN=FILTER_CONTROLLER_SERIAL_NUMBER) self.emission_filter_wheel.set_speed(OPTOSPIN_EMISSION_FILTER_WHEEL_SPEED_HZ) - def set_channel(self,channel): + def set_channel(self, channel): self.liveController.set_channel(channel) def acquire_image(self): @@ -84,17 +88,19 @@ def acquire_image(self): self.waitForMicrocontroller() self.camera.send_trigger() elif self.liveController.trigger_mode == TriggerMode.HARDWARE: - self.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=self.camera.exposure_time*1000) - + self.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=self.camera.exposure_time * 1000 + ) + # read a frame from camera image = self.camera.read_frame() if image is None: - print('self.camera.read_frame() returned None') - + print("self.camera.read_frame() returned None") + # tunr off the illumination if using software trigger if self.liveController.trigger_mode == TriggerMode.SOFTWARE: self.liveController.turn_off_illumination() - + return image def home_xyz(self): @@ -106,16 +112,16 @@ def home_xyz(self): self.stage.home(x=True, y=False, z=False, theta=False) self.slidePositionController.homing_done = True - def move_x(self,distance,blocking=True): + def move_x(self, distance, blocking=True): self.stage.move_x(distance, blocking=blocking) - def move_y(self,distance,blocking=True): + def move_y(self, distance, blocking=True): self.stage.move_y(distance, blocking=blocking) - def move_x_to(self,position,blocking=True): + def move_x_to(self, position, blocking=True): self.stage.move_x_to(position, blocking=blocking) - def move_y_to(self,position,blocking=True): + def move_y_to(self, position, blocking=True): self.stage.move_y_to(position, blocking=blocking) def get_x(self): @@ -127,12 +133,14 @@ def get_y(self): def get_z(self): return self.stage.get_pos().z_mm - def move_z_to(self,z_mm,blocking=True): + def move_z_to(self, z_mm, blocking=True): self.stage.move_z_to(z_mm, blocking=blocking) clear_backlash = z_mm >= self.stage.get_pos().z_mm # clear backlash if moving backward in open loop mode if blocking and clear_backlash: - distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units(max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP)) + distance_to_clear_backlash = self.stage.get_config().Z_AXIS.convert_to_real_units( + max(160, 20 * self.stage.get_config().Z_AXIS.MICROSTEPS_PER_STEP) + ) self.stage.move_z(-distance_to_clear_backlash) self.stage.move_z(distance_to_clear_backlash) @@ -167,16 +175,21 @@ class LightSourceType(Enum): VersaLase = 4 SCI = 5 + class IntensityControlMode(Enum): SquidControllerDAC = 0 Software = 1 + class ShutterControlMode(Enum): TTL = 0 Software = 1 -class IlluminationController(): - def __init__(self, microcontroller, intensity_control_mode, shutter_control_mode, light_source_type=None, light_source=None): + +class IlluminationController: + def __init__( + self, microcontroller, intensity_control_mode, shutter_control_mode, light_source_type=None, light_source=None + ): self.microcontroller = microcontroller self.intensity_control_mode = intensity_control_mode self.shutter_control_mode = shutter_control_mode @@ -194,9 +207,9 @@ def __init__(self, microcontroller, intensity_control_mode, shutter_control_mode 640: 13, 730: 15, 735: 15, - 750: 15 + 750: 15, } - + self.channel_mappings_software = {} self.is_on = {} self.intensity_settings = {} @@ -245,7 +258,7 @@ def get_intensity(self, channel): power = self.light_source.get_intensity(self.channel_mappings_software[channel]) intensity = power / self.pmax * 100 self.intensity_settings[channel] = intensity - return intensity # 0 - 100 + return intensity # 0 - 100 def turn_on_illumination(self, channel=None): if channel is None: @@ -254,8 +267,8 @@ def turn_on_illumination(self, channel=None): if self.shutter_control_mode == ShutterControlMode.Software: self.light_source.set_shutter_state(self.channel_mappings_software[channel], on=True) elif self.shutter_control_mode == ShutterControlMode.TTL: - print('TTL!!') - #self.microcontroller.set_illumination(self.channel_mappings_TTL[channel], self.intensity_settings[channel]) + print("TTL!!") + # self.microcontroller.set_illumination(self.channel_mappings_TTL[channel], self.intensity_settings[channel]) self.microcontroller.turn_on_illumination() self.is_on[channel] = True @@ -286,4 +299,4 @@ def get_shutter_state(self): return self.is_on def close(self): - self.light_source.shut_down() \ No newline at end of file + self.light_source.shut_down() diff --git a/software/control/multipoint_custom_script_entry.py b/software/control/multipoint_custom_script_entry.py index 41c61be9a..354835661 100644 --- a/software/control/multipoint_custom_script_entry.py +++ b/software/control/multipoint_custom_script_entry.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -15,70 +16,107 @@ from control._def import * import control.utils as utils -def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coordinate_id,coordiante_name,i,j): - - print( 'in custom script; t ' + str(multiPointWorker.time_point) + ', location ' + coordiante_name + ': ' + str(i) + '_' + str(j) ) + +def multipoint_custom_script_entry(multiPointWorker, time_point, current_path, coordinate_id, coordiante_name, i, j): + + print( + "in custom script; t " + + str(multiPointWorker.time_point) + + ", location " + + coordiante_name + + ": " + + str(i) + + "_" + + str(j) + ) # autofocus # if z location is included in the scan coordinates - if multiPointWorker.use_scan_coordinates and multiPointWorker.scan_coordinates_mm.shape[1] == 3 : + if multiPointWorker.use_scan_coordinates and multiPointWorker.scan_coordinates_mm.shape[1] == 3: if multiPointWorker.do_autofocus: - + # autofocus for every FOV in the first scan and update the coordinates if multiPointWorker.time_point == 0: configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() - multiPointWorker.scan_coordinates_mm[coordinate_id,2] = multiPointWorker.navigationController.z_pos_mm + multiPointWorker.scan_coordinates_mm[coordinate_id, 2] = multiPointWorker.navigationController.z_pos_mm # in subsequent scans, autofocus at the first FOV and offset the rest else: if coordinate_id == 0: - z0 = multiPointWorker.scan_coordinates_mm[0,2] + z0 = multiPointWorker.scan_coordinates_mm[0, 2] configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() - multiPointWorker.scan_coordinates_mm[0,2] = multiPointWorker.navigationController.z_pos_mm - offset = multiPointWorker.scan_coordinates_mm[0,2] - z0 - print('offset is ' + str(offset)) - multiPointWorker.scan_coordinates_mm[1:,2] = multiPointWorker.scan_coordinates_mm[1:,2] + offset + multiPointWorker.scan_coordinates_mm[0, 2] = multiPointWorker.navigationController.z_pos_mm + offset = multiPointWorker.scan_coordinates_mm[0, 2] - z0 + print("offset is " + str(offset)) + multiPointWorker.scan_coordinates_mm[1:, 2] = multiPointWorker.scan_coordinates_mm[1:, 2] + offset else: pass - # if z location is not included in the scan coordinates else: if multiPointWorker.do_reflection_af == False: # perform AF only if when not taking z stack or doing z stack from center - if ( (multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == 'FROM CENTER' ) and (multiPointWorker.do_autofocus) and (multiPointWorker.FOV_counter%Acquisition.NUMBER_OF_FOVS_PER_AF==0): - # temporary: replace the above line with the line below to AF every FOV - # if (multiPointWorker.NZ == 1) and (multiPointWorker.do_autofocus): + if ( + ((multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == "FROM CENTER") + and (multiPointWorker.do_autofocus) + and (multiPointWorker.FOV_counter % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) + ): + # temporary: replace the above line with the line below to AF every FOV + # if (multiPointWorker.NZ == 1) and (multiPointWorker.do_autofocus): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() else: - # initialize laser autofocus - if multiPointWorker.reflection_af_initialized==False: + # initialize laser autofocus + if multiPointWorker.reflection_af_initialized == False: # initialize the reflection AF multiPointWorker.microscope.laserAutofocusController.initialize_auto() multiPointWorker.reflection_af_initialized = True # do contrast AF for the first FOV - if multiPointWorker.do_autofocus and ( (multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == 'FROM CENTER' ) : + if multiPointWorker.do_autofocus and ((multiPointWorker.NZ == 1) or Z_STACKING_CONFIG == "FROM CENTER"): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - config_AF = next((config for config in multiPointWorker.configurationManager.configurations if config.name == configuration_name_AF)) + config_AF = next( + ( + config + for config in multiPointWorker.configurationManager.configurations + if config.name == configuration_name_AF + ) + ) multiPointWorker.signal_current_configuration.emit(config_AF) multiPointWorker.autofocusController.autofocus() multiPointWorker.autofocusController.wait_till_autofocus_has_completed() @@ -86,36 +124,47 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.microscope.laserAutofocusController.set_reference() else: multiPointWorker.microscope.laserAutofocusController.move_to_target(0) - multiPointWorker.microscope.laserAutofocusController.move_to_target(0) # for stepper in open loop mode, repeat the operation to counter backlash + multiPointWorker.microscope.laserAutofocusController.move_to_target( + 0 + ) # for stepper in open loop mode, repeat the operation to counter backlash - if (multiPointWorker.NZ > 1): + if multiPointWorker.NZ > 1: # move to bottom of the z stack - if Z_STACKING_CONFIG == 'FROM CENTER': - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2)) + if Z_STACKING_CONFIG == "FROM CENTER": + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # maneuver for achiving uniform step size and repeatability when using open-loop control multiPointWorker.navigationController.move_z_usteps(-160) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(160) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) # z-stack for k in range(multiPointWorker.NZ): - - file_ID = coordiante_name + str(i) + '_' + str(j if multiPointWorker.x_scan_direction==1 else multiPointWorker.NX-1-j) + '_' + str(k) + + file_ID = ( + coordiante_name + + str(i) + + "_" + + str(j if multiPointWorker.x_scan_direction == 1 else multiPointWorker.NX - 1 - j) + + "_" + + str(k) + ) # metadata = dict(x = multiPointWorker.navigationController.x_pos_mm, y = multiPointWorker.navigationController.y_pos_mm, z = multiPointWorker.navigationController.z_pos_mm) # metadata = json.dumps(metadata) # iterate through selected modes for config in multiPointWorker.selected_configurations: - if 'USB Spectrometer' not in config.name: + if "USB Spectrometer" not in config.name: - if time_point%10 != 0: + if time_point % 10 != 0: - if 'Fluorescence' in config.name: + if "Fluorescence" in config.name: # only do fluorescence every 10th timepoint continue @@ -128,65 +177,89 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.wait_till_operation_is_completed() multiPointWorker.camera.send_trigger() elif multiPointWorker.liveController.trigger_mode == TriggerMode.HARDWARE: - multiPointWorker.microcontroller.send_hardware_trigger(control_illumination=True,illumination_on_time_us=multiPointWorker.camera.exposure_time*1000) + multiPointWorker.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=multiPointWorker.camera.exposure_time * 1000 + ) # read camera frame image = multiPointWorker.camera.read_frame() if image is None: - print('multiPointWorker.camera.read_frame() returned None') + print("multiPointWorker.camera.read_frame() returned None") continue # tunr of the illumination if using software trigger if multiPointWorker.liveController.trigger_mode == TriggerMode.SOFTWARE: multiPointWorker.liveController.turn_off_illumination() # process the image - @@@ to move to camera - image = utils.crop_image(image,multiPointWorker.crop_width,multiPointWorker.crop_height) - image = utils.rotate_and_flip_image(image,rotate_image_angle=multiPointWorker.camera.rotate_image_angle,flip_image=multiPointWorker.camera.flip_image) + image = utils.crop_image(image, multiPointWorker.crop_width, multiPointWorker.crop_height) + image = utils.rotate_and_flip_image( + image, + rotate_image_angle=multiPointWorker.camera.rotate_image_angle, + flip_image=multiPointWorker.camera.flip_image, + ) # multiPointWorker.image_to_display.emit(cv2.resize(image,(round(multiPointWorker.crop_width*multiPointWorker.display_resolution_scaling), round(multiPointWorker.crop_height*multiPointWorker.display_resolution_scaling)),cv2.INTER_LINEAR)) - image_to_display = utils.crop_image(image,round(multiPointWorker.crop_width*multiPointWorker.display_resolution_scaling), round(multiPointWorker.crop_height*multiPointWorker.display_resolution_scaling)) + image_to_display = utils.crop_image( + image, + round(multiPointWorker.crop_width * multiPointWorker.display_resolution_scaling), + round(multiPointWorker.crop_height * multiPointWorker.display_resolution_scaling), + ) multiPointWorker.image_to_display.emit(image_to_display) - multiPointWorker.image_to_display_multi.emit(image_to_display,config.illumination_source) + multiPointWorker.image_to_display_multi.emit(image_to_display, config.illumination_source) if image.dtype == np.uint16: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.tiff') + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + ".tiff" + ) if multiPointWorker.camera.is_color: - if 'BF LED matrix' in config.name: - if MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': - image = image[:,:,1] - iio.imwrite(saving_path,image) + if "BF LED matrix" in config.name: + if MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": + image = image[:, :, 1] + iio.imwrite(saving_path, image) else: - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '.' + Acquisition.IMAGE_FORMAT) + saving_path = os.path.join( + current_path, + file_ID + "_" + str(config.name).replace(" ", "_") + "." + Acquisition.IMAGE_FORMAT, + ) if multiPointWorker.camera.is_color: - if 'BF LED matrix' in config.name: - if MULTIPOINT_BF_SAVING_OPTION == 'Raw': - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - elif MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': - image = image[:,:,1] + if "BF LED matrix" in config.name: + if MULTIPOINT_BF_SAVING_OPTION == "Raw": + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + elif MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": + image = image[:, :, 1] else: - image = cv2.cvtColor(image,cv2.COLOR_RGB2BGR) - cv2.imwrite(saving_path,image) + image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(saving_path, image) QApplication.processEvents() - + else: if multiPointWorker.usb_spectrometer != None: for l in range(N_SPECTRUM_PER_POINT): data = multiPointWorker.usb_spectrometer.read_spectrum() multiPointWorker.spectrum_to_display.emit(data) - saving_path = os.path.join(current_path, file_ID + '_' + str(config.name).replace(' ','_') + '_' + str(l) + '.csv') - np.savetxt(saving_path,data,delimiter=',') + saving_path = os.path.join( + current_path, file_ID + "_" + str(config.name).replace(" ", "_") + "_" + str(l) + ".csv" + ) + np.savetxt(saving_path, data, delimiter=",") # add the coordinate of the current location - new_row = pd.DataFrame({'i':[i],'j':[multiPointWorker.NX-1-j],'k':[k], - 'x (mm)':[multiPointWorker.navigationController.x_pos_mm], - 'y (mm)':[multiPointWorker.navigationController.y_pos_mm], - 'z (um)':[multiPointWorker.navigationController.z_pos_mm*1000]}, - ) + new_row = pd.DataFrame( + { + "i": [i], + "j": [multiPointWorker.NX - 1 - j], + "k": [k], + "x (mm)": [multiPointWorker.navigationController.x_pos_mm], + "y (mm)": [multiPointWorker.navigationController.y_pos_mm], + "z (um)": [multiPointWorker.navigationController.z_pos_mm * 1000], + }, + ) multiPointWorker.coordinates_pd = pd.concat([multiPointWorker.coordinates_pd, new_row], ignore_index=True) - # register the current fov in the navigationViewer - multiPointWorker.signal_register_current_fov.emit(multiPointWorker.navigationController.x_pos_mm,multiPointWorker.navigationController.y_pos_mm) + # register the current fov in the navigationViewer + multiPointWorker.signal_register_current_fov.emit( + multiPointWorker.navigationController.x_pos_mm, multiPointWorker.navigationController.y_pos_mm + ) # check if the acquisition should be aborted if multiPointWorker.multiPointController.abort_acqusition_requested: @@ -196,8 +269,10 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.navigationController.move_y_usteps(-multiPointWorker.dy_usteps) multiPointWorker.wait_till_operation_is_completed() if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.dz_usteps-_usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.dz_usteps - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() @@ -205,7 +280,9 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.dz_usteps) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.coordinates_pd.to_csv(os.path.join(current_path,'coordinates.csv'),index=False,header=True) + multiPointWorker.coordinates_pd.to_csv( + os.path.join(current_path, "coordinates.csv"), index=False, header=True + ) multiPointWorker.navigationController.enable_joystick_button_action = True return @@ -214,35 +291,52 @@ def multipoint_custom_script_entry(multiPointWorker,time_point,current_path,coor if k < multiPointWorker.NZ - 1: multiPointWorker.navigationController.move_z_usteps(multiPointWorker.deltaZ_usteps) multiPointWorker.wait_till_operation_is_completed() - time.sleep(SCAN_STABILIZATION_TIME_MS_Z/1000) + time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) multiPointWorker.dz_usteps = multiPointWorker.dz_usteps + multiPointWorker.deltaZ_usteps - + if multiPointWorker.NZ > 1: # move z back - if Z_STACKING_CONFIG == 'FROM CENTER': + if Z_STACKING_CONFIG == "FROM CENTER": if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps( -multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2) - _usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() else: - multiPointWorker.navigationController.move_z_usteps( -multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2) ) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.deltaZ_usteps*round((multiPointWorker.NZ-1)/2) + multiPointWorker.dz_usteps = ( + multiPointWorker.dz_usteps + - multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + + multiPointWorker.deltaZ_usteps * round((multiPointWorker.NZ - 1) / 2) + ) else: if multiPointWorker.navigationController.get_pid_control_flag(2) is False: - _usteps_to_clear_backlash = max(160,20*multiPointWorker.navigationController.z_microstepping) - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) - _usteps_to_clear_backlash) + _usteps_to_clear_backlash = max(160, 20 * multiPointWorker.navigationController.z_microstepping) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) - _usteps_to_clear_backlash + ) multiPointWorker.wait_till_operation_is_completed() multiPointWorker.navigationController.move_z_usteps(_usteps_to_clear_backlash) multiPointWorker.wait_till_operation_is_completed() else: - multiPointWorker.navigationController.move_z_usteps(-multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1)) + multiPointWorker.navigationController.move_z_usteps( + -multiPointWorker.deltaZ_usteps * (multiPointWorker.NZ - 1) + ) multiPointWorker.wait_till_operation_is_completed() - multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps*(multiPointWorker.NZ-1) + multiPointWorker.dz_usteps = multiPointWorker.dz_usteps - multiPointWorker.deltaZ_usteps * ( + multiPointWorker.NZ - 1 + ) # update FOV counter multiPointWorker.FOV_counter = multiPointWorker.FOV_counter + 1 diff --git a/software/control/multipoint_custom_script_entry_v2.py b/software/control/multipoint_custom_script_entry_v2.py index 32a7b536b..607ecf9be 100644 --- a/software/control/multipoint_custom_script_entry_v2.py +++ b/software/control/multipoint_custom_script_entry_v2.py @@ -9,19 +9,22 @@ BACKLASH_USTEPS = 160 + def multipoint_custom_script_entry(worker, current_path, region_id, fov, i, j): - print(f'In custom script; t={worker.time_point}, region={region_id}, fov={fov}: {i}_{j}') - + print(f"In custom script; t={worker.time_point}, region={region_id}, fov={fov}: {i}_{j}") + perform_autofocus(worker, region_id) prepare_z_stack(worker) acquire_z_stack(worker, current_path, region_id, fov, i, j) + def perform_autofocus(worker, region_id): if worker.do_reflection_af: perform_laser_autofocus(worker) else: perform_contrast_autofocus(worker, region_id) + def perform_laser_autofocus(worker): if not worker.microscope.laserAutofocusController.is_initialized: initialize_laser_autofocus(worker) @@ -30,17 +33,21 @@ def perform_laser_autofocus(worker): if worker.navigationController.get_pid_control_flag(2) is False: worker.microscope.laserAutofocusController.move_to_target(0) + def initialize_laser_autofocus(worker): print("Initializing reflection AF") worker.microscope.laserAutofocusController.initialize_auto() - if worker.do_autofocus and ((worker.NZ == 1) or worker.z_stacking_config == 'FROM CENTER'): + if worker.do_autofocus and ((worker.NZ == 1) or worker.z_stacking_config == "FROM CENTER"): perform_contrast_autofocus(worker, 0) worker.microscope.laserAutofocusController.set_reference() + def perform_contrast_autofocus(worker, region_id): - if ((worker.NZ == 1 or worker.z_stacking_config == 'FROM CENTER') - and worker.do_autofocus - and (worker.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0)): + if ( + (worker.NZ == 1 or worker.z_stacking_config == "FROM CENTER") + and worker.do_autofocus + and (worker.af_fov_count % Acquisition.NUMBER_OF_FOVS_PER_AF == 0) + ): config_AF = get_autofocus_config(worker) worker.signal_current_configuration.emit(config_AF) worker.autofocusController.autofocus() @@ -49,6 +56,7 @@ def perform_contrast_autofocus(worker, region_id): worker.scan_coordinates_mm[region_id][2] = worker.navigationController.z_pos_mm update_widget_z_level(worker, region_id) + def update_widget_z_level(worker, region_id): if worker.coordinate_dict is not None: worker.microscope.multiPointWidgetGrid.update_region_z_level(region_id, worker.navigationController.z_pos_mm) @@ -58,17 +66,23 @@ def update_widget_z_level(worker, region_id): except: print("Failed to update flexible widget z") try: - worker.microscope.multiPointWidgetGrid.update_region_z_level(region_id, worker.navigationController.z_pos_mm) + worker.microscope.multiPointWidgetGrid.update_region_z_level( + region_id, worker.navigationController.z_pos_mm + ) except: print("Failed to update grid widget z") + def get_autofocus_config(worker): configuration_name_AF = MULTIPOINT_AUTOFOCUS_CHANNEL - return next((config for config in worker.configurationManager.configurations if config.name == configuration_name_AF)) + return next( + (config for config in worker.configurationManager.configurations if config.name == configuration_name_AF) + ) + def prepare_z_stack(worker): if worker.NZ > 1: - if worker.z_stacking_config == 'FROM CENTER': + if worker.z_stacking_config == "FROM CENTER": worker.navigationController.move_z_usteps(-worker.deltaZ_usteps * round((worker.NZ - 1) / 2)) worker.wait_till_operation_is_completed() time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) @@ -78,18 +92,20 @@ def prepare_z_stack(worker): worker.wait_till_operation_is_completed() time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) + def acquire_z_stack(worker, current_path, region_id, fov, i, j): total_images = worker.NZ * len(worker.selected_configurations) for z_level in range(worker.NZ): acquire_single_z_plane(worker, current_path, region_id, fov, i, j, z_level, total_images) if z_level < worker.NZ - 1: move_to_next_z_plane(worker) - + if worker.NZ > 1: move_z_stack_back(worker) - + worker.af_fov_count += 1 + def acquire_single_z_plane(worker, current_path, region_id, fov, i, j, z_level, total_images): if i is not None and j is not None: file_ID = f"{region_id}_{i}_{j}_{z_level}" @@ -99,7 +115,7 @@ def acquire_single_z_plane(worker, current_path, region_id, fov, i, j, z_level, current_round_images = {} for config_idx, config in enumerate(worker.selected_configurations): acquire_image_for_configuration(worker, config, file_ID, current_path, current_round_images, i, j, z_level) - + # Calculate current image number and emit progress signal current_image = (fov * total_images) + (z_level * len(worker.selected_configurations)) + config_idx + 1 worker.signal_region_progress.emit(current_image, worker.total_scans) @@ -113,18 +129,20 @@ def acquire_single_z_plane(worker, current_path, region_id, fov, i, j, z_level, if check_for_abort(worker, current_path, region_id): return + def acquire_image_for_configuration(worker, config, file_ID, current_path, current_round_images, i, j, z_level): worker.handle_z_offset(config, True) # Added this line to perform config z-offset - if 'USB Spectrometer' not in config.name and 'RGB' not in config.name: + if "USB Spectrometer" not in config.name and "RGB" not in config.name: acquire_camera_image(worker, config, file_ID, current_path, current_round_images, i, j, z_level) - elif 'RGB' in config.name: + elif "RGB" in config.name: acquire_rgb_image(worker, config, file_ID, current_path, current_round_images, i, j, z_level) else: acquire_spectrometer_data(worker, config, file_ID, current_path, i, j, z_level) worker.handle_z_offset(config, False) # Added this line to undo z-offset + def acquire_camera_image(worker, config, file_ID, current_path, current_round_images, i, j, z_level): worker.signal_current_configuration.emit(config) worker.wait_till_operation_is_completed() @@ -133,6 +151,7 @@ def acquire_camera_image(worker, config, file_ID, current_path, current_round_im if image is not None: process_and_save_image(worker, image, file_ID, config, current_path, current_round_images, i, j, z_level) + def capture_image(worker, config): if worker.liveController.trigger_mode == TriggerMode.SOFTWARE: return capture_image_software_trigger(worker) @@ -141,6 +160,7 @@ def capture_image(worker, config): else: return worker.camera.read_frame() + def capture_image_software_trigger(worker): worker.liveController.turn_on_illumination() worker.wait_till_operation_is_completed() @@ -149,19 +169,29 @@ def capture_image_software_trigger(worker): worker.liveController.turn_off_illumination() return image + def capture_image_hardware_trigger(worker, config): - if 'Fluorescence' in config.name and ENABLE_NL5 and NL5_USE_DOUT: + if "Fluorescence" in config.name and ENABLE_NL5 and NL5_USE_DOUT: worker.camera.image_is_ready = False worker.microscope.nl5.start_acquisition() return worker.camera.read_frame(reset_image_ready_flag=False) else: - worker.microcontroller.send_hardware_trigger(control_illumination=True, illumination_on_time_us=worker.camera.exposure_time * 1000) + worker.microcontroller.send_hardware_trigger( + control_illumination=True, illumination_on_time_us=worker.camera.exposure_time * 1000 + ) return worker.camera.read_frame() + def process_and_save_image(worker, image, file_ID, config, current_path, current_round_images, i, j, z_level): image = utils.crop_image(image, worker.crop_width, worker.crop_height) - image = utils.rotate_and_flip_image(image, rotate_image_angle=worker.camera.rotate_image_angle, flip_image=worker.camera.flip_image) - image_to_display = utils.crop_image(image, round(worker.crop_width * worker.display_resolution_scaling), round(worker.crop_height * worker.display_resolution_scaling)) + image = utils.rotate_and_flip_image( + image, rotate_image_angle=worker.camera.rotate_image_angle, flip_image=worker.camera.flip_image + ) + image_to_display = utils.crop_image( + image, + round(worker.crop_width * worker.display_resolution_scaling), + round(worker.crop_height * worker.display_resolution_scaling), + ) worker.image_to_display.emit(image_to_display) worker.image_to_display_multi.emit(image_to_display, config.illumination_source) @@ -170,26 +200,31 @@ def process_and_save_image(worker, image, file_ID, config, current_path, current current_round_images[config.name] = np.copy(image) + def save_image(worker, image, file_ID, config, current_path): if image.dtype == np.uint16: saving_path = os.path.join(current_path, f"{file_ID}_{config.name.replace(' ', '_')}.tiff") else: - saving_path = os.path.join(current_path, f"{file_ID}_{config.name.replace(' ', '_')}.{Acquisition.IMAGE_FORMAT}") - - if worker.camera.is_color and 'BF LED matrix' in config.name: + saving_path = os.path.join( + current_path, f"{file_ID}_{config.name.replace(' ', '_')}.{Acquisition.IMAGE_FORMAT}" + ) + + if worker.camera.is_color and "BF LED matrix" in config.name: image = process_color_image(image) - + iio.imwrite(saving_path, image) + def process_color_image(image): - if MULTIPOINT_BF_SAVING_OPTION == 'RGB2GRAY': + if MULTIPOINT_BF_SAVING_OPTION == "RGB2GRAY": return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) - elif MULTIPOINT_BF_SAVING_OPTION == 'Green Channel Only': + elif MULTIPOINT_BF_SAVING_OPTION == "Green Channel Only": return image[:, :, 1] return image + def acquire_rgb_image(worker, config, file_ID, current_path, current_round_images, i, j, z_level): - rgb_channels = ['BF LED matrix full_R', 'BF LED matrix full_G', 'BF LED matrix full_B'] + rgb_channels = ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"] images = {} for channel_config in worker.configurationManager.configurations: @@ -199,49 +234,66 @@ def acquire_rgb_image(worker, config, file_ID, current_path, current_round_image image = capture_image(worker, channel_config) if image is not None: image = utils.crop_image(image, worker.crop_width, worker.crop_height) - image = utils.rotate_and_flip_image(image, rotate_image_angle=worker.camera.rotate_image_angle, flip_image=worker.camera.flip_image) + image = utils.rotate_and_flip_image( + image, rotate_image_angle=worker.camera.rotate_image_angle, flip_image=worker.camera.flip_image + ) images[channel_config.name] = np.copy(image) if images: process_and_save_rgb_image(worker, images, file_ID, config, current_path, current_round_images, i, j, z_level) + def process_and_save_rgb_image(worker, images, file_ID, config, current_path, current_round_images, i, j, z_level): - if len(images['BF LED matrix full_R'].shape) == 3: + if len(images["BF LED matrix full_R"].shape) == 3: handle_rgb_channels(worker, images, file_ID, current_path, config, i, j, z_level) else: construct_rgb_image(worker, images, file_ID, current_path, config, i, j, z_level) + def handle_rgb_channels(worker, images, file_ID, current_path, config, i, j, z_level): - for channel in ['BF LED matrix full_R', 'BF LED matrix full_G', 'BF LED matrix full_B']: - image_to_display = utils.crop_image(images[channel], round(worker.crop_width * worker.display_resolution_scaling), round(worker.crop_height * worker.display_resolution_scaling)) + for channel in ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"]: + image_to_display = utils.crop_image( + images[channel], + round(worker.crop_width * worker.display_resolution_scaling), + round(worker.crop_height * worker.display_resolution_scaling), + ) worker.image_to_display.emit(image_to_display) worker.image_to_display_multi.emit(image_to_display, config.illumination_source) worker.update_napari(images[channel], channel, i, j, z_level) file_name = f"{file_ID}_{channel.replace(' ', '_')}{'.tiff' if images[channel].dtype == np.uint16 else '.' + Acquisition.IMAGE_FORMAT}" iio.imwrite(os.path.join(current_path, file_name), images[channel]) -def construct_rgb_image(worker, images, file_ID, current_path, config, i, j, z_level): - rgb_image = np.zeros((*images['BF LED matrix full_R'].shape, 3), dtype=images['BF LED matrix full_R'].dtype) - rgb_image[:, :, 0] = images['BF LED matrix full_R'] - rgb_image[:, :, 1] = images['BF LED matrix full_G'] - rgb_image[:, :, 2] = images['BF LED matrix full_B'] - image_to_display = utils.crop_image(rgb_image, round(worker.crop_width * worker.display_resolution_scaling), round(worker.crop_height * worker.display_resolution_scaling)) +def construct_rgb_image(worker, images, file_ID, current_path, config, i, j, z_level): + rgb_image = np.zeros((*images["BF LED matrix full_R"].shape, 3), dtype=images["BF LED matrix full_R"].dtype) + rgb_image[:, :, 0] = images["BF LED matrix full_R"] + rgb_image[:, :, 1] = images["BF LED matrix full_G"] + rgb_image[:, :, 2] = images["BF LED matrix full_B"] + + image_to_display = utils.crop_image( + rgb_image, + round(worker.crop_width * worker.display_resolution_scaling), + round(worker.crop_height * worker.display_resolution_scaling), + ) worker.image_to_display.emit(image_to_display) worker.image_to_display_multi.emit(image_to_display, config.illumination_source) worker.update_napari(rgb_image, config.name, i, j, z_level) - file_name = f"{file_ID}_BF_LED_matrix_full_RGB{'.tiff' if rgb_image.dtype == np.uint16 else '.' + Acquisition.IMAGE_FORMAT}" + file_name = ( + f"{file_ID}_BF_LED_matrix_full_RGB{'.tiff' if rgb_image.dtype == np.uint16 else '.' + Acquisition.IMAGE_FORMAT}" + ) iio.imwrite(os.path.join(current_path, file_name), rgb_image) + def acquire_spectrometer_data(worker, config, file_ID, current_path, i, j, z_level): if worker.usb_spectrometer is not None: for l in range(N_SPECTRUM_PER_POINT): data = worker.usb_spectrometer.read_spectrum() worker.spectrum_to_display.emit(data) saving_path = os.path.join(current_path, f"{file_ID}_{config.name.replace(' ', '_')}_{l}.csv") - np.savetxt(saving_path, data, delimiter=',') + np.savetxt(saving_path, data, delimiter=",") + def update_coordinates_dataframe(worker, region_id, z_level, fov, i, j): if i is None or j is None: @@ -250,28 +302,47 @@ def update_coordinates_dataframe(worker, region_id, z_level, fov, i, j): worker.update_coordinates_dataframe(region_id, z_level, i=i, j=j) worker.signal_register_current_fov.emit(worker.navigationController.x_pos_mm, worker.navigationController.y_pos_mm) + def run_real_time_processing(worker, current_round_images, i, j, z_level): acquired_image_configs = list(current_round_images.keys()) - if 'BF LED matrix left half' in current_round_images and 'BF LED matrix right half' in current_round_images and 'Fluorescence 405 nm Ex' in current_round_images: + if ( + "BF LED matrix left half" in current_round_images + and "BF LED matrix right half" in current_round_images + and "Fluorescence 405 nm Ex" in current_round_images + ): try: print("real time processing", worker.count_rtp) - if (worker.microscope.model is None) or (worker.microscope.device is None) or (worker.microscope.classification_th is None) or (worker.microscope.dataHandler is None): - raise AttributeError('microscope missing model, device, classification_th, and/or dataHandler') - I_fluorescence = current_round_images['Fluorescence 405 nm Ex'] - I_left = current_round_images['BF LED matrix left half'] - I_right = current_round_images['BF LED matrix right half'] + if ( + (worker.microscope.model is None) + or (worker.microscope.device is None) + or (worker.microscope.classification_th is None) + or (worker.microscope.dataHandler is None) + ): + raise AttributeError("microscope missing model, device, classification_th, and/or dataHandler") + I_fluorescence = current_round_images["Fluorescence 405 nm Ex"] + I_left = current_round_images["BF LED matrix left half"] + I_right = current_round_images["BF LED matrix right half"] if len(I_left.shape) == 3: I_left = cv2.cvtColor(I_left, cv2.COLOR_RGB2GRAY) if len(I_right.shape) == 3: I_right = cv2.cvtColor(I_right, cv2.COLOR_RGB2GRAY) - malaria_rtp(I_fluorescence, I_left, I_right, i, j, z_level, worker, - classification_test_mode=worker.microscope.classification_test_mode, - sort_during_multipoint=SORT_DURING_MULTIPOINT, - disp_th_during_multipoint=DISP_TH_DURING_MULTIPOINT) + malaria_rtp( + I_fluorescence, + I_left, + I_right, + i, + j, + z_level, + worker, + classification_test_mode=worker.microscope.classification_test_mode, + sort_during_multipoint=SORT_DURING_MULTIPOINT, + disp_th_during_multipoint=DISP_TH_DURING_MULTIPOINT, + ) worker.count_rtp += 1 except AttributeError as e: print(repr(e)) + def move_to_next_z_plane(worker): if worker.use_piezo: worker.z_piezo_um += worker.deltaZ * 1000 @@ -287,6 +358,7 @@ def move_to_next_z_plane(worker): time.sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000) worker.dz_usteps = worker.dz_usteps + worker.deltaZ_usteps + def move_z_stack_back(worker): if worker.use_piezo: worker.z_piezo_um = OBJECTIVE_PIEZO_HOME_UM @@ -297,22 +369,32 @@ def move_z_stack_back(worker): if MULTIPOINT_PIEZO_UPDATE_DISPLAY: worker.signal_z_piezo_um.emit(worker.z_piezo_um) else: - if worker.z_stacking_config == 'FROM CENTER': + if worker.z_stacking_config == "FROM CENTER": move_z_stack_back_from_center(worker) else: move_z_stack_back_from_top(worker) + def move_z_stack_back_from_center(worker): _usteps_to_clear_backlash = max(BACKLASH_USTEPS, 20 * worker.navigationController.z_microstepping) if worker.navigationController.get_pid_control_flag(2) is False: - worker.navigationController.move_z_usteps(-worker.deltaZ_usteps * (worker.NZ - 1) + worker.deltaZ_usteps * round((worker.NZ - 1) / 2) - _usteps_to_clear_backlash) + worker.navigationController.move_z_usteps( + -worker.deltaZ_usteps * (worker.NZ - 1) + + worker.deltaZ_usteps * round((worker.NZ - 1) / 2) + - _usteps_to_clear_backlash + ) worker.wait_till_operation_is_completed() worker.navigationController.move_z_usteps(_usteps_to_clear_backlash) worker.wait_till_operation_is_completed() else: - worker.navigationController.move_z_usteps(-worker.deltaZ_usteps * (worker.NZ - 1) + worker.deltaZ_usteps * round((worker.NZ - 1) / 2)) + worker.navigationController.move_z_usteps( + -worker.deltaZ_usteps * (worker.NZ - 1) + worker.deltaZ_usteps * round((worker.NZ - 1) / 2) + ) worker.wait_till_operation_is_completed() - worker.dz_usteps = worker.dz_usteps - worker.deltaZ_usteps * (worker.NZ - 1) + worker.deltaZ_usteps * round((worker.NZ - 1) / 2) + worker.dz_usteps = ( + worker.dz_usteps - worker.deltaZ_usteps * (worker.NZ - 1) + worker.deltaZ_usteps * round((worker.NZ - 1) / 2) + ) + def move_z_stack_back_from_top(worker): _usteps_to_clear_backlash = max(BACKLASH_USTEPS, 20 * worker.navigationController.z_microstepping) @@ -326,12 +408,14 @@ def move_z_stack_back_from_top(worker): worker.wait_till_operation_is_completed() worker.dz_usteps = worker.dz_usteps - worker.deltaZ_usteps * (worker.NZ - 1) + def check_for_abort(worker, current_path, region_id): if worker.multiPointController.abort_acqusition_requested: worker.handle_acquisition_abort(current_path, region_id) return True return False + def move_stage_back(worker): worker.navigationController.move_x_usteps(-worker.dx_usteps) worker.wait_till_operation_is_completed() @@ -339,6 +423,7 @@ def move_stage_back(worker): worker.wait_till_operation_is_completed() move_z_back(worker) + def move_z_back(worker): if worker.navigationController.get_pid_control_flag(2) is False: _usteps_to_clear_backlash = max(BACKLASH_USTEPS, 20 * worker.navigationController.z_microstepping) @@ -350,6 +435,7 @@ def move_z_back(worker): worker.navigationController.move_z_usteps(-worker.dz_usteps) worker.wait_till_operation_is_completed() + # This function is called by the MultiPointWorker's run_single_time_point method def run_custom_multipoint(worker, current_path, region_id, fov, i, j): - multipoint_custom_script_entry(worker, current_path, region_id, fov, i, j) \ No newline at end of file + multipoint_custom_script_entry(worker, current_path, region_id, fov, i, j) diff --git a/software/control/processing_handler.py b/software/control/processing_handler.py index 3c1980ddb..086765bad 100644 --- a/software/control/processing_handler.py +++ b/software/control/processing_handler.py @@ -4,6 +4,7 @@ import pandas as pd import control.utils as utils + def default_image_preprocessor(image, callable_list): """ :param image: ndarray representing an image @@ -16,34 +17,36 @@ def default_image_preprocessor(image, callable_list): """ output_image = np.copy(image) for c in callable_list: - output_image = c['func'](output_image, *c['args'],**c['kwargs']) + output_image = c["func"](output_image, *c["args"], **c["kwargs"]) return output_image -class ProcessingHandler(): + +class ProcessingHandler: """ :brief: Handler class for parallelizing FOV processing. GENERAL NOTE: REMEMBER TO PASS COPIES OF IMAGES WHEN QUEUEING THEM FOR PROCESSING """ + def __init__(self): - self.processing_queue = queue.Queue() # elements in this queue are - # dicts in the form - # {'function': callable, 'args':list - # of positional arguments to pass, - # 'kwargs': dict of kwargs to pass} - # a dict in the form {'function':'end'} - # will cause processing to terminate - # the function called should return - # a dict in the same form it received, - # in appropriate form to pass to the - # upload queue + self.processing_queue = queue.Queue() # elements in this queue are + # dicts in the form + # {'function': callable, 'args':list + # of positional arguments to pass, + # 'kwargs': dict of kwargs to pass} + # a dict in the form {'function':'end'} + # will cause processing to terminate + # the function called should return + # a dict in the same form it received, + # in appropriate form to pass to the + # upload queue - self.upload_queue = queue.Queue() # elements in this queue are - # dicts in the form - # {'function': callable, 'args':list - # of positional arguments to pass, - # 'kwargs': dict of kwargs to pass} - # a dict in the form {'function':'end'} - # will cause the uploading to terminate + self.upload_queue = queue.Queue() # elements in this queue are + # dicts in the form + # {'function': callable, 'args':list + # of positional arguments to pass, + # 'kwargs': dict of kwargs to pass} + # a dict in the form {'function':'end'} + # will cause the uploading to terminate self.processing_thread = None self.uploading_thread = None @@ -54,13 +57,11 @@ def processing_queue_handler(self, queue_timeout=None): processing_task = self.processing_queue.get(timeout=queue_timeout) except queue.Empty: break - if processing_task['function'] == 'end': + if processing_task["function"] == "end": self.processing_queue.task_done() break else: - upload_task = processing_task['function']( - *processing_task['args'], - **processing_task['kwargs']) + upload_task = processing_task["function"](*processing_task["args"], **processing_task["kwargs"]) self.upload_queue.put(upload_task) self.processing_queue.task_done() @@ -71,25 +72,24 @@ def upload_queue_handler(self, queue_timeout=None): upload_task = self.upload_queue.get(timeout=queue_timeout) except queue.Empty: break - if upload_task['function'] == 'end': + if upload_task["function"] == "end": self.upload_queue.task_done() break else: - upload_task['function'](*upload_task['args'],**upload_task['kwargs']) + upload_task["function"](*upload_task["args"], **upload_task["kwargs"]) self.upload_queue.task_done() def start_processing(self, queue_timeout=None): - self.processing_thread =\ - threading.Thread(target=self.processing_queue_handler, args=[queue_timeout]) + self.processing_thread = threading.Thread(target=self.processing_queue_handler, args=[queue_timeout]) self.processing_thread.start() - def start_uploading(self,queue_timeout=None): - self.uploading_thread =\ - threading.Thread(target=self.upload_queue_handler,args=[queue_timeout]) + + def start_uploading(self, queue_timeout=None): + self.uploading_thread = threading.Thread(target=self.upload_queue_handler, args=[queue_timeout]) self.uploading_thread.start() + def end_uploading(self, *args, **kwargs): - return {'function':'end'} - def end_processing(self): - self.processing_queue.put({'function':self.end_uploading,'args':[], - 'kwargs':{}}) - self.processing_queue.put({'function':'end'}) + return {"function": "end"} + def end_processing(self): + self.processing_queue.put({"function": self.end_uploading, "args": [], "kwargs": {}}) + self.processing_queue.put({"function": "end"}) diff --git a/software/control/serial_peripherals.py b/software/control/serial_peripherals.py index 9c0cd159e..f6dadae62 100644 --- a/software/control/serial_peripherals.py +++ b/software/control/serial_peripherals.py @@ -6,15 +6,18 @@ from squid.abc import LightSource import squid.logging + log = squid.logging.get_logger(__name__) + class SerialDevice: """ General wrapper for serial devices, with automating device finding based on VID/PID or serial number. """ - def __init__(self, port=None, VID=None,PID=None,SN=None, baudrate=9600, read_timeout=0.1, **kwargs): + + def __init__(self, port=None, VID=None, PID=None, SN=None, baudrate=9600, read_timeout=0.1, **kwargs): # Initialize the serial connection self.port = port self.VID = VID @@ -24,7 +27,7 @@ def __init__(self, port=None, VID=None,PID=None,SN=None, baudrate=9600, read_tim self.baudrate = baudrate self.read_timeout = read_timeout self.serial_kwargs = kwargs - + self.serial = None if VID is not None and PID is not None: @@ -41,10 +44,10 @@ def __init__(self, port=None, VID=None,PID=None,SN=None, baudrate=9600, read_tim if self.port is not None: self.serial = serial.Serial(self.port, baudrate=baudrate, timeout=read_timeout, **kwargs) - def open_ser(self, SN=None, VID=None, PID=None, baudrate =None, read_timeout=None,**kwargs): + def open_ser(self, SN=None, VID=None, PID=None, baudrate=None, read_timeout=None, **kwargs): if self.serial is not None and not self.serial.is_open: self.serial.open() - + if SN is None: SN = self.SN @@ -76,9 +79,18 @@ def open_ser(self, SN=None, VID=None, PID=None, baudrate =None, read_timeout=Non self.port = d.device break if self.port is not None: - self.serial = serial.Serial(self.port,**kwargs) - - def write_and_check(self, command, expected_response, read_delay=0.1, max_attempts=5, attempt_delay=1, check_prefix=True, print_response=False): + self.serial = serial.Serial(self.port, **kwargs) + + def write_and_check( + self, + command, + expected_response, + read_delay=0.1, + max_attempts=5, + attempt_delay=1, + check_prefix=True, + print_response=False, + ): # Write a command and check the response for attempt in range(max_attempts): self.serial.write(command.encode()) @@ -123,6 +135,7 @@ def close(self): # Close the serial connection self.serial.close() + class XLight_Simulation: def __init__(self): self.has_spinning_disk_motor = True @@ -140,7 +153,7 @@ def __init__(self): self.disk_motor_state = False self.spinning_disk_pos = 0 - def set_emission_filter(self,position, extraction=False, validate=False): + def set_emission_filter(self, position, extraction=False, validate=False): self.emission_wheel_pos = position return position @@ -168,22 +181,23 @@ def set_disk_motor_state(self, state): def get_disk_motor_state(self): return self.disk_motor_state - def set_illumination_iris(self,value): + def set_illumination_iris(self, value): # value: 0 - 100 self.illumination_iris = value return self.illumination_iris - def set_emission_iris(self,value): + def set_emission_iris(self, value): # value: 0 - 100 self.emission_iris = value return self.emission_iris - def set_filter_slider(self,position): - if str(position) not in ["0","1","2","3"]: + def set_filter_slider(self, position): + if str(position) not in ["0", "1", "2", "3"]: raise ValueError("Invalid slider position!") self.slider_position = position return self.slider_position + # CrestOptics X-Light Port specs: # 9600 baud # 8 data bits @@ -191,10 +205,11 @@ def set_filter_slider(self,position): # No parity # no flow control -class XLight: +class XLight: """Wrapper for communicating with CrestOptics X-Light devices over serial""" - def __init__(self, SN, sleep_time_for_wheel = 0.25, disable_emission_filter_wheel=True): + + def __init__(self, SN, sleep_time_for_wheel=0.25, disable_emission_filter_wheel=True): """ Provide serial number (default is that of the device cephla already has) for device-finding purposes. Otherwise, all @@ -215,10 +230,16 @@ def __init__(self, SN, sleep_time_for_wheel = 0.25, disable_emission_filter_whee self.disable_emission_filter_wheel = disable_emission_filter_wheel - self.serial_connection = SerialDevice(SN=SN,baudrate=115200, - bytesize=serial.EIGHTBITS,stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - xonxoff=False,rtscts=False,dsrdtr=False) + self.serial_connection = SerialDevice( + SN=SN, + baudrate=115200, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) self.serial_connection.open_ser() self.parse_idc_response(self.serial_connection.write_and_read("idc\r")) @@ -240,63 +261,69 @@ def parse_idc_response(self, response): self.has_ttl_control = bool(config_value & 0x00001000) def print_config(self): - self.log.info(( - "Machine Configuration:\n" - f" Spinning disk motor: {self.has_spinning_disk_motor}\n", - f" Spinning disk slider: {self.has_spinning_disk_slider}\n", - f" Dichroic filters wheel: {self.has_dichroic_filters_wheel}\n", - f" Emission filters wheel: {self.has_emission_filters_wheel}\n", - f" Excitation filters wheel: {self.has_excitation_filters_wheel}\n", - f" Illumination Iris diaphragm: {self.has_illumination_iris_diaphragm}\n", - f" Emission Iris diaphragm: {self.has_emission_iris_diaphragm}\n", - f" Dichroic filter slider: {self.has_dichroic_filter_slider}\n", - f" TTL control and combined commands subsystem: {self.has_ttl_control}")) - - def set_emission_filter(self,position,extraction=False,validate=True): + self.log.info( + ( + "Machine Configuration:\n" f" Spinning disk motor: {self.has_spinning_disk_motor}\n", + f" Spinning disk slider: {self.has_spinning_disk_slider}\n", + f" Dichroic filters wheel: {self.has_dichroic_filters_wheel}\n", + f" Emission filters wheel: {self.has_emission_filters_wheel}\n", + f" Excitation filters wheel: {self.has_excitation_filters_wheel}\n", + f" Illumination Iris diaphragm: {self.has_illumination_iris_diaphragm}\n", + f" Emission Iris diaphragm: {self.has_emission_iris_diaphragm}\n", + f" Dichroic filter slider: {self.has_dichroic_filter_slider}\n", + f" TTL control and combined commands subsystem: {self.has_ttl_control}", + ) + ) + + def set_emission_filter(self, position, extraction=False, validate=True): if self.disable_emission_filter_wheel: - print('emission filter wheel disabled') + print("emission filter wheel disabled") return -1 - if str(position) not in ["1","2","3","4","5","6","7","8"]: + if str(position) not in ["1", "2", "3", "4", "5", "6", "7", "8"]: raise ValueError("Invalid emission filter wheel position!") position_to_write = str(position) position_to_read = str(position) if extraction: - position_to_write+="m" + position_to_write += "m" if validate: - current_pos = self.serial_connection.write_and_check("B"+position_to_write+"\r","B"+position_to_read,read_delay=0.01) + current_pos = self.serial_connection.write_and_check( + "B" + position_to_write + "\r", "B" + position_to_read, read_delay=0.01 + ) self.emission_wheel_pos = int(current_pos[1]) else: - self.serial_connection.write("B"+position_to_write+"\r") + self.serial_connection.write("B" + position_to_write + "\r") time.sleep(self.sleep_time_for_wheel) self.emission_wheel_pos = position return self.emission_wheel_pos def get_emission_filter(self): - current_pos = self.serial_connection.write_and_check("rB\r","rB",read_delay=0.01) + current_pos = self.serial_connection.write_and_check("rB\r", "rB", read_delay=0.01) self.emission_wheel_pos = int(current_pos[2]) return self.emission_wheel_pos - def set_dichroic(self, position,extraction=False): - if str(position) not in ["1","2","3","4","5"]: + def set_dichroic(self, position, extraction=False): + if str(position) not in ["1", "2", "3", "4", "5"]: raise ValueError("Invalid dichroic wheel position!") position_to_write = str(position) position_to_read = str(position) if extraction: - position_to_write+="m" + position_to_write += "m" - current_pos = self.serial_connection.write_and_check("C"+position_to_write+"\r","C"+position_to_read,read_delay=0.01) + current_pos = self.serial_connection.write_and_check( + "C" + position_to_write + "\r", "C" + position_to_read, read_delay=0.01 + ) self.dichroic_wheel_pos = int(current_pos[1]) return self.dichroic_wheel_pos def get_dichroic(self): - current_pos = self.serial_connection.write_and_check("rC\r","rC",read_delay=0.01) + current_pos = self.serial_connection.write_and_check("rC\r", "rC", read_delay=0.01) self.dichroic_wheel_pos = int(current_pos[2]) return self.dichroic_wheel_pos - def set_disk_position(self,position): - if str(position) not in ["0","1","2","wide field","confocal"]: + def set_disk_position(self, position): + if str(position) not in ["0", "1", "2", "wide field", "confocal"]: raise ValueError("Invalid disk position!") if position == "wide field": position = "0" @@ -307,35 +334,37 @@ def set_disk_position(self,position): position_to_write = str(position) position_to_read = str(position) - current_pos = self.serial_connection.write_and_check("D"+position_to_write+"\r","D"+position_to_read,read_delay=5) + current_pos = self.serial_connection.write_and_check( + "D" + position_to_write + "\r", "D" + position_to_read, read_delay=5 + ) self.spinning_disk_pos = int(current_pos[1]) return self.spinning_disk_pos - def set_illumination_iris(self,value): + def set_illumination_iris(self, value): # value: 0 - 100 self.illumination_iris = value - value = str(int(10*value)) - self.serial_connection.write_and_check("J"+value+"\r","J"+value,read_delay=3) + value = str(int(10 * value)) + self.serial_connection.write_and_check("J" + value + "\r", "J" + value, read_delay=3) return self.illumination_iris - def set_emission_iris(self,value): + def set_emission_iris(self, value): # value: 0 - 100 self.emission_iris = value - value = str(int(10*value)) - self.serial_connection.write_and_check("V"+value+"\r","V"+value,read_delay=3) + value = str(int(10 * value)) + self.serial_connection.write_and_check("V" + value + "\r", "V" + value, read_delay=3) return self.emission_iris - def set_filter_slider(self,position): - if str(position) not in ["0","1","2","3"]: + def set_filter_slider(self, position): + if str(position) not in ["0", "1", "2", "3"]: raise ValueError("Invalid slider position!") self.slider_position = position position_to_write = str(position) position_to_read = str(position) - self.serial_connection.write_and_check("P"+position_to_write+"\r","V"+position_to_read,read_delay=5) + self.serial_connection.write_and_check("P" + position_to_write + "\r", "V" + position_to_read, read_delay=5) return self.slider_position def get_disk_position(self): - current_pos = self.serial_connection.write_and_check("rD\r","rD",read_delay=0.01) + current_pos = self.serial_connection.write_and_check("rD\r", "rD", read_delay=0.01) self.spinning_disk_pos = int(current_pos[2]) return self.spinning_disk_pos @@ -346,35 +375,45 @@ def set_disk_motor_state(self, state): else: state_to_write = "0" - current_pos = self.serial_connection.write_and_check("N"+state_to_write+"\r","N"+state_to_write,read_delay=2.5) + current_pos = self.serial_connection.write_and_check( + "N" + state_to_write + "\r", "N" + state_to_write, read_delay=2.5 + ) self.disk_motor_state = bool(int(current_pos[1])) def get_disk_motor_state(self): """Return True for on, Off otherwise""" - current_pos = self.serial_connection.write_and_check("rN\r","rN",read_delay=0.01) + current_pos = self.serial_connection.write_and_check("rN\r", "rN", read_delay=0.01) self.disk_motor_state = bool(int(current_pos[2])) return self.disk_motor_state + class LDI(LightSource): """Wrapper for communicating with LDI over serial""" + def __init__(self, SN="00000001"): """ Provide serial number """ self.log = squid.logging.get_logger(self.__class__.__name__) - self.serial_connection = SerialDevice(SN=SN,baudrate=9600, - bytesize=serial.EIGHTBITS,stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - xonxoff=False,rtscts=False,dsrdtr=False) + self.serial_connection = SerialDevice( + SN=SN, + baudrate=9600, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) self.serial_connection.open_ser() - if LDI_INTENSITY_MODE == 'PC': + if LDI_INTENSITY_MODE == "PC": self.intensity_mode = IntensityControlMode.Software - elif LDI_INTENSITY_MODE == 'EXT': + elif LDI_INTENSITY_MODE == "EXT": self.intensity_mode = IntensityControlMode.SquidControllerDAC - if LDI_SHUTTER_MODE == 'PC': + if LDI_SHUTTER_MODE == "PC": self.shutter_mode = ShutterControlMode.Software - elif LDI_SHUTTER_MODE == 'EXT': + elif LDI_SHUTTER_MODE == "EXT": self.shutter_mode = ShutterControlMode.TTL self.channel_mappings = { @@ -389,67 +428,68 @@ def __init__(self, SN="00000001"): 640: 640, 730: 730, 735: 730, - 750: 730 + 750: 730, } def initialize(self): - self.serial_connection.write_and_check("run!\r","ok") + self.serial_connection.write_and_check("run!\r", "ok") - def set_shutter_control_mode(self,mode): + def set_shutter_control_mode(self, mode): if mode == ShutterControlMode.TTL: - self.serial_connection.write_and_check('SH_MODE=EXT\r',"ok") + self.serial_connection.write_and_check("SH_MODE=EXT\r", "ok") elif mode == ShutterControlMode.Software: - self.serial_connection.write_and_check('SH_MODE=PC\r',"ok") + self.serial_connection.write_and_check("SH_MODE=PC\r", "ok") self.shutter_mode = mode def get_shutter_control_mode(self): pass - def set_intensity_control_mode(self,mode): + def set_intensity_control_mode(self, mode): if mode == IntensityControlMode.SquidControllerDAC: - self.serial_connection.write_and_check('INT_MODE=EXT\r',"ok") + self.serial_connection.write_and_check("INT_MODE=EXT\r", "ok") elif mode == IntensityControlMode.Software: - self.serial_connection.write_and_check('INT_MODE=PC\r',"ok") + self.serial_connection.write_and_check("INT_MODE=PC\r", "ok") self.intensity_mode = mode def get_intensity_control_mode(self): pass - def set_intensity(self,channel,intensity): + def set_intensity(self, channel, intensity): channel = str(channel) intensity = "{:.2f}".format(intensity) - self.log.debug('set:'+channel+'='+intensity+'\r') - self.serial_connection.write_and_check('set:'+channel+'='+intensity+'\r',"ok") - self.log.debug('active channel: ' + str(self.active_channel)) + self.log.debug("set:" + channel + "=" + intensity + "\r") + self.serial_connection.write_and_check("set:" + channel + "=" + intensity + "\r", "ok") + self.log.debug("active channel: " + str(self.active_channel)) def get_intensity(self, channel): - return 0 # To be implemented + return 0 # To be implemented def get_intensity_range(self): return [0, 100] - def set_shutter_state(self,channel,state): + def set_shutter_state(self, channel, state): channel = str(channel) state = str(state) - self.serial_connection.write_and_check('shutter:'+channel+'='+state+'\r',"ok") + self.serial_connection.write_and_check("shutter:" + channel + "=" + state + "\r", "ok") def get_shutter_state(self, channel): - self.serial_connection.write_and_check('shutter?\r','') - return 0 # To be implemented + self.serial_connection.write_and_check("shutter?\r", "") + return 0 # To be implemented - def set_active_channel(self,channel): + def set_active_channel(self, channel): self.active_channel = channel - self.log.debug('[set active channel to ' + str(channel) + ']') + self.log.debug("[set active channel to " + str(channel) + "]") - def set_active_channel_shutter(self,state): + def set_active_channel_shutter(self, state): channel = str(self.active_channel) state = str(state) - self.log.debug('shutter:'+channel+'='+state+'\r') - self.serial_connection.write_and_check('shutter:'+channel+'='+state+'\r',"ok") + self.log.debug("shutter:" + channel + "=" + state + "\r") + self.serial_connection.write_and_check("shutter:" + channel + "=" + state + "\r", "ok") class LDI_Simulation(LightSource): """Wrapper for communicating with LDI over serial""" + def __init__(self, SN="00000001"): """ Provide serial number @@ -470,31 +510,31 @@ def __init__(self, SN="00000001"): 640: 640, 730: 730, 735: 730, - 750: 730 + 750: 730, } def initialize(self): pass - def set_shutter_mode(self,mode): + def set_shutter_mode(self, mode): if mode == ShutterControlMode.TTL: - self.serial_connection.write_and_check('SH_MODE=EXT\r',"ok") + self.serial_connection.write_and_check("SH_MODE=EXT\r", "ok") elif mode == ShutterControlMode.Software: - self.serial_connection.write_and_check('SH_MODE=PC\r',"ok") + self.serial_connection.write_and_check("SH_MODE=PC\r", "ok") self.shutter_mode = mode - def set_intensity_mode(self,mode): + def set_intensity_mode(self, mode): if mode == IntensityControlMode.SquidControllerDAC: - self.serial_connection.write_and_check('INT_MODE=EXT\r',"ok") + self.serial_connection.write_and_check("INT_MODE=EXT\r", "ok") elif mode == IntensityControlMode.Software: - self.serial_connection.write_and_check('INT_MODE=PC\r',"ok") + self.serial_connection.write_and_check("INT_MODE=PC\r", "ok") self.intensity_mode = mode - def set_intensity(self,channel,intensity): + def set_intensity(self, channel, intensity): channel = str(channel) intensity = "{:.2f}".format(intensity) - self.log.debug('set:'+channel+'='+intensity+'\r') - self.log.debug('active channel: ' + str(self.active_channel)) + self.log.debug("set:" + channel + "=" + intensity + "\r") + self.log.debug("active channel: " + str(self.active_channel)) def get_intensity(self, channel): return 0 @@ -502,32 +542,40 @@ def get_intensity(self, channel): def get_intensity_range(self): return [0, 100] - def set_shutter_state(self,channel,state): + def set_shutter_state(self, channel, state): channel = str(channel) state = str(state) def get_shutter_state(self, channel): return 0 - def set_active_channel(self,channel): + def set_active_channel(self, channel): self.active_channel = channel - self.log.debug('[set active channel to ' + str(channel) + ']') + self.log.debug("[set active channel to " + str(channel) + "]") - def set_active_channel_shutter(self,state): + def set_active_channel_shutter(self, state): channel = str(self.active_channel) state = str(state) - self.log.debug('shutter:'+channel+'='+state+'\r') + self.log.debug("shutter:" + channel + "=" + state + "\r") + class SciMicroscopyLEDArray: """Wrapper for communicating with SciMicroscopy over serial""" - def __init__(self, SN, array_distance = 50, turn_on_delay = 0.03): + + def __init__(self, SN, array_distance=50, turn_on_delay=0.03): """ Provide serial number """ - self.serial_connection = SerialDevice(SN=SN,baudrate=115200, - bytesize=serial.EIGHTBITS,stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - xonxoff=False,rtscts=False,dsrdtr=False) + self.serial_connection = SerialDevice( + SN=SN, + baudrate=115200, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) self.serial_connection.open_ser() self.check_about() self.set_distance(array_distance) @@ -537,59 +585,75 @@ def __init__(self, SN, array_distance = 50, turn_on_delay = 0.03): self.NA = 0.5 self.turn_on_delay = turn_on_delay - def write(self,command): - self.serial_connection.write_and_check(command+'\r','',read_delay=0.01,print_response=True) + def write(self, command): + self.serial_connection.write_and_check(command + "\r", "", read_delay=0.01, print_response=True) def check_about(self): - self.serial_connection.write_and_check('about'+'\r','=',read_delay=0.01,print_response=True) + self.serial_connection.write_and_check("about" + "\r", "=", read_delay=0.01, print_response=True) - def set_distance(self,array_distance): + def set_distance(self, array_distance): # array distance in mm array_distance = str(int(array_distance)) - self.serial_connection.write_and_check('sad.'+array_distance+'\r','Current array distance from sample is '+array_distance+'mm',read_delay=0.01,print_response=False) - - def set_NA(self,NA): + self.serial_connection.write_and_check( + "sad." + array_distance + "\r", + "Current array distance from sample is " + array_distance + "mm", + read_delay=0.01, + print_response=False, + ) + + def set_NA(self, NA): self.NA = NA - NA = str(int(NA*100)) - self.serial_connection.write_and_check('na.'+NA+'\r','Current NA is 0.'+NA,read_delay=0.01,print_response=False) + NA = str(int(NA * 100)) + self.serial_connection.write_and_check( + "na." + NA + "\r", "Current NA is 0." + NA, read_delay=0.01, print_response=False + ) - def set_color(self,color): + def set_color(self, color): # (r,g,b), 0-1 - r = int(255*color[0]) - g = int(255*color[1]) - b = int(255*color[2]) - self.serial_connection.write_and_check(f'sc.{r}.{g}.{b}\r',f'Current color balance values are {r}.{g}.{b}',read_delay=0.01,print_response=False) + r = int(255 * color[0]) + g = int(255 * color[1]) + b = int(255 * color[2]) + self.serial_connection.write_and_check( + f"sc.{r}.{g}.{b}\r", f"Current color balance values are {r}.{g}.{b}", read_delay=0.01, print_response=False + ) def set_brightness(self, brightness): # 0 to 100 - brightness = str(int(255*(brightness/100.0))) - self.serial_connection.write_and_check(f'sb.{brightness}\r',f'Current brightness value is {brightness}.',read_delay=0.01,print_response=False) + brightness = str(int(255 * (brightness / 100.0))) + self.serial_connection.write_and_check( + f"sb.{brightness}\r", f"Current brightness value is {brightness}.", read_delay=0.01, print_response=False + ) def turn_on_bf(self): - self.serial_connection.write_and_check(f'bf\r','-==-',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check(f"bf\r", "-==-", read_delay=0.01, print_response=False) - def turn_on_dpc(self,quadrant): - self.serial_connection.write_and_check(f'dpc.{quadrant[0]}\r','-==-',read_delay=0.01,print_response=False) + def turn_on_dpc(self, quadrant): + self.serial_connection.write_and_check(f"dpc.{quadrant[0]}\r", "-==-", read_delay=0.01, print_response=False) def turn_on_df(self): - self.serial_connection.write_and_check(f'df\r','-==-',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check(f"df\r", "-==-", read_delay=0.01, print_response=False) - def set_illumination(self,illumination): + def set_illumination(self, illumination): self.illumination = illumination def clear(self): - self.serial_connection.write_and_check('x\r','-==-',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check("x\r", "-==-", read_delay=0.01, print_response=False) def turn_on_illumination(self): if self.illumination is not None: - self.serial_connection.write_and_check(f'{self.illumination}\r','-==-',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + f"{self.illumination}\r", "-==-", read_delay=0.01, print_response=False + ) time.sleep(self.turn_on_delay) + def turn_off_illumination(self): self.clear() + class SciMicroscopyLEDArray_Simulation: """Wrapper for communicating with SciMicroscopy over serial""" - def __init__(self, SN, array_distance = 50, turn_on_delay = 0.03): + + def __init__(self, SN, array_distance=50, turn_on_delay=0.03): """ Provide serial number """ @@ -602,40 +666,40 @@ def __init__(self, SN, array_distance = 50, turn_on_delay = 0.03): self.NA = 0.5 self.turn_on_delay = turn_on_delay - def write(self,command): + def write(self, command): pass def check_about(self): pass - def set_distance(self,array_distance): + def set_distance(self, array_distance): # array distance in mm array_distance = str(int(array_distance)) - def set_NA(self,NA): + def set_NA(self, NA): self.NA = NA - NA = str(int(NA*100)) + NA = str(int(NA * 100)) - def set_color(self,color): + def set_color(self, color): # (r,g,b), 0-1 - r = int(255*color[0]) - g = int(255*color[1]) - b = int(255*color[2]) + r = int(255 * color[0]) + g = int(255 * color[1]) + b = int(255 * color[2]) def set_brightness(self, brightness): # 0 to 100 - brightness = str(int(255*(brightness/100.0))) + brightness = str(int(255 * (brightness / 100.0))) def turn_on_bf(self): pass - def turn_on_dpc(self,quadrant): + def turn_on_dpc(self, quadrant): pass def turn_on_df(self): pass - def set_illumination(self,illumination): + def set_illumination(self, illumination): pass def clear(self): @@ -647,50 +711,77 @@ def turn_on_illumination(self): def turn_off_illumination(self): pass + class CellX: - VALID_MODULATIONS = ['INT','EXT Digital','EXT Analog','EXT Mixed'] + VALID_MODULATIONS = ["INT", "EXT Digital", "EXT Analog", "EXT Mixed"] """Wrapper for communicating with LDI over serial""" + def __init__(self, SN=""): - self.serial_connection = SerialDevice(SN=SN,baudrate=115200, - bytesize=serial.EIGHTBITS,stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - xonxoff=False,rtscts=False,dsrdtr=False) + self.serial_connection = SerialDevice( + SN=SN, + baudrate=115200, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) self.serial_connection.open_ser() self.power = {} def turn_on(self, channel): - self.serial_connection.write_and_check('SOUR'+str(channel)+':AM:STAT ON\r','OK',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + "SOUR" + str(channel) + ":AM:STAT ON\r", "OK", read_delay=0.01, print_response=False + ) def turn_off(self, channel): - self.serial_connection.write_and_check('SOUR'+str(channel)+':AM:STAT OFF\r','OK',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + "SOUR" + str(channel) + ":AM:STAT OFF\r", "OK", read_delay=0.01, print_response=False + ) def set_laser_power(self, channel, power): if not (power >= 1 and power <= 100): raise ValueError(f"Power={power} not in the range 1 to 100") if channel not in self.power.keys() or power != self.power[channel]: - self.serial_connection.write_and_check('SOUR'+str(channel)+':POW:LEV:IMM:AMPL '+str(power/1000)+'\r','OK',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + "SOUR" + str(channel) + ":POW:LEV:IMM:AMPL " + str(power / 1000) + "\r", + "OK", + read_delay=0.01, + print_response=False, + ) self.power[channel] = power else: - pass # power is the same + pass # power is the same def set_modulation(self, channel, modulation): if modulation not in CellX.VALID_MODULATIONS: raise ValueError(f"Modulation '{modulation}' not in valid modulations: {CellX.VALID_MODULATIONS}") - self.serial_connection.write_and_check('SOUR'+str(channel)+':AM:' + modulation +'\r','OK',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + "SOUR" + str(channel) + ":AM:" + modulation + "\r", "OK", read_delay=0.01, print_response=False + ) def close(self): self.serial_connection.close() + class CellX_Simulation: """Wrapper for communicating with LDI over serial""" + def __init__(self, SN=""): - self.serial_connection = SerialDevice(SN=SN,baudrate=115200, - bytesize=serial.EIGHTBITS,stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - xonxoff=False,rtscts=False,dsrdtr=False) + self.serial_connection = SerialDevice( + SN=SN, + baudrate=115200, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + xonxoff=False, + rtscts=False, + dsrdtr=False, + ) self.serial_connection.open_ser() self.power = {} @@ -707,39 +798,45 @@ def set_laser_power(self, channel, power): if channel not in self.power.keys() or power != self.power[channel]: self.power[channel] = power else: - pass # power is the same + pass # power is the same def set_modulation(self, channel, modulation): if modulation not in CellX.VALID_MODULATIONS: raise ValueError(f"modulation '{modulation}' not in valid choices: {CellX.VALID_MODULATIONS}") - self.serial_connection.write_and_check('SOUR'+str(channel)+'AM:' + modulation +'\r','OK',read_delay=0.01,print_response=False) + self.serial_connection.write_and_check( + "SOUR" + str(channel) + "AM:" + modulation + "\r", "OK", read_delay=0.01, print_response=False + ) def close(self): pass + class FilterDeviceInfo: """ - keep filter device information + keep filter device information """ + # default: 7.36 - firmware_version = '' + firmware_version = "" # default: 250000 maxspeed = 0 - # default: 900 + # default: 900 accel = 0 + class FilterController_Simulation: """ controller of filter device """ + def __init__(self, _baudrate, _bytesize, _parity, _stopbits): self.each_hole_microsteps = 4800 self.current_position = 0 self.current_index = 1 - ''' + """ the variable be used to keep current offset of wheel it could be used by get the index of wheel position, the index could be '1', '2', '3' ... - ''' + """ self.offset_position = 0 self.deviceinfo = FilterDeviceInfo() @@ -770,10 +867,13 @@ def complete_homing_sequence(self): def wait_for_homing_complete(self): pass + class FilterControllerError(Exception): """Custom exception for FilterController errors.""" + pass + class FilterController: """Controller for filter device.""" @@ -790,23 +890,32 @@ def __init__(self, serial_number: str, baudrate: int, bytesize: int, parity: str self.serial = self._initialize_serial(serial_number, baudrate, bytesize, parity, stopbits) self._configure_device() - def _initialize_serial(self, serial_number: str, baudrate: int, bytesize: int, parity: str, stopbits: int) -> serial.Serial: + def _initialize_serial( + self, serial_number: str, baudrate: int, bytesize: int, parity: str, stopbits: int + ) -> serial.Serial: ports = [p.device for p in list_ports.comports() if serial_number == p.serial_number] if not ports: raise ValueError(f"No device found with serial number: {serial_number}") - return serial.Serial(ports[0], baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=self.COMMAND_TIMEOUT) + return serial.Serial( + ports[0], + baudrate=baudrate, + bytesize=bytesize, + parity=parity, + stopbits=stopbits, + timeout=self.COMMAND_TIMEOUT, + ) def _configure_device(self): time.sleep(0.2) - self.firmware_version = self._get_device_info('/get version') - self._send_command_with_reply('/set maxspeed 250000') - self._send_command_with_reply('/set accel 900') - self.maxspeed = self._get_device_info('/get maxspeed') - self.accel = self._get_device_info('/get accel') + self.firmware_version = self._get_device_info("/get version") + self._send_command_with_reply("/set maxspeed 250000") + self._send_command_with_reply("/set accel 900") + self.maxspeed = self._get_device_info("/get maxspeed") + self.accel = self._get_device_info("/get accel") def __del__(self): - if hasattr(self, 'serial') and self.serial.is_open: - self._send_command('/stop') + if hasattr(self, "serial") and self.serial.is_open: + self._send_command("/stop") time.sleep(0.5) self.serial.close() @@ -828,13 +937,13 @@ def _send_command(self, cmd: str) -> Tuple[bool, str]: for attempt in range(self.MAX_RETRIES): try: - self.serial.write(f"{cmd}\n".encode('utf-8')) - response = self.serial.readline().decode('utf-8').strip() + self.serial.write(f"{cmd}\n".encode("utf-8")) + response = self.serial.readline().decode("utf-8").strip() success, message = self._parse_response(response) if success: return True, message - elif message.startswith('BUSY'): + elif message.startswith("BUSY"): time.sleep(0.1) # Wait a bit if the device is busy continue else: @@ -864,28 +973,28 @@ def _parse_response(self, response: str) -> Tuple[bool, str]: if len(parts) < 4: return False, f"Invalid response format: {response}" - if parts[0].startswith('@'): - if parts[2] == 'OK': - return True, ' '.join(parts[3:]) + if parts[0].startswith("@"): + if parts[2] == "OK": + return True, " ".join(parts[3:]) else: - return False, ' '.join(parts[2:]) - elif parts[0].startswith('!'): + return False, " ".join(parts[2:]) + elif parts[0].startswith("!"): return False, f"Alert: {' '.join(parts[1:])}" - elif parts[0].startswith('#'): + elif parts[0].startswith("#"): return True, f"Info: {' '.join(parts[1:])}" else: return False, f"Unknown response format: {response}" def _send_command_with_reply(self, cmd: str) -> bool: success, message = self._send_command(cmd) - return success and (message == 'IDLE' or message.startswith('BUSY')) + return success and (message == "IDLE" or message.startswith("BUSY")) def _get_device_info(self, cmd: str) -> Optional[str]: success, message = self._send_command(cmd) return message if success else None def get_current_position(self) -> Tuple[bool, int]: - success, message = self._send_command('/get pos') + success, message = self._send_command("/get pos") if success: try: return True, int(message.split()[-1]) @@ -900,7 +1009,7 @@ def move_to_offset_position(self): self._move_to_absolute_position(self.OFFSET_POSITION) def _move_to_absolute_position(self, target_position: int, timeout: int = 5): - success, _ = self._send_command(f'/move abs {target_position}') + success, _ = self._send_command(f"/move abs {target_position}") if not success: raise FilterControllerError("Failed to initiate filter movement") self._wait_for_position(target_position, target_index=None, timeout=timeout) @@ -908,12 +1017,12 @@ def _move_to_absolute_position(self, target_position: int, timeout: int = 5): def set_emission_filter(self, index: int, blocking: bool = True, timeout: int = 5): """ Set the emission filter to the specified position. - + Args: position (int): The desired filter position (1-7). blocking (bool): If True, wait for the movement to complete. If False, return immediately. timeout (int): Maximum time to wait for the movement to complete (in seconds). - + Raises: ValueError: If the position is invalid. FilterControllerError: If the command fails to initiate movement. @@ -923,7 +1032,7 @@ def set_emission_filter(self, index: int, blocking: bool = True, timeout: int = raise ValueError(f"Invalid emission filter wheel index position: {index}") target_position = self.OFFSET_POSITION + (index - 1) * self.MICROSTEPS_PER_HOLE - success, _ = self._send_command(f'/move abs {target_position}') + success, _ = self._send_command(f"/move abs {target_position}") if not success: raise FilterControllerError("Failed to initiate filter movement") @@ -935,14 +1044,14 @@ def set_emission_filter(self, index: int, blocking: bool = True, timeout: int = self.current_position = target_position self.current_index = index - def _wait_for_position(self, target_position: int, target_index : int, timeout: int): + def _wait_for_position(self, target_position: int, target_index: int, timeout: int): """ Wait for the filter to reach the target position. - + Args: target_position (int): The expected final position. timeout (int): Maximum time to wait (in seconds). - + Raises: TimeoutError: If the movement doesn't complete within the specified timeout. """ @@ -969,7 +1078,7 @@ def start_homing(self): Raises: FilterControllerError: If the homing command fails to initiate. """ - success, _ = self._send_command('/home') + success, _ = self._send_command("/home") if not success: raise FilterControllerError("Failed to initiate homing sequence") @@ -1029,7 +1138,7 @@ def __init__(self, SN, baudrate=115200, timeout=1, max_retries=3, retry_delay=0. def _send_command(self, command, data=None): if data is None: data = [] - full_command = struct.pack('>H', command) + bytes(data) + full_command = struct.pack(">H", command) + bytes(data) for attempt in range(self.max_retries): try: @@ -1039,7 +1148,7 @@ def _send_command(self, command, data=None): if len(response) != 2: raise serial.SerialTimeoutException("Timeout: No response from device") - status, length = struct.unpack('>BB', response) + status, length = struct.unpack(">BB", response) if status != 0xFF: raise Exception(f"Command failed with status: {status}") @@ -1059,14 +1168,13 @@ def _send_command(self, command, data=None): else: raise Exception(f"Command failed after {self.max_retries} attempts: {str(e)}") - def get_version(self): result = self._send_command(0x0040) - return struct.unpack('>BB', result) + return struct.unpack(">BB", result) def set_speed(self, speed): speed_int = int(speed * 100) - self._send_command(0x0048, struct.pack('BBBB', result) + return struct.unpack(">BBBB", result) def close(self): self.ser.close() + class Optospin_Simulation: def __init__(self, SN, baudrate=115200, timeout=1, max_retries=3, retry_delay=0.5): self.current_index = 1 @@ -1128,7 +1237,7 @@ def set_emission_filter(self, index): pass def get_rotor_positions(self): - return 0,0,0,0 + return 0, 0, 0, 0 def measure_temperatures(self): pass diff --git a/software/control/spectrometer_oceanoptics.py b/software/control/spectrometer_oceanoptics.py index 721daff73..9bf197ce0 100644 --- a/software/control/spectrometer_oceanoptics.py +++ b/software/control/spectrometer_oceanoptics.py @@ -3,20 +3,22 @@ import time import numpy as np import threading + try: import seabreeze as sb import seabreeze.spectrometers except: - print('seabreeze import error') + print("seabreeze import error") # installation: $ pip3 install seabreeze # installation: $ seabreeze_os_setup from control._def import * + class Spectrometer(object): - def __init__(self,sn=None): + def __init__(self, sn=None): if sn == None: self.spectrometer = sb.spectrometers.Spectrometer.from_first_available() else: @@ -31,16 +33,16 @@ def __init__(self,sn=None): self.thread_streaming = threading.Thread(target=self.stream, daemon=True) - def set_integration_time_ms(self,integration_time_ms): - self.spectrometer.integration_time_micros(int(1000*integration_time_ms)) + def set_integration_time_ms(self, integration_time_ms): + self.spectrometer.integration_time_micros(int(1000 * integration_time_ms)) - def read_spectrum(self,correct_dark_counts=False,correct_nonlinearity=False): + def read_spectrum(self, correct_dark_counts=False, correct_nonlinearity=False): self.is_reading_spectrum = True - data = self.spectrometer.spectrum(correct_dark_counts,correct_nonlinearity) + data = self.spectrometer.spectrum(correct_dark_counts, correct_nonlinearity) self.is_reading_spectrum = False return data - def set_callback(self,function): + def set_callback(self, function): self.new_data_callback_external = function def start_streaming(self): @@ -74,9 +76,10 @@ def close(self): self.thread_streaming.join() self.spectrometer.close() + class Spectrometer_Simulation(object): - - def __init__(self,sn=None): + + def __init__(self, sn=None): self.new_data_callback_external = None self.streaming_started = False self.stop_streaming = False @@ -84,16 +87,16 @@ def __init__(self,sn=None): self.is_reading_spectrum = False self.thread_streaming = threading.Thread(target=self.stream, daemon=True) - def set_integration_time_us(self,integration_time_us): + def set_integration_time_us(self, integration_time_us): pass - def read_spectrum(self,correct_dark_counts=False,correct_nonlinearity=False): + def read_spectrum(self, correct_dark_counts=False, correct_nonlinearity=False): N = 4096 - wavelength = np.linspace(400,1100,N) - intensity = np.random.randint(0,65536,N) - return np.stack((wavelength,intensity)) - - def set_callback(self,function): + wavelength = np.linspace(400, 1100, N) + intensity = np.random.randint(0, 65536, N) + return np.stack((wavelength, intensity)) + + def set_callback(self, function): self.new_data_callback_external = function def start_streaming(self): @@ -119,10 +122,10 @@ def stream(self): while self.is_reading_spectrum: time.sleep(0.05) if self.new_data_callback_external != None: - print('read spectrum...') + print("read spectrum...") self.new_data_callback_external(self.read_spectrum()) def close(self): if self.streaming_started: self.stop_streaming = True - self.thread_streaming.join() \ No newline at end of file + self.thread_streaming.join() diff --git a/software/control/stage_prior.py b/software/control/stage_prior.py index 0a73d86eb..2739949ac 100644 --- a/software/control/stage_prior.py +++ b/software/control/stage_prior.py @@ -3,7 +3,8 @@ import re import threading -class PriorStage(): + +class PriorStage: def __init__(self, sn, baudrate=115200, timeout=0.1, parent=None): port = [p.device for p in serial.tools.list_ports.comports() if sn == p.serial_number] self.serial = serial.Serial(port[0], baudrate=baudrate, timeout=timeout) @@ -23,14 +24,14 @@ def __init__(self, sn, baudrate=115200, timeout=0.1, parent=None): self.joystick_enabled = False # Prior-specific properties - self.stage_microsteps_per_mm = 100000 # Stage property + self.stage_microsteps_per_mm = 100000 # Stage property self.user_unit = None self.stage_model = None self.stage_limits = None self.resolution = 0.1 - self.x_direction = 1 # 1 or -1 - self.y_direction = 1 # 1 or -1 - self.speed = 200 # Default value + self.x_direction = 1 # 1 or -1 + self.y_direction = 1 # 1 or -1 + self.speed = 200 # Default value self.acceleration = 500 # Default value # Position updating callback @@ -45,9 +46,9 @@ def __init__(self, sn, baudrate=115200, timeout=0.1, parent=None): self.position_updating_thread.start() def set_baudrate(self, baud): - allowed_baudrates = {9600: '96', 19200: '19', 38400: '38', 115200: '115'} + allowed_baudrates = {9600: "96", 19200: "19", 38400: "38", 115200: "115"} if baud not in allowed_baudrates: - print('Baudrate not allowed. Setting baudrate to 9600') + print("Baudrate not allowed. Setting baudrate to 9600") baud_command = "BAUD 96" else: baud_command = "BAUD " + allowed_baudrates[baud] @@ -55,14 +56,14 @@ def set_baudrate(self, baud): for bd in allowed_baudrates: self.serial.baudrate = bd - self.serial.write(b'\r') + self.serial.write(b"\r") time.sleep(0.1) self.serial.flushInput() self.send_command(baud_command) self.serial.baudrate = baud - + try: test_response = self.send_command("$") # Send a simple query command if not test_response: @@ -78,12 +79,12 @@ def set_baudrate(self, baud): print(f"Failed to verify communication at new baud rate: {e}") raise Exception("Failed to set baudrate.") - + def initialize(self): self.send_command("COMP 0") # Set to standard mode self.send_command("BLSH 1") # Enable backlash correction self.send_command("RES,S," + str(self.resolution)) # Set resolution - response = self.send_command("H 0") # Joystick enabled + response = self.send_command("H 0") # Joystick enabled self.joystick_enabled = True self.user_unit = self.stage_microsteps_per_mm * self.resolution self.get_stage_info() @@ -94,13 +95,13 @@ def send_command(self, command): with self.serial_lock: self.serial.write(f"{command}\r".encode()) response = self.serial.readline().decode().strip() - if response.startswith('E'): + if response.startswith("E"): raise Exception(f"Error from controller: {response}") return response def get_stage_info(self): stage_info = self.send_command("STAGE") - self.stage_model = re.search(r'STAGE\s*=\s*(\S+)', stage_info).group(1) + self.stage_model = re.search(r"STAGE\s*=\s*(\S+)", stage_info).group(1) print("Stage model: ", self.stage_model) def mm_to_steps(self, mm): @@ -152,7 +153,7 @@ def home_xy(self): self.wait_for_stop() self.x_pos = 0 self.y_pos = 0 - print('finished homing') + print("finished homing") def home_x(self): self.move_relative(-self.x_pos, 0) @@ -175,7 +176,7 @@ def zero_y(self): def get_pos(self): response = self.send_command("P") - x, y, z = map(int, response.split(',')) + x, y, z = map(int, response.split(",")) self.x_pos = x self.y_pos = y return x, y, 0, 0 # Z and theta are 0 @@ -250,7 +251,7 @@ def wait_for_stop(self): status = int(self.send_command("$,S")) if status == 0: self.get_pos() - print('xy position: ', self.x_pos, self.y_pos) + print("xy position: ", self.x_pos, self.y_pos) break time.sleep(0.05) @@ -262,11 +263,10 @@ def close(self): self.position_updating_event.set() self.position_updating_thread.join() self.serial.close() - print('Stage closed') + print("Stage closed") def home_z(self): pass def zero_z(self): pass - diff --git a/software/control/stitcher.py b/software/control/stitcher.py index 81a7138c2..e58148cf2 100644 --- a/software/control/stitcher.py +++ b/software/control/stitcher.py @@ -27,6 +27,7 @@ from aicsimageio import types from basicpy import BaSiC + class Stitcher(QThread, QObject): update_progress = Signal(int, int) @@ -35,7 +36,17 @@ class Stitcher(QThread, QObject): starting_saving = Signal(bool) finished_saving = Signal(str, object) - def __init__(self, input_folder, output_name='', output_format=".ome.zarr", apply_flatfield=0, use_registration=0, registration_channel='', registration_z_level=0, flexible=True): + def __init__( + self, + input_folder, + output_name="", + output_format=".ome.zarr", + apply_flatfield=0, + use_registration=0, + registration_channel="", + registration_z_level=0, + flexible=True, + ): QThread.__init__(self) QObject.__init__(self) self.input_folder = input_folder @@ -51,7 +62,7 @@ def __init__(self, input_folder, output_name='', output_format=".ome.zarr", appl self.acquisition_params = self.extract_acquisition_parameters(self.input_folder) self.time_points = self.get_time_points(self.input_folder) print("timepoints:", self.time_points) - self.is_reversed = self.determine_directions(self.input_folder) # init: top to bottom, left to right + self.is_reversed = self.determine_directions(self.input_folder) # init: top to bottom, left to right print(self.is_reversed) self.is_wellplate = IS_HCS self.flexible = flexible @@ -69,58 +80,66 @@ def init_stitching_parameters(self): self.num_cols = self.num_rows = 1 self.input_height = self.input_width = 0 self.num_pyramid_levels = 5 - self.v_shift = self.h_shift = (0,0) + self.v_shift = self.h_shift = (0, 0) self.max_x_overlap = self.max_y_overlap = 0 self.flatfields = {} self.stitching_data = {} - self.tczyx_shape = (len(self.time_points),self.num_c,self.num_z,self.num_rows*self.input_height,self.num_cols*self.input_width) + self.tczyx_shape = ( + len(self.time_points), + self.num_c, + self.num_z, + self.num_rows * self.input_height, + self.num_cols * self.input_width, + ) self.stitched_images = None self.chunks = None self.dtype = np.uint16 def get_time_points(self, input_folder): - try: # detects directories named as integers, representing time points. - time_points = [d for d in os.listdir(input_folder) if os.path.isdir(os.path.join(input_folder, d)) and d.isdigit()] + try: # detects directories named as integers, representing time points. + time_points = [ + d for d in os.listdir(input_folder) if os.path.isdir(os.path.join(input_folder, d)) and d.isdigit() + ] time_points.sort(key=int) return time_points except Exception as e: print(f"Error detecting time points: {e}") - return ['0'] + return ["0"] def extract_selected_modes(self, input_folder): try: - configs_path = os.path.join(input_folder, 'configurations.xml') + configs_path = os.path.join(input_folder, "configurations.xml") tree = etree.parse(configs_path) root = tree.getroot() selected_modes = {} - for mode in root.findall('.//mode'): - if mode.get('Selected') == '1': - mode_id = mode.get('ID') + for mode in root.findall(".//mode"): + if mode.get("Selected") == "1": + mode_id = mode.get("ID") selected_modes[mode_id] = { - 'Name': mode.get('Name'), - 'ExposureTime': mode.get('ExposureTime'), - 'AnalogGain': mode.get('AnalogGain'), - 'IlluminationSource': mode.get('IlluminationSource'), - 'IlluminationIntensity': mode.get('IlluminationIntensity') + "Name": mode.get("Name"), + "ExposureTime": mode.get("ExposureTime"), + "AnalogGain": mode.get("AnalogGain"), + "IlluminationSource": mode.get("IlluminationSource"), + "IlluminationIntensity": mode.get("IlluminationIntensity"), } return selected_modes except Exception as e: print(f"Error reading selected modes: {e}") def extract_acquisition_parameters(self, input_folder): - acquistion_params_path = os.path.join(input_folder, 'acquisition parameters.json') - with open(acquistion_params_path, 'r') as file: + acquistion_params_path = os.path.join(input_folder, "acquisition parameters.json") + with open(acquistion_params_path, "r") as file: acquisition_params = json.load(file) return acquisition_params def extract_wavelength(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 "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']: + for color in ["R", "G", "B"]: if color in parts: return color return None @@ -129,18 +148,18 @@ def determine_directions(self, input_folder): # return {'rows': self.acquisition_params.get("row direction", False), # 'cols': self.acquisition_params.get("col direction", False), # 'z-planes': False} - coordinates = pd.read_csv(os.path.join(input_folder, self.time_points[0], 'coordinates.csv')) + coordinates = pd.read_csv(os.path.join(input_folder, self.time_points[0], "coordinates.csv")) try: - first_region = coordinates['region'].unique()[0] - coordinates = coordinates[coordinates['region'] == first_region] + first_region = coordinates["region"].unique()[0] + coordinates = coordinates[coordinates["region"] == first_region] self.is_wellplate = True except Exception as e: print("no coordinates.csv well data:", e) self.is_wellplate = False - i_rev = not coordinates.sort_values(by='i')['y (mm)'].is_monotonic_increasing - j_rev = not coordinates.sort_values(by='j')['x (mm)'].is_monotonic_increasing - k_rev = not coordinates.sort_values(by='z_level')['z (um)'].is_monotonic_increasing - return {'rows': i_rev, 'cols': j_rev, 'z-planes': k_rev} + i_rev = not coordinates.sort_values(by="i")["y (mm)"].is_monotonic_increasing + j_rev = not coordinates.sort_values(by="j")["x (mm)"].is_monotonic_increasing + k_rev = not coordinates.sort_values(by="z_level")["z (um)"].is_monotonic_increasing + return {"rows": i_rev, "cols": j_rev, "z-planes": k_rev} def parse_filenames(self, time_point): # Initialize directories and read files @@ -150,19 +169,23 @@ def parse_filenames(self, time_point): all_files = os.listdir(self.image_folder) sorted_input_files = sorted( - [filename for filename in all_files if filename.endswith((".bmp", ".tiff")) and 'focus_camera' not in filename] + [ + filename + for filename in all_files + if filename.endswith((".bmp", ".tiff")) and "focus_camera" not in filename + ] ) if not sorted_input_files: raise Exception("No valid files found in directory.") first_filename = sorted_input_files[0] try: - first_region, first_i, first_j, first_k, channel_name = os.path.splitext(first_filename)[0].split('_', 4) + first_region, first_i, first_j, first_k, channel_name = os.path.splitext(first_filename)[0].split("_", 4) first_k = int(first_k) print("region_i_j_k_channel_name: ", os.path.splitext(first_filename)[0]) self.is_wellplate = True except ValueError as ve: - first_i, first_j, first_k, channel_name = os.path.splitext(first_filename)[0].split('_', 3) + first_i, first_j, first_k, channel_name = os.path.splitext(first_filename)[0].split("_", 3) print("i_j_k_channel_name: ", os.path.splitext(first_filename)[0]) self.is_wellplate = False @@ -172,10 +195,10 @@ def parse_filenames(self, time_point): for filename in sorted_input_files: if self.is_wellplate: - region, i, j, k, channel_name = os.path.splitext(filename)[0].split('_', 4) + region, i, j, k, channel_name = os.path.splitext(filename)[0].split("_", 4) else: - region = '0' - i, j, k, channel_name = os.path.splitext(filename)[0].split('_', 3) + region = "0" + i, j, k, channel_name = os.path.splitext(filename)[0].split("_", 3) channel_name = channel_name.replace("_", " ").replace("full ", "full_") i, j, k = int(i), int(j), int(k) @@ -185,20 +208,26 @@ def parse_filenames(self, time_point): max_i, max_j, max_k = max(max_i, i), max(max_j, j), max(max_k, k) tile_info = { - 'filepath': os.path.join(self.image_folder, filename), - 'region': region, - 'channel': channel_name, - 'z_level': k, - 'row': i, - 'col': j + "filepath": os.path.join(self.image_folder, filename), + "region": region, + "channel": channel_name, + "z_level": k, + "row": i, + "col": j, } - self.stitching_data.setdefault(region, {}).setdefault(channel_name, {}).setdefault(k, {}).setdefault((i, j), tile_info) + self.stitching_data.setdefault(region, {}).setdefault(channel_name, {}).setdefault(k, {}).setdefault( + (i, j), tile_info + ) self.regions = sorted(regions) self.channel_names = sorted(channel_names) self.num_z, self.num_cols, self.num_rows = max_k + 1, max_j + 1, max_i + 1 - first_coord = f"{self.regions[0]}_{first_i}_{first_j}_{first_k}_" if self.is_wellplate else f"{first_i}_{first_j}_{first_k}_" + first_coord = ( + f"{self.regions[0]}_{first_i}_{first_j}_{first_k}_" + if self.is_wellplate + else f"{first_i}_{first_j}_{first_k}_" + ) found_dims = False mono_channel_names = [] @@ -209,13 +238,13 @@ def parse_filenames(self, time_point): if not found_dims: self.dtype = np.dtype(image.dtype) self.input_height, self.input_width = image.shape[:2] - self.chunks = (1, 1, 1, self.input_height//2, self.input_width//2) + self.chunks = (1, 1, 1, self.input_height // 2, self.input_width // 2) found_dims = True print("chunks", self.chunks) if len(image.shape) == 3: self.is_rgb[channel] = True - channel = channel.split('_')[0] + channel = channel.split("_")[0] mono_channel_names.extend([f"{channel}_R", f"{channel}_G", f"{channel}_B"]) else: self.is_rgb[channel] = False @@ -223,7 +252,10 @@ def parse_filenames(self, time_point): self.mono_channel_names = mono_channel_names self.num_c = len(mono_channel_names) - self.channel_colors = [CHANNEL_COLORS_MAP.get(self.extract_wavelength(name), {'hex': 0xFFFFFF})['hex'] for name in self.mono_channel_names] + self.channel_colors = [ + CHANNEL_COLORS_MAP.get(self.extract_wavelength(name), {"hex": 0xFFFFFF})["hex"] + for name in self.mono_channel_names + ] print(self.mono_channel_names) print(self.regions) @@ -248,20 +280,20 @@ def process_images(images, channel_name): # Shuffle and select a subset of tiles for flatfield calculation random.shuffle(all_tiles) - selected_tiles = all_tiles[:min(32, len(all_tiles))] + selected_tiles = all_tiles[: min(32, len(all_tiles))] if self.is_rgb[channel]: # Process each color channel if the channel is RGB - images_r = [dask_imread(tile['filepath'])[0][:, :, 0] for tile in selected_tiles] - images_g = [dask_imread(tile['filepath'])[0][:, :, 1] for tile in selected_tiles] - images_b = [dask_imread(tile['filepath'])[0][:, :, 2] for tile in selected_tiles] - channel = channel.split('_')[0] - process_images(images_r, channel + '_R') - process_images(images_g, channel + '_G') - process_images(images_b, channel + '_B') + images_r = [dask_imread(tile["filepath"])[0][:, :, 0] for tile in selected_tiles] + images_g = [dask_imread(tile["filepath"])[0][:, :, 1] for tile in selected_tiles] + images_b = [dask_imread(tile["filepath"])[0][:, :, 2] for tile in selected_tiles] + channel = channel.split("_")[0] + process_images(images_r, channel + "_R") + process_images(images_g, channel + "_G") + process_images(images_b, channel + "_B") else: # Process monochrome images - images = [dask_imread(tile['filepath'])[0] for tile in selected_tiles] + images = [dask_imread(tile["filepath"])[0] for tile in selected_tiles] process_images(images, channel) def normalize_image(self, img): @@ -271,7 +303,7 @@ def normalize_image(self, img): return (img_normalized * scale_factor).astype(self.dtype) def visualize_image(self, img1, img2, title): - if title == 'horizontal': + if title == "horizontal": combined_image = np.hstack((img1, img2)) else: combined_image = np.vstack((img1, img2)) @@ -315,15 +347,17 @@ def calculate_vertical_shift(self, img1_path, img2_path, max_overlap, margin_rat def calculate_shifts(self, roi=""): roi = self.regions[0] if roi not in self.regions else roi - self.registration_channel = self.registration_channel if self.registration_channel in self.channel_names else self.channel_names[0] + self.registration_channel = ( + self.registration_channel if self.registration_channel in self.channel_names else self.channel_names[0] + ) # Calculate estimated overlap from acquisition parameters - dx_mm = self.acquisition_params['dx(mm)'] - dy_mm = self.acquisition_params['dy(mm)'] - obj_mag = self.acquisition_params['objective']['magnification'] - obj_tube_lens_mm = self.acquisition_params['objective']['tube_lens_f_mm'] - sensor_pixel_size_um = self.acquisition_params['sensor_pixel_size_um'] - tube_lens_mm = self.acquisition_params['tube_lens_mm'] + dx_mm = self.acquisition_params["dx(mm)"] + dy_mm = self.acquisition_params["dy(mm)"] + obj_mag = self.acquisition_params["objective"]["magnification"] + obj_tube_lens_mm = self.acquisition_params["objective"]["tube_lens_f_mm"] + sensor_pixel_size_um = self.acquisition_params["sensor_pixel_size_um"] + tube_lens_mm = self.acquisition_params["tube_lens_mm"] obj_focal_length_mm = obj_tube_lens_mm / obj_mag actual_mag = tube_lens_mm / obj_focal_length_mm @@ -336,24 +370,28 @@ def calculate_shifts(self, roi=""): self.max_x_overlap = round(abs(self.input_width - dx_pixels) / 2) self.max_y_overlap = round(abs(self.input_height - dy_pixels) / 2) - print("objective calculated - vertical overlap:", self.max_y_overlap, ", horizontal overlap:", self.max_x_overlap) + print( + "objective calculated - vertical overlap:", self.max_y_overlap, ", horizontal overlap:", self.max_x_overlap + ) col_left, col_right = (self.num_cols - 1) // 2, (self.num_cols - 1) // 2 + 1 - if self.is_reversed['cols']: + if self.is_reversed["cols"]: col_left, col_right = col_right, col_left row_top, row_bottom = (self.num_rows - 1) // 2, (self.num_rows - 1) // 2 + 1 - if self.is_reversed['rows']: + if self.is_reversed["rows"]: row_top, row_bottom = row_bottom, row_top img1_path = img2_path_vertical = img2_path_horizontal = None - for (row, col), tile_info in self.stitching_data[roi][self.registration_channel][self.registration_z_level].items(): + for (row, col), tile_info in self.stitching_data[roi][self.registration_channel][ + self.registration_z_level + ].items(): if col == col_left and row == row_top: - img1_path = tile_info['filepath'] + img1_path = tile_info["filepath"] elif col == col_left and row == row_bottom: - img2_path_vertical = tile_info['filepath'] + img2_path_vertical = tile_info["filepath"] elif col == col_right and row == row_top: - img2_path_horizontal = tile_info['filepath'] + img2_path_horizontal = tile_info["filepath"] if img1_path is None: raise Exception( @@ -363,11 +401,13 @@ def calculate_shifts(self, roi=""): self.v_shift = ( self.calculate_vertical_shift(img1_path, img2_path_vertical, self.max_y_overlap) - if self.max_y_overlap > 0 and img2_path_vertical and img1_path != img2_path_vertical else (0, 0) + if self.max_y_overlap > 0 and img2_path_vertical and img1_path != img2_path_vertical + else (0, 0) ) self.h_shift = ( self.calculate_horizontal_shift(img1_path, img2_path_horizontal, self.max_x_overlap) - if self.max_x_overlap > 0 and img2_path_horizontal and img1_path != img2_path_horizontal else (0, 0) + if self.max_x_overlap > 0 and img2_path_horizontal and img1_path != img2_path_horizontal + else (0, 0) ) print("vertical shift:", self.v_shift, ", horizontal shift:", self.h_shift) @@ -376,27 +416,31 @@ def calculate_dynamic_shifts(self, roi, channel, z_level, row, col): # Check for left neighbor if (row, col - 1) in self.stitching_data[roi][channel][z_level]: - left_tile_path = self.stitching_data[roi][channel][z_level][row, col - 1]['filepath'] - current_tile_path = self.stitching_data[roi][channel][z_level][row, col]['filepath'] + left_tile_path = self.stitching_data[roi][channel][z_level][row, col - 1]["filepath"] + current_tile_path = self.stitching_data[roi][channel][z_level][row, col]["filepath"] # Calculate horizontal shift new_h_shift = self.calculate_horizontal_shift(left_tile_path, current_tile_path, abs(self.h_shift[1])) # Check if the new horizontal shift is within 10% of the precomputed shift - if self.h_shift == (0,0) or (0.95 * abs(self.h_shift[1]) <= abs(new_h_shift[1]) <= 1.05 * abs(self.h_shift[1]) and - 0.95 * abs(self.h_shift[0]) <= abs(new_h_shift[0]) <= 1.05 * abs(self.h_shift[0])): + if self.h_shift == (0, 0) or ( + 0.95 * abs(self.h_shift[1]) <= abs(new_h_shift[1]) <= 1.05 * abs(self.h_shift[1]) + and 0.95 * abs(self.h_shift[0]) <= abs(new_h_shift[0]) <= 1.05 * abs(self.h_shift[0]) + ): print("new h shift", new_h_shift, h_shift) h_shift = new_h_shift # Check for top neighbor if (row - 1, col) in self.stitching_data[roi][channel][z_level]: - top_tile_path = self.stitching_data[roi][channel][z_level][row - 1, col]['filepath'] - current_tile_path = self.stitching_data[roi][channel][z_level][row, col]['filepath'] + top_tile_path = self.stitching_data[roi][channel][z_level][row - 1, col]["filepath"] + current_tile_path = self.stitching_data[roi][channel][z_level][row, col]["filepath"] # Calculate vertical shift new_v_shift = self.calculate_vertical_shift(top_tile_path, current_tile_path, abs(self.v_shift[0])) # Check if the new vertical shift is within 10% of the precomputed shift - if self.v_shift == (0,0) or (0.95 * abs(self.v_shift[0]) <= abs(new_v_shift[0]) <= 1.05 * abs(self.v_shift[0]) and - 0.95 * abs(self.v_shift[1]) <= abs(new_v_shift[1]) <= 1.05 * abs(self.v_shift[1])): + if self.v_shift == (0, 0) or ( + 0.95 * abs(self.v_shift[0]) <= abs(new_v_shift[0]) <= 1.05 * abs(self.v_shift[0]) + and 0.95 * abs(self.v_shift[1]) <= abs(new_v_shift[1]) <= 1.05 * abs(self.v_shift[1]) + ): print("new v shift", new_v_shift, v_shift) v_shift = new_v_shift @@ -405,21 +449,29 @@ def calculate_dynamic_shifts(self, roi, channel, z_level, row, col): def init_output(self, time_point, region_id): output_folder = os.path.join(self.input_folder, f"{time_point}_stitched") os.makedirs(output_folder, exist_ok=True) - self.output_path = os.path.join(output_folder, f"{region_id}_{self.output_name}" if self.is_wellplate else self.output_name) + self.output_path = os.path.join( + output_folder, f"{region_id}_{self.output_name}" if self.is_wellplate else self.output_name + ) - x_max = (self.input_width + ((self.num_cols - 1) * (self.input_width + self.h_shift[1])) + # horizontal width with overlap - abs((self.num_rows - 1) * self.v_shift[1])) # horizontal shift from vertical registration - y_max = (self.input_height + ((self.num_rows - 1) * (self.input_height + self.v_shift[0])) + # vertical height with overlap - abs((self.num_cols - 1) * self.h_shift[0])) # vertical shift from horizontal registration + x_max = ( + self.input_width + + ((self.num_cols - 1) * (self.input_width + self.h_shift[1])) # horizontal width with overlap + + abs((self.num_rows - 1) * self.v_shift[1]) + ) # horizontal shift from vertical registration + y_max = ( + self.input_height + + ((self.num_rows - 1) * (self.input_height + self.v_shift[0])) # vertical height with overlap + + abs((self.num_cols - 1) * self.h_shift[0]) + ) # vertical shift from horizontal registration if self.use_registration and DYNAMIC_REGISTRATION: y_max *= 1.05 x_max *= 1.05 size = max(y_max, x_max) num_levels = 1 - + # Get the number of rows and columns if self.is_wellplate and STITCH_COMPLETE_ACQUISITION: - rows, columns = self.get_rows_and_columns() + rows, columns = self.get_rows_and_columns() self.num_pyramid_levels = math.ceil(np.log2(max(x_max, y_max) / 1024 * max(len(rows), len(columns)))) else: self.num_pyramid_levels = math.ceil(np.log2(max(x_max, y_max) / 1024)) @@ -432,52 +484,63 @@ def init_output(self, time_point, region_id): def stitch_images(self, time_point, roi, progress_callback=None): self.stitched_images = self.init_output(time_point, roi) - total_tiles = sum(len(z_data) for channel_data in self.stitching_data[roi].values() for z_data in channel_data.values()) + total_tiles = sum( + len(z_data) for channel_data in self.stitching_data[roi].values() for z_data in channel_data.values() + ) processed_tiles = 0 for z_level in range(self.num_z): for row in range(self.num_rows): - row = self.num_rows - 1 - row if self.is_reversed['rows'] else row + row = self.num_rows - 1 - row if self.is_reversed["rows"] else row for col in range(self.num_cols): - col = self.num_cols - 1 - col if self.is_reversed['cols'] else col + col = self.num_cols - 1 - col if self.is_reversed["cols"] else col if self.use_registration and DYNAMIC_REGISTRATION and z_level == self.registration_z_level: if (row, col) in self.stitching_data[roi][self.registration_channel][z_level]: tile_info = self.stitching_data[roi][self.registration_channel][z_level][(row, col)] - self.h_shift, self.v_shift = self.calculate_dynamic_shifts(roi, self.registration_channel, z_level, row, col) + self.h_shift, self.v_shift = self.calculate_dynamic_shifts( + roi, self.registration_channel, z_level, row, col + ) # Now apply the same shifts to all channels for channel in self.channel_names: if (row, col) in self.stitching_data[roi][channel][z_level]: tile_info = self.stitching_data[roi][channel][z_level][(row, col)] - tile = dask_imread(tile_info['filepath'])[0] - #tile = tile[:, ::-1] + tile = dask_imread(tile_info["filepath"])[0] + # tile = tile[:, ::-1] if self.is_rgb[channel]: - for color_idx, color in enumerate(['R', 'G', 'B']): + for color_idx, color in enumerate(["R", "G", "B"]): tile_color = tile[:, :, color_idx] color_channel = f"{channel}_{color}" - self.stitch_single_image(tile_color, z_level, self.mono_channel_names.index(color_channel), row, col) + self.stitch_single_image( + tile_color, z_level, self.mono_channel_names.index(color_channel), row, col + ) processed_tiles += 1 else: - self.stitch_single_image(tile, z_level, self.mono_channel_names.index(channel), row, col) + self.stitch_single_image( + tile, z_level, self.mono_channel_names.index(channel), row, col + ) processed_tiles += 1 if progress_callback is not None: progress_callback(processed_tiles, total_tiles) def stitch_single_image(self, tile, z_level, channel_idx, row, col): - #print(tile.shape) + # print(tile.shape) if self.apply_flatfield: - tile = (tile / self.flatfields[channel_idx]).clip(min=np.iinfo(self.dtype).min, - max=np.iinfo(self.dtype).max).astype(self.dtype) + tile = ( + (tile / self.flatfields[channel_idx]) + .clip(min=np.iinfo(self.dtype).min, max=np.iinfo(self.dtype).max) + .astype(self.dtype) + ) # Determine crop for tile edges top_crop = max(0, (-self.v_shift[0] // 2) - abs(self.h_shift[0]) // 2) if row > 0 else 0 bottom_crop = max(0, (-self.v_shift[0] // 2) - abs(self.h_shift[0]) // 2) if row < self.num_rows - 1 else 0 left_crop = max(0, (-self.h_shift[1] // 2) - abs(self.v_shift[1]) // 2) if col > 0 else 0 right_crop = max(0, (-self.h_shift[1] // 2) - abs(self.v_shift[1]) // 2) if col < self.num_cols - 1 else 0 - tile = tile[top_crop:tile.shape[0]-bottom_crop, left_crop:tile.shape[1]-right_crop] + tile = tile[top_crop : tile.shape[0] - bottom_crop, left_crop : tile.shape[1] - right_crop] # Initialize starting coordinates based on tile position and shift y = row * (self.input_height + self.v_shift[0]) + top_crop @@ -493,7 +556,7 @@ def stitch_single_image(self, tile, z_level, channel_idx, row, col): x += row * self.v_shift[1] # Moves right if positive # Place cropped tile on the stitched image canvas - self.stitched_images[0, channel_idx, z_level, y:y+tile.shape[0], x:x+tile.shape[1]] = tile + self.stitched_images[0, channel_idx, z_level, y : y + tile.shape[0], x : x + tile.shape[1]] = tile # print(f" col:{col}, \trow:{row},\ty:{y}-{y+tile.shape[0]}, \tx:{x}-{x+tile.shape[-1]}") def save_as_ome_tiff(self): @@ -510,15 +573,15 @@ def save_as_ome_tiff(self): dimension_order=[dims], channel_names=[self.mono_channel_names], physical_pixel_sizes=[types.PhysicalPixelSizes(dz_um, self.pixel_size_um, self.pixel_size_um)], - #is_rgb=self.is_rgb - #channel colors + # is_rgb=self.is_rgb + # channel colors ) OmeTiffWriter.save( data=self.stitched_images, uri=self.output_path, ome_xml=ome_metadata, - dimension_order=[dims] - #channel colors / names + dimension_order=[dims], + # channel colors / names ) self.stitched_images = None @@ -530,7 +593,14 @@ def save_as_ome_zarr(self): intensity_max = np.iinfo(self.dtype).max channel_minmax = [(intensity_min, intensity_max)] * self.num_c for i in range(self.num_c): - print(f"Channel {i}:", self.mono_channel_names[i], " \tColor:", self.channel_colors[i], " \tPixel Range:", channel_minmax[i]) + print( + f"Channel {i}:", + self.mono_channel_names[i], + " \tColor:", + self.channel_colors[i], + " \tPixel Range:", + channel_minmax[i], + ) zarr_writer = OmeZarrWriter(self.output_path) zarr_writer.build_ome( @@ -538,7 +608,7 @@ def save_as_ome_zarr(self): image_name=os.path.basename(self.output_path), channel_names=self.mono_channel_names, channel_colors=self.channel_colors, - channel_minmax=channel_minmax + channel_minmax=channel_minmax, ) zarr_writer.write_image( image_data=self.stitched_images, @@ -548,16 +618,18 @@ def save_as_ome_zarr(self): channel_colors=self.channel_colors, dimension_order=dims, scale_num_levels=self.num_pyramid_levels, - chunk_dims=self.chunks + chunk_dims=self.chunks, ) self.stitched_images = None def create_complete_ome_zarr(self): - """ Creates a complete OME-ZARR with proper channel metadata. """ - final_path = os.path.join(self.input_folder, self.output_name.replace(".ome.zarr","") + "_complete_acquisition.ome.zarr") + """Creates a complete OME-ZARR with proper channel metadata.""" + final_path = os.path.join( + self.input_folder, self.output_name.replace(".ome.zarr", "") + "_complete_acquisition.ome.zarr" + ) if len(self.time_points) == 1: zarr_path = os.path.join(self.input_folder, f"0_stitched", self.output_name) - #final_path = zarr_path + # final_path = zarr_path shutil.copytree(zarr_path, final_path) else: store = ome_zarr.io.parse_url(final_path, mode="w").store @@ -571,31 +643,36 @@ def create_complete_ome_zarr(self): group=root_group, axes="tczyx", channel_names=self.mono_channel_names, - storage_options=dict(chunks=self.chunks) + storage_options=dict(chunks=self.chunks), ) - channel_info = [{ - "label": self.mono_channel_names[i], - "color": f"{self.channel_colors[i]:06X}", - "window": {"start": intensity_min, "end": intensity_max}, - "active": True - } for i in range(self.num_c)] + channel_info = [ + { + "label": self.mono_channel_names[i], + "color": f"{self.channel_colors[i]:06X}", + "window": {"start": intensity_min, "end": intensity_max}, + "active": True, + } + for i in range(self.num_c) + ] # Assign the channel metadata to the image group root_group.attrs["omero"] = {"channels": channel_info} print(f"Data saved in OME-ZARR format at: {final_path}") - root = zarr.open(final_path, mode='r') + root = zarr.open(final_path, mode="r") print(root.tree()) print(dict(root.attrs)) self.finished_saving.emit(final_path, self.dtype) def create_hcs_ome_zarr(self): """Creates a hierarchical Zarr file in the HCS OME-ZARR format for visualization in napari.""" - hcs_path = os.path.join(self.input_folder, self.output_name.replace(".ome.zarr","") + "_complete_acquisition.ome.zarr") + hcs_path = os.path.join( + self.input_folder, self.output_name.replace(".ome.zarr", "") + "_complete_acquisition.ome.zarr" + ) if len(self.time_points) == 1 and len(self.regions) == 1: stitched_zarr_path = os.path.join(self.input_folder, f"0_stitched", f"{self.regions[0]}_{self.output_name}") - #hcs_path = stitched_zarr_path # replace next line with this if no copy wanted + # hcs_path = stitched_zarr_path # replace next line with this if no copy wanted shutil.copytree(stitched_zarr_path, hcs_path) else: store = ome_zarr.io.parse_url(hcs_path, mode="w").store @@ -617,7 +694,7 @@ def create_hcs_ome_zarr(self): print(f"Data saved in HCS OME-ZARR format at: {hcs_path}") print("HCS root attributes:") - root = zarr.open(hcs_path, mode='r') + root = zarr.open(hcs_path, mode="r") print(root.tree()) print(dict(root.attrs)) @@ -629,23 +706,27 @@ def write_well_and_metadata(self, well_id, well_group): data = self.load_and_merge_timepoints(well_id) intensity_min = np.iinfo(self.dtype).min intensity_max = np.iinfo(self.dtype).max - #dataset = well_group.create_dataset("data", data=data, chunks=(1, 1, 1, self.input_height, self.input_width), dtype=data.dtype) + # dataset = well_group.create_dataset("data", data=data, chunks=(1, 1, 1, self.input_height, self.input_width), dtype=data.dtype) field_paths = ["0"] # Assuming single field of view ome_zarr.writer.write_well_metadata(well_group, field_paths) for fi, field in enumerate(field_paths): image_group = well_group.require_group(str(field)) - ome_zarr.writer.write_image(image=data, - group=image_group, - axes="tczyx", - channel_names=self.mono_channel_names, - storage_options=dict(chunks=self.chunks) - ) - channel_info = [{ - "label": self.mono_channel_names[c], - "color": f"{self.channel_colors[c]:06X}", - "window": {"start": intensity_min, "end": intensity_max}, - "active": True - } for c in range(self.num_c)] + ome_zarr.writer.write_image( + image=data, + group=image_group, + axes="tczyx", + channel_names=self.mono_channel_names, + storage_options=dict(chunks=self.chunks), + ) + channel_info = [ + { + "label": self.mono_channel_names[c], + "color": f"{self.channel_colors[c]:06X}", + "window": {"start": intensity_min, "end": intensity_max}, + "active": True, + } + for c in range(self.num_c) + ] image_group.attrs["omero"] = {"channels": channel_info} @@ -653,9 +734,9 @@ def pad_to_largest(self, array, target_shape): if array.shape == target_shape: return array pad_widths = [(0, max(0, ts - s)) for s, ts in zip(array.shape, target_shape)] - return da.pad(array, pad_widths, mode='constant', constant_values=0) + return da.pad(array, pad_widths, mode="constant", constant_values=0) - def load_and_merge_timepoints(self, well_id=''): + def load_and_merge_timepoints(self, well_id=""): """Load and merge data for a well from Zarr files for each timepoint.""" t_data = [] t_shapes = [] @@ -666,11 +747,19 @@ def load_and_merge_timepoints(self, well_id=''): filepath = f"{self.output_name}" zarr_path = os.path.join(self.input_folder, f"{t}_stitched", filepath) print(f"t:{t} well:{well_id}, \t{zarr_path}") - z = zarr.open(zarr_path, mode='r') + z = zarr.open(zarr_path, mode="r") # Ensure that '0' contains the data and it matches expected dimensions - x_max = self.input_width + ((self.num_cols - 1) * (self.input_width + self.h_shift[1])) + abs((self.num_rows - 1) * self.v_shift[1]) - y_max = self.input_height + ((self.num_rows - 1) * (self.input_height + self.v_shift[0])) + abs((self.num_cols - 1) * self.h_shift[0]) - t_array = da.from_zarr(z['0'], chunks=self.chunks) + x_max = ( + self.input_width + + ((self.num_cols - 1) * (self.input_width + self.h_shift[1])) + + abs((self.num_rows - 1) * self.v_shift[1]) + ) + y_max = ( + self.input_height + + ((self.num_rows - 1) * (self.input_height + self.v_shift[0])) + + abs((self.num_cols - 1) * self.h_shift[0]) + ) + t_array = da.from_zarr(z["0"], chunks=self.chunks) t_data.append(t_array) t_shapes.append(t_array.shape) @@ -711,7 +800,6 @@ def run(self): self.get_flatfields(progress_callback=self.update_progress.emit) print("time to apply flatfields", time.time() - ttime) - if self.use_registration: shtime = time.time() print(f"calculating shifts...") @@ -736,7 +824,7 @@ def run(self): print("time to save stitched well", time.time() - sttime) print("time per well", time.time() - wtime) - if well != '0': + if well != "0": print(f"...done saving well:{well}") print(f"...finished t:{time_point}") print("time per timepoint", time.time() - ttime) @@ -760,7 +848,6 @@ def run(self): print(f"error While Stitching: {e}") - class CoordinateStitcher(QThread, QObject): update_progress = Signal(int, int) getting_flatfields = Signal() @@ -768,7 +855,17 @@ class CoordinateStitcher(QThread, QObject): starting_saving = Signal(bool) finished_saving = Signal(str, object) - def __init__(self, input_folder, output_name='', output_format=".ome.zarr", apply_flatfield=0, use_registration=0, registration_channel='', registration_z_level=0, overlap_percent=0): + def __init__( + self, + input_folder, + output_name="", + output_format=".ome.zarr", + apply_flatfield=0, + use_registration=0, + registration_channel="", + registration_z_level=0, + overlap_percent=0, + ): super().__init__() self.input_folder = input_folder self.output_name = output_name + output_format @@ -787,7 +884,6 @@ def __init__(self, input_folder, output_name='', output_format=".ome.zarr", appl self.scan_pattern = FOV_PATTERN self.init_stitching_parameters() - def init_stitching_parameters(self): self.is_rgb = {} self.channel_names = [] @@ -801,28 +897,32 @@ def init_stitching_parameters(self): self.dtype = np.uint16 self.chunks = None self.h_shift = (0, 0) - if self.scan_pattern == 'S-Pattern': + if self.scan_pattern == "S-Pattern": self.h_shift_rev = (0, 0) - self.h_shift_rev_odd = 0 # 0 reverse even rows, 1 reverse odd rows + self.h_shift_rev_odd = 0 # 0 reverse even rows, 1 reverse odd rows self.v_shift = (0, 0) self.x_positions = set() self.y_positions = set() def get_time_points(self): - self.time_points = [d for d in os.listdir(self.input_folder) if os.path.isdir(os.path.join(self.input_folder, d)) and d.isdigit()] + self.time_points = [ + d + for d in os.listdir(self.input_folder) + if os.path.isdir(os.path.join(self.input_folder, d)) and d.isdigit() + ] self.time_points.sort(key=int) return self.time_points def extract_acquisition_parameters(self): - acquistion_params_path = os.path.join(self.input_folder, 'acquisition parameters.json') - with open(acquistion_params_path, 'r') as file: + acquistion_params_path = os.path.join(self.input_folder, "acquisition parameters.json") + with open(acquistion_params_path, "r") as file: self.acquisition_params = json.load(file) def get_pixel_size_from_params(self): - obj_mag = self.acquisition_params['objective']['magnification'] - obj_tube_lens_mm = self.acquisition_params['objective']['tube_lens_f_mm'] - sensor_pixel_size_um = self.acquisition_params['sensor_pixel_size_um'] - tube_lens_mm = self.acquisition_params['tube_lens_mm'] + obj_mag = self.acquisition_params["objective"]["magnification"] + obj_tube_lens_mm = self.acquisition_params["objective"]["tube_lens_f_mm"] + sensor_pixel_size_um = self.acquisition_params["sensor_pixel_size_um"] + tube_lens_mm = self.acquisition_params["tube_lens_mm"] obj_focal_length_mm = obj_tube_lens_mm / obj_mag actual_mag = tube_lens_mm / obj_focal_length_mm @@ -841,24 +941,28 @@ def parse_filenames(self): for t, time_point in enumerate(self.time_points): image_folder = os.path.join(self.input_folder, str(time_point)) - coordinates_path = os.path.join(self.input_folder, time_point, 'coordinates.csv') + coordinates_path = os.path.join(self.input_folder, time_point, "coordinates.csv") coordinates_df = pd.read_csv(coordinates_path) print(f"Processing timepoint {time_point}, image folder: {image_folder}") - image_files = sorted([f for f in os.listdir(image_folder) if f.endswith(('.bmp', '.tiff')) and 'focus_camera' not in f]) - + image_files = sorted( + [f for f in os.listdir(image_folder) if f.endswith((".bmp", ".tiff")) and "focus_camera" not in f] + ) + if not image_files: raise Exception(f"No valid files found in directory for timepoint {time_point}.") for file in image_files: - parts = file.split('_', 3) + parts = file.split("_", 3) region, fov, z_level, channel = parts[0], int(parts[1]), int(parts[2]), os.path.splitext(parts[3])[0] channel = channel.replace("_", " ").replace("full ", "full_") - coord_row = coordinates_df[(coordinates_df['region'] == region) & - (coordinates_df['fov'] == fov) & - (coordinates_df['z_level'] == z_level)] + coord_row = coordinates_df[ + (coordinates_df["region"] == region) + & (coordinates_df["fov"] == fov) + & (coordinates_df["z_level"] == z_level) + ] if coord_row.empty: print(f"Warning: No matching coordinates found for file {file}") @@ -868,15 +972,15 @@ def parse_filenames(self): key = (t, region, fov, z_level, channel) self.stitching_data[key] = { - 'filepath': os.path.join(image_folder, file), - 'x': coord_row['x (mm)'], - 'y': coord_row['y (mm)'], - 'z': coord_row['z (um)'], - 'channel': channel, - 'z_level': z_level, - 'region': region, - 'fov_idx': fov, - 't': t + "filepath": os.path.join(image_folder, file), + "x": coord_row["x (mm)"], + "y": coord_row["y (mm)"], + "z": coord_row["z (um)"], + "channel": channel, + "z_level": z_level, + "region": region, + "fov_idx": fov, + "t": t, } self.regions.add(region) @@ -889,13 +993,13 @@ def parse_filenames(self): self.num_t = len(self.time_points) self.num_z = max_z + 1 self.num_fovs_per_region = max_fov + 1 - + # Set up image parameters based on the first image first_key = list(self.stitching_data.keys())[0] - first_region = self.stitching_data[first_key]['region'] - first_fov = self.stitching_data[first_key]['fov_idx'] - first_z_level = self.stitching_data[first_key]['z_level'] - first_image = dask_imread(self.stitching_data[first_key]['filepath'])[0] + first_region = self.stitching_data[first_key]["region"] + first_fov = self.stitching_data[first_key]["fov_idx"] + first_z_level = self.stitching_data[first_key]["z_level"] + first_image = dask_imread(self.stitching_data[first_key]["filepath"])[0] self.dtype = first_image.dtype if len(first_image.shape) == 2: @@ -905,15 +1009,15 @@ def parse_filenames(self): else: raise ValueError(f"Unexpected image shape: {first_image.shape}") self.chunks = (1, 1, 1, 512, 512) - + # Set up final monochrome channels self.mono_channel_names = [] for channel in self.channel_names: channel_key = (t, first_region, first_fov, first_z_level, channel) - channel_image = dask_imread(self.stitching_data[channel_key]['filepath'])[0] + channel_image = dask_imread(self.stitching_data[channel_key]["filepath"])[0] if len(channel_image.shape) == 3 and channel_image.shape[2] == 3: self.is_rgb[channel] = True - channel = channel.split('_')[0] + channel = channel.split("_")[0] self.mono_channel_names.extend([f"{channel}_R", f"{channel}_G", f"{channel}_B"]) else: self.is_rgb[channel] = False @@ -928,14 +1032,14 @@ def parse_filenames(self): def get_channel_color(self, channel_name): color_map = { - '405': 0x0000FF, # Blue - '488': 0x00FF00, # Green - '561': 0xFFCF00, # Yellow - '638': 0xFF0000, # Red - '730': 0x770000, # Dark Red" - '_B': 0x0000FF, # Blue - '_G': 0x00FF00, # Green - '_R': 0xFF0000 # Red + "405": 0x0000FF, # Blue + "488": 0x00FF00, # Green + "561": 0xFFCF00, # Yellow + "638": 0xFF0000, # Red + "730": 0x770000, # Dark Red" + "_B": 0x0000FF, # Blue + "_G": 0x00FF00, # Green + "_R": 0xFF0000, # Red } for key in color_map: if key in channel_name: @@ -944,28 +1048,32 @@ def get_channel_color(self, channel_name): def calculate_output_dimensions(self, region): region_data = [tile_info for key, tile_info in self.stitching_data.items() if key[1] == region] - + if not region_data: raise ValueError(f"No data found for region {region}") - self.x_positions = sorted(set(tile_info['x'] for tile_info in region_data)) - self.y_positions = sorted(set(tile_info['y'] for tile_info in region_data)) + self.x_positions = sorted(set(tile_info["x"] for tile_info in region_data)) + self.y_positions = sorted(set(tile_info["y"] for tile_info in region_data)) - if self.use_registration: # Add extra space for shifts + if self.use_registration: # Add extra space for shifts num_cols = len(self.x_positions) num_rows = len(self.y_positions) - if self.scan_pattern == 'S-Pattern': + if self.scan_pattern == "S-Pattern": max_h_shift = (max(self.h_shift[0], self.h_shift_rev[0]), max(self.h_shift[1], self.h_shift_rev[1])) else: max_h_shift = self.h_shift - width_pixels = int(self.input_width + ((num_cols - 1) * (self.input_width + max_h_shift[1]))) # horizontal width with overlap - width_pixels += abs((num_rows - 1) * self.v_shift[1]) # horizontal shift from vertical registration - height_pixels = int(self.input_height + ((num_rows - 1) * (self.input_height + self.v_shift[0]))) # vertical height with overlap - height_pixels += abs((num_cols - 1) * max_h_shift[0]) # vertical shift from horizontal registration - - else: # Use coordinates shifts + width_pixels = int( + self.input_width + ((num_cols - 1) * (self.input_width + max_h_shift[1])) + ) # horizontal width with overlap + width_pixels += abs((num_rows - 1) * self.v_shift[1]) # horizontal shift from vertical registration + height_pixels = int( + self.input_height + ((num_rows - 1) * (self.input_height + self.v_shift[0])) + ) # vertical height with overlap + height_pixels += abs((num_cols - 1) * max_h_shift[0]) # vertical shift from horizontal registration + + else: # Use coordinates shifts width_mm = max(self.x_positions) - min(self.x_positions) + (self.input_width * self.pixel_size_um / 1000) height_mm = max(self.y_positions) - min(self.y_positions) + (self.input_height * self.pixel_size_um / 1000) @@ -1001,7 +1109,9 @@ def process_images(images, channel_name): return if images.ndim != 3 and images.ndim != 4: - raise ValueError(f"Images must be 3 or 4-dimensional array, with dimension of (T, Y, X) or (T, Z, Y, X). Got shape {images.shape}") + raise ValueError( + f"Images must be 3 or 4-dimensional array, with dimension of (T, Y, X) or (T, Z, Y, X). Got shape {images.shape}" + ) basic = BaSiC(get_darkfield=False, smoothness_flatfield=1) basic.fit(images) @@ -1014,12 +1124,16 @@ def process_images(images, channel_name): print(f"Calculating {channel} flatfield...") images = [] for t in self.time_points: - time_images = [dask_imread(tile['filepath'])[0] for key, tile in self.stitching_data.items() if tile['channel'] == channel and key[0] == int(t)] + time_images = [ + dask_imread(tile["filepath"])[0] + for key, tile in self.stitching_data.items() + if tile["channel"] == channel and key[0] == int(t) + ] if not time_images: print(f"WARNING: No images found for channel {channel} at timepoint {t}") continue random.shuffle(time_images) - selected_tiles = time_images[:min(32, len(time_images))] + selected_tiles = time_images[: min(32, len(time_images))] images.extend(selected_tiles) if not images: @@ -1037,10 +1151,10 @@ def process_images(images, channel_name): images_r = images[..., 0] images_g = images[..., 1] images_b = images[..., 2] - channel = channel.split('_')[0] - process_images(images_r, channel + '_R') - process_images(images_g, channel + '_G') - process_images(images_b, channel + '_B') + channel = channel.split("_")[0] + process_images(images_r, channel + "_R") + process_images(images_g, channel + "_G") + process_images(images_b, channel + "_B") else: # Images are in the shape (N, Z, Y, X) process_images(images, channel) @@ -1049,11 +1163,11 @@ def process_images(images, channel_name): def calculate_shifts(self, region): region_data = [v for k, v in self.stitching_data.items() if k[1] == region] - + # Get unique x and y positions - x_positions = sorted(set(tile['x'] for tile in region_data)) - y_positions = sorted(set(tile['y'] for tile in region_data)) - + x_positions = sorted(set(tile["x"] for tile in region_data)) + y_positions = sorted(set(tile["y"] for tile in region_data)) + # Initialize shifts self.h_shift = (0, 0) self.v_shift = (0, 0) @@ -1062,10 +1176,11 @@ def calculate_shifts(self, region): if not self.registration_channel: self.registration_channel = self.channel_names[0] elif self.registration_channel not in self.channel_names: - print(f"Warning: Specified registration channel '{self.registration_channel}' not found. Using {self.channel_names[0]}.") + print( + f"Warning: Specified registration channel '{self.registration_channel}' not found. Using {self.channel_names[0]}." + ) self.registration_channel = self.channel_names[0] - max_x_overlap = round(self.input_width * self.overlap_percent / 2 / 100) max_y_overlap = round(self.input_height * self.overlap_percent / 2 / 100) print(f"Expected shifts - Horizontal: {(0, -max_x_overlap)}, Vertical: {(-max_y_overlap , 0)}") @@ -1073,7 +1188,7 @@ def calculate_shifts(self, region): # Find center positions center_x_index = (len(x_positions) - 1) // 2 center_y_index = (len(y_positions) - 1) // 2 - + center_x = x_positions[center_x_index] center_y = y_positions[center_y_index] @@ -1083,27 +1198,35 @@ def calculate_shifts(self, region): # Calculate horizontal shift if center_x_index + 1 < len(x_positions): right_x = x_positions[center_x_index + 1] - center_tile = self.get_tile(region, center_x, center_y, self.registration_channel, self.registration_z_level) + center_tile = self.get_tile( + region, center_x, center_y, self.registration_channel, self.registration_z_level + ) right_tile = self.get_tile(region, right_x, center_y, self.registration_channel, self.registration_z_level) - + if center_tile is not None and right_tile is not None: self.h_shift = self.calculate_horizontal_shift(center_tile, right_tile, max_x_overlap) else: print(f"Warning: Missing tiles for horizontal shift calculation in region {region}.") - + # Calculate vertical shift if center_y_index + 1 < len(y_positions): bottom_y = y_positions[center_y_index + 1] - center_tile = self.get_tile(region, center_x, center_y, self.registration_channel, self.registration_z_level) - bottom_tile = self.get_tile(region, center_x, bottom_y, self.registration_channel, self.registration_z_level) - + center_tile = self.get_tile( + region, center_x, center_y, self.registration_channel, self.registration_z_level + ) + bottom_tile = self.get_tile( + region, center_x, bottom_y, self.registration_channel, self.registration_z_level + ) + if center_tile is not None and bottom_tile is not None: self.v_shift = self.calculate_vertical_shift(center_tile, bottom_tile, max_y_overlap) else: print(f"Warning: Missing tiles for vertical shift calculation in region {region}.") - if self.scan_pattern == 'S-Pattern' and right_x and bottom_y: - center_tile = self.get_tile(region, center_x, bottom_y, self.registration_channel, self.registration_z_level) + if self.scan_pattern == "S-Pattern" and right_x and bottom_y: + center_tile = self.get_tile( + region, center_x, bottom_y, self.registration_channel, self.registration_z_level + ) right_tile = self.get_tile(region, right_x, bottom_y, self.registration_channel, self.registration_z_level) if center_tile is not None and right_tile is not None: @@ -1115,7 +1238,6 @@ def calculate_shifts(self, region): print(f"Calculated Uni-Directional Shifts - Horizontal: {self.h_shift}, Vertical: {self.v_shift}") - def calculate_horizontal_shift(self, img1, img2, max_overlap): img1 = self.normalize_image(img1) img2 = self.normalize_image(img2) @@ -1124,7 +1246,7 @@ def calculate_horizontal_shift(self, img1, img2, max_overlap): img1_overlap = img1[margin:-margin, -max_overlap:] img2_overlap = img2[margin:-margin, :max_overlap] - self.visualize_image(img1_overlap, img2_overlap, 'horizontal') + self.visualize_image(img1_overlap, img2_overlap, "horizontal") shift, error, diffphase = phase_cross_correlation(img1_overlap, img2_overlap, upsample_factor=10) return round(shift[0]), round(shift[1] - img1_overlap.shape[1]) @@ -1137,20 +1259,22 @@ def calculate_vertical_shift(self, img1, img2, max_overlap): img1_overlap = img1[-max_overlap:, margin:-margin] img2_overlap = img2[:max_overlap, margin:-margin] - self.visualize_image(img1_overlap, img2_overlap, 'vertical') + self.visualize_image(img1_overlap, img2_overlap, "vertical") shift, error, diffphase = phase_cross_correlation(img1_overlap, img2_overlap, upsample_factor=10) return round(shift[0] - img1_overlap.shape[0]), round(shift[1]) def get_tile(self, region, x, y, channel, z_level): for key, value in self.stitching_data.items(): - if (key[1] == region and - value['x'] == x and - value['y'] == y and - value['channel'] == channel and - value['z_level'] == z_level): + if ( + key[1] == region + and value["x"] == x + and value["y"] == y + and value["channel"] == channel + and value["z_level"] == z_level + ): try: - return dask_imread(value['filepath'])[0] + return dask_imread(value["filepath"])[0] except FileNotFoundError: print(f"Warning: Tile file not found: {value['filepath']}") return None @@ -1169,16 +1293,16 @@ def visualize_image(self, img1, img2, title): img1 = np.asarray(img1) img2 = np.asarray(img2) - if title == 'horizontal': + if title == "horizontal": combined_image = np.hstack((img1, img2)) else: combined_image = np.vstack((img1, img2)) - + # Convert to uint8 for saving as PNG combined_image_uint8 = (combined_image / np.iinfo(self.dtype).max * 255).astype(np.uint8) - + cv2.imwrite(f"{self.input_folder}/{title}.png", combined_image_uint8) - + print(f"Saved {title}.png successfully") except Exception as e: print(f"Error in visualize_image: {e}") @@ -1194,12 +1318,12 @@ def stitch_and_save_region(self, region, progress_callback=None): for key, tile_info in region_data.items(): t, _, fov, z_level, channel = key - tile = dask_imread(tile_info['filepath'])[0] + tile = dask_imread(tile_info["filepath"])[0] if self.use_registration: - self.col_index = self.x_positions.index(tile_info['x']) - self.row_index = self.y_positions.index(tile_info['y']) + self.col_index = self.x_positions.index(tile_info["x"]) + self.row_index = self.y_positions.index(tile_info["y"]) - if self.scan_pattern == 'S-Pattern' and self.row_index % 2 == self.h_shift_rev_odd: + if self.scan_pattern == "S-Pattern" and self.row_index % 2 == self.h_shift_rev_odd: h_shift = self.h_shift_rev else: h_shift = self.h_shift @@ -1210,20 +1334,24 @@ def stitch_and_save_region(self, region, progress_callback=None): # Apply horizontal shift effect on y-coordinate if h_shift[0] < 0: - y_pixel += int((len(self.x_positions) - 1 - self.col_index) * abs(h_shift[0])) # Fov moves up as cols go right + y_pixel += int( + (len(self.x_positions) - 1 - self.col_index) * abs(h_shift[0]) + ) # Fov moves up as cols go right else: y_pixel += int(self.col_index * h_shift[0]) # Fov moves down as cols go right # Apply vertical shift effect on x-coordinate if self.v_shift[1] < 0: - x_pixel += int((len(self.y_positions) - 1 - self.row_index) * abs(self.v_shift[1])) # Fov moves left as rows go down + x_pixel += int( + (len(self.y_positions) - 1 - self.row_index) * abs(self.v_shift[1]) + ) # Fov moves left as rows go down else: - x_pixel += int(self.row_index * self.v_shift[1]) # Fov moves right as rows go down + x_pixel += int(self.row_index * self.v_shift[1]) # Fov moves right as rows go down else: # Calculate base position - x_pixel = int((tile_info['x'] - x_min) * 1000 / self.pixel_size_um) - y_pixel = int((tile_info['y'] - y_min) * 1000 / self.pixel_size_um) + x_pixel = int((tile_info["x"] - x_min) * 1000 / self.pixel_size_um) + y_pixel = int((tile_info["y"] - y_min) * 1000 / self.pixel_size_um) self.place_tile(stitched_images, tile, x_pixel, y_pixel, z_level, channel, t) @@ -1236,7 +1364,9 @@ def stitch_and_save_region(self, region, progress_callback=None): self.save_region_to_hcs_ome_zarr(region, stitched_images) else: # self.save_as_ome_zarr(region, stitched_images) - self.save_region_to_ome_zarr(region, stitched_images) # bugs: when starting to save, main gui lags and disconnects + self.save_region_to_ome_zarr( + region, stitched_images + ) # bugs: when starting to save, main gui lags and disconnects def place_tile(self, stitched_images, tile, x_pixel, y_pixel, z_level, channel, t): if len(tile.shape) == 2: @@ -1247,10 +1377,12 @@ def place_tile(self, stitched_images, tile, x_pixel, y_pixel, z_level, channel, elif len(tile.shape) == 3: if tile.shape[2] == 3: # Handle RGB image - channel = channel.split('_')[0] - for i, color in enumerate(['R', 'G', 'B']): + channel = channel.split("_")[0] + for i, color in enumerate(["R", "G", "B"]): channel_idx = self.mono_channel_names.index(f"{channel}_{color}") - self.place_single_channel_tile(stitched_images, tile[:,:,i], x_pixel, y_pixel, z_level, channel_idx, t) + self.place_single_channel_tile( + stitched_images, tile[:, :, i], x_pixel, y_pixel, z_level, channel_idx, t + ) elif tile.shape[0] == 1: channel_idx = self.mono_channel_names.index(channel) self.place_single_channel_tile(stitched_images, tile[0], x_pixel, y_pixel, z_level, channel_idx, t) @@ -1259,51 +1391,68 @@ def place_tile(self, stitched_images, tile, x_pixel, y_pixel, z_level, channel, def place_single_channel_tile(self, stitched_images, tile, x_pixel, y_pixel, z_level, channel_idx, t): if len(stitched_images.shape) != 5: - raise ValueError(f"Unexpected stitched_images shape: {stitched_images.shape}. Expected 5D array (t, c, z, y, x).") + raise ValueError( + f"Unexpected stitched_images shape: {stitched_images.shape}. Expected 5D array (t, c, z, y, x)." + ) if self.apply_flatfield: tile = self.apply_flatfield_correction(tile, channel_idx) if self.use_registration: - if self.scan_pattern == 'S-Pattern' and self.row_index % 2 == self.h_shift_rev_odd: + if self.scan_pattern == "S-Pattern" and self.row_index % 2 == self.h_shift_rev_odd: h_shift = self.h_shift_rev else: h_shift = self.h_shift # Determine crop for tile edges - top_crop = max(0, (-self.v_shift[0] // 2) - abs(h_shift[0]) // 2) if self.row_index > 0 else 0 # if y - bottom_crop = max(0, (-self.v_shift[0] // 2) - abs(h_shift[0]) // 2) if self.row_index < len(self.y_positions) - 1 else 0 + top_crop = max(0, (-self.v_shift[0] // 2) - abs(h_shift[0]) // 2) if self.row_index > 0 else 0 # if y + bottom_crop = ( + max(0, (-self.v_shift[0] // 2) - abs(h_shift[0]) // 2) + if self.row_index < len(self.y_positions) - 1 + else 0 + ) left_crop = max(0, (-h_shift[1] // 2) - abs(self.v_shift[1]) // 2) if self.col_index > 0 else 0 - right_crop = max(0, (-h_shift[1] // 2) - abs(self.v_shift[1]) // 2) if self.col_index < len(self.x_positions) - 1 else 0 + right_crop = ( + max(0, (-h_shift[1] // 2) - abs(self.v_shift[1]) // 2) + if self.col_index < len(self.x_positions) - 1 + else 0 + ) # Apply cropping to the tile - tile = tile[top_crop:tile.shape[0]-bottom_crop, left_crop:tile.shape[1]-right_crop] + tile = tile[top_crop : tile.shape[0] - bottom_crop, left_crop : tile.shape[1] - right_crop] # Adjust x_pixel and y_pixel based on cropping x_pixel += left_crop y_pixel += top_crop - + y_end = min(y_pixel + tile.shape[0], stitched_images.shape[3]) x_end = min(x_pixel + tile.shape[1], stitched_images.shape[4]) - + try: - stitched_images[t, channel_idx, z_level, y_pixel:y_end, x_pixel:x_end] = tile[:y_end-y_pixel, :x_end-x_pixel] + stitched_images[t, channel_idx, z_level, y_pixel:y_end, x_pixel:x_end] = tile[ + : y_end - y_pixel, : x_end - x_pixel + ] except Exception as e: print(f"ERROR: Failed to place tile. Details: {str(e)}") - print(f"DEBUG: t:{t}, channel_idx:{channel_idx}, z_level:{z_level}, y:{y_pixel}-{y_end}, x:{x_pixel}-{x_end}") + print( + f"DEBUG: t:{t}, channel_idx:{channel_idx}, z_level:{z_level}, y:{y_pixel}-{y_end}, x:{x_pixel}-{x_end}" + ) print(f"DEBUG: tile slice shape: {tile[:y_end-y_pixel, :x_end-x_pixel].shape}") raise def apply_flatfield_correction(self, tile, channel_idx): if channel_idx in self.flatfields: - return (tile / self.flatfields[channel_idx]).clip(min=np.iinfo(self.dtype).min, - max=np.iinfo(self.dtype).max).astype(self.dtype) + return ( + (tile / self.flatfields[channel_idx]) + .clip(min=np.iinfo(self.dtype).min, max=np.iinfo(self.dtype).max) + .astype(self.dtype) + ) return tile def generate_pyramid(self, image, num_levels): pyramid = [image] for level in range(1, num_levels): - scale_factor = 2 ** level + scale_factor = 2**level factors = {0: 1, 1: 1, 2: 1, 3: scale_factor, 4: scale_factor} if isinstance(image, da.Array): downsampled = da.coarsen(np.mean, image, factors, trim_excess=True) @@ -1322,20 +1471,29 @@ def save_region_to_hcs_ome_zarr(self, region, stitched_images): row_group = root.require_group(row) well_group = row_group.require_group(col) - if 'well' not in well_group.attrs: + if "well" not in well_group.attrs: well_metadata = { "images": [{"path": "0", "acquisition": 0}], } ome_zarr.writer.write_well_metadata(well_group, well_metadata["images"]) image_group = well_group.require_group("0") - + pyramid = self.generate_pyramid(stitched_images, self.num_pyramid_levels) coordinate_transformations = [ - [{ - "type": "scale", - "scale": [1, 1, self.acquisition_params.get("dz(um)", 1), self.pixel_size_um * (2 ** i), self.pixel_size_um * (2 ** i)] - }] for i in range(self.num_pyramid_levels) + [ + { + "type": "scale", + "scale": [ + 1, + 1, + self.acquisition_params.get("dz(um)", 1), + self.pixel_size_um * (2**i), + self.pixel_size_um * (2**i), + ], + } + ] + for i in range(self.num_pyramid_levels) ] axes = [ @@ -1343,21 +1501,20 @@ def save_region_to_hcs_ome_zarr(self, region, stitched_images): {"name": "c", "type": "channel"}, {"name": "z", "type": "space", "unit": "micrometer"}, {"name": "y", "type": "space", "unit": "micrometer"}, - {"name": "x", "type": "space", "unit": "micrometer"} + {"name": "x", "type": "space", "unit": "micrometer"}, ] # Prepare channels metadata - omero_channels = [{ - "label": name, - "color": f"{color:06X}", - "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max} - } for name, color in zip(self.mono_channel_names, self.channel_colors)] + omero_channels = [ + { + "label": name, + "color": f"{color:06X}", + "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max}, + } + for name, color in zip(self.mono_channel_names, self.channel_colors) + ] - omero = { - "name": f"{region}", - "version": "0.4", - "channels": omero_channels - } + omero = {"name": f"{region}", "version": "0.4", "channels": omero_channels} image_group.attrs["omero"] = omero @@ -1369,7 +1526,7 @@ def save_region_to_hcs_ome_zarr(self, region, stitched_images): axes=axes, coordinate_transformations=coordinate_transformations, storage_options=dict(chunks=self.chunks), - name=f"{region}" + name=f"{region}", ) def save_as_ome_zarr(self, region, stitched_images): @@ -1378,7 +1535,14 @@ def save_as_ome_zarr(self, region, stitched_images): sensor_pixel_size_um = self.acquisition_params.get("sensor_pixel_size_um", None) channel_minmax = [(np.iinfo(self.dtype).min, np.iinfo(self.dtype).max)] * self.num_c for i in range(self.num_c): - print(f"Channel {i}:", self.mono_channel_names[i], " \tColor:", self.channel_colors[i], " \tPixel Range:", channel_minmax[i]) + print( + f"Channel {i}:", + self.mono_channel_names[i], + " \tColor:", + self.channel_colors[i], + " \tPixel Range:", + channel_minmax[i], + ) zarr_writer = OmeZarrWriter(output_path) zarr_writer.build_ome( @@ -1386,7 +1550,7 @@ def save_as_ome_zarr(self, region, stitched_images): image_name=region, channel_names=self.mono_channel_names, channel_colors=self.channel_colors, - channel_minmax=channel_minmax + channel_minmax=channel_minmax, ) zarr_writer.write_image( image_data=stitched_images, @@ -1396,7 +1560,7 @@ def save_as_ome_zarr(self, region, stitched_images): channel_colors=self.channel_colors, dimension_order="TCZYX", scale_num_levels=self.num_pyramid_levels, - chunk_dims=self.chunks + chunk_dims=self.chunks, ) def save_region_to_ome_zarr(self, region, stitched_images): @@ -1410,20 +1574,30 @@ def save_region_to_ome_zarr(self, region, stitched_images): datasets = [] for i in range(self.num_pyramid_levels): scale = 2**i - datasets.append({ - "path": str(i), - "coordinateTransformations": [{ - "type": "scale", - "scale": [1, 1, self.acquisition_params.get("dz(um)", 1), self.pixel_size_um * scale, self.pixel_size_um * scale] - }] - }) + datasets.append( + { + "path": str(i), + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1, + 1, + self.acquisition_params.get("dz(um)", 1), + self.pixel_size_um * scale, + self.pixel_size_um * scale, + ], + } + ], + } + ) axes = [ {"name": "t", "type": "time", "unit": "second"}, {"name": "c", "type": "channel"}, {"name": "z", "type": "space", "unit": "micrometer"}, {"name": "y", "type": "space", "unit": "micrometer"}, - {"name": "x", "type": "space", "unit": "micrometer"} + {"name": "x", "type": "space", "unit": "micrometer"}, ] ome_zarr.writer.write_multiscales_metadata(root, datasets, axes=axes, name="stitched_image") @@ -1431,24 +1605,25 @@ def save_region_to_ome_zarr(self, region, stitched_images): omero = { "name": "stitched_image", "version": "0.4", - "channels": [{ - "label": name, - "color": f"{color:06X}", - "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max} - } for name, color in zip(self.mono_channel_names, self.channel_colors)] + "channels": [ + { + "label": name, + "color": f"{color:06X}", + "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max}, + } + for name, color in zip(self.mono_channel_names, self.channel_colors) + ], } root.attrs["omero"] = omero - coordinate_transformations = [ - dataset["coordinateTransformations"] for dataset in datasets - ] + coordinate_transformations = [dataset["coordinateTransformations"] for dataset in datasets] ome_zarr.writer.write_multiscale( pyramid=pyramid, group=root, axes="tczyx", coordinate_transformations=coordinate_transformations, - storage_options=dict(chunks=self.chunks) + storage_options=dict(chunks=self.chunks), ) def write_stitched_plate_metadata(self): @@ -1458,21 +1633,19 @@ def write_stitched_plate_metadata(self): rows, columns = self.get_rows_and_columns() well_paths = [f"{well_id[0]}/{well_id[1:]}" for well_id in sorted(self.regions)] - + plate_metadata = { "name": "Stitched Plate", "rows": [{"name": row} for row in rows], "columns": [{"name": col} for col in columns], - "wells": [{"path": path, "rowIndex": rows.index(path[0]), "columnIndex": columns.index(path[2:])} - for path in well_paths], + "wells": [ + {"path": path, "rowIndex": rows.index(path[0]), "columnIndex": columns.index(path[2:])} + for path in well_paths + ], "field_count": 1, - "acquisitions": [{ - "id": 0, - "maximumfieldcount": 1, - "name": "Stitched Acquisition" - }] + "acquisitions": [{"id": 0, "maximumfieldcount": 1, "name": "Stitched Acquisition"}], } - + ome_zarr.writer.write_plate_metadata( root, rows=[row["name"] for row in plate_metadata["rows"]], @@ -1480,7 +1653,7 @@ def write_stitched_plate_metadata(self): wells=plate_metadata["wells"], acquisitions=plate_metadata["acquisitions"], name=plate_metadata["name"], - field_count=plate_metadata["field_count"] + field_count=plate_metadata["field_count"], ) def get_rows_and_columns(self): @@ -1490,33 +1663,32 @@ def get_rows_and_columns(self): def create_ome_tiff(self, stitched_images): output_path = os.path.join(self.input_folder, self.output_name) - + with TiffWriter(output_path, bigtiff=True, ome=True) as tif: tif.write( data=stitched_images, shape=stitched_images.shape, dtype=self.dtype, - photometric='minisblack', - planarconfig='separate', + photometric="minisblack", + planarconfig="separate", metadata={ - 'axes': 'TCZYX', - 'Channel': {'Name': self.mono_channel_names}, - 'SignificantBits': stitched_images.dtype.itemsize * 8, - 'Pixels': { - 'PhysicalSizeX': self.pixel_size_um, - 'PhysicalSizeXUnit': 'µm', - 'PhysicalSizeY': self.pixel_size_um, - 'PhysicalSizeYUnit': 'µm', - 'PhysicalSizeZ': self.acquisition_params.get("dz(um)", 1.0), - 'PhysicalSizeZUnit': 'µm', + "axes": "TCZYX", + "Channel": {"Name": self.mono_channel_names}, + "SignificantBits": stitched_images.dtype.itemsize * 8, + "Pixels": { + "PhysicalSizeX": self.pixel_size_um, + "PhysicalSizeXUnit": "µm", + "PhysicalSizeY": self.pixel_size_um, + "PhysicalSizeYUnit": "µm", + "PhysicalSizeZ": self.acquisition_params.get("dz(um)", 1.0), + "PhysicalSizeZUnit": "µm", }, - } + }, ) - + print(f"Data saved in OME-TIFF format at: {output_path}") self.finished_saving.emit(output_path, self.dtype) - def run(self): stime = time.time() # try: @@ -1532,14 +1704,13 @@ def run(self): if self.num_fovs_per_region > 1: self.run_regions() else: - self.run_fovs() # only displays one fov per region even though all fovs are saved in zarr with metadata + self.run_fovs() # only displays one fov per region even though all fovs are saved in zarr with metadata # except Exception as e: # print("time before error", time.time() - stime) # print(f"Error while stitching: {e}") # raise - def run_regions(self): stime = time.time() if len(self.regions) > 1: @@ -1564,7 +1735,7 @@ def run_regions(self): print(f"time to stitch and save region {region}", time.time() - wtime) print(f"...done with region:{region}") - if self.output_format.endswith('.ome.tiff'): + if self.output_format.endswith(".ome.tiff"): self.create_ome_tiff(self.stitched_images) else: output_path = os.path.join(self.input_folder, self.output_name) @@ -1574,14 +1745,13 @@ def run_regions(self): self.finished_saving.emit(os.path.join(self.input_folder, self.output_name), self.dtype) print("total time to stitch + save:", time.time() - stime) - -#________________________________________________________________________________________________________________________________ -# run_fovs: directly save fovs to final hcs ome zarr -# -# issue: -# only shows one fov per region when there are multiple fovs -# - (fix metadata? translation, scale, path, multiscale?) -# correct channels in napari, well + plate metadata, z-stack shape, time-point shape + # ________________________________________________________________________________________________________________________________ + # run_fovs: directly save fovs to final hcs ome zarr + # + # issue: + # only shows one fov per region when there are multiple fovs + # - (fix metadata? translation, scale, path, multiscale?) + # correct channels in napari, well + plate metadata, z-stack shape, time-point shape def run_fovs(self): stime = time.time() @@ -1593,7 +1763,9 @@ def run_fovs(self): self.write_fov_plate_metadata(root) - total_fovs = sum(len(set([k[2] for k in self.stitching_data.keys() if k[1] == region])) for region in self.regions) + total_fovs = sum( + len(set([k[2] for k in self.stitching_data.keys() if k[1] == region])) for region in self.regions + ) processed_fovs = 0 for region in self.regions: @@ -1602,7 +1774,7 @@ def run_fovs(self): for fov_idx in range(self.num_fovs_per_region): fov_data = {k: v for k, v in region_data.items() if k[2] == fov_idx} - + if not fov_data: continue # Skip if no data for this FOV index @@ -1611,17 +1783,16 @@ def run_fovs(self): processed_fovs += 1 self.update_progress.emit(processed_fovs, total_fovs) - omero_channels = [{ - "label": name, - "color": f"{color:06X}", - "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max} - } for name, color in zip(self.mono_channel_names, self.channel_colors)] + omero_channels = [ + { + "label": name, + "color": f"{color:06X}", + "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max}, + } + for name, color in zip(self.mono_channel_names, self.channel_colors) + ] - omero = { - "name": "hcs-acquisition", - "version": "0.4", - "channels": omero_channels - } + omero = {"name": "hcs-acquisition", "version": "0.4", "channels": omero_channels} root.attrs["omero"] = omero @@ -1637,15 +1808,15 @@ def compile_single_fov_data(self, fov_data): for key, scan_info in fov_data.items(): t, _, _, z_level, channel = key - image = dask_imread(scan_info['filepath'])[0] - + image = dask_imread(scan_info["filepath"])[0] + if self.apply_flatfield: channel_idx = self.mono_channel_names.index(channel) image = self.apply_flatfield_correction(image, channel_idx) if len(image.shape) == 3 and image.shape[2] == 3: # RGB image - channel = channel.split('_')[0] - for i, color in enumerate(['R', 'G', 'B']): + channel = channel.split("_")[0] + for i, color in enumerate(["R", "G", "B"]): c_idx = self.mono_channel_names.index(f"{channel}_{color}") tcz_fov[t, c_idx, z_level] = image[:, :, i] else: # Grayscale image @@ -1657,21 +1828,21 @@ def compile_single_fov_data(self, fov_data): def write_fov_plate_metadata(self, root): rows, columns = self.get_rows_and_columns() well_paths = [f"{well_id[0]}/{well_id[1:]}" for well_id in sorted(self.regions)] - + plate_metadata = { "name": "Sample", "rows": [{"name": row} for row in rows], "columns": [{"name": col} for col in columns], - "wells": [{"path": path, "rowIndex": rows.index(path[0]), "columnIndex": columns.index(path[2:])} - for path in well_paths], + "wells": [ + {"path": path, "rowIndex": rows.index(path[0]), "columnIndex": columns.index(path[2:])} + for path in well_paths + ], "field_count": self.num_fovs_per_region * len(self.regions), - "acquisitions": [{ - "id": 0, - "maximumfieldcount": self.num_fovs_per_region, - "name": "Multipoint Acquisition" - }] + "acquisitions": [ + {"id": 0, "maximumfieldcount": self.num_fovs_per_region, "name": "Multipoint Acquisition"} + ], } - + ome_zarr.writer.write_plate_metadata( root, rows=[row["name"] for row in plate_metadata["rows"]], @@ -1679,7 +1850,7 @@ def write_fov_plate_metadata(self, root): wells=plate_metadata["wells"], acquisitions=plate_metadata["acquisitions"], name=plate_metadata["name"], - field_count=plate_metadata["field_count"] + field_count=plate_metadata["field_count"], ) def write_fov_well_metadata(self, root, region): @@ -1687,7 +1858,7 @@ def write_fov_well_metadata(self, root, region): row_group = root.require_group(row) well_group = row_group.require_group(col) - if 'well' not in well_group.attrs: + if "well" not in well_group.attrs: well_metadata = { "images": [{"path": str(fov_idx), "acquisition": 0} for fov_idx in range(self.num_fovs_per_region)] } @@ -1700,7 +1871,7 @@ def write_fov_to_zarr(self, well_group, tcz_fov, fov_idx, fov_data): {"name": "c", "type": "channel"}, {"name": "z", "type": "space", "unit": "micrometer"}, {"name": "y", "type": "space", "unit": "micrometer"}, - {"name": "x", "type": "space", "unit": "micrometer"} + {"name": "x", "type": "space", "unit": "micrometer"}, ] # Generate pyramid levels @@ -1708,36 +1879,33 @@ def write_fov_to_zarr(self, well_group, tcz_fov, fov_idx, fov_data): # Get the position of the FOV (use the first scan in fov_data) first_scan = next(iter(fov_data.values())) - x_mm, y_mm = first_scan['x'], first_scan['y'] - + x_mm, y_mm = first_scan["x"], first_scan["y"] + # Get the z positions - z_positions = sorted(set(scan_info['z'] for scan_info in fov_data.values())) + z_positions = sorted(set(scan_info["z"] for scan_info in fov_data.values())) z_min = min(z_positions) dz = self.acquisition_params.get("dz(um)", 1.0) - + # Create coordinate transformations for each pyramid level coordinate_transformations = [] for level in range(len(pyramid)): - scale_factor = 2 ** level - coordinate_transformations.append([ - { - "type": "scale", - "scale": [1, 1, dz, self.pixel_size_um * scale_factor, self.pixel_size_um * scale_factor] - }, - { - "type": "translation", - "translation": [0, 0, z_min, y_mm*1000, x_mm*1000] - } - ]) + scale_factor = 2**level + coordinate_transformations.append( + [ + { + "type": "scale", + "scale": [1, 1, dz, self.pixel_size_um * scale_factor, self.pixel_size_um * scale_factor], + }, + {"type": "translation", "translation": [0, 0, z_min, y_mm * 1000, x_mm * 1000]}, + ] + ) image_group = well_group.require_group(str(fov_idx)) # Prepare datasets for multiscales metadata datasets = [ - { - "path": str(i), - "coordinateTransformations": coord_trans - } for i, coord_trans in enumerate(coordinate_transformations) + {"path": str(i), "coordinateTransformations": coord_trans} + for i, coord_trans in enumerate(coordinate_transformations) ] # Write multiscales metadata @@ -1745,7 +1913,7 @@ def write_fov_to_zarr(self, well_group, tcz_fov, fov_idx, fov_data): group=image_group, datasets=datasets, axes=axes, - name=f"FOV_{fov_idx}" # This will be passed as part of **metadata + name=f"FOV_{fov_idx}", # This will be passed as part of **metadata ) # Write the actual data @@ -1758,22 +1926,21 @@ def write_fov_to_zarr(self, well_group, tcz_fov, fov_idx, fov_data): ) # Add OMERO metadata - omero_channels = [{ - "label": name, - "color": f"{color:06X}", - "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max} - } for name, color in zip(self.mono_channel_names, self.channel_colors)] + omero_channels = [ + { + "label": name, + "color": f"{color:06X}", + "window": {"start": 0, "end": np.iinfo(self.dtype).max, "min": 0, "max": np.iinfo(self.dtype).max}, + } + for name, color in zip(self.mono_channel_names, self.channel_colors) + ] - omero = { - "name": f"FOV_{fov_idx}", - "version": "0.4", - "channels": omero_channels - } + omero = {"name": f"FOV_{fov_idx}", "version": "0.4", "channels": omero_channels} image_group.attrs["omero"] = omero def print_zarr_structure(self, path, indent=""): - root = zarr.open(path, mode='r') + root = zarr.open(path, mode="r") print(f"Zarr Tree and Metadata for: {path}") print(root.tree()) - print(dict(root.attrs)) \ No newline at end of file + print(dict(root.attrs)) diff --git a/software/control/tracking.py b/software/control/tracking.py index ec4b944c1..93e224b53 100755 --- a/software/control/tracking.py +++ b/software/control/tracking.py @@ -3,223 +3,232 @@ from os.path import realpath, dirname, join try: - import torch - from control.DaSiamRPN.code.net import SiamRPNvot - print(1) - from control.DaSiamRPN.code import vot - print(2) - from control.DaSiamRPN.code.utils import get_axis_aligned_bbox, cxy_wh_2_rect - print(3) - from control.DaSiamRPN.code.run_SiamRPN import SiamRPN_init, SiamRPN_track - print(4) + import torch + from control.DaSiamRPN.code.net import SiamRPNvot + + print(1) + from control.DaSiamRPN.code import vot + + print(2) + from control.DaSiamRPN.code.utils import get_axis_aligned_bbox, cxy_wh_2_rect + + print(3) + from control.DaSiamRPN.code.run_SiamRPN import SiamRPN_init, SiamRPN_track + + print(4) except Exception as e: - print(e) - # print('Warning: DaSiamRPN is not available!') + print(e) + # print('Warning: DaSiamRPN is not available!') from control._def import Tracking import cv2 + class Tracker_Image(object): - ''' - SLOTS: update_tracker_type, Connected to: Tracking Widget - ''' - - def __init__(self): - # Define list of trackers being used(maybe do this as a definition?) - # OpenCV tracking suite - # self.OPENCV_OBJECT_TRACKERS = {} - self.OPENCV_OBJECT_TRACKERS = { - "csrt": cv2.legacy.TrackerCSRT_create, - "kcf": cv2.legacy.TrackerKCF_create, - "mil": cv2.legacy.TrackerMIL_create, - } - try: - self.OPENCV_OBJECT_TRACKERS = { - "csrt": cv2.legacy.TrackerCSRT_create, - "kcf": cv2.legacy.TrackerKCF_create, - "boosting": cv2.legacy.TrackerBoosting_create, - "mil": cv2.legacy.TrackerMIL_create, - "tld": cv2.legacy.TrackerTLD_create, - "medianflow": cv2.legacy.TrackerMedianFlow_create, - "mosse": cv2.legacy.TrackerMOSSE_create - } - except: - print('Warning: OpenCV-Contrib trackers unavailable!') - - # Neural Net based trackers - self.NEURALNETTRACKERS = {"daSiamRPN":[]} - try: - # load net - self.net = SiamRPNvot() - self.net.load_state_dict(torch.load(join(realpath(dirname(__file__)),'DaSiamRPN','code','SiamRPNOTB.model'))) - self.net.eval().cuda() - print('Finished loading net ...') - except Exception as e: - print(e) - print('No neural net model found ...') - print('reverting to default OpenCV tracker') - - # Image Tracker type - self.tracker_type = Tracking.DEFAULT_TRACKER - # Init method for tracker - self.init_method = Tracking.DEFAULT_INIT_METHOD - # Create the tracker - self.create_tracker() - - # Centroid of object from the image - self.centroid_image = None # (2,1) - self.bbox = None - self.rect_pts = None - self.roi_bbox = None - self.origin = np.array([0,0]) - - self.isCentroidFound = False - self.trackerActive = False - self.searchArea = None - self.is_color = None - - def track(self, image, thresh_image, is_first_frame = False): - - # case 1: initialize the tracker - if(is_first_frame == True or self.trackerActive == False): - # tracker initialization - using ROI - if(self.init_method=="roi"): - self.bbox = tuple(self.roi_bbox) - self.centroid_image = self.centroid_from_bbox(self.bbox) - self.isCentroidFound = True - # tracker initialization - using thresholded image - else: - self.isCentroidFound, self.centroid_image, self.bbox = image_processing.find_centroid_basic_Rect(thresh_image) - self.bbox = image_processing.scale_square_bbox(self.bbox, Tracking.BBOX_SCALE_FACTOR, square = True) - # initialize the tracker - if(self.bbox is not None): - print('Starting tracker with initial bbox: {}'.format(self.bbox)) - self._initialize_tracker(image, self.centroid_image, self.bbox) - self.trackerActive = True - self.rect_pts = self.rectpts_from_bbox(self.bbox) - - # case 2: continue tracking an object using tracking - else: - # Find centroid using the tracking. - objectFound, self.bbox = self._update_tracker(image, thresh_image) # (x,y,w,h) - if(objectFound): - self.isCentroidFound = True - self.centroid_image = self.centroid_from_bbox(self.bbox) + self.origin - self.bbox = np.array(self.bbox) - self.bbox[0], self.bbox[1] = self.bbox[0] + self.origin[0], self.bbox[1] + self.origin[1] - self.rect_pts = self.rectpts_from_bbox(self.bbox) - else: - print('No object found ...') - self.isCentroidFound = False - self.trackerActive = False - return self.isCentroidFound, self.centroid_image, self.rect_pts - - def reset(self): - print('Reset image tracker state') - self.is_first_frame = True - self.trackerActive = False - self.isCentroidFound = False - - def create_tracker(self): - if(self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys()): - self.tracker = self.OPENCV_OBJECT_TRACKERS[self.tracker_type]() - elif(self.tracker_type in self.NEURALNETTRACKERS.keys()): - print('Using {} tracker'.format(self.tracker_type)) - pass - - def _initialize_tracker(self, image, centroid, bbox): - bbox = tuple(int(x) for x in bbox) - # check if the image is color or not - if(len(image.shape)<3): - self.is_color = False - # Initialize the OpenCV based tracker - if(self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys()): - print('Initializing openCV tracker') - print(self.tracker_type) - print(bbox) - if(self.is_color == False): - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - self.create_tracker() # for a new track, just calling self.tracker.init(image,bbox) is not sufficient, this line needs to be called - self.tracker.init(image, bbox) - # Initialize Neural Net based Tracker - elif(self.tracker_type in self.NEURALNETTRACKERS.keys()): - # Initialize the tracker with this centroid position - print('Initializing with daSiamRPN tracker') - target_pos, target_sz = np.array([centroid[0], centroid[1]]), np.array([bbox[2], bbox[3]]) - if(self.is_color==False): - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - self.state = SiamRPN_init(image, target_pos, target_sz, self.net) - print('daSiamRPN tracker initialized') - else: - pass - - def _update_tracker(self, image, thresh_image): - # Input: image or thresh_image - # Output: new_bbox based on tracking - new_bbox = None - # tracking w/ openCV tracker - if(self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys()): - self.origin = np.array([0,0]) - # (x,y,w,h)\ - if(self.is_color==False): - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - ok, new_bbox = self.tracker.update(image) - return ok, new_bbox - # tracking w/ the neural network-based tracker - elif(self.tracker_type in self.NEURALNETTRACKERS.keys()): - self.origin = np.array([0,0]) - if(self.is_color==False): - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - self.state = SiamRPN_track(self.state, image) - ok = True - if(ok): - # (x,y,w,h) - new_bbox = cxy_wh_2_rect(self.state['target_pos'], self.state['target_sz']) - new_bbox = [int(l) for l in new_bbox] - # print('Updated daSiamRPN tracker') - return ok, new_bbox - # tracking w/ nearest neighbhour using the thresholded image - else: - # If no tracker is specified, use basic thresholding and - # nearest neighbhour tracking. i.e Look for objects in a search region - # near the last detected centroid - - # Get the latest thresholded image from the queue - # thresh_image = - pts, thresh_image_cropped = image_processing.crop(thresh_image, self.centroid_image, self.searchArea) - self.origin = pts[0] - isCentroidFound, centroid, new_bbox = image_processing.find_centroid_basic_Rect(thresh_image_cropped) - return isCentroidFound, new_bbox - # @@@ Can add additional methods here for future tracker implementations - - # Signal from Tracking Widget connects to this Function - def update_tracker_type(self, tracker_type): - self.tracker_type = tracker_type - print('set tracker set to {}'.format(self.tracker_type)) - # self.create_tracker() - - def update_init_method(self, method): - self.init_method = method - print("Tracking init method set to : {}".format(self.init_method)) - - def centroid_from_bbox(self, bbox): - # Coordinates of the object centroid are taken as the center of the bounding box - assert(len(bbox) == 4) - cx = int(bbox[0] + bbox[2]/2) - cy = int(bbox[1] + bbox[3]/2) - centroid = np.array([cx, cy]) - return centroid - - def rectpts_from_bbox(self, bbox): - if(self.bbox is not None): - pts = np.array([[bbox[0], bbox[1]],[bbox[0] + bbox[2], bbox[1] + bbox[3]]], dtype = 'int') - else: - pts = None - return pts - - def update_searchArea(self, value): - self.searchArea = value - - def set_roi_bbox(self, bbox): - # Updates roi bbox from ImageDisplayWindow - self.roi_bbox = bbox - print('Rec bbox from ImageDisplay: {}'.format(self.roi_bbox)) + """ + SLOTS: update_tracker_type, Connected to: Tracking Widget + """ + + def __init__(self): + # Define list of trackers being used(maybe do this as a definition?) + # OpenCV tracking suite + # self.OPENCV_OBJECT_TRACKERS = {} + self.OPENCV_OBJECT_TRACKERS = { + "csrt": cv2.legacy.TrackerCSRT_create, + "kcf": cv2.legacy.TrackerKCF_create, + "mil": cv2.legacy.TrackerMIL_create, + } + try: + self.OPENCV_OBJECT_TRACKERS = { + "csrt": cv2.legacy.TrackerCSRT_create, + "kcf": cv2.legacy.TrackerKCF_create, + "boosting": cv2.legacy.TrackerBoosting_create, + "mil": cv2.legacy.TrackerMIL_create, + "tld": cv2.legacy.TrackerTLD_create, + "medianflow": cv2.legacy.TrackerMedianFlow_create, + "mosse": cv2.legacy.TrackerMOSSE_create, + } + except: + print("Warning: OpenCV-Contrib trackers unavailable!") + + # Neural Net based trackers + self.NEURALNETTRACKERS = {"daSiamRPN": []} + try: + # load net + self.net = SiamRPNvot() + self.net.load_state_dict( + torch.load(join(realpath(dirname(__file__)), "DaSiamRPN", "code", "SiamRPNOTB.model")) + ) + self.net.eval().cuda() + print("Finished loading net ...") + except Exception as e: + print(e) + print("No neural net model found ...") + print("reverting to default OpenCV tracker") + + # Image Tracker type + self.tracker_type = Tracking.DEFAULT_TRACKER + # Init method for tracker + self.init_method = Tracking.DEFAULT_INIT_METHOD + # Create the tracker + self.create_tracker() + + # Centroid of object from the image + self.centroid_image = None # (2,1) + self.bbox = None + self.rect_pts = None + self.roi_bbox = None + self.origin = np.array([0, 0]) + + self.isCentroidFound = False + self.trackerActive = False + self.searchArea = None + self.is_color = None + + def track(self, image, thresh_image, is_first_frame=False): + + # case 1: initialize the tracker + if is_first_frame == True or self.trackerActive == False: + # tracker initialization - using ROI + if self.init_method == "roi": + self.bbox = tuple(self.roi_bbox) + self.centroid_image = self.centroid_from_bbox(self.bbox) + self.isCentroidFound = True + # tracker initialization - using thresholded image + else: + self.isCentroidFound, self.centroid_image, self.bbox = image_processing.find_centroid_basic_Rect( + thresh_image + ) + self.bbox = image_processing.scale_square_bbox(self.bbox, Tracking.BBOX_SCALE_FACTOR, square=True) + # initialize the tracker + if self.bbox is not None: + print("Starting tracker with initial bbox: {}".format(self.bbox)) + self._initialize_tracker(image, self.centroid_image, self.bbox) + self.trackerActive = True + self.rect_pts = self.rectpts_from_bbox(self.bbox) + + # case 2: continue tracking an object using tracking + else: + # Find centroid using the tracking. + objectFound, self.bbox = self._update_tracker(image, thresh_image) # (x,y,w,h) + if objectFound: + self.isCentroidFound = True + self.centroid_image = self.centroid_from_bbox(self.bbox) + self.origin + self.bbox = np.array(self.bbox) + self.bbox[0], self.bbox[1] = self.bbox[0] + self.origin[0], self.bbox[1] + self.origin[1] + self.rect_pts = self.rectpts_from_bbox(self.bbox) + else: + print("No object found ...") + self.isCentroidFound = False + self.trackerActive = False + return self.isCentroidFound, self.centroid_image, self.rect_pts + + def reset(self): + print("Reset image tracker state") + self.is_first_frame = True + self.trackerActive = False + self.isCentroidFound = False + + def create_tracker(self): + if self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys(): + self.tracker = self.OPENCV_OBJECT_TRACKERS[self.tracker_type]() + elif self.tracker_type in self.NEURALNETTRACKERS.keys(): + print("Using {} tracker".format(self.tracker_type)) + pass + + def _initialize_tracker(self, image, centroid, bbox): + bbox = tuple(int(x) for x in bbox) + # check if the image is color or not + if len(image.shape) < 3: + self.is_color = False + # Initialize the OpenCV based tracker + if self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys(): + print("Initializing openCV tracker") + print(self.tracker_type) + print(bbox) + if self.is_color == False: + image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) + self.create_tracker() # for a new track, just calling self.tracker.init(image,bbox) is not sufficient, this line needs to be called + self.tracker.init(image, bbox) + # Initialize Neural Net based Tracker + elif self.tracker_type in self.NEURALNETTRACKERS.keys(): + # Initialize the tracker with this centroid position + print("Initializing with daSiamRPN tracker") + target_pos, target_sz = np.array([centroid[0], centroid[1]]), np.array([bbox[2], bbox[3]]) + if self.is_color == False: + image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) + self.state = SiamRPN_init(image, target_pos, target_sz, self.net) + print("daSiamRPN tracker initialized") + else: + pass + + def _update_tracker(self, image, thresh_image): + # Input: image or thresh_image + # Output: new_bbox based on tracking + new_bbox = None + # tracking w/ openCV tracker + if self.tracker_type in self.OPENCV_OBJECT_TRACKERS.keys(): + self.origin = np.array([0, 0]) + # (x,y,w,h)\ + if self.is_color == False: + image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) + ok, new_bbox = self.tracker.update(image) + return ok, new_bbox + # tracking w/ the neural network-based tracker + elif self.tracker_type in self.NEURALNETTRACKERS.keys(): + self.origin = np.array([0, 0]) + if self.is_color == False: + image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) + self.state = SiamRPN_track(self.state, image) + ok = True + if ok: + # (x,y,w,h) + new_bbox = cxy_wh_2_rect(self.state["target_pos"], self.state["target_sz"]) + new_bbox = [int(l) for l in new_bbox] + # print('Updated daSiamRPN tracker') + return ok, new_bbox + # tracking w/ nearest neighbhour using the thresholded image + else: + # If no tracker is specified, use basic thresholding and + # nearest neighbhour tracking. i.e Look for objects in a search region + # near the last detected centroid + + # Get the latest thresholded image from the queue + # thresh_image = + pts, thresh_image_cropped = image_processing.crop(thresh_image, self.centroid_image, self.searchArea) + self.origin = pts[0] + isCentroidFound, centroid, new_bbox = image_processing.find_centroid_basic_Rect(thresh_image_cropped) + return isCentroidFound, new_bbox + # @@@ Can add additional methods here for future tracker implementations + + # Signal from Tracking Widget connects to this Function + def update_tracker_type(self, tracker_type): + self.tracker_type = tracker_type + print("set tracker set to {}".format(self.tracker_type)) + # self.create_tracker() + + def update_init_method(self, method): + self.init_method = method + print("Tracking init method set to : {}".format(self.init_method)) + + def centroid_from_bbox(self, bbox): + # Coordinates of the object centroid are taken as the center of the bounding box + assert len(bbox) == 4 + cx = int(bbox[0] + bbox[2] / 2) + cy = int(bbox[1] + bbox[3] / 2) + centroid = np.array([cx, cy]) + return centroid + + def rectpts_from_bbox(self, bbox): + if self.bbox is not None: + pts = np.array([[bbox[0], bbox[1]], [bbox[0] + bbox[2], bbox[1] + bbox[3]]], dtype="int") + else: + pts = None + return pts + + def update_searchArea(self, value): + self.searchArea = value + + def set_roi_bbox(self, bbox): + # Updates roi bbox from ImageDisplayWindow + self.roi_bbox = bbox + print("Rec bbox from ImageDisplay: {}".format(self.roi_bbox)) diff --git a/software/control/utils.py b/software/control/utils.py index f15ff3a8a..06d586394 100644 --- a/software/control/utils.py +++ b/software/control/utils.py @@ -4,73 +4,78 @@ from scipy.ndimage import label import os -def crop_image(image,crop_width,crop_height): + +def crop_image(image, crop_width, crop_height): image_height = image.shape[0] image_width = image.shape[1] - roi_left = int(max(image_width/2 - crop_width/2,0)) - roi_right = int(min(image_width/2 + crop_width/2,image_width)) - roi_top = int(max(image_height/2 - crop_height/2,0)) - roi_bottom = int(min(image_height/2 + crop_height/2,image_height)) - image_cropped = image[roi_top:roi_bottom,roi_left:roi_right] + roi_left = int(max(image_width / 2 - crop_width / 2, 0)) + roi_right = int(min(image_width / 2 + crop_width / 2, image_width)) + roi_top = int(max(image_height / 2 - crop_height / 2, 0)) + roi_bottom = int(min(image_height / 2 + crop_height / 2, image_height)) + image_cropped = image[roi_top:roi_bottom, roi_left:roi_right] return image_cropped -def calculate_focus_measure(image,method='LAPE'): + +def calculate_focus_measure(image, method="LAPE"): if len(image.shape) == 3: - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) # optional - if method == 'LAPE': + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) # optional + if method == "LAPE": if image.dtype == np.uint16: - lap = cv2.Laplacian(image,cv2.CV_32F) + lap = cv2.Laplacian(image, cv2.CV_32F) else: - lap = cv2.Laplacian(image,cv2.CV_16S) + lap = cv2.Laplacian(image, cv2.CV_16S) focus_measure = mean(square(lap)) - elif method == 'GLVA': - focus_measure = np.std(image,axis=None)# GLVA + elif method == "GLVA": + focus_measure = np.std(image, axis=None) # GLVA else: - focus_measure = np.std(image,axis=None)# GLVA + focus_measure = np.std(image, axis=None) # GLVA return focus_measure -def unsigned_to_signed(unsigned_array,N): + +def unsigned_to_signed(unsigned_array, N): signed = 0 for i in range(N): - signed = signed + int(unsigned_array[i])*(256**(N-1-i)) - signed = signed - (256**N)/2 + signed = signed + int(unsigned_array[i]) * (256 ** (N - 1 - i)) + signed = signed - (256**N) / 2 return signed -def rotate_and_flip_image(image,rotate_image_angle,flip_image): + +def rotate_and_flip_image(image, rotate_image_angle, flip_image): ret_image = image.copy() - if(rotate_image_angle != 0): - ''' - # ROTATE_90_CLOCKWISE - # ROTATE_90_COUNTERCLOCKWISE - ''' - if(rotate_image_angle == 90): - ret_image = cv2.rotate(ret_image,cv2.ROTATE_90_CLOCKWISE) - elif(rotate_image_angle == -90): - ret_image = cv2.rotate(ret_image,cv2.ROTATE_90_COUNTERCLOCKWISE) - elif(rotate_image_angle == 180): - ret_image = cv2.rotate(ret_image,cv2.ROTATE_180) - - if(flip_image is not None): - ''' - flipcode = 0: flip vertically - flipcode > 0: flip horizontally - flipcode < 0: flip vertically and horizontally - ''' - if(flip_image == 'Vertical'): + if rotate_image_angle != 0: + """ + # ROTATE_90_CLOCKWISE + # ROTATE_90_COUNTERCLOCKWISE + """ + if rotate_image_angle == 90: + ret_image = cv2.rotate(ret_image, cv2.ROTATE_90_CLOCKWISE) + elif rotate_image_angle == -90: + ret_image = cv2.rotate(ret_image, cv2.ROTATE_90_COUNTERCLOCKWISE) + elif rotate_image_angle == 180: + ret_image = cv2.rotate(ret_image, cv2.ROTATE_180) + + if flip_image is not None: + """ + flipcode = 0: flip vertically + flipcode > 0: flip horizontally + flipcode < 0: flip vertically and horizontally + """ + if flip_image == "Vertical": ret_image = cv2.flip(ret_image, 0) - elif(flip_image == 'Horizontal'): + elif flip_image == "Horizontal": ret_image = cv2.flip(ret_image, 1) - elif(flip_image == 'Both'): + elif flip_image == "Both": ret_image = cv2.flip(ret_image, -1) return ret_image + def generate_dpc(im_left, im_right): # Normalize the images - im_left = im_left.astype(float)/255 - im_right = im_right.astype(float)/255 + im_left = im_left.astype(float) / 255 + im_right = im_right.astype(float) / 255 # differential phase contrast calculation - im_dpc = 0.5 + np.divide(im_left-im_right, im_left+im_right) + im_dpc = 0.5 + np.divide(im_left - im_right, im_left + im_right) # take care of errors im_dpc[im_dpc < 0] = 0 im_dpc[im_dpc > 1] = 1 @@ -80,6 +85,7 @@ def generate_dpc(im_left, im_right): return im_dpc + def colorize_mask(mask): # Label the detected objects labeled_mask, ___ = label(mask) @@ -90,6 +96,7 @@ def colorize_mask(mask): colored_mask[labeled_mask == 0] = 0 return colored_mask + def colorize_mask_get_counts(mask): # Label the detected objects labeled_mask, no_cells = label(mask) @@ -100,20 +107,23 @@ def colorize_mask_get_counts(mask): colored_mask[labeled_mask == 0] = 0 return colored_mask, no_cells + def overlay_mask_dpc(color_mask, im_dpc): # Overlay the colored mask and DPC image # make DPC 3-channel - im_dpc = np.stack([im_dpc]*3, axis=2) - return (0.75*im_dpc + 0.25*color_mask).astype(np.uint8) - + im_dpc = np.stack([im_dpc] * 3, axis=2) + return (0.75 * im_dpc + 0.25 * color_mask).astype(np.uint8) + + def centerCrop(image, crop_sz): center = image.shape - x = int(center[1]/2 - crop_sz/2) - y = int(center[0]/2 - crop_sz/2) - cropped = image[y:y+crop_sz, x:x+crop_sz] - + x = int(center[1] / 2 - crop_sz / 2) + y = int(center[0] / 2 - crop_sz / 2) + cropped = image[y : y + crop_sz, x : x + crop_sz] + return cropped + def interpolate_plane(triple1, triple2, triple3, point): """ Given 3 triples triple1-3 of coordinates (x,y,z) @@ -125,7 +135,7 @@ def interpolate_plane(triple1, triple2, triple3, point): x2, y2, z2 = triple2 x3, y3, z3 = triple3 - x,y = point + x, y = point # Calculate barycentric coordinates detT = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3) if detT == 0: @@ -139,6 +149,7 @@ def interpolate_plane(triple1, triple2, triple3, point): return z + def create_done_file(path): - with open(os.path.join(path,'.done'), 'w') as file: + with open(os.path.join(path, ".done"), "w") as file: pass # This creates an empty file diff --git a/software/control/utils_/image_processing.py b/software/control/utils_/image_processing.py index a7d996160..8621b3543 100644 --- a/software/control/utils_/image_processing.py +++ b/software/control/utils_/image_processing.py @@ -10,60 +10,69 @@ from scipy.ndimage.filters import laplace from numpy import std, square, mean -#color is a vector HSV whose size is 3 +# color is a vector HSV whose size is 3 def default_lower_HSV(color): - c=[0,100,100] - c[0]=np.max([color[0]-10,0]) - c[1]=np.max([color[1]-40,0]) - c[2]=np.max([color[2]-40,0]) - return np.array(c,dtype="uint8") + c = [0, 100, 100] + c[0] = np.max([color[0] - 10, 0]) + c[1] = np.max([color[1] - 40, 0]) + c[2] = np.max([color[2] - 40, 0]) + return np.array(c, dtype="uint8") + def default_upper_HSV(color): - c=[0,255,255] - c[0]=np.min([color[0]+10,178]) - c[1]=np.min([color[1]+40,255]) - c[2]=np.min([color[2]+40,255]) - return np.array(c,dtype="uint8") - -def threshold_image(image_BGR,LOWER,UPPER): - image_HSV = cv2.cvtColor(image_BGR,cv2.COLOR_BGR2HSV) - imgMask = 255*np.array(cv2.inRange(image_HSV, LOWER, UPPER), dtype='uint8') #The tracked object will be in white - imgMask = cv2.erode(imgMask, None, iterations=2) # Do a series of erosions and dilations on the thresholded image to reduce smaller blobs + c = [0, 255, 255] + c[0] = np.min([color[0] + 10, 178]) + c[1] = np.min([color[1] + 40, 255]) + c[2] = np.min([color[2] + 40, 255]) + return np.array(c, dtype="uint8") + + +def threshold_image(image_BGR, LOWER, UPPER): + image_HSV = cv2.cvtColor(image_BGR, cv2.COLOR_BGR2HSV) + imgMask = 255 * np.array(cv2.inRange(image_HSV, LOWER, UPPER), dtype="uint8") # The tracked object will be in white + imgMask = cv2.erode( + imgMask, None, iterations=2 + ) # Do a series of erosions and dilations on the thresholded image to reduce smaller blobs imgMask = cv2.dilate(imgMask, None, iterations=2) - + return imgMask + def threshold_image_gray(image_gray, LOWER, UPPER): - imgMask = np.array((image_gray >= LOWER) & (image_gray <= UPPER), dtype='uint8') - + imgMask = np.array((image_gray >= LOWER) & (image_gray <= UPPER), dtype="uint8") + # imgMask = cv2.inRange(cv2.UMat(image_gray), LOWER, UPPER) #The tracked object will be in white - imgMask = cv2.erode(imgMask, None, iterations=2) # Do a series of erosions and dilations on the thresholded image to reduce smaller blobs + imgMask = cv2.erode( + imgMask, None, iterations=2 + ) # Do a series of erosions and dilations on the thresholded image to reduce smaller blobs imgMask = cv2.dilate(imgMask, None, iterations=2) - + return imgMask + def bgr2gray(image_BGR): - return cv2.cvtColor(image_BGR,cv2.COLOR_BGR2GRAY) + return cv2.cvtColor(image_BGR, cv2.COLOR_BGR2GRAY) -def crop(image,center,imSize): #center is the vector [x,y] - imH,imW,*rest=image.shape #image.shape:[nb of row -->height,nb of column --> Width] - xmin = max(10,center[0] - int(imSize)) - xmax = min(imW-10,center[0] + int(imSize)) - ymin = max(10,center[1] - int(imSize)) - ymax = min(imH-10,center[1] + int(imSize)) - return np.array([[xmin,ymin],[xmax,ymax]]),np.array(image[ymin:ymax,xmin:xmax]) +def crop(image, center, imSize): # center is the vector [x,y] + imH, imW, *rest = image.shape # image.shape:[nb of row -->height,nb of column --> Width] + xmin = max(10, center[0] - int(imSize)) + xmax = min(imW - 10, center[0] + int(imSize)) + ymin = max(10, center[1] - int(imSize)) + ymax = min(imH - 10, center[1] + int(imSize)) + return np.array([[xmin, ymin], [xmax, ymax]]), np.array(image[ymin:ymax, xmin:xmax]) -def crop_image(image,crop_width,crop_height): + +def crop_image(image, crop_width, crop_height): image_height = image.shape[0] image_width = image.shape[1] - roi_left = int(max(image_width/2 - crop_width/2,0)) - roi_right = int(min(image_width/2 + crop_width/2,image_width)) - roi_top = int(max(image_height/2 - crop_height/2,0)) - roi_bottom = int(min(image_height/2 + crop_height/2,image_height)) - image_cropped = image[roi_top:roi_bottom,roi_left:roi_right] + roi_left = int(max(image_width / 2 - crop_width / 2, 0)) + roi_right = int(min(image_width / 2 + crop_width / 2, image_width)) + roi_top = int(max(image_height / 2 - crop_height / 2, 0)) + roi_bottom = int(min(image_height / 2 + crop_height / 2, image_height)) + image_cropped = image[roi_top:roi_bottom, roi_left:roi_right] image_cropped_height = image_cropped.shape[0] image_cropped_width = image_cropped.shape[1] return image_cropped, image_cropped_width, image_cropped_height @@ -73,194 +82,202 @@ def get_bbox(cnt): return cv2.boundingRect(cnt) -def find_centroid_enhanced(image,last_centroid): - #find contour takes image with 8 bit int and only one channel - #find contour looks for white object on a black back ground +def find_centroid_enhanced(image, last_centroid): + # find contour takes image with 8 bit int and only one channel + # find contour looks for white object on a black back ground # This looks for all contours in the thresholded image and then finds the centroid that maximizes a tracking metric # Tracking metric : current centroid area/(1 + dist_to_prev_centroid**2) - contours = cv2.findContours(image, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2] - centroid=False - isCentroidFound=False - if len(contours)>0: - all_centroid=[] - dist=[] + contours = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2] + centroid = False + isCentroidFound = False + if len(contours) > 0: + all_centroid = [] + dist = [] for cnt in contours: M = cv2.moments(cnt) - if M['m00']!=0: - cx = int(M['m10']/M['m00']) - cy = int(M['m01']/M['m00']) - centroid=np.array([cx,cy]) - isCentroidFound=True + if M["m00"] != 0: + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + centroid = np.array([cx, cy]) + isCentroidFound = True all_centroid.append(centroid) - dist.append([cv2.contourArea(cnt)/(1+(centroid-last_centroid)**2)]) + dist.append([cv2.contourArea(cnt) / (1 + (centroid - last_centroid) ** 2)]) if isCentroidFound: - ind=dist.index(max(dist)) - centroid=all_centroid[ind] + ind = dist.index(max(dist)) + centroid = all_centroid[ind] - return isCentroidFound,centroid + return isCentroidFound, centroid -def find_centroid_enhanced_Rect(image,last_centroid): - #find contour takes image with 8 bit int and only one channel - #find contour looks for white object on a black back ground + +def find_centroid_enhanced_Rect(image, last_centroid): + # find contour takes image with 8 bit int and only one channel + # find contour looks for white object on a black back ground # This looks for all contours in the thresholded image and then finds the centroid that maximizes a tracking metric # Tracking metric : current centroid area/(1 + dist_to_prev_centroid**2) - contours = cv2.findContours(image, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2] - centroid=False - isCentroidFound=False + contours = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2] + centroid = False + isCentroidFound = False rect = False - if len(contours)>0: - all_centroid=[] - dist=[] + if len(contours) > 0: + all_centroid = [] + dist = [] for cnt in contours: M = cv2.moments(cnt) - if M['m00']!=0: - cx = int(M['m10']/M['m00']) - cy = int(M['m01']/M['m00']) - centroid=np.array([cx,cy]) - isCentroidFound=True + if M["m00"] != 0: + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + centroid = np.array([cx, cy]) + isCentroidFound = True all_centroid.append(centroid) - dist.append([cv2.contourArea(cnt)/(1+(centroid-last_centroid)**2)]) + dist.append([cv2.contourArea(cnt) / (1 + (centroid - last_centroid) ** 2)]) if isCentroidFound: - ind=dist.index(max(dist)) - centroid=all_centroid[ind] + ind = dist.index(max(dist)) + centroid = all_centroid[ind] cnt = contours[ind] - xmin,ymin,width,height = cv2.boundingRect(cnt) - xmin = max(0,xmin) - ymin = max(0,ymin) + xmin, ymin, width, height = cv2.boundingRect(cnt) + xmin = max(0, xmin) + ymin = max(0, ymin) width = min(width, imW - int(cx)) height = min(height, imH - int(cy)) rect = (xmin, ymin, width, height) + return isCentroidFound, centroid, rect - return isCentroidFound,centroid, rect def find_centroid_basic(image): - #find contour takes image with 8 bit int and only one channel - #find contour looks for white object on a black back ground + # find contour takes image with 8 bit int and only one channel + # find contour looks for white object on a black back ground # This finds the centroid with the maximum area in the current frame - contours = cv2.findContours(image, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2] - centroid=False - isCentroidFound=False - if len(contours)>0: + contours = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2] + centroid = False + isCentroidFound = False + if len(contours) > 0: cnt = max(contours, key=cv2.contourArea) M = cv2.moments(cnt) - if M['m00']!=0: - cx = int(M['m10']/M['m00']) - cy = int(M['m01']/M['m00']) - centroid=np.array([cx,cy]) - isCentroidFound=True - return isCentroidFound,centroid + if M["m00"] != 0: + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + centroid = np.array([cx, cy]) + isCentroidFound = True + return isCentroidFound, centroid + def find_centroid_basic_Rect(image): - #find contour takes image with 8 bit int and only one channel - #find contour looks for white object on a black back ground + # find contour takes image with 8 bit int and only one channel + # find contour looks for white object on a black back ground # This finds the centroid with the maximum area in the current frame and alsio the bounding rectangle. - DK 2018_12_12 - imH,imW = image.shape - contours = cv2.findContours(image, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2] - centroid=False - isCentroidFound=False + imH, imW = image.shape + contours = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2] + centroid = False + isCentroidFound = False bbox = None rect = False - if len(contours)>0: + if len(contours) > 0: # Find contour with max area cnt = max(contours, key=cv2.contourArea) M = cv2.moments(cnt) - if M['m00']!=0: + if M["m00"] != 0: # Centroid coordinates - cx = int(M['m10']/M['m00']) - cy = int(M['m01']/M['m00']) - centroid=np.array([cx,cy]) - isCentroidFound=True - - # Find the bounding rectangle - xmin,ymin,width,height = cv2.boundingRect(cnt) - xmin = max(0,xmin) - ymin = max(0,ymin) + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + centroid = np.array([cx, cy]) + isCentroidFound = True + + # Find the bounding rectangle + xmin, ymin, width, height = cv2.boundingRect(cnt) + xmin = max(0, xmin) + ymin = max(0, ymin) width = min(width, imW - xmin) height = min(height, imH - ymin) - + bbox = (xmin, ymin, width, height) - return isCentroidFound,centroid, bbox + return isCentroidFound, centroid, bbox + -def scale_square_bbox(bbox, scale_factor, square = True): +def scale_square_bbox(bbox, scale_factor, square=True): xmin, ymin, width, height = bbox - if(square==True): + if square == True: min_dim = min(width, height) width, height = min_dim, min_dim - new_width, new_height = int(scale_factor*width), int(scale_factor*height) + new_width, new_height = int(scale_factor * width), int(scale_factor * height) - new_xmin = xmin - (new_width - width)/2 - new_ymin = ymin - (new_height - height)/2 + new_xmin = xmin - (new_width - width) / 2 + new_ymin = ymin - (new_height - height) / 2 new_bbox = (new_xmin, new_ymin, new_width, new_height) return new_bbox + def get_image_center_width(image): - ImShape=image.shape - ImH,ImW=ImShape[0],ImShape[1] - return np.array([ImW*0.5,ImH*0.5]), ImW + ImShape = image.shape + ImH, ImW = ImShape[0], ImShape[1] + return np.array([ImW * 0.5, ImH * 0.5]), ImW + def get_image_height_width(image): - ImShape=image.shape - ImH,ImW=ImShape[0],ImShape[1] + ImShape = image.shape + ImH, ImW = ImShape[0], ImShape[1] return ImH, ImW + def get_image_top_center_width(image): - ImShape=image.shape - ImH,ImWs=ImShape[0],ImShape[1] - return np.array([ImW*0.5,0.25*ImH]),ImW + ImShape = image.shape + ImH, ImWs = ImShape[0], ImShape[1] + return np.array([ImW * 0.5, 0.25 * ImH]), ImW def YTracking_Objective_Function(image, color): - #variance method - if(image.size != 0): - if(color): + # variance method + if image.size != 0: + if color: image = bgr2gray(image) - mean,std=cv2.meanStdDev(image) - return std[0][0]**2 + mean, std = cv2.meanStdDev(image) + return std[0][0] ** 2 else: return 0 + def calculate_focus_measure(image): if len(image.shape) == 3: - image = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY) # optional - lap = cv2.Laplacian(image,cv2.CV_16S) + image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) # optional + lap = cv2.Laplacian(image, cv2.CV_16S) focus_measure = mean(square(lap)) return focus_measure -#test part + +# test part if __name__ == "__main__": # Load an color image in grayscale - rouge=np.array([[[255,0,0]]],dtype="uint8") - vert=np.array([[[0,255,0]]],dtype="uint8") - bleu=np.array([[[0,0,255]]],dtype="uint8") - - rouge_HSV=cv2.cvtColor(rouge,cv2.COLOR_RGB2HSV)[0][0] - vert_HSV=cv2.cvtColor(vert,cv2.COLOR_RGB2HSV)[0][0] - bleu_HSV=cv2.cvtColor(bleu,cv2.COLOR_RGB2HSV)[0][0] - - img = cv2.imread('C:/Users/Francois/Documents/11-Stage_3A/6-Code_Python/ConsoleWheel/test/rouge.jpg') + rouge = np.array([[[255, 0, 0]]], dtype="uint8") + vert = np.array([[[0, 255, 0]]], dtype="uint8") + bleu = np.array([[[0, 0, 255]]], dtype="uint8") + + rouge_HSV = cv2.cvtColor(rouge, cv2.COLOR_RGB2HSV)[0][0] + vert_HSV = cv2.cvtColor(vert, cv2.COLOR_RGB2HSV)[0][0] + bleu_HSV = cv2.cvtColor(bleu, cv2.COLOR_RGB2HSV)[0][0] + + img = cv2.imread("C:/Users/Francois/Documents/11-Stage_3A/6-Code_Python/ConsoleWheel/test/rouge.jpg") print(img) - img2=cv2.cvtColor(img,cv2.COLOR_RGB2BGR) - + img2 = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + couleur = bleu_HSV LOWER = default_lower_HSV(couleur) UPPER = default_upper_HSV(couleur) - - img3=threshold_image(img2,LOWER,UPPER) - cv2.imshow('image',img3) + + img3 = threshold_image(img2, LOWER, UPPER) + cv2.imshow("image", img3) cv2.waitKey(0) cv2.destroyAllWindows() -#for more than one tracked object -''' +# for more than one tracked object +""" def find_centroid_many(image,contour_area_min,contour_area_max): contours = cv2.findContours(image, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2] count=0 @@ -274,5 +291,4 @@ def find_centroid_many(image,contour_area_min,contour_area_max): last_centroids.append([cx,cy]) count+=1 return last_centroids,count -''' - +""" diff --git a/software/control/utils_config.py b/software/control/utils_config.py index e2fbe15af..4eb2b8a15 100644 --- a/software/control/utils_config.py +++ b/software/control/utils_config.py @@ -1,111 +1,113 @@ from lxml import etree as ET -top = ET.Element('modes') + +top = ET.Element("modes") + def generate_default_configuration(filename): - mode_1 = ET.SubElement(top,'mode') - mode_1.set('ID','1') - mode_1.set('Name','BF LED matrix full') - mode_1.set('ExposureTime','12') - mode_1.set('AnalogGain','0') - mode_1.set('IlluminationSource','0') - mode_1.set('IlluminationIntensity','5') - mode_1.set('CameraSN','') - mode_1.set('ZOffset','0.0') - mode_1.set('PixelFormat','default') - mode_1.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_1.set('EmissionFilterPosition','1') - - mode_4 = ET.SubElement(top,'mode') - mode_4.set('ID','4') - mode_4.set('Name','DF LED matrix') - mode_4.set('ExposureTime','22') - mode_4.set('AnalogGain','0') - mode_4.set('IlluminationSource','3') - mode_4.set('IlluminationIntensity','5') - mode_4.set('CameraSN','') - mode_4.set('ZOffset','0.0') - mode_4.set('PixelFormat','default') - mode_4.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_4.set('EmissionFilterPosition','1') - - mode_5 = ET.SubElement(top,'mode') - mode_5.set('ID','5') - mode_5.set('Name','Fluorescence 405 nm Ex') - mode_5.set('ExposureTime','100') - mode_5.set('AnalogGain','10') - mode_5.set('IlluminationSource','11') - mode_5.set('IlluminationIntensity','100') - mode_5.set('CameraSN','') - mode_5.set('ZOffset','0.0') - mode_5.set('PixelFormat','default') - mode_5.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_5.set('EmissionFilterPosition','1') - - mode_6 = ET.SubElement(top,'mode') - mode_6.set('ID','6') - mode_6.set('Name','Fluorescence 488 nm Ex') - mode_6.set('ExposureTime','100') - mode_6.set('AnalogGain','10') - mode_6.set('IlluminationSource','12') - mode_6.set('IlluminationIntensity','100') - mode_6.set('CameraSN','') - mode_6.set('ZOffset','0.0') - mode_6.set('PixelFormat','default') - mode_6.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_6.set('EmissionFilterPosition','1') - - mode_7 = ET.SubElement(top,'mode') - mode_7.set('ID','7') - mode_7.set('Name','Fluorescence 638 nm Ex') - mode_7.set('ExposureTime','100') - mode_7.set('AnalogGain','10') - mode_7.set('IlluminationSource','13') - mode_7.set('IlluminationIntensity','100') - mode_7.set('CameraSN','') - mode_7.set('ZOffset','0.0') - mode_7.set('PixelFormat','default') - mode_7.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_7.set('EmissionFilterPosition','1') - - mode_8 = ET.SubElement(top,'mode') - mode_8.set('ID','8') - mode_8.set('Name','Fluorescence 561 nm Ex') - mode_8.set('ExposureTime','100') - mode_8.set('AnalogGain','10') - mode_8.set('IlluminationSource','14') - mode_8.set('IlluminationIntensity','100') - mode_8.set('CameraSN','') - mode_8.set('ZOffset','0.0') - mode_8.set('PixelFormat','default') - mode_8.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_8.set('EmissionFilterPosition','1') - - mode_12 = ET.SubElement(top,'mode') - mode_12.set('ID','12') - mode_12.set('Name','Fluorescence 730 nm Ex') - mode_12.set('ExposureTime','50') - mode_12.set('AnalogGain','10') - mode_12.set('IlluminationSource','15') - mode_12.set('IlluminationIntensity','100') - mode_12.set('CameraSN','') - mode_12.set('ZOffset','0.0') - mode_12.set('PixelFormat','default') - mode_12.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_12.set('EmissionFilterPosition','1') - - mode_9 = ET.SubElement(top,'mode') - mode_9.set('ID','9') - mode_9.set('Name','BF LED matrix low NA') - mode_9.set('ExposureTime','20') - mode_9.set('AnalogGain','0') - mode_9.set('IlluminationSource','4') - mode_9.set('IlluminationIntensity','20') - mode_9.set('CameraSN','') - mode_9.set('ZOffset','0.0') - mode_9.set('PixelFormat','default') - mode_9.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_9.set('EmissionFilterPosition','1') + mode_1 = ET.SubElement(top, "mode") + mode_1.set("ID", "1") + mode_1.set("Name", "BF LED matrix full") + mode_1.set("ExposureTime", "12") + mode_1.set("AnalogGain", "0") + mode_1.set("IlluminationSource", "0") + mode_1.set("IlluminationIntensity", "5") + mode_1.set("CameraSN", "") + mode_1.set("ZOffset", "0.0") + mode_1.set("PixelFormat", "default") + mode_1.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_1.set("EmissionFilterPosition", "1") + + mode_4 = ET.SubElement(top, "mode") + mode_4.set("ID", "4") + mode_4.set("Name", "DF LED matrix") + mode_4.set("ExposureTime", "22") + mode_4.set("AnalogGain", "0") + mode_4.set("IlluminationSource", "3") + mode_4.set("IlluminationIntensity", "5") + mode_4.set("CameraSN", "") + mode_4.set("ZOffset", "0.0") + mode_4.set("PixelFormat", "default") + mode_4.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_4.set("EmissionFilterPosition", "1") + + mode_5 = ET.SubElement(top, "mode") + mode_5.set("ID", "5") + mode_5.set("Name", "Fluorescence 405 nm Ex") + mode_5.set("ExposureTime", "100") + mode_5.set("AnalogGain", "10") + mode_5.set("IlluminationSource", "11") + mode_5.set("IlluminationIntensity", "100") + mode_5.set("CameraSN", "") + mode_5.set("ZOffset", "0.0") + mode_5.set("PixelFormat", "default") + mode_5.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_5.set("EmissionFilterPosition", "1") + + mode_6 = ET.SubElement(top, "mode") + mode_6.set("ID", "6") + mode_6.set("Name", "Fluorescence 488 nm Ex") + mode_6.set("ExposureTime", "100") + mode_6.set("AnalogGain", "10") + mode_6.set("IlluminationSource", "12") + mode_6.set("IlluminationIntensity", "100") + mode_6.set("CameraSN", "") + mode_6.set("ZOffset", "0.0") + mode_6.set("PixelFormat", "default") + mode_6.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_6.set("EmissionFilterPosition", "1") + + mode_7 = ET.SubElement(top, "mode") + mode_7.set("ID", "7") + mode_7.set("Name", "Fluorescence 638 nm Ex") + mode_7.set("ExposureTime", "100") + mode_7.set("AnalogGain", "10") + mode_7.set("IlluminationSource", "13") + mode_7.set("IlluminationIntensity", "100") + mode_7.set("CameraSN", "") + mode_7.set("ZOffset", "0.0") + mode_7.set("PixelFormat", "default") + mode_7.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_7.set("EmissionFilterPosition", "1") + + mode_8 = ET.SubElement(top, "mode") + mode_8.set("ID", "8") + mode_8.set("Name", "Fluorescence 561 nm Ex") + mode_8.set("ExposureTime", "100") + mode_8.set("AnalogGain", "10") + mode_8.set("IlluminationSource", "14") + mode_8.set("IlluminationIntensity", "100") + mode_8.set("CameraSN", "") + mode_8.set("ZOffset", "0.0") + mode_8.set("PixelFormat", "default") + mode_8.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_8.set("EmissionFilterPosition", "1") + + mode_12 = ET.SubElement(top, "mode") + mode_12.set("ID", "12") + mode_12.set("Name", "Fluorescence 730 nm Ex") + mode_12.set("ExposureTime", "50") + mode_12.set("AnalogGain", "10") + mode_12.set("IlluminationSource", "15") + mode_12.set("IlluminationIntensity", "100") + mode_12.set("CameraSN", "") + mode_12.set("ZOffset", "0.0") + mode_12.set("PixelFormat", "default") + mode_12.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_12.set("EmissionFilterPosition", "1") + + mode_9 = ET.SubElement(top, "mode") + mode_9.set("ID", "9") + mode_9.set("Name", "BF LED matrix low NA") + mode_9.set("ExposureTime", "20") + mode_9.set("AnalogGain", "0") + mode_9.set("IlluminationSource", "4") + mode_9.set("IlluminationIntensity", "20") + mode_9.set("CameraSN", "") + mode_9.set("ZOffset", "0.0") + mode_9.set("PixelFormat", "default") + mode_9.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_9.set("EmissionFilterPosition", "1") # mode_10 = ET.SubElement(top,'mode') # mode_10.set('ID','10') @@ -114,7 +116,7 @@ def generate_default_configuration(filename): # mode_10.set('AnalogGain','0') # mode_10.set('IlluminationSource','5') # mode_10.set('IlluminationIntensity','20') - # mode_10.set('CameraSN','') + # mode_10.set('CameraSN','') # mode_10.set('ZOffset','0.0') # mode_10.set('PixelFormat','default') # mode_10.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') @@ -131,122 +133,122 @@ def generate_default_configuration(filename): # mode_11.set('PixelFormat','default') # mode_11.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_2 = ET.SubElement(top,'mode') - mode_2.set('ID','2') - mode_2.set('Name','BF LED matrix left half') - mode_2.set('ExposureTime','16') - mode_2.set('AnalogGain','0') - mode_2.set('IlluminationSource','1') - mode_2.set('IlluminationIntensity','5') - mode_2.set('CameraSN','') - mode_2.set('ZOffset','0.0') - mode_2.set('PixelFormat','default') - mode_2.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_2.set('EmissionFilterPosition','1') - - mode_3 = ET.SubElement(top,'mode') - mode_3.set('ID','3') - mode_3.set('Name','BF LED matrix right half') - mode_3.set('ExposureTime','16') - mode_3.set('AnalogGain','0') - mode_3.set('IlluminationSource','2') - mode_3.set('IlluminationIntensity','5') - mode_3.set('CameraSN','') - mode_3.set('ZOffset','0.0') - mode_3.set('PixelFormat','default') - mode_3.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_3.set('EmissionFilterPosition','1') - - mode_12 = ET.SubElement(top,'mode') - mode_12.set('ID','12') - mode_12.set('Name','BF LED matrix top half') - mode_12.set('ExposureTime','20') - mode_12.set('AnalogGain','0') - mode_12.set('IlluminationSource','7') - mode_12.set('IlluminationIntensity','20') - mode_12.set('CameraSN','') - mode_12.set('ZOffset','0.0') - mode_12.set('PixelFormat','default') - mode_12.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_12.set('EmissionFilterPosition','1') - - mode_13 = ET.SubElement(top,'mode') - mode_13.set('ID','13') - mode_13.set('Name','BF LED matrix bottom half') - mode_13.set('ExposureTime','20') - mode_13.set('AnalogGain','0') - mode_13.set('IlluminationSource','8') - mode_13.set('IlluminationIntensity','20') - mode_13.set('CameraSN','') - mode_13.set('ZOffset','0.0') - mode_13.set('PixelFormat','default') - mode_13.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_13.set('EmissionFilterPosition','1') - - mode_14 = ET.SubElement(top,'mode') - mode_14.set('ID','1') - mode_14.set('Name','BF LED matrix full_R') - mode_14.set('ExposureTime','12') - mode_14.set('AnalogGain','0') - mode_14.set('IlluminationSource','0') - mode_14.set('IlluminationIntensity','5') - mode_14.set('CameraSN','') - mode_14.set('ZOffset','0.0') - mode_14.set('PixelFormat','default') - mode_14.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_14.set('EmissionFilterPosition','1') - - mode_15 = ET.SubElement(top,'mode') - mode_15.set('ID','1') - mode_15.set('Name','BF LED matrix full_G') - mode_15.set('ExposureTime','12') - mode_15.set('AnalogGain','0') - mode_15.set('IlluminationSource','0') - mode_15.set('IlluminationIntensity','5') - mode_15.set('CameraSN','') - mode_15.set('ZOffset','0.0') - mode_15.set('PixelFormat','default') - mode_15.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_15.set('EmissionFilterPosition','1') - - mode_16 = ET.SubElement(top,'mode') - mode_16.set('ID','1') - mode_16.set('Name','BF LED matrix full_B') - mode_16.set('ExposureTime','12') - mode_16.set('AnalogGain','0') - mode_16.set('IlluminationSource','0') - mode_16.set('IlluminationIntensity','5') - mode_16.set('CameraSN','') - mode_16.set('ZOffset','0.0') - mode_16.set('PixelFormat','default') - mode_16.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_16.set('EmissionFilterPosition','1') - - mode_21 = ET.SubElement(top,'mode') - mode_21.set('ID','21') - mode_21.set('Name','BF LED matrix full_RGB') - mode_21.set('ExposureTime','12') - mode_21.set('AnalogGain','0') - mode_21.set('IlluminationSource','0') - mode_21.set('IlluminationIntensity','5') - mode_21.set('CameraSN','') - mode_21.set('ZOffset','0.0') - mode_21.set('PixelFormat','default') - mode_21.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_21.set('EmissionFilterPosition','1') - - mode_20 = ET.SubElement(top,'mode') - mode_20.set('ID','20') - mode_20.set('Name','USB Spectrometer') - mode_20.set('ExposureTime','20') - mode_20.set('AnalogGain','0') - mode_20.set('IlluminationSource','6') - mode_20.set('IlluminationIntensity','0') - mode_20.set('CameraSN','') - mode_20.set('ZOffset','0.0') - mode_20.set('PixelFormat','default') - mode_20.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - mode_20.set('EmissionFilterPosition','1') + mode_2 = ET.SubElement(top, "mode") + mode_2.set("ID", "2") + mode_2.set("Name", "BF LED matrix left half") + mode_2.set("ExposureTime", "16") + mode_2.set("AnalogGain", "0") + mode_2.set("IlluminationSource", "1") + mode_2.set("IlluminationIntensity", "5") + mode_2.set("CameraSN", "") + mode_2.set("ZOffset", "0.0") + mode_2.set("PixelFormat", "default") + mode_2.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_2.set("EmissionFilterPosition", "1") + + mode_3 = ET.SubElement(top, "mode") + mode_3.set("ID", "3") + mode_3.set("Name", "BF LED matrix right half") + mode_3.set("ExposureTime", "16") + mode_3.set("AnalogGain", "0") + mode_3.set("IlluminationSource", "2") + mode_3.set("IlluminationIntensity", "5") + mode_3.set("CameraSN", "") + mode_3.set("ZOffset", "0.0") + mode_3.set("PixelFormat", "default") + mode_3.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_3.set("EmissionFilterPosition", "1") + + mode_12 = ET.SubElement(top, "mode") + mode_12.set("ID", "12") + mode_12.set("Name", "BF LED matrix top half") + mode_12.set("ExposureTime", "20") + mode_12.set("AnalogGain", "0") + mode_12.set("IlluminationSource", "7") + mode_12.set("IlluminationIntensity", "20") + mode_12.set("CameraSN", "") + mode_12.set("ZOffset", "0.0") + mode_12.set("PixelFormat", "default") + mode_12.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_12.set("EmissionFilterPosition", "1") + + mode_13 = ET.SubElement(top, "mode") + mode_13.set("ID", "13") + mode_13.set("Name", "BF LED matrix bottom half") + mode_13.set("ExposureTime", "20") + mode_13.set("AnalogGain", "0") + mode_13.set("IlluminationSource", "8") + mode_13.set("IlluminationIntensity", "20") + mode_13.set("CameraSN", "") + mode_13.set("ZOffset", "0.0") + mode_13.set("PixelFormat", "default") + mode_13.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_13.set("EmissionFilterPosition", "1") + + mode_14 = ET.SubElement(top, "mode") + mode_14.set("ID", "1") + mode_14.set("Name", "BF LED matrix full_R") + mode_14.set("ExposureTime", "12") + mode_14.set("AnalogGain", "0") + mode_14.set("IlluminationSource", "0") + mode_14.set("IlluminationIntensity", "5") + mode_14.set("CameraSN", "") + mode_14.set("ZOffset", "0.0") + mode_14.set("PixelFormat", "default") + mode_14.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_14.set("EmissionFilterPosition", "1") + + mode_15 = ET.SubElement(top, "mode") + mode_15.set("ID", "1") + mode_15.set("Name", "BF LED matrix full_G") + mode_15.set("ExposureTime", "12") + mode_15.set("AnalogGain", "0") + mode_15.set("IlluminationSource", "0") + mode_15.set("IlluminationIntensity", "5") + mode_15.set("CameraSN", "") + mode_15.set("ZOffset", "0.0") + mode_15.set("PixelFormat", "default") + mode_15.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_15.set("EmissionFilterPosition", "1") + + mode_16 = ET.SubElement(top, "mode") + mode_16.set("ID", "1") + mode_16.set("Name", "BF LED matrix full_B") + mode_16.set("ExposureTime", "12") + mode_16.set("AnalogGain", "0") + mode_16.set("IlluminationSource", "0") + mode_16.set("IlluminationIntensity", "5") + mode_16.set("CameraSN", "") + mode_16.set("ZOffset", "0.0") + mode_16.set("PixelFormat", "default") + mode_16.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_16.set("EmissionFilterPosition", "1") + + mode_21 = ET.SubElement(top, "mode") + mode_21.set("ID", "21") + mode_21.set("Name", "BF LED matrix full_RGB") + mode_21.set("ExposureTime", "12") + mode_21.set("AnalogGain", "0") + mode_21.set("IlluminationSource", "0") + mode_21.set("IlluminationIntensity", "5") + mode_21.set("CameraSN", "") + mode_21.set("ZOffset", "0.0") + mode_21.set("PixelFormat", "default") + mode_21.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_21.set("EmissionFilterPosition", "1") + + mode_20 = ET.SubElement(top, "mode") + mode_20.set("ID", "20") + mode_20.set("Name", "USB Spectrometer") + mode_20.set("ExposureTime", "20") + mode_20.set("AnalogGain", "0") + mode_20.set("IlluminationSource", "6") + mode_20.set("IlluminationIntensity", "0") + mode_20.set("CameraSN", "") + mode_20.set("ZOffset", "0.0") + mode_20.set("PixelFormat", "default") + mode_20.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + mode_20.set("EmissionFilterPosition", "1") tree = ET.ElementTree(top) - tree.write(filename,encoding="utf-8", xml_declaration=True, pretty_print=True) + tree.write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) diff --git a/software/control/widgets.py b/software/control/widgets.py index b95c4b390..9fdb52038 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -50,19 +50,19 @@ def closeForReal(self, event): class CollapsibleGroupBox(QGroupBox): def __init__(self, title): - super(CollapsibleGroupBox,self).__init__(title) + super(CollapsibleGroupBox, self).__init__(title) self.setCheckable(True) self.setChecked(True) self.higher_layout = QVBoxLayout() self.content = QVBoxLayout() - #self.content.setAlignment(Qt.AlignTop) + # self.content.setAlignment(Qt.AlignTop) self.content_widget = QWidget() self.content_widget.setLayout(self.content) self.higher_layout.addWidget(self.content_widget) self.setLayout(self.higher_layout) self.toggled.connect(self.toggle_content) - def toggle_content(self,state): + def toggle_content(self, state): self.content_widget.setVisible(state) @@ -72,7 +72,7 @@ def __init__(self, configManager, only_z_offset=True): self.config = configManager - self.only_z_offset=only_z_offset + self.only_z_offset = only_z_offset self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) @@ -117,19 +117,19 @@ def init_ui(self, only_z_offset=None): self.groups[str(section.id)] = group_box for option in section.__dict__.keys(): - if option.startswith('_') and option.endswith('_options'): + if option.startswith("_") and option.endswith("_options"): continue - if option == 'id': + if option == "id": continue - if only_z_offset and option != 'z_offset': + if only_z_offset and option != "z_offset": continue option_value = str(getattr(section, option)) option_name = QLabel(option) option_layout = QHBoxLayout() option_layout.addWidget(option_name) - if f'_{option}_options' in list(section.__dict__.keys()): - option_value_list = getattr(section,f'_{option}_options') - values = option_value_list.strip('[]').split(',') + if f"_{option}_options" in list(section.__dict__.keys()): + option_value_list = getattr(section, f"_{option}_options") + values = option_value_list.strip("[]").split(",") for i in range(len(values)): values[i] = values[i].strip() if option_value not in values: @@ -158,13 +158,13 @@ def save_config(self): for option in section.__dict__.keys(): if option.startswith("_") and option.endswith("_options"): continue - old_val = getattr(section,option) - if option == 'id': + old_val = getattr(section, option) + if option == "id": continue - elif option == 'camera_sn': - option_name_in_xml = 'CameraSN' + elif option == "camera_sn": + option_name_in_xml = "CameraSN" else: - option_name_in_xml = option.replace("_"," ").title().replace(" ","") + option_name_in_xml = option.replace("_", " ").title().replace(" ", "") try: widget = self.config_value_widgets[str(section.id)][option] except KeyError: @@ -178,12 +178,16 @@ def save_config(self): def save_to_file(self): self.save_config() - file_path, _ = QFileDialog.getSaveFileName(self, "Save Acquisition Config File", '', "XML Files (*.xml);;All Files (*)") + file_path, _ = QFileDialog.getSaveFileName( + self, "Save Acquisition Config File", "", "XML Files (*.xml);;All Files (*)" + ) if file_path: self.config.write_configuration(file_path) - def load_config_from_file(self,only_z_offset=None): - file_path, _ = QFileDialog.getOpenFileName(self, "Load Acquisition Config File", '', "XML Files (*.xml);;All Files (*)") + def load_config_from_file(self, only_z_offset=None): + file_path, _ = QFileDialog.getOpenFileName( + self, "Load Acquisition Config File", "", "XML Files (*.xml);;All Files (*)" + ) if file_path: self.config.config_filename = file_path self.config.configurations = [] @@ -240,15 +244,15 @@ def init_ui(self): self.groups[section] = group_box for option in self.config.options(section): - if option.startswith('_') and option.endswith('_options'): + if option.startswith("_") and option.endswith("_options"): continue option_value = self.config.get(section, option) option_name = QLabel(option) option_layout = QHBoxLayout() option_layout.addWidget(option_name) - if f'_{option}_options' in self.config.options(section): - option_value_list = self.config.get(section,f'_{option}_options') - values = option_value_list.strip('[]').split(',') + if f"_{option}_options" in self.config.options(section): + option_value_list = self.config.get(section, f"_{option}_options") + values = option_value_list.strip("[]").split(",") for i in range(len(values)): values[i] = values[i].strip() if option_value not in values: @@ -279,18 +283,18 @@ def save_config(self): self.config.set(section, option, widget.text()) else: self.config.set(section, option, widget.currentText()) - if old_val != self.config.get(section,option): - print(self.config.get(section,option)) + if old_val != self.config.get(section, option): + print(self.config.get(section, option)) def save_to_file(self): self.save_config() - file_path, _ = QFileDialog.getSaveFileName(self, "Save Config File", '', "INI Files (*.ini);;All Files (*)") + file_path, _ = QFileDialog.getSaveFileName(self, "Save Config File", "", "INI Files (*.ini);;All Files (*)") if file_path: - with open(file_path, 'w') as configfile: + with open(file_path, "w") as configfile: self.config.write(configfile) def load_config_from_file(self): - file_path, _ = QFileDialog.getOpenFileName(self, "Load Config File", '', "INI Files (*.ini);;All Files (*)") + file_path, _ = QFileDialog.getOpenFileName(self, "Load Config File", "", "INI Files (*.ini);;All Files (*)") if file_path: self.config.read(file_path) # Clear and re-initialize the UI @@ -315,7 +319,7 @@ def __init__(self, config, original_filepath, main_window): def apply_and_exit(self): self.save_config() - with open(self.original_filepath, 'w') as configfile: + with open(self.original_filepath, "w") as configfile: self.config.write(configfile) try: self.main_window.close() @@ -326,7 +330,7 @@ def apply_and_exit(self): class SpinningDiskConfocalWidget(QWidget): def __init__(self, xlight, config_manager=None): - super(SpinningDiskConfocalWidget,self).__init__() + super(SpinningDiskConfocalWidget, self).__init__() self.config_manager = config_manager @@ -367,13 +371,13 @@ def init_ui(self): emissionFilterLayout = QHBoxLayout() emissionFilterLayout.addWidget(QLabel("Emission Position")) self.dropdown_emission_filter = QComboBox(self) - self.dropdown_emission_filter.addItems([str(i+1) for i in range(8)]) + self.dropdown_emission_filter.addItems([str(i + 1) for i in range(8)]) emissionFilterLayout.addWidget(self.dropdown_emission_filter) dichroicLayout = QHBoxLayout() dichroicLayout.addWidget(QLabel("Dichroic Position")) self.dropdown_dichroic = QComboBox(self) - self.dropdown_dichroic.addItems([str(i+1) for i in range(5)]) + self.dropdown_dichroic.addItems([str(i + 1) for i in range(5)]) dichroicLayout.addWidget(self.dropdown_dichroic) illuminationIrisLayout = QHBoxLayout() @@ -398,8 +402,8 @@ def init_ui(self): filterSliderLayout = QHBoxLayout() filterSliderLayout.addWidget(QLabel("Filter Slider")) - #self.dropdown_filter_slider = QComboBox(self) - #self.dropdown_filter_slider.addItems(["0", "1", "2", "3"]) + # self.dropdown_filter_slider = QComboBox(self) + # self.dropdown_filter_slider.addItems(["0", "1", "2", "3"]) self.dropdown_filter_slider = QSlider(Qt.Horizontal) self.dropdown_filter_slider.setRange(0, 3) self.dropdown_filter_slider.setTickPosition(QSlider.TicksBelow) @@ -415,29 +419,28 @@ def init_ui(self): # row 1 if self.xlight.has_dichroic_filter_slider: - layout.addLayout(filterSliderLayout,0,0,1,2) - layout.addWidget(self.btn_toggle_motor,0,2) - layout.addWidget(self.btn_toggle_widefield,0,3) + layout.addLayout(filterSliderLayout, 0, 0, 1, 2) + layout.addWidget(self.btn_toggle_motor, 0, 2) + layout.addWidget(self.btn_toggle_widefield, 0, 3) # row 2 if self.xlight.has_dichroic_filters_wheel: - layout.addWidget(QLabel("Dichroic Filter Wheel"),1,0) - layout.addWidget(self.dropdown_dichroic,1,1) + layout.addWidget(QLabel("Dichroic Filter Wheel"), 1, 0) + layout.addWidget(self.dropdown_dichroic, 1, 1) if self.xlight.has_illumination_iris_diaphragm: - layout.addLayout(illuminationIrisLayout,1,2,1,2) + layout.addLayout(illuminationIrisLayout, 1, 2, 1, 2) # row 3 if self.xlight.has_emission_filters_wheel: - layout.addWidget(QLabel("Emission Filter Wheel"),2,0) - layout.addWidget(self.dropdown_emission_filter,2,1) + layout.addWidget(QLabel("Emission Filter Wheel"), 2, 0) + layout.addWidget(self.dropdown_emission_filter, 2, 1) if self.xlight.has_emission_iris_diaphragm: - layout.addLayout(emissionIrisLayout,2,2,1,2) + layout.addLayout(emissionIrisLayout, 2, 2, 1, 2) - layout.setColumnStretch(2,1) - layout.setColumnStretch(3,1) + layout.setColumnStretch(2, 1) + layout.setColumnStretch(3, 1) self.setLayout(layout) - def disable_all_buttons(self): self.dropdown_emission_filter.setEnabled(False) self.dropdown_dichroic.setEnabled(False) @@ -462,14 +465,14 @@ def enable_all_buttons(self): def toggle_disk_position(self): self.disable_all_buttons() - if self.disk_position_state==1: + if self.disk_position_state == 1: self.disk_position_state = self.xlight.set_disk_position(0) self.btn_toggle_widefield.setText("Switch to Confocal") else: self.disk_position_state = self.xlight.set_disk_position(1) self.btn_toggle_widefield.setText("Switch to Widefield") if self.config_manager is not None: - if self.disk_position_state ==1: + if self.disk_position_state == 1: self.config_manager.config_filename = "confocal_configurations.xml" else: self.config_manager.config_filename = "widefield_configurations.xml" @@ -600,17 +603,17 @@ def update_focusmap_display(self): self.fmap_coord_2.setText("Focus Map Point 2: (xxx,yyy,zzz)") self.fmap_coord_3.setText("Focus Map Point 3: (xxx,yyy,zzz)") try: - x,y,z = self.autofocusController.focus_map_coords[0] + x, y, z = self.autofocusController.focus_map_coords[0] self.fmap_coord_1.setText(f"Focus Map Point 1: ({x:.3f},{y:.3f},{z:.3f})") except IndexError: pass try: - x,y,z = self.autofocusController.focus_map_coords[1] + x, y, z = self.autofocusController.focus_map_coords[1] self.fmap_coord_2.setText(f"Focus Map Point 2: ({x:.3f},{y:.3f},{z:.3f})") except IndexError: pass try: - x,y,z = self.autofocusController.focus_map_coords[2] + x, y, z = self.autofocusController.focus_map_coords[2] self.fmap_coord_3.setText(f"Focus Map Point 3: ({x:.3f},{y:.3f},{z:.3f})") except IndexError: pass @@ -639,15 +642,28 @@ def add_to_focusmap(self): class CameraSettingsWidget(QFrame): - def __init__(self, camera, include_gain_exposure_time = False, include_camera_temperature_setting = False, include_camera_auto_wb_setting = False, main=None, *args, **kwargs): + def __init__( + self, + camera, + include_gain_exposure_time=False, + include_camera_temperature_setting=False, + include_camera_auto_wb_setting=False, + main=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.camera = camera - self.add_components(include_gain_exposure_time,include_camera_temperature_setting,include_camera_auto_wb_setting) + self.add_components( + include_gain_exposure_time, include_camera_temperature_setting, include_camera_auto_wb_setting + ) # set frame style self.setFrameStyle(QFrame.Panel | QFrame.Raised) - def add_components(self,include_gain_exposure_time,include_camera_temperature_setting,include_camera_auto_wb_setting): + def add_components( + self, include_gain_exposure_time, include_camera_temperature_setting, include_camera_auto_wb_setting + ): # add buttons and input fields self.entry_exposureTime = QDoubleSpinBox() @@ -665,7 +681,7 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se self.camera.set_analog_gain(0) self.dropdown_pixelFormat = QComboBox() - self.dropdown_pixelFormat.addItems(['MONO8','MONO12','MONO14','MONO16','BAYER_RG8','BAYER_RG12']) + self.dropdown_pixelFormat.addItems(["MONO8", "MONO12", "MONO14", "MONO16", "BAYER_RG8", "BAYER_RG12"]) if self.camera.pixel_format is not None: self.dropdown_pixelFormat.setCurrentText(self.camera.pixel_format) else: @@ -724,20 +740,20 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se self.camera_layout = QVBoxLayout() if include_gain_exposure_time: exposure_line = QHBoxLayout() - exposure_line.addWidget(QLabel('Exposure Time (ms)')) + exposure_line.addWidget(QLabel("Exposure Time (ms)")) exposure_line.addWidget(self.entry_exposureTime) self.camera_layout.addLayout(exposure_line) gain_line = QHBoxLayout() - gain_line.addWidget(QLabel('Analog Gain')) + gain_line.addWidget(QLabel("Analog Gain")) gain_line.addWidget(self.entry_analogGain) self.camera_layout.addLayout(gain_line) format_line = QHBoxLayout() - format_line.addWidget(QLabel('Pixel Format')) + format_line.addWidget(QLabel("Pixel Format")) format_line.addWidget(self.dropdown_pixelFormat) try: current_res = self.camera.resolution - current_res_string = "x".join([str(current_res[0]),str(current_res[1])]) + current_res_string = "x".join([str(current_res[0]), str(current_res[1])]) res_options = [f"{res[0]} x {res[1]}" for res in self.camera.res_list] self.dropdown_res = QComboBox() self.dropdown_res.addItems(res_options) @@ -755,9 +771,9 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se if include_camera_temperature_setting: temp_line = QHBoxLayout() - temp_line.addWidget(QLabel('Set Temperature (C)')) + temp_line.addWidget(QLabel("Set Temperature (C)")) temp_line.addWidget(self.entry_temperature) - temp_line.addWidget(QLabel('Actual Temperature (C)')) + temp_line.addWidget(QLabel("Actual Temperature (C)")) temp_line.addWidget(self.label_temperature_measured) try: self.entry_temperature.valueChanged.connect(self.set_temperature) @@ -767,22 +783,22 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se self.camera_layout.addLayout(temp_line) roi_line = QHBoxLayout() - roi_line.addWidget(QLabel('Height')) + roi_line.addWidget(QLabel("Height")) roi_line.addWidget(self.entry_ROI_height) roi_line.addStretch() - roi_line.addWidget(QLabel('Y-offset')) + roi_line.addWidget(QLabel("Y-offset")) roi_line.addWidget(self.entry_ROI_offset_y) roi_line.addStretch() - roi_line.addWidget(QLabel('Width')) + roi_line.addWidget(QLabel("Width")) roi_line.addWidget(self.entry_ROI_width) roi_line.addStretch() - roi_line.addWidget(QLabel('X-offset')) + roi_line.addWidget(QLabel("X-offset")) roi_line.addWidget(self.entry_ROI_offset_x) self.camera_layout.addLayout(roi_line) if DISPLAY_TOUPCAMER_BLACKLEVEL_SETTINGS is True: blacklevel_line = QHBoxLayout() - blacklevel_line.addWidget(QLabel('Black Level')) + blacklevel_line.addWidget(QLabel("Black Level")) self.label_blackLevel = QSpinBox() self.label_blackLevel.setMinimum(0) @@ -803,7 +819,7 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se if is_color is True: # auto white balance - self.btn_auto_wb = QPushButton('Auto White Balance') + self.btn_auto_wb = QPushButton("Auto White Balance") self.btn_auto_wb.setCheckable(True) self.btn_auto_wb.setChecked(False) self.btn_auto_wb.clicked.connect(self.toggle_auto_wb) @@ -813,45 +829,60 @@ def add_components(self,include_gain_exposure_time,include_camera_temperature_se self.setLayout(self.camera_layout) - def toggle_auto_wb(self,pressed): + def toggle_auto_wb(self, pressed): # 0: OFF 1:CONTINUOUS 2:ONCE if pressed: self.camera.set_balance_white_auto(1) else: self.camera.set_balance_white_auto(0) - def set_exposure_time(self,exposure_time): + def set_exposure_time(self, exposure_time): self.entry_exposureTime.setValue(exposure_time) - def set_analog_gain(self,analog_gain): + def set_analog_gain(self, analog_gain): self.entry_analogGain.setValue(analog_gain) def set_Width(self): - width = int(self.entry_ROI_width.value()//8)*8 + width = int(self.entry_ROI_width.value() // 8) * 8 self.entry_ROI_width.blockSignals(True) self.entry_ROI_width.setValue(width) self.entry_ROI_width.blockSignals(False) - offset_x = (self.camera.WidthMax - self.entry_ROI_width.value())/2 - offset_x = int(offset_x//8)*8 + offset_x = (self.camera.WidthMax - self.entry_ROI_width.value()) / 2 + offset_x = int(offset_x // 8) * 8 self.entry_ROI_offset_x.blockSignals(True) self.entry_ROI_offset_x.setValue(offset_x) self.entry_ROI_offset_x.blockSignals(False) - self.camera.set_ROI(self.entry_ROI_offset_x.value(),self.entry_ROI_offset_y.value(),self.entry_ROI_width.value(),self.entry_ROI_height.value()) + self.camera.set_ROI( + self.entry_ROI_offset_x.value(), + self.entry_ROI_offset_y.value(), + self.entry_ROI_width.value(), + self.entry_ROI_height.value(), + ) def set_Height(self): - height = int(self.entry_ROI_height.value()//8)*8 + height = int(self.entry_ROI_height.value() // 8) * 8 self.entry_ROI_height.blockSignals(True) self.entry_ROI_height.setValue(height) self.entry_ROI_height.blockSignals(False) - offset_y = (self.camera.HeightMax - self.entry_ROI_height.value())/2 - offset_y = int(offset_y//8)*8 + offset_y = (self.camera.HeightMax - self.entry_ROI_height.value()) / 2 + offset_y = int(offset_y // 8) * 8 self.entry_ROI_offset_y.blockSignals(True) self.entry_ROI_offset_y.setValue(offset_y) self.entry_ROI_offset_y.blockSignals(False) - self.camera.set_ROI(self.entry_ROI_offset_x.value(),self.entry_ROI_offset_y.value(),self.entry_ROI_width.value(),self.entry_ROI_height.value()) + self.camera.set_ROI( + self.entry_ROI_offset_x.value(), + self.entry_ROI_offset_y.value(), + self.entry_ROI_width.value(), + self.entry_ROI_height.value(), + ) def set_ROI_offset(self): - self.camera.set_ROI(self.entry_ROI_offset_x.value(),self.entry_ROI_offset_y.value(),self.entry_ROI_width.value(),self.entry_ROI_height.value()) + self.camera.set_ROI( + self.entry_ROI_offset_x.value(), + self.entry_ROI_offset_y.value(), + self.entry_ROI_width.value(), + self.entry_ROI_height.value(), + ) def set_temperature(self): try: @@ -859,14 +890,14 @@ def set_temperature(self): except AttributeError: pass - def update_measured_temperature(self,temperature): + def update_measured_temperature(self, temperature): self.label_temperature_measured.setNum(temperature) def change_full_res(self, index): res_strings = self.dropdown_res.currentText().split("x") res_x = int(res_strings[0]) res_y = int(res_strings[1]) - self.camera.set_resolution(res_x,res_y) + self.camera.set_resolution(res_x, res_y) self.entry_ROI_offset_x.blockSignals(True) self.entry_ROI_offset_y.blockSignals(True) self.entry_ROI_height.blockSignals(True) @@ -878,10 +909,10 @@ def change_full_res(self, index): self.entry_ROI_offset_x.setMaximum(self.camera.WidthMax) self.entry_ROI_offset_y.setMaximum(self.camera.HeightMax) - self.entry_ROI_offset_x.setValue(int(8*self.camera.OffsetX//8)) - self.entry_ROI_offset_y.setValue(int(8*self.camera.OffsetY//8)) - self.entry_ROI_height.setValue(int(8*self.camera.Height//8)) - self.entry_ROI_width.setValue(int(8*self.camera.Width//8)) + self.entry_ROI_offset_x.setValue(int(8 * self.camera.OffsetX // 8)) + self.entry_ROI_offset_y.setValue(int(8 * self.camera.OffsetY // 8)) + self.entry_ROI_height.setValue(int(8 * self.camera.Height // 8)) + self.entry_ROI_width.setValue(int(8 * self.camera.Width // 8)) self.entry_ROI_offset_x.blockSignals(False) self.entry_ROI_offset_y.blockSignals(False) @@ -903,7 +934,20 @@ class LiveControlWidget(QFrame): signal_live_configuration = Signal(object) signal_start_live = Signal() - def __init__(self, streamHandler, liveController, configurationManager=None, show_trigger_options=True, show_display_options=False, show_autolevel = False, autolevel=False, stretch=True, main=None, *args, **kwargs): + def __init__( + self, + streamHandler, + liveController, + configurationManager=None, + show_trigger_options=True, + show_display_options=False, + show_autolevel=False, + autolevel=False, + stretch=True, + main=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.liveController = liveController self.streamHandler = streamHandler @@ -917,17 +961,17 @@ def __init__(self, streamHandler, liveController, configurationManager=None, sho # note that this references the object in self.configurationManager.configurations self.currentConfiguration = self.configurationManager.configurations[0] - self.add_components(show_trigger_options,show_display_options,show_autolevel,autolevel,stretch) + self.add_components(show_trigger_options, show_display_options, show_autolevel, autolevel, stretch) self.setFrameStyle(QFrame.Panel | QFrame.Raised) self.update_microscope_mode_by_name(self.currentConfiguration.name) - self.is_switching_mode = False # flag used to prevent from settings being set by twice - from both mode change slot and value change slot; another way is to use blockSignals(True) + self.is_switching_mode = False # flag used to prevent from settings being set by twice - from both mode change slot and value change slot; another way is to use blockSignals(True) - def add_components(self,show_trigger_options,show_display_options,show_autolevel,autolevel,stretch): + def add_components(self, show_trigger_options, show_display_options, show_autolevel, autolevel, stretch): # line 0: trigger mode self.triggerMode = None self.dropdown_triggerManu = QComboBox() - self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE,TriggerMode.HARDWARE,TriggerMode.CONTINUOUS]) + self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE, TriggerMode.HARDWARE, TriggerMode.CONTINUOUS]) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.dropdown_triggerManu.setSizePolicy(sizePolicy) @@ -958,7 +1002,7 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.entry_exposureTime.setMinimum(self.liveController.camera.EXPOSURE_TIME_MS_MIN) self.entry_exposureTime.setMaximum(self.liveController.camera.EXPOSURE_TIME_MS_MAX) self.entry_exposureTime.setSingleStep(1) - self.entry_exposureTime.setSuffix(' ms') + self.entry_exposureTime.setSuffix(" ms") self.entry_exposureTime.setValue(0) self.entry_exposureTime.setSizePolicy(sizePolicy) @@ -982,7 +1026,7 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.entry_illuminationIntensity.setMinimum(0) self.entry_illuminationIntensity.setMaximum(100) self.entry_illuminationIntensity.setSingleStep(1) - self.entry_illuminationIntensity.setSuffix('%') + self.entry_illuminationIntensity.setSuffix("%") self.entry_illuminationIntensity.setValue(100) # line 4: display fps and resolution scaling @@ -1011,7 +1055,7 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.label_resolutionScaling.valueChanged.connect(lambda v: self.slider_resolutionScaling.setValue(round(v))) # autolevel - self.btn_autolevel = QPushButton('Autolevel') + self.btn_autolevel = QPushButton("Autolevel") self.btn_autolevel.setCheckable(True) self.btn_autolevel.setChecked(autolevel) @@ -1019,10 +1063,7 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.entry_illuminationIntensity.setMinimumWidth(self.btn_live.sizeHint().width()) self.btn_autolevel.setMinimumWidth(self.btn_autolevel.sizeHint().width()) - max_width = max( - self.btn_autolevel.minimumWidth(), - self.entry_illuminationIntensity.minimumWidth() - ) + max_width = max(self.btn_autolevel.minimumWidth(), self.entry_illuminationIntensity.minimumWidth()) # Set the fixed width for all three widgets self.entry_illuminationIntensity.setFixedWidth(max_width) @@ -1039,20 +1080,22 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.entry_exposureTime.valueChanged.connect(self.update_config_exposure_time) self.entry_analogGain.valueChanged.connect(self.update_config_analog_gain) self.entry_illuminationIntensity.valueChanged.connect(self.update_config_illumination_intensity) - self.entry_illuminationIntensity.valueChanged.connect(lambda x: self.slider_illuminationIntensity.setValue(int(x))) + self.entry_illuminationIntensity.valueChanged.connect( + lambda x: self.slider_illuminationIntensity.setValue(int(x)) + ) self.slider_illuminationIntensity.valueChanged.connect(self.entry_illuminationIntensity.setValue) self.btn_autolevel.toggled.connect(self.signal_autoLevelSetting.emit) # layout grid_line1 = QHBoxLayout() - grid_line1.addWidget(QLabel('Live Configuration')) + grid_line1.addWidget(QLabel("Live Configuration")) grid_line1.addWidget(self.dropdown_modeSelection, 2) grid_line1.addWidget(self.btn_live, 1) grid_line2 = QHBoxLayout() - grid_line2.addWidget(QLabel('Exposure Time')) + grid_line2.addWidget(QLabel("Exposure Time")) grid_line2.addWidget(self.entry_exposureTime) - gain_label = QLabel(' Analog Gain') + gain_label = QLabel(" Analog Gain") gain_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) grid_line2.addWidget(gain_label) grid_line2.addWidget(self.entry_analogGain) @@ -1060,26 +1103,26 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel grid_line2.addWidget(self.btn_autolevel) grid_line4 = QHBoxLayout() - grid_line4.addWidget(QLabel('Illumination')) + grid_line4.addWidget(QLabel("Illumination")) grid_line4.addWidget(self.slider_illuminationIntensity) grid_line4.addWidget(self.entry_illuminationIntensity) grid_line0 = QHBoxLayout() if show_trigger_options: - grid_line0.addWidget(QLabel('Trigger Mode')) + grid_line0.addWidget(QLabel("Trigger Mode")) grid_line0.addWidget(self.dropdown_triggerManu) - grid_line0.addWidget(QLabel('Trigger FPS')) + grid_line0.addWidget(QLabel("Trigger FPS")) grid_line0.addWidget(self.entry_triggerFPS) grid_line05 = QHBoxLayout() show_dislpay_fps = False if show_display_options: - resolution_label = QLabel('Display Resolution') + resolution_label = QLabel("Display Resolution") resolution_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) grid_line05.addWidget(resolution_label) grid_line05.addWidget(self.slider_resolutionScaling) if show_dislpay_fps: - grid_line05.addWidget(QLabel('Display FPS')) + grid_line05.addWidget(QLabel("Display FPS")) grid_line05.addWidget(self.entry_displayFPS) else: grid_line05.addWidget(self.label_resolutionScaling) @@ -1096,27 +1139,33 @@ def add_components(self,show_trigger_options,show_display_options,show_autolevel self.grid.addStretch() self.setLayout(self.grid) - - def toggle_live(self,pressed): + def toggle_live(self, pressed): if pressed: self.liveController.start_live() - self.btn_live.setText('Stop Live') + self.btn_live.setText("Stop Live") self.signal_start_live.emit() else: self.liveController.stop_live() - self.btn_live.setText('Start Live') + self.btn_live.setText("Start Live") - def toggle_autolevel(self,autolevel_on): + def toggle_autolevel(self, autolevel_on): self.btn_autolevel.setChecked(autolevel_on) def update_camera_settings(self): self.signal_newAnalogGain.emit(self.entry_analogGain.value()) self.signal_newExposureTime.emit(self.entry_exposureTime.value()) - def update_microscope_mode_by_name(self,current_microscope_mode_name): + def update_microscope_mode_by_name(self, current_microscope_mode_name): self.is_switching_mode = True # identify the mode selected (note that this references the object in self.configurationManager.configurations) - self.currentConfiguration = next((config for config in self.configurationManager.configurations if config.name == current_microscope_mode_name), None) + self.currentConfiguration = next( + ( + config + for config in self.configurationManager.configurations + if config.name == current_microscope_mode_name + ), + None, + ) self.signal_live_configuration.emit(self.currentConfiguration) # update the microscope to the current configuration self.liveController.set_microscope_mode(self.currentConfiguration) @@ -1129,29 +1178,33 @@ def update_microscope_mode_by_name(self,current_microscope_mode_name): def update_trigger_mode(self): self.liveController.set_trigger_mode(self.dropdown_triggerManu.currentText()) - def update_config_exposure_time(self,new_value): + def update_config_exposure_time(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.exposure_time = new_value - self.configurationManager.update_configuration(self.currentConfiguration.id,'ExposureTime',new_value) + self.configurationManager.update_configuration(self.currentConfiguration.id, "ExposureTime", new_value) self.signal_newExposureTime.emit(new_value) - def update_config_analog_gain(self,new_value): + def update_config_analog_gain(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.analog_gain = new_value - self.configurationManager.update_configuration(self.currentConfiguration.id,'AnalogGain',new_value) + self.configurationManager.update_configuration(self.currentConfiguration.id, "AnalogGain", new_value) self.signal_newAnalogGain.emit(new_value) - def update_config_illumination_intensity(self,new_value): + def update_config_illumination_intensity(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.illumination_intensity = new_value - self.configurationManager.update_configuration(self.currentConfiguration.id,'IlluminationIntensity',new_value) - self.liveController.set_illumination(self.currentConfiguration.illumination_source, self.currentConfiguration.illumination_intensity) + self.configurationManager.update_configuration( + self.currentConfiguration.id, "IlluminationIntensity", new_value + ) + self.liveController.set_illumination( + self.currentConfiguration.illumination_source, self.currentConfiguration.illumination_intensity + ) - def set_microscope_mode(self,config): + def set_microscope_mode(self, config): # self.liveController.set_microscope_mode(config) self.dropdown_modeSelection.setCurrentText(config.name) - def set_trigger_mode(self,trigger_mode): + def set_trigger_mode(self, trigger_mode): self.dropdown_triggerManu.setCurrentText(trigger_mode) self.liveController.set_trigger_mode(self.dropdown_triggerManu.currentText()) @@ -1173,7 +1226,7 @@ def add_components(self): self.spinBox.setRange(0.0, OBJECTIVE_PIEZO_RANGE_UM) self.spinBox.setDecimals(2) self.spinBox.setSingleStep(0.01) - self.spinBox.setSuffix(' μm') + self.spinBox.setSuffix(" μm") # Row 3: Home Button self.home_btn = QPushButton(f" Set to {OBJECTIVE_PIEZO_HOME_UM} μm ", self) @@ -1189,7 +1242,7 @@ def add_components(self): self.increment_spinBox.setDecimals(2) self.increment_spinBox.setSingleStep(1) self.increment_spinBox.setValue(1.00) - self.increment_spinBox.setSuffix(' μm') + self.increment_spinBox.setSuffix(" μm") self.move_up_btn = QPushButton("Move Up", self) self.move_down_btn = QPushButton("Move Down", self) @@ -1261,20 +1314,20 @@ def update_displacement_um_display(self, displacement): class RecordingWidget(QFrame): def __init__(self, streamHandler, imageSaver, main=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.imageSaver = imageSaver # for saving path control + self.imageSaver = imageSaver # for saving path control self.streamHandler = streamHandler 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 = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + 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("Choose a base saving directory") self.lineEdit_savingDir.setText(DEFAULT_SAVING_PATH) self.imageSaver.set_base_path(DEFAULT_SAVING_PATH) @@ -1290,7 +1343,7 @@ def add_components(self): self.entry_timeLimit = QSpinBox() self.entry_timeLimit.setMinimum(-1) - self.entry_timeLimit.setMaximum(60*60*24*30) + self.entry_timeLimit.setMaximum(60 * 60 * 24 * 30) self.entry_timeLimit.setSingleStep(1) self.entry_timeLimit.setValue(-1) @@ -1300,19 +1353,19 @@ def add_components(self): self.btn_record.setDefault(False) grid_line1 = QGridLayout() - grid_line1.addWidget(QLabel('Saving Path')) - grid_line1.addWidget(self.lineEdit_savingDir, 0,1) - grid_line1.addWidget(self.btn_setSavingDir, 0,2) + grid_line1.addWidget(QLabel("Saving Path")) + grid_line1.addWidget(self.lineEdit_savingDir, 0, 1) + grid_line1.addWidget(self.btn_setSavingDir, 0, 2) grid_line2 = QGridLayout() - grid_line2.addWidget(QLabel('Experiment ID'), 0,0) - grid_line2.addWidget(self.lineEdit_experimentID,0,1) + grid_line2.addWidget(QLabel("Experiment ID"), 0, 0) + grid_line2.addWidget(self.lineEdit_experimentID, 0, 1) grid_line3 = QGridLayout() - grid_line3.addWidget(QLabel('Saving FPS'), 0,0) - grid_line3.addWidget(self.entry_saveFPS, 0,1) - grid_line3.addWidget(QLabel('Time Limit (s)'), 0,2) - grid_line3.addWidget(self.entry_timeLimit, 0,3) + grid_line3.addWidget(QLabel("Saving FPS"), 0, 0) + grid_line3.addWidget(self.entry_saveFPS, 0, 1) + grid_line3.addWidget(QLabel("Time Limit (s)"), 0, 2) + grid_line3.addWidget(self.entry_timeLimit, 0, 3) self.grid = QVBoxLayout() self.grid.addLayout(grid_line1) @@ -1338,7 +1391,7 @@ def set_saving_dir(self): self.lineEdit_savingDir.setText(save_dir_base) self.base_path_is_set = True - def toggle_recording(self,pressed): + def toggle_recording(self, pressed): if self.base_path_is_set == False: self.btn_record.setChecked(False) msg = QMessageBox() @@ -1364,7 +1417,15 @@ def stop_recording(self): class NavigationWidget(QFrame): - def __init__(self, stage: AbstractStage, slidePositionController=None, main=None, widget_configuration = 'full', *args, **kwargs): + def __init__( + self, + stage: AbstractStage, + slidePositionController=None, + main=None, + widget_configuration="full", + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.log = squid.logging.get_logger(self.__class__.__name__) self.stage = stage @@ -1387,7 +1448,7 @@ def _update_position(self): self.label_Zpos.setNum(pos.z_mm * 1000) def add_components(self): - x_label = QLabel('X :') + x_label = QLabel("X :") x_label.setFixedWidth(20) self.label_Xpos = QLabel() self.label_Xpos.setNum(0) @@ -1398,24 +1459,24 @@ def add_components(self): self.entry_dX.setSingleStep(0.2) self.entry_dX.setValue(0) self.entry_dX.setDecimals(3) - self.entry_dX.setSuffix(' mm') + self.entry_dX.setSuffix(" mm") self.entry_dX.setKeyboardTracking(False) - self.btn_moveX_forward = QPushButton('Forward') + self.btn_moveX_forward = QPushButton("Forward") self.btn_moveX_forward.setDefault(False) - self.btn_moveX_backward = QPushButton('Backward') + self.btn_moveX_backward = QPushButton("Backward") self.btn_moveX_backward.setDefault(False) - self.btn_home_X = QPushButton('Home X') + self.btn_home_X = QPushButton("Home X") self.btn_home_X.setDefault(False) self.btn_home_X.setEnabled(HOMING_ENABLED_X) - self.btn_zero_X = QPushButton('Zero X') + self.btn_zero_X = QPushButton("Zero X") self.btn_zero_X.setDefault(False) - self.checkbox_clickToMove = QCheckBox('Click to Move') + self.checkbox_clickToMove = QCheckBox("Click to Move") self.checkbox_clickToMove.setChecked(False) self.checkbox_clickToMove.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) - y_label = QLabel('Y :') + y_label = QLabel("Y :") y_label.setFixedWidth(20) self.label_Ypos = QLabel() self.label_Ypos.setNum(0) @@ -1426,21 +1487,21 @@ def add_components(self): self.entry_dY.setSingleStep(0.2) self.entry_dY.setValue(0) self.entry_dY.setDecimals(3) - self.entry_dY.setSuffix(' mm') + self.entry_dY.setSuffix(" mm") self.entry_dY.setKeyboardTracking(False) - self.btn_moveY_forward = QPushButton('Forward') + self.btn_moveY_forward = QPushButton("Forward") self.btn_moveY_forward.setDefault(False) - self.btn_moveY_backward = QPushButton('Backward') + self.btn_moveY_backward = QPushButton("Backward") self.btn_moveY_backward.setDefault(False) - self.btn_home_Y = QPushButton('Home Y') + self.btn_home_Y = QPushButton("Home Y") self.btn_home_Y.setDefault(False) self.btn_home_Y.setEnabled(HOMING_ENABLED_Y) - self.btn_zero_Y = QPushButton('Zero Y') + self.btn_zero_Y = QPushButton("Zero Y") self.btn_zero_Y.setDefault(False) - z_label = QLabel('Z :') + z_label = QLabel("Z :") z_label.setFixedWidth(20) self.label_Zpos = QLabel() self.label_Zpos.setNum(0) @@ -1451,44 +1512,44 @@ def add_components(self): self.entry_dZ.setSingleStep(0.2) self.entry_dZ.setValue(0) self.entry_dZ.setDecimals(3) - self.entry_dZ.setSuffix(' μm') + self.entry_dZ.setSuffix(" μm") self.entry_dZ.setKeyboardTracking(False) - self.btn_moveZ_forward = QPushButton('Forward') + self.btn_moveZ_forward = QPushButton("Forward") self.btn_moveZ_forward.setDefault(False) - self.btn_moveZ_backward = QPushButton('Backward') + self.btn_moveZ_backward = QPushButton("Backward") self.btn_moveZ_backward.setDefault(False) - self.btn_home_Z = QPushButton('Home Z') + self.btn_home_Z = QPushButton("Home Z") self.btn_home_Z.setDefault(False) self.btn_home_Z.setEnabled(HOMING_ENABLED_Z) - self.btn_zero_Z = QPushButton('Zero Z') + self.btn_zero_Z = QPushButton("Zero Z") self.btn_zero_Z.setDefault(False) - self.btn_load_slide = QPushButton('Move To Loading Position') + self.btn_load_slide = QPushButton("Move To Loading Position") self.btn_load_slide.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) grid_line0 = QGridLayout() - grid_line0.addWidget(x_label, 0,0) - grid_line0.addWidget(self.label_Xpos, 0,1) - grid_line0.addWidget(self.entry_dX, 0,2) - grid_line0.addWidget(self.btn_moveX_forward, 0,3) - grid_line0.addWidget(self.btn_moveX_backward, 0,4) - - grid_line0.addWidget(y_label, 1,0) - grid_line0.addWidget(self.label_Ypos, 1,1) - grid_line0.addWidget(self.entry_dY, 1,2) - grid_line0.addWidget(self.btn_moveY_forward, 1,3) - grid_line0.addWidget(self.btn_moveY_backward, 1,4) - - grid_line0.addWidget(z_label, 2,0) - grid_line0.addWidget(self.label_Zpos, 2,1) - grid_line0.addWidget(self.entry_dZ, 2,2) - grid_line0.addWidget(self.btn_moveZ_forward, 2,3) - grid_line0.addWidget(self.btn_moveZ_backward, 2,4) + grid_line0.addWidget(x_label, 0, 0) + grid_line0.addWidget(self.label_Xpos, 0, 1) + grid_line0.addWidget(self.entry_dX, 0, 2) + grid_line0.addWidget(self.btn_moveX_forward, 0, 3) + grid_line0.addWidget(self.btn_moveX_backward, 0, 4) + + grid_line0.addWidget(y_label, 1, 0) + grid_line0.addWidget(self.label_Ypos, 1, 1) + grid_line0.addWidget(self.entry_dY, 1, 2) + grid_line0.addWidget(self.btn_moveY_forward, 1, 3) + grid_line0.addWidget(self.btn_moveY_backward, 1, 4) + + grid_line0.addWidget(z_label, 2, 0) + grid_line0.addWidget(self.label_Zpos, 2, 1) + grid_line0.addWidget(self.entry_dZ, 2, 2) + grid_line0.addWidget(self.btn_moveZ_forward, 2, 3) + grid_line0.addWidget(self.btn_moveZ_backward, 2, 4) grid_line3 = QHBoxLayout() - if self.widget_configuration == 'full': + if self.widget_configuration == "full": grid_line3.addWidget(self.btn_home_X) grid_line3.addWidget(self.btn_home_Y) grid_line3.addWidget(self.btn_home_Z) @@ -1556,28 +1617,35 @@ def setEnabled_all(self, enabled): def move_x_forward(self): self.stage.move_x(self.entry_dX.value()) + def move_x_backward(self): self.stage.move_x(-self.entry_dX.value()) + def move_y_forward(self): self.stage.move_y(self.entry_dY.value()) + def move_y_backward(self): self.stage.move_y(-self.entry_dY.value()) + def move_z_forward(self): - self.stage.move_z(self.entry_dZ.value()/1000) + self.stage.move_z(self.entry_dZ.value() / 1000) + def move_z_backward(self): - self.stage.move_z(-self.entry_dZ.value()/1000) + self.stage.move_z(-self.entry_dZ.value() / 1000) - def set_deltaX(self,value): + def set_deltaX(self, value): mm_per_ustep = 1.0 / self.stage.get_config().X_AXIS.convert_real_units_to_ustep(1.0) - deltaX = round(value/mm_per_ustep)*mm_per_ustep + deltaX = round(value / mm_per_ustep) * mm_per_ustep self.entry_dX.setValue(deltaX) - def set_deltaY(self,value): + + def set_deltaY(self, value): mm_per_ustep = 1.0 / self.stage.get_config().Y_AXIS.convert_real_units_to_ustep(1.0) - deltaY = round(value/mm_per_ustep)*mm_per_ustep + deltaY = round(value / mm_per_ustep) * mm_per_ustep self.entry_dY.setValue(deltaY) - def set_deltaZ(self,value): + + def set_deltaZ(self, value): mm_per_ustep = 1.0 / self.stage.get_config().Z_AXIS.convert_real_units_to_ustep(1.0) - deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 + deltaZ = round(value / 1000 / mm_per_ustep) * mm_per_ustep * 1000 self.entry_dZ.setValue(deltaZ) def home_x(self): @@ -1626,9 +1694,9 @@ def zero_z(self): self.stage.zero(x=False, y=False, z=True, theta=False) def slot_slide_loading_position_reached(self): - self.slide_position = 'loading' + self.slide_position = "loading" self.btn_load_slide.setStyleSheet("background-color: #C2FFC2") - self.btn_load_slide.setText('Move to Scanning Position') + self.btn_load_slide.setText("Move to Scanning Position") self.btn_moveX_forward.setEnabled(False) self.btn_moveX_backward.setEnabled(False) self.btn_moveY_forward.setEnabled(False) @@ -1638,9 +1706,9 @@ def slot_slide_loading_position_reached(self): self.btn_load_slide.setEnabled(True) def slot_slide_scanning_position_reached(self): - self.slide_position = 'scanning' + self.slide_position = "scanning" self.btn_load_slide.setStyleSheet("background-color: #C2C2FF") - self.btn_load_slide.setText('Move to Loading Position') + self.btn_load_slide.setText("Move to Loading Position") self.btn_moveX_forward.setEnabled(True) self.btn_moveX_backward.setEnabled(True) self.btn_moveY_forward.setEnabled(True) @@ -1650,7 +1718,7 @@ def slot_slide_scanning_position_reached(self): self.btn_load_slide.setEnabled(True) def switch_position(self): - if self.slide_position != 'loading': + if self.slide_position != "loading": self.slidePositionController.move_to_slide_loading_position() else: self.slidePositionController.move_to_slide_scanning_position() @@ -1658,12 +1726,16 @@ def switch_position(self): def replace_slide_controller(self, slidePositionController): self.slidePositionController = slidePositionController - self.slidePositionController.signal_slide_loading_position_reached.connect(self.slot_slide_loading_position_reached) - self.slidePositionController.signal_slide_scanning_position_reached.connect(self.slot_slide_scanning_position_reached) + self.slidePositionController.signal_slide_loading_position_reached.connect( + self.slot_slide_loading_position_reached + ) + self.slidePositionController.signal_slide_scanning_position_reached.connect( + self.slot_slide_scanning_position_reached + ) class DACControWidget(QFrame): - def __init__(self, microcontroller ,*args, **kwargs): + def __init__(self, microcontroller, *args, **kwargs): super().__init__(*args, **kwargs) self.microcontroller = microcontroller self.add_components() @@ -1708,22 +1780,22 @@ def add_components(self): # layout grid_line1 = QHBoxLayout() - grid_line1.addWidget(QLabel('DAC0')) + grid_line1.addWidget(QLabel("DAC0")) grid_line1.addWidget(self.slider_DAC0) grid_line1.addWidget(self.entry_DAC0) - grid_line1.addWidget(QLabel('DAC1')) + grid_line1.addWidget(QLabel("DAC1")) grid_line1.addWidget(self.slider_DAC1) grid_line1.addWidget(self.entry_DAC1) self.grid = QGridLayout() - self.grid.addLayout(grid_line1,1,0) + self.grid.addLayout(grid_line1, 1, 0) self.setLayout(self.grid) - def set_DAC0(self,value): - self.microcontroller.analog_write_onboard_DAC(0,round(value*65535/100)) + def set_DAC0(self, value): + self.microcontroller.analog_write_onboard_DAC(0, round(value * 65535 / 100)) - def set_DAC1(self,value): - self.microcontroller.analog_write_onboard_DAC(1,round(value*65535/100)) + def set_DAC1(self, value): + self.microcontroller.analog_write_onboard_DAC(1, round(value * 65535 / 100)) class AutoFocusWidget(QFrame): @@ -1743,7 +1815,7 @@ def add_components(self): self.entry_delta.setMaximum(20) self.entry_delta.setSingleStep(0.2) self.entry_delta.setDecimals(3) - self.entry_delta.setSuffix(' μm') + self.entry_delta.setSuffix(" μm") self.entry_delta.setValue(1.524) self.entry_delta.setKeyboardTracking(False) self.entry_delta.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -1760,12 +1832,12 @@ def add_components(self): self.entry_N.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.autofocusController.set_N(10) - self.btn_autofocus = QPushButton('Autofocus') + self.btn_autofocus = QPushButton("Autofocus") self.btn_autofocus.setDefault(False) self.btn_autofocus.setCheckable(True) self.btn_autofocus.setChecked(False) - self.btn_autolevel = QPushButton('Autolevel') + self.btn_autolevel = QPushButton("Autolevel") self.btn_autolevel.setCheckable(True) self.btn_autolevel.setChecked(False) self.btn_autolevel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -1773,10 +1845,10 @@ def add_components(self): # layout self.grid = QVBoxLayout() grid_line0 = QHBoxLayout() - grid_line0.addWidget(QLabel('\u0394 Z')) + grid_line0.addWidget(QLabel("\u0394 Z")) grid_line0.addWidget(self.entry_delta) grid_line0.addSpacing(20) - grid_line0.addWidget(QLabel('# of Z-Planes')) + grid_line0.addWidget(QLabel("# of Z-Planes")) grid_line0.addWidget(self.entry_N) grid_line0.addSpacing(20) grid_line0.addWidget(self.btn_autolevel) @@ -1786,15 +1858,15 @@ def add_components(self): self.setLayout(self.grid) # connections - self.btn_autofocus.toggled.connect(lambda : self.autofocusController.autofocus(False)) + self.btn_autofocus.toggled.connect(lambda: self.autofocusController.autofocus(False)) self.btn_autolevel.toggled.connect(self.signal_autoLevelSetting.emit) self.entry_delta.valueChanged.connect(self.set_deltaZ) self.entry_N.valueChanged.connect(self.autofocusController.set_N) self.autofocusController.autofocusFinished.connect(self.autofocus_is_finished) - def set_deltaZ(self,value): + def set_deltaZ(self, value): mm_per_ustep = 1.0 / self.stage.get_config().Z_AXIS.convert_real_units_to_ustep(1.0) - deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 + deltaZ = round(value / 1000 / mm_per_ustep) * mm_per_ustep * 1000 self.log.debug(f"{deltaZ=}") self.entry_delta.setValue(deltaZ) @@ -1819,9 +1891,9 @@ def add_components(self): self.checkBox = QCheckBox("Disable filter wheel movement on changing Microscope Configuration", self) layout = QGridLayout() - layout.addWidget(QLabel('Filter wheel position:'), 0,0) - layout.addWidget(self.comboBox, 0,1) - layout.addWidget(self.checkBox, 2,0) + layout.addWidget(QLabel("Filter wheel position:"), 0, 0) + layout.addWidget(self.comboBox, 0, 1) + layout.addWidget(self.checkBox, 2, 0) self.setLayout(layout) @@ -1831,7 +1903,7 @@ def add_components(self): def on_selection_change(self, index): # The 'index' parameter is the new index of the combo box if index >= 0 and index <= 7: # Making sure the index is valid - self.filterController.set_emission_filter(index+1) + self.filterController.set_emission_filter(index + 1) def disable_movement_by_switching_channels(self, state): if state: @@ -1858,30 +1930,41 @@ def initUI(self): def display_stats(self, stats): print("displaying parasite stats") - locale.setlocale(locale.LC_ALL, '') + locale.setlocale(locale.LC_ALL, "") self.table_widget.setRowCount(len(stats)) row = 0 for key, value in stats.items(): key_item = QTableWidgetItem(str(key)) value_item = None try: - value_item = QTableWidgetItem(f'{value:n}') + value_item = QTableWidgetItem(f"{value:n}") except: value_item = QTableWidgetItem(str(value)) - self.table_widget.setItem(row,0,key_item) - self.table_widget.setItem(row,1,value_item) - row+=1 + self.table_widget.setItem(row, 0, key_item) + self.table_widget.setItem(row, 1, value_item) + row += 1 class FlexibleMultiPointWidget(QFrame): - 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, stage: AbstractStage, navigationViewer, multipointController, objectiveStore, configurationManager, scanCoordinates, focusMapWidget, *args, **kwargs): + 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, + stage: AbstractStage, + navigationViewer, + multipointController, + objectiveStore, + configurationManager, + scanCoordinates, + focusMapWidget, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self._log = squid.logging.get_logger(self.__class__.__name__) self.last_used_locations = None @@ -1895,7 +1978,7 @@ def __init__(self, stage: AbstractStage, navigationViewer, multipointController, self.focusMapWidget = focusMapWidget self.base_path_is_set = False self.location_list = np.empty((0, 3), dtype=float) - self.location_ids = np.empty((0,), dtype=' 0 else 0 + self.eta_seconds = ( + remaining_fovs / fov_per_second + (Nt - 1 - self.current_time_point) * dt if fov_per_second > 0 else 0 + ) self.update_eta_display() # Start or restart the timer @@ -2480,20 +2575,29 @@ def update_fov_positions(self): region_id = self.location_ids[i] if self.use_overlap: self.scanCoordinates.add_flexible_region( - region_id, x, y, z, - self.entry_NX.value(), self.entry_NY.value(), - overlap_percent=self.entry_overlap.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + overlap_percent=self.entry_overlap.value(), + ) else: self.scanCoordinates.add_flexible_region_with_step_size( - region_id, x, y, z, - self.entry_NX.value(), self.entry_NY.value(), - self.entry_deltaX.value(), self.entry_deltaY.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + self.entry_deltaX.value(), + self.entry_deltaY.value(), + ) - def set_deltaZ(self,value): + def set_deltaZ(self, value): mm_per_ustep = 1.0 / self.stage.get_config().Z_AXIS.convert_real_units_to_ustep(1.0) - deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 + deltaZ = round(value / 1000 / mm_per_ustep) * mm_per_ustep * 1000 self.entry_deltaZ.setValue(deltaZ) self.multipointController.set_deltaZ(deltaZ) @@ -2511,14 +2615,14 @@ def emit_selected_channels(self): def display_stitcher_widget(self, checked): self.signal_stitcher_widget.emit(checked) - def toggle_acquisition(self,pressed): + 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 not self.list_configurations.selectedItems(): # no channel selected + 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") @@ -2527,7 +2631,7 @@ def toggle_acquisition(self,pressed): if pressed: # @@@ to do: add a widgetManger to enable and disable widget # @@@ to do: emit signal to widgetManager to disable other widgets - self.is_current_acquisition_widget = True # keep track of what widget started the acquisition + self.is_current_acquisition_widget = True # keep track of what widget started the acquisition # add the current location to the location list if the list is empty if len(self.location_list) == 0: @@ -2561,7 +2665,9 @@ def toggle_acquisition(self,pressed): 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.set_selected_configurations( + (item.text() for item in self.list_configurations.selectedItems()) + ) self.multipointController.start_new_experiment(self.lineEdit_experimentID.text()) # emit signals @@ -2586,21 +2692,29 @@ def load_last_used_locations(self): z = row[2] name = row_ind[0] if not np.any(np.all(self.location_list[:, :2] == [x, y], axis=1)): - location_str = 'x:' + str(round(x,3)) + 'mm y:' + str(round(y,3)) + 'mm z:' + str(round(1000*z,1)) + 'μm' + location_str = ( + "x:" + str(round(x, 3)) + "mm y:" + str(round(y, 3)) + "mm z:" + str(round(1000 * z, 1)) + "μm" + ) self.dropdown_location_list.addItem(location_str) - self.location_list = np.vstack((self.location_list, [[x,y,z]])) + self.location_list = np.vstack((self.location_list, [[x, y, z]])) self.location_ids = np.append(self.location_ids, name) self.table_location_list.insertRow(self.table_location_list.rowCount()) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,0, QTableWidgetItem(str(round(x,3)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,1, QTableWidgetItem(str(round(y,3)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,2, QTableWidgetItem(str(round(z*1000,1)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,3, QTableWidgetItem(name)) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 0, QTableWidgetItem(str(round(x, 3))) + ) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 1, QTableWidgetItem(str(round(y, 3))) + ) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 2, QTableWidgetItem(str(round(z * 1000, 1))) + ) + self.table_location_list.setItem(self.table_location_list.rowCount() - 1, 3, QTableWidgetItem(name)) index = self.dropdown_location_list.count() - 1 self.dropdown_location_list.setCurrentIndex(index) print(self.location_list) else: print("Duplicate values not added based on x and y.") - #to-do: update z coordinate + # to-do: update z coordinate def add_location(self): # Get raw positions without rounding @@ -2608,10 +2722,10 @@ def add_location(self): x = pos.x_mm y = pos.y_mm z = pos.z_mm - region_id = f'R{len(self.location_ids)}' + region_id = 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)): + if not np.any(np.all(self.location_list[:, :2] == [round(x, 3), round(y, 3)], axis=1)): # Block signals to prevent triggering cell_was_changed self.table_location_list.blockSignals(True) self.dropdown_location_list.blockSignals(True) @@ -2625,24 +2739,33 @@ def add_location(self): self.dropdown_location_list.addItem(location_str) 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, 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(region_id)) # Store actual values in region coordinates if self.use_overlap: self.scanCoordinates.add_flexible_region( - region_id, x, y, z, - self.entry_NX.value(),self.entry_NY.value(), - overlap_percent=self.entry_overlap.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + overlap_percent=self.entry_overlap.value(), + ) else: self.scanCoordinates.add_flexible_region_with_step_size( - region_id, x, y, z, - self.entry_NX.value(),self.entry_NY.value(), - self.entry_deltaX.value(),self.entry_deltaY.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + self.entry_deltaX.value(), + self.entry_deltaY.value(), + ) # Set the current index to the newly added location self.dropdown_location_list.setCurrentIndex(len(self.location_ids) - 1) @@ -2682,12 +2805,14 @@ def remove_location(self): # Reindex remaining regions and update UI for i in range(index, len(self.location_ids)): old_id = self.location_ids[i] - new_id = f'R{i}' + new_id = f"R{i}" self.location_ids[i] = new_id # Update dictionaries self.scanCoordinates.region_centers[new_id] = self.scanCoordinates.region_centers.pop(old_id, None) - self.scanCoordinates.region_fov_coordinates[new_id] = self.scanCoordinates.region_fov_coordinates.pop(old_id, []) + self.scanCoordinates.region_fov_coordinates[new_id] = self.scanCoordinates.region_fov_coordinates.pop( + old_id, [] + ) # Update UI with new ID and coordinates x, y, z = self.location_list[i] @@ -2733,9 +2858,9 @@ def next(self): index = (index + 1) % num_regions self.dropdown_location_list.setCurrentIndex(index) - x = self.location_list[index,0] - y = self.location_list[index,1] - z = self.location_list[index,2] + x = self.location_list[index, 0] + y = self.location_list[index, 1] + z = self.location_list[index, 2] self.stage.move_x_to(x) self.stage.move_y_to(y) self.stage.move_z_to(z) @@ -2744,16 +2869,16 @@ def previous(self): index = self.dropdown_location_list.currentIndex() index = max(index - 1, 0) self.dropdown_location_list.setCurrentIndex(index) - x = self.location_list[index,0] - y = self.location_list[index,1] - z = self.location_list[index,2] + x = self.location_list[index, 0] + y = self.location_list[index, 1] + z = self.location_list[index, 2] self.stage.move_x_to(x) self.stage.move_y_to(y) self.stage.move_z_to(z) def clear(self): self.location_list = np.empty((0, 3), dtype=float) - self.location_ids = np.empty((0,), dtype=' index: - self.table_location_list.setItem(index, 2, QTableWidgetItem(str(round(1000*z_mm,1)))) + self.table_location_list.setItem(index, 2, QTableWidgetItem(str(round(1000 * z_mm, 1)))) self.table_location_list.blockSignals(False) self.dropdown_location_list.blockSignals(False) def export_location_list(self): - file_path, _ = QFileDialog.getSaveFileName(self, "Export Location List", '', "CSV Files (*.csv);;All Files (*)") + file_path, _ = QFileDialog.getSaveFileName(self, "Export Location List", "", "CSV Files (*.csv);;All Files (*)") if file_path: - location_list_df = pd.DataFrame(self.location_list,columns=['x (mm)','y (mm)', 'z (um)']) - location_list_df['ID'] = self.location_ids - location_list_df['i'] = 0 - location_list_df['j'] = 0 - location_list_df['k'] = 0 - location_list_df.to_csv(file_path,index=False,header=True) + location_list_df = pd.DataFrame(self.location_list, columns=["x (mm)", "y (mm)", "z (um)"]) + location_list_df["ID"] = self.location_ids + location_list_df["i"] = 0 + location_list_df["j"] = 0 + location_list_df["k"] = 0 + location_list_df.to_csv(file_path, index=False, header=True) def import_location_list(self): - file_path, _ = QFileDialog.getOpenFileName(self, "Import Location List", '', "CSV Files (*.csv);;All Files (*)") + file_path, _ = QFileDialog.getOpenFileName(self, "Import Location List", "", "CSV Files (*.csv);;All Files (*)") if file_path: location_list_df = pd.read_csv(file_path) location_list_df_relevant = None try: - location_list_df_relevant = location_list_df[['x (mm)', 'y (mm)', 'z (um)']] + location_list_df_relevant = location_list_df[["x (mm)", "y (mm)", "z (um)"]] except KeyError: self._log.error("Improperly formatted location list being imported") return - if 'ID' in location_list_df.columns: - location_list_df_relevant['ID'] = location_list_df['ID'].astype(str) + if "ID" in location_list_df.columns: + location_list_df_relevant["ID"] = location_list_df["ID"].astype(str) else: - location_list_df_relevant['ID'] = 'None' + location_list_df_relevant["ID"] = "None" self.clear_only_location_list() for index, row in location_list_df_relevant.iterrows(): - x = row['x (mm)'] - y = row['y (mm)'] - z = row['z (um)'] - region_id = row['ID'] + x = row["x (mm)"] + y = row["y (mm)"] + z = row["z (um)"] + region_id = row["ID"] if not np.any(np.all(self.location_list[:, :2] == [x, y], axis=1)): - location_str = 'x:' + str(round(x,3)) + 'mm y:' + str(round(y,3)) + 'mm z:' + str(round(1000*z,1)) + 'μm' + location_str = ( + "x:" + + str(round(x, 3)) + + "mm y:" + + str(round(y, 3)) + + "mm z:" + + str(round(1000 * z, 1)) + + "μm" + ) self.dropdown_location_list.addItem(location_str) index = self.dropdown_location_list.count() - 1 self.dropdown_location_list.setCurrentIndex(index) - self.location_list = np.vstack((self.location_list, [[x,y,z]])) + self.location_list = np.vstack((self.location_list, [[x, y, z]])) self.location_ids = np.append(self.location_ids, region_id) self.table_location_list.insertRow(self.table_location_list.rowCount()) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,0, QTableWidgetItem(str(round(x,3)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,1, QTableWidgetItem(str(round(y,3)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,2, QTableWidgetItem(str(round(1000*z,1)))) - self.table_location_list.setItem(self.table_location_list.rowCount()-1,3, QTableWidgetItem(region_id)) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 0, QTableWidgetItem(str(round(x, 3))) + ) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 1, QTableWidgetItem(str(round(y, 3))) + ) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 2, QTableWidgetItem(str(round(1000 * z, 1))) + ) + self.table_location_list.setItem( + self.table_location_list.rowCount() - 1, 3, QTableWidgetItem(region_id) + ) if self.use_overlap: self.scanCoordinates.add_flexible_region( - region_id, x, y, z, - self.entry_NX.value(),self.entry_NY.value(), - overlap_percent=self.entry_overlap.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + overlap_percent=self.entry_overlap.value(), + ) else: self.scanCoordinates.add_flexible_region_with_step_size( - region_id, x, y, z, - self.entry_NX.value(),self.entry_NY.value(), - self.entry_deltaX.value(),self.entry_deltaY.value() - ) + region_id, + x, + y, + z, + self.entry_NX.value(), + self.entry_NY.value(), + self.entry_deltaX.value(), + self.entry_deltaY.value(), + ) else: self._log.warning("Duplicate values not added based on x and y.") self._log.debug(self.location_list) @@ -2922,7 +3091,7 @@ def acquisition_is_finished(self): self.setEnabled_all(True) self.is_current_acquisition_widget = False - def setEnabled_all(self,enabled,exclude_btn_startAcquisition=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) @@ -2959,12 +3128,24 @@ class WellplateMultiPointWidget(QFrame): signal_acquisition_started = Signal(bool) signal_acquisition_channels = Signal(list) - signal_acquisition_shape = Signal(int, float) # acquisition Nz, dz - signal_stitcher_z_levels = Signal(int) # live Nz - signal_stitcher_widget = Signal(bool) # start stitching - signal_manual_shape_mode = Signal(bool) # enable manual shape layer on mosaic display - - def __init__(self, stage: AbstractStage, navigationViewer, multipointController, objectiveStore, configurationManager, scanCoordinates, focusMapWidget, napariMosaicWidget=None, *args, **kwargs): + signal_acquisition_shape = Signal(int, float) # acquisition Nz, dz + signal_stitcher_z_levels = Signal(int) # live Nz + signal_stitcher_widget = Signal(bool) # start stitching + signal_manual_shape_mode = Signal(bool) # enable manual shape layer on mosaic display + + def __init__( + self, + stage: AbstractStage, + navigationViewer, + multipointController, + objectiveStore, + configurationManager, + scanCoordinates, + focusMapWidget, + napariMosaicWidget=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.stage = stage self.navigationViewer = navigationViewer @@ -2997,9 +3178,9 @@ def add_components(self): self.entry_well_coverage.setSuffix("%") btn_width = self.entry_well_coverage.sizeHint().width() - self.btn_setSavingDir = QPushButton('Browse') + self.btn_setSavingDir = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + self.btn_setSavingDir.setIcon(QIcon("icon/folder.png")) self.btn_setSavingDir.setFixedWidth(btn_width) self.lineEdit_savingDir = QLineEdit() @@ -3028,9 +3209,9 @@ def add_components(self): self.entry_minZ.setSingleStep(1) # Step by 1 μm self.entry_minZ.setValue(self.stage.get_pos().z_mm * 1000) # Set to minimum self.entry_minZ.setSuffix(" μm") - #self.entry_minZ.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + # self.entry_minZ.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.set_minZ_button = QPushButton('Set') + self.set_minZ_button = QPushButton("Set") self.set_minZ_button.clicked.connect(self.set_z_min) self.entry_maxZ = QDoubleSpinBox() @@ -3039,9 +3220,9 @@ def add_components(self): self.entry_maxZ.setSingleStep(1) # Step by 1 μm self.entry_maxZ.setValue(self.stage.get_pos().z_mm * 1000) # Set to maximum self.entry_maxZ.setSuffix(" μm") - #self.entry_maxZ.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + # self.entry_maxZ.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.set_maxZ_button = QPushButton('Set') + self.set_maxZ_button = QPushButton("Set") self.set_maxZ_button.clicked.connect(self.set_z_max) self.entry_deltaZ = QDoubleSpinBox() @@ -3050,7 +3231,7 @@ def add_components(self): self.entry_deltaZ.setSingleStep(0.2) self.entry_deltaZ.setValue(Acquisition.DZ) self.entry_deltaZ.setDecimals(3) - #self.entry_deltaZ.setEnabled(False) + # self.entry_deltaZ.setEnabled(False) self.entry_deltaZ.setSuffix(" μm") self.entry_deltaZ.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -3063,7 +3244,7 @@ def add_components(self): self.entry_dt = QDoubleSpinBox() self.entry_dt.setMinimum(0) - self.entry_dt.setMaximum(24*3600) + self.entry_dt.setMaximum(24 * 3600) self.entry_dt.setSingleStep(1) self.entry_dt.setValue(0) self.entry_dt.setSuffix(" s") @@ -3076,7 +3257,7 @@ def add_components(self): self.entry_Nt.setValue(1) self.combobox_z_stack = QComboBox() - self.combobox_z_stack.addItems(['From Bottom (Z-min)', 'From Center', 'From Top (Z-max)']) + self.combobox_z_stack.addItems(["From Bottom (Z-min)", "From Center", "From Top (Z-max)"]) self.combobox_z_stack.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.list_configurations = QListWidget() @@ -3087,45 +3268,45 @@ def add_components(self): # Add a combo box for shape selection self.combobox_shape = QComboBox() if self.performance_mode: - self.combobox_shape.addItems(['Square', 'Circle']) + self.combobox_shape.addItems(["Square", "Circle"]) else: - self.combobox_shape.addItems(['Square', 'Circle', 'Manual']) + self.combobox_shape.addItems(["Square", "Circle", "Manual"]) self.combobox_shape.model().item(2).setEnabled(False) self.combobox_shape.setFixedWidth(btn_width) - #self.combobox_shape.currentTextChanged.connect(self.on_shape_changed) + # self.combobox_shape.currentTextChanged.connect(self.on_shape_changed) - self.checkbox_genAFMap = QCheckBox('Generate Focus Map') + self.checkbox_genAFMap = QCheckBox("Generate Focus Map") self.checkbox_genAFMap.setChecked(False) - self.checkbox_useFocusMap = QCheckBox('Use Focus Map') + self.checkbox_useFocusMap = QCheckBox("Use Focus Map") self.checkbox_useFocusMap.setChecked(False) - self.checkbox_withAutofocus = QCheckBox('Contrast AF') + 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_withReflectionAutofocus = QCheckBox('Reflection AF') + self.checkbox_withReflectionAutofocus = QCheckBox("Reflection AF") self.checkbox_withReflectionAutofocus.setChecked(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) self.multipointController.set_reflection_af_flag(MULTIPOINT_REFLECTION_AUTOFOCUS_ENABLE_BY_DEFAULT) - self.checkbox_usePiezo = QCheckBox('Piezo Z-Stack') + self.checkbox_usePiezo = QCheckBox("Piezo Z-Stack") self.checkbox_usePiezo.setChecked(MULTIPOINT_USE_PIEZO_FOR_ZSTACKS) - self.checkbox_set_z_range = QCheckBox('Set Z-range') + self.checkbox_set_z_range = QCheckBox("Set Z-range") self.checkbox_set_z_range.toggled.connect(self.toggle_z_range_controls) - self.checkbox_stitchOutput = QCheckBox('Stitch Scans') + self.checkbox_stitchOutput = QCheckBox("Stitch Scans") self.checkbox_stitchOutput.setChecked(False) - self.btn_startAcquisition = QPushButton('Start\n Acquisition ') + self.btn_startAcquisition = QPushButton("Start\n Acquisition ") self.btn_startAcquisition.setStyleSheet("background-color: #C2C2FF") self.btn_startAcquisition.setCheckable(True) self.btn_startAcquisition.setChecked(False) - #self.btn_startAcquisition.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + # self.btn_startAcquisition.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.progress_label = QLabel('Region -/-') + self.progress_label = QLabel("Region -/-") self.progress_bar = QProgressBar() - self.eta_label = QLabel('--:--:--') + self.eta_label = QLabel("--:--:--") self.progress_bar.setVisible(False) self.progress_label.setVisible(False) self.eta_label.setVisible(False) @@ -3137,28 +3318,28 @@ def add_components(self): # Saving Path saving_path_layout = QHBoxLayout() - saving_path_layout.addWidget(QLabel('Saving Path')) + saving_path_layout.addWidget(QLabel("Saving Path")) saving_path_layout.addWidget(self.lineEdit_savingDir) saving_path_layout.addWidget(self.btn_setSavingDir) main_layout.addLayout(saving_path_layout) # Experiment ID and Scan Shape row_1_layout = QHBoxLayout() - row_1_layout.addWidget(QLabel('Experiment ID')) + row_1_layout.addWidget(QLabel("Experiment ID")) row_1_layout.addWidget(self.lineEdit_experimentID) - row_1_layout.addWidget(QLabel('Well Shape')) + row_1_layout.addWidget(QLabel("Well Shape")) row_1_layout.addWidget(self.combobox_shape) main_layout.addLayout(row_1_layout) # Well Coverage, Scan Size, and Overlap row_4_layout = QHBoxLayout() - row_4_layout.addWidget(QLabel('Size')) + row_4_layout.addWidget(QLabel("Size")) row_4_layout.addWidget(self.entry_scan_size) - #row_4_layout.addStretch(1) - row_4_layout.addWidget(QLabel('FOV Overlap')) + # row_4_layout.addStretch(1) + row_4_layout.addWidget(QLabel("FOV Overlap")) row_4_layout.addWidget(self.entry_overlap) - #row_4_layout.addStretch(1) - row_4_layout.addWidget(QLabel('Well Coverage')) + # row_4_layout.addStretch(1) + row_4_layout.addWidget(QLabel("Well Coverage")) row_4_layout.addWidget(self.entry_well_coverage) main_layout.addLayout(row_4_layout) @@ -3166,33 +3347,33 @@ def add_components(self): # dz and Nz dz_layout = QHBoxLayout() - dz_layout.addWidget(QLabel('dz')) + dz_layout.addWidget(QLabel("dz")) dz_layout.addWidget(self.entry_deltaZ) - dz_layout.addWidget(QLabel('Nz')) + dz_layout.addWidget(QLabel("Nz")) dz_layout.addWidget(self.entry_NZ) grid.addLayout(dz_layout, 0, 0) - # dt and Nt + # dt and Nt dt_layout = QHBoxLayout() - dt_layout.addWidget(QLabel('dt')) + dt_layout.addWidget(QLabel("dt")) dt_layout.addWidget(self.entry_dt) - dt_layout.addWidget(QLabel('Nt')) + dt_layout.addWidget(QLabel("Nt")) dt_layout.addWidget(self.entry_Nt) grid.addLayout(dt_layout, 0, 2) # Z-min self.z_min_layout = QHBoxLayout() self.z_min_layout.addWidget(self.set_minZ_button) - min_label = QLabel('Z-min') + 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) grid.addLayout(self.z_min_layout, 1, 0) - # Z-max + # Z-max self.z_max_layout = QHBoxLayout() self.z_max_layout.addWidget(self.set_maxZ_button) - max_label = QLabel('Z-max') + 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) @@ -3269,7 +3450,7 @@ def add_components(self): if not self.performance_mode: self.napariMosaicWidget.signal_layers_initialized.connect(self.enable_manual_ROI) self.entry_NZ.valueChanged.connect(self.signal_stitcher_z_levels.emit) - #self.combobox_z_stack.currentIndexChanged.connect(self.signal_z_stacking.emit) + # self.combobox_z_stack.currentIndexChanged.connect(self.signal_z_stacking.emit) def enable_manual_ROI(self, enable): self.combobox_shape.model().item(2).setEnabled(enable) @@ -3286,13 +3467,19 @@ def update_region_progress(self, current_fov, num_fovs): dt = self.entry_dt.value() # Calculate total processed FOVs and total FOVs - processed_fovs = (self.current_region - 1) * num_fovs + current_fov + self.current_time_point * self.num_regions * num_fovs + processed_fovs = ( + (self.current_region - 1) * num_fovs + + current_fov + + self.current_time_point * self.num_regions * num_fovs + ) total_fovs = self.num_regions * num_fovs * Nt remaining_fovs = total_fovs - processed_fovs # Calculate ETA fov_per_second = processed_fovs / elapsed_time - self.eta_seconds = remaining_fovs / fov_per_second + (Nt - 1 - self.current_time_point) * dt if fov_per_second > 0 else 0 + self.eta_seconds = ( + remaining_fovs / fov_per_second + (Nt - 1 - self.current_time_point) * dt if fov_per_second > 0 else 0 + ) self.update_eta_display() # Start or restart the timer @@ -3390,8 +3577,8 @@ def set_default_scan_size(self): self.set_default_shape() - if 'glass slide' in self.navigationViewer.sample: - self.entry_scan_size.setValue(1.0) # init to 1mm when switching to 'glass slide' + if "glass slide" in self.navigationViewer.sample: + self.entry_scan_size.setValue(1.0) # init to 1mm when switching to 'glass slide' self.entry_scan_size.setEnabled(True) self.entry_well_coverage.setEnabled(False) else: @@ -3407,21 +3594,21 @@ def set_default_scan_size(self): self.entry_scan_size.blockSignals(False) def set_default_shape(self): - if self.scanCoordinates.format in ['384 well plate', '1536 well plate']: - self.combobox_shape.setCurrentText('Square') + if self.scanCoordinates.format in ["384 well plate", "1536 well plate"]: + self.combobox_shape.setCurrentText("Square") elif self.scanCoordinates.format != 0: - self.combobox_shape.setCurrentText('Circle') + self.combobox_shape.setCurrentText("Circle") def get_effective_well_size(self): well_size = self.scanCoordinates.well_size_mm - if self.combobox_shape.currentText() == 'Circle': + if self.combobox_shape.currentText() == "Circle": fov_size_mm = (self.objectiveStore.get_pixel_size() / 1000) * Acquisition.CROP_WIDTH return well_size + fov_size_mm * (1 + math.sqrt(2)) return well_size def reset_coordinates(self): shape = self.combobox_shape.currentText() - if shape == 'Manual': + if shape == "Manual": self.signal_manual_shape_mode.emit(True) else: self.signal_manual_shape_mode.emit(False) @@ -3443,25 +3630,27 @@ def update_manual_shape(self, shapes_data_mm): def convert_pixel_to_mm(self, pixel_coords): # Convert pixel coordinates to millimeter coordinates mm_coords = pixel_coords * self.napariMosaicWidget.viewer_pixel_size_mm - mm_coords += np.array([self.napariMosaicWidget.top_left_coordinate[1], self.napariMosaicWidget.top_left_coordinate[0]]) + mm_coords += np.array( + [self.napariMosaicWidget.top_left_coordinate[1], self.napariMosaicWidget.top_left_coordinate[0]] + ) return mm_coords def update_coverage_from_scan_size(self): - if 'glass slide' not in self.navigationViewer.sample: + if "glass slide" not in self.navigationViewer.sample: effective_well_size = self.get_effective_well_size() scan_size = self.entry_scan_size.value() coverage = round((scan_size / effective_well_size) * 100, 2) self.entry_well_coverage.blockSignals(True) self.entry_well_coverage.setValue(coverage) self.entry_well_coverage.blockSignals(False) - print('COVERAGE', coverage) + print("COVERAGE", coverage) def update_scan_size_from_coverage(self): effective_well_size = self.get_effective_well_size() coverage = self.entry_well_coverage.value() scan_size = round((coverage / 100) * effective_well_size, 3) self.entry_scan_size.setValue(scan_size) - print('SIZE', scan_size) + print("SIZE", scan_size) def update_dz(self): z_min = self.entry_minZ.value() @@ -3503,8 +3692,8 @@ def init_z(self, z_pos_mm=None): self.entry_maxZ.blockSignals(True) # set entry range values bith to current z pos - self.entry_minZ.setValue(z_pos_mm*1000) - self.entry_maxZ.setValue(z_pos_mm*1000) + self.entry_minZ.setValue(z_pos_mm * 1000) + self.entry_maxZ.setValue(z_pos_mm * 1000) print("init z-level wellplate:", self.entry_minZ.value()) # reallow updates from entry sinals (signal enforces min <= max when we update either entry) @@ -3512,16 +3701,16 @@ def init_z(self, z_pos_mm=None): self.entry_maxZ.blockSignals(False) def update_coordinates(self): - if hasattr(self.parent, 'recordTabWidget') and self.parent.recordTabWidget.currentWidget() != self: + if hasattr(self.parent, "recordTabWidget") and self.parent.recordTabWidget.currentWidget() != self: return scan_size_mm = self.entry_scan_size.value() overlap_percent = self.entry_overlap.value() shape = self.combobox_shape.currentText() - if shape == 'Manual': + if shape == "Manual": self.scanCoordinates.set_manual_coordinates(self.shapes_mm, overlap_percent) - elif 'glass slide' in self.navigationViewer.sample: + elif "glass slide" in self.navigationViewer.sample: pos = self.stage.get_pos() self.scanCoordinates.set_live_scan_coordinates(pos.x_mm, pos.y_mm) else: @@ -3541,7 +3730,7 @@ def update_well_coordinates(self, selected): self.scanCoordinates.clear_regions() def update_live_coordinates(self, x, y): - if hasattr(self.parent, 'recordTabWidget') and self.parent.recordTabWidget.currentWidget() != self: + if hasattr(self.parent, "recordTabWidget") and self.parent.recordTabWidget.currentWidget() != self: return scan_size_mm = self.entry_scan_size.value() overlap_percent = self.entry_overlap.value() @@ -3574,7 +3763,7 @@ def toggle_acquisition(self, pressed): x = pos.x_mm y = pos.y_mm z = pos.z_mm - self.scanCoordinates.add_region('current', x, y, scan_size_mm, overlap_percent, shape) + self.scanCoordinates.add_region("current", x, y, scan_size_mm, overlap_percent, shape) # Calculate total number of positions for signal emission # not needed ever total_positions = sum(len(coords) for coords in self.scanCoordinates.region_fov_coordinates.values()) @@ -3615,7 +3804,9 @@ def toggle_acquisition(self, pressed): self.multipointController.set_use_piezo(self.checkbox_usePiezo.isChecked()) self.multipointController.set_af_flag(self.checkbox_withAutofocus.isChecked()) self.multipointController.set_reflection_af_flag(self.checkbox_withReflectionAutofocus.isChecked()) - self.multipointController.set_selected_configurations([item.text() for item in self.list_configurations.selectedItems()]) + self.multipointController.set_selected_configurations( + [item.text() for item in self.list_configurations.selectedItems()] + ) self.multipointController.start_new_experiment(self.lineEdit_experimentID.text()) # Emit signals @@ -3642,13 +3833,15 @@ def acquisition_is_finished(self): def setEnabled_all(self, enabled): for widget in self.findChildren(QWidget): - if (widget != self.btn_startAcquisition and - widget != self.progress_bar and - widget != self.progress_label and - widget != self.eta_label): + if ( + widget != self.btn_startAcquisition + and widget != self.progress_bar + and widget != self.progress_label + and widget != self.eta_label + ): widget.setEnabled(enabled) - if self.scanCoordinates.format == 'glass slide': + if self.scanCoordinates.format == "glass slide": self.entry_well_coverage.setEnabled(False) def disable_the_start_aquisition_button(self): @@ -3666,7 +3859,7 @@ def set_saving_dir(self): def set_deltaZ(self, value): mm_per_ustep = 1.0 / self.stage.get_config().Z_AXIS.convert_real_units_to_ustep(1.0) - deltaZ = round(value/1000/mm_per_ustep)*mm_per_ustep*1000 + deltaZ = round(value / 1000 / mm_per_ustep) * mm_per_ustep * 1000 self.entry_deltaZ.setValue(deltaZ) self.multipointController.set_deltaZ(deltaZ) @@ -3693,12 +3886,12 @@ def __init__(self, stage: AbstractStage, navigationViewer, scanCoordinates, focu # Store focus points in widget self.focus_points = [] # list of (x,y,z) tuples - self.enabled = False # toggled when focus map enabled for next acquisition + self.enabled = False # toggled when focus map enabled for next acquisition self.setup_ui() self.make_connections() self.setEnabled(False) - self.add_margin = False # margin for focus grid makes it smaller, but will avoid points at the borders + self.add_margin = False # margin for focus grid makes it smaller, but will avoid points at the borders def setup_ui(self): """Create and arrange UI components""" @@ -3727,22 +3920,22 @@ def setup_ui(self): # Surface fitting controls settings_layout = QHBoxLayout() - settings_layout.addWidget(QLabel('Focus Grid:')) + settings_layout.addWidget(QLabel("Focus Grid:")) self.rows_spin = QSpinBox() self.rows_spin.setRange(2, 10) self.rows_spin.setValue(4) settings_layout.addWidget(self.rows_spin) - settings_layout.addWidget(QLabel('×')) + settings_layout.addWidget(QLabel("×")) self.cols_spin = QSpinBox() self.cols_spin.setRange(2, 10) self.cols_spin.setValue(4) settings_layout.addWidget(self.cols_spin) settings_layout.addStretch() - settings_layout.addWidget(QLabel('Fit Method:')) + settings_layout.addWidget(QLabel("Fit Method:")) self.fit_method_combo = QComboBox() - self.fit_method_combo.addItems(['spline', 'rbf']) + self.fit_method_combo.addItems(["spline", "rbf"]) settings_layout.addWidget(self.fit_method_combo) - settings_layout.addWidget(QLabel('Smoothing:')) + settings_layout.addWidget(QLabel("Smoothing:")) self.smoothing_spin = QDoubleSpinBox() self.smoothing_spin.setRange(0.01, 1.0) self.smoothing_spin.setValue(0.1) @@ -3783,9 +3976,11 @@ def update_point_list(self): rows = self.rows_spin.value() cols = self.cols_spin.value() for idx, (x, y, z) in enumerate(self.focus_points): - point_text = f'x:' + str(round(x,3)) + 'mm y:' + str(round(y,3)) + 'mm z:' + str(round(1000*z,2)) + 'μm' + point_text = ( + f"x:" + str(round(x, 3)) + "mm y:" + str(round(y, 3)) + "mm z:" + str(round(1000 * z, 2)) + "μm" + ) self.point_combo.addItem(point_text) - self.point_combo.setCurrentIndex(max(0,min(curr_focus_point, len(self.focus_points) - 1))) + self.point_combo.setCurrentIndex(max(0, min(curr_focus_point, len(self.focus_points) - 1))) # self.point_combo.blockSignals(False) def edit_current_point(self): @@ -3813,7 +4008,9 @@ def edit_current_point(self): y_spin.setSuffix(" mm") z_spin = QDoubleSpinBox() - z_spin.setRange(SOFTWARE_POS_LIMIT.Z_NEGATIVE * 1000, SOFTWARE_POS_LIMIT.Z_POSITIVE * 1000) # Convert mm limits to μm + z_spin.setRange( + SOFTWARE_POS_LIMIT.Z_NEGATIVE * 1000, SOFTWARE_POS_LIMIT.Z_POSITIVE * 1000 + ) # Convert mm limits to μm z_spin.setDecimals(2) z_spin.setValue(z * 1000) # Convert mm to μm z_spin.setSuffix(" μm") @@ -3857,8 +4054,8 @@ def generate_grid(self, rows=4, cols=4): if not bounds: return - x_min, x_max = bounds['x'] - y_min, y_max = bounds['y'] + x_min, x_max = bounds["x"] + y_min, y_max = bounds["y"] if self.add_margin: x_step = (x_max - x_min) / (cols) if cols > 1 else 0 y_step = (y_max - y_min) / (rows) if rows > 1 else 0 @@ -3995,7 +4192,7 @@ def initUI(self): self.rowLayout1.addStretch() # Output format dropdown - self.outputFormatLabel = QLabel('Output Format', self) + self.outputFormatLabel = QLabel("Output Format", self) self.outputFormatCombo = QComboBox(self) self.outputFormatCombo.addItem("OME-ZARR") self.outputFormatCombo.addItem("OME-TIFF") @@ -4067,7 +4264,7 @@ def updateRegistrationZLevels(self, Nz): self.registrationZCombo.setMaximum(Nz - 1) def gettingFlatfields(self): - self.statusLabel.setText('Status: Calculating Flatfields') + self.statusLabel.setText("Status: Calculating Flatfields") self.viewOutputButton.setVisible(False) self.viewOutputButton.setStyleSheet("") self.progressBar.setValue(0) @@ -4075,7 +4272,7 @@ def gettingFlatfields(self): self.progressBar.setVisible(True) def startingStitching(self): - self.statusLabel.setText('Status: Stitching Scans') + self.statusLabel.setText("Status: Stitching Scans") self.viewOutputButton.setVisible(False) self.progressBar.setValue(0) self.statusLabel.setVisible(True) @@ -4088,9 +4285,9 @@ def updateProgressBar(self, value, total): def startingSaving(self, stitch_complete=False): if stitch_complete: - self.statusLabel.setText('Status: Saving Stitched Acquisition') + self.statusLabel.setText("Status: Saving Stitched Acquisition") else: - self.statusLabel.setText('Status: Saving Stitched Region') + self.statusLabel.setText("Status: Saving Stitched Region") self.statusLabel.setVisible(True) self.progressBar.setRange(0, 0) # indeterminate mode. self.progressBar.setVisible(True) @@ -4115,11 +4312,11 @@ def finishedSaving(self, output_path, dtype): 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 "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']: + for color in ["R", "G", "B"]: if color in parts or "full_" + color in parts: return color return None @@ -4127,10 +4324,12 @@ def extractWavelength(self, name): 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']) + 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 updateContrastLimits(self, channel, min_val, max_val): self.contrastManager.update_limits(channel, min_val, max_val) @@ -4139,16 +4338,18 @@ def viewOutputNapari(self): try: napari_viewer = napari.Viewer() if ".ome.zarr" in self.output_path: - napari_viewer.open(self.output_path, plugin='napari-ome-zarr') + napari_viewer.open(self.output_path, plugin="napari-ome-zarr") else: napari_viewer.open(self.output_path) for layer in napari_viewer.layers: layer_name = layer.name.replace("_", " ").replace("full ", "full_") - channel_info = CHANNEL_COLORS_MAP.get(self.extractWavelength(layer_name), {'hex': 0xFFFFFF, 'name': 'gray'}) + channel_info = CHANNEL_COLORS_MAP.get( + self.extractWavelength(layer_name), {"hex": 0xFFFFFF, "name": "gray"} + ) - if channel_info['name'] in AVAILABLE_COLORMAPS: - layer.colormap = AVAILABLE_COLORMAPS[channel_info['name']] + if channel_info["name"] in AVAILABLE_COLORMAPS: + layer.colormap = AVAILABLE_COLORMAPS[channel_info["name"]] else: layer.colormap = self.generateColormap(channel_info) @@ -4193,7 +4394,20 @@ class NapariLiveWidget(QWidget): signal_newAnalogGain = Signal(float) signal_autoLevelSetting = Signal(bool) - def __init__(self, streamHandler, liveController, stage: AbstractStage, configurationManager, contrastManager, wellSelectionWidget=None, show_trigger_options=True, show_display_options=True, show_autolevel=False, autolevel=False, parent=None): + def __init__( + self, + streamHandler, + liveController, + stage: AbstractStage, + configurationManager, + contrastManager, + wellSelectionWidget=None, + show_trigger_options=True, + show_display_options=True, + show_autolevel=False, + autolevel=False, + parent=None, + ): super().__init__(parent) self.streamHandler = streamHandler self.liveController = liveController @@ -4223,7 +4437,7 @@ def __init__(self, streamHandler, liveController, stage: AbstractStage, configur def initNapariViewer(self): self.viewer = napari.Viewer(show=False) self.viewerWidget = self.viewer.window._qt_window - self.viewer.dims.axis_labels = ['Y-axis', 'X-axis'] + self.viewer.dims.axis_labels = ["Y-axis", "X-axis"] self.layout = QVBoxLayout() self.layout.addWidget(self.viewerWidget) self.setLayout(self.layout) @@ -4231,11 +4445,11 @@ def initNapariViewer(self): def customizeViewer(self): # Hide the status bar (which includes the activity button) - if hasattr(self.viewer.window, '_status_bar'): + 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'): + if hasattr(self.viewer.window._qt_viewer, "layerButtons"): self.viewer.window._qt_viewer.layerButtons.hide() def updateHistogram(self, layer): @@ -4248,7 +4462,7 @@ def updateHistogram(self, layer): self.histogram_widget.region.setRegion(layer.contrast_limits) # Update colormap only if it has changed - if hasattr(self, 'last_colormap') and self.last_colormap != layer.colormap.name: + if hasattr(self, "last_colormap") and self.last_colormap != layer.colormap.name: self.histogram_widget.gradient.setColorMap(self.createColorMap(layer.colormap)) self.last_colormap = layer.colormap.name @@ -4262,9 +4476,7 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au self.pg_image_item = pg.ImageItem() self.histogram_widget = pg.HistogramLUTWidget(image=self.pg_image_item) self.histogram_widget.setFixedWidth(100) - self.histogram_dock = self.viewer.window.add_dock_widget( - self.histogram_widget, area='right', name="hist" - ) + self.histogram_dock = self.viewer.window.add_dock_widget(self.histogram_widget, area="right", name="hist") self.histogram_dock.setFeatures(QDockWidget.NoDockWidgetFeatures) self.histogram_dock.setTitleBarWidget(QWidget()) self.histogram_widget.region.sigRegionChanged.connect(self.on_histogram_region_changed) @@ -4303,14 +4515,16 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au } """ self.btn_live.setStyleSheet(gradient_style) - #self.btn_live.setStyleSheet("font-weight: bold; background-color: #7676F7") #6666D3 + # self.btn_live.setStyleSheet("font-weight: bold; background-color: #7676F7") #6666D3 current_height = self.btn_live.sizeHint().height() self.btn_live.setFixedHeight(int(current_height * 1.5)) self.btn_live.clicked.connect(self.toggle_live) # Exposure Time self.entry_exposureTime = QDoubleSpinBox() - self.entry_exposureTime.setRange(self.liveController.camera.EXPOSURE_TIME_MS_MIN, self.liveController.camera.EXPOSURE_TIME_MS_MAX) + self.entry_exposureTime.setRange( + self.liveController.camera.EXPOSURE_TIME_MS_MIN, self.liveController.camera.EXPOSURE_TIME_MS_MAX + ) self.entry_exposureTime.setValue(self.live_configuration.exposure_time) self.entry_exposureTime.setSuffix(" ms") self.entry_exposureTime.valueChanged.connect(self.update_config_exposure_time) @@ -4331,14 +4545,16 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au self.slider_illuminationIntensity.setTickInterval(10) self.slider_illuminationIntensity.valueChanged.connect(self.update_config_illumination_intensity) self.label_illuminationIntensity = QLabel(str(self.slider_illuminationIntensity.value()) + "%") - self.slider_illuminationIntensity.valueChanged.connect(lambda v: self.label_illuminationIntensity.setText(str(v) + "%")) + self.slider_illuminationIntensity.valueChanged.connect( + lambda v: self.label_illuminationIntensity.setText(str(v) + "%") + ) # Trigger mode self.dropdown_triggerMode = QComboBox() trigger_modes = [ - ('Software', TriggerMode.SOFTWARE), - ('Hardware', TriggerMode.HARDWARE), - ('Continuous', TriggerMode.CONTINUOUS) + ("Software", TriggerMode.SOFTWARE), + ("Hardware", TriggerMode.HARDWARE), + ("Continuous", TriggerMode.CONTINUOUS), ] for display_name, mode in trigger_modes: self.dropdown_triggerMode.addItem(display_name, mode) @@ -4351,14 +4567,14 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au self.entry_triggerFPS = QDoubleSpinBox() self.entry_triggerFPS.setRange(0.02, 1000) self.entry_triggerFPS.setValue(self.fps_trigger) - #self.entry_triggerFPS.setSuffix(" fps") + # self.entry_triggerFPS.setSuffix(" fps") self.entry_triggerFPS.valueChanged.connect(self.liveController.set_trigger_fps) # Display FPS self.entry_displayFPS = QDoubleSpinBox() self.entry_displayFPS.setRange(1, 240) self.entry_displayFPS.setValue(self.fps_display) - #self.entry_displayFPS.setSuffix(" fps") + # self.entry_displayFPS.setSuffix(" fps") self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) # Resolution Scaling @@ -4372,7 +4588,7 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au self.slider_resolutionScaling.valueChanged.connect(lambda v: self.label_resolutionScaling.setText(str(v) + "%")) # Autolevel - self.btn_autolevel = QPushButton('Autolevel') + self.btn_autolevel = QPushButton("Autolevel") self.btn_autolevel.setCheckable(True) self.btn_autolevel.setChecked(autolevel) self.btn_autolevel.clicked.connect(self.signal_autoLevelSetting.emit) @@ -4392,27 +4608,27 @@ def make_row(label_widget, entry_widget, value_label=None): control_layout.addWidget(self.btn_live) control_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)) - row1 = make_row(QLabel('Exposure Time'), self.entry_exposureTime) + row1 = make_row(QLabel("Exposure Time"), self.entry_exposureTime) control_layout.addLayout(row1) - row2 = make_row(QLabel('Illumination'), self.slider_illuminationIntensity, self.label_illuminationIntensity) + row2 = make_row(QLabel("Illumination"), self.slider_illuminationIntensity, self.label_illuminationIntensity) control_layout.addLayout(row2) - row3 = make_row((QLabel('Analog Gain')), self.entry_analogGain) + row3 = make_row((QLabel("Analog Gain")), self.entry_analogGain) control_layout.addLayout(row3) control_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)) if show_trigger_options: - row0 = make_row(QLabel('Trigger Mode'), self.dropdown_triggerMode) + row0 = make_row(QLabel("Trigger Mode"), self.dropdown_triggerMode) control_layout.addLayout(row0) - row00 = make_row(QLabel('Trigger FPS'), self.entry_triggerFPS) + row00 = make_row(QLabel("Trigger FPS"), self.entry_triggerFPS) control_layout.addLayout(row00) control_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)) if show_display_options: - row4 = make_row((QLabel('Display FPS')), self.entry_displayFPS) + row4 = make_row((QLabel("Display FPS")), self.entry_displayFPS) control_layout.addLayout(row4) - row5 = make_row(QLabel('Display Resolution'), self.slider_resolutionScaling, self.label_resolutionScaling) + row5 = make_row(QLabel("Display Resolution"), self.slider_resolutionScaling, self.label_resolutionScaling) control_layout.addLayout(row5) control_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)) @@ -4436,18 +4652,24 @@ def make_row(label_widget, entry_widget, value_label=None): self.viewer.window.remove_dock_widget(self.viewer.window._qt_viewer.dockLayerList) # Add the actual dock widgets - self.dock_layer_controls = self.viewer.window.add_dock_widget(layer_controls_widget, area='left', name='layer controls', tabify=True) - self.dock_layer_list = self.viewer.window.add_dock_widget(layer_list_widget, area='left', name='layer list', tabify=True) - self.dock_live_controls = self.viewer.window.add_dock_widget(live_controls_widget, area='left', name='live controls', tabify=True) + self.dock_layer_controls = self.viewer.window.add_dock_widget( + layer_controls_widget, area="left", name="layer controls", tabify=True + ) + self.dock_layer_list = self.viewer.window.add_dock_widget( + layer_list_widget, area="left", name="layer list", tabify=True + ) + self.dock_live_controls = self.viewer.window.add_dock_widget( + live_controls_widget, area="left", name="live controls", tabify=True + ) self.viewer.window.window_menu.addAction(self.dock_live_controls.toggleViewAction()) if USE_NAPARI_WELL_SELECTION: well_selector_layout = QVBoxLayout() - #title_label = QLabel("Well Selector") - #title_label.setAlignment(Qt.AlignCenter) # Center the title - #title_label.setStyleSheet("font-weight: bold;") # Optional: style the title - #well_selector_layout.addWidget(title_label) + # title_label = QLabel("Well Selector") + # title_label.setAlignment(Qt.AlignCenter) # Center the title + # title_label.setStyleSheet("font-weight: bold;") # Optional: style the title + # well_selector_layout.addWidget(title_label) well_selector_row = QHBoxLayout() well_selector_row.addStretch(1) @@ -4458,7 +4680,9 @@ def make_row(label_widget, entry_widget, value_label=None): well_selector_dock_widget = QWidget() well_selector_dock_widget.setLayout(well_selector_layout) - self.dock_well_selector = self.viewer.window.add_dock_widget(well_selector_dock_widget, area='bottom', name='well selector') + self.dock_well_selector = self.viewer.window.add_dock_widget( + well_selector_dock_widget, area="bottom", name="well selector" + ) self.dock_well_selector.setFixedHeight(self.dock_well_selector.minimumSizeHint().height()) layer_controls_widget = self.viewer.window._qt_viewer.dockLayerControls.widget() @@ -4508,13 +4732,22 @@ def replace_well_selector(self, wellSelector): well_selector_layout.addStretch(1) # Add stretch on the right well_selector_dock_widget = QWidget() well_selector_dock_widget.setLayout(well_selector_layout) - self.dock_well_selector = self.viewer.window.add_dock_widget(well_selector_dock_widget, area='bottom', name='well selector', tabify=True) + self.dock_well_selector = self.viewer.window.add_dock_widget( + well_selector_dock_widget, area="bottom", name="well selector", tabify=True + ) - def set_microscope_mode(self,config): + def set_microscope_mode(self, config): self.dropdown_modeSelection.setCurrentText(config.name) def update_microscope_mode_by_name(self, current_microscope_mode_name): - self.live_configuration = next((config for config in self.configurationManager.configurations if config.name == current_microscope_mode_name), None) + self.live_configuration = next( + ( + config + for config in self.configurationManager.configurations + if config.name == current_microscope_mode_name + ), + None, + ) if self.live_configuration: self.liveController.set_microscope_mode(self.live_configuration) self.entry_exposureTime.setValue(self.live_configuration.exposure_time) @@ -4523,17 +4756,17 @@ def update_microscope_mode_by_name(self, current_microscope_mode_name): def update_config_exposure_time(self, new_value): self.live_configuration.exposure_time = new_value - self.configurationManager.update_configuration(self.live_configuration.id, 'ExposureTime', new_value) + self.configurationManager.update_configuration(self.live_configuration.id, "ExposureTime", new_value) self.signal_newExposureTime.emit(new_value) def update_config_analog_gain(self, new_value): self.live_configuration.analog_gain = new_value - self.configurationManager.update_configuration(self.live_configuration.id, 'AnalogGain', new_value) + self.configurationManager.update_configuration(self.live_configuration.id, "AnalogGain", new_value) self.signal_newAnalogGain.emit(new_value) def update_config_illumination_intensity(self, new_value): self.live_configuration.illumination_intensity = new_value - self.configurationManager.update_configuration(self.live_configuration.id, 'IlluminationIntensity', new_value) + self.configurationManager.update_configuration(self.live_configuration.id, "IlluminationIntensity", new_value) self.liveController.set_illumination(self.live_configuration.illumination_source, new_value) def update_resolution_scaling(self, value): @@ -4546,13 +4779,13 @@ def on_trigger_mode_changed(self, index): print(f"Selected: {self.dropdown_triggerMode.currentText()} (actual value: {actual_value})") def addNapariGrayclipColormap(self): - if hasattr(napari.utils.colormaps.AVAILABLE_COLORMAPS, 'grayclip'): + if hasattr(napari.utils.colormaps.AVAILABLE_COLORMAPS, "grayclip"): return grayclip = [] for i in range(255): grayclip.append([i / 255, i / 255, i / 255]) grayclip.append([1, 0, 0]) - napari.utils.colormaps.AVAILABLE_COLORMAPS['grayclip'] = napari.utils.Colormap(name='grayclip', colors=grayclip) + napari.utils.colormaps.AVAILABLE_COLORMAPS["grayclip"] = napari.utils.Colormap(name="grayclip", colors=grayclip) def initLiveLayer(self, channel, image_height, image_width, image_dtype, rgb=False): """Initializes the full canvas for each channel based on the acquisition parameters.""" @@ -4561,7 +4794,9 @@ def initLiveLayer(self, channel, image_height, image_width, image_dtype, rgb=Fal self.image_height = image_height if self.dtype != np.dtype(image_dtype): - self.contrastManager.scale_contrast_limits(np.dtype(image_dtype)) # Fix This to scale existing contrast limits to new dtype range + self.contrastManager.scale_contrast_limits( + np.dtype(image_dtype) + ) # Fix This to scale existing contrast limits to new dtype range self.dtype = image_dtype self.channels.add(channel) @@ -4572,8 +4807,15 @@ def initLiveLayer(self, channel, image_height, image_width, image_dtype, rgb=Fal else: canvas = np.zeros((image_height, image_width), dtype=self.dtype) limits = self.getContrastLimits(self.dtype) - layer = self.viewer.add_image(canvas, name="Live View", visible=True, rgb=rgb, colormap='grayclip', - contrast_limits=limits, blending='additive') + layer = self.viewer.add_image( + canvas, + name="Live View", + visible=True, + rgb=rgb, + colormap="grayclip", + contrast_limits=limits, + blending="additive", + ) layer.contrast_limits = self.contrastManager.get_limits(self.live_configuration.name, self.dtype) layer.mouse_double_click_callbacks.append(self.onDoubleClick) layer.events.contrast_limits.connect(self.signalContrastLimits) @@ -4599,7 +4841,7 @@ def updateLiveLayer(self, image, from_autofocus=False): self.live_configuration.name = self.liveController.currentConfiguration.name rgb = len(image.shape) >= 3 - if not rgb and not self.init_live or 'Live View' not in self.viewer.layers: + if not rgb and not self.init_live or "Live View" not in self.viewer.layers: self.initLiveLayer(self.live_configuration.name, image.shape[0], image.shape[1], image.dtype, rgb) self.init_live = True self.init_live_rgb = False @@ -4702,7 +4944,7 @@ def initNapariViewer(self): self.viewer = napari.Viewer(show=False) if self.grid_enabled: self.viewer.grid.enabled = True - self.viewer.dims.axis_labels = ['Z-axis', 'Y-axis', 'X-axis'] + self.viewer.dims.axis_labels = ["Z-axis", "Y-axis", "X-axis"] self.viewerWidget = self.viewer.window._qt_window self.layout = QVBoxLayout() self.layout.addWidget(self.viewerWidget) @@ -4711,11 +4953,11 @@ def initNapariViewer(self): def customizeViewer(self): # Hide the status bar (which includes the activity button) - if hasattr(self.viewer.window, '_status_bar'): + 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'): + if hasattr(self.viewer.window._qt_viewer, "layerButtons"): self.viewer.window._qt_viewer.layerButtons.hide() def initLayersShape(self, Nz, dz): @@ -4732,11 +4974,11 @@ def initChannels(self, 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 "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']: + for color in ["R", "G", "B"]: if color in parts or f"full_{color}" in parts: return color return None @@ -4745,10 +4987,12 @@ def generateColormap(self, channel_info): """Convert a HEX value to a normalized RGB tuple.""" positions = [0, 1] 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']) + 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.""" @@ -4773,7 +5017,7 @@ def updateLayers(self, image, x, y, k, channel_name): rgb = len(image.shape) == 3 # Check if the layer exists and has a different dtype - if self.dtype != np.dtype(image.dtype): # or self.viewer.layers[channel_name].data.dtype != image.dtype: + if self.dtype != np.dtype(image.dtype): # or self.viewer.layers[channel_name].data.dtype != image.dtype: # Remove the existing layer self.layers_initialized = False self.acquisition_initialized = False @@ -4787,17 +5031,26 @@ def updateLayers(self, image, x, y, k, channel_name): color = None # RGB images do not need a colormap canvas = np.zeros((self.Nz, self.image_height, 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']] + 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.image_height, 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)) + 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"multi channel - 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) @@ -4822,7 +5075,7 @@ def updateLayers(self, image, x, y, k, channel_name): def updateRTPLayers(self, image, channel_name): """Updates the appropriate slice of the canvas with the new image data.""" # Check if the layer exists and has a different dtype - if self.dtype != image.dtype: # or self.viewer.layers[channel_name].data.dtype != image.dtype: + if self.dtype != image.dtype: # or self.viewer.layers[channel_name].data.dtype != image.dtype: # Remove the existing layer self.layers_initialized = False self.acquisition_initialized = False @@ -4837,15 +5090,24 @@ def updateRTPLayers(self, image, channel_name): color = None # RGB images do not need a colormap canvas = np.zeros((self.image_height, 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']] + 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.image_height, self.image_width), dtype=self.dtype) - layer = self.viewer.add_image(canvas, name=channel_name, visible=True, rgb=rgb, colormap=color, - blending='additive', contrast_limits=self.getContrastLimits(self.dtype)) + layer = self.viewer.add_image( + canvas, + name=channel_name, + visible=True, + rgb=rgb, + colormap=color, + blending="additive", + contrast_limits=self.getContrastLimits(self.dtype), + ) layer.events.contrast_limits.connect(self.signalContrastLimits) self.resetView() @@ -4909,32 +5171,34 @@ def __init__(self, objectiveStore, contrastManager, parent=None): def customizeViewer(self): # hide status bar - if hasattr(self.viewer.window, '_status_bar'): + if hasattr(self.viewer.window, "_status_bar"): self.viewer.window._status_bar.hide() - self.viewer.bind_key('D', self.toggle_draw_mode) + self.viewer.bind_key("D", self.toggle_draw_mode) def toggle_draw_mode(self, viewer): self.is_drawing_shape = not self.is_drawing_shape - if 'Manual ROI' not in self.viewer.layers: - self.shape_layer = self.viewer.add_shapes(name='Manual ROI', edge_width=40, edge_color='red', face_color='transparent') + if "Manual ROI" not in self.viewer.layers: + self.shape_layer = self.viewer.add_shapes( + name="Manual ROI", edge_width=40, edge_color="red", face_color="transparent" + ) self.shape_layer.events.data.connect(self.on_shape_change) else: - self.shape_layer = self.viewer.layers['Manual ROI'] + self.shape_layer = self.viewer.layers["Manual ROI"] if self.is_drawing_shape: # if there are existing shapes, switch to vertex select mode if len(self.shape_layer.data) > 0: - self.shape_layer.mode = 'select' - self.shape_layer.select_mode = 'vertex' + self.shape_layer.mode = "select" + self.shape_layer.select_mode = "vertex" else: # if no shapes exist, switch to add polygon mode # start drawing a new polygon on click, add vertices with additional clicks, finish/close polygon with double-click - self.shape_layer.mode = 'add_polygon' + self.shape_layer.mode = "add_polygon" else: # if no shapes exist, switch to pan/zoom mode - self.shape_layer.mode = 'pan_zoom' + self.shape_layer.mode = "pan_zoom" self.on_shape_change() @@ -4944,7 +5208,7 @@ def enable_shape_drawing(self, enable): else: self.is_drawing_shape = False if self.shape_layer is not None: - self.shape_layer.mode = 'pan_zoom' + self.shape_layer.mode = "pan_zoom" def on_shape_change(self, event=None): if self.shape_layer is not None and len(self.shape_layer.data) > 0: @@ -4990,6 +5254,7 @@ def update_shape_layer_position(self, prev_top_left, new_top_left): except Exception as e: print(f"Error updating shape layer position: {e}") import traceback + traceback.print_exc() def initChannels(self, channels): @@ -5002,11 +5267,11 @@ def initLayersShape(self, Nz, dz): def extractWavelength(self, name): # extract wavelength from channel name parts = name.split() - if 'Fluorescence' in parts: - index = parts.index('Fluorescence') + 1 + if "Fluorescence" in parts: + index = parts.index("Fluorescence") + 1 if index < len(parts): return parts[index].split()[0] - for color in ['R', 'G', 'B']: + for color in ["R", "G", "B"]: if color in parts or f"full_{color}" in parts: return color return None @@ -5014,10 +5279,12 @@ def extractWavelength(self, name): def generateColormap(self, channel_info): # generate colormap from hex value c0 = (0, 0, 0) - c1 = (((channel_info['hex'] >> 16) & 0xFF) / 255, - ((channel_info['hex'] >> 8) & 0xFF) / 255, - (channel_info['hex'] & 0xFF) / 255) - return Colormap(colors=[c0, c1], controls=[0, 1], name=channel_info['name']) + c1 = ( + ((channel_info["hex"] >> 16) & 0xFF) / 255, + ((channel_info["hex"] >> 8) & 0xFF) / 255, + (channel_info["hex"] & 0xFF) / 255, + ) + return Colormap(colors=[c0, c1], controls=[0, 1], name=channel_info["name"]) def updateMosaic(self, image, x_mm, y_mm, k, channel_name): # calculate pixel size @@ -5027,7 +5294,11 @@ def updateMosaic(self, image, x_mm, y_mm, k, channel_name): # downsample image if self.downsample_factor != 1: - image = cv2.resize(image, (image.shape[1] // self.downsample_factor, image.shape[0] // self.downsample_factor), interpolation=cv2.INTER_AREA) + image = cv2.resize( + image, + (image.shape[1] // self.downsample_factor, image.shape[0] // self.downsample_factor), + interpolation=cv2.INTER_AREA, + ) # adjust image position x_mm -= (image.shape[1] * image_pixel_size_mm) / 2 @@ -5038,8 +5309,12 @@ def updateMosaic(self, image, x_mm, y_mm, k, channel_name): self.layers_initialized = True self.signal_layers_initialized.emit(self.layers_initialized) self.viewer_pixel_size_mm = image_pixel_size_mm - self.viewer_extents = [y_mm, y_mm + image.shape[0] * image_pixel_size_mm, - x_mm, x_mm + image.shape[1] * image_pixel_size_mm] + self.viewer_extents = [ + y_mm, + y_mm + image.shape[0] * image_pixel_size_mm, + x_mm, + x_mm + image.shape[1] * image_pixel_size_mm, + ] self.top_left_coordinate = [y_mm, x_mm] self.mosaic_dtype = image_dtype else: @@ -5047,19 +5322,30 @@ def updateMosaic(self, image, x_mm, y_mm, k, channel_name): image = self.convertImageDtype(image, self.mosaic_dtype) if image_pixel_size_mm != self.viewer_pixel_size_mm: scale_factor = image_pixel_size_mm / self.viewer_pixel_size_mm - image = cv2.resize(image, (int(image.shape[1] * scale_factor), int(image.shape[0] * scale_factor)), interpolation=cv2.INTER_LINEAR) + image = cv2.resize( + image, + (int(image.shape[1] * scale_factor), int(image.shape[0] * scale_factor)), + interpolation=cv2.INTER_LINEAR, + ) if channel_name not in self.viewer.layers: # create new layer for channel - 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']] + 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) layer = self.viewer.add_image( - np.zeros_like(image), name=channel_name, rgb=len(image.shape) == 3, colormap=color, - visible=True, blending='additive', scale=(self.viewer_pixel_size_mm * 1000, self.viewer_pixel_size_mm * 1000) + np.zeros_like(image), + name=channel_name, + rgb=len(image.shape) == 3, + colormap=color, + visible=True, + blending="additive", + scale=(self.viewer_pixel_size_mm * 1000, self.viewer_pixel_size_mm * 1000), ) layer.mouse_double_click_callbacks.append(self.onDoubleClick) layer.events.contrast_limits.connect(self.signalContrastLimits) @@ -5099,7 +5385,7 @@ def updateLayer(self, layer, image, x_mm, y_mm, k, prev_top_left): x_offset = int(math.floor((prev_top_left[1] - self.top_left_coordinate[1]) / self.viewer_pixel_size_mm)) for mosaic in self.viewer.layers: - if mosaic.name != 'Manual ROI': + if mosaic.name != "Manual ROI": if len(mosaic.data.shape) == 3 and mosaic.data.shape[2] == 3: new_data = np.zeros((mosaic_height, mosaic_width, 3), dtype=mosaic.data.dtype) else: @@ -5111,12 +5397,14 @@ def updateLayer(self, layer, image, x_mm, y_mm, k, prev_top_left): # shift existing data if len(mosaic.data.shape) == 3 and mosaic.data.shape[2] == 3: - new_data[y_offset:y_end, x_offset:x_end, :] = mosaic.data[:y_end-y_offset, :x_end-x_offset, :] + new_data[y_offset:y_end, x_offset:x_end, :] = mosaic.data[ + : y_end - y_offset, : x_end - x_offset, : + ] else: - new_data[y_offset:y_end, x_offset:x_end] = mosaic.data[:y_end-y_offset, :x_end-x_offset] + new_data[y_offset:y_end, x_offset:x_end] = mosaic.data[: y_end - y_offset, : x_end - x_offset] mosaic.data = new_data - if 'Manual ROI' in self.viewer.layers: + if "Manual ROI" in self.viewer.layers: self.update_shape_layer_position(prev_top_left, self.top_left_coordinate) self.resetView() @@ -5131,9 +5419,9 @@ def updateLayer(self, layer, image, x_mm, y_mm, k, prev_top_left): # insert image data if is_rgb: - layer.data[y_pos:y_end, x_pos:x_end, :] = image[:y_end - y_pos, :x_end - x_pos, :] + layer.data[y_pos:y_end, x_pos:x_end, :] = image[: y_end - y_pos, : x_end - x_pos, :] else: - layer.data[y_pos:y_end, x_pos:x_end] = image[:y_end - y_pos, :x_end - x_pos] + layer.data[y_pos:y_end, x_pos:x_end] = image[: y_end - y_pos, : x_end - x_pos] layer.refresh() def convertImageDtype(self, image, target_dtype): @@ -5224,7 +5512,15 @@ def activate(self): class TrackingControllerWidget(QFrame): - def __init__(self, trackingController: TrackingController, configurationManager, show_configurations = True, main=None, *args, **kwargs): + def __init__( + self, + trackingController: TrackingController, + configurationManager, + show_configurations=True, + main=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.trackingController = trackingController self.configurationManager = configurationManager @@ -5232,15 +5528,17 @@ def __init__(self, trackingController: TrackingController, configurationManager, self.add_components(show_configurations) self.setFrameStyle(QFrame.Panel | QFrame.Raised) - self.trackingController.microcontroller.add_joystick_button_listener(lambda button_pressed: self.handle_button_state(button_pressed)) + self.trackingController.microcontroller.add_joystick_button_listener( + lambda button_pressed: self.handle_button_state(button_pressed) + ) - def add_components(self,show_configurations): - self.btn_setSavingDir = QPushButton('Browse') + def add_components(self, show_configurations): + self.btn_setSavingDir = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + 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("Choose a base saving directory") self.lineEdit_savingDir.setText(DEFAULT_SAVING_PATH) self.trackingController.set_base_path(DEFAULT_SAVING_PATH) self.base_path_is_set = True @@ -5265,61 +5563,63 @@ def add_components(self,show_configurations): 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.list_configurations.setSelectionMode( + QAbstractItemView.MultiSelection + ) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum - self.checkbox_withAutofocus = QCheckBox('With AF') - self.checkbox_saveImages = QCheckBox('Save Images') - self.btn_track = QPushButton('Start Tracking') + self.checkbox_withAutofocus = QCheckBox("With AF") + self.checkbox_saveImages = QCheckBox("Save Images") + self.btn_track = QPushButton("Start Tracking") self.btn_track.setCheckable(True) self.btn_track.setChecked(False) - self.checkbox_enable_stage_tracking = QCheckBox(' Enable Stage Tracking') + self.checkbox_enable_stage_tracking = QCheckBox(" Enable Stage Tracking") self.checkbox_enable_stage_tracking.setChecked(True) # layout grid_line0 = QGridLayout() - tmp = QLabel('Saving Path') + tmp = QLabel("Saving Path") tmp.setFixedWidth(90) - grid_line0.addWidget(tmp, 0,0) - grid_line0.addWidget(self.lineEdit_savingDir, 0,1, 1,2) - grid_line0.addWidget(self.btn_setSavingDir, 0,3) - tmp = QLabel('Experiment ID') + grid_line0.addWidget(tmp, 0, 0) + grid_line0.addWidget(self.lineEdit_savingDir, 0, 1, 1, 2) + grid_line0.addWidget(self.btn_setSavingDir, 0, 3) + tmp = QLabel("Experiment ID") tmp.setFixedWidth(90) - grid_line0.addWidget(tmp, 1,0) - grid_line0.addWidget(self.lineEdit_experimentID, 1,1, 1,1) - tmp = QLabel('Objective') + grid_line0.addWidget(tmp, 1, 0) + grid_line0.addWidget(self.lineEdit_experimentID, 1, 1, 1, 1) + tmp = QLabel("Objective") tmp.setFixedWidth(90) # grid_line0.addWidget(tmp,1,2) # grid_line0.addWidget(self.dropdown_objective, 1,3) - grid_line0.addWidget(tmp,1,2) - grid_line0.addWidget(self.objectivesWidget, 1,3) + grid_line0.addWidget(tmp, 1, 2) + grid_line0.addWidget(self.objectivesWidget, 1, 3) grid_line3 = QHBoxLayout() - tmp = QLabel('Configurations') + tmp = QLabel("Configurations") tmp.setFixedWidth(90) grid_line3.addWidget(tmp) grid_line3.addWidget(self.list_configurations) grid_line1 = QHBoxLayout() - tmp = QLabel('Tracker') + tmp = QLabel("Tracker") grid_line1.addWidget(tmp) grid_line1.addWidget(self.dropdown_tracker) - tmp = QLabel('Tracking Interval (s)') + tmp = QLabel("Tracking Interval (s)") grid_line1.addWidget(tmp) grid_line1.addWidget(self.entry_tracking_interval) grid_line1.addWidget(self.checkbox_withAutofocus) grid_line1.addWidget(self.checkbox_saveImages) grid_line4 = QGridLayout() - grid_line4.addWidget(self.btn_track,0,0,1,3) - grid_line4.addWidget(self.checkbox_enable_stage_tracking,0,4) + grid_line4.addWidget(self.btn_track, 0, 0, 1, 3) + grid_line4.addWidget(self.checkbox_enable_stage_tracking, 0, 4) self.grid = QVBoxLayout() self.grid.addLayout(grid_line0) if show_configurations: self.grid.addLayout(grid_line3) else: - self.list_configurations.setCurrentRow(0) # select the first configuration + self.list_configurations.setCurrentRow(0) # select the first configuration self.grid.addLayout(grid_line1) self.grid.addLayout(grid_line4) self.grid.addStretch() @@ -5334,22 +5634,18 @@ def add_components(self,show_configurations): self.btn_track.clicked.connect(self.toggle_acquisition) # connections - selections and entries self.dropdown_tracker.currentIndexChanged.connect(self.update_tracker) - #self.dropdown_objective.currentIndexChanged.connect(self.update_pixel_size) + # self.dropdown_objective.currentIndexChanged.connect(self.update_pixel_size) self.objectivesWidget.dropdown.currentIndexChanged.connect(self.update_pixel_size) # controller to widget self.trackingController.signal_tracking_stopped.connect(self.slot_tracking_stopped) # run initialization functions self.update_pixel_size() - self.trackingController.update_image_resizing_factor(1) # to add: image resizing slider + self.trackingController.update_image_resizing_factor(1) # to add: image resizing slider # TODO(imo): This needs testing! def handle_button_pressed(self, button_state): - QMetaObject.invokeMethod( - self, - "slot_joystick_button_pressed", - Qt.AutoConnection, - button_state) + QMetaObject.invokeMethod(self, "slot_joystick_button_pressed", Qt.AutoConnection, button_state) def slot_joystick_button_pressed(self, button_state): self.btn_track.setChecked(button_state) @@ -5362,7 +5658,9 @@ def slot_joystick_button_pressed(self, button_state): return self.setEnabled_all(False) self.trackingController.start_new_experiment(self.lineEdit_experimentID.text()) - self.trackingController.set_selected_configurations((item.text() for item in self.list_configurations.selectedItems())) + self.trackingController.set_selected_configurations( + (item.text() for item in self.list_configurations.selectedItems()) + ) self.trackingController.start_tracking() else: self.trackingController.stop_tracking() @@ -5370,7 +5668,7 @@ def slot_joystick_button_pressed(self, button_state): def slot_tracking_stopped(self): self.btn_track.setChecked(False) self.setEnabled_all(True) - print('tracking stopped') + print("tracking stopped") def set_saving_dir(self): dialog = QFileDialog() @@ -5379,7 +5677,7 @@ def set_saving_dir(self): self.lineEdit_savingDir.setText(save_dir_base) self.base_path_is_set = True - def toggle_acquisition(self,pressed): + def toggle_acquisition(self, pressed): if pressed: if self.base_path_is_set == False: self.btn_track.setChecked(False) @@ -5391,12 +5689,14 @@ def toggle_acquisition(self,pressed): # @@@ to do: emit signal to widgetManager to disable other widgets self.setEnabled_all(False) self.trackingController.start_new_experiment(self.lineEdit_experimentID.text()) - self.trackingController.set_selected_configurations((item.text() for item in self.list_configurations.selectedItems())) + self.trackingController.set_selected_configurations( + (item.text() for item in self.list_configurations.selectedItems()) + ) self.trackingController.start_tracking() else: self.trackingController.stop_tracking() - def setEnabled_all(self,enabled): + def setEnabled_all(self, enabled): self.btn_setSavingDir.setEnabled(enabled) self.lineEdit_savingDir.setEnabled(enabled) self.lineEdit_experimentID.setEnabled(enabled) @@ -5411,9 +5711,11 @@ def update_pixel_size(self): objective = self.dropdown_objective.currentText() self.trackingController.objective = objective # self.internal_state.data['Objective'] = self.objective - pixel_size_um = CAMERA_PIXEL_SIZE_UM[CAMERA_SENSOR] / ( TUBE_LENS_MM/ (OBJECTIVES[objective]['tube_lens_f_mm']/OBJECTIVES[objective]['magnification']) ) + pixel_size_um = CAMERA_PIXEL_SIZE_UM[CAMERA_SENSOR] / ( + TUBE_LENS_MM / (OBJECTIVES[objective]["tube_lens_f_mm"] / OBJECTIVES[objective]["magnification"]) + ) self.trackingController.update_pixel_size(pixel_size_um) - print('pixel size is ' + str(pixel_size_um) + ' μm') + print("pixel size is " + str(pixel_size_um) + " μm") def update_pixel_size(self): objective = self.objectiveStore.current_objective @@ -5425,9 +5727,9 @@ def update_pixel_size(self): pixel_size_um = CAMERA_PIXEL_SIZE_UM[CAMERA_SENSOR] pixel_size_xy = pixel_size_um / (magnification / (objective_tube_lens_mm / tube_lens_mm)) self.trackingController.update_pixel_size(pixel_size_xy) - print(f'pixel size is {pixel_size_xy:.2f} μm') + print(f"pixel size is {pixel_size_xy:.2f} μm") - ''' + """ # connections self.checkbox_withAutofocus.stateChanged.connect(self.trackingController.set_af_flag) self.btn_setSavingDir.clicked.connect(self.set_saving_dir) @@ -5473,10 +5775,13 @@ def setEnabled_all(self,enabled,exclude_btn_startAcquisition=False): self.checkbox_withAutofocus.setEnabled(enabled) if exclude_btn_startAcquisition is not True: self.btn_startAcquisition.setEnabled(enabled) - ''' + """ + class PlateReaderAcquisitionWidget(QFrame): - def __init__(self, plateReadingController, configurationManager = None, show_configurations = True, main=None, *args, **kwargs): + def __init__( + self, plateReadingController, configurationManager=None, show_configurations=True, main=None, *args, **kwargs + ): super().__init__(*args, **kwargs) self.plateReadingController = plateReadingController self.configurationManager = configurationManager @@ -5484,13 +5789,13 @@ def __init__(self, plateReadingController, configurationManager = None, show_con self.add_components(show_configurations) self.setFrameStyle(QFrame.Panel | QFrame.Raised) - def add_components(self,show_configurations): - self.btn_setSavingDir = QPushButton('Browse') + def add_components(self, show_configurations): + self.btn_setSavingDir = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + 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("Choose a base saving directory") self.lineEdit_savingDir.setText(DEFAULT_SAVING_PATH) self.plateReadingController.set_base_path(DEFAULT_SAVING_PATH) self.base_path_is_set = True @@ -5499,16 +5804,20 @@ def add_components(self,show_configurations): self.list_columns = QListWidget() for i in range(PLATE_READER.NUMBER_OF_COLUMNS): - self.list_columns.addItems([str(i+1)]) - self.list_columns.setSelectionMode(QAbstractItemView.MultiSelection) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum + self.list_columns.addItems([str(i + 1)]) + self.list_columns.setSelectionMode( + QAbstractItemView.MultiSelection + ) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum 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.list_configurations.setSelectionMode( + QAbstractItemView.MultiSelection + ) # ref: https://doc.qt.io/qt-5/qabstractitemview.html#SelectionMode-enum - self.checkbox_withAutofocus = QCheckBox('With AF') - self.btn_startAcquisition = QPushButton('Start Acquisition') + self.checkbox_withAutofocus = QCheckBox("With AF") + self.btn_startAcquisition = QPushButton("Start Acquisition") self.btn_startAcquisition.setCheckable(True) self.btn_startAcquisition.setChecked(False) @@ -5516,40 +5825,40 @@ def add_components(self,show_configurations): # layout grid_line0 = QGridLayout() - tmp = QLabel('Saving Path') + tmp = QLabel("Saving Path") tmp.setFixedWidth(90) grid_line0.addWidget(tmp) - grid_line0.addWidget(self.lineEdit_savingDir, 0,1) - grid_line0.addWidget(self.btn_setSavingDir, 0,2) + grid_line0.addWidget(self.lineEdit_savingDir, 0, 1) + grid_line0.addWidget(self.btn_setSavingDir, 0, 2) grid_line1 = QGridLayout() - tmp = QLabel('Sample ID') + tmp = QLabel("Sample ID") tmp.setFixedWidth(90) grid_line1.addWidget(tmp) - grid_line1.addWidget(self.lineEdit_experimentID,0,1) + grid_line1.addWidget(self.lineEdit_experimentID, 0, 1) grid_line2 = QGridLayout() - tmp = QLabel('Columns') + tmp = QLabel("Columns") tmp.setFixedWidth(90) grid_line2.addWidget(tmp) - grid_line2.addWidget(self.list_columns, 0,1) + grid_line2.addWidget(self.list_columns, 0, 1) grid_line3 = QHBoxLayout() - tmp = QLabel('Configurations') + tmp = QLabel("Configurations") tmp.setFixedWidth(90) grid_line3.addWidget(tmp) grid_line3.addWidget(self.list_configurations) # grid_line3.addWidget(self.checkbox_withAutofocus) 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_line0, 0, 0) + self.grid.addLayout(grid_line1, 1, 0) + self.grid.addLayout(grid_line2, 2, 0) if show_configurations: - self.grid.addLayout(grid_line3,3,0) + self.grid.addLayout(grid_line3, 3, 0) else: - self.list_configurations.setCurrentRow(0) # select the first configuration - self.grid.addWidget(self.btn_startAcquisition,4,0) + self.list_configurations.setCurrentRow(0) # select the first configuration + self.grid.addWidget(self.btn_startAcquisition, 4, 0) self.setLayout(self.grid) # add and display a timer - to be implemented @@ -5568,7 +5877,7 @@ def set_saving_dir(self): self.lineEdit_savingDir.setText(save_dir_base) self.base_path_is_set = True - def toggle_acquisition(self,pressed): + def toggle_acquisition(self, pressed): if self.base_path_is_set == False: self.btn_startAcquisition.setChecked(False) msg = QMessageBox() @@ -5580,18 +5889,22 @@ def toggle_acquisition(self,pressed): # @@@ to do: emit signal to widgetManager to disable other widgets self.setEnabled_all(False) self.plateReadingController.start_new_experiment(self.lineEdit_experimentID.text()) - self.plateReadingController.set_selected_configurations((item.text() for item in self.list_configurations.selectedItems())) - self.plateReadingController.set_selected_columns(list(map(int,[item.text() for item in self.list_columns.selectedItems()]))) + self.plateReadingController.set_selected_configurations( + (item.text() for item in self.list_configurations.selectedItems()) + ) + self.plateReadingController.set_selected_columns( + list(map(int, [item.text() for item in self.list_columns.selectedItems()])) + ) self.plateReadingController.run_acquisition() else: - self.plateReadingController.stop_acquisition() # to implement + self.plateReadingController.stop_acquisition() # to implement pass def acquisition_is_finished(self): self.btn_startAcquisition.setChecked(False) self.setEnabled_all(True) - def setEnabled_all(self,enabled,exclude_btn_startAcquisition=False): + def setEnabled_all(self, enabled, exclude_btn_startAcquisition=False): self.btn_setSavingDir.setEnabled(enabled) self.lineEdit_savingDir.setEnabled(enabled) self.lineEdit_experimentID.setEnabled(enabled) @@ -5615,13 +5928,13 @@ def __init__(self, plateReaderNavigationController, *args, **kwargs): def add_components(self): self.dropdown_column = QComboBox() - self.dropdown_column.addItems(['']) - self.dropdown_column.addItems([str(i+1) for i in range(PLATE_READER.NUMBER_OF_COLUMNS)]) + self.dropdown_column.addItems([""]) + self.dropdown_column.addItems([str(i + 1) for i in range(PLATE_READER.NUMBER_OF_COLUMNS)]) self.dropdown_row = QComboBox() - self.dropdown_row.addItems(['']) - self.dropdown_row.addItems([chr(i) for i in range(ord('A'),ord('A')+PLATE_READER.NUMBER_OF_ROWS)]) + self.dropdown_row.addItems([""]) + self.dropdown_row.addItems([chr(i) for i in range(ord("A"), ord("A") + PLATE_READER.NUMBER_OF_ROWS)]) self.btn_moveto = QPushButton("Move To") - self.btn_home = QPushButton('Home') + self.btn_home = QPushButton("Home") self.label_current_location = QLabel() self.label_current_location.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.label_current_location.setFixedWidth(50) @@ -5635,16 +5948,16 @@ def add_components(self): # tmp = QLabel('Saving Path') # tmp.setFixedWidth(90) grid_line0.addWidget(self.btn_home) - grid_line0.addWidget(QLabel('Column')) + grid_line0.addWidget(QLabel("Column")) grid_line0.addWidget(self.dropdown_column) - grid_line0.addWidget(QLabel('Row')) + grid_line0.addWidget(QLabel("Row")) grid_line0.addWidget(self.dropdown_row) grid_line0.addWidget(self.btn_moveto) grid_line0.addStretch() grid_line0.addWidget(self.label_current_location) self.grid = QGridLayout() - self.grid.addLayout(grid_line0,0,0) + self.grid.addLayout(grid_line0, 0, 0) self.setLayout(self.grid) self.btn_home.clicked.connect(self.home) @@ -5663,14 +5976,14 @@ def home(self): self.plateReaderNavigationController.home() def move(self): - self.plateReaderNavigationController.moveto(self.dropdown_column.currentText(),self.dropdown_row.currentText()) + self.plateReaderNavigationController.moveto(self.dropdown_column.currentText(), self.dropdown_row.currentText()) def slot_homing_complete(self): self.dropdown_column.setEnabled(True) self.dropdown_row.setEnabled(True) self.btn_moveto.setEnabled(True) - def update_current_location(self,location_str): + def update_current_location(self, location_str): self.label_current_location.setText(location_str) row = location_str[0] column = location_str[1:] @@ -5697,7 +6010,7 @@ def add_components(self): # line 0: trigger mode self.triggerMode = None self.dropdown_triggerManu = QComboBox() - self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE,TriggerMode.HARDWARE]) + self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE, TriggerMode.HARDWARE]) # line 1: fps self.entry_triggerFPS = QDoubleSpinBox() @@ -5721,14 +6034,14 @@ def add_components(self): # layout grid_line0 = QGridLayout() - grid_line0.addWidget(QLabel('Trigger Mode'), 0,0) - grid_line0.addWidget(self.dropdown_triggerManu, 0,1) - grid_line0.addWidget(QLabel('Trigger FPS'), 0,2) - grid_line0.addWidget(self.entry_triggerFPS, 0,3) - grid_line0.addWidget(self.btn_live, 1,0,1,4) + grid_line0.addWidget(QLabel("Trigger Mode"), 0, 0) + grid_line0.addWidget(self.dropdown_triggerManu, 0, 1) + grid_line0.addWidget(QLabel("Trigger FPS"), 0, 2) + grid_line0.addWidget(self.entry_triggerFPS, 0, 3) + grid_line0.addWidget(self.btn_live, 1, 0, 1, 4) self.setLayout(grid_line0) - def toggle_live(self,pressed): + def toggle_live(self, pressed): self.signal_toggle_live.emit(pressed) if pressed: self.microcontroller2.start_camera_trigger() @@ -5738,7 +6051,7 @@ def toggle_live(self,pressed): def update_trigger_mode(self): self.signal_trigger_mode.emit(self.dropdown_triggerManu.currentText()) - def update_trigger_fps(self,fps): + def update_trigger_fps(self, fps): self.fps_trigger = fps self.signal_trigger_fps.emit(fps) self.microcontroller2.set_camera_trigger_frequency(self.fps_trigger) @@ -5747,7 +6060,7 @@ def update_trigger_fps(self,fps): class MultiCameraRecordingWidget(QFrame): def __init__(self, streamHandler, imageSaver, channels, main=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.imageSaver = imageSaver # for saving path control + self.imageSaver = imageSaver # for saving path control self.streamHandler = streamHandler self.channels = channels self.base_path_is_set = False @@ -5755,13 +6068,13 @@ def __init__(self, streamHandler, imageSaver, channels, main=None, *args, **kwar self.setFrameStyle(QFrame.Panel | QFrame.Raised) def add_components(self): - self.btn_setSavingDir = QPushButton('Browse') + self.btn_setSavingDir = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) + 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("Choose a base saving directory") self.lineEdit_experimentID = QLineEdit() @@ -5775,7 +6088,7 @@ def add_components(self): self.entry_timeLimit = QSpinBox() self.entry_timeLimit.setMinimum(-1) - self.entry_timeLimit.setMaximum(60*60*24*30) + self.entry_timeLimit.setMaximum(60 * 60 * 24 * 30) self.entry_timeLimit.setSingleStep(1) self.entry_timeLimit.setValue(-1) @@ -5785,25 +6098,25 @@ def add_components(self): self.btn_record.setDefault(False) grid_line1 = QGridLayout() - grid_line1.addWidget(QLabel('Saving Path')) - grid_line1.addWidget(self.lineEdit_savingDir, 0,1) - grid_line1.addWidget(self.btn_setSavingDir, 0,2) + grid_line1.addWidget(QLabel("Saving Path")) + grid_line1.addWidget(self.lineEdit_savingDir, 0, 1) + grid_line1.addWidget(self.btn_setSavingDir, 0, 2) grid_line2 = QGridLayout() - grid_line2.addWidget(QLabel('Experiment ID'), 0,0) - grid_line2.addWidget(self.lineEdit_experimentID,0,1) + grid_line2.addWidget(QLabel("Experiment ID"), 0, 0) + grid_line2.addWidget(self.lineEdit_experimentID, 0, 1) grid_line3 = QGridLayout() - grid_line3.addWidget(QLabel('Saving FPS'), 0,0) - grid_line3.addWidget(self.entry_saveFPS, 0,1) - grid_line3.addWidget(QLabel('Time Limit (s)'), 0,2) - grid_line3.addWidget(self.entry_timeLimit, 0,3) - grid_line3.addWidget(self.btn_record, 0,4) + grid_line3.addWidget(QLabel("Saving FPS"), 0, 0) + grid_line3.addWidget(self.entry_saveFPS, 0, 1) + grid_line3.addWidget(QLabel("Time Limit (s)"), 0, 2) + grid_line3.addWidget(self.entry_timeLimit, 0, 3) + grid_line3.addWidget(self.btn_record, 0, 4) self.grid = QGridLayout() - self.grid.addLayout(grid_line1,0,0) - self.grid.addLayout(grid_line2,1,0) - self.grid.addLayout(grid_line3,2,0) + self.grid.addLayout(grid_line1, 0, 0) + self.grid.addLayout(grid_line2, 1, 0) + self.grid.addLayout(grid_line3, 2, 0) self.setLayout(self.grid) # add and display a timer - to be implemented @@ -5826,7 +6139,7 @@ def set_saving_dir(self): self.save_dir_base = save_dir_base self.base_path_is_set = True - def toggle_recording(self,pressed): + def toggle_recording(self, pressed): if self.base_path_is_set == False: self.btn_record.setChecked(False) msg = QMessageBox() @@ -5837,10 +6150,10 @@ def toggle_recording(self,pressed): self.lineEdit_experimentID.setEnabled(False) self.btn_setSavingDir.setEnabled(False) experiment_ID = self.lineEdit_experimentID.text() - experiment_ID = experiment_ID + '_' + datetime.now().strftime('%Y-%m-%d_%H-%M-%S.%f') - os.mkdir(os.path.join(self.save_dir_base,experiment_ID)) + experiment_ID = experiment_ID + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") + os.mkdir(os.path.join(self.save_dir_base, experiment_ID)) for channel in self.channels: - self.imageSaver[channel].start_new_experiment(os.path.join(experiment_ID,channel),add_timestamp=False) + self.imageSaver[channel].start_new_experiment(os.path.join(experiment_ID, channel), add_timestamp=False) self.streamHandler[channel].start_recording() else: for channel in self.channels: @@ -5869,41 +6182,41 @@ def __init__(self, N=1000, include_x=True, include_y=True, main=None, *args, **k def add_components(self): self.plotWidget = {} - self.plotWidget['X'] = PlotWidget('X', N=self.N, add_legend=True) - self.plotWidget['Y'] = PlotWidget('X', N=self.N, add_legend=True) + self.plotWidget["X"] = PlotWidget("X", N=self.N, add_legend=True) + self.plotWidget["Y"] = PlotWidget("X", N=self.N, add_legend=True) - layout = QGridLayout() #layout = QStackedLayout() + layout = QGridLayout() # layout = QStackedLayout() if self.include_x: - layout.addWidget(self.plotWidget['X'],0,0) + layout.addWidget(self.plotWidget["X"], 0, 0) if self.include_y: - layout.addWidget(self.plotWidget['Y'],1,0) + layout.addWidget(self.plotWidget["Y"], 1, 0) self.setLayout(layout) - def plot(self,time,data): + def plot(self, time, data): if self.include_x: - self.plotWidget['X'].plot(time,data[0,:],'X',color=(255,255,255),clear=True) + self.plotWidget["X"].plot(time, data[0, :], "X", color=(255, 255, 255), clear=True) if self.include_y: - self.plotWidget['Y'].plot(time,data[1,:],'Y',color=(255,255,255),clear=True) + self.plotWidget["Y"].plot(time, data[1, :], "Y", color=(255, 255, 255), clear=True) - def update_N(self,N): + def update_N(self, N): self.N = N - self.plotWidget['X'].update_N(N) - self.plotWidget['Y'].update_N(N) + self.plotWidget["X"].update_N(N) + self.plotWidget["Y"].update_N(N) class PlotWidget(pg.GraphicsLayoutWidget): - def __init__(self, title='', N = 1000, parent=None,add_legend=False): + def __init__(self, title="", N=1000, parent=None, add_legend=False): super().__init__(parent) - self.plotWidget = self.addPlot(title = '', axisItems = {'bottom': pg.DateAxisItem()}) + self.plotWidget = self.addPlot(title="", axisItems={"bottom": pg.DateAxisItem()}) if add_legend: self.plotWidget.addLegend() self.N = N - def plot(self,x,y,label,color,clear=False): - self.plotWidget.plot(x[-self.N:],y[-self.N:],pen=pg.mkPen(color=color,width=4),name=label,clear=clear) + def plot(self, x, y, label, color, clear=False): + self.plotWidget.plot(x[-self.N :], y[-self.N :], pen=pg.mkPen(color=color, width=4), name=label, clear=clear) - def update_N(self,N): + def update_N(self, N): self.N = N @@ -5972,28 +6285,28 @@ def add_components(self): # layout grid_line0 = QGridLayout() - grid_line0.addWidget(QLabel('x offset'), 0,0) - grid_line0.addWidget(self.entry_x_offset, 0,1) - grid_line0.addWidget(QLabel('x scaling'), 0,2) - grid_line0.addWidget(self.entry_x_scaling, 0,3) - grid_line0.addWidget(QLabel('y offset'), 0,4) - grid_line0.addWidget(self.entry_y_offset, 0,5) - grid_line0.addWidget(QLabel('y scaling'), 0,6) - grid_line0.addWidget(self.entry_y_scaling, 0,7) + grid_line0.addWidget(QLabel("x offset"), 0, 0) + grid_line0.addWidget(self.entry_x_offset, 0, 1) + grid_line0.addWidget(QLabel("x scaling"), 0, 2) + grid_line0.addWidget(self.entry_x_scaling, 0, 3) + grid_line0.addWidget(QLabel("y offset"), 0, 4) + grid_line0.addWidget(self.entry_y_offset, 0, 5) + grid_line0.addWidget(QLabel("y scaling"), 0, 6) + grid_line0.addWidget(self.entry_y_scaling, 0, 7) grid_line1 = QGridLayout() - grid_line1.addWidget(QLabel('d from x'), 0,0) - grid_line1.addWidget(self.reading_x, 0,1) - grid_line1.addWidget(QLabel('d from y'), 0,2) - grid_line1.addWidget(self.reading_y, 0,3) - grid_line1.addWidget(QLabel('N average'), 0,4) - grid_line1.addWidget(self.entry_N_average, 0,5) - grid_line1.addWidget(QLabel('N'), 0,6) - grid_line1.addWidget(self.entry_N, 0,7) + grid_line1.addWidget(QLabel("d from x"), 0, 0) + grid_line1.addWidget(self.reading_x, 0, 1) + grid_line1.addWidget(QLabel("d from y"), 0, 2) + grid_line1.addWidget(self.reading_y, 0, 3) + grid_line1.addWidget(QLabel("N average"), 0, 4) + grid_line1.addWidget(self.entry_N_average, 0, 5) + grid_line1.addWidget(QLabel("N"), 0, 6) + grid_line1.addWidget(self.entry_N, 0, 7) self.grid = QGridLayout() - self.grid.addLayout(grid_line0,0,0) - self.grid.addLayout(grid_line1,1,0) + self.grid.addLayout(grid_line0, 0, 0) + self.grid.addLayout(grid_line1, 1, 0) self.setLayout(self.grid) # connections @@ -6005,14 +6318,21 @@ def add_components(self): self.entry_N.valueChanged.connect(self.update_settings) self.entry_N.valueChanged.connect(self.update_waveformDisplay_N) - def update_settings(self,new_value): - print('update settings') - self.displacementMeasurementController.update_settings(self.entry_x_offset.value(),self.entry_y_offset.value(),self.entry_x_scaling.value(),self.entry_y_scaling.value(),self.entry_N_average.value(),self.entry_N.value()) + def update_settings(self, new_value): + print("update settings") + self.displacementMeasurementController.update_settings( + self.entry_x_offset.value(), + self.entry_y_offset.value(), + self.entry_x_scaling.value(), + self.entry_y_scaling.value(), + self.entry_N_average.value(), + self.entry_N.value(), + ) - def update_waveformDisplay_N(self,N): + def update_waveformDisplay_N(self, N): self.waveformDisplay.update_N(N) - def display_readings(self,readings): + def display_readings(self, readings): self.reading_x.setText("{:.2f}".format(readings[0])) self.reading_y.setText("{:.2f}".format(readings[1])) @@ -6064,16 +6384,16 @@ def add_components(self): self.grid = QGridLayout() - self.grid.addWidget(self.btn_initialize,0,0,1,2) - self.grid.addWidget(self.btn_set_reference,0,2,1,2) + self.grid.addWidget(self.btn_initialize, 0, 0, 1, 2) + self.grid.addWidget(self.btn_set_reference, 0, 2, 1, 2) - self.grid.addWidget(QLabel('Displacement (um)'),1,0) - self.grid.addWidget(self.label_displacement,1,1) - self.grid.addWidget(self.btn_measure_displacement,1,2,1,2) + self.grid.addWidget(QLabel("Displacement (um)"), 1, 0) + self.grid.addWidget(self.label_displacement, 1, 1) + self.grid.addWidget(self.btn_measure_displacement, 1, 2, 1, 2) - self.grid.addWidget(QLabel('Target (um)'),2,0) - self.grid.addWidget(self.entry_target,2,1) - self.grid.addWidget(self.btn_move_to_target,2,2,1,2) + self.grid.addWidget(QLabel("Target (um)"), 2, 0) + self.grid.addWidget(self.entry_target, 2, 1) + self.grid.addWidget(self.btn_move_to_target, 2, 2, 1, 2) self.setLayout(self.grid) # make connections @@ -6090,7 +6410,7 @@ def init_controller(self): self.btn_measure_displacement.setEnabled(True) self.btn_move_to_target.setEnabled(True) - def move_to_target(self,target_um): + def move_to_target(self, target_um): self.laserAutofocusController.move_to_target(self.entry_target.value()) @@ -6105,7 +6425,7 @@ def __init__(self, stage: AbstractStage, navigationViewer, streamHandler, liveCo self.streamHandler = streamHandler self.liveController = liveController self.wellplate_format = WELLPLATE_FORMAT - self.csv_path = SAMPLE_FORMATS_CSV_PATH # 'sample_formats.csv' + self.csv_path = SAMPLE_FORMATS_CSV_PATH # 'sample_formats.csv' self.initUI() def initUI(self): @@ -6127,7 +6447,7 @@ def populate_combo_box(self): self.comboBox.addItem(format_, format_) # Add custom item and set its font to italic - self.comboBox.addItem("calibrate format...", 'custom') + self.comboBox.addItem("calibrate format...", "custom") index = self.comboBox.count() - 1 # Get the index of the last item font = QFont() font.setItalic(True) @@ -6136,7 +6456,9 @@ def populate_combo_box(self): def wellplateChanged(self, index): self.wellplate_format = self.comboBox.itemData(index) if self.wellplate_format == "custom": - calibration_dialog = WellplateCalibration(self, self.stage, self.navigationViewer, self.streamHandler, self.liveController) + calibration_dialog = WellplateCalibration( + self, self.stage, self.navigationViewer, self.streamHandler, self.liveController + ) result = calibration_dialog.exec_() if result == QDialog.Rejected: # If the dialog was closed without adding a new format, revert to the previous selection @@ -6148,8 +6470,8 @@ def wellplateChanged(self, index): def setWellplateSettings(self, wellplate_format): if wellplate_format in WELLPLATE_FORMAT_SETTINGS: settings = WELLPLATE_FORMAT_SETTINGS[wellplate_format] - elif wellplate_format == 'glass slide': - self.signalWellplateSettings.emit(QVariant('glass slide'), 0, 0, 0, 0, 0, 0, 0, 1, 1) + elif wellplate_format == "glass slide": + self.signalWellplateSettings.emit(QVariant("glass slide"), 0, 0, 0, 0, 0, 0, 0, 1, 1) return else: print(f"Wellplate format {wellplate_format} not recognized") @@ -6157,32 +6479,32 @@ def setWellplateSettings(self, wellplate_format): self.signalWellplateSettings.emit( QVariant(wellplate_format), - settings['a1_x_mm'], - settings['a1_y_mm'], - settings['a1_x_pixel'], - settings['a1_y_pixel'], - settings['well_size_mm'], - settings['well_spacing_mm'], - settings['number_of_skip'], - settings['rows'], - settings['cols'] + settings["a1_x_mm"], + settings["a1_y_mm"], + settings["a1_x_pixel"], + settings["a1_y_pixel"], + settings["well_size_mm"], + settings["well_spacing_mm"], + settings["number_of_skip"], + settings["rows"], + settings["cols"], ) def getWellplateSettings(self, wellplate_format): if wellplate_format in WELLPLATE_FORMAT_SETTINGS: settings = WELLPLATE_FORMAT_SETTINGS[wellplate_format] - elif wellplate_format == 'glass slide': + elif wellplate_format == "glass slide": settings = { - 'format': 'glass slide', - 'a1_x_mm': 0, - 'a1_y_mm': 0, - 'a1_x_pixel': 0, - 'a1_y_pixel': 0, - 'well_size_mm': 0, - 'well_spacing_mm': 0, - 'number_of_skip': 0, - 'rows': 1, - 'cols': 1 + "format": "glass slide", + "a1_x_mm": 0, + "a1_y_mm": 0, + "a1_x_pixel": 0, + "a1_y_pixel": 0, + "well_size_mm": 0, + "well_spacing_mm": 0, + "number_of_skip": 0, + "rows": 1, + "cols": 1, } else: return None @@ -6197,28 +6519,39 @@ def add_custom_format(self, name, settings): self.wellplateChanged(index) def save_formats_to_csv(self): - cache_path = os.path.join('cache', self.csv_path) - os.makedirs('cache', exist_ok=True) - - fieldnames = ['format', 'a1_x_mm', 'a1_y_mm', 'a1_x_pixel', 'a1_y_pixel', 'well_size_mm', 'well_spacing_mm', 'number_of_skip', 'rows', 'cols'] - with open(cache_path, 'w', newline='') as csvfile: + cache_path = os.path.join("cache", self.csv_path) + os.makedirs("cache", exist_ok=True) + + fieldnames = [ + "format", + "a1_x_mm", + "a1_y_mm", + "a1_x_pixel", + "a1_y_pixel", + "well_size_mm", + "well_spacing_mm", + "number_of_skip", + "rows", + "cols", + ] + with open(cache_path, "w", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for format_, settings in WELLPLATE_FORMAT_SETTINGS.items(): - writer.writerow({**{'format': format_}, **settings}) + writer.writerow({**{"format": format_}, **settings}) @staticmethod def parse_csv_row(row): return { - 'a1_x_mm': float(row['a1_x_mm']), - 'a1_y_mm': float(row['a1_y_mm']), - 'a1_x_pixel': int(row['a1_x_pixel']), - 'a1_y_pixel': int(row['a1_y_pixel']), - 'well_size_mm': float(row['well_size_mm']), - 'well_spacing_mm': float(row['well_spacing_mm']), - 'number_of_skip': int(row['number_of_skip']), - 'rows': int(row['rows']), - 'cols': int(row['cols']) + "a1_x_mm": float(row["a1_x_mm"]), + "a1_y_mm": float(row["a1_y_mm"]), + "a1_x_pixel": int(row["a1_x_pixel"]), + "a1_y_pixel": int(row["a1_y_pixel"]), + "well_size_mm": float(row["well_size_mm"]), + "well_spacing_mm": float(row["well_spacing_mm"]), + "number_of_skip": int(row["number_of_skip"]), + "rows": int(row["rows"]), + "cols": int(row["cols"]), } @@ -6234,7 +6567,7 @@ def __init__(self, wellplateFormatWidget, stage: AbstractStage, navigationViewer self.liveController = liveController self.was_live = self.liveController.is_live self.corners = [None, None, None] - self.show_virtual_joystick = True # FLAG + self.show_virtual_joystick = True # FLAG self.initUI() # Initially allow click-to-move and hide the joystick controls self.clickToMoveCheckbox.setChecked(True) @@ -6287,13 +6620,13 @@ def initUI(self): self.plateWidthInput = QDoubleSpinBox(self) self.plateWidthInput.setRange(10, 500) # Adjust range as needed self.plateWidthInput.setValue(127.76) # Default value for a standard 96-well plate - self.plateWidthInput.setSuffix(' mm') + self.plateWidthInput.setSuffix(" mm") self.form_layout.addRow("Plate Width:", self.plateWidthInput) self.plateHeightInput = QDoubleSpinBox(self) self.plateHeightInput.setRange(10, 500) # Adjust range as needed self.plateHeightInput.setValue(85.48) # Default value for a standard 96-well plate - self.plateHeightInput.setSuffix(' mm') + self.plateHeightInput.setSuffix(" mm") self.form_layout.addRow("Plate Height:", self.plateHeightInput) self.wellSpacingInput = QDoubleSpinBox(self) @@ -6301,7 +6634,7 @@ def initUI(self): self.wellSpacingInput.setValue(9) self.wellSpacingInput.setSingleStep(0.1) self.wellSpacingInput.setDecimals(2) - self.wellSpacingInput.setSuffix(' mm') + self.wellSpacingInput.setSuffix(" mm") self.form_layout.addRow("Well Spacing:", self.wellSpacingInput) left_layout.addLayout(self.form_layout) @@ -6317,13 +6650,13 @@ def initUI(self): label = QLabel(f"Point {i}: N/A") button = QPushButton("Set Point") button.setFixedWidth(button.sizeHint().width()) - button.clicked.connect(lambda checked, index=i-1: self.setCorner(index)) + button.clicked.connect(lambda checked, index=i - 1: self.setCorner(index)) points_layout.addWidget(label, i, 0) points_layout.addWidget(button, i, 1) self.cornerLabels.append(label) self.setPointButtons.append(button) - points_layout.setColumnStretch(0,1) + points_layout.setColumnStretch(0, 1) left_layout.addLayout(points_layout) # Add 'Click to Move' checkbox @@ -6394,22 +6727,30 @@ def toggleVirtualJoystick(self, state): self.joystick.show() self.sensitivitySlider.show() self.right_layout.itemAt(self.right_layout.indexOf(self.joystick)).widget().show() - self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt(0).widget().show() # Show sensitivity label - self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt(1).widget().show() # Show sensitivity slider + self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt( + 0 + ).widget().show() # Show sensitivity label + self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt( + 1 + ).widget().show() # Show sensitivity slider else: self.joystick.hide() self.sensitivitySlider.hide() self.right_layout.itemAt(self.right_layout.indexOf(self.joystick)).widget().hide() - self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt(0).widget().hide() # Hide sensitivity label - self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt(1).widget().hide() # Hide sensitivity slider + self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt( + 0 + ).widget().hide() # Hide sensitivity label + self.right_layout.itemAt(self.right_layout.count() - 1).layout().itemAt( + 1 + ).widget().hide() # Hide sensitivity slider def moveStage(self, x, y): sensitivity = self.sensitivitySlider.value() / 50.0 # Normalize to 0-2 range max_speed = 0.1 * sensitivity exponent = 2 - dx = math.copysign(max_speed * abs(x)**exponent, x) - dy = math.copysign(max_speed * abs(y)**exponent, y) + dx = math.copysign(max_speed * abs(x) ** exponent, x) + dy = math.copysign(max_speed * abs(y) ** exponent, y) self.stage.move_x(dx) self.stage.move_y(dy) @@ -6438,7 +6779,11 @@ def setCorner(self, index): # Check if the new point is different from existing points if any(corner is not None and np.allclose([x, y], corner) for corner in self.corners): - QMessageBox.warning(self, "Duplicate Point", "This point is too close to an existing point. Please choose a different location.") + QMessageBox.warning( + self, + "Duplicate Point", + "This point is too close to an existing point. Please choose a different location.", + ) return self.corners[index] = (x, y) @@ -6472,7 +6817,11 @@ def calibrate(self): try: if self.new_format_radio.isChecked(): if not self.nameInput.text() or not all(self.corners): - QMessageBox.warning(self, "Incomplete Information", "Please fill in all fields and set 3 corner points before calibrating.") + QMessageBox.warning( + self, + "Incomplete Information", + "Please fill in all fields and set 3 corner points before calibrating.", + ) return name = self.nameInput.text() @@ -6490,15 +6839,15 @@ def calibrate(self): a1_y_pixel = round(a1_y_mm * scale) new_format = { - 'a1_x_mm': a1_x_mm, - 'a1_y_mm': a1_y_mm, - 'a1_x_pixel': a1_x_pixel, - 'a1_y_pixel': a1_y_pixel, - 'well_size_mm': well_size_mm, - 'well_spacing_mm': well_spacing_mm, - 'number_of_skip': 0, - 'rows': rows, - 'cols': cols, + "a1_x_mm": a1_x_mm, + "a1_y_mm": a1_y_mm, + "a1_x_pixel": a1_x_pixel, + "a1_y_pixel": a1_y_pixel, + "well_size_mm": well_size_mm, + "well_spacing_mm": well_spacing_mm, + "number_of_skip": 0, + "rows": rows, + "cols": cols, } self.wellplateFormatWidget.add_custom_format(name, new_format) @@ -6510,7 +6859,9 @@ def calibrate(self): else: selected_format = self.existing_format_combo.currentData() if not all(self.corners): - QMessageBox.warning(self, "Incomplete Information", "Please set 3 corner points before calibrating.") + QMessageBox.warning( + self, "Incomplete Information", "Please set 3 corner points before calibrating." + ) return center, radius = self.calculate_circle(self.corners) @@ -6521,13 +6872,15 @@ def calibrate(self): existing_settings = WELLPLATE_FORMAT_SETTINGS[selected_format] print(f"Updating existing format {selected_format} well plate") - print(f"OLD: 'a1_x_mm': {existing_settings['a1_x_mm']}, 'a1_y_mm': {existing_settings['a1_y_mm']}, 'well_size_mm': {existing_settings['well_size_mm']}") + print( + f"OLD: 'a1_x_mm': {existing_settings['a1_x_mm']}, 'a1_y_mm': {existing_settings['a1_y_mm']}, 'well_size_mm': {existing_settings['well_size_mm']}" + ) print(f"NEW: 'a1_x_mm': {a1_x_mm}, 'a1_y_mm': {a1_y_mm}, 'well_size_mm': {well_size_mm}") updated_settings = { - 'a1_x_mm': a1_x_mm, - 'a1_y_mm': a1_y_mm, - 'well_size_mm': well_size_mm, + "a1_x_mm": a1_x_mm, + "a1_y_mm": a1_y_mm, + "well_size_mm": well_size_mm, } WELLPLATE_FORMAT_SETTINGS[selected_format].update(updated_settings) @@ -6538,7 +6891,9 @@ def calibrate(self): # Update the WellplateFormatWidget's combo box to reflect the newly calibrated format self.wellplateFormatWidget.populate_combo_box() - index = self.wellplateFormatWidget.comboBox.findData(selected_format if self.calibrate_format_radio.isChecked() else name) + index = self.wellplateFormatWidget.comboBox.findData( + selected_format if self.calibrate_format_radio.isChecked() else name + ) if index >= 0: self.wellplateFormatWidget.comboBox.setCurrentIndex(index) @@ -6552,30 +6907,31 @@ def calibrate(self): def create_wellplate_image(self, name, format_data, plate_width_mm, plate_height_mm): scale = 1 / 0.084665 + def mm_to_px(mm): return round(mm * scale) width = mm_to_px(plate_width_mm) height = mm_to_px(plate_height_mm) - image = Image.new('RGB', (width, height), color='white') + image = Image.new("RGB", (width, height), color="white") draw = ImageDraw.Draw(image) - rows, cols = format_data['rows'], format_data['cols'] - well_spacing_mm = format_data['well_spacing_mm'] - well_size_mm = format_data['well_size_mm'] - a1_x_mm, a1_y_mm = format_data['a1_x_mm'], format_data['a1_y_mm'] + rows, cols = format_data["rows"], format_data["cols"] + well_spacing_mm = format_data["well_spacing_mm"] + well_size_mm = format_data["well_size_mm"] + a1_x_mm, a1_y_mm = format_data["a1_x_mm"], format_data["a1_y_mm"] - def draw_left_slanted_rectangle(draw, xy, slant, width=4, outline='black', fill=None): + def draw_left_slanted_rectangle(draw, xy, slant, width=4, outline="black", fill=None): x1, y1, x2, y2 = xy # Define the polygon points points = [ (x1 + slant, y1), # Top-left after slant - (x2, y1), # Top-right - (x2, y2), # Bottom-right + (x2, y1), # Top-right + (x2, y2), # Bottom-right (x1 + slant, y2), # Bottom-left after slant (x1, y2 - slant), # Bottom of left slant - (x1, y1 + slant) # Top of left slant + (x1, y1 + slant), # Top of left slant ] # Draw the filled polygon with outline @@ -6583,19 +6939,21 @@ def draw_left_slanted_rectangle(draw, xy, slant, width=4, outline='black', fill= # Draw the outer rectangle with rounded corners corner_radius = 20 - draw.rounded_rectangle([0, 0, width-1, height-1], radius=corner_radius, outline='black', width=4, fill='grey') + draw.rounded_rectangle( + [0, 0, width - 1, height - 1], radius=corner_radius, outline="black", width=4, fill="grey" + ) # Draw the inner rectangle with left slanted corners margin = 20 slant = 40 - draw_left_slanted_rectangle(draw, - [margin, margin, width-margin, height-margin], - slant, width=4, outline='black', fill='lightgrey') + draw_left_slanted_rectangle( + draw, [margin, margin, width - margin, height - margin], slant, width=4, outline="black", fill="lightgrey" + ) # Function to draw a circle def draw_circle(x, y, diameter): radius = diameter / 2 - draw.ellipse([x-radius, y-radius, x+radius, y+radius], outline='black', width=4, fill='white') + draw.ellipse([x - radius, y - radius, x + radius, y + radius], outline="black", width=4, fill="white") # Draw the wells for row in range(rows): @@ -6612,23 +6970,23 @@ def draw_circle(x, y, diameter): for col in range(cols): label = str(col + 1) x = mm_to_px(a1_x_mm + col * well_spacing_mm) - y = mm_to_px((a1_y_mm - well_size_mm/2) / 2) + y = mm_to_px((a1_y_mm - well_size_mm / 2) / 2) bbox = font.getbbox(label) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] - draw.text((x - text_width/2, y), label, fill="black", font=font) + draw.text((x - text_width / 2, y), label, fill="black", font=font) # Add row labels for row in range(rows): label = chr(65 + row) if row < 26 else chr(65 + row // 26 - 1) + chr(65 + row % 26) - x = mm_to_px((a1_x_mm - well_size_mm/2 ) / 2) + x = mm_to_px((a1_x_mm - well_size_mm / 2) / 2) y = mm_to_px(a1_y_mm + row * well_spacing_mm) bbox = font.getbbox(label) text_height = bbox[3] - bbox[1] text_width = bbox[2] - bbox[0] - draw.text((x + 20 - text_width/2, y - text_height + 1), label, fill="black", font=font) + draw.text((x + 20 - text_width / 2, y - text_height + 1), label, fill="black", font=font) - image_path = os.path.join('images', f'{name.replace(" ", "_")}.png') + image_path = os.path.join("images", f'{name.replace(" ", "_")}.png') image.save(image_path) print(f"Wellplate image saved as {image_path}") return image_path @@ -6667,14 +7025,14 @@ def reject(self): sample = self.navigationViewer.sample # Convert sample string to format int - if 'glass slide' in sample: - sample_format = 'glass slide' + if "glass slide" in sample: + sample_format = "glass slide" else: try: sample_format = int(sample.split()[0]) except (ValueError, IndexError): print(f"Unable to parse sample format from '{sample}'. Defaulting to 0.") - sample_format = 'glass slide' + sample_format = "glass slide" # Set dropdown to the current sample format index = self.wellplateFormatWidget.comboBox.findData(sample_format) @@ -6763,11 +7121,15 @@ def display_image(self, image): # Step 6: Always center the view around the image center (for seamless transitions) else: self.viewbox.setRange( - xRange=(image_center_x - current_view_range.width() / 2, - image_center_x + current_view_range.width() / 2), - yRange=(image_center_y - current_view_range.height() / 2, - image_center_y + current_view_range.height() / 2), - padding=0 + xRange=( + image_center_x - current_view_range.width() / 2, + image_center_x + current_view_range.width() / 2, + ), + yRange=( + image_center_y - current_view_range.height() / 2, + image_center_y + current_view_range.height() / 2, + ), + padding=0, ) # Step 7: Ensure the crosshair is updated @@ -6778,7 +7140,7 @@ def display_image(self, image): def onMouseClicked(self, event): # Map the scene position to view position - if event.double(): # double click to move + if event.double(): # double click to move pos = event.pos() scene_pos = self.viewbox.mapSceneToView(pos) @@ -6904,15 +7266,15 @@ def __init__(self, format_, wellplateFormatWidget, *args, **kwargs): def setFormat(self, format_): self.format = format_ settings = self.wellplateFormatWidget.getWellplateSettings(self.format) - self.rows = settings['rows'] - self.columns = settings['cols'] - self.spacing_mm = settings['well_spacing_mm'] - self.number_of_skip = settings['number_of_skip'] - self.a1_x_mm = settings['a1_x_mm'] - self.a1_y_mm = settings['a1_y_mm'] - self.a1_x_pixel = settings['a1_x_pixel'] - self.a1_y_pixel = settings['a1_y_pixel'] - self.well_size_mm = settings['well_size_mm'] + self.rows = settings["rows"] + self.columns = settings["cols"] + self.spacing_mm = settings["well_spacing_mm"] + self.number_of_skip = settings["number_of_skip"] + self.a1_x_mm = settings["a1_x_mm"] + self.a1_y_mm = settings["a1_y_mm"] + self.a1_x_pixel = settings["a1_x_pixel"] + self.a1_y_pixel = settings["a1_y_pixel"] + self.well_size_mm = settings["well_size_mm"] self.setRowCount(self.rows) self.setColumnCount(self.columns) @@ -6933,7 +7295,7 @@ def initUI(self): self.setDragDropOverwriteMode(False) self.setMouseTracking(False) - if self.format == '1536 well plate': + if self.format == "1536 well plate": font = QFont() font.setPointSize(6) # You can adjust this value as needed else: @@ -6993,19 +7355,23 @@ def setData(self): for i in range(self.number_of_skip): for j in range(self.columns): # Apply to rows self.item(i, j).setFlags(self.item(i, j).flags() & ~Qt.ItemIsSelectable) - self.item(self.rows - 1 - i, j).setFlags(self.item(self.rows - 1 - i, j).flags() & ~Qt.ItemIsSelectable) + self.item(self.rows - 1 - i, j).setFlags( + self.item(self.rows - 1 - i, j).flags() & ~Qt.ItemIsSelectable + ) for k in range(self.rows): # Apply to columns self.item(k, i).setFlags(self.item(k, i).flags() & ~Qt.ItemIsSelectable) - self.item(k, self.columns - 1 - i).setFlags(self.item(k, self.columns - 1 - i).flags() & ~Qt.ItemIsSelectable) + self.item(k, self.columns - 1 - i).setFlags( + self.item(k, self.columns - 1 - i).flags() & ~Qt.ItemIsSelectable + ) # Update row headers row_headers = [] for i in range(self.rows): if i < 26: - label = chr(ord('A') + i) + label = chr(ord("A") + i) else: - first_letter = chr(ord('A') + (i // 26) - 1) - second_letter = chr(ord('A') + (i % 26)) + first_letter = chr(ord("A") + (i // 26) - 1) + second_letter = chr(ord("A") + (i % 26)) label = first_letter + second_letter row_headers.append(label) self.setVerticalHeaderLabels(row_headers) @@ -7015,38 +7381,43 @@ def setData(self): def onDoubleClick(self, row, col): print("double click well", row, col) - if (row >= 0 + self.number_of_skip and row <= self.rows-1-self.number_of_skip ) and ( col >= 0 + self.number_of_skip and col <= self.columns-1-self.number_of_skip ): - x_mm = col*self.spacing_mm + self.a1_x_mm + WELLPLATE_OFFSET_X_mm - y_mm = row*self.spacing_mm + self.a1_y_mm + WELLPLATE_OFFSET_Y_mm - self.signal_wellSelectedPos.emit(x_mm,y_mm) - print("well location:", (x_mm,y_mm)) + if (row >= 0 + self.number_of_skip and row <= self.rows - 1 - self.number_of_skip) and ( + col >= 0 + self.number_of_skip and col <= self.columns - 1 - self.number_of_skip + ): + x_mm = col * self.spacing_mm + self.a1_x_mm + WELLPLATE_OFFSET_X_mm + y_mm = row * self.spacing_mm + self.a1_y_mm + WELLPLATE_OFFSET_Y_mm + self.signal_wellSelectedPos.emit(x_mm, y_mm) + print("well location:", (x_mm, y_mm)) self.signal_wellSelected.emit(True) else: self.signal_wellSelected.emit(False) - def onSingleClick(self,row,col): + def onSingleClick(self, row, col): print("single click well", row, col) - if (row >= 0 + self.number_of_skip and row <= self.rows-1-self.number_of_skip ) and ( col >= 0 + self.number_of_skip and col <= self.columns-1-self.number_of_skip ): + if (row >= 0 + self.number_of_skip and row <= self.rows - 1 - self.number_of_skip) and ( + col >= 0 + self.number_of_skip and col <= self.columns - 1 - self.number_of_skip + ): self.signal_wellSelected.emit(True) else: self.signal_wellSelected.emit(False) def onSelectionChanged(self): # Check if there are any selected indexes before proceeding - if self.format != 'glass slide': + if self.format != "glass slide": has_selection = bool(self.selectedIndexes()) self.signal_wellSelected.emit(has_selection) def get_selected_cells(self): list_of_selected_cells = [] print("getting selected cells...") - if self.format == 'glass slide': + if self.format == "glass slide": return list_of_selected_cells for index in self.selectedIndexes(): row, col = index.row(), index.column() # Check if the cell is within the allowed bounds - if (row >= 0 + self.number_of_skip and row <= self.rows - 1 - self.number_of_skip) and \ - (col >= 0 + self.number_of_skip and col <= self.columns - 1 - self.number_of_skip): + if (row >= 0 + self.number_of_skip and row <= self.rows - 1 - self.number_of_skip) and ( + col >= 0 + self.number_of_skip and col <= self.columns - 1 - self.number_of_skip + ): list_of_selected_cells.append((row, col)) if list_of_selected_cells: print("cells:", list_of_selected_cells) @@ -7084,11 +7455,11 @@ def set_white_boundaries_style(self): class Well1536SelectionWidget(QWidget): signal_wellSelected = Signal(bool) - signal_wellSelectedPos = Signal(float,float) + signal_wellSelectedPos = Signal(float, float) def __init__(self): super().__init__() - self.format = '1536 well plate' + self.format = "1536 well plate" self.selected_cells = {} # Dictionary to keep track of selected cells and their colors self.current_cell = None # To track the current (green) cell self.rows = 32 @@ -7096,14 +7467,14 @@ def __init__(self): self.spacing_mm = 2.25 self.number_of_skip = 0 self.well_size_mm = 1.5 - self.a1_x_mm = 11.0 # measured stage position - to update - self.a1_y_mm = 7.86 # measured stage position - to update - self.a1_x_pixel = 144 # coordinate on the png - to update - self.a1_y_pixel = 108 # coordinate on the png - to update + self.a1_x_mm = 11.0 # measured stage position - to update + self.a1_y_mm = 7.86 # measured stage position - to update + self.a1_x_pixel = 144 # coordinate on the png - to update + self.a1_y_pixel = 108 # coordinate on the png - to update self.initUI() def initUI(self): - self.setWindowTitle('1536 Well Plate') + self.setWindowTitle("1536 Well Plate") self.setGeometry(100, 100, 750, 400) # Increased width to accommodate controls self.a = 11 @@ -7111,7 +7482,7 @@ def initUI(self): image_height = 32 * self.a self.image = QPixmap(image_width, image_height) - self.image.fill(QColor('white')) + self.image.fill(QColor("white")) self.label = QLabel() self.label.setPixmap(self.image) self.label.setFixedSize(image_width, image_height) @@ -7119,18 +7490,18 @@ def initUI(self): self.cell_input = QLineEdit(self) self.cell_input.setPlaceholderText("e.g. AE12 or B4") - go_button = QPushButton('Go to well', self) + go_button = QPushButton("Go to well", self) go_button.clicked.connect(self.go_to_cell) self.selection_input = QLineEdit(self) self.selection_input.setPlaceholderText("e.g. A1:E48, X1, AC24, Z2:AF6, ...") self.selection_input.editingFinished.connect(self.select_cells) # Create navigation buttons - up_button = QPushButton('↑', self) - left_button = QPushButton('←', self) - right_button = QPushButton('→', self) - down_button = QPushButton('↓', self) - add_button = QPushButton('Select', self) + up_button = QPushButton("↑", self) + left_button = QPushButton("←", self) + right_button = QPushButton("→", self) + down_button = QPushButton("↓", self) + add_button = QPushButton("Select", self) # Connect navigation buttons to their respective functions up_button.clicked.connect(self.move_up) @@ -7213,7 +7584,7 @@ def add_current_well(self): print(f"Removed well {cell_name}") else: # If the well is not selected, add it - self.selected_cells[(row, col)] = '#1f77b4' # Add to selected cells with blue color + self.selected_cells[(row, col)] = "#1f77b4" # Add to selected cells with blue color self.add_well_to_selection_input(cell_name) print(f"Added well {cell_name}") @@ -7229,10 +7600,10 @@ def add_well_to_selection_input(self, cell_name): def remove_well_from_selection_input(self, cell_name): current_selection = self.selection_input.text() - cells = [cell.strip() for cell in current_selection.split(',')] + cells = [cell.strip() for cell in current_selection.split(",")] if cell_name in cells: cells.remove(cell_name) - self.selection_input.setText(', '.join(cells)) + self.selection_input.setText(", ".join(cells)) def update_current_cell(self): self.redraw_wells() @@ -7249,9 +7620,9 @@ def update_current_cell(self): self.signal_wellSelectedPos.emit(x_mm, y_mm) def redraw_wells(self): - self.image.fill(QColor('white')) # Clear the pixmap first + self.image.fill(QColor("white")) # Clear the pixmap first painter = QPainter(self.image) - painter.setPen(QColor('white')) + painter.setPen(QColor("white")) # Draw selected cells in red for (row, col), color in self.selected_cells.items(): painter.setBrush(QColor(color)) @@ -7259,15 +7630,15 @@ def redraw_wells(self): # Draw current cell in green if self.current_cell: painter.setBrush(Qt.NoBrush) # No fill - painter.setPen(QPen(QColor('red'), 2)) # Red outline, 2 pixels wide + painter.setPen(QPen(QColor("red"), 2)) # Red outline, 2 pixels wide row, col = self.current_cell - painter.drawRect(col * self.a+2, row * self.a+2, self.a-3, self.a-3) + painter.drawRect(col * self.a + 2, row * self.a + 2, self.a - 3, self.a - 3) painter.end() self.label.setPixmap(self.image) def go_to_cell(self): cell_desc = self.cell_input.text().strip() - match = re.match(r'([A-Za-z]+)(\d+)', cell_desc) + match = re.match(r"([A-Za-z]+)(\d+)", cell_desc) if match: row_part, col_part = match.groups() row_index = self.row_to_index(row_part) @@ -7276,14 +7647,14 @@ def go_to_cell(self): self.redraw_wells() # Redraw with the new current cell x_mm = col_index * self.spacing_mm + self.a1_x_mm + WELLPLATE_OFFSET_X_mm y_mm = row_index * self.spacing_mm + self.a1_y_mm + WELLPLATE_OFFSET_Y_mm - self.signal_wellSelectedPos.emit(x_mm,y_mm) + self.signal_wellSelectedPos.emit(x_mm, y_mm) def select_cells(self): # first clear selection self.selected_cells = {} - pattern = r'([A-Za-z]+)(\d+):?([A-Za-z]*)(\d*)' - cell_descriptions = self.selection_input.text().split(',') + pattern = r"([A-Za-z]+)(\d+):?([A-Za-z]*)(\d*)" + cell_descriptions = self.selection_input.text().split(",") for desc in cell_descriptions: match = re.match(pattern, desc.strip()) if match: @@ -7296,9 +7667,9 @@ def select_cells(self): end_col_index = int(end_col) - 1 for row in range(min(start_row_index, end_row_index), max(start_row_index, end_row_index) + 1): for col in range(min(start_col_index, end_col_index), max(start_col_index, end_col_index) + 1): - self.selected_cells[(row, col)] = '#1f77b4' + self.selected_cells[(row, col)] = "#1f77b4" else: # It's a single cell - self.selected_cells[(start_row_index, start_col_index)] = '#1f77b4' + self.selected_cells[(start_row_index, start_col_index)] = "#1f77b4" self.redraw_wells() if self.selected_cells: self.signal_wellSelected.emit(True) @@ -7306,7 +7677,7 @@ def select_cells(self): def row_to_index(self, row): index = 0 for char in row: - index = index * 26 + (ord(char.upper()) - ord('A') + 1) + index = index * 26 + (ord(char.upper()) - ord("A") + 1) return index - 1 def onSelectionChanged(self): @@ -7314,11 +7685,11 @@ def onSelectionChanged(self): def get_selected_cells(self): list_of_selected_cells = list(self.selected_cells.keys()) - return(list_of_selected_cells) + return list_of_selected_cells class LedMatrixSettingsDialog(QDialog): - def __init__(self,led_array): + def __init__(self, led_array): self.led_array = led_array super().__init__() self.setWindowTitle("LED Matrix Settings") @@ -7371,13 +7742,13 @@ def __init__(self, ObjectivesWidget, WellplateFormatWidget, *args, **kwargs): def save_settings(self): """Save current objective and wellplate format to cache""" - os.makedirs('cache', exist_ok=True) + os.makedirs("cache", exist_ok=True) data = { - 'objective': self.objectivesWidget.dropdown.currentText(), - 'wellplate_format': self.wellplateFormatWidget.wellplate_format + "objective": self.objectivesWidget.dropdown.currentText(), + "wellplate_format": self.wellplateFormatWidget.wellplate_format, } - with open('cache/objective_and_sample_format.txt', 'w') as f: + with open("cache/objective_and_sample_format.txt", "w") as f: json.dump(data, f) @@ -7401,7 +7772,7 @@ def add_components(self): # Layout for the editText, label, and button self.edit_text = QLineEdit(self) self.edit_text.setMaxLength(1) # Restrict to one character - self.edit_text.setText(f'{SQUID_FILTERWHEEL_MIN_INDEX}') + self.edit_text.setText(f"{SQUID_FILTERWHEEL_MIN_INDEX}") move_to_pos_label = QLabel("move to pos.", self) self.move_spin_btn = QPushButton("Move To", self) @@ -7447,7 +7818,9 @@ def decrement_position(self): def increment_position(self): current_position = int(self.position_label.text().split(": ")[1]) - new_position = min(SQUID_FILTERWHEEL_MAX_INDEX, current_position + 1) # Ensure position doesn't go above SQUID_FILTERWHEEL_MAX_INDEX + new_position = min( + SQUID_FILTERWHEEL_MAX_INDEX, current_position + 1 + ) # Ensure position doesn't go above SQUID_FILTERWHEEL_MAX_INDEX if current_position != new_position: self.microscope.squid_filter_wheel.next_position() self.update_position(new_position) @@ -7460,7 +7833,7 @@ def home_position(self): def move_to_position(self): try: position = int(self.edit_text.text()) - if position in range(SQUID_FILTERWHEEL_MIN_INDEX, SQUID_FILTERWHEEL_MAX_INDEX + 1): + if position in range(SQUID_FILTERWHEEL_MIN_INDEX, SQUID_FILTERWHEEL_MAX_INDEX + 1): if position != int(self.position_label.text().split(": ")[1]): self.microscope.squid_filter_wheel.set_emission(position) self.update_position(position) diff --git a/software/control/widgets_usbspectrometer.py b/software/control/widgets_usbspectrometer.py index 3856f60c9..358348eb6 100644 --- a/software/control/widgets_usbspectrometer.py +++ b/software/control/widgets_usbspectrometer.py @@ -1,5 +1,6 @@ # set QT_API environment variable -import os +import os + os.environ["QT_API"] = "pyqt5" import qtpy @@ -11,6 +12,7 @@ from datetime import datetime from control._def import * + class SpectrometerControlWidget(QFrame): signal_newExposureTime = Signal(float) @@ -31,8 +33,8 @@ def add_components(self): # line 3: exposure time and analog gain associated with the current mode self.entry_exposureTime = QDoubleSpinBox() - self.entry_exposureTime.setMinimum(0.001) - self.entry_exposureTime.setMaximum(5000) + self.entry_exposureTime.setMinimum(0.001) + self.entry_exposureTime.setMaximum(5000) self.entry_exposureTime.setSingleStep(1) self.entry_exposureTime.setValue(50) self.entry_exposureTime.setKeyboardTracking(False) @@ -44,8 +46,8 @@ def add_components(self): # layout grid_line2 = QHBoxLayout() - grid_line2.addWidget(QLabel('USB spectrometer')) - grid_line2.addWidget(QLabel('Integration Time (ms)')) + grid_line2.addWidget(QLabel("USB spectrometer")) + grid_line2.addWidget(QLabel("Integration Time (ms)")) grid_line2.addWidget(self.entry_exposureTime) grid_line2.addWidget(self.btn_live) @@ -54,42 +56,43 @@ def add_components(self): # self.grid.addStretch() self.setLayout(self.grid) - def toggle_live(self,pressed): + def toggle_live(self, pressed): if pressed: self.spectrometer.start_streaming() else: self.spectrometer.pause_streaming() + class RecordingWidget(QFrame): def __init__(self, streamHandler, imageSaver, main=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.imageSaver = imageSaver # for saving path control + self.imageSaver = imageSaver # for saving path control self.streamHandler = streamHandler 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 = QPushButton("Browse") self.btn_setSavingDir.setDefault(False) - self.btn_setSavingDir.setIcon(QIcon('icon/folder.png')) - + 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("Choose a base saving directory") self.lineEdit_experimentID = QLineEdit() self.entry_saveFPS = QDoubleSpinBox() - self.entry_saveFPS.setMinimum(0.02) - self.entry_saveFPS.setMaximum(1000) + self.entry_saveFPS.setMinimum(0.02) + self.entry_saveFPS.setMaximum(1000) self.entry_saveFPS.setSingleStep(1) self.entry_saveFPS.setValue(1) self.streamHandler.set_save_fps(1) self.entry_timeLimit = QSpinBox() - self.entry_timeLimit.setMinimum(-1) - self.entry_timeLimit.setMaximum(60*60*24*30) + self.entry_timeLimit.setMinimum(-1) + self.entry_timeLimit.setMaximum(60 * 60 * 24 * 30) self.entry_timeLimit.setSingleStep(1) self.entry_timeLimit.setValue(-1) @@ -99,25 +102,25 @@ def add_components(self): self.btn_record.setDefault(False) grid_line1 = QGridLayout() - grid_line1.addWidget(QLabel('Saving Path')) - grid_line1.addWidget(self.lineEdit_savingDir, 0,1) - grid_line1.addWidget(self.btn_setSavingDir, 0,2) + grid_line1.addWidget(QLabel("Saving Path")) + grid_line1.addWidget(self.lineEdit_savingDir, 0, 1) + grid_line1.addWidget(self.btn_setSavingDir, 0, 2) grid_line2 = QGridLayout() - grid_line2.addWidget(QLabel('Experiment ID'), 0,0) - grid_line2.addWidget(self.lineEdit_experimentID,0,1) + grid_line2.addWidget(QLabel("Experiment ID"), 0, 0) + grid_line2.addWidget(self.lineEdit_experimentID, 0, 1) grid_line3 = QGridLayout() - grid_line3.addWidget(QLabel('Saving FPS'), 0,0) - grid_line3.addWidget(self.entry_saveFPS, 0,1) - grid_line3.addWidget(QLabel('Time Limit (s)'), 0,2) - grid_line3.addWidget(self.entry_timeLimit, 0,3) - grid_line3.addWidget(self.btn_record, 0,4) + grid_line3.addWidget(QLabel("Saving FPS"), 0, 0) + grid_line3.addWidget(self.entry_saveFPS, 0, 1) + grid_line3.addWidget(QLabel("Time Limit (s)"), 0, 2) + grid_line3.addWidget(self.entry_timeLimit, 0, 3) + grid_line3.addWidget(self.btn_record, 0, 4) self.grid = QGridLayout() - self.grid.addLayout(grid_line1,0,0) - self.grid.addLayout(grid_line2,1,0) - self.grid.addLayout(grid_line3,2,0) + self.grid.addLayout(grid_line1, 0, 0) + self.grid.addLayout(grid_line2, 1, 0) + self.grid.addLayout(grid_line3, 2, 0) self.grid.setRowStretch(self.grid.rowCount(), 1) self.setLayout(self.grid) @@ -138,7 +141,7 @@ def set_saving_dir(self): self.lineEdit_savingDir.setText(save_dir_base) self.base_path_is_set = True - def toggle_recording(self,pressed): + def toggle_recording(self, pressed): if self.base_path_is_set == False: self.btn_record.setChecked(False) msg = QMessageBox() @@ -172,23 +175,23 @@ def __init__(self, N=1000, main=None, *args, **kwargs): self.setFrameStyle(QFrame.Panel | QFrame.Raised) def add_components(self): - self.plotWidget = PlotWidget('', add_legend=True) + self.plotWidget = PlotWidget("", add_legend=True) - layout = QGridLayout() #layout = QStackedLayout() - layout.addWidget(self.plotWidget,0,0) + layout = QGridLayout() # layout = QStackedLayout() + layout.addWidget(self.plotWidget, 0, 0) self.setLayout(layout) - def plot(self,data): - self.plotWidget.plot(data[0,:],data[1,:],clear=True) + def plot(self, data): + self.plotWidget.plot(data[0, :], data[1, :], clear=True) class PlotWidget(pg.GraphicsLayoutWidget): - - def __init__(self,title='',parent=None,add_legend=False): + + def __init__(self, title="", parent=None, add_legend=False): super().__init__(parent) self.plotWidget = self.addPlot(title=title) if add_legend: self.plotWidget.addLegend() - - def plot(self,x,y,clear=False): - self.plotWidget.plot(x,y,clear=clear) + + def plot(self, x, y, clear=False): + self.plotWidget.plot(x, y, clear=clear) diff --git a/software/main_hcs.py b/software/main_hcs.py index 3aed2f7f4..4f07f7534 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -3,6 +3,7 @@ import glob import logging import os + os.environ["QT_API"] = "pyqt5" import signal import sys @@ -12,6 +13,7 @@ from qtpy.QtGui import * import squid.logging + squid.logging.setup_uncaught_exception_logging() # app specific libraries @@ -31,10 +33,11 @@ def show_acq_config(cfm): acq_config_widget = ConfigEditorForAcquisitions(cfm) acq_config_widget.exec_() + if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--simulation", help="Run the GUI with simulated hardware.", action='store_true') - parser.add_argument("--live-only", help="Run the GUI only the live viewer.", action='store_true') + parser.add_argument("--simulation", help="Run the GUI with simulated hardware.", action="store_true") + parser.add_argument("--live-only", help="Run the GUI only the live viewer.", action="store_true") parser.add_argument("--verbose", help="Turn on verbose logging (DEBUG level)", action="store_true") args = parser.parse_args() @@ -50,34 +53,34 @@ def show_acq_config(cfm): legacy_config = False cf_editor_parser = ConfigParser() - config_files = glob.glob('.' + '/' + 'configuration*.ini') + config_files = glob.glob("." + "/" + "configuration*.ini") if config_files: cf_editor_parser.read(CACHED_CONFIG_FILE_PATH) else: - log.error('configuration*.ini file not found, defaulting to legacy configuration') + log.error("configuration*.ini file not found, defaulting to legacy configuration") legacy_config = True app = QApplication([]) - app.setStyle('Fusion') + app.setStyle("Fusion") # This allows shutdown via ctrl+C even after the gui has popped up. signal.signal(signal.SIGINT, signal.SIG_DFL) win = gui.HighContentScreeningGui(is_simulation=args.simulation, live_only_mode=args.live_only) acq_config_action = QAction("Acquisition Settings", win) - acq_config_action.triggered.connect(lambda : show_acq_config(win.configurationManager)) + acq_config_action.triggered.connect(lambda: show_acq_config(win.configurationManager)) file_menu = QMenu("File", win) file_menu.addAction(acq_config_action) if not legacy_config: config_action = QAction("Microscope Settings", win) - config_action.triggered.connect(lambda : show_config(cf_editor_parser, config_files[0], win)) + config_action.triggered.connect(lambda: show_config(cf_editor_parser, config_files[0], win)) file_menu.addAction(config_action) try: csw = win.cswWindow if csw is not None: - csw_action = QAction("Camera Settings",win) + csw_action = QAction("Camera Settings", win) csw_action.triggered.connect(csw.show) file_menu.addAction(csw_action) except AttributeError: @@ -96,9 +99,7 @@ def show_acq_config(cfm): menu_bar.addMenu(file_menu) win.show() - console_locals = { - 'microscope': win.microscope - } + console_locals = {"microscope": win.microscope} console_thread = ConsoleThread(console_locals) console_thread.start() diff --git a/software/pyproject.toml b/software/pyproject.toml new file mode 100644 index 000000000..514e6e476 --- /dev/null +++ b/software/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +line-length = 120 +target-version = ["py34", "py35", "py36", "py38", "py39", "py310", "py311", "py312"] +extend-exclude = ''' +( +^/drivers and libraries/ +| ^/control/gxipy/ +| ^/control/RCM_API.py +| ^/control/TUCam.py +| ^/control/dcam.py +| ^/control/dcamapi4.py +| ^/control/toupcam.py +| ^/control/toupcam_exceptions.py +)''' diff --git a/software/squid/abc.py b/software/squid/abc.py index c80056568..f9f25577a 100644 --- a/software/squid/abc.py +++ b/software/squid/abc.py @@ -1,9 +1,10 @@ from abc import ABC, abstractmethod from typing import Tuple + class LightSource(ABC): """Abstract base class defining the interface for different light sources.""" - + @abstractmethod def __init__(self): """Initialize the light source and establish communication.""" @@ -21,17 +22,17 @@ def initialize(self): def set_intensity_control_mode(self, mode): """ Set intensity control mode. - + Args: mode: IntensityControlMode(Enum) """ pass - + @abstractmethod def get_intensity_control_mode(self): """ Get current intensity control mode. - + Returns: IntensityControlMode(Enum) """ @@ -41,86 +42,86 @@ def get_intensity_control_mode(self): def set_shutter_control_mode(self, mode): """ Set shutter control mode. - + Args: mode: ShutterControlMode(Enum) """ pass - + @abstractmethod def get_shutter_control_mode(self): """ Get current shutter control mode. - + Returns: ShutterControlMode(Enum) """ pass - + @abstractmethod def set_shutter_state(self, channel, state): """ Turn a specific channel on or off. - + Args: channel: Channel ID state: True to turn on, False to turn off """ pass - + @abstractmethod def get_shutter_state(self, channel): """ Get the current shutter state of a specific channel. - + Args: channel: Channel ID - + Returns: bool: True if channel is on, False if off """ pass - + @abstractmethod def set_intensity(self, channel, intensity): """ Set the intensity for a specific channel. - + Args: channel: Channel ID intensity: Intensity value (0-100) """ pass - + @abstractmethod def get_intensity(self, channel) -> float: """ Get the current intensity of a specific channel. - + Args: channel: Channel ID - + Returns: float: Current intensity value """ pass - + @abstractmethod def get_intensity_range(self) -> Tuple[float, float]: """ Get the valid intensity range. - + Returns: Tuple[float, float]: (minimum intensity, maximum intensity) """ pass - @abstractmethod def shut_down(self): """Safely shut down the light source.""" pass - + + import abc import time from typing import Optional @@ -139,36 +140,38 @@ class Pos(pydantic.BaseModel): # NOTE/TODO(imo): If essentially none of our stages have a theta, this is probably fine. But If it's a mix we probably want a better way of handling the "maybe has theta" case. theta_rad: Optional[float] + class StageStage(pydantic.BaseModel): busy: bool + class AbstractStage(metaclass=abc.ABCMeta): def __init__(self, stage_config: StageConfig): self._config = stage_config self._log = squid.logging.get_logger(self.__class__.__name__) @abc.abstractmethod - def move_x(self, rel_mm: float, blocking: bool=True): + def move_x(self, rel_mm: float, blocking: bool = True): pass @abc.abstractmethod - def move_y(self, rel_mm: float, blocking: bool=True): + def move_y(self, rel_mm: float, blocking: bool = True): pass @abc.abstractmethod - def move_z(self, rel_mm: float, blocking: bool=True): + def move_z(self, rel_mm: float, blocking: bool = True): pass @abc.abstractmethod - def move_x_to(self, abs_mm: float, blocking: bool=True): + def move_x_to(self, abs_mm: float, blocking: bool = True): pass @abc.abstractmethod - def move_y_to(self, abs_mm: float, blocking: bool=True): + def move_y_to(self, abs_mm: float, blocking: bool = True): pass @abc.abstractmethod - def move_z_to(self, abs_mm: float, blocking: bool=True): + def move_z_to(self, abs_mm: float, blocking: bool = True): pass # TODO(imo): We need a stop or halt or something along these lines @@ -185,23 +188,25 @@ def get_state(self) -> StageStage: pass @abc.abstractmethod - def home(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool=True): + def home(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): pass @abc.abstractmethod - def zero(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool=True): + def zero(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): pass @abc.abstractmethod - def set_limits(self, - x_pos_mm: Optional[float] = None, - x_neg_mm: Optional[float] = None, - y_pos_mm: Optional[float] = None, - y_neg_mm: Optional[float] = None, - z_pos_mm: Optional[float] = None, - z_neg_mm: Optional[float] = None, - theta_pos_rad: Optional[float] = None, - theta_neg_rad: Optional[float] = None): + def set_limits( + self, + x_pos_mm: Optional[float] = None, + x_neg_mm: Optional[float] = None, + y_pos_mm: Optional[float] = None, + y_neg_mm: Optional[float] = None, + z_pos_mm: Optional[float] = None, + z_neg_mm: Optional[float] = None, + theta_pos_rad: Optional[float] = None, + theta_neg_rad: Optional[float] = None, + ): pass def get_config(self) -> StageConfig: @@ -219,4 +224,4 @@ def wait_for_idle(self, timeout_s): error_message = f"Timed out waiting after {timeout_s:0.3f} [s]" self._log.error(error_message) - raise SquidTimeout(error_message) \ No newline at end of file + raise SquidTimeout(error_message) diff --git a/software/squid/config.py b/software/squid/config.py index 95e1473a0..b4c580683 100644 --- a/software/squid/config.py +++ b/software/squid/config.py @@ -6,16 +6,19 @@ import control._def as _def + class DirectionSign(enum.IntEnum): DIRECTION_SIGN_POSITIVE = 1 DIRECTION_SIGN_NEGATIVE = -1 + class PIDConfig(pydantic.BaseModel): ENABLED: bool P: float I: float D: float + class AxisConfig(pydantic.BaseModel): MOVEMENT_SIGN: DirectionSign USE_ENCODER: bool @@ -52,10 +55,19 @@ def convert_to_real_units(self, usteps: float): if self.USE_ENCODER: return usteps * self.MOVEMENT_SIGN.value * self.ENCODER_STEP_SIZE * self.ENCODER_SIGN.value else: - return usteps * self.MOVEMENT_SIGN.value * self.SCREW_PITCH / (self.MICROSTEPS_PER_STEP * self.FULL_STEPS_PER_REV) + return ( + usteps + * self.MOVEMENT_SIGN.value + * self.SCREW_PITCH + / (self.MICROSTEPS_PER_STEP * self.FULL_STEPS_PER_REV) + ) def convert_real_units_to_ustep(self, real_unit: float): - return round(real_unit / (self.MOVEMENT_SIGN.value * self.SCREW_PITCH / (self.MICROSTEPS_PER_STEP * self.FULL_STEPS_PER_REV))) + return round( + real_unit + / (self.MOVEMENT_SIGN.value * self.SCREW_PITCH / (self.MICROSTEPS_PER_STEP * self.FULL_STEPS_PER_REV)) + ) + class StageConfig(pydantic.BaseModel): X_AXIS: AxisConfig @@ -63,6 +75,7 @@ class StageConfig(pydantic.BaseModel): Z_AXIS: AxisConfig THETA_AXIS: AxisConfig + # NOTE(imo): This is temporary until we can just pass in instances of AxisConfig wherever we need it. Having # this getter for the temporary singleton will help with the refactor once we can get rid of it. _stage_config = StageConfig( @@ -78,7 +91,7 @@ class StageConfig(pydantic.BaseModel): MAX_ACCELERATION=_def.MAX_ACCELERATION_X_mm, MIN_POSITION=_def.SOFTWARE_POS_LIMIT.X_NEGATIVE, MAX_POSITION=_def.SOFTWARE_POS_LIMIT.X_POSITIVE, - PID=None + PID=None, ), Y_AXIS=AxisConfig( MOVEMENT_SIGN=_def.STAGE_MOVEMENT_SIGN_Y, @@ -92,7 +105,7 @@ class StageConfig(pydantic.BaseModel): MAX_ACCELERATION=_def.MAX_ACCELERATION_Y_mm, MIN_POSITION=_def.SOFTWARE_POS_LIMIT.Y_NEGATIVE, MAX_POSITION=_def.SOFTWARE_POS_LIMIT.Y_POSITIVE, - PID=None + PID=None, ), Z_AXIS=AxisConfig( MOVEMENT_SIGN=_def.STAGE_MOVEMENT_SIGN_Z, @@ -106,7 +119,7 @@ class StageConfig(pydantic.BaseModel): MAX_ACCELERATION=_def.MAX_ACCELERATION_Z_mm, MIN_POSITION=_def.SOFTWARE_POS_LIMIT.Z_NEGATIVE, MAX_POSITION=_def.SOFTWARE_POS_LIMIT.Z_POSITIVE, - PID=None + PID=None, ), THETA_AXIS=AxisConfig( MOVEMENT_SIGN=_def.STAGE_MOVEMENT_SIGN_THETA, @@ -114,18 +127,22 @@ class StageConfig(pydantic.BaseModel): ENCODER_SIGN=_def.ENCODER_POS_SIGN_THETA, ENCODER_STEP_SIZE=_def.ENCODER_STEP_SIZE_THETA, FULL_STEPS_PER_REV=_def.FULLSTEPS_PER_REV_THETA, - SCREW_PITCH=2.0*math.pi/_def.FULLSTEPS_PER_REV_THETA , + SCREW_PITCH=2.0 * math.pi / _def.FULLSTEPS_PER_REV_THETA, MICROSTEPS_PER_STEP=_def.MICROSTEPPING_DEFAULT_Y, - MAX_SPEED=2.0 * math.pi / 4, # NOTE(imo): I arbitrarily guessed this at 4 sec / rev, so it probably needs adjustment. + MAX_SPEED=2.0 + * math.pi + / 4, # NOTE(imo): I arbitrarily guessed this at 4 sec / rev, so it probably needs adjustment. MAX_ACCELERATION=_def.MAX_ACCELERATION_X_mm, MIN_POSITION=0, # NOTE(imo): Min and Max need adjusting. They are arbitrary right now! MAX_POSITION=2.0 * math.pi / 4, - PID=None - ) + PID=None, + ), ) """ Returns the StageConfig that existed at process startup. """ + + def get_stage_config(): return _stage_config diff --git a/software/squid/exceptions.py b/software/squid/exceptions.py index 9c1971f8a..bb5ef899a 100644 --- a/software/squid/exceptions.py +++ b/software/squid/exceptions.py @@ -1,5 +1,6 @@ class SquidError(RuntimeError): pass + class SquidTimeout(SquidError, TimeoutError): pass diff --git a/software/squid/logging.py b/software/squid/logging.py index fe8018191..b29b33050 100644 --- a/software/squid/logging.py +++ b/software/squid/logging.py @@ -26,16 +26,19 @@ class _CustomFormatter(py_logging.Formatter): py_logging.INFO: GRAY + FORMAT + RESET, py_logging.WARNING: YELLOW + FORMAT + RESET, py_logging.ERROR: RED + FORMAT + RESET, - py_logging.CRITICAL: BOLD_RED + FORMAT + RESET + py_logging.CRITICAL: BOLD_RED + FORMAT + RESET, } # NOTE(imo): The datetime hackery is so that we can have millisecond timestamps using a period instead # of comma. The default asctime + datefmt uses a comma. - FORMATTERS = {level: py_logging.Formatter(fmt, datefmt=_baseline_log_dateformat) for (level, fmt) in FORMATS.items()} + FORMATTERS = { + level: py_logging.Formatter(fmt, datefmt=_baseline_log_dateformat) for (level, fmt) in FORMATS.items() + } def format(self, record): return self.FORMATTERS[record.levelno].format(record) + _COLOR_STREAM_HANDLER = py_logging.StreamHandler() _COLOR_STREAM_HANDLER.setFormatter(_CustomFormatter()) @@ -60,8 +63,10 @@ def get_logger(name: Optional[str] = None) -> py_logging.Logger: return logger + log = get_logger(__name__) + def set_stdout_log_level(level): """ All squid code should use this set_stdout_log_level method, and the corresponding squid.logging.get_logger, @@ -129,7 +134,9 @@ def new_unraisable_hook(info): if call_existing_too: old_unraisable_hook(info) - logger.info(f"Registering custom excepthook, threading excepthook, and unraisable hook using handler={handler.__name__}") + logger.info( + f"Registering custom excepthook, threading excepthook, and unraisable hook using handler={handler.__name__}" + ) sys.excepthook = new_excepthook threading.excepthook = new_thread_excepthook sys.unraisablehook = new_unraisable_hook @@ -140,14 +147,17 @@ def setup_uncaught_exception_logging(): This will make sure uncaught exceptions are sent to the root squid logger as error messages. """ logger = get_logger() + def uncaught_exception_logger(exception_type: Type[BaseException], value: BaseException, tb: TracebackType): logger.exception("Uncaught Exception!", exc_info=value) register_crash_handler(uncaught_exception_logger, call_existing_too=False) + def get_default_log_directory(): return platformdirs.user_log_path(_squid_root_logger_name, "cephla") + def add_file_logging(log_filename, replace_existing=False): root_logger = get_logger() abs_path = os.path.abspath(log_filename) @@ -180,4 +190,4 @@ def add_file_logging(log_filename, replace_existing=False): if log_file_existed: new_handler.doRollover() - return True \ No newline at end of file + return True diff --git a/software/squid/stage/cephla.py b/software/squid/stage/cephla.py index 3ba18a767..90cf9948a 100644 --- a/software/squid/stage/cephla.py +++ b/software/squid/stage/cephla.py @@ -30,45 +30,54 @@ def _configure_axis(self, microcontroller_axis_number: int, axis_config: AxisCon # TODO(imo): The original navigationController had a "flip_direction" on configure_encoder, but it was unused in the implementation? self._microcontroller.configure_stage_pid( axis=microcontroller_axis_number, - transitions_per_revolution = axis_config.SCREW_PITCH / axis_config.ENCODER_STEP_SIZE) + transitions_per_revolution=axis_config.SCREW_PITCH / axis_config.ENCODER_STEP_SIZE, + ) if axis_config.PID and axis_config.PID.ENABLED: - self._microcontroller.set_pid_arguments(microcontroller_axis_number, axis_config.PID.P, axis_config.PID.I, axis_config.PID.D) + self._microcontroller.set_pid_arguments( + microcontroller_axis_number, axis_config.PID.P, axis_config.PID.I, axis_config.PID.D + ) self._microcontroller.turn_on_stage_pid(microcontroller_axis_number) def move_x(self, rel_mm: float, blocking: bool = True): self._microcontroller.move_x_usteps(self._config.X_AXIS.convert_real_units_to_ustep(rel_mm)) if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(rel_mm, self.get_config().X_AXIS.MAX_SPEED)) + self._calc_move_timeout(rel_mm, self.get_config().X_AXIS.MAX_SPEED) + ) def move_y(self, rel_mm: float, blocking: bool = True): self._microcontroller.move_y_usteps(self._config.Y_AXIS.convert_real_units_to_ustep(rel_mm)) if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(rel_mm, self.get_config().Y_AXIS.MAX_SPEED)) + self._calc_move_timeout(rel_mm, self.get_config().Y_AXIS.MAX_SPEED) + ) def move_z(self, rel_mm: float, blocking: bool = True): self._microcontroller.move_z_usteps(self._config.Z_AXIS.convert_real_units_to_ustep(rel_mm)) if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(rel_mm, self.get_config().Z_AXIS.MAX_SPEED)) + self._calc_move_timeout(rel_mm, self.get_config().Z_AXIS.MAX_SPEED) + ) def move_x_to(self, abs_mm: float, blocking: bool = True): self._microcontroller.move_x_to_usteps(self._config.X_AXIS.convert_real_units_to_ustep(abs_mm)) if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(abs_mm - self.get_pos().x_mm, self.get_config().X_AXIS.MAX_SPEED)) + self._calc_move_timeout(abs_mm - self.get_pos().x_mm, self.get_config().X_AXIS.MAX_SPEED) + ) def move_y_to(self, abs_mm: float, blocking: bool = True): self._microcontroller.move_y_to_usteps(self._config.Y_AXIS.convert_real_units_to_ustep(abs_mm)) if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(abs_mm - self.get_pos().y_mm, self.get_config().Y_AXIS.MAX_SPEED)) + self._calc_move_timeout(abs_mm - self.get_pos().y_mm, self.get_config().Y_AXIS.MAX_SPEED) + ) def move_z_to(self, abs_mm: float, blocking: bool = True): if blocking: self._microcontroller.wait_till_operation_is_completed( - self._calc_move_timeout(abs_mm - self.get_pos().z_mm, self.get_config().Z_AXIS.MAX_SPEED)) + self._calc_move_timeout(abs_mm - self.get_pos().z_mm, self.get_config().Z_AXIS.MAX_SPEED) + ) def get_pos(self) -> Pos: pos_usteps = self._microcontroller.get_pos() @@ -86,13 +95,16 @@ def home(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): # NOTE(imo): Arbitrarily use max speed / 5 for homing speed. It'd be better to have it exactly! x_timeout = self._calc_move_timeout( self.get_config().X_AXIS.MAX_POSITION - self.get_config().X_AXIS.MIN_POSITION, - self.get_config().X_AXIS.MAX_SPEED / 5.0) + self.get_config().X_AXIS.MAX_SPEED / 5.0, + ) y_timeout = self._calc_move_timeout( self.get_config().Y_AXIS.MAX_POSITION - self.get_config().Y_AXIS.MIN_POSITION, - self.get_config().Y_AXIS.MAX_SPEED / 5.0) + self.get_config().Y_AXIS.MAX_SPEED / 5.0, + ) z_timeout = self._calc_move_timeout( self.get_config().Z_AXIS.MAX_POSITION - self.get_config().Z_AXIS.MIN_POSITION, - self.get_config().Z_AXIS.MAX_SPEED / 5.0) + self.get_config().Z_AXIS.MAX_SPEED / 5.0, + ) theta_timeout = self._calc_move_timeout(2.0 * math.pi, self.get_config().THETA_AXIS.MAX_SPEED / 5.0) if x and y: self._microcontroller.home_xy() @@ -134,33 +146,46 @@ def zero(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): if blocking: self._microcontroller.wait_till_operation_is_completed() - def set_limits(self, x_pos_mm: Optional[float] = None, x_neg_mm: Optional[float] = None, - y_pos_mm: Optional[float] = None, y_neg_mm: Optional[float] = None, z_pos_mm: Optional[float] = None, - z_neg_mm: Optional[float] = None, theta_pos_rad: Optional[float] = None, - theta_neg_rad: Optional[float] = None): + def set_limits( + self, + x_pos_mm: Optional[float] = None, + x_neg_mm: Optional[float] = None, + y_pos_mm: Optional[float] = None, + y_neg_mm: Optional[float] = None, + z_pos_mm: Optional[float] = None, + z_neg_mm: Optional[float] = None, + theta_pos_rad: Optional[float] = None, + theta_neg_rad: Optional[float] = None, + ): if x_pos_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.X_POSITIVE, - self._config.X_AXIS.convert_real_units_to_ustep(x_pos_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.X_POSITIVE, self._config.X_AXIS.convert_real_units_to_ustep(x_pos_mm) + ) if x_neg_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.X_NEGATIVE, - self._config.X_AXIS.convert_real_units_to_ustep(x_neg_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.X_NEGATIVE, self._config.X_AXIS.convert_real_units_to_ustep(x_neg_mm) + ) if y_pos_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.Y_POSITIVE, - self._config.Y_AXIS.convert_real_units_to_ustep(y_pos_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.Y_POSITIVE, self._config.Y_AXIS.convert_real_units_to_ustep(y_pos_mm) + ) if y_neg_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.Y_NEGATIVE, - self._config.Y_AXIS.convert_real_units_to_ustep(y_neg_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.Y_NEGATIVE, self._config.Y_AXIS.convert_real_units_to_ustep(y_neg_mm) + ) if z_pos_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.Z_POSITIVE, - self._config.Z_AXIS.convert_real_units_to_ustep(z_pos_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.Z_POSITIVE, self._config.Z_AXIS.convert_real_units_to_ustep(z_pos_mm) + ) if z_neg_mm is not None: - self._microcontroller.set_lim(_def.LIMIT_CODE.Z_NEGATIVE, - self._config.Z_AXIS.convert_real_units_to_ustep(z_neg_mm)) + self._microcontroller.set_lim( + _def.LIMIT_CODE.Z_NEGATIVE, self._config.Z_AXIS.convert_real_units_to_ustep(z_neg_mm) + ) if theta_neg_rad or theta_pos_rad: raise ValueError("Setting limits for the theta axis is not supported on the CephlaStage") diff --git a/software/squid/stage/prior.py b/software/squid/stage/prior.py index 5fa62e483..03805444a 100644 --- a/software/squid/stage/prior.py +++ b/software/squid/stage/prior.py @@ -44,10 +44,17 @@ def home(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): def zero(self, x: bool, y: bool, z: bool, theta: bool, blocking: bool = True): self._not_impl() - def set_limits(self, x_pos_mm: Optional[float] = None, x_neg_mm: Optional[float] = None, - y_pos_mm: Optional[float] = None, y_neg_mm: Optional[float] = None, z_pos_mm: Optional[float] = None, - z_neg_mm: Optional[float] = None, theta_pos_rad: Optional[float] = None, - theta_neg_rad: Optional[float] = None): + def set_limits( + self, + x_pos_mm: Optional[float] = None, + x_neg_mm: Optional[float] = None, + y_pos_mm: Optional[float] = None, + y_neg_mm: Optional[float] = None, + z_pos_mm: Optional[float] = None, + z_neg_mm: Optional[float] = None, + theta_pos_rad: Optional[float] = None, + theta_neg_rad: Optional[float] = None, + ): self._not_impl() def get_config(self) -> StageConfig: diff --git a/software/squid/stage/utils.py b/software/squid/stage/utils.py index f6c72fa29..c3b32609e 100644 --- a/software/squid/stage/utils.py +++ b/software/squid/stage/utils.py @@ -10,6 +10,8 @@ """ Attempts to load a cached stage position and return it. """ + + def get_cached_position(cache_path=_DEFAULT_CACHE_PATH) -> Optional[Pos]: if not os.path.isfile(cache_path): _log.debug(f"Cache file '{cache_path}' not found, no cached pos found.") @@ -27,9 +29,12 @@ def get_cached_position(cache_path=_DEFAULT_CACHE_PATH) -> Optional[Pos]: pass return None + """ Write out the current x, y, z position, in mm, so we can use it later as a cached position. """ + + def cache_position(pos: Pos, stage_config: StageConfig, cache_path=_DEFAULT_CACHE_PATH): x_min = stage_config.X_AXIS.MIN_POSITION x_max = stage_config.X_AXIS.MAX_POSITION @@ -37,10 +42,10 @@ def cache_position(pos: Pos, stage_config: StageConfig, cache_path=_DEFAULT_CACH y_max = stage_config.Y_AXIS.MAX_POSITION z_min = stage_config.Z_AXIS.MIN_POSITION z_max = stage_config.Z_AXIS.MAX_POSITION - if not (x_min <= pos.x_mm <= x_max and - y_min <= pos.y_mm <= y_max and - z_min <= pos.z_mm <= z_max): - raise ValueError(f"Position {pos} is not cacheable because it is outside of the min/max of at least one axis. x_range=({x_min}, {x_max}), y_range=({y_min}, {y_max}), z_range=({z_min}, {z_max})") + if not (x_min <= pos.x_mm <= x_max and y_min <= pos.y_mm <= y_max and z_min <= pos.z_mm <= z_max): + raise ValueError( + f"Position {pos} is not cacheable because it is outside of the min/max of at least one axis. x_range=({x_min}, {x_max}), y_range=({y_min}, {y_max}), z_range=({z_min}, {z_max})" + ) with open(cache_path, "w") as f: _log.debug(f"Writing position={pos} to cache path='{cache_path}'") f.write(",".join([str(pos.x_mm), str(pos.y_mm), str(pos.z_mm)])) diff --git a/software/tests/control/test_microcontroller.py b/software/tests/control/test_microcontroller.py index 243b59293..440701393 100644 --- a/software/tests/control/test_microcontroller.py +++ b/software/tests/control/test_microcontroller.py @@ -2,14 +2,17 @@ import control._def import control.microcontroller + def assert_pos_almost_equal(expected, actual): assert len(actual) == len(expected) - for (e, a) in zip(expected, actual): + for e, a in zip(expected, actual): assert a == pytest.approx(e) + def test_create_simulated_microcontroller(): micro = control.microcontroller.Microcontroller(existing_serial=control.microcontroller.SimSerial()) + def test_microcontroller_simulated_positions(): micro = control.microcontroller.Microcontroller(existing_serial=control.microcontroller.SimSerial()) @@ -94,7 +97,10 @@ def test_microcontroller_simulated_positions(): micro.wait_till_operation_is_completed() assert_pos_almost_equal((0, 0, 3000, 0), micro.get_pos()) -@pytest.mark.skip(reason="This is likely a bug, but I'm not sure yet. Tracking in https://linear.app/cephla/issue/S-115/microcontroller-relative-and-absolute-position-sign-mismatch") + +@pytest.mark.skip( + reason="This is likely a bug, but I'm not sure yet. Tracking in https://linear.app/cephla/issue/S-115/microcontroller-relative-and-absolute-position-sign-mismatch" +) def test_microcontroller_absolute_and_relative_match(): micro = control.microcontroller.Microcontroller(existing_serial=control.microcontroller.SimSerial()) @@ -142,4 +148,4 @@ def wait(): micro.move_z_usteps(-abs_position) wait() - assert_pos_almost_equal((0, 0, 0, 0), micro.get_pos()) \ No newline at end of file + assert_pos_almost_equal((0, 0, 0, 0), micro.get_pos()) diff --git a/software/tests/squid/test_logging.py b/software/tests/squid/test_logging.py index f1416e346..2d25dbbba 100644 --- a/software/tests/squid/test_logging.py +++ b/software/tests/squid/test_logging.py @@ -3,10 +3,12 @@ import squid.logging + def test_root_logger(): root_logger = squid.logging.get_logger() assert root_logger.name == squid.logging._squid_root_logger_name + def test_children_loggers(): child_a = "a" child_b = "b" @@ -17,6 +19,7 @@ def test_children_loggers(): assert child_a_logger.name == f"{squid.logging._squid_root_logger_name}.{child_a}" assert child_b_logger.name == f"{squid.logging._squid_root_logger_name}.{child_a}.{child_b}" + def test_file_loggers(): log_file_name = tempfile.mktemp() diff --git a/software/tests/squid/test_stage.py b/software/tests/squid/test_stage.py index 861775379..e8ce91cab 100644 --- a/software/tests/squid/test_stage.py +++ b/software/tests/squid/test_stage.py @@ -8,6 +8,7 @@ from control.microcontroller import Microcontroller, SimSerial import squid.abc + def test_create_simulated_stages(): microcontroller = Microcontroller(existing_serial=SimSerial()) cephla_stage = squid.stage.cephla.CephlaStage(microcontroller, squid.config.get_stage_config()) @@ -15,9 +16,12 @@ def test_create_simulated_stages(): with pytest.raises(NotImplementedError): prior_stage = squid.stage.prior.PriorStage(squid.config.get_stage_config()) + def test_simulated_cephla_stage_ops(): microcontroller = Microcontroller(existing_serial=SimSerial()) - stage: squid.stage.cephla.CephlaStage = squid.stage.cephla.CephlaStage(microcontroller, squid.config.get_stage_config()) + stage: squid.stage.cephla.CephlaStage = squid.stage.cephla.CephlaStage( + microcontroller, squid.config.get_stage_config() + ) assert stage.get_pos() == squid.abc.Pos(x_mm=0.0, y_mm=0.0, z_mm=0.0, theta_rad=0.0) diff --git a/software/tools/list_cameras.py b/software/tools/list_cameras.py index a5e43449b..4f3106829 100644 --- a/software/tools/list_cameras.py +++ b/software/tools/list_cameras.py @@ -1,8 +1,9 @@ # version:1.0.1808.9101 import control.gxipy as gx + def main(): - + # create a device manager device_manager = gx.DeviceManager() dev_num, dev_info_list = device_manager.update_device_list() @@ -12,5 +13,6 @@ def main(): for i in range(dev_num): print(dev_info_list[i]) + if __name__ == "__main__": main() diff --git a/software/tools/list_controllers.py b/software/tools/list_controllers.py index a921e010c..eac53cd68 100644 --- a/software/tools/list_controllers.py +++ b/software/tools/list_controllers.py @@ -1,8 +1,8 @@ import serial import serial.tools.list_ports -print('\n') +print("\n") for p in serial.tools.list_ports.comports(): - print(p.__dict__) - print('\n') \ No newline at end of file + print(p.__dict__) + print("\n") diff --git a/software/tools/script_create_configurations_xml.py b/software/tools/script_create_configurations_xml.py index 8d97998cb..1b2a3c106 100644 --- a/software/tools/script_create_configurations_xml.py +++ b/software/tools/script_create_configurations_xml.py @@ -1,100 +1,100 @@ from lxml import etree as ET -top = ET.Element('modes') -mode_1 = ET.SubElement(top,'mode') +top = ET.Element("modes") + +mode_1 = ET.SubElement(top, "mode") # ID = ET.SubElement(mode_1,'ID') # ID.text = '123' -mode_1.set('ID','1') -mode_1.set('Name','BF LED matrix full') -mode_1.set('ExposureTime','100') -mode_1.set('AnalogGain','10') -mode_1.set('IlluminationSource','0') -mode_1.set('IlluminationIntensity','100') -mode_1.set('CameraSN','') -mode_1.set('ZOffset','0.0') -mode_1.set('PixelFormat','default') -mode_1.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - -mode_2 = ET.SubElement(top,'mode') -mode_2.set('ID','2') -mode_2.set('Name','BF LED matrix left half') -mode_2.set('ExposureTime','100') -mode_2.set('AnalogGain','10') -mode_2.set('IlluminationSource','1') -mode_2.set('IlluminationIntensity','100') -mode_2.set('CameraSN','') -mode_2.set('ZOffset','0.0') -mode_2.set('PixelFormat','default') -mode_2.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - -mode_3 = ET.SubElement(top,'mode') -mode_3.set('ID','3') -mode_3.set('Name','BF LED matrix right half') -mode_3.set('ExposureTime','100') -mode_3.set('AnalogGain','10') -mode_3.set('IlluminationSource','2') -mode_3.set('IlluminationIntensity','100') -mode_3.set('CameraSN','') -mode_3.set('ZOffset','0.0') -mode_3.set('PixelFormat','default') -mode_3.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - -mode_4 = ET.SubElement(top,'mode') -mode_4.set('ID','4') -mode_4.set('Name','BF LED matrix color PDAF') -mode_4.set('ExposureTime','100') -mode_4.set('AnalogGain','10') -mode_4.set('IlluminationSource','3') -mode_4.set('IlluminationIntensity','100') -mode_4.set('CameraSN','') -mode_4.set('ZOffset','0.0') -mode_4.set('PixelFormat','default') -mode_4.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - - -mode_5 = ET.SubElement(top,'mode') -mode_5.set('ID','5') -mode_5.set('Name','Fluorescence 405 nm Ex') -mode_5.set('ExposureTime','100') -mode_5.set('AnalogGain','10') -mode_5.set('IlluminationSource','11') -mode_5.set('IlluminationIntensity','100') -mode_5.set('CameraSN','') -mode_5.set('ZOffset','0.0') -mode_5.set('PixelFormat','default') -mode_5.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - -mode_6 = ET.SubElement(top,'mode') -mode_6.set('ID','6') -mode_6.set('Name','Fluorescence 488 nm Ex') -mode_6.set('ExposureTime','100') -mode_6.set('AnalogGain','10') -mode_6.set('IlluminationSource','12') -mode_6.set('IlluminationIntensity','100') -mode_6.set('CameraSN','') -mode_6.set('ZOffset','0.0') -mode_6.set('PixelFormat','default') -mode_6.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - -mode_7 = ET.SubElement(top,'mode') -mode_7.set('ID','7') -mode_7.set('Name','Fluorescence 638 nm Ex') -mode_7.set('ExposureTime','100') -mode_7.set('AnalogGain','10') -mode_7.set('IlluminationSource','13') -mode_7.set('IlluminationIntensity','100') -mode_7.set('CameraSN','') -mode_7.set('ZOffset','0.0') -mode_7.set('PixelFormat','default') -mode_7.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') +mode_1.set("ID", "1") +mode_1.set("Name", "BF LED matrix full") +mode_1.set("ExposureTime", "100") +mode_1.set("AnalogGain", "10") +mode_1.set("IlluminationSource", "0") +mode_1.set("IlluminationIntensity", "100") +mode_1.set("CameraSN", "") +mode_1.set("ZOffset", "0.0") +mode_1.set("PixelFormat", "default") +mode_1.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_2 = ET.SubElement(top, "mode") +mode_2.set("ID", "2") +mode_2.set("Name", "BF LED matrix left half") +mode_2.set("ExposureTime", "100") +mode_2.set("AnalogGain", "10") +mode_2.set("IlluminationSource", "1") +mode_2.set("IlluminationIntensity", "100") +mode_2.set("CameraSN", "") +mode_2.set("ZOffset", "0.0") +mode_2.set("PixelFormat", "default") +mode_2.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_3 = ET.SubElement(top, "mode") +mode_3.set("ID", "3") +mode_3.set("Name", "BF LED matrix right half") +mode_3.set("ExposureTime", "100") +mode_3.set("AnalogGain", "10") +mode_3.set("IlluminationSource", "2") +mode_3.set("IlluminationIntensity", "100") +mode_3.set("CameraSN", "") +mode_3.set("ZOffset", "0.0") +mode_3.set("PixelFormat", "default") +mode_3.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_4 = ET.SubElement(top, "mode") +mode_4.set("ID", "4") +mode_4.set("Name", "BF LED matrix color PDAF") +mode_4.set("ExposureTime", "100") +mode_4.set("AnalogGain", "10") +mode_4.set("IlluminationSource", "3") +mode_4.set("IlluminationIntensity", "100") +mode_4.set("CameraSN", "") +mode_4.set("ZOffset", "0.0") +mode_4.set("PixelFormat", "default") +mode_4.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_5 = ET.SubElement(top, "mode") +mode_5.set("ID", "5") +mode_5.set("Name", "Fluorescence 405 nm Ex") +mode_5.set("ExposureTime", "100") +mode_5.set("AnalogGain", "10") +mode_5.set("IlluminationSource", "11") +mode_5.set("IlluminationIntensity", "100") +mode_5.set("CameraSN", "") +mode_5.set("ZOffset", "0.0") +mode_5.set("PixelFormat", "default") +mode_5.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_6 = ET.SubElement(top, "mode") +mode_6.set("ID", "6") +mode_6.set("Name", "Fluorescence 488 nm Ex") +mode_6.set("ExposureTime", "100") +mode_6.set("AnalogGain", "10") +mode_6.set("IlluminationSource", "12") +mode_6.set("IlluminationIntensity", "100") +mode_6.set("CameraSN", "") +mode_6.set("ZOffset", "0.0") +mode_6.set("PixelFormat", "default") +mode_6.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") + + +mode_7 = ET.SubElement(top, "mode") +mode_7.set("ID", "7") +mode_7.set("Name", "Fluorescence 638 nm Ex") +mode_7.set("ExposureTime", "100") +mode_7.set("AnalogGain", "10") +mode_7.set("IlluminationSource", "13") +mode_7.set("IlluminationIntensity", "100") +mode_7.set("CameraSN", "") +mode_7.set("ZOffset", "0.0") +mode_7.set("PixelFormat", "default") +mode_7.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") # print(ET.tostring(top, encoding="UTF-8", pretty_print=True).decode()) tree = ET.ElementTree(top) -tree.write('configurations.xml',encoding="utf-8", xml_declaration=True, pretty_print=True) +tree.write("configurations.xml", encoding="utf-8", xml_declaration=True, pretty_print=True) diff --git a/software/tools/script_create_desktop_shortcut.py b/software/tools/script_create_desktop_shortcut.py index 3476846e1..629582475 100644 --- a/software/tools/script_create_desktop_shortcut.py +++ b/software/tools/script_create_desktop_shortcut.py @@ -1,78 +1,82 @@ import os import stat + + def create_desktop_shortcut_simulation(directory_path, script_name): - squid_suffix = script_name.replace("main_","") + squid_suffix = script_name.replace("main_", "") icon_path = os.path.join(directory_path, "icon/cephla_logo.svg") if squid_suffix != "main" and squid_suffix != "": - shortcut_content = f'''\ + shortcut_content = f"""\ [Desktop Entry] Name=Squid_{squid_suffix}_simulation Icon={icon_path} Exec=gnome-terminal --working-directory="{directory_path}" -e "/usr/bin/env python3 {directory_path}/{script_name}.py --simulation" Type=Application Terminal=true -''' +""" else: - shortcut_content = f'''\ + shortcut_content = f"""\ [Desktop Entry] Name=Squid_simulation Icon={icon_path} Exec=gnome-terminal --working-directory="{directory_path}" -e "/usr/bin/env python3 {directory_path}/{script_name}.py --simulation" Type=Application Terminal=true -''' +""" if squid_suffix != "main" and squid_suffix != "": - desktop_path_base = f'~/Desktop/Squid_{squid_suffix}_simulation.desktop' + desktop_path_base = f"~/Desktop/Squid_{squid_suffix}_simulation.desktop" else: - desktop_path_base = f'~/Desktop/Squid_simulation.desktop' + desktop_path_base = f"~/Desktop/Squid_simulation.desktop" desktop_path = os.path.expanduser(desktop_path_base) - with open(desktop_path, 'w') as shortcut_file: + with open(desktop_path, "w") as shortcut_file: shortcut_file.write(shortcut_content) os.chmod(desktop_path, stat.S_IRWXU) return desktop_path - def create_desktop_shortcut(directory_path, script_name): - squid_suffix = script_name.replace("main_","") + squid_suffix = script_name.replace("main_", "") icon_path = os.path.join(directory_path, "icon/cephla_logo.svg") if squid_suffix != "main" and squid_suffix != "": - shortcut_content = f'''\ + shortcut_content = f"""\ [Desktop Entry] Name=Squid_{squid_suffix} Icon={icon_path} Exec=gnome-terminal --working-directory="{directory_path}" -e "/usr/bin/env python3 {directory_path}/{script_name}.py" Type=Application Terminal=true -''' +""" else: - shortcut_content = f'''\ + shortcut_content = f"""\ [Desktop Entry] Name=Squid Icon={icon_path} Exec=gnome-terminal --working-directory="{directory_path}" -e "/usr/bin/env python3 {directory_path}/{script_name}.py" Type=Application Terminal=true -''' +""" if squid_suffix != "main" and squid_suffix != "": - desktop_path_base = f'~/Desktop/Squid_{squid_suffix}.desktop' + desktop_path_base = f"~/Desktop/Squid_{squid_suffix}.desktop" else: - desktop_path_base = f'~/Desktop/Squid.desktop' + desktop_path_base = f"~/Desktop/Squid.desktop" desktop_path = os.path.expanduser(desktop_path_base) - with open(desktop_path, 'w') as shortcut_file: + with open(desktop_path, "w") as shortcut_file: shortcut_file.write(shortcut_content) os.chmod(desktop_path, stat.S_IRWXU) return desktop_path + def main(): # Prompt for directory path and script name - directory_path = input('Enter the directory path to octopi-research/software (default: current directory): ') or os.getcwd() - script_name = input('Enter the main script name under octopi-research/software (without .py extension): ') + directory_path = ( + input("Enter the directory path to octopi-research/software (default: current directory): ") or os.getcwd() + ) + script_name = input("Enter the main script name under octopi-research/software (without .py extension): ") - simulation = input('Is this for launching in simulation mode? [NO/yes]: ') or False - if str(simulation).lower() == 'yes': + simulation = input("Is this for launching in simulation mode? [NO/yes]: ") or False + if str(simulation).lower() == "yes": simulation = True else: simulation = False @@ -82,8 +86,8 @@ def main(): desktop_path = create_desktop_shortcut(directory_path, script_name) else: desktop_path = create_desktop_shortcut_simulation(directory_path, script_name) - print(f'Desktop shortcut created at: {desktop_path}') + print(f"Desktop shortcut created at: {desktop_path}") -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/software/tools/script_create_zarr_from_acquisition.py b/software/tools/script_create_zarr_from_acquisition.py index 648b4e512..73a781a62 100644 --- a/software/tools/script_create_zarr_from_acquisition.py +++ b/software/tools/script_create_zarr_from_acquisition.py @@ -16,21 +16,29 @@ lazy_imread = delayed(imread) + def read_configurations_used(filepath): xml_tree = ET.parse(filepath) xml_tree_root = xml_tree.getroot() conf_list = [] - for mode in xml_tree_root.iter('mode'): + for mode in xml_tree_root.iter("mode"): selected = int(mode.get("Selected")) if selected != 0: mode_id = int(mode.get("ID")) - mode_name = mode.get('Name') - conf_list.append((mode_id,mode_name)) - conf_list = sorted(conf_list,key= lambda tup: tup[0]) + mode_name = mode.get("Name") + conf_list.append((mode_id, mode_name)) + conf_list = sorted(conf_list, key=lambda tup: tup[0]) conf_list = [tup[1] for tup in conf_list] return conf_list -def get_dimensions_for_dataset(dataset_folder_path, sensor_pixel_size_um_default = 1.0, objective_magnification_default=1.0, Nz_override = None, Nt_override = None): + +def get_dimensions_for_dataset( + dataset_folder_path, + sensor_pixel_size_um_default=1.0, + objective_magnification_default=1.0, + Nz_override=None, + Nt_override=None, +): """Returns dict of dimensions and then step sizes in mm for dx/dy, um for dz, and s in dt. @@ -49,118 +57,156 @@ def get_dimensions_for_dataset(dataset_folder_path, sensor_pixel_size_um_default 'FOV_shape': int 2-tuple that is the shape of a single channel's FOV, 'FOV_dtype': numpy dtype representing a single FOV image's dtype }""" - acq_param_path = os.path.join(dataset_folder_path,"acquisition parameters.json") - config_xml_path = os.path.join(dataset_folder_path,"configurations.xml") + acq_param_path = os.path.join(dataset_folder_path, "acquisition parameters.json") + config_xml_path = os.path.join(dataset_folder_path, "configurations.xml") acq_params = None - with open(acq_param_path,'r') as file: + with open(acq_param_path, "r") as file: acq_params = json.load(file) - Nt = int(acq_params.get('Nt')) + Nt = int(acq_params.get("Nt")) if Nt_override is not None: if Nt_override < Nt: Nt = Nt_override - Nz = int(acq_params.get('Nz')) + Nz = int(acq_params.get("Nz")) if Nz_override is not None: if Nz_override < Nz: Nz = Nz_override - dt = float(acq_params.get('dt(s)')) - dz = float(acq_params.get('dz(um)')) - - Nx = int(acq_params.get('Nx')) - Ny = int(acq_params.get('Ny')) - dx = float(acq_params.get('dx(mm)')) - dy = float(acq_params.get('dy(mm)')) + dt = float(acq_params.get("dt(s)")) + dz = float(acq_params.get("dz(um)")) + + Nx = int(acq_params.get("Nx")) + Ny = int(acq_params.get("Ny")) + dx = float(acq_params.get("dx(mm)")) + dy = float(acq_params.get("dy(mm)")) try: - objective = acq_params.get('objective') - objective_magnification = float(objective['magnification']) + objective = acq_params.get("objective") + objective_magnification = float(objective["magnification"]) except (KeyError, ValueError, AttributeError, TypeError): objective_magnification = objective_magnification_default try: - sensor = acq_params.get('sensor') - sensor_pixel_size = float(sensor['pixel_size_um']) + sensor = acq_params.get("sensor") + sensor_pixel_size = float(sensor["pixel_size_um"]) except (KeyError, ValueError, AttributeError, TypeError): sensor_pixel_size = sensor_pixel_size_um_default - pixel_size_um = sensor_pixel_size/objective_magnification + pixel_size_um = sensor_pixel_size / objective_magnification - imagespath = os.path.join(dataset_folder_path, '0/0_*.*') - first_file = sorted(glob(imagespath), key=alphanumeric_key)[0] + imagespath = os.path.join(dataset_folder_path, "0/0_*.*") + first_file = sorted(glob(imagespath), key=alphanumeric_key)[0] sample = imread(first_file) - + FOV_shape = sample.shape FOV_dtype = sample.dtype channels = read_configurations_used(config_xml_path) Nc = len(channels) - return {'Nx':Nx, - 'Ny':Ny, - 'Nz':Nz, - 'dx':dx, - 'dy':dy, - 'dz':dz, - 'Nt':Nt, - 'dt':dt, - 'Nc':Nc, - 'channels':channels, - 'pixel_size_um':pixel_size_um, - 'FOV_shape':FOV_shape, - 'FOV_dtype':FOV_dtype - } - - -def create_dask_array_for_single_fov(dataset_folder_path, x=0,y=0, sensor_pixel_size_um_default = 1.0, objective_magnification_default=1.0, z_to_use=None, t_to_use=None, well=0): + return { + "Nx": Nx, + "Ny": Ny, + "Nz": Nz, + "dx": dx, + "dy": dy, + "dz": dz, + "Nt": Nt, + "dt": dt, + "Nc": Nc, + "channels": channels, + "pixel_size_um": pixel_size_um, + "FOV_shape": FOV_shape, + "FOV_dtype": FOV_dtype, + } + + +def create_dask_array_for_single_fov( + dataset_folder_path, + x=0, + y=0, + sensor_pixel_size_um_default=1.0, + objective_magnification_default=1.0, + z_to_use=None, + t_to_use=None, + well=0, +): Nt_override = None if t_to_use is not None: Nt_override = len(t_to_use) Nz_override = None if z_to_use is not None: Nz_override = len(z_to_use) - dimension_data = get_dimensions_for_dataset(dataset_folder_path, sensor_pixel_size_um_default, objective_magnification_default, Nz_override, Nt_override) + dimension_data = get_dimensions_for_dataset( + dataset_folder_path, sensor_pixel_size_um_default, objective_magnification_default, Nz_override, Nt_override + ) if t_to_use is not None: - if max(t_to_use) >= dimension_data['Nt'] or min(t_to_use) < 0: + if max(t_to_use) >= dimension_data["Nt"] or min(t_to_use) < 0: raise IndexError("t index given in list out of bounds") if z_to_use is not None: - if max(z_to_use) >= dimension_data['Nz'] or min(z_to_use) < 0: + if max(z_to_use) >= dimension_data["Nz"] or min(z_to_use) < 0: raise IndexError("z index given in list out of bounds") if t_to_use is None: - t_to_use = list(range(dimension_data['Nt'])) + t_to_use = list(range(dimension_data["Nt"])) if z_to_use is None: - z_to_use = list(range(dimension_data['Nz'])) - if x >= dimension_data['Nx'] or x<0 or y>= dimension_data['Ny'] or y < 0: + z_to_use = list(range(dimension_data["Nz"])) + if x >= dimension_data["Nx"] or x < 0 or y >= dimension_data["Ny"] or y < 0: raise IndexError("FOV indices out of range.") dask_arrays_time = [] for t in t_to_use: dask_arrays_channel = [] - for channel in dimension_data['channels']: + for channel in dimension_data["channels"]: filenames = [] for z in z_to_use: - image_path = str(t)+"/"+str(y)+"_"+str(x)+"_"+str(z)+"_"+channel.strip().replace(" ","_")+".*" + image_path = ( + str(t) + "/" + str(y) + "_" + str(x) + "_" + str(z) + "_" + channel.strip().replace(" ", "_") + ".*" + ) image_path = os.path.join(dataset_folder_path, image_path) file_matches = glob(image_path) if len(file_matches) > 0: filenames.append(file_matches[0]) else: - image_path = str(t)+"/"+str(well)+"_"+str(y)+"_"+str(x)+"_"+str(z)+"_"+channel.strip().replace(" ","_")+".*" + image_path = ( + str(t) + + "/" + + str(well) + + "_" + + str(y) + + "_" + + str(x) + + "_" + + str(z) + + "_" + + channel.strip().replace(" ", "_") + + ".*" + ) image_path = os.path.join(dataset_folder_path, image_path) file_matches = glob(image_path) if len(file_matches) > 0: filenames.append(file_matches[0]) - filenames = sorted(filenames,key=alphanumeric_key) + filenames = sorted(filenames, key=alphanumeric_key) lazy_arrays = [lazy_imread(fn) for fn in filenames] dask_arrays = [ - da.from_delayed(delayed_reader, shape = dimension_data['FOV_shape'], dtype=dimension_data['FOV_dtype']) - for delayed_reader in lazy_arrays - ] + da.from_delayed(delayed_reader, shape=dimension_data["FOV_shape"], dtype=dimension_data["FOV_dtype"]) + for delayed_reader in lazy_arrays + ] stack = da.stack(dask_arrays, axis=0) dask_arrays_channel.append(stack) channel_stack = da.stack(dask_arrays_channel, axis=0) dask_arrays_time.append(channel_stack) - time_stack = da.stack(dask_arrays_time,axis=0) + time_stack = da.stack(dask_arrays_time, axis=0) return time_stack -def create_zarr_for_single_fov(dataset_folder_path, saving_path, x=0,y=0,sensor_pixel_size_um=1.0, objective_magnification=1.0, z_to_use=None, t_to_use = None, well=0): + +def create_zarr_for_single_fov( + dataset_folder_path, + saving_path, + x=0, + y=0, + sensor_pixel_size_um=1.0, + objective_magnification=1.0, + z_to_use=None, + t_to_use=None, + well=0, +): try: os.mkdir(saving_path) except FileExistsError: @@ -173,20 +219,29 @@ def create_zarr_for_single_fov(dataset_folder_path, saving_path, x=0,y=0,sensor_ scale_t = dimension_data["dt"] if scale_t == 0.0: scale_t = 1.0 - coord_transform=[{"type":"scale","scale":[scale_t,1.0,scale_z,scale_xy,scale_xy]}] + coord_transform = [{"type": "scale", "scale": [scale_t, 1.0, scale_z, scale_xy, scale_xy]}] - fov_dask_array = create_dask_array_for_single_fov(dataset_folder_path, x,y, sensor_pixel_size_um, objective_magnification, z_to_use, t_to_use, well) + fov_dask_array = create_dask_array_for_single_fov( + dataset_folder_path, x, y, sensor_pixel_size_um, objective_magnification, z_to_use, t_to_use, well + ) xy_only_dims = fov_dask_array.shape[3:] store = parse_url(saving_path, mode="w").store root = zarr.group(store=store) - write_image(image=fov_dask_array, group=root, - scaler = None, axes=["t","c","z","y","x"], - coordinate_transformations=[coord_transform], - storage_options=dict(chunks=(1,1,1,*xy_only_dims))) + write_image( + image=fov_dask_array, + group=root, + scaler=None, + axes=["t", "c", "z", "y", "x"], + coordinate_transformations=[coord_transform], + storage_options=dict(chunks=(1, 1, 1, *xy_only_dims)), + ) + if __name__ == "__main__": if len(sys.argv) != 5 and len(sys.argv) != 3 and len(sys.argv) != 7 and len(sys.argv) != 8 and len(sys.argv) != 9: - raise RuntimeError("2 positional arguments required: path to slide data folder, and path to zarr to write. The following 2 positional arguments, if they exist, must be the x-index and the y-index of the FOV to convert (default 0). The last two positional arguments should be the pixel_size_um parameter of the sensor, and the magnification of the objective used. The last two positional arguments are an override on the number of z steps to use and an override on the number of t steps to use.") + raise RuntimeError( + "2 positional arguments required: path to slide data folder, and path to zarr to write. The following 2 positional arguments, if they exist, must be the x-index and the y-index of the FOV to convert (default 0). The last two positional arguments should be the pixel_size_um parameter of the sensor, and the magnification of the objective used. The last two positional arguments are an override on the number of z steps to use and an override on the number of t steps to use." + ) folderpath = sys.argv[1] saving_path = sys.argv[2] try: @@ -200,8 +255,8 @@ def create_zarr_for_single_fov(dataset_folder_path, saving_path, x=0,y=0,sensor_ sensor_pixel_size = float(sys.argv[5]) objective_magnification = float(sys.argv[6]) except IndexError: - sensor_pixel_size=1.85 - objective_magnification=20.0 + sensor_pixel_size = 1.85 + objective_magnification = 20.0 try: Nz_override = int(sys.argv[7]) @@ -215,6 +270,8 @@ def create_zarr_for_single_fov(dataset_folder_path, saving_path, x=0,y=0,sensor_ except IndexError: t_to_use = None - create_zarr_for_single_fov(folderpath, saving_path,x,y, sensor_pixel_size, objective_magnification, z_to_use, t_to_use) - print("OME-Zarr written to "+saving_path) - print("Use the command\n $> napari --plugin napari-ome-zarr "+saving_path+"\nto view.") + create_zarr_for_single_fov( + folderpath, saving_path, x, y, sensor_pixel_size, objective_magnification, z_to_use, t_to_use + ) + print("OME-Zarr written to " + saving_path) + print("Use the command\n $> napari --plugin napari-ome-zarr " + saving_path + "\nto view.") diff --git a/software/tools/script_flip_i_indices.py b/software/tools/script_flip_i_indices.py index 9ec199a0a..584525de9 100644 --- a/software/tools/script_flip_i_indices.py +++ b/software/tools/script_flip_i_indices.py @@ -5,43 +5,46 @@ import sys import pandas as pd + def get_ny(slide_path): parameter_path = os.path.join(slide_path, "acquisition parameters.json") parameters = {} with open(parameter_path, "r") as f: parameters = json.load(f) - Ny = int(parameters['Ny']) + Ny = int(parameters["Ny"]) return Ny + def get_inverted_y_filepath(filepath, channel_name, Ny): """Given a channel name to strip and a number of y indices, returns a version of the slide name with its y-index inverted.""" channel_name = channel_name.replace(" ", "_") filename = filepath.split("/")[-1] extension = filename.split(".")[-1] - coord_list = filename.replace(channel_name, "").replace("."+extension,"").strip("_").split("_") + coord_list = filename.replace(channel_name, "").replace("." + extension, "").strip("_").split("_") if len(coord_list) > 3: - coord_list[1] = str(Ny-1-int(coord_list[1])) + coord_list[1] = str(Ny - 1 - int(coord_list[1])) else: - coord_list[0] = str(Ny-1-int(coord_list[0])) + coord_list[0] = str(Ny - 1 - int(coord_list[0])) - inverted_y_filename = "_".join([*coord_list, channel_name])+"."+extension + inverted_y_filename = "_".join([*coord_list, channel_name]) + "." + extension inverted_y_filepath = filepath.replace(filename, inverted_y_filename) return inverted_y_filepath def invert_y_in_folder(fovs_path, channel_names, Ny): """Given a folder with FOVs, channel names, and Ny, inverts the y-indices of all of them""" - + for channel in channel_names: channel = channel.replace(" ", "_") - filepaths = list(glob(os.path.join(fovs_path, "*_*_*_"+channel+".*"))) + filepaths = list(glob(os.path.join(fovs_path, "*_*_*_" + channel + ".*"))) for path in filepaths: inv_y_filepath = get_inverted_y_filepath(path, channel, Ny) - os.rename(path, inv_y_filepath+"._inverted") + os.rename(path, inv_y_filepath + "._inverted") for path in filepaths: - os.rename(path+"._inverted", path) + os.rename(path + "._inverted", path) + def invert_y_in_slide(slide_path): Ny = get_ny(slide_path) @@ -54,12 +57,13 @@ def invert_y_in_slide(slide_path): # invert the y-index in the CSV too coord_csv_path = os.path.join(fovs_path, "coordinates.csv") coord_df = pd.read_csv(coord_csv_path) - coord_df["i"] = (Ny-1)-coord_df["i"] + coord_df["i"] = (Ny - 1) - coord_df["i"] coord_df.to_csv(coord_csv_path, index=False) + if __name__ == "__main__": if len(sys.argv) <= 1: print("Must provide a path to a slide folder.") exit() invert_y_in_slide(sys.argv[1]) - print("Inverted all i/y-indices in "+sys.argv[1]) + print("Inverted all i/y-indices in " + sys.argv[1]) diff --git a/software/tools/script_stitch_slide.py b/software/tools/script_stitch_slide.py index 43addbb72..250354ede 100644 --- a/software/tools/script_stitch_slide.py +++ b/software/tools/script_stitch_slide.py @@ -6,32 +6,40 @@ from stitcher import stitch_slide, compute_overlap_percent import sys -def get_pixel_size(slide_path, default_pixel_size=1.85, default_tube_lens_mm=50.0, default_objective_tube_lens_mm=180.0, default_magnification=20.0): + +def get_pixel_size( + slide_path, + default_pixel_size=1.85, + default_tube_lens_mm=50.0, + default_objective_tube_lens_mm=180.0, + default_magnification=20.0, +): parameter_path = os.path.join(slide_path, "acquisition parameters.json") parameters = {} with open(parameter_path, "r") as f: parameters = json.load(f) try: - tube_lens_mm = float(parameters['tube_lens_mm']) + tube_lens_mm = float(parameters["tube_lens_mm"]) except KeyError: tube_lens_mm = default_tube_lens_mm try: - pixel_size_um = float(parameters['sensor_pixel_size_um']) + pixel_size_um = float(parameters["sensor_pixel_size_um"]) except KeyError: pixel_size_um = default_pixel_size try: - objective_tube_lens_mm = float(parameters['objective']['tube_lens_f_mm']) + objective_tube_lens_mm = float(parameters["objective"]["tube_lens_f_mm"]) except KeyError: objective_tube_lens_mm = default_objective_tube_lens_mm try: - magnification = float(parameters['objective']['magnification']) + magnification = float(parameters["objective"]["magnification"]) except KeyError: magnification = default_magnification - pixel_size_xy = pixel_size_um/(magnification/(objective_tube_lens_mm/tube_lens_mm)) + pixel_size_xy = pixel_size_um / (magnification / (objective_tube_lens_mm / tube_lens_mm)) return pixel_size_xy + def get_overlap(slide_path, **kwargs): sample_fov_path = os.path.join(slide_path, "0/*0_0_0_*.*") sample_fov_path = glob(sample_fov_path)[0] @@ -40,60 +48,64 @@ def get_overlap(slide_path, **kwargs): fov_height = sample_fov_shape[0] pixel_size_xy = get_pixel_size(slide_path, **kwargs) - + parameter_path = os.path.join(slide_path, "acquisition parameters.json") parameters = {} with open(parameter_path, "r") as f: parameters = json.load(f) - dx = float(parameters['dx(mm)'])*1000.0 - dy = float(parameters['dy(mm)'])*1000.0 + dx = float(parameters["dx(mm)"]) * 1000.0 + dy = float(parameters["dy(mm)"]) * 1000.0 overlap_percent = compute_overlap_percent(dx, dy, fov_width, fov_height, pixel_size_xy) return overlap_percent + def get_time_indices(slide_path): - + parameter_path = os.path.join(slide_path, "acquisition parameters.json") parameters = {} with open(parameter_path, "r") as f: parameters = json.load(f) - time_indices = list(range(int(parameters['Nt']))) + time_indices = list(range(int(parameters["Nt"]))) return time_indices + def get_channels(slide_path): config_xml_tree_root = ET.parse(os.path.join(slide_path, "configurations.xml")).getroot() channel_names = [] - for mode in config_xml_tree_root.iter('mode'): + for mode in config_xml_tree_root.iter("mode"): if mode.get("Selected") == "1": - channel_names.append(mode.get('Name').replace(" ","_")) + channel_names.append(mode.get("Name").replace(" ", "_")) return channel_names + def get_z_indices(slide_path): parameter_path = os.path.join(slide_path, "acquisition parameters.json") parameters = {} with open(parameter_path, "r") as f: parameters = json.load(f) - z_indices = list(range(int(parameters['Nz']))) + z_indices = list(range(int(parameters["Nz"]))) return z_indices def get_coord_names(slide_path): - sample_fovs_path=os.path.join(slide_path, "0/*_0_0_0_*.*") + sample_fovs_path = os.path.join(slide_path, "0/*_0_0_0_*.*") sample_fovs = glob(sample_fovs_path) coord_names = [] for fov in sample_fovs: filename = fov.split("/")[-1] coord_name = filename.split("_0_")[0] - coord_names.append(coord_name+"_") + coord_names.append(coord_name + "_") coord_names = list(set(coord_names)) if len(coord_names) == 0: - coord_names = [''] + coord_names = [""] return coord_names + def stitch_slide_from_path(slide_path, **kwargs): time_indices = get_time_indices(slide_path) z_indices = get_z_indices(slide_path) @@ -101,9 +113,22 @@ def stitch_slide_from_path(slide_path, **kwargs): coord_names = get_coord_names(slide_path) overlap_percent = get_overlap(slide_path, **kwargs) - recompute_overlap = (overlap_percent > 10) + recompute_overlap = overlap_percent > 10 + + stitch_slide( + slide_path, + time_indices, + channels, + z_indices, + coord_names, + overlap_percent=overlap_percent, + reg_threshold=0.30, + avg_displacement_threshold=2.50, + abs_displacement_threshold=3.50, + tile_downsampling=1.0, + recompute_overlap=recompute_overlap, + ) - stitch_slide(slide_path, time_indices, channels, z_indices, coord_names, overlap_percent = overlap_percent, reg_threshold=0.30, avg_displacement_threshold=2.50, abs_displacement_threshold=3.50, tile_downsampling=1.0, recompute_overlap=recompute_overlap) def print_usage(): usage_str = """ @@ -127,18 +152,19 @@ def print_usage(): print(usage_str) + if __name__ == "__main__": if len(sys.argv) < 2: print("No slide path name provided!") print_usage() exit() - + parameter_names = { - "--sensor-size":"default_pixel_size", - "--tube-lens":"default_tube_lens_mm", - "--objective-tube-lens":"default_objective_tube_lens_mm", - "--magnification":"default_magnification" - } + "--sensor-size": "default_pixel_size", + "--tube-lens": "default_tube_lens_mm", + "--objective-tube-lens": "default_objective_tube_lens_mm", + "--magnification": "default_magnification", + } param_list = list(parameter_names.keys()) @@ -151,12 +177,10 @@ def print_usage(): for i in range(len(sys.argv)): if sys.argv[i] in param_list: try: - arg_value = float(sys.argv[i+1]) + arg_value = float(sys.argv[i + 1]) user_kwargs[parameter_names[sys.argv[i]]] = arg_value except (IndexError, ValueError): print("Malformed argument, exiting.") exit() - - stitch_slide_from_path(sys.argv[1], **user_kwargs) diff --git a/software/tools/stitcher.py b/software/tools/stitcher.py index 84572f74d..d2373ab52 100644 --- a/software/tools/stitcher.py +++ b/software/tools/stitcher.py @@ -9,35 +9,56 @@ JVM_MAX_MEMORY_GB = 4.0 + def compute_overlap_percent(deltaX, deltaY, image_width, image_height, pixel_size_xy, min_overlap=0): """Helper function to calculate percent overlap between images in a grid""" - shift_x = deltaX/pixel_size_xy - shift_y = deltaY/pixel_size_xy - overlap_x = max(0,image_width-shift_x) - overlap_y = max(0,image_height-shift_y) - overlap_x = overlap_x*100.0/image_width - overlap_y = overlap_y*100.0/image_height + shift_x = deltaX / pixel_size_xy + shift_y = deltaY / pixel_size_xy + overlap_x = max(0, image_width - shift_x) + overlap_y = max(0, image_height - shift_y) + overlap_x = overlap_x * 100.0 / image_width + overlap_y = overlap_y * 100.0 / image_height overlap = max(min_overlap, overlap_x, overlap_y) return overlap + def stitch_slide_mp(*args, **kwargs): - ctx = mp.get_context('spawn') + ctx = mp.get_context("spawn") stitch_process = ctx.Process(target=stitch_slide, args=args, kwargs=kwargs) stitch_process.start() return stitch_process - -def migrate_tile_config(fovs_path, coord_name, channel_name_source, z_index_source, channel_name_target, z_index_target): + + +def migrate_tile_config( + fovs_path, coord_name, channel_name_source, z_index_source, channel_name_target, z_index_target +): channel_name_source = channel_name_source.replace(" ", "_") - channel_name_target = channel_name_target.replace(" ","_") - + channel_name_target = channel_name_target.replace(" ", "_") + if z_index_source == z_index_target and channel_name_source == channel_name_target: raise RuntimeError("Source and target for channel/z-index migration are the same!") - tile_conf_name_source = "TileConfiguration_COORD_"+coord_name+"_Z_"+str(z_index_source)+"_"+channel_name_source+".registered.txt" - tile_conf_name_target = "TileConfiguration_COORD_"+coord_name+"_Z_"+str(z_index_target)+"_"+channel_name_target+".registered.txt" + tile_conf_name_source = ( + "TileConfiguration_COORD_" + + coord_name + + "_Z_" + + str(z_index_source) + + "_" + + channel_name_source + + ".registered.txt" + ) + tile_conf_name_target = ( + "TileConfiguration_COORD_" + + coord_name + + "_Z_" + + str(z_index_target) + + "_" + + channel_name_target + + ".registered.txt" + ) tile_config_source_path = os.path.join(fovs_path, tile_conf_name_source) - + if not os.path.isfile(tile_config_source_path): tile_config_source_path = tile_config_source_path.replace(".registered.txt", ".txt") @@ -45,34 +66,106 @@ def migrate_tile_config(fovs_path, coord_name, channel_name_source, z_index_sour tile_config_target_path = os.path.join(fovs_path, tile_conf_name_target) - tile_conf_target = open(tile_config_target_path, 'w') + tile_conf_target = open(tile_config_target_path, "w") - with open(tile_config_source_path, 'r') as tile_conf_source: + with open(tile_config_source_path, "r") as tile_conf_source: for line in tile_conf_source: if line.startswith("#") or line.startswith("dim") or len(line) <= 1: tile_conf_target.write(line) continue - line_to_write = line.replace("_"+str(z_index_source)+"_"+channel_name_source, "_"+str(z_index_target)+"_"+channel_name_target) + line_to_write = line.replace( + "_" + str(z_index_source) + "_" + channel_name_source, + "_" + str(z_index_target) + "_" + channel_name_target, + ) tile_conf_target.write(line_to_write) tile_conf_target.close() return tile_conf_name_target -def stitch_slide(slide_path, time_indices, channels, z_indices, coord_names=[''], overlap_percent=10, reg_threshold=0.30, avg_displacement_threshold=2.50, abs_displacement_threshold=3.50, tile_downsampling=0.5, recompute_overlap=False, **kwargs): + +def stitch_slide( + slide_path, + time_indices, + channels, + z_indices, + coord_names=[""], + overlap_percent=10, + reg_threshold=0.30, + avg_displacement_threshold=2.50, + abs_displacement_threshold=3.50, + tile_downsampling=0.5, + recompute_overlap=False, + **kwargs +): st = Stitcher() - st.stitch_slide(slide_path, time_indices, channels, z_indices, coord_names, overlap_percent, reg_threshold, avg_displacement_threshold, abs_displacement_threshold, tile_downsampling, recompute_overlap, **kwargs) + st.stitch_slide( + slide_path, + time_indices, + channels, + z_indices, + coord_names, + overlap_percent, + reg_threshold, + avg_displacement_threshold, + abs_displacement_threshold, + tile_downsampling, + recompute_overlap, + **kwargs + ) + class Stitcher: def __init__(self): - scyjava.config.add_option('-Xmx'+str(int(JVM_MAX_MEMORY_GB))+'g') - self.ij = imagej.init('sc.fiji:fiji', mode='headless') - - def stitch_slide(self, slide_path, time_indices, channels, z_indices, coord_names=[''], overlap_percent = 10, reg_threshold=0.30, avg_displacement_threshold=2.50, abs_displacement_threshold=3.50, tile_downsampling=0.5, recompute_overlap=False, **kwargs): + scyjava.config.add_option("-Xmx" + str(int(JVM_MAX_MEMORY_GB)) + "g") + self.ij = imagej.init("sc.fiji:fiji", mode="headless") + + def stitch_slide( + self, + slide_path, + time_indices, + channels, + z_indices, + coord_names=[""], + overlap_percent=10, + reg_threshold=0.30, + avg_displacement_threshold=2.50, + abs_displacement_threshold=3.50, + tile_downsampling=0.5, + recompute_overlap=False, + **kwargs + ): for time_index in time_indices: - self.stitch_single_time_point(slide_path, time_index, channels, z_indices, coord_names, overlap_percent, reg_threshold, avg_displacement_threshold, abs_displacement_threshold, tile_downsampling, recompute_overlap, **kwargs) + self.stitch_single_time_point( + slide_path, + time_index, + channels, + z_indices, + coord_names, + overlap_percent, + reg_threshold, + avg_displacement_threshold, + abs_displacement_threshold, + tile_downsampling, + recompute_overlap, + **kwargs + ) - def stitch_single_time_point(self, slide_path, time_index, channels, z_indices, coord_names = [''], overlap_percent=10, reg_threshold=0.30, avg_displacement_threshold=2.50, abs_displacement_threshold=3.50, tile_downsampling=0.5, recompute_overlap=False, **kwargs): + def stitch_single_time_point( + self, + slide_path, + time_index, + channels, + z_indices, + coord_names=[""], + overlap_percent=10, + reg_threshold=0.30, + avg_displacement_threshold=2.50, + abs_displacement_threshold=3.50, + tile_downsampling=0.5, + recompute_overlap=False, + **kwargs + ): fovs_path = os.path.join(slide_path, str(time_index)) for coord_name in coord_names: already_registered = False @@ -81,18 +174,37 @@ def stitch_single_time_point(self, slide_path, time_index, channels, z_indices, for channel_name in channels: for z_index in z_indices: if already_registered: - migrate_tile_config(fovs_path, coord_name, registered_channel_name, registered_z_index, channel_name.replace(" ", "_"), z_index) - output_dir = self.stitch_single_channel_from_tile_config(fovs_path, channel_name, z_index, coord_name) + migrate_tile_config( + fovs_path, + coord_name, + registered_channel_name, + registered_z_index, + channel_name.replace(" ", "_"), + z_index, + ) + output_dir = self.stitch_single_channel_from_tile_config( + fovs_path, channel_name, z_index, coord_name + ) combine_stitched_channels(output_dir, **kwargs) else: - output_dir = self.stitch_single_channel(fovs_path, channel_name, z_index, coord_name, overlap_percent, reg_threshold, avg_displacement_threshold, abs_displacement_threshold, tile_downsampling, recompute_overlap) + output_dir = self.stitch_single_channel( + fovs_path, + channel_name, + z_index, + coord_name, + overlap_percent, + reg_threshold, + avg_displacement_threshold, + abs_displacement_threshold, + tile_downsampling, + recompute_overlap, + ) combine_stitched_channels(output_dir, **kwargs) if not already_registered: already_registered = True registered_z_index = z_index registered_channel_name = channel_name.replace(" ", "_") - def stitch_single_channel_from_tile_config(self, fovs_path, channel_name, z_index, coord_name): """ Stitches images using grid/collection stitching, reading registered @@ -100,31 +212,34 @@ def stitch_single_channel_from_tile_config(self, fovs_path, channel_name, z_inde already-registered channel/z-level at the same coordinate name """ channel_name = channel_name.replace(" ", "_") - tile_conf_name = "TileConfiguration_COORD_"+coord_name+"_Z_"+str(z_index)+"_"+channel_name+".registered.txt" + tile_conf_name = ( + "TileConfiguration_COORD_" + coord_name + "_Z_" + str(z_index) + "_" + channel_name + ".registered.txt" + ) assert os.path.isfile(os.path.join(fovs_path, tile_conf_name)) - stitching_output_dir = 'COORD_'+coord_name+"_Z_"+str(z_index)+"_"+channel_name+"_stitched/" + stitching_output_dir = "COORD_" + coord_name + "_Z_" + str(z_index) + "_" + channel_name + "_stitched/" - stitching_output_dir = os.path.join(fovs_path,stitching_output_dir) + stitching_output_dir = os.path.join(fovs_path, stitching_output_dir) os.makedirs(stitching_output_dir, exist_ok=True) - stitching_params = {'type':'Positions from file', - 'order':'Defined by TileConfiguration', - 'fusion_mode':'Linear Blending', - 'ignore_z_stage':True, - 'downsample_tiles':False, - 'directory':fovs_path, - 'layout_file':tile_conf_name, - 'fusion_method':'Linear Blending', - 'regression_threshold':"0.30", - 'max/avg_displacement_threshold':"2.50", - 'absolute_displacement_threshold':"3.50", - 'compute_overlap':False, - 'computation_parameters':'Save computation time (but use more RAM)', - 'image_output':'Write to disk', - 'output_directory':stitching_output_dir - } + stitching_params = { + "type": "Positions from file", + "order": "Defined by TileConfiguration", + "fusion_mode": "Linear Blending", + "ignore_z_stage": True, + "downsample_tiles": False, + "directory": fovs_path, + "layout_file": tile_conf_name, + "fusion_method": "Linear Blending", + "regression_threshold": "0.30", + "max/avg_displacement_threshold": "2.50", + "absolute_displacement_threshold": "3.50", + "compute_overlap": False, + "computation_parameters": "Save computation time (but use more RAM)", + "image_output": "Write to disk", + "output_directory": stitching_output_dir, + } plugin = "Grid/Collection stitching" @@ -132,8 +247,19 @@ def stitch_single_channel_from_tile_config(self, fovs_path, channel_name, z_inde return stitching_output_dir - - def stitch_single_channel(self, fovs_path, channel_name, z_index, coord_name='', overlap_percent=10, reg_threshold = 0.30, avg_displacement_threshold=2.50, abs_displacement_threshold=3.50, tile_downsampling=0.5, recompute_overlap=False): + def stitch_single_channel( + self, + fovs_path, + channel_name, + z_index, + coord_name="", + overlap_percent=10, + reg_threshold=0.30, + avg_displacement_threshold=2.50, + abs_displacement_threshold=3.50, + tile_downsampling=0.5, + recompute_overlap=False, + ): """ Stitches images using grid/collection stitching with filename-defined positions following the format that squid saves multipoint acquisitions @@ -144,63 +270,63 @@ def stitch_single_channel(self, fovs_path, channel_name, z_index, coord_name='', """ channel_name = channel_name.replace(" ", "_") - file_search_name = coord_name+"0_0_"+str(z_index)+"_"+channel_name+".*" + file_search_name = coord_name + "0_0_" + str(z_index) + "_" + channel_name + ".*" - ext_glob = list(glob(os.path.join(fovs_path,file_search_name))) + ext_glob = list(glob(os.path.join(fovs_path, file_search_name))) file_ext = ext_glob[0].split(".")[-1] - y_length_pattern = coord_name+"*_0_"+str(z_index)+"_"+channel_name+"."+file_ext + y_length_pattern = coord_name + "*_0_" + str(z_index) + "_" + channel_name + "." + file_ext - x_length_pattern = coord_name+"0_*_"+str(z_index)+"_"+channel_name+"."+file_ext + x_length_pattern = coord_name + "0_*_" + str(z_index) + "_" + channel_name + "." + file_ext - grid_size_y = len(list(glob(os.path.join(fovs_path,y_length_pattern)))) + grid_size_y = len(list(glob(os.path.join(fovs_path, y_length_pattern)))) - grid_size_x = len(list(glob(os.path.join(fovs_path,x_length_pattern)))) + grid_size_x = len(list(glob(os.path.join(fovs_path, x_length_pattern)))) - stitching_filename_pattern = coord_name+"{y}_{x}_"+str(z_index)+"_"+channel_name+"."+file_ext + stitching_filename_pattern = coord_name + "{y}_{x}_" + str(z_index) + "_" + channel_name + "." + file_ext - stitching_output_dir = 'COORD_'+coord_name+"_Z_"+str(z_index)+"_"+channel_name+"_stitched/" + stitching_output_dir = "COORD_" + coord_name + "_Z_" + str(z_index) + "_" + channel_name + "_stitched/" - tile_conf_name = "TileConfiguration_COORD_"+coord_name+"_Z_"+str(z_index)+"_"+channel_name+".txt" + tile_conf_name = "TileConfiguration_COORD_" + coord_name + "_Z_" + str(z_index) + "_" + channel_name + ".txt" - stitching_output_dir = os.path.join(fovs_path,stitching_output_dir) + stitching_output_dir = os.path.join(fovs_path, stitching_output_dir) os.makedirs(stitching_output_dir, exist_ok=True) - - sample_tile_name = coord_name+"0_0_"+str(z_index)+"_"+channel_name+"."+file_ext + sample_tile_name = coord_name + "0_0_" + str(z_index) + "_" + channel_name + "." + file_ext sample_tile_shape = cv2.imread(os.path.join(fovs_path, sample_tile_name)).shape - tile_downsampled_width=int(sample_tile_shape[1]*tile_downsampling) - tile_downsampled_height=int(sample_tile_shape[0]*tile_downsampling) - stitching_params = {'type':'Filename defined position', - 'order':'Defined by filename', - 'fusion_mode':'Linear Blending', - 'grid_size_x':grid_size_x, - 'grid_size_y':grid_size_y, - 'first_file_index_x':str(0), - 'first_file_index_y':str(0), - 'ignore_z_stage':True, - 'downsample_tiles':False, - 'tile_overlap':overlap_percent, - 'directory':fovs_path, - 'file_names':stitching_filename_pattern, - 'output_textfile_name':tile_conf_name, - 'fusion_method':'Linear Blending', - 'regression_threshold':str(reg_threshold), - 'max/avg_displacement_threshold':str(avg_displacement_threshold), - 'absolute_displacement_threshold':str(abs_displacement_threshold), - 'compute_overlap':recompute_overlap, - 'computation_parameters':'Save computation time (but use more RAM)', - 'image_output':'Write to disk', - 'output_directory':stitching_output_dir #, - #'x':str(tile_downsampling), - #'y':str(tile_downsampling), - #'width':str(tile_downsampled_width), - #'height':str(tile_downsampled_height), - #'interpolation':'Bicubic average' - } + tile_downsampled_width = int(sample_tile_shape[1] * tile_downsampling) + tile_downsampled_height = int(sample_tile_shape[0] * tile_downsampling) + stitching_params = { + "type": "Filename defined position", + "order": "Defined by filename", + "fusion_mode": "Linear Blending", + "grid_size_x": grid_size_x, + "grid_size_y": grid_size_y, + "first_file_index_x": str(0), + "first_file_index_y": str(0), + "ignore_z_stage": True, + "downsample_tiles": False, + "tile_overlap": overlap_percent, + "directory": fovs_path, + "file_names": stitching_filename_pattern, + "output_textfile_name": tile_conf_name, + "fusion_method": "Linear Blending", + "regression_threshold": str(reg_threshold), + "max/avg_displacement_threshold": str(avg_displacement_threshold), + "absolute_displacement_threshold": str(abs_displacement_threshold), + "compute_overlap": recompute_overlap, + "computation_parameters": "Save computation time (but use more RAM)", + "image_output": "Write to disk", + "output_directory": stitching_output_dir, # , + #'x':str(tile_downsampling), + #'y':str(tile_downsampling), + #'width':str(tile_downsampled_width), + #'height':str(tile_downsampled_height), + #'interpolation':'Bicubic average' + } plugin = "Grid/Collection stitching" @@ -208,11 +334,15 @@ def stitch_single_channel(self, fovs_path, channel_name, z_index, coord_name='', return stitching_output_dir + def images_identical(im_1, im_2): """Return True if two opencv arrays are exactly the same""" - return im_1.shape == im_2.shape and not (np.bitwise_xor(im_1,im_2).any()) + return im_1.shape == im_2.shape and not (np.bitwise_xor(im_1, im_2).any()) -def combine_stitched_channels(stitched_image_folder_path, write_multiscale_tiff = False, pixel_size_um=1.0, tile_side_length=1024, subresolutions=3): + +def combine_stitched_channels( + stitched_image_folder_path, write_multiscale_tiff=False, pixel_size_um=1.0, tile_side_length=1024, subresolutions=3 +): """Combines the three channel images created into one TIFF. Currently not recommended to run this with multiscale TIFF enabled, combining all channels/z-levels in one region of the acquisition into one OME-TIFF @@ -230,76 +360,76 @@ def combine_stitched_channels(stitched_image_folder_path, write_multiscale_tiff combine_to_mono = True if write_multiscale_tiff: - output_path = os.path.join(stitched_image_folder_path,"stitched_img.ome.tif") + output_path = os.path.join(stitched_image_folder_path, "stitched_img.ome.tif") else: - output_path = os.path.join(stitched_image_folder_path,"stitched_img.tif") + output_path = os.path.join(stitched_image_folder_path, "stitched_img.tif") if not combine_to_mono: - if images_identical(c1,c2) and images_identical(c2,c3): + if images_identical(c1, c2) and images_identical(c2, c3): combine_to_mono = True if not combine_to_mono: - c1 = c1[:,:,0] - c2 = c2[:,:,1] - c3 = c3[:,:,2] + c1 = c1[:, :, 0] + c2 = c2[:, :, 1] + c3 = c3[:, :, 2] if write_multiscale_tiff: - data = np.stack((c1,c2,c3), axis=0) + data = np.stack((c1, c2, c3), axis=0) else: - data = np.stack((c1,c2,c3),axis=-1) - axes = 'CYX' - channels = {'Name':['Channel 1', 'Channel 2', 'Channel 3']} + data = np.stack((c1, c2, c3), axis=-1) + axes = "CYX" + channels = {"Name": ["Channel 1", "Channel 2", "Channel 3"]} else: - data = c1[:,:,0] - axes = 'YX' + data = c1[:, :, 0] + axes = "YX" channels = None metadata = { - 'axes':axes, - 'SignificantBits':16 if data.dtype==np.uint8 else 8, - 'PhysicalSizeX':pixel_size_um, - 'PhysicalSizeY':pixel_size_um, - 'PhysicalSizeXUnit':'um', - 'PhysicalSizeYUnit':'um', - } + "axes": axes, + "SignificantBits": 16 if data.dtype == np.uint8 else 8, + "PhysicalSizeX": pixel_size_um, + "PhysicalSizeY": pixel_size_um, + "PhysicalSizeXUnit": "um", + "PhysicalSizeYUnit": "um", + } if channels is not None: - metadata['Channel'] = channels + metadata["Channel"] = channels options = dict( - photometric = 'rgb' if not combine_to_mono else 'minisblack', - tile = (tile_side_length, tile_side_length), - compression = 'jpeg', - resolutionunit='CENTIMETER', - maxworkers = 2 - ) + photometric="rgb" if not combine_to_mono else "minisblack", + tile=(tile_side_length, tile_side_length), + compression="jpeg", + resolutionunit="CENTIMETER", + maxworkers=2, + ) if write_multiscale_tiff: with tifffile.TiffWriter(output_path, bigtiff=True) as tif: - tif.write(data, subifds=subresolutions, - resolution=(1e4/pixel_size_um, 1e4/pixel_size_um), - metadata = metadata, - **options) - for level in range(subresolutions): - mag = 2**(level+1) - if combine_to_mono: - subdata = data[::mag,::mag] - else: - subdata = data[:,::mag,::mag] - tif.write( - subdata, - subfiletype=1, - resolution=(1e4/mag/pixel_size_um, 1e3/mag/pixel_size_um), - **options - ) - + tif.write( + data, + subifds=subresolutions, + resolution=(1e4 / pixel_size_um, 1e4 / pixel_size_um), + metadata=metadata, + **options + ) + for level in range(subresolutions): + mag = 2 ** (level + 1) if combine_to_mono: - thumbnail = (data[::8,::8] >> 2).astype('uint8') + subdata = data[::mag, ::mag] else: - thumbnail = (data[0,::8,::8] >> 2).astype('uint8') - tif.write(thumbnail,metadata={'Name':'thumbnail'}) + subdata = data[:, ::mag, ::mag] + tif.write( + subdata, subfiletype=1, resolution=(1e4 / mag / pixel_size_um, 1e3 / mag / pixel_size_um), **options + ) + + if combine_to_mono: + thumbnail = (data[::8, ::8] >> 2).astype("uint8") + else: + thumbnail = (data[0, ::8, ::8] >> 2).astype("uint8") + tif.write(thumbnail, metadata={"Name": "thumbnail"}) else: cv2.imwrite(output_path, data) - channel_files = [os.path.join(stitched_image_folder_path,'img_t1_z1_c')+str(i+1) for i in range(3)] + channel_files = [os.path.join(stitched_image_folder_path, "img_t1_z1_c") + str(i + 1) for i in range(3)] for filename in channel_files: try: diff --git a/software/toupcam_tests.py b/software/toupcam_tests.py index 75e32efd2..a0cfe7392 100644 --- a/software/toupcam_tests.py +++ b/software/toupcam_tests.py @@ -7,13 +7,13 @@ sn = get_sn_by_model(model) -camera = Camera(sn =sn,rotate_image_angle = ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE) +camera = Camera(sn=sn, rotate_image_angle=ROTATE_IMAGE_ANGLE, flip_image=FLIP_IMAGE) camera.open() -camera.set_gain_mode('HCG') +camera.set_gain_mode("HCG") -camera.set_resolution(2000,2000) +camera.set_resolution(2000, 2000) camera.set_continuous_acquisition() @@ -26,7 +26,7 @@ time.sleep(0.5) -camera.set_pixel_format('MONO16') +camera.set_pixel_format("MONO16") time.sleep(0.5) @@ -34,7 +34,7 @@ time.sleep(0.5) -camera.set_ROI(10,10,32,32) +camera.set_ROI(10, 10, 32, 32) time.sleep(0.5) @@ -42,7 +42,7 @@ print(myframe) print(myframe.shape) print(myframe.dtype) -camera.set_pixel_format('MONO8') +camera.set_pixel_format("MONO8") time.sleep(0.5) myframe2 = camera.read_frame() @@ -59,12 +59,11 @@ print(myframe2.dtype) - -camera.set_ROI(0,0,0,0) +camera.set_ROI(0, 0, 0, 0) time.sleep(0.5) -camera.set_ROI(2500,2500,3000,3000) +camera.set_ROI(2500, 2500, 3000, 3000) time.sleep(1.0)