diff --git a/.gitignore b/.gitignore index f73e2fd..976055a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__ /**/.ipynb_checkpoints **/*.ipynb_checkpoints/ venv/ +mesoSPIM/test/fixtures/ *.log *.bin .idea @@ -16,14 +17,9 @@ prototypes/python-essentials/temp\.tif mesoSPIM/src/devices/stages/galil/gclib/gclib\.py mesoSPIM/src/devices/stages/galil/gclib\.py mesoSPIM/src/devices/stages/galil/gclib/__init__\.py - - +mesoSPIM/config/etl_parameters/* mesoSPIM/config/* !mesoSPIM/config/demo_config.py - -mesoSPIM/config/etl_parameters/*.* -!mesoSPIM/config/etl_parameters/ETL-parameters.csv -mesoSPIM/src/mesoSPIM_DemoSerial\.py mesoSPIM/src/devices/zoom/dynamixel\.zip mesoSPIM/acq/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b311e46..9c95701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,55 @@ +## Latest version +* :gem: Support of multiple PI single-axis controllers, thanks to #52 by @drchrisch. +Note the changes in config file: single multi-axis controller (C-884) is initialized by `'PI_1controllerNstages'`, +while multiple single-axis controllers (C-663) by `'PI_NcontrollersNstages'`. + +## Version [0.1.5] +* :gem: Improved Tiling Wizard: + * buttons `x-start, x-end, y-start, y-end` added for easier navigation: + no need to search for corners of imaginary box around the sample. + * `left, then right` illuminations can be created automatically for each tile: no need for manual duplication + and changing the illumination directions in the Acquisition Manager. + +* :gem: Improved saving options in Fiji/BigStitcher H5 format: + * `laser`, `illumination`, `angle` attributes are saved in the BigStitcher XML file. + * (optional) downsampling and compression are supported. +* :gem: Image window got `Adjust levels` button for automatic intensity adjustment. +* :gem: Image window got optional `Box overlay` to help measure sample dimensions. +* :mag: Tests for tiling and serial communication are created. +* :bug: **Bugfix:** long-standing `permission denied` issues with serial communication +to filter wheel and zoom servo are fixed. +The fix opens serial ports once and keeps them open during the session. +The root cause was due to laser control software polling serial ports regularly, thus blocking access to them. + +## Version [0.1.4] +### Features & updates +* :warning: **Config files need to be updated** Please note: Updating to this version requires updating your microscope configuration file. Please copy the new configuration options from the `demo.cfg` file into your config files. +* :warning: :gem: **New handling of config files** - If there is a single config file (without a 'demo' prefix in the filename and apart from the `demo_config.py`-file) in the config folder, the software will automatically load this file. Otherwise, the config selection GUI is opened. This is especially helpful when operating a mesoSPIM with multiple users. Thanks to @raacampbell for this feature! +* :gem: **New: Writing HDF5** - If all rows in the acquistion manager contain the same file name (ending in `.h5`), the entire acquisition list will be saved in a single hdf5 file and a XML created automatically. Both can then be loaded into [Bigstitcher](https://imagej.net/BigStitcher) for stitching & multiview fusion. +For this, the `npy2bdv` package by @nvladimus needs to be installed via `python -m pip install npy2bdv` +* :gem: **New: Dark mode** - If the `dark_mode` option in the config file is set to `True`, the user interface appears in a dark mode. For this, the `qdarkstyle` package needs to be installed via `python -m pip install qdarkstyle`. +* :gem: **New: Camera and Acquisition Manager Windows can be reopened** - A new menu allows the camera and acquisition manager windows to be reopened in case they get closed. The same menu bar allows exiting the program as well. +* :gem: **New: Disabling arrow buttons** - To allow mesoSPIM configurations with less than 5 motorized stages, the arrow buttons in the main window can now be disabled in the configuration file. Typical examples are a mesoSPIM without a rotation stage or a mesoSPIM using only a single motorized z-stage. This feature can also be useful if the serial connection to the stages is too slow and pressing the arrow buttons leads to incorrect movements. +* :gem: **Interactive IPython console** - If the software is launched via `python mesoSPIM-control.py -C`, an interactive IPython console is launched for debugging. Feature by @raacampbell. +* :gem: **Command-line demo mode option** - If the software is launched via `python mesoSPIM-control.py -D`, it launches automatically into demo mode. Feature by @raacampbell. +* :gem: **New: Support for PCO cameras** - PCO cameras with lightsheet mode are now supported. For this the `pco` Python package needs to be installed via `python -m pip install pco`. Currently, the only tested camera is the PCO panda 4.2 bi with lightsheet firmware. +* :gem: **New: Support for Sutter Lambda 10B Filter Controller** Thanks to Kevin Dean @AdvancedImagingUTSW, Sutter filter wheels are now supported. +* :gem: **New: Support for Physik Instrumente stepper motor stages in a XYZ configuration** Thanks to @drchrisch, a mesoSPIM configuration ('PI_xyz') using stepper motor stages for sample movement is now supported. Please note that this is currently not supporting focus movements or sample rotations. +* :gem: **New: Support for Physik Instrumente C-863 controller in a single-stage config** To allow setting up a simplified mesoSPIM using only a single motorized z-stage (all other stages need to be manually operated), the combination of the C-863 motor controller and L-509 stage is now supported ('PI_z') +* :sparkles: **Improvement:** **Disabling movement buttons in the GUI** By modifying the `ui_options` dictionary in the configuration file, the X,Y,Z, focus, rotation, and load/unload buttons can be disabled. This allows modifing the UI for mesoSPIM setups which do not utilize the full set of 5 axes. Disabled buttons are greyed out. +* :sparkles: **Improvement:** **Updated multicolor tiling wizard** The tiling wizard now displays the FOV size and calculates the X and Y FOV offsets using a percentage setting. For this, the pixel size settings in the configuration file need to be set correctly. +* :sparkles: **Improvement:** **Physik Instrumente stages now report their referencing status after startup in the logfile** This allows for easier diagnosis of unreferenced stages during startup. Feature by @raacampbell. +* :bug: **Bugfix:** Binning was not working properly with all cameras. +* :bug: **Bugfix:** Removed unnecessary imports. +* :bug: **Bugfix:** Laser power setting `max_laser_voltage` was always 10V, ignoring the config file. This can damage some lasers that operate on lower command voltage. + +### Contributors +* Fabian Voigt (@ffvoigt) +* Nikita Vladimirov (@nvladimus) +* Kevin Dean (@AdvancedImagingUTSW) +* Christian Schulze (@drchrisch) +* Rob Campbell (@raacampbell) + ## Version [0.1.3] - March 13, 2020 * :warning: **Depending on your microscope configuration, this release breaks backward compatibility with previous configuration files. If necessary, update your configuration file using `demo_config.py` as an example.** * :warning: **There are new startup parameters in the config file - make sure to update your config files accordingly**. For example, `average_frame_rate` has been added. @@ -27,7 +79,7 @@ to f_start and at z_end, the detection path focus is at z_end. This allows imagi * :bug: **Bugfix #34:** Fixed: Last frame in a stack is blank due to an off-by-one error * :bug: **Bugfix #35:** Fixed: Software crashes when one folder (to save data in) in the acquisition list does not exist ---- +-- ## Version [0.1.2] - August 19th, 2019 * **New:** Logging is now supported. Logfiles go in the `log` folder. diff --git a/README.md b/README.md index 65c63a5..08afaf0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ If you are updating `mesoSPIM-control` from a previous version: Please have a cl ### Prerequisites * Windows 7 or Windows 10 -* Python >3.6 +* Python >=3.6 #### Device drivers * [Hamamatsu DCAM API](https://dcam-api.com/) when using Hamamatsu Orca Flash 4.0 V2 or V3 sCMOS cameras. To test camera functionality, [HCImage](https://dcam-api.com/hamamatsu-software/) can be used. @@ -24,26 +24,31 @@ If you are updating `mesoSPIM-control` from a previous version: Please have a cl * [Robotis DynamixelSDK](https://github.com/ROBOTIS-GIT/DynamixelSDK/releases) for Dynamixel Zoom servos. Make sure you download version 3.5.4 of the SDK. #### Python -mesoSPIM-control is usually running with [Anaconda](https://www.anaconda.com/download/) using a >3.6 Python. For a clean python install, the following packages are necessary (part of Anaconda): - -* csv -* traceback -* pprint -* numpy -* scipy -* ctypes -* importlib -* PyQt5 (if there are problems with PyQt5 such as `ModuleNotFoundError: No module named 'PyQt5.QtWinExtras` after starting `mesoSPIM-control`, try reinstalling PyQt5 by: `python -m pip install --user -I PyQt5` and `python -m pip install --user -I PyQt5-sip`) - -In addition (for Anaconda), the following packages need to be installed: -* nidaqmx (`python -m pip install nidaqmx`) -* indexed (`python -m pip install indexed`) -* serial (`python -m pip install pyserial`) -* pyqtgraph (`python -m pip install pyqtgraph`) -* pywinusb (`python -m pip install pywinusb`) -* PIPython (part of the Physik Instrumente software collection. Unzip it, `cd` to the directory with the Anaconda terminal as an admin user, then install with `python setup.py install`. Test install with test installation with `import pipython`). You can also download PIPython [here](https://github.com/royerlab/pipython) -* tifffile (`python -m pip install tifffile`) -* ([PyVCAM when using Photometrics cameras](https://github.com/Photometrics/PyVCAM) +mesoSPIM-control is usually running with [Anaconda](https://www.anaconda.com/download/) using a >=3.6 Python. +##### Anaconda +(optional) Create and activate a Python 3.6 environment from Anaconda prompt (you can use any name instead of `py36`): +``` +conda create -n py36 python=3.6 +conda activate py36 +``` +The step above is optional because the latest Python 3.8 is backward compatible with Python 3.6 code. + +Many libraries are already included in Anaconda. +Install mesoSPIM-specific libraries: +``` +pip install -r requirements-anaconda.txt +``` + +##### Clean python +For a clean (non-Anaconda) python interpreter, install all required libraries: +``` +pip install -r requirements-clean-python.txt +``` + +##### Additional libraries +Camera libraries are not hosted on PyPi and need to be installed manually: +* [PyVCAM when using a Photometrics camera](https://github.com/Photometrics/PyVCAM) +* pco (`python -m pip install pco`) when using a PCO camera ([Link](https://pypi.org/project/pco/)). A Version ≥0.1.3 is recommended. #### Preparing python bindings for device drivers * For PI stages, copy `C:\ProgramData\PI\GCSTranslator\PI_GCS2_DLL_x64.dll` in the main mesoSPIM folder: `PI_GCS2_DLL_x64.dll` @@ -63,12 +68,30 @@ At time of writing that means the master trigger out (`PXI6259/port0/line1`) sho Use BNC T connectors to split each analog output line to both lasers. * You will need to set the ThorLabs shutter controllers to run on TTL input mode. -#### Run the software. +## Launching +#### From Anaconda prompt ``` +conda activate py36 python mesoSPIM_Control.py ``` -After launch, it will prompt you for a configuration file. Please choose a file -with demo devices (e.g. `DemoStage`) for testing. +The software will now start. If you have multiple configuration files you will be prompted to choose one. + +#### From start_mesoSPIM.bat file +Open the `start_mesoSPIM.bat` file in text editor and configure Anaconda and `py36` path to your own. +Once done, launch mesoSPIM by double-clicking the file. +Optionally, create a Windows shortcut (via right-click menu) and place it e.g. on your desktop. +Using shortcut saves a lot of time. + +#### Starting with interactive console +You may also run the software with an interactive IPython console for de-bugging: +``` +python mesoSPIM_Control.py -C +``` +For example, executing `mSpim.state.__dict__` in this console will show the current mesoSPIM state. + +## Troubleshooting +If there are problems with PyQt5 such as `ModuleNotFoundError: No module named 'PyQt5.QtWinExtras` after starting +`mesoSPIM-control`, try reinstalling PyQt5 by: `python -m pip install --user -I PyQt5` and `python -m pip install --user -I PyQt5-sip`) -#### Documentation for users +## Documentation for users For instructions on how to use mesoSPIM-control, please check out the documentation [here](https://github.com/mesoSPIM/mesoSPIM-powerpoint-documentation). diff --git a/mesoSPIM/config/demo_config.py b/mesoSPIM/config/demo_config.py index 5b218fa..375924a 100644 --- a/mesoSPIM/config/demo_config.py +++ b/mesoSPIM/config/demo_config.py @@ -8,6 +8,18 @@ (The extension has to be .py). ''' +''' +Dark mode: Renders the UI dark +''' +ui_options = {'dark_mode' : True, # Dark mode: Renders the UI dark if enabled + 'enable_x_buttons' : True, # Here, specific sets of UI buttons can be disabled + 'enable_y_buttons' : True, + 'enable_z_buttons' : True, + 'enable_f_buttons' : True, + 'enable_rotation_buttons' : True, + 'enable_loading_buttons' : True, + } + ''' Waveform output for Galvos, ETLs etc. ''' @@ -186,9 +198,12 @@ and safety limits. The rotation position defines a XYZ position (in absolute coordinates) where sample rotation is safe. Additional hardware dictionaries (e.g. pi_parameters) define the stage configuration details. +All positions are absolute. + +'stage_type' options: 'DemoStage', 'PI_1controllerNstages' (former 'PI'), 'PI_NcontrollersNstages' ''' -stage_parameters = {'stage_type' : 'DemoStage', # 'DemoStage' or 'PI' or other configs found in mesoSPIM_serial.py +stage_parameters = {'stage_type' : 'DemoStage', # 'DemoStage'. 'PI_1controllerNstages', 'PI_NcontrollersNstages', see below 'startfocus' : -10000, 'y_load_position': -86000, 'y_unload_position': -120000, @@ -198,8 +213,8 @@ 'y_min' : -160000, 'z_max' : 99000, 'z_min' : -99000, - 'f_max' : 99000, - 'f_min' : -99000, + 'f_max' : 10000, + 'f_min' : -10000, 'theta_max' : 999, 'theta_min' : -999, 'x_rot_position': 0, @@ -207,45 +222,31 @@ 'z_rot_position': 66000, } -''' -Depending on the stage hardware, further dictionaries define further details of the stage configuration - -For a standard mesoSPIM V4 with PI stages, the following pi_parameters are necessary (replace the -serialnumber with the one of your controller): - +'''' +If 'stage_type' = 'PI_1controllerNstages' (vanilla mesoSPIM V5 with single 6-axis controller): pi_parameters = {'controllername' : 'C-884', - 'stages' : ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE'), + 'stages' : ('L-509.20DG10','L-509.40DG10','L-509.20DG10','M-060.DG','M-406.4PD','NOSTAGE'), 'refmode' : ('FRF',), - 'serialnum' : ('118015797'), + 'serialnum' : ('118075764'), } -For a standard mesoSPIM V5 with PI stages, the following pi_parameters are necessary (replace the -serialnumber with the one of your controller): - -pi_parameters = {'controllername' : 'C-884', - 'stages' : ('L-509.20DG10','L-509.40DG10','L-509.20DG10','M-060.DG','M-406.4PD','NOSTAGE'), - 'refmode' : ('FRF',), - 'serialnum' : ('118015799'), +If 'stage_type' = 'PI_NcontrollersNstages' (mesoSPIM V5 with multiple single-axis controllers): +pi_parameters = {'axes_names': ('x', 'y', 'z', 'theta', 'f'), + 'stages': ('L-509.20SD00', 'L-509.40SD00', 'L-509.20SD00', None, 'MESOSPIM_FOCUS'), + 'controllername': ('C-663', 'C-663', 'C-663', None, 'C-663'), + 'serialnum': ('**********', '**********', '**********', None, '**********'), + 'refmode': ('FRF', 'FRF', 'FRF', None, 'RON') + } ''' ''' Filterwheel configuration -''' - -''' -For a DemoFilterWheel, no COMport needs to be specified, for a Ludl Filterwheel, -a valid COMport is necessary. +For a DemoFilterWheel, no COMport needs to be specified. +For a Ludl Filterwheel, a valid COMport is necessary. Ludl marking 10 = position 0. ''' filterwheel_parameters = {'filterwheel_type' : 'DemoFilterWheel', # 'DemoFilterWheel' or 'Ludl' 'COMport' : 'COM53'} -# Ludl marking 10 = position 0 - -''' - -A Ludl double filter wheel can be -''' - filterdict = {'Empty-Alignment' : 0, # Every config should contain this '405-488-647-Tripleblock' : 1, '405-488-561-640-Quadrupleblock' : 2, @@ -259,9 +260,6 @@ ''' Zoom configuration -''' - -''' For the DemoZoom, servo_id, COMport and baudrate do not matter. For a Dynamixel zoom, these values have to be there ''' @@ -302,10 +300,18 @@ '6.3x' : 1.03} ''' -Initial acquisition parameters + HDF5 parameters, if this format is used for data saving (optional). +Downsampling and compression slows down writing by 5x - 10x, use with caution. +Imaris can open these files if no subsampling and no compression is used. +''' +hdf5 = {'subsamp': ((1, 1, 1),), #((1, 1, 1),) no subsamp, ((1, 1, 1), (1, 4, 4)) for 2-level (z,y,x) subsamp. + 'compression': None, # None, 'gzip', 'lzf' + 'flip_xyz': (True, True, False) # match BigStitcher coordinates to mesoSPIM axes. + } +''' +Initial acquisition parameters Used as initial values after startup - When setting up a new mesoSPIM, make sure that: * 'max_laser_voltage' is correct (5 V for Toptica MLEs, 10 V for Omicron SOLE) * 'galvo_l_amplitude' and 'galvo_r_amplitude' (in V) are correct (not above the max input allowed by your galvos) @@ -369,4 +375,4 @@ 'camera_binning':'1x1', 'camera_sensor_mode':'ASLM', 'average_frame_rate': 4.969, -} +} \ No newline at end of file diff --git a/mesoSPIM/config/etl_parameters/ETL-parameters.csv b/mesoSPIM/config/etl_parameters/ETL-parameters.csv index 35e9dea..070ef24 100644 --- a/mesoSPIM/config/etl_parameters/ETL-parameters.csv +++ b/mesoSPIM/config/etl_parameters/ETL-parameters.csv @@ -1,134 +1,67 @@ -Objective;Wavelength;Zoom;ETL-Left-Offset;ETL-Left-Amp;ETL-Right-Offset;ETL-Right-Amp - -1x;405 nm;0.63x;2.4900000000000038;0.6950000000000021;2.655999999999999;1.2649999999999966 - -1x;405 nm;0.8x;2.4950000000000037;0.7649999999999995;2.6500000000000004;0.725 - -1x;405 nm;1x;2.4900000000000038;0.61;2.6550000000000002;0.5949999999999999 - -1x;405 nm;1.25x;2.418000000000004;0.6830000000000002;2.6609999999999996;0.5439999999999998 - -1x;405 nm;1.6x;2.465999999999994;0.43899999999999995;2.687999999999999;0.42499999999999993 - -1x;405 nm;2x;2.498999999999993;0.2859999999999996;2.6669999999999994;0.312 - -1x;405 nm;2.5x;2.4839999999999955;0.2900000000000001;2.667;0.20799999999999985 - -1x;405 nm;3.2x;2.4809999999999928;0.243;2.67;0.21900000000000014 - -1x;405 nm;4x;2.512999999999999;0.197;2.67;0.25 - -1x;405 nm;5x;2.486999999999993;0.17800000000000007;2.6850000000000005;0.2 - -1x;405 nm;6.3x;2.67;0.25;2.677999999999998;0.058999999999999886 - -1x;488 nm;0.63x;2.336000000000007;1.3;2.5560000000000027;1.24 - -1x;488 nm;0.8x;2.334000000000004;0.942;2.5919999999999996;0.936 - -1x;488 nm;1x;2.3939999999999975;0.8880000000000002;2.4920000000000018;0.804 - -1x;488 nm;1.25x;2.3960000000000017;0.692;2.5759999999999974;0.6260000000000001 - -1x;488 nm;1.6x;2.3880000000000026;0.524;2.547999999999999;0.449 - -1x;488 nm;2x;2.397000000000001;0.4610000000000001;2.5450000000000004;0.366 - -1x;488 nm;2.5x;2.386999999999998;0.3990000000000001;2.5870000000000006;0.26999999999999996 - -1x;488 nm;3.2x;2.3499999999999965;0.251;2.5459999999999994;0.23 - -1x;488 nm;4x;2.398000000000001;0.158;2.7359999999999944;0.09600000000000002 - -1x;488 nm;5x;2.3949999999999987;0.21100000000000008;2.593000000000001;0.13299999999999998 - -1x;488 nm;6.3x;2.3919999999999972;0.14100000000000001;2.590000000000002;0.11099999999999996 - -1x;515 nm;0.63x;2.422999999999997;1.123;2.4930000000000025;1.1460000000000001 - -1x;515 nm;0.8x;2.3970000000000007;0.81;2.571999999999999;0.8360000000000001 - -1x;515 nm;1x;2.3919999999999986;0.782;2.4500000000000024;0.702 - -1x;515 nm;1.25x;2.396999999999999;0.7190000000000001;2.5840000000000014;0.5319999999999998 - -1x;515 nm;1.6x;2.3340000000000067;0.41400000000000003;2.58899999999999;0.45099999999999996 - -1x;515 nm;2x;2.3899999999999992;0.388;2.584;0.392 - -1x;515 nm;2.5x;2.3920000000000075;0.317;2.59199999999999;0.26199999999999996 - -1x;515 nm;3.2x;2.3910000000000076;0.20499999999999974;2.58999999999999;0.174 - -1x;515 nm;4x;2.388;0.219;2.596999999999998;0.17100000000000004 - -1x;515 nm;5x;2.3920000000000075;0.16799999999999998;2.59499999999999;0.12599999999999997 - -1x;515 nm;6.3x;2.389000000000008;0.12300000000000003;2.59299999999999;0.09399999999999992 - -1x;561 nm;0.63x;2.361;1.086;2.579000000000002;1.107999999999997 - -1x;561 nm;0.8x;2.3620000000000014;0.87;2.547;0.75 - -1x;561 nm;1x;2.399999999999999;0.744;2.7329999999999974;0.594 - -1x;561 nm;1.25x;2.3959999999999995;0.688;2.5409999999999955;0.586 - -1x;561 nm;1.6x;2.3800000000000017;0.584;2.628999999999992;0.472 - -1x;561 nm;2x;2.3800000000000012;0.504;2.5990000000000024;0.296 - -1x;561 nm;2.5x;2.39;0.37600000000000017;2.593;0.324 - -1x;561 nm;3.2x;2.4259999999999997;0.243;2.626;0.184 - -1x;561 nm;4x;2.410999999999999;0.216;2.611999999999997;0.158 - -1x;561 nm;5x;2.387;0.13599999999999995;2.598;0.18600000000000003 - -1x;561 nm;6.3x;2.391;0.10400000000000001;2.5939999999999994;0.12800000000000003 - -1x;594 nm;0.63x;2.307999999999999;1.314;2.492000000000001;1.14 - -1x;594 nm;0.8x;2.4180000000000015;1.12;2.537;0.78 - -1x;594 nm;1x;2.4180000000000015;0.9000000000000004;2.5949999999999984;0.8300000000000001 - -1x;594 nm;1.25x;2.403000000000002;0.6150000000000002;2.5949999999999984;0.5299999999999999 - -1x;594 nm;1.6x;2.4080000000000017;0.44000000000000006;2.5999999999999983;0.35999999999999976 - -1x;594 nm;2x;2.403000000000002;0.37000000000000005;2.5519999999999996;0.31 - -1x;594 nm;2.5x;2.398000000000002;0.3900000000000002;2.5999999999999983;0.24499999999999966 - -1x;594 nm;3.2x;2.4040000000000084;0.23799999999999957;2.6029999999999998;0.21500000000000002 - -1x;594 nm;4x;2.405000000000008;0.26400000000000007;2.6060000000000008;0.1419999999999999 - -1x;594 nm;5x;2.4010000000000065;0.14399999999999996;2.6039999999999934;0.12999999999999998 - -1x;594 nm;6.3x;2.401000000000008;0.13800000000000004;2.5999999999999908;0.09199999999999992 - -1x;647 nm;0.63x;2.379000000000006;1.344;2.5710000000000024;1.224 - -1x;647 nm;0.8x;2.423000000000008;0.9560000000000002;2.6049999999999938;1.018 - -1x;647 nm;1x;2.495;0.87;2.602999999999993;0.7479999999999999 - -1x;647 nm;1.25x;2.423000000000008;0.6479999999999999;2.6029999999999927;0.582 - -1x;647 nm;1.6x;2.4210000000000083;0.5359999999999999;2.6129999999999955;0.4299999999999998 - -1x;647 nm;2x;2.3930000000000025;0.432;2.6189999999999993;0.366 - -1x;647 nm;2.5x;2.417000000000007;0.3799999999999999;2.606999999999996;0.27999999999999997 - -1x;647 nm;3.2x;2.415000000000008;0.23199999999999998;2.608999999999992;0.24199999999999977 - -1x;647 nm;4x;2.4190000000000085;0.2;2.6129999999999924;0.20800000000000002 - -1x;647 nm;5x;2.415000000000002;0.14500000000000002;2.6099999999999923;0.12499999999999994 - -1x;647 nm;6.3x;2.481;0.07400000000000001;2.7779999999999996;0.092 - +Objective;Wavelength;Zoom;ETL-Left-Offset;ETL-Left-Amp;ETL-Right-Offset;ETL-Right-Amp +1x;405 nm;0.63x;2.4900000000000038;0.6950000000000021;2.655999999999999;1.2649999999999966 +1x;405 nm;0.8x;2.4950000000000037;0.7649999999999995;2.6500000000000004;0.725 +1x;405 nm;1x;2.4900000000000038;0.61;2.6550000000000002;0.5949999999999999 +1x;405 nm;1.25x;2.418000000000004;0.6830000000000002;2.6609999999999996;0.5439999999999998 +1x;405 nm;1.6x;2.465999999999994;0.43899999999999995;2.687999999999999;0.42499999999999993 +1x;405 nm;2x;2.498999999999993;0.2859999999999996;2.6669999999999994;0.312 +1x;405 nm;2.5x;2.4839999999999955;0.2900000000000001;2.667;0.20799999999999985 +1x;405 nm;3.2x;2.4809999999999928;0.243;2.67;0.21900000000000014 +1x;405 nm;4x;2.512999999999999;0.197;2.67;0.25 +1x;405 nm;5x;2.486999999999993;0.17800000000000007;2.6850000000000005;0.2 +1x;405 nm;6.3x;2.67;0.25;2.677999999999998;0.058999999999999886 +1x;488 nm;0.63x;2.204000000000013;1.0;2.5040000000000053;1.0 +1x;488 nm;0.8x;2.334000000000004;0.942;2.5919999999999996;0.936 +1x;488 nm;1x;2.3939999999999975;0.8880000000000002;2.4920000000000018;0.804 +1x;488 nm;1.25x;2.3960000000000017;0.692;2.5759999999999974;0.6260000000000001 +1x;488 nm;1.6x;2.3880000000000026;0.524;2.547999999999999;0.449 +1x;488 nm;2x;2.397000000000001;0.4610000000000001;2.5450000000000004;0.366 +1x;488 nm;2.5x;2.386999999999998;0.3990000000000001;2.5870000000000006;0.26999999999999996 +1x;488 nm;3.2x;2.2040000000000157;0.2;2.5140000000000025;0.2 +1x;488 nm;4x;2.398000000000001;0.158;2.7359999999999944;0.09600000000000002 +1x;488 nm;5x;2.3949999999999987;0.21100000000000008;2.593000000000001;0.13299999999999998 +1x;488 nm;6.3x;2.2000000000000175;0.1;2.5240000000000045;0.1 +1x;515 nm;0.63x;2.422999999999997;1.123;2.4930000000000025;1.1460000000000001 +1x;515 nm;0.8x;2.3970000000000007;0.81;2.571999999999999;0.8360000000000001 +1x;515 nm;1x;2.3919999999999986;0.782;2.4500000000000024;0.702 +1x;515 nm;1.25x;2.396999999999999;0.7190000000000001;2.5840000000000014;0.5319999999999998 +1x;515 nm;1.6x;2.3340000000000067;0.41400000000000003;2.58899999999999;0.45099999999999996 +1x;515 nm;2x;2.3899999999999992;0.388;2.584;0.392 +1x;515 nm;2.5x;2.3920000000000075;0.317;2.59199999999999;0.26199999999999996 +1x;515 nm;3.2x;2.3910000000000076;0.20499999999999974;2.58999999999999;0.174 +1x;515 nm;4x;2.388;0.219;2.596999999999998;0.17100000000000004 +1x;515 nm;5x;2.3920000000000075;0.16799999999999998;2.59499999999999;0.12599999999999997 +1x;515 nm;6.3x;2.389000000000008;0.12300000000000003;2.59299999999999;0.09399999999999992 +1x;561 nm;0.63x;2.361;1.086;2.579000000000002;1.107999999999997 +1x;561 nm;0.8x;2.3620000000000014;0.87;2.547;0.75 +1x;561 nm;1x;2.399999999999999;0.744;2.7329999999999974;0.594 +1x;561 nm;1.25x;2.3959999999999995;0.688;2.5409999999999955;0.586 +1x;561 nm;1.6x;2.3800000000000017;0.584;2.628999999999992;0.472 +1x;561 nm;2x;2.3800000000000012;0.504;2.5990000000000024;0.296 +1x;561 nm;2.5x;2.39;0.37600000000000017;2.593;0.324 +1x;561 nm;3.2x;2.4259999999999997;0.243;2.626;0.184 +1x;561 nm;4x;2.410999999999999;0.216;2.611999999999997;0.158 +1x;561 nm;5x;2.387;0.13599999999999995;2.598;0.18600000000000003 +1x;561 nm;6.3x;2.391;0.10400000000000001;2.5939999999999994;0.12800000000000003 +1x;594 nm;0.63x;2.307999999999999;1.314;2.492000000000001;1.14 +1x;594 nm;0.8x;2.4180000000000015;1.12;2.537;0.78 +1x;594 nm;1x;2.4180000000000015;0.9000000000000004;2.5949999999999984;0.8300000000000001 +1x;594 nm;1.25x;2.403000000000002;0.6150000000000002;2.5949999999999984;0.5299999999999999 +1x;594 nm;1.6x;2.4080000000000017;0.44000000000000006;2.5999999999999983;0.35999999999999976 +1x;594 nm;2x;2.403000000000002;0.37000000000000005;2.5519999999999996;0.31 +1x;594 nm;2.5x;2.398000000000002;0.3900000000000002;2.5999999999999983;0.24499999999999966 +1x;594 nm;3.2x;2.4040000000000084;0.23799999999999957;2.6029999999999998;0.21500000000000002 +1x;594 nm;4x;2.405000000000008;0.26400000000000007;2.6060000000000008;0.1419999999999999 +1x;594 nm;5x;2.4010000000000065;0.14399999999999996;2.6039999999999934;0.12999999999999998 +1x;594 nm;6.3x;2.401000000000008;0.13800000000000004;2.5999999999999908;0.09199999999999992 +1x;647 nm;0.63x;2.379000000000006;1.344;2.5710000000000024;1.224 +1x;647 nm;0.8x;2.423000000000008;0.9560000000000002;2.6049999999999938;1.018 +1x;647 nm;1x;2.495;0.87;2.602999999999993;0.7479999999999999 +1x;647 nm;1.25x;2.423000000000008;0.6479999999999999;2.6029999999999927;0.582 +1x;647 nm;1.6x;2.4210000000000083;0.5359999999999999;2.6129999999999955;0.4299999999999998 +1x;647 nm;2x;2.3930000000000025;0.432;2.6189999999999993;0.366 +1x;647 nm;2.5x;2.417000000000007;0.3799999999999999;2.606999999999996;0.27999999999999997 +1x;647 nm;3.2x;2.415000000000008;0.23199999999999998;2.608999999999992;0.24199999999999977 +1x;647 nm;4x;2.4190000000000085;0.2;2.6129999999999924;0.20800000000000002 +1x;647 nm;5x;2.415000000000002;0.14500000000000002;2.6099999999999923;0.12499999999999994 +1x;647 nm;6.3x;2.481;0.07400000000000001;2.7779999999999996;0.092 diff --git a/mesoSPIM/gui/mesoSPIM_CameraWindow.ui b/mesoSPIM/gui/mesoSPIM_CameraWindow.ui index 23fe68e..3cbc412 100644 --- a/mesoSPIM/gui/mesoSPIM_CameraWindow.ui +++ b/mesoSPIM/gui/mesoSPIM_CameraWindow.ui @@ -6,8 +6,8 @@ 0 0 - 1114 - 1015 + 1200 + 1080 @@ -17,6 +17,53 @@ + + + + + + Adjust levels + + + + + + + + 110 + 0 + + + + Image overlay + + + + Overlay: none + + + + + Box roi + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/mesoSPIM/gui/mesoSPIM_MainWindow.ui b/mesoSPIM/gui/mesoSPIM_MainWindow.ui index 07e0c75..25adacc 100644 --- a/mesoSPIM/gui/mesoSPIM_MainWindow.ui +++ b/mesoSPIM/gui/mesoSPIM_MainWindow.ui @@ -52,8 +52,8 @@ 0 0 - 924 - 1222 + 927 + 1205 @@ -424,6 +424,11 @@ 51 + + + 16 + + false @@ -658,6 +663,11 @@ 401 + + + 16 + + XY @@ -846,6 +856,11 @@ 401 + + + 16 + + Z @@ -964,6 +979,7 @@ + 16 50 false @@ -1005,6 +1021,11 @@ 41 + + + 16 + + µm @@ -1029,6 +1050,7 @@ + 16 50 false @@ -1170,6 +1192,11 @@ 41 + + + 16 + + µm @@ -1327,6 +1354,11 @@ 41 + + + 16 + + ° @@ -1354,6 +1386,7 @@ + 16 50 false false @@ -1468,6 +1501,11 @@ Position 631 + + + 14 + + Tunable lenses @@ -1480,6 +1518,11 @@ Position 31 + + + 14 + + Offset @@ -1493,6 +1536,11 @@ Position 31 + + + 14 + + Amplitude @@ -1509,6 +1557,11 @@ Position 31 + + + 14 + + V @@ -1534,6 +1587,11 @@ Position 31 + + + 14 + + Left @@ -1547,6 +1605,11 @@ Position 31 + + + 14 + + Right @@ -1563,6 +1626,11 @@ Position 31 + + + 14 + + V @@ -1591,6 +1659,11 @@ Position 31 + + + 14 + + V @@ -1619,6 +1692,11 @@ Position 31 + + + 14 + + V @@ -1689,6 +1767,11 @@ Position 31 + + + 14 + + Increment @@ -1705,6 +1788,11 @@ Position 31 + + + 14 + + V @@ -1730,6 +1818,11 @@ Position 131 + + + 14 + + false @@ -1746,6 +1839,11 @@ Position 41 + + + 14 + + Config File: @@ -1818,6 +1916,7 @@ Position + 14 50 false @@ -1845,6 +1944,11 @@ Position 91 + + + 14 + + Sweep @@ -1860,6 +1964,11 @@ Position 31 + + + 14 + + ms @@ -1885,6 +1994,11 @@ Position 31 + + + 14 + + Sweeptime @@ -1899,6 +2013,11 @@ Position 191 + + + 14 + + Camera parameters @@ -1911,6 +2030,11 @@ Position 31 + + + 14 + + Delay @@ -1924,6 +2048,11 @@ Position 31 + + + 14 + + Pulselength @@ -1940,6 +2069,11 @@ Position 31 + + + 14 + + % @@ -1965,6 +2099,11 @@ Position 31 + + + 14 + + % @@ -1987,6 +2126,11 @@ Position 31 + + + 14 + + Exposure @@ -2003,6 +2147,11 @@ Position 31 + + + 14 + + ms @@ -2028,6 +2177,11 @@ Position 31 + + + 14 + + @@ -2038,6 +2192,11 @@ Position 31 + + + 14 + + Binning @@ -2052,6 +2211,11 @@ Position 201 + + + 14 + + Laser pulse @@ -2064,6 +2228,11 @@ Position 31 + + + 14 + + Left @@ -2077,6 +2246,11 @@ Position 31 + + + 14 + + Right @@ -2090,6 +2264,11 @@ Position 31 + + + 14 + + Delay @@ -2106,6 +2285,11 @@ Position 31 + + + 14 + + % @@ -2131,6 +2315,11 @@ Position 31 + + + 14 + + % @@ -2153,6 +2342,11 @@ Position 31 + + + 14 + + Pulselength @@ -2169,6 +2363,11 @@ Position 31 + + + 14 + + % @@ -2194,6 +2393,11 @@ Position 31 + + + 14 + + % @@ -2216,6 +2420,11 @@ Position 31 + + + 14 + + Max Amplitude @@ -2232,6 +2441,11 @@ Position 31 + + + 14 + + % @@ -2257,6 +2471,11 @@ Position 31 + + + 14 + + % @@ -2283,6 +2502,11 @@ Position 221 + + + 14 + + Galvo (Shadow reduction) @@ -2295,6 +2519,11 @@ Position 31 + + + 14 + + Frequency @@ -2311,6 +2540,11 @@ Position 31 + + + 14 + + Hz @@ -2336,6 +2570,11 @@ Position 31 + + + 14 + + Left @@ -2349,6 +2588,11 @@ Position 31 + + + 14 + + Right @@ -2362,6 +2606,11 @@ Position 31 + + + 14 + + Offset @@ -2375,6 +2624,11 @@ Position 31 + + + 14 + + Amplitude @@ -2391,6 +2645,11 @@ Position 31 + + + 14 + + V @@ -2416,6 +2675,11 @@ Position 31 + + + 14 + + V @@ -2444,6 +2708,11 @@ Position 31 + + + 14 + + V @@ -2469,6 +2738,11 @@ Position 31 + + + 14 + + Phase @@ -2485,6 +2759,11 @@ Position 31 + + + 14 + + 360.000000000000000 @@ -2507,6 +2786,11 @@ Position 31 + + + 14 + + 360.000000000000000 @@ -2530,6 +2814,11 @@ Position 251 + + + 14 + + Tunable lenses @@ -2542,6 +2831,11 @@ Position 41 + + + 14 + + For offset & amplitude see ETL tab @@ -2556,6 +2850,11 @@ ETL tab 31 + + + 14 + + Left @@ -2569,6 +2868,11 @@ ETL tab 31 + + + 14 + + Right @@ -2582,6 +2886,11 @@ ETL tab 31 + + + 14 + + Delay @@ -2595,6 +2904,11 @@ ETL tab 31 + + + 14 + + Ramp rising @@ -2608,6 +2922,11 @@ ETL tab 31 + + + 14 + + Ramp falling @@ -2624,6 +2943,11 @@ ETL tab 31 + + + 14 + + % @@ -2649,6 +2973,11 @@ ETL tab 31 + + + 14 + + % @@ -2674,6 +3003,11 @@ ETL tab 31 + + + 14 + + % @@ -2699,6 +3033,11 @@ ETL tab 31 + + + 14 + + % @@ -2724,6 +3063,11 @@ ETL tab 31 + + + 14 + + % @@ -2749,6 +3093,11 @@ ETL tab 31 + + + 14 + + % @@ -2806,6 +3155,11 @@ ETL tab 81 + + + 14 + + false @@ -2823,6 +3177,11 @@ ETL tab 151 + + + 14 + + Display Subsampling @@ -2835,6 +3194,11 @@ ETL tab 31 + + + 14 + + @@ -2845,6 +3209,11 @@ ETL tab 31 + + + 14 + + During Live display: @@ -2858,6 +3227,11 @@ ETL tab 31 + + + 14 + + When Snapping Images: @@ -2871,6 +3245,11 @@ ETL tab 31 + + + 14 + + During Acquisitions: @@ -2884,6 +3263,11 @@ ETL tab 31 + + + 14 + + @@ -2894,6 +3278,11 @@ ETL tab 31 + + + 14 + + -1 @@ -2962,6 +3351,46 @@ ETL tab + + + + 0 + 0 + 959 + 28 + + + + + 14 + + + + + + 14 + + + + File + + + + + + + 14 + + + + View + + + + + + + Close @@ -2977,6 +3406,38 @@ ETL tab Script Window + + + Open Camera Window + + + Opens a new Camera Window + + + + + Open Acquisition Manager + + + Opens a new Acquisition Manager + + + + + Exit + + + Close mesoSPIM-control + + + Ctrl+Q + + + + + About + + diff --git a/mesoSPIM/mesoSPIM_Control.py b/mesoSPIM/mesoSPIM_Control.py index 533b7c1..50f3cfb 100644 --- a/mesoSPIM/mesoSPIM_Control.py +++ b/mesoSPIM/mesoSPIM_Control.py @@ -4,9 +4,16 @@ The core module of the mesoSPIM software ''' +__author__ = "Fabian Voigt" +__license__ = "GPL v3" +__maintainer__ = "Fabian Voigt" + + ''' Configuring the logging module before doing anything else''' import time import logging +import argparse +import glob timestr = time.strftime("%Y%m%d-%H%M%S") logging_filename = timestr + '.log' logging.basicConfig(filename='log/'+logging_filename, level=logging.INFO, format='%(asctime)-8s:%(levelname)s:%(threadName)s:%(thread)d:%(module)s:%(name)s:%(message)s') @@ -23,25 +30,22 @@ logger.info('Modules loaded') -def load_config(): +def load_config_UI(current_path): ''' - Import microscope configuration at startup + Bring up a GUI that allows the user to select a microscope configuration to import ''' ''' This needs an placeholder QApplication to work ''' cfg_app = QtWidgets.QApplication(sys.argv) + current_path = os.path.abspath('./config') global_config_path = '' - global_config_path , _ = QtWidgets.QFileDialog.getOpenFileName(None,\ - 'Open microscope configuration file',current_path) + global_config_path , _ = QtWidgets.QFileDialog.getOpenFileName(None, + 'Open microscope configuration file',current_path) if global_config_path != '': - ''' Using importlib to load the config file ''' - spec = importlib.util.spec_from_file_location('module.name', global_config_path) - config = importlib.util.module_from_spec(spec) - spec.loader.exec_module(config) - logger.info(f'Configuration file loaded: {global_config_path}') + config = load_config_from_file(global_config_path) return config else: ''' Application shutdown ''' @@ -52,6 +56,16 @@ def load_config(): sys.exit(cfg_app.exec_()) +def load_config_from_file(path_to_config): + ''' + Load a microscope configuration from a file using importlib + ''' + spec = importlib.util.spec_from_file_location('module.name', path_to_config) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + logger.info(f'Configuration file loaded: {path_to_config}') + return config + def stage_referencing_check(cfg): ''' Due to problems with some PI stages loosing reference information @@ -75,19 +89,90 @@ def stage_referencing_check(cfg): else: return True -def main(): +def get_parser(): + """ + Parse command-line input arguments + + :return: The argparse parser object + """ + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-C', '--console', action='store_true', # store_true makes it False by default + help='Start a ipython console') + parser.add_argument('-D', '--demo', action='store_true', + help='Start in demo mode') + return parser + +def dark_mode_check(cfg, app): + if (hasattr(cfg, 'dark_mode') and cfg.dark_mode) or (hasattr(cfg, 'ui_options') and cfg.ui_options['dark_mode']): + import qdarkstyle + app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt5')) + + +def main(embed_console=False, demo_mode=False): """ Main function """ + print('Starting control software') + logging.info('mesoSPIM Program started.') - cfg = load_config() + + # Load a configuration file according to the following rules: + # 1. If the user did not ask for demo mode and there is only one config file in the path then load that. + # 2. If the user did not ask for demo mode and there are multiple config files in the path, then bring up the UI loader. + # 3. If the user asked for demo mode and there is only one demo file in path: load it. + # 4. If the user asked for demo mode and there are multiple demo files in the path: bring up the UI loader + # 5. Otherwise bring up the UI loader + + current_path = os.path.abspath('./config') + + cfgLoaded = False + if demo_mode: + demo_fname = glob.glob(os.path.join(current_path, '*demo*.py')) + if len(demo_fname) == 1: + cfg = load_config_from_file(demo_fname[0]) + print(f'Demo settings are loaded from file {demo_fname[0]}') + cfgLoaded = True + else: + all_configs = glob.glob(os.path.join(current_path,'*.py')) # All possible config files + # Strip the paths so when we remove "demo" files we do so based only on the file name itself + strip_path = [tFile.replace(os.path.commonprefix(all_configs), '') for tFile in all_configs] + all_configs_no_demo = list(filter(lambda tFile: str.find(tFile, 'demo') < 0, strip_path)) + + # If only one file left, we load it + if len(all_configs_no_demo) == 1: + cfg = load_config_from_file(os.path.join(current_path, all_configs_no_demo[0])) + cfgLoaded = True + + if not cfgLoaded: + # Otherwise bring up the UI loader + cfg = load_config_UI(current_path) + app = QtWidgets.QApplication(sys.argv) + + dark_mode_check(cfg, app) stage_referencing_check(cfg) ex = mesoSPIM_MainWindow(cfg) ex.show() ex.display_icons() - sys.exit(app.exec_()) + print('Done!') + + if embed_console: + from traitlets.config import Config + cfg = Config() + cfg.InteractiveShellApp.gui = 'qt5' + import IPython + IPython.start_ipython(config=cfg, argv=[], user_ns=dict(mSpim=ex, app=app)) + else: + sys.exit(app.exec_()) + + +def run(): + args = get_parser().parse_args() + main(embed_console=args.console,demo_mode=args.demo) + + if __name__ == '__main__': - main() + run() diff --git a/mesoSPIM/src/devices/filter_wheels/ludlcontrol.py b/mesoSPIM/src/devices/filter_wheels/ludlcontrol.py index 5921572..ba0e2af 100644 --- a/mesoSPIM/src/devices/filter_wheels/ludlcontrol.py +++ b/mesoSPIM/src/devices/filter_wheels/ludlcontrol.py @@ -1,17 +1,17 @@ """ mesoSPIM Module for controlling Ludl filterwheels -Author: Fabian Voigt +Authors: Fabian Voigt, Nikita Vladimirov #TODO """ -import serial as Serial -import io as Io +import serial +import io import time '''PyQt5 Imports''' -from PyQt5 import QtWidgets, QtCore, QtGui +from PyQt5 import QtCore class LudlFilterwheel(QtCore.QObject): @@ -43,7 +43,9 @@ def __init__(self, COMport, filterdict, baudrate=9600): self.baudrate = baudrate self.filterdict = filterdict self.double_wheel = False - + self.ser = None + self.sio = None + self._connect() ''' Delay in s for the wait until done function ''' self.wait_until_done_delay = 0.5 @@ -59,6 +61,20 @@ def __init__(self, COMport, filterdict, baudrate=9600): if type(self.filterdict[self.first_item_in_filterdict]) is tuple: self.double_wheel = True + def _connect(self): + """"Note: Only one connection should be done per session. Connecting frequently is error-prone, + because COM port can be scanned by another program (e.g. laser control) and thus be permission-denied at random + times.""" + try: + self.ser = serial.Serial(self.COMport, + self.baudrate, + parity=serial.PARITY_NONE, + timeout=0, write_timeout=0, + xonxoff=False, + stopbits=serial.STOPBITS_TWO) + self.sio = io.TextIOWrapper(io.BufferedRWPair(self.ser, self.ser)) + except serial.SerialException as e: + print(f"ERROR: Serial connection to Ludl filter wheel failed: {e}") def _check_if_filter_in_filterdict(self, filter): ''' @@ -73,23 +89,12 @@ def _check_if_filter_in_filterdict(self, filter): def set_filter(self, filter, wait_until_done=False): ''' Moves filter using the pyserial command set. - No checks are done whether the movement is completed or finished in time. - - ''' if self._check_if_filter_in_filterdict(filter) is True: - self.ser = Serial.Serial(self.COMport, - self.baudrate, - parity=Serial.PARITY_NONE, - timeout=0, - xonxoff=False, - stopbits=Serial.STOPBITS_TWO) - self.sio = Io.TextIOWrapper(Io.BufferedRWPair(self.ser, self.ser)) """ Check for double or single wheel - TODO: A bit of repeating code in here. Might be better to spin the create and send commands off. """ @@ -98,10 +103,10 @@ def set_filter(self, filter, wait_until_done=False): # Get the filter position from the filterdict: self.filternumber = self.filterdict[filter] # Rotat is the Ludl high-level command for moving a filter wheel + self.ser.flush() self.ludlstring = 'Rotat S M ' + str(self.filternumber) + '\n' self.sio.write(str(self.ludlstring)) self.sio.flush() - self.ser.close() if wait_until_done: ''' Wait a certain number of seconds. This is a hack @@ -131,9 +136,13 @@ def set_filter(self, filter, wait_until_done=False): self.ludlstring1 = 'Rotat S A ' + str(self.filternumber[1]) + '\n' self.sio.write(str(self.ludlstring1)) self.sio.flush() - self.ser.close() if wait_until_done: time.sleep(self.wait_until_done_delay) else: print(f'Filter {filter} not found in configuration.') + + + def __del__(self): + self.sio.flush() + self.ser.close() diff --git a/mesoSPIM/src/devices/filter_wheels/sutterLambdaControl.py b/mesoSPIM/src/devices/filter_wheels/sutterLambdaControl.py new file mode 100644 index 0000000..b37ce1c --- /dev/null +++ b/mesoSPIM/src/devices/filter_wheels/sutterLambdaControl.py @@ -0,0 +1,106 @@ +""" +mesoSPIM Module for controlling Sutter Lambda Filter Wheels + +Author: Kevin Dean, +Basically 100% stolen from Andrew York's GitHub Account :) +""" + +import serial +import time + + +class Lambda10B: + def __init__(self, comport, filterdict, baudrate=9600, read_on_init=True): + super().__init__() + self.COMport = comport + self.baudrate = baudrate + self.filterdict = filterdict + self.double_wheel = False + + ''' Delay in s for the wait until done function ''' + self.wait_until_done_delay = 0.5 + + self.first_item_in_filterdict = list(self.filterdict.keys())[0] + if type(self.filterdict[self.first_item_in_filterdict]) is tuple: + self.double_wheel = True + + # Open Serial Port + try: + self.serial = serial.Serial(self.COMport, self.baudrate, timeout=.25) + except serial.SerialException: + raise UserWarning('Could not open the serial port to the Sutter Lambda 10-B.') + + # Place Controller Into Online Mode + self.serial.write(bytes.fromhex('ee')) + + # Check to see if the initialization sequence has finished. + if read_on_init: + self.read(2) # class 'bytes' + self.init_finished = True + print('Done initializing filter wheel') + else: + self.init_finished = False + self.filternumber = 0 + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def _check_if_filter_in_filterdict(self, filterposition): + # Checks if the filter designation (string) given as argument exists in the filterdict + if filterposition in self.filterdict: + return True + else: + raise ValueError('Filter designation not in the configuration') + + def set_filter(self, filterposition=0, speed=0, wait_until_done=False): + + # Confirm that the filter is present in the filter dictionary + if self._check_if_filter_in_filterdict(filterposition) is True: + + # Confirm that you are only operating in a single filter wheel configuration. + if self.double_wheel is False: + + # Identify the Filter Number from the Filter Dictionary + self.wheel_position = self.filterdict[filterposition] + + # Make sure you are moving it to a reasonable filter position, at a reasonable speed. + assert self.wheel_position in range(10) + assert speed in range(8) + + # If previously we did not confirm that the initialization was complete, check now. + if not self.init_finished: + self.read(2) + self.init_finished = True + print('Done initializing filter wheel.') + + # Filter Wheel Command Byte Encoding = wheel + (speed*16) + position = command byte + outputcommand = self.wheel_position + 16 * speed + outputcommand = outputcommand.to_bytes(1, 'little') + + # Send out Command + self.serial.write(outputcommand) + if wait_until_done: + time.sleep(self.wait_until_done_delay) + + # Read up to 2 bytes + self.read(2) + + else: + raise UserWarning("Sutter Operates only in a Single Filter Wheel Configuration.") + + def read(self, num_bytes): + for i in range(100): + num_waiting = self.serial.inWaiting() + if num_waiting == num_bytes: + break + time.sleep(0.02) + else: + raise UserWarning("The serial port to the Sutter Lambda 10-B is on, but it isn't responding as expected.") + return self.serial.read(num_bytes) + + def close(self): + self.set_filter() + self.serial.close() diff --git a/mesoSPIM/src/devices/stages/galil/galilcontrol.py b/mesoSPIM/src/devices/stages/galil/galilcontrol.py index 85669da..2f0daf1 100644 --- a/mesoSPIM/src/devices/stages/galil/galilcontrol.py +++ b/mesoSPIM/src/devices/stages/galil/galilcontrol.py @@ -11,11 +11,12 @@ from PyQt5 import QtWidgets, QtCore, QtGui -import gclib +import src.devices.stages.galil.gclib as gclib import logging logger = logging.getLogger(__name__) + class StageControlGalil(QtCore.QObject): ''' Class to control a Galil mechanical stage controller diff --git a/mesoSPIM/src/devices/zoom/mesoSPIM_Zoom.py b/mesoSPIM/src/devices/zoom/mesoSPIM_Zoom.py index dbf0791..be7f78b 100644 --- a/mesoSPIM/src/devices/zoom/mesoSPIM_Zoom.py +++ b/mesoSPIM/src/devices/zoom/mesoSPIM_Zoom.py @@ -8,7 +8,7 @@ import time -from PyQt5 import QtWidgets, QtCore, QtGui +from PyQt5 import QtCore class DemoZoom(QtCore.QObject): def __init__(self, zoomdict): @@ -44,7 +44,7 @@ def __init__(self, zoomdict, COMport, identifier=2, baudrate=1000000): self.goal_position_offset = 10 ''' Specifies how long to sleep for the wait until done function''' self.sleeptime = 0.05 - self.timeout = 15 + self.timeout = 10 # the dynamixel library uses integers instead of booleans for binary information self.torque_enable = 1 @@ -52,6 +52,12 @@ def __init__(self, zoomdict, COMport, identifier=2, baudrate=1000000): self.port_num = dynamixel.portHandler(self.devicename) self.dynamixel.packetHandler() + self._connect() + + def _connect(self): + # open port and set baud rate + self.dynamixel.openPort(self.port_num) + self.dynamixel.setBaudRate(self.port_num, self.baudrate) def set_zoom(self, zoom, wait_until_done=False): """Changes zoom after checking that the commanded value exists""" @@ -62,9 +68,6 @@ def set_zoom(self, zoom, wait_until_done=False): raise ValueError('Zoom designation not in the configuration') def _move(self, position, wait_until_done=False): - # open port and set baud rate - self.dynamixel.openPort(self.port_num) - self.dynamixel.setBaudRate(self.port_num, self.baudrate) # Enable servo self.dynamixel.write1ByteTxRx(self.port_num, 1, self.id, self.addr_mx_torque_enable, self.torque_enable) # Write Moving Speed @@ -92,23 +95,18 @@ def _move(self, position, wait_until_done=False): while (cur_position < lower_limit) or (cur_position > upper_limit): ''' Timeout ''' if time.time()-start_time > self.timeout: + print("Dynamixel zoom servo: timeout") break - time.sleep(0.05) + time.sleep(self.sleeptime) cur_position = self.dynamixel.read4ByteTxRx(self.port_num, 1, self.id, self.addr_mx_present_position) # print(cur_position) - self.dynamixel.closePort(self.port_num) - def read_position(self): ''' Returns position as an int between 0 and 4096 - - Opens & closes the port ''' - self.dynamixel.openPort(self.port_num) - self.dynamixel.setBaudRate(self.port_num, self.baudrate) cur_position = self.dynamixel.read4ByteTxRx(self.port_num, 1, self.id, self.addr_mx_present_position) + return cur_position + def __del__(self): self.dynamixel.closePort(self.port_num) - - return cur_position diff --git a/mesoSPIM/src/mesoSPIM_AcquisitionManagerWindow.py b/mesoSPIM/src/mesoSPIM_AcquisitionManagerWindow.py index 4e84e81..c221a1d 100644 --- a/mesoSPIM/src/mesoSPIM_AcquisitionManagerWindow.py +++ b/mesoSPIM/src/mesoSPIM_AcquisitionManagerWindow.py @@ -32,7 +32,6 @@ from .utils.widgets import MarkPositionWidget -from .utils.acquisition_wizards import TilingWizard from .utils.multicolor_acquisition_wizard import MulticolorTilingWizard from .utils.filename_wizard import FilenameWizard from .utils.focus_tracking_wizard import FocusTrackingWizard @@ -96,6 +95,7 @@ def __init__(self, parent=None): self.table.setDropIndicatorShown(True) self.table.setSortingEnabled(True) + self.set_item_delegates() ''' Set our custom style - this draws the drop indicator across the whole row ''' @@ -127,6 +127,10 @@ def __init__(self, parent=None): # self.SetRotationPointButton.clicked.connect(lambda bool: self.set_rotation_point() if bool is True else self.delete_rotation_point()) self.SetFoldersButton.clicked.connect(self.set_folder_names) + font = QtGui.QFont() + font.setPointSize(14) + self.table.horizontalHeader().setFont(font) + self.table.verticalHeader().setFont(font) logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) @@ -458,16 +462,6 @@ def display_warning(self, string): warning = QtWidgets.QMessageBox.warning(None,'mesoSPIM Warning', string, QtWidgets.QMessageBox.Ok) - def generate_xml(self): - print('generating BDV XML') - - timestr = time.strftime("%Y%m%d-%H%M%S") - filename = timestr + '.xml' - - path = self.state['acq_list'][0]['folder']+'/'+filename - - xml_exporter = mesoSPIM_XMLexporter(self) - xml_exporter.generate_xml_from_acqlist(self.state['acq_list'],path) diff --git a/mesoSPIM/src/mesoSPIM_Camera.py b/mesoSPIM/src/mesoSPIM_Camera.py index 49c1a48..8260696 100644 --- a/mesoSPIM/src/mesoSPIM_Camera.py +++ b/mesoSPIM/src/mesoSPIM_Camera.py @@ -1,6 +1,7 @@ ''' mesoSPIM Camera class, intended to run in its own thread ''' + import os import time import numpy as np @@ -18,6 +19,7 @@ logger.info('Error: Hamamatsu camera could not be imported') ''' from .mesoSPIM_State import mesoSPIM_StateSingleton +from .mesoSPIM_ImageWriter import mesoSPIM_ImageWriter from .utils.acquisitions import AcquisitionList, Acquisition class mesoSPIM_Camera(QtCore.QObject): @@ -34,6 +36,7 @@ def __init__(self, parent = None): self.cfg = parent.cfg self.state = mesoSPIM_StateSingleton() + self.image_writer = mesoSPIM_ImageWriter(self) self.stopflag = False @@ -74,6 +77,8 @@ def __init__(self, parent = None): self.camera = mesoSPIM_HamamatsuCamera(self) elif self.cfg.camera == 'PhotometricsIris15': self.camera = mesoSPIM_PhotometricsCamera(self) + elif self.cfg.camera == 'PCO': + self.camera = mesoSPIM_PCOCamera(self) elif self.cfg.camera == 'DemoCamera': self.camera = mesoSPIM_DemoCamera(self) @@ -150,61 +155,50 @@ def set_camera_display_acquisition_subsampling(self, factor): self.camera_display_acquisition_subsampling = factor def set_camera_binning(self, value): + print('Setting camera binning: '+value) self.camera.set_binning(value) - @QtCore.pyqtSlot(Acquisition) - def prepare_image_series(self, acq): + @QtCore.pyqtSlot(Acquisition, AcquisitionList) + def prepare_image_series(self, acq, acq_list): ''' Row is a row in a AcquisitionList ''' logger.info('Camera: Preparing Image Series') - #print('Cam: Preparing Image Series') self.stopflag = False - ''' TODO: Needs cam delay, sweeptime, QTimer, line delay, exp_time ''' + self.image_writer.prepare_acquisition(acq, acq_list) - self.folder = acq['folder'] - self.filename = acq['filename'] - self.path = self.folder+'/'+self.filename - - logger.info(f'Camera: Save path: {self.path}') - self.z_start = acq['z_start'] - self.z_end = acq['z_end'] - self.z_stepsize = acq['z_step'] self.max_frame = acq.get_image_count() - self.processing_options_string = acq['processing'] - self.fsize = self.x_pixels*self.y_pixels - - self.xy_stack = np.memmap(self.path, mode = "write", dtype = np.uint16, shape = self.fsize * self.max_frame) - self.camera.initialize_image_series() self.cur_image = 0 logger.info(f'Camera: Finished Preparing Image Series') self.start_time = time.time() - @QtCore.pyqtSlot() - def add_images_to_series(self): + @QtCore.pyqtSlot(Acquisition, AcquisitionList) + def add_images_to_series(self, acq, acq_list): if self.cur_image == 0: logger.info('Thread ID during add images: '+str(int(QtCore.QThread.currentThreadId()))) if self.stopflag is False: if self.cur_image < self.max_frame: - # logger.info('self.cur_image + 1: '+str(self.cur_image + 1)) images = self.camera.get_images_in_series() for image in images: image = np.rot90(image) self.sig_camera_frame.emit(image[0:self.x_pixels:self.camera_display_acquisition_subsampling,0:self.y_pixels:self.camera_display_acquisition_subsampling]) - image = image.flatten() - self.xy_stack[self.cur_image*self.fsize:(self.cur_image+1)*self.fsize] = image + self.image_writer.write_image(image, acq, acq_list) self.cur_image += 1 - @QtCore.pyqtSlot() - def end_image_series(self): + @QtCore.pyqtSlot(Acquisition, AcquisitionList) + def end_image_series(self, acq, acq_list): if self.stopflag is False: if self.processing_options_string != '': if self.processing_options_string == 'MAX': + ''' Image processing needs to be reimplemented in an incremental fashion ''' + pass + + ''' self.sig_status_message.emit('Doing Max Projection') logger.info('Camera: Started Max Projection of '+str(self.max_frame)+' Images') stackview = self.xy_stack.view() @@ -215,12 +209,14 @@ def end_image_series(self): tifffile.imsave(path, max_proj, photometric='minisblack') logger.info('Camera: Saved Max Projection') self.sig_status_message.emit('Done with image processing') + ''' try: self.camera.close_image_series() - del self.xy_stack except: - pass + logger.warning('Camera: Image Series could not be closed') + + self.image_writer.end_acquisition(acq, acq_list) self.end_time = time.time() framerate = (self.cur_image + 1)/(self.end_time - self.start_time) @@ -231,15 +227,8 @@ def end_image_series(self): def snap_image(self): image = self.camera.get_image() image = np.rot90(image) - - timestr = time.strftime("%Y%m%d-%H%M%S") - filename = timestr + '.tif' - - path = self.state['snap_folder']+'/'+filename - self.sig_camera_frame.emit(image[0:self.x_pixels:self.camera_display_snap_subsampling,0:self.y_pixels:self.camera_display_snap_subsampling]) - - tifffile.imsave(path, image, photometric='minisblack') + self.image_writer.write_snap_image(image) @QtCore.pyqtSlot() def prepare_live(self): @@ -309,10 +298,11 @@ def set_line_interval(self, time): pass def set_binning(self, binning_string): - self.x_binning = int(self.binning_string[0]) - self.y_binning = int(self.binning_string[2]) + self.x_binning = int(binning_string[0]) + self.y_binning = int(binning_string[2]) self.x_pixels = int(self.x_pixels / self.x_binning) self.y_pixels = int(self.y_pixels / self.y_binning) + self.state['camera_binning'] = str(self.x_binning)+'x'+str(self.y_binning) def initialize_image_series(self): pass @@ -338,15 +328,14 @@ def close_live_mode(self): pass class mesoSPIM_DemoCamera(mesoSPIM_GenericCamera): - def __init__(self, parent = None): super().__init__(parent) + self.count = 0 + self.line = np.linspace(0,6*np.pi,self.x_pixels) self.line = 400*np.sin(self.line)+1200 - self.count = 0 - def open_camera(self): logger.info('Initialized Demo Camera') @@ -354,12 +343,14 @@ def close_camera(self): logger.info('Closed Demo Camera') def set_binning(self, binning_string): - self.x_binning = int(self.binning_string[0]) - self.y_binning = int(self.binning_string[2]) + self.x_binning = int(binning_string[0]) + self.y_binning = int(binning_string[2]) self.x_pixels = int(self.x_pixels / self.x_binning) self.y_pixels = int(self.y_pixels / self.y_binning) + ''' Changing the number of pixels also affects the random image, so we need to update self.line ''' self.line = np.linspace(0,6*np.pi,self.x_pixels) self.line = 400*np.sin(self.line)+1200 + self.state['camera_binning'] = str(self.x_binning)+'x'+str(self.y_binning) def _create_random_image(self): data = np.array([np.roll(self.line, 4*i+self.count) for i in range(0, self.y_pixels)], dtype='uint16') @@ -368,8 +359,6 @@ def _create_random_image(self): self.count += 20 return data - # return np.random.randint(low=0, high=2**16, size=(self.x_pixels,self.y_pixels), dtype='l') - def get_images_in_series(self): return [self._create_random_image()] @@ -428,10 +417,11 @@ def set_line_interval(self, time): def set_binning(self, binningstring): self.hcam.setPropertyValue("binning", binningstring) - self.x_binning = int(self.binning_string[0]) - self.y_binning = int(self.binning_string[2]) + self.x_binning = int(binning_string[0]) + self.y_binning = int(binning_string[2]) self.x_pixels = int(self.x_pixels / self.x_binning) self.y_pixels = int(self.y_pixels / self.y_binning) + self.state['camera_binning'] = str(self.x_binning)+'x'+str(self.y_binning) def initialize_image_series(self): self.hcam.startAcquisition() @@ -570,11 +560,12 @@ def set_line_interval(self, time): print('Setting line interval is not implemented, set the interval in the config file') def set_binning(self, binningstring): - self.x_binning = int(self.binning_string[0]) - self.y_binning = int(self.binning_string[2]) + self.x_binning = int(binning_string[0]) + self.y_binning = int(binning_string[2]) self.x_pixels = int(self.x_pixels / self.x_binning) self.y_pixels = int(self.y_pixels / self.y_binning) self.pvcam.binning = (self.x_binning, self.y_binning) + self.state['camera_binning'] = str(self.x_binning)+'x'+str(self.y_binning) def get_image(self): return self.pvcam.get_live_frame() @@ -602,4 +593,67 @@ def get_live_image(self): def close_live_mode(self): self.pvcam.stop_live() +class mesoSPIM_PCOCamera(mesoSPIM_GenericCamera): + def __init__(self, parent = None): + super().__init__(parent) + logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) + logger.info('PCO Cam initialized') + + def open_camera(self): + import pco + self.cam = pco.Camera() # no logging + # self.cam = pco.Camera(debuglevel='verbose', timestamp='on') + + self.cam.sdk.set_cmos_line_timing('on', self.cfg.camera_parameters['line_interval']) # 75 us delay + self.cam.set_exposure_time(self.cfg.camera_parameters['exp_time']) + # self.cam.sdk.set_cmos_line_exposure_delay(80, 0) # 266 lines = 20 ms / 75 us + self.cam.configuration = {'trigger' : self.cfg.camera_parameters['trigger']} + + line_time = self.cam.sdk.get_cmos_line_timing()['line time'] + lines_exposure = self.cam.sdk.get_cmos_line_exposure_delay()['lines exposure'] + t = self.cam.get_exposure_time() + #print('Exposure Time: {:9.6f} s'.format(t)) + #print('Line Time: {:9.6f} s'.format(line_time)) + #print('Number of Lines: {:d}'.format(lines_exposure)) + + self.cam.record(number_of_images=4, mode='fifo') + + def close_camera(self): + self.cam.stop() + self.cam.close() + + def set_exposure_time(self, time): + self.cam.set_exposure_time(time) + self.camera_exposure_time = time + + def set_line_interval(self, time): + print('Setting line interval is not implemented, set the interval in the config file') + + def set_binning(self, binningstring): + pass + + def get_image(self): + image, meta = self.cam.image(image_number=-1) + return image + + def initialize_image_series(self): + pass + + def get_images_in_series(self): + image, meta = self.cam.image(image_number=-1) + return [image] + + def close_image_series(self): + pass + + def initialize_live_mode(self): + pass + + def get_live_image(self): + image, meta = self.cam.image(image_number=-1) + return [image] + + def close_live_mode(self): + pass + \ No newline at end of file diff --git a/mesoSPIM/src/mesoSPIM_CameraWindow.py b/mesoSPIM/src/mesoSPIM_CameraWindow.py index b20fff8..c1d9379 100644 --- a/mesoSPIM/src/mesoSPIM_CameraWindow.py +++ b/mesoSPIM/src/mesoSPIM_CameraWindow.py @@ -10,16 +10,24 @@ from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.uic import loadUi - import pyqtgraph as pg -pg.setConfigOptions(imageAxisOrder='row-major') -pg.setConfigOptions(foreground='k') -pg.setConfigOptions(background='w') +from .mesoSPIM_State import mesoSPIM_StateSingleton class mesoSPIM_CameraWindow(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__() + self.parent = parent + self.cfg = parent.cfg + self.state = mesoSPIM_StateSingleton() + + pg.setConfigOptions(imageAxisOrder='row-major') + if (hasattr(self.cfg, 'ui_options') and self.cfg.ui_options['dark_mode']) or\ + (hasattr(self.cfg, 'dark_mode') and self.cfg.dark_mode): + pg.setConfigOptions(background=pg.mkColor('#19232D')) # To avoid pitch black bg for the image view + else: + pg.setConfigOptions(background="w") + '''Set up the UI''' if __name__ == '__main__': loadUi('../gui/mesoSPIM_CameraWindow.ui', self) @@ -27,11 +35,6 @@ def __init__(self, parent=None): loadUi('gui/mesoSPIM_CameraWindow.ui', self) self.setWindowTitle('mesoSPIM-Control: Camera Window') - self.parent = parent - self.cfg = parent.cfg - - - ''' Set histogram Range ''' self.graphicsView.setLevels(100,4000) @@ -44,13 +47,6 @@ def __init__(self, parent=None): ''' This is flipped to account for image rotation ''' self.y_image_width = self.cfg.camera_parameters['x_pixels'] self.x_image_width = self.cfg.camera_parameters['y_pixels'] - ''' Debugging info - - logger.info('x_image_width: '+str(self.x_image_width)) - logger.info('y_image_width: '+str(self.y_image_width)) - logger.info('x_image_width/2: '+str(self.x_image_width/2)) - logger.info('y_image_width/2: '+str(self.y_image_width/2)) - ''' ''' Initialize crosshairs ''' self.crosspen = pg.mkPen({'color': "r", 'width': 1}) @@ -58,11 +54,52 @@ def __init__(self, parent=None): self.hLine = pg.InfiniteLine(pos=self.y_image_width/2, angle=0, movable=False, pen=self.crosspen) self.graphicsView.addItem(self.vLine, ignoreBounds=True) self.graphicsView.addItem(self.hLine, ignoreBounds=True) - # print(self.vLine.getXPos()) - # print(self.hLine.getYPos()) + + # Create overlay ROIs + x, y, w, h = 100, 100, 200, 200 + self.roi_box = pg.RectROI((x, y), (w, h), sideScalers=True) + font = QtGui.QFont() + font.setPixelSize(16) + self.roi_box_w_text, self.roi_box_h_text = pg.TextItem(color='r'), pg.TextItem(color='r', angle=90) + self.roi_box_w_text.setFont(font), self.roi_box_h_text.setFont(font) + self.roi_box_w_text.setPos(x, y + h), self.roi_box_h_text.setPos(x, y + h) + self.roi_list = [self.roi_box, self.roi_box_w_text, self.roi_box_h_text] + + # Set up CameraWindow signals + self.adjustLevelsButton.clicked.connect(self.adjust_levels) + self.overlayCombo.currentTextChanged.connect(self.change_overlay) + self.roi_box.sigRegionChangeFinished.connect(self.update_box_roi_labels) logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) + def adjust_levels(self, pct_low=25, pct_hi=99.99): + ''''Adjust histogram levels''' + img = self.graphicsView.getImageItem().image + self.graphicsView.setLevels(min=np.percentile(img, pct_low), max=np.percentile(img, pct_hi)) + + def px2um(self, px): + '''Unit converter''' + return px * self.cfg.pixelsize[self.state['zoom']] + + @QtCore.pyqtSlot(str) + def change_overlay(self, overlay_name): + ''''Changes the image overlay''' + if overlay_name == 'Box roi': + self.update_box_roi_labels() + for item in self.roi_list: + self.graphicsView.addItem(item) + elif overlay_name == 'Overlay: none': + for item in self.roi_list: + self.graphicsView.removeItem(item) + + @QtCore.pyqtSlot() + def update_box_roi_labels(self): + w, h = self.roi_box.size() + x, y = self.roi_box.pos() + self.roi_box_w_text.setText(f"{int(self.px2um(w)):,} \u03BCm") + self.roi_box_h_text.setText(f"{int(self.px2um(h)):,} \u03BCm") + self.roi_box_w_text.setPos(x, y + h) + self.roi_box_h_text.setPos(x, y + h) @QtCore.pyqtSlot(str) def display_status_message(self, string, time=0): @@ -71,7 +108,6 @@ def display_status_message(self, string, time=0): If time=0, the message will stay. ''' - if time == 0: self.statusBar().showMessage(string) else: @@ -91,13 +127,6 @@ def set_image(self, image): self.hLine.setPos(self.y_image_width/2) # Stating a single value works for orthogonal lines self.graphicsView.addItem(self.vLine, ignoreBounds=True) self.graphicsView.addItem(self.hLine, ignoreBounds=True) - ''' Debugging info - - logger.info('x_image_width: '+str(self.x_image_width)) - logger.info('y_image_width: '+str(self.y_image_width)) - logger.info('x_image_width/2: '+str(self.x_image_width/2)) - logger.info('y_image_width/2: '+str(self.y_image_width/2)) - ''' else: self.draw_crosshairs() @@ -106,4 +135,4 @@ def set_image(self, image): camera_window = mesoSPIM_CameraWindow() camera_window.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/mesoSPIM/src/mesoSPIM_Core.py b/mesoSPIM/src/mesoSPIM_Core.py index 290f4cf..5e7b882 100644 --- a/mesoSPIM/src/mesoSPIM_Core.py +++ b/mesoSPIM/src/mesoSPIM_Core.py @@ -38,7 +38,6 @@ from .utils.acquisitions import AcquisitionList, Acquisition from .utils.utility_functions import convert_seconds_to_string -from .utils.demo_threads import mesoSPIM_DemoThread class mesoSPIM_Core(QtCore.QObject): '''This class is the pacemaker of a mesoSPIM @@ -61,10 +60,10 @@ class mesoSPIM_Core(QtCore.QObject): sig_progress = QtCore.pyqtSignal(dict) ''' Camera-related signals ''' - sig_prepare_image_series = QtCore.pyqtSignal(Acquisition) - sig_add_images_to_image_series = QtCore.pyqtSignal() - sig_add_images_to_image_series_and_wait_until_done = QtCore.pyqtSignal() - sig_end_image_series = QtCore.pyqtSignal() + sig_prepare_image_series = QtCore.pyqtSignal(Acquisition, AcquisitionList) + sig_add_images_to_image_series = QtCore.pyqtSignal(Acquisition, AcquisitionList) + sig_add_images_to_image_series_and_wait_until_done = QtCore.pyqtSignal(Acquisition, AcquisitionList) + sig_end_image_series = QtCore.pyqtSignal(Acquisition, AcquisitionList) sig_prepare_live = QtCore.pyqtSignal() sig_get_live_image = QtCore.pyqtSignal() @@ -201,11 +200,9 @@ def __init__(self, config, parent): self.state['snap_folder'] = self.cfg.startup['snap_folder'] self.start_time = 0 - self.stopflag = False - logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) - + self.metadata_file = None # self.acquisition_list_rotation_position = {} def __del__(self): @@ -576,9 +573,9 @@ def prepare_acquisition_list(self, acq_list): def run_acquisition_list(self, acq_list): for acq in acq_list: if not self.stopflag: - self.prepare_acquisition(acq) - self.run_acquisition(acq) - self.close_acquisition(acq) + self.prepare_acquisition(acq, acq_list) + self.run_acquisition(acq, acq_list) + self.close_acquisition(acq, acq_list) def close_acquisition_list(self, acq_list): self.sig_status_message.emit('Closing Acquisition List') @@ -669,7 +666,7 @@ def preview_acquisition(self, z_update=True): self.state['state'] = 'idle' - def prepare_acquisition(self, acq): + def prepare_acquisition(self, acq, acq_list): ''' Housekeeping: Prepare the acquisition ''' @@ -715,15 +712,15 @@ def prepare_acquisition(self, acq): self.f_step_generator = acq.get_focus_stepsize_generator() self.sig_status_message.emit('Preparing camera: Allocating memory') - self.sig_prepare_image_series.emit(acq) + self.sig_prepare_image_series.emit(acq, acq_list) self.prepare_image_series() # ''' HICKUP DEBUGGING: Measure z position ''' # self.z_start_measured = self.state['position']['z_pos'] - self.write_metadata(acq) + self.write_metadata(acq, acq_list) - def run_acquisition(self, acq): + def run_acquisition(self, acq, acq_list): steps = acq.get_image_count() self.sig_status_message.emit('Running Acquisition') self.open_shutters() @@ -734,12 +731,12 @@ def run_acquisition(self, acq): for i in range(steps): if self.stopflag is True: self.close_image_series() - self.sig_end_image_series.emit() + self.sig_end_image_series.emit(acq, acq_list) self.sig_finished.emit() break else: self.snap_image_in_series() - self.sig_add_images_to_image_series.emit() + self.sig_add_images_to_image_series.emit(acq, acq_list) #time.sleep(0.02) # self.sig_add_images_to_image_series_and_wait_until_done.emit() @@ -790,7 +787,7 @@ def run_acquisition(self, acq): self.close_shutters() - def close_acquisition(self, acq): + def close_acquisition(self, acq, acq_list): # ''' HICKUP DEBUGGING ''' # self.z_end_measured = self.state['position']['z_pos'] @@ -803,7 +800,7 @@ def close_acquisition(self, acq): if self.stopflag is False: # self.move_absolute(acq.get_startpoint(), wait_until_done=True) self.close_image_series() - self.sig_end_image_series.emit() + self.sig_end_image_series.emit(acq, acq_list) self.acq_end_time = time.time() self.acq_end_time_string = time.strftime("%Y%m%d-%H%M%S") @@ -886,66 +883,76 @@ def write_line(self, file, key='', value=''): else: file.write('\n') - def write_metadata(self, acq): + def write_metadata(self, acq, acq_list): ''' Writes a metadata.txt file Path contains the file to be written ''' - path = acq['folder']+'/'+acq['filename'] + path = acq['folder'] + '/' + acq['filename'] - metadata_path = os.path.dirname(path)+'/'+os.path.basename(path)+'_meta.txt' + metadata_path = os.path.dirname(path) + '/' + os.path.basename(path) + '_meta.txt' # print('Metadata_path: ', metadata_path) - - with open(metadata_path,'w') as file: - self.write_line(file, 'Metadata for file', path) - self.write_line(file, 'z_stepsize', acq['z_step']) - self.write_line(file, 'z_planes', acq['planes']) - self.write_line(file) - # self.write_line(file, 'COMMENTS') - # self.write_line(file, 'Comment: ', acq(['comment'])) - # self.write_line(file) - self.write_line(file, 'CFG') - self.write_line(file, 'Laser', acq['laser']) - self.write_line(file, 'Intensity (%)', acq['intensity']) - self.write_line(file, 'Zoom', acq['zoom']) - self.write_line(file, 'Pixelsize in um', self.state['pixelsize']) - self.write_line(file, 'Filter', acq['filter']) - self.write_line(file, 'Shutter', acq['shutterconfig']) - self.write_line(file) - self.write_line(file, 'POSITION') - self.write_line(file, 'x_pos', acq['x_pos']) - self.write_line(file, 'y_pos', acq['y_pos']) - self.write_line(file, 'f_start', acq['f_start']) - self.write_line(file, 'f_end', acq['f_end']) - self.write_line(file, 'z_start', acq['z_start']) - self.write_line(file, 'z_end', acq['z_end']) - self.write_line(file, 'z_stepsize', acq['z_step']) - self.write_line(file, 'z_planes', acq.get_image_count()) - self.write_line(file) - - ''' Attention: change to true ETL values ASAP ''' - self.write_line(file,'ETL PARAMETERS') - self.write_line(file, 'ETL CFG File', self.state['ETL_cfg_file']) - self.write_line(file,'etl_l_offset', self.state['etl_l_offset']) - self.write_line(file,'etl_l_amplitude', self.state['etl_l_amplitude']) - self.write_line(file,'etl_r_offset', self.state['etl_r_offset']) - self.write_line(file,'etl_r_amplitude', self.state['etl_r_amplitude']) - self.write_line(file) - self.write_line(file, 'GALVO PARAMETERS') - self.write_line(file, 'galvo_l_frequency',self.state['galvo_l_frequency']) - self.write_line(file, 'galvo_l_amplitude',self.state['galvo_l_amplitude']) - self.write_line(file, 'galvo_l_offset', self.state['galvo_l_offset']) - self.write_line(file, 'galvo_r_amplitude', self.state['galvo_r_amplitude']) - self.write_line(file, 'galvo_r_offset', self.state['galvo_r_offset']) - self.write_line(file) - self.write_line(file, 'CAMERA PARAMETERS') - self.write_line(file, 'camera_type', self.cfg.camera) - self.write_line(file, 'camera_exposure', self.state['camera_exposure_time']) - self.write_line(file, 'camera_line_interval', self.state['camera_line_interval']) - self.write_line(file, 'x_pixels',self.cfg.camera_parameters['x_pixels']) - self.write_line(file, 'y_pixels',self.cfg.camera_parameters['y_pixels']) + if acq['filename'][-3:] == '.h5': + if acq == acq_list[0]: + self.metadata_file = open(metadata_path, 'w') + else: + self.metadata_file = open(metadata_path, 'w') + self.write_line(self.metadata_file, 'Metadata for file', path) + self.write_line(self.metadata_file, 'z_stepsize', acq['z_step']) + self.write_line(self.metadata_file, 'z_planes', acq['planes']) + self.write_line(self.metadata_file) + # self.write_line(file, 'COMMENTS') + # self.write_line(file, 'Comment: ', acq(['comment'])) + # self.write_line(file) + self.write_line(self.metadata_file, 'CFG') + self.write_line(self.metadata_file, 'Laser', acq['laser']) + self.write_line(self.metadata_file, 'Intensity (%)', acq['intensity']) + self.write_line(self.metadata_file, 'Zoom', acq['zoom']) + self.write_line(self.metadata_file, 'Pixelsize in um', self.state['pixelsize']) + self.write_line(self.metadata_file, 'Filter', acq['filter']) + self.write_line(self.metadata_file, 'Shutter', acq['shutterconfig']) + self.write_line(self.metadata_file) + self.write_line(self.metadata_file, 'POSITION') + self.write_line(self.metadata_file, 'x_pos', acq['x_pos']) + self.write_line(self.metadata_file, 'y_pos', acq['y_pos']) + self.write_line(self.metadata_file, 'f_start', acq['f_start']) + self.write_line(self.metadata_file, 'f_end', acq['f_end']) + self.write_line(self.metadata_file, 'z_start', acq['z_start']) + self.write_line(self.metadata_file, 'z_end', acq['z_end']) + self.write_line(self.metadata_file, 'z_stepsize', acq['z_step']) + self.write_line(self.metadata_file, 'z_planes', acq.get_image_count()) + self.write_line(self.metadata_file, 'rot', acq['rot']) + self.write_line(self.metadata_file) + + ''' Attention: change to true ETL values ASAP ''' + self.write_line(self.metadata_file, 'ETL PARAMETERS') + self.write_line(self.metadata_file, 'ETL CFG File', self.state['ETL_cfg_file']) + self.write_line(self.metadata_file, 'etl_l_offset', self.state['etl_l_offset']) + self.write_line(self.metadata_file, 'etl_l_amplitude', self.state['etl_l_amplitude']) + self.write_line(self.metadata_file, 'etl_r_offset', self.state['etl_r_offset']) + self.write_line(self.metadata_file, 'etl_r_amplitude', self.state['etl_r_amplitude']) + self.write_line(self.metadata_file) + self.write_line(self.metadata_file, 'GALVO PARAMETERS') + self.write_line(self.metadata_file, 'galvo_l_frequency', self.state['galvo_l_frequency']) + self.write_line(self.metadata_file, 'galvo_l_amplitude', self.state['galvo_l_amplitude']) + self.write_line(self.metadata_file, 'galvo_l_offset', self.state['galvo_l_offset']) + self.write_line(self.metadata_file, 'galvo_r_amplitude', self.state['galvo_r_amplitude']) + self.write_line(self.metadata_file, 'galvo_r_offset', self.state['galvo_r_offset']) + self.write_line(self.metadata_file) + self.write_line(self.metadata_file, 'CAMERA PARAMETERS') + self.write_line(self.metadata_file, 'camera_type', self.cfg.camera) + self.write_line(self.metadata_file, 'camera_exposure', self.state['camera_exposure_time']) + self.write_line(self.metadata_file, 'camera_line_interval', self.state['camera_line_interval']) + self.write_line(self.metadata_file, 'x_pixels', self.cfg.camera_parameters['x_pixels']) + self.write_line(self.metadata_file, 'y_pixels', self.cfg.camera_parameters['y_pixels']) + + if acq['filename'][-3:] == '.h5': + if acq == acq_list[-1]: + self.metadata_file.close() + else: + self.metadata_file.close() def execute_galil_program(self): '''Little helper method to execute the program loaded onto the Galil stage: diff --git a/mesoSPIM/src/mesoSPIM_ImageWriter.py b/mesoSPIM/src/mesoSPIM_ImageWriter.py new file mode 100644 index 0000000..4127ba8 --- /dev/null +++ b/mesoSPIM/src/mesoSPIM_ImageWriter.py @@ -0,0 +1,141 @@ +''' +mesoSPIM Image Writer class, intended to run in the Camera Thread and handle file I/O +''' + +import os +import time +import numpy as np +import tifffile +import logging +logger = logging.getLogger(__name__) +import sys +from PyQt5 import QtCore + +from .mesoSPIM_State import mesoSPIM_StateSingleton + +import npy2bdv + +class mesoSPIM_ImageWriter(QtCore.QObject): + def __init__(self, parent = None): + super().__init__() + + self.parent = parent + self.cfg = parent.cfg + + self.state = mesoSPIM_StateSingleton() + + self.x_pixels = self.cfg.camera_parameters['x_pixels'] + self.y_pixels = self.cfg.camera_parameters['y_pixels'] + + self.binning_string = self.cfg.camera_parameters['binning'] # Should return a string in the form '2x4' + self.x_binning = int(self.binning_string[0]) + self.y_binning = int(self.binning_string[2]) + + self.x_pixels = int(self.x_pixels / self.x_binning) + self.y_pixels = int(self.y_pixels / self.y_binning) + + self.file_extension = '' + self.bdv_writer = None + + def prepare_acquisition(self, acq, acq_list): + self.folder = acq['folder'] + self.filename = acq['filename'] + self.path = self.folder+'/'+self.filename + logger.info(f'Image Writer: Save path: {self.path}') + + _ , self.file_extension = os.path.splitext(self.filename) + + self.binning_string = self.state['camera_binning'] # Should return a string in the form '2x4' + self.x_binning = int(self.binning_string[0]) + self.y_binning = int(self.binning_string[2]) + + self.x_pixels = int(self.x_pixels / self.x_binning) + self.y_pixels = int(self.y_pixels / self.y_binning) + + self.max_frame = acq.get_image_count() + self.processing_options_string = acq['processing'] + + if self.file_extension == '.h5': + if hasattr(self.cfg, "hdf5"): + subsamp = self.cfg.hdf5['subsamp'] + compression = self.cfg.hdf5['compression'] + flip_flags = self.cfg.hdf5['flip_xyz'] + else: + subsamp = ((1, 1, 1),) + compression = None + flip_flags = (False, False, False) + # create writer object if the view is first in the list + if acq == acq_list[0]: + self.bdv_writer = npy2bdv.BdvWriter(self.path, + nilluminations=acq_list.get_n_shutter_configs(), + nchannels=acq_list.get_n_lasers(), + nangles=acq_list.get_n_angles(), + ntiles=acq_list.get_n_tiles(), + blockdim=((1, 256, 256),), + subsamp=subsamp, + compression=compression) + # x and y need to be exchanged to account for the image rotation + shape = (self.max_frame, self.y_pixels, self.x_pixels) + px_size_um = self.cfg.pixelsize[acq['zoom']] + sign_xyz = (1 - np.array(flip_flags)) * 2 - 1 + affine_matrix = np.array(((1.0, 0.0, 0.0, sign_xyz[0] * acq['x_pos']/px_size_um), + (0.0, 1.0, 0.0, sign_xyz[1] * acq['y_pos']/px_size_um), + (0.0, 0.0, 1.0, sign_xyz[2] * acq['z_start']/acq['z_step']))) + self.bdv_writer.append_view(stack=None, virtual_stack_dim=shape, + illumination=acq_list.find_value_index(acq['shutterconfig'], 'shutterconfig'), + channel=acq_list.find_value_index(acq['laser'], 'laser'), + angle=acq_list.find_value_index(acq['rot'], 'rot'), + tile=acq_list.get_tile_index(acq), + voxel_units='um', + voxel_size_xyz=(px_size_um, px_size_um, acq['z_step']), + calibration=(1.0, 1.0, acq['z_step']/px_size_um), + m_affine=affine_matrix, + name_affine="Translation to Regular Grid" + ) + else: + self.fsize = self.x_pixels*self.y_pixels + self.xy_stack = np.memmap(self.path, mode="write", dtype=np.uint16, shape=self.fsize * self.max_frame) + + self.cur_image = 0 + + def write_image(self, image, acq, acq_list): + if self.file_extension == '.h5': + self.bdv_writer.append_plane(plane=image, z=self.cur_image, + illumination=acq_list.find_value_index(acq['shutterconfig'], 'shutterconfig'), + channel=acq_list.find_value_index(acq['laser'], 'laser'), + angle=acq_list.find_value_index(acq['rot'], 'rot'), + tile=acq_list.get_tile_index(acq) + ) + else: + image = image.flatten() + self.xy_stack[self.cur_image*self.fsize:(self.cur_image+1)*self.fsize] = image + + self.cur_image += 1 + + def end_acquisition(self, acq, acq_list): + if self.file_extension == '.h5': + if acq == acq_list[-1]: + try: + self.bdv_writer.set_attribute_labels('channel', tuple(acq_list.get_unique_attr_list('laser'))) + self.bdv_writer.set_attribute_labels('illumination', tuple(acq_list.get_unique_attr_list('shutterconfig'))) + self.bdv_writer.set_attribute_labels('angle', tuple(acq_list.get_unique_attr_list('rot'))) + self.bdv_writer.write_xml() + except: + logger.error(f'HDF5 XML could not be written: {sys.exc_info()}') + try: + self.bdv_writer.close() + except: + logger.error(f'HDF5 file could not be closed: {sys.exc_info()}') + else: + try: + del self.xy_stack + except: + logger.warning('Raw data stack could not be deleted') + + def write_snap_image(self, image): + timestr = time.strftime("%Y%m%d-%H%M%S") + filename = timestr + '.tif' + path = self.state['snap_folder']+'/'+filename + tifffile.imsave(path, image, photometric='minisblack') + + diff --git a/mesoSPIM/src/mesoSPIM_MainWindow.py b/mesoSPIM/src/mesoSPIM_MainWindow.py index 943f360..3296ea0 100644 --- a/mesoSPIM/src/mesoSPIM_MainWindow.py +++ b/mesoSPIM/src/mesoSPIM_MainWindow.py @@ -25,9 +25,6 @@ from .mesoSPIM_Core import mesoSPIM_Core from .devices.joysticks.mesoSPIM_JoystickHandlers import mesoSPIM_JoystickHandler -from .utils.demo_threads import mesoSPIM_DemoThread - - class mesoSPIM_MainWindow(QtWidgets.QMainWindow): ''' Main application window which instantiates worker objects and moves them @@ -108,6 +105,7 @@ def __init__(self, config=None): #logger.info('Core thread affinity after moveToThread? Answer:'+str(id(self.core.thread()))) ''' Get buttons & connections ready ''' + self.initialize_and_connect_menubar() self.initialize_and_connect_widgets() ''' Widget list for blockSignals during status updates ''' @@ -170,6 +168,11 @@ def __del__(self): except: pass + def close_app(self): + self.camera_window.close() + self.acquisition_manager_window.close() + self.close() + def display_icons(self): pass ''' Disabled taskbar button progress display due to problems with Anaconda default @@ -293,6 +296,12 @@ def create_script_window(self): exec(windowstring+'.sig_execute_script.connect(self.execute_script)') self.script_window_counter += 1 + def initialize_and_connect_menubar(self): + self.actionExit.triggered.connect(self.close_app) + self.actionOpen_Camera_Window.triggered.connect(self.camera_window.show) + self.actionOpen_Acquisition_Manager.triggered.connect(self.acquisition_manager_window.show) + + def initialize_and_connect_widgets(self): ''' Connecting the menu actions ''' self.openScriptEditorButton.clicked.connect(self.create_script_window) @@ -322,7 +331,42 @@ def initialize_and_connect_widgets(self): self.rotZeroButton.clicked.connect(lambda bool: self.sig_zero_axes.emit(['theta']) if bool is True else self.sig_unzero_axes.emit(['theta'])) self.xyzLoadButton.clicked.connect(self.sig_load_sample.emit) self.xyzUnloadButton.clicked.connect(self.sig_unload_sample.emit) - + + ''' Disabling UI buttons if necessary ''' + if hasattr(self.cfg, 'ui_options'): + if self.cfg.ui_options['enable_x_buttons'] is False: + self.xPlusButton.setEnabled(False) + self.xMinusButton.setEnabled(False) + + if self.cfg.ui_options['enable_y_buttons'] is False: + self.yPlusButton.setEnabled(False) + self.yMinusButton.setEnabled(False) + + if self.cfg.ui_options['enable_x_buttons'] is False and self.cfg.ui_options['enable_y_buttons'] is False: + self.xyZeroButton.setEnabled(False) + + if self.cfg.ui_options['enable_z_buttons'] is False: + self.zPlusButton.setEnabled(False) + self.zMinusButton.setEnabled(False) + self.zZeroButton.setEnabled(False) + + if self.cfg.ui_options['enable_f_buttons'] is False: + self.focusPlusButton.setEnabled(False) + self.focusMinusButton.setEnabled(False) + self.focusZeroButton.setEnabled(False) + + if self.cfg.ui_options['enable_rotation_buttons'] is False: + self.rotPlusButton.setEnabled(False) + self.rotMinusButton.setEnabled(False) + self.rotZeroButton.setEnabled(False) + self.goToRotationPositionButton.setEnabled(False) + self.markRotationPositionButton.setEnabled(False) + + if self.cfg.ui_options['enable_loading_buttons'] is False: + self.xyzLoadButton.setEnabled(False) + self.xyzUnloadButton.setEnabled(False) + + ''' Connecting state-changing buttons ''' self.LiveButton.clicked.connect(self.run_live) self.SnapButton.clicked.connect(self.run_snap) self.RunSelectedAcquisitionButton.clicked.connect(self.run_selected_acquisition) diff --git a/mesoSPIM/src/mesoSPIM_Serial.py b/mesoSPIM/src/mesoSPIM_Serial.py index 4ba2ad5..866ffb8 100644 --- a/mesoSPIM/src/mesoSPIM_Serial.py +++ b/mesoSPIM/src/mesoSPIM_Serial.py @@ -6,32 +6,25 @@ filter wheels, zoom systems etc. ''' -import numpy as np -import time - import logging logger = logging.getLogger(__name__) - -'''PyQt5 Imports''' -from PyQt5 import QtWidgets, QtCore, QtGui +from PyQt5 import QtCore ''' Import mesoSPIM modules ''' from .mesoSPIM_State import mesoSPIM_StateSingleton from .devices.filter_wheels.ludlcontrol import LudlFilterwheel +from .devices.filter_wheels.sutterLambdaControl import Lambda10B from .devices.filter_wheels.mesoSPIM_FilterWheel import mesoSPIM_DemoFilterWheel from .devices.zoom.mesoSPIM_Zoom import DynamixelZoom, DemoZoom -from .mesoSPIM_Stages import mesoSPIM_PIstage, mesoSPIM_DemoStage, mesoSPIM_GalilStages, mesoSPIM_PI_f_rot_and_Galil_xyz_Stages, mesoSPIM_PI_rot_and_Galil_xyzf_Stages, mesoSPIM_PI_rotz_and_Galil_xyf_Stages, mesoSPIM_PI_rotzf_and_Galil_xy_Stages -# from .mesoSPIM_State import mesoSPIM_State +from .mesoSPIM_Stages import mesoSPIM_PI_1toN, mesoSPIM_PI_NtoN, mesoSPIM_DemoStage, mesoSPIM_GalilStages, mesoSPIM_PI_f_rot_and_Galil_xyz_Stages, mesoSPIM_PI_rot_and_Galil_xyzf_Stages, mesoSPIM_PI_rotz_and_Galil_xyf_Stages, mesoSPIM_PI_rotzf_and_Galil_xy_Stages + class mesoSPIM_Serial(QtCore.QObject): '''This class handles mesoSPIM serial connections''' sig_finished = QtCore.pyqtSignal() - sig_state_request = QtCore.pyqtSignal(dict) - sig_position = QtCore.pyqtSignal(dict) - sig_zero_axes = QtCore.pyqtSignal(list) sig_unzero_axes = QtCore.pyqtSignal(list) sig_stop_movement = QtCore.pyqtSignal() @@ -45,43 +38,47 @@ def __init__(self, parent): ''' Assign the parent class to a instance variable for callbacks ''' self.parent = parent self.cfg = parent.cfg - self.state = mesoSPIM_StateSingleton() ''' Handling of state changing requests ''' self.parent.sig_state_request.connect(self.state_request_handler) - self.parent.sig_state_request_and_wait_until_done.connect(lambda dict: self.state_request_handler(dict, wait_until_done=True), type=3) + self.parent.sig_state_request_and_wait_until_done.connect(lambda sdict: self.state_request_handler(sdict, wait_until_done=True), type=3) ''' Attaching the filterwheel ''' if self.cfg.filterwheel_parameters['filterwheel_type'] == 'Ludl': - self.filterwheel = LudlFilterwheel(self.cfg.filterwheel_parameters['COMport'],self.cfg.filterdict) + self.filterwheel = LudlFilterwheel(self.cfg.filterwheel_parameters['COMport'], self.cfg.filterdict) elif self.cfg.filterwheel_parameters['filterwheel_type'] == 'DemoFilterWheel': self.filterwheel = mesoSPIM_DemoFilterWheel(self.cfg.filterdict) + elif self.cfg.filterwheel_parameters['filterwheel_type'] == 'Sutter': + self.filterwheel = Lambda10B(self.cfg.filterwheel_parameters['COMport'], self.cfg.filterdict) ''' Attaching the zoom ''' if self.cfg.zoom_parameters['zoom_type'] == 'Dynamixel': - self.zoom = DynamixelZoom(self.cfg.zoomdict,self.cfg.zoom_parameters['COMport'],self.cfg.zoom_parameters['servo_id']) + self.zoom = DynamixelZoom(self.cfg.zoomdict, self.cfg.zoom_parameters['COMport'],self.cfg.zoom_parameters['servo_id']) elif self.cfg.zoom_parameters['zoom_type'] == 'DemoZoom': self.zoom = DemoZoom(self.cfg.zoomdict) ''' Attaching the stage ''' - if self.cfg.stage_parameters['stage_type'] == 'PI': - self.stage = mesoSPIM_PIstage(self) + if self.cfg.stage_parameters['stage_type'] in {'PI', 'PI_1controllerNstages'}: + self.stage = mesoSPIM_PI_1toN(self) + elif self.cfg.stage_parameters['stage_type'] == 'PI_NcontrollersNstages': + self.stage = mesoSPIM_PI_NtoN(self) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'GalilStage': self.stage = mesoSPIM_GalilStages(self) - self.stage.sig_position.connect(lambda dict: self.sig_position.emit({'position': dict})) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'PI_rot_and_Galil_xyzf': self.stage = mesoSPIM_PI_rot_and_Galil_xyzf_Stages(self) - self.stage.sig_position.connect(lambda dict: self.sig_position.emit({'position': dict})) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'PI_f_rot_and_Galil_xyz': self.stage = mesoSPIM_PI_f_rot_and_Galil_xyz_Stages(self) - self.stage.sig_position.connect(lambda dict: self.sig_position.emit({'position': dict})) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'PI_rotz_and_Galil_xyf': self.stage = mesoSPIM_PI_rotz_and_Galil_xyf_Stages(self) - self.stage.sig_position.connect(lambda dict: self.sig_position.emit({'position': dict})) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'PI_rotzf_and_Galil_xy': self.stage = mesoSPIM_PI_rotzf_and_Galil_xy_Stages(self) - self.stage.sig_position.connect(lambda dict: self.sig_position.emit({'position': dict})) + self.stage.sig_position.connect(lambda sdict: self.sig_position.emit({'position': sdict})) elif self.cfg.stage_parameters['stage_type'] == 'DemoStage': self.stage = mesoSPIM_DemoStage(self) try: @@ -91,10 +88,10 @@ def __init__(self, parent): ''' Wiring signals through to child objects ''' self.parent.sig_move_relative.connect(self.move_relative) - self.parent.sig_move_relative_and_wait_until_done.connect(lambda dict: self.move_relative(dict, wait_until_done=True), type=3) + self.parent.sig_move_relative_and_wait_until_done.connect(lambda sdict: self.move_relative(sdict, wait_until_done=True), type=3) self.parent.sig_move_absolute.connect(self.move_absolute) - self.parent.sig_move_absolute_and_wait_until_done.connect(lambda dict: self.move_absolute(dict, wait_until_done=True), type=3) + self.parent.sig_move_absolute_and_wait_until_done.connect(lambda sdict: self.move_absolute(sdict, wait_until_done=True), type=3) self.parent.sig_zero_axes.connect(self.sig_zero_axes.emit) self.parent.sig_unzero_axes.connect(self.sig_unzero_axes.emit) @@ -110,8 +107,8 @@ def __init__(self, parent): @QtCore.pyqtSlot(dict) - def state_request_handler(self, dict, wait_until_done=False): - for key, value in zip(dict.keys(),dict.values()): + def state_request_handler(self, sdict, wait_until_done=False): + for key, value in zip(sdict.keys(), sdict.values()): # print('Serial thread: state request: Key: ', key, ' Value: ', value) ''' Here, the request handling is done with lots if 'ifs' @@ -135,25 +132,25 @@ def state_request_handler(self, dict, wait_until_done=False): logger.info('Thread ID during live: '+str(int(QtCore.QThread.currentThreadId()))) @QtCore.pyqtSlot(dict) - def move_relative(self, dict, wait_until_done=False): + def move_relative(self, sdict, wait_until_done=False): # logger.info('Thread ID during relative movement: '+str(int(QtCore.QThread.currentThreadId()))) # logger.info('Thread ID during move rel: '+str(int(QtCore.QThread.currentThreadId()))) if wait_until_done: - self.stage.move_relative(dict, wait_until_done=True) + self.stage.move_relative(sdict, wait_until_done=True) else: - self.stage.move_relative(dict) + self.stage.move_relative(sdict) @QtCore.pyqtSlot(dict) - def move_absolute(self, dict, wait_until_done=False): + def move_absolute(self, sdict, wait_until_done=False): if wait_until_done: - self.stage.move_absolute(dict, wait_until_done=True) + self.stage.move_absolute(sdict, wait_until_done=True) else: - self.stage.move_absolute(dict) + self.stage.move_absolute(sdict) @QtCore.pyqtSlot(dict) - def report_position(self, dict): - self.sig_position.emit({'position': dict}) + def report_position(self, sdict): + self.sig_position.emit({'position': sdict}) @QtCore.pyqtSlot() def go_to_rotation_position(self, wait_until_done=False): @@ -163,13 +160,13 @@ def go_to_rotation_position(self, wait_until_done=False): self.stage.go_to_rotation_position() @QtCore.pyqtSlot(str) - def set_filter(self, filter, wait_until_done=False): + def set_filter(self, sfilter, wait_until_done=False): # logger.info('Thread ID during set filter: '+str(int(QtCore.QThread.currentThreadId()))) if wait_until_done: - self.filterwheel.set_filter(filter, wait_until_done=True) + self.filterwheel.set_filter(sfilter, wait_until_done=True) else: - self.filterwheel.set_filter(filter, wait_until_done=False) - self.state['filter'] = filter + self.filterwheel.set_filter(sfilter, wait_until_done=False) + self.state['filter'] = sfilter @QtCore.pyqtSlot(str) def set_zoom(self, zoom, wait_until_done=False): diff --git a/mesoSPIM/src/mesoSPIM_Stages.py b/mesoSPIM/src/mesoSPIM_Stages.py index b518024..6b56fdc 100644 --- a/mesoSPIM/src/mesoSPIM_Stages.py +++ b/mesoSPIM/src/mesoSPIM_Stages.py @@ -3,14 +3,11 @@ ====================== ''' import time - import logging + logger = logging.getLogger(__name__) from PyQt5 import QtCore -from .mesoSPIM_State import mesoSPIM_StateSingleton - -# from .mesoSPIM_State import mesoSPIM_StateSingleton class mesoSPIM_Stage(QtCore.QObject): ''' @@ -31,14 +28,14 @@ class mesoSPIM_Stage(QtCore.QObject): ''' sig_position = QtCore.pyqtSignal(dict) - sig_status_message = QtCore.pyqtSignal(str,int) + sig_status_message = QtCore.pyqtSignal(str, int) - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__() self.parent = parent self.cfg = parent.cfg - #self.state = mesoSPIM_StateSingleton() + # self.state = mesoSPIM_StateSingleton() ''' The movement signals are emitted by the mesoSPIM_Core, which in turn instantiates the mesoSPIM_Serial thread. @@ -119,7 +116,7 @@ def __init__(self, parent = None): ''' # self.sig_status_message.connect(lambda string, time: print(string)) - logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) + logger.info('Thread ID at Startup: ' + str(int(QtCore.QThread.currentThreadId()))) def create_position_dict(self): self.position_dict = {'x_pos': self.x_pos, @@ -160,35 +157,35 @@ def move_relative(self, dict, wait_until_done=False): if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: self.x_pos = self.x_pos + x_rel else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: self.y_pos = self.y_pos + y_rel else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: self.z_pos = self.z_pos + z_rel else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: self.theta_pos = self.theta_pos + theta_rel else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: self.f_pos = self.f_pos + f_rel else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if wait_until_done == True: time.sleep(0.02) @@ -203,7 +200,7 @@ def move_absolute(self, dict, wait_until_done=False): if self.x_min < x_abs and self.x_max > x_abs: self.x_pos = x_abs else: - self.sig_status_message.emit('Absolute movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: X Motion limit would be reached!', 1000) if 'y_abs' in dict: y_abs = dict['y_abs'] @@ -211,7 +208,7 @@ def move_absolute(self, dict, wait_until_done=False): if self.y_min < y_abs and self.y_max > y_abs: self.y_pos = y_abs else: - self.sig_status_message.emit('Absolute movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Y Motion limit would be reached!', 1000) if 'z_abs' in dict: z_abs = dict['z_abs'] @@ -219,7 +216,7 @@ def move_absolute(self, dict, wait_until_done=False): if self.z_min < z_abs and self.z_max > z_abs: self.z_pos = z_abs else: - self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!', 1000) if 'f_abs' in dict: f_abs = dict['f_abs'] @@ -227,7 +224,7 @@ def move_absolute(self, dict, wait_until_done=False): if self.f_min < f_abs and self.f_max > f_abs: self.f_pos = f_abs else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] @@ -235,26 +232,26 @@ def move_absolute(self, dict, wait_until_done=False): if self.theta_min < theta_abs and self.theta_max > theta_abs: self.theta_pos = theta_abs else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: time.sleep(3) @QtCore.pyqtSlot() def stop(self): - self.sig_status_message.emit('Stopped',0) + self.sig_status_message.emit('Stopped', 0) def zero_axes(self, list): for axis in list: try: - exec('self.int_'+axis+'_pos_offset = -self.'+axis+'_pos') + exec('self.int_' + axis + '_pos_offset = -self.' + axis + '_pos') except: logger.info('Zeroing of axis: ', axis, 'failed') def unzero_axes(self, list): for axis in list: try: - exec('self.int_'+axis+'_pos_offset = 0') + exec('self.int_' + axis + '_pos_offset = 0') except: logger.info('Unzeroing of axis: ', axis, 'failed') @@ -269,7 +266,8 @@ def mark_rotation_position(self): self.x_rot_position = self.x_pos self.y_rot_position = self.y_pos self.z_rot_position = self.z_pos - logger.info('Marking new rotation position (absolute coordinates): X: ', self.x_pos, ' Y: ', self.y_pos, ' Z: ', self.z_pos) + logger.info('Marking new rotation position (absolute coordinates): X: ', self.x_pos, ' Y: ', self.y_pos, ' Z: ', + self.z_pos) def go_to_rotation_position(self, wait_until_done=False): ''' Move to the proper rotation position @@ -279,74 +277,74 @@ def go_to_rotation_position(self, wait_until_done=False): print('Going to rotation position: NOT IMPLEMENTED / DEMO MODE') logger.info('Going to rotation position: NOT IMPLEMENTED / DEMO MODE') + class mesoSPIM_DemoStage(mesoSPIM_Stage): - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) -class mesoSPIM_PIstage(mesoSPIM_Stage): - ''' - It is expected that the parent class has the following signals: - sig_move_relative = pyqtSignal(dict) - sig_move_relative_and_wait_until_done = pyqtSignal(dict) - sig_move_absolute = pyqtSignal(dict) - sig_move_absolute_and_wait_until_done = pyqtSignal(dict) - sig_zero = pyqtSignal(list) - sig_unzero = pyqtSignal(list) - sig_stop_movement = pyqtSignal() - sig_mark_rotation_position = pyqtSignal() - - Also contains a QTimer that regularily sends position updates, e.g - during the execution of movements. +class mesoSPIM_PI_1toN(mesoSPIM_Stage): + ''' + Configuration with 1 controller connected to N stages, (e.g. C-884, default mesoSPIM V5 setup). + + Note: + configs as declared in mesoSPIM_config.py: + stage_parameters = {'stage_type' : 'PI_1controllerNstages', + ... + } + pi_parameters = {'controllername' : 'C-884', + 'stages' : ('L-509.20DG10','L-509.40DG10','L-509.20DG10','M-060.DG','M-406.4PD','NOSTAGE'), + 'refmode' : ('FRF',), + 'serialnum' : ('118075764'), + } ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) - - ''' - PI-specific code - ''' from pipython import GCSDevice, pitools - self.pitools = pitools ''' Setting up the PI stages ''' self.pi = self.cfg.pi_parameters - self.controllername = self.cfg.pi_parameters['controllername'] - self.pi_stages = self.cfg.pi_parameters['stages'] - # ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE') + self.pi_stages = list(self.cfg.pi_parameters['stages']) self.refmode = self.cfg.pi_parameters['refmode'] - # self.serialnum = ('118015439') # Wyss Geneva - self.serialnum = self.cfg.pi_parameters['serialnum'] # UZH Irchel H45 - + self.serialnum = self.cfg.pi_parameters['serialnum'] self.pidevice = GCSDevice(self.controllername) self.pidevice.ConnectUSB(serialnum=self.serialnum) ''' PI startup ''' - ''' with refmode enabled: pretty dangerous pitools.startup(self.pidevice, stages=self.pi_stages, refmode=self.refmode) ''' pitools.startup(self.pidevice, stages=self.pi_stages) + ''' Report reference status of all stages ''' + for ii in range(1, len(self.pi_stages) + 1): + tStage = self.pi_stages[ii - 1] + if tStage == 'NOSTAGE': + continue + + tState = self.pidevice.qFRF(ii) + if tState[ii]: + msg = 'referenced' + else: + msg = '*UNREFERENCED*' + + logger.info("Axis %d (%s) reference status: %s" % (ii, tStage, msg)) + ''' Stage 5 referencing hack ''' - # print('Referencing status 3: ', self.pidevice.qFRF(3)) - # print('Referencing status 5: ', self.pidevice.qFRF(5)) self.pidevice.FRF(5) logger.info('mesoSPIM_Stages: M-406 Emergency referencing hack: Waiting for referencing move') self.block_till_controller_is_ready() logger.info('mesoSPIM_Stages: M-406 Emergency referencing hack done') - # print('Again: Referencing status 3: ', self.pidevice.qFRF(3)) - # print('Again: Referencing status 5: ', self.pidevice.qFRF(5)) ''' Stage 5 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] - self.pidevice.MOV(5,self.startfocus/1000) + self.pidevice.MOV(5, self.startfocus / 1000) def __del__(self): try: - '''Close the PI connection''' self.pidevice.unload() logger.info('Stage disconnected') except: @@ -354,11 +352,10 @@ def __del__(self): def report_position(self): positions = self.pidevice.qPOS(self.pidevice.axes) - - self.x_pos = round(positions['1']*1000,2) - self.y_pos = round(positions['2']*1000,2) - self.z_pos = round(positions['3']*1000,2) - self.f_pos = round(positions['5']*1000,2) + self.x_pos = round(positions['1'] * 1000, 2) + self.y_pos = round(positions['2'] * 1000, 2) + self.z_pos = round(positions['3'] * 1000, 2) + self.f_pos = round(positions['5'] * 1000, 2) self.theta_pos = positions['4'] self.create_position_dict() @@ -370,9 +367,7 @@ def report_position(self): self.int_theta_pos = self.theta_pos + self.int_theta_pos_offset self.create_internal_position_dict() - # self.state['position'] = self.int_position_dict - self.sig_position.emit(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): @@ -382,50 +377,48 @@ def move_relative(self, dict, wait_until_done=False): ''' if 'x_rel' in dict: x_rel = dict['x_rel'] - if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - x_rel = x_rel/1000 - self.pidevice.MVR({1 : x_rel}) + if self.x_min < self.x_pos + x_rel < self.x_max: + x_rel = x_rel / 1000 + self.pidevice.MVR({1: x_rel}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] - if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - y_rel = y_rel/1000 - self.pidevice.MVR({2 : y_rel}) + if self.y_min < self.y_pos + y_rel < self.y_max: + y_rel = y_rel / 1000 + self.pidevice.MVR({2: y_rel}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] - if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - z_rel = z_rel/1000 - self.pidevice.MVR({3 : z_rel}) + if self.z_min < self.z_pos + z_rel < self.z_max: + z_rel = z_rel / 1000 + self.pidevice.MVR({3: z_rel}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] - if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({4 : theta_rel}) + if self.theta_min < self.theta_pos + theta_rel < self.theta_max: + self.pidevice.MVR({4: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] - if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - f_rel = f_rel/1000 - self.pidevice.MVR({5 : f_rel}) + if self.f_min < self.f_pos + f_rel < self.f_max: + f_rel = f_rel / 1000 + self.pidevice.MVR({5: f_rel}) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) - if wait_until_done == True: + if wait_until_done: self.pitools.waitontarget(self.pidevice) def move_absolute(self, dict, wait_until_done=False): ''' - PI move absolute method - Lots of implementation details in here, should be replaced by a facade TODO: Also lots of repeating code. @@ -435,74 +428,74 @@ def move_absolute(self, dict, wait_until_done=False): if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - if self.x_min < x_abs and self.x_max > x_abs: + if self.x_min < x_abs < self.x_max: ''' Conversion to mm and command emission''' - x_abs= x_abs/1000 - self.pidevice.MOV({1 : x_abs}) + x_abs = x_abs / 1000 + self.pidevice.MOV({1: x_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: X Motion limit would be reached!', 1000) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - if self.y_min < y_abs and self.y_max > y_abs: + if self.y_min < y_abs < self.y_max: ''' Conversion to mm and command emission''' - y_abs= y_abs/1000 - self.pidevice.MOV({2 : y_abs}) + y_abs = y_abs / 1000 + self.pidevice.MOV({2: y_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Y Motion limit would be reached!', 1000) if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset - if self.z_min < z_abs and self.z_max > z_abs: + if self.z_min < z_abs < self.z_max: ''' Conversion to mm and command emission''' - z_abs= z_abs/1000 - self.pidevice.MOV({3 : z_abs}) + z_abs = z_abs / 1000 + self.pidevice.MOV({3: z_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!', 1000) if 'f_abs' in dict: f_abs = dict['f_abs'] f_abs = f_abs - self.int_f_pos_offset - if self.f_min < f_abs and self.f_max > f_abs: + if self.f_min < f_abs < self.f_max: ''' Conversion to mm and command emission''' - f_abs= f_abs/1000 - self.pidevice.MOV({5 : f_abs}) + f_abs = f_abs / 1000 + self.pidevice.MOV({5: f_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset - if self.theta_min < theta_abs and self.theta_max > theta_abs: + if self.theta_min < theta_abs < self.theta_max: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({4 : theta_abs}) + self.pidevice.MOV({4: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) - if wait_until_done == True: + if wait_until_done: self.pitools.waitontarget(self.pidevice) def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - y_abs = self.cfg.stage_parameters['y_load_position']/1000 - self.pidevice.MOV({2 : y_abs}) + y_abs = self.cfg.stage_parameters['y_load_position'] / 1000 + self.pidevice.MOV({2: y_abs}) def unload_sample(self): - y_abs = self.cfg.stage_parameters['y_unload_position']/1000 - self.pidevice.MOV({2 : y_abs}) + y_abs = self.cfg.stage_parameters['y_unload_position'] / 1000 + self.pidevice.MOV({2: y_abs}) def go_to_rotation_position(self, wait_until_done=False): - x_abs = self.x_rot_position/1000 - y_abs = self.y_rot_position/1000 - z_abs = self.z_rot_position/1000 + x_abs = self.x_rot_position / 1000 + y_abs = self.y_rot_position / 1000 + z_abs = self.z_rot_position / 1000 - self.pidevice.MOV({1 : x_abs, 2 : y_abs, 3 : z_abs}) + self.pidevice.MOV({1: x_abs, 2: y_abs, 3: z_abs}) - if wait_until_done == True: + if wait_until_done: self.pitools.waitontarget(self.pidevice) def block_till_controller_is_ready(self): @@ -517,6 +510,205 @@ def block_till_controller_is_ready(self): else: time.sleep(0.1) + +class mesoSPIM_PI_NtoN(mesoSPIM_Stage): + ''' + Expects following microscope configuration: + Sample XYZ movement: Physik Instrumente stage with three L-509-type stepper motor stages and individual C-663 controller. + F movement: Physik Instrumente C-663 controller and custom stage with stepper motor + Rotation: not implemented + + All stage controller are of same type and the sample stages work with reference setting. + Focus stage has reference mode set to off. + + Note: + configs as declared in mesoSPIM_config.py: + stage_parameters = {'stage_type' : 'PI_NcontrollersNstages', + ... + } + pi_parameters = {'axes_names': ('x', 'y', 'z', 'theta', 'f'), + 'stages': ('L-509.20SD00', 'L-509.40SD00', 'L-509.20SD00', None, 'MESOSPIM_FOCUS'), + 'controllername': ('C-663', 'C-663', 'C-663', None, 'C-663'), + 'serialnum': ('**********', '**********', '**********', None, '**********'), + 'refmode': ('FRF', 'FRF', 'FRF', None, 'RON') + } + make sure that reference points are not in conflict with general microscope setup + and will not hurt optics under referencing at startup + ''' + + def __init__(self, parent=None): + super().__init__(parent) + from pipython import GCSDevice, pitools + self.pitools = pitools + self.pi = self.cfg.pi_parameters + print("Connecting stage drive...") + + # Setting up the stages with separate PI controller. + # Explicitly set referencing status and get position + + # gather stage devices in VirtualStages class + class VirtualStages: + pass + + assert len(self.pi['axes_names']) == len(self.pi['stages']) == len(self.pi['controllername']) \ + == len(self.pi['serialnum']) == len(self.pi['refmode']), \ + "Config file, pi_parameters dictionary: numbers of axes_names, stages, controllername, serialnum, refmode must match " + self.pi_stages = VirtualStages() + for axis_name, stage, controller, serialnum, refmode in zip(self.pi['axes_names'], self.pi['stages'], + self.pi['controllername'], self.pi['serialnum'], + self.pi['refmode']): + # run stage startup procedure for each axis + if stage: + print(f'starting stage {stage}') + pidevice_ = GCSDevice(controller) + pidevice_.ConnectUSB(serialnum=serialnum) + if refmode is None: + pitools.startup(pidevice_, stages=stage) + elif refmode == 'FRF': + pitools.startup(pidevice_, stages=stage, refmodes=refmode) + pidevice_.FRF(1) + elif refmode == 'RON': + pitools.startup(pidevice_, stages=stage) + pidevice_.RON({1: 0}) # set reference mode + # activate servo + pidevice_.SVO(pidevice_.axes, [True] * len(pidevice_.axes)) + # print('servo state: {}'.format(pidevice_.qSVO())) + # set/get actual position as home position + # assumes that starting position is within reasonable distance from optimal focus + pidevice_.POS({1: 0.0}) + pidevice_.DFH(1) + else: + raise ValueError(f"refmode {refmode} is not supported, PI stage {stage} initialization failed") + print(f'stage {stage} started') + print('axis {}, referencing mode: {}'.format(axis_name, pidevice_.qRON())) + self.wait_for_controller(pidevice_) + print('axis {}, stage {} ready'.format(axis_name, stage)) + setattr(self.pi_stages, ('pidevice_' + axis_name), pidevice_) + else: + setattr(self.pi_stages, axis_name, None) + + logger.info('mesoSPIM_PI_NtoN: started') + + + def wait_for_controller(self, controller): + # function used during stage setup + blockflag = True + while blockflag: + if controller.IsControllerReady(): + blockflag = False + else: + time.sleep(0.1) + + + def __del__(self): + '''Close the PI connection''' + try: + [(getattr(self.pi_stages, ('pidevice_' + axis_name))).unload() for axis_name in self.pi['axes_names'] if + (hasattr(self.pi_stages, ('pidevice_' + axis_name)))] + logger.info('Stages disconnected') + except: + logger.info('Error while disconnecting the PI stage') + + + def report_position(self): + '''report stage position''' + for axis_name in self.pi['axes_names']: + pidevice_name = 'pidevice_' + str(axis_name) + if hasattr(self.pi_stages, pidevice_name): + try: + if axis_name is None: + pos = 0 + elif axis_name == 'theta': + pos = (getattr(self.pi_stages, pidevice_name)).qPOS(1)[1] + else: + pos = round((getattr(self.pi_stages, pidevice_name)).qPOS(1)[1] * 1000, 2) + except: + print(f"Failed to report_position for axis_name {axis_name}, pidevice_name {pidevice_name}.") + else: + pos = 0 + + setattr(self, (axis_name + '_pos'), pos) + int_pos = pos + getattr(self, ('int_' + axis_name + '_pos_offset')) + setattr(self, ('int_' + axis_name + '_pos'), int_pos) + + self.create_position_dict() + self.create_internal_position_dict() + + self.sig_position.emit(self.int_position_dict) + + + def move_relative(self, move_dict, wait_until_done=False): + ''' PI move relative method ''' + for axis_move in move_dict.keys(): + axis_name = axis_move.split('_')[0] + move_value = move_dict[axis_move] + + if (hasattr(self.pi_stages, ('pidevice_' + axis_name))): + if (getattr(self, (axis_name + '_min')) < getattr(self, (axis_name + '_pos')) + move_value) and \ + (getattr(self, (axis_name + '_max')) > getattr(self, (axis_name + '_pos')) + move_value): + if not axis_name=='theta': + move_value = move_value/1000 + (getattr(self.pi_stages, ('pidevice_' + axis_name))).MVR({1 : move_value}) + else: + self.sig_status_message.emit('Relative movement stopped: {} Motion limit would be reached!'.format(axis_name),1000) + if (axis_name == 'f') or (wait_until_done == True): + self.pitools.waitontarget(getattr(self.pi_stages, ('pidevice_' + axis_name))) # focus may be slower than expected + + + def move_absolute(self, move_dict, wait_until_done=False): + ''' PI move absolute method ''' + for axis_move in move_dict.keys(): + axis_name = axis_move.split('_')[0] + move_value = move_dict[axis_move] + move_value = move_value - getattr(self, ('int_' + axis_name + '_pos_offset')) + + if (hasattr(self.pi_stages, ('pidevice_' + axis_name))): + if (getattr(self, (axis_name + '_min')) < move_value) and \ + (getattr(self, (axis_name + '_max')) > move_value): + if not axis_name == 'theta': + move_value = move_value / 1000 + (getattr(self.pi_stages, ('pidevice_' + axis_name))).MOV({1: move_value}) + else: + self.sig_status_message.emit( + 'Absolute movement stopped: {} Motion limit would be reached!'.format(axis_name), 1000) + if (axis_name == 'f') or (wait_until_done == True): + self.pitools.waitontarget(getattr(self.pi_stages, ('pidevice_' + axis_name))) # focus may be slower than expected + + + def stop(self): + '''stop stage movement''' + [(getattr(self.pi_stages, ('pidevice_' + axis_name))).STP(noraise=True) for axis_name in self.pi['axes_names'] + if (hasattr(self.pi_stages, ('pidevice_' + axis_name)))] + + + def load_sample(self): + '''bring sample to imaging position''' + axis_name = 'y' + y_abs = self.cfg.stage_parameters['y_load_position'] / 1000 + (getattr(self.pi_stages, ('pidevice_' + axis_name))).MOV({1: y_abs}) + + + def unload_sample(self): + '''lift sample to sample handling position''' + axis_name = 'y' + y_abs = self.cfg.stage_parameters['y_unload_position'] / 1000 + (getattr(self.pi_stages, ('pidevice_' + axis_name))).MOV({1: y_abs}) + + + ''' + # currently not implemented for this microscope configuration + def go_to_rotation_position(self, wait_until_done=False): + x_abs = self.x_rot_position/1000 + y_abs = self.y_rot_position/1000 + z_abs = self.z_rot_position/1000 + + self.pidevice.MOV({1 : x_abs, 2 : y_abs, 3 : z_abs}) + + if wait_until_done == True: + self.pitools.waitontarget(self.pidevice) + ''' + + class mesoSPIM_GalilStages(mesoSPIM_Stage): ''' @@ -536,7 +728,7 @@ class mesoSPIM_GalilStages(mesoSPIM_Stage): Todo: Rotation handling not implemented! ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) ''' @@ -550,15 +742,15 @@ def __init__(self, parent = None): self.f_encodercounts_per_um = self.cfg.f_galil_parameters['z_encodercounts_per_um'] ''' Setting up the Galil stages ''' - self.xyz_stage = StageControlGalil(COMport = self.cfg.xyz_galil_parameters['COMport'], - x_encodercounts_per_um = self.x_encodercounts_per_um, - y_encodercounts_per_um = self.y_encodercounts_per_um, - z_encodercounts_per_um = self.z_encodercounts_per_um) + self.xyz_stage = StageControlGalil(COMport=self.cfg.xyz_galil_parameters['COMport'], + x_encodercounts_per_um=self.x_encodercounts_per_um, + y_encodercounts_per_um=self.y_encodercounts_per_um, + z_encodercounts_per_um=self.z_encodercounts_per_um) - self.f_stage = StageControlGalil(COMport = self.cfg.f_galil_parameters['COMport'], - x_encodercounts_per_um = 0, - y_encodercounts_per_um = 0, - z_encodercounts_per_um = self.f_encodercounts_per_um) + self.f_stage = StageControlGalil(COMport=self.cfg.f_galil_parameters['COMport'], + x_encodercounts_per_um=0, + y_encodercounts_per_um=0, + z_encodercounts_per_um=self.f_encodercounts_per_um) ''' print('Galil: ', self.xyz_stage.read_position('x')) print('Galil: ', self.xyz_stage.read_position('y')) @@ -601,42 +793,41 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - self.xyz_stage.move_relative(xrel = int(x_rel)) + self.xyz_stage.move_relative(xrel=int(x_rel)) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - self.xyz_stage.move_relative(yrel = int(y_rel)) + self.xyz_stage.move_relative(yrel=int(y_rel)) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - self.xyz_stage.move_relative(zrel = int(z_rel)) + self.xyz_stage.move_relative(zrel=int(z_rel)) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - print('No rotation stage attached') + print('No rotation stage attached') else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - self.f_stage.move_relative(zrel = f_rel) + self.f_stage.move_relative(zrel=f_rel) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if wait_until_done == True: pass - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -644,7 +835,7 @@ def move_absolute(self, dict, wait_until_done=False): Lots of implementation details in here, should be replaced by a facade ''' - #print(dict) + # print(dict) # if ('x_abs', 'y_abs', 'z_abs', 'f_abs') in dict: x_abs = dict['x_abs'] @@ -673,30 +864,18 @@ def move_absolute(self, dict, wait_until_done=False): # y_abs = self.cfg.stage_parameters['y_unload_position']/1000 # # self.pidevice.MOV({2 : y_abs}) + class mesoSPIM_PI_f_rot_and_Galil_xyz_Stages(mesoSPIM_Stage): ''' - - It is expected that the parent class has the following signals: - sig_move_relative = pyqtSignal(dict) - sig_move_relative_and_wait_until_done = pyqtSignal(dict) - sig_move_absolute = pyqtSignal(dict) - sig_move_absolute_and_wait_until_done = pyqtSignal(dict) - sig_zero = pyqtSignal(list) - sig_unzero = pyqtSignal(list) - sig_stop_movement = pyqtSignal() - sig_mark_rotation_position = pyqtSignal() - - Also contains a QTimer that regularily sends position updates, e.g - during the execution of movements. - + Deprecated? Todo: Rotation handling not implemented! Todo: Rotation axes are hardcoded! (M-605: #5, M-061.PD: #6) ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) - #self.state = mesoSPIM_StateSingleton() + # self.state = mesoSPIM_StateSingleton() self.pos_timer = QtCore.QTimer(self) self.pos_timer.timeout.connect(self.report_position) @@ -711,15 +890,16 @@ def __init__(self, parent = None): self.z_encodercounts_per_um = self.cfg.xyz_galil_parameters['z_encodercounts_per_um'] ''' Setting up the Galil stages ''' - self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.z_encodercounts_per_um]) + self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.z_encodercounts_per_um]) ''' self.f_stage = StageControlGalil(COMport = self.cfg.f_galil_parameters['COMport'], x_encodercounts_per_um = 0, y_encodercounts_per_um = 0, z_encodercounts_per_um = self.f_encodercounts_per_um) ''' - + ''' print('Galil: ', self.xyz_stage.read_position('x')) print('Galil: ', self.xyz_stage.read_position('y')) @@ -735,7 +915,7 @@ def __init__(self, parent = None): self.pi = self.cfg.pi_parameters self.controllername = self.cfg.pi_parameters['controllername'] - self.pi_stages = self.cfg.pi_parameters['stages'] + self.pi_stages = list(self.cfg.pi_parameters['stages']) # ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE') self.refmode = self.cfg.pi_parameters['refmode'] # self.serialnum = ('118015439') # Wyss Geneva @@ -768,7 +948,7 @@ def __init__(self, parent = None): ''' Stage 5 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] - self.pidevice.MOV(5,self.startfocus/1000) + self.pidevice.MOV(5, self.startfocus / 1000) def __del__(self): try: @@ -787,8 +967,8 @@ def report_position(self): position reports: Do not update positions in exceptional circumstances. ''' - self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() - self.f_pos = round(positions['5']*1000,2) + self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() + self.f_pos = round(positions['5'] * 1000, 2) self.theta_pos = positions['6'] self.create_position_dict() @@ -802,7 +982,7 @@ def report_position(self): self.create_internal_position_dict() self.sig_position.emit(self.int_position_dict) - #print(self.int_position_dict) + # print(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): ''' Galil move relative method @@ -814,47 +994,46 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - xyz_motion_dict.update({1:int(x_rel)}) + xyz_motion_dict.update({1: int(x_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - xyz_motion_dict.update({2:int(y_rel)}) + xyz_motion_dict.update({2: int(y_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - xyz_motion_dict.update({3:int(z_rel)}) + xyz_motion_dict.update({3: int(z_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) - + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) + if xyz_motion_dict != {}: self.xyz_stage.move_relative(xyz_motion_dict) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({6 : theta_rel}) + self.pidevice.MVR({6: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - f_rel = f_rel/1000 - self.pidevice.MVR({5 : f_rel}) + f_rel = f_rel / 1000 + self.pidevice.MVR({5: f_rel}) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') self.pitools.waitontarget(self.pidevice) - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -868,42 +1047,42 @@ def move_absolute(self, dict, wait_until_done=False): if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - xyz_motion_dict.update({1:x_abs}) + xyz_motion_dict.update({1: x_abs}) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - xyz_motion_dict.update({2:y_abs}) - + xyz_motion_dict.update({2: y_abs}) + if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset - xyz_motion_dict.update({3:z_abs}) - + xyz_motion_dict.update({3: z_abs}) + if xyz_motion_dict != {}: self.xyz_stage.move_absolute(xyz_motion_dict) - + if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') - + if 'f_abs' in dict: f_abs = dict['f_abs'] f_abs = f_abs - self.int_f_pos_offset if self.f_min < f_abs and self.f_max > f_abs: ''' Conversion to mm and command emission''' - f_abs= f_abs/1000 - self.pidevice.MOV({5 : f_abs}) + f_abs = f_abs / 1000 + self.pidevice.MOV({5: f_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset if self.theta_min < theta_abs and self.theta_max > theta_abs: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({6 : theta_abs}) + self.pidevice.MOV({6: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: self.pitools.waitontarget(self.pidevice) @@ -913,13 +1092,15 @@ def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - self.xyz_stage.move_absolute({1:self.int_x_pos, 2:self.cfg.stage_parameters['y_load_position'], 3:self.int_z_pos}) + self.xyz_stage.move_absolute( + {1: self.int_x_pos, 2: self.cfg.stage_parameters['y_load_position'], 3: self.int_z_pos}) def unload_sample(self): - self.xyz_stage.move_absolute({1:self.int_x_pos, 2:self.cfg.stage_parameters['y_unload_position'], 3:self.int_z_pos}) - + self.xyz_stage.move_absolute( + {1: self.int_x_pos, 2: self.cfg.stage_parameters['y_unload_position'], 3: self.int_z_pos}) + def go_to_rotation_position(self, wait_until_done=False): - self.xyz_stage.move_absolute({1:self.x_rot_position, 2:self.y_rot_position, 3:self.z_rot_position}) + self.xyz_stage.move_absolute({1: self.x_rot_position, 2: self.y_rot_position, 3: self.z_rot_position}) if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') @@ -939,6 +1120,7 @@ def execute_program(self): '''Executes program stored on the Galil controller''' self.xyz_stage.execute_program() + class mesoSPIM_PI_rot_and_Galil_xyzf_Stages(mesoSPIM_Stage): ''' Expects following microscope configuration: @@ -962,10 +1144,10 @@ class mesoSPIM_PI_rot_and_Galil_xyzf_Stages(mesoSPIM_Stage): ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) - #self.state = mesoSPIM_StateSingleton() + # self.state = mesoSPIM_StateSingleton() self.pos_timer = QtCore.QTimer(self) self.pos_timer.timeout.connect(self.report_position) @@ -981,19 +1163,21 @@ def __init__(self, parent = None): self.f_encodercounts_per_um = self.cfg.f_galil_parameters['f_encodercounts_per_um'] ''' Setting up the Galil stages: XYZ ''' - self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.z_encodercounts_per_um]) + self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.z_encodercounts_per_um]) ''' Setting up the Galil stages: F with two dummy axes.''' - self.f_stage = StageControlGalil(self.cfg.f_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.f_encodercounts_per_um]) + self.f_stage = StageControlGalil(self.cfg.f_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.f_encodercounts_per_um]) ''' self.f_stage = StageControlGalil(COMport = self.cfg.f_galil_parameters['COMport'], x_encodercounts_per_um = 0, y_encodercounts_per_um = 0, z_encodercounts_per_um = self.f_encodercounts_per_um) ''' - + ''' print('Galil: ', self.xyz_stage.read_position('x')) print('Galil: ', self.xyz_stage.read_position('y')) @@ -1024,7 +1208,7 @@ def __init__(self, parent = None): pitools.startup(self.pidevice, stages=self.pi_stages, refmode=self.refmode) ''' pitools.startup(self.pidevice, stages=self.pi_stages) - + ''' Setting PI velocities ''' self.pidevice.VEL(self.cfg.pi_parameters['velocity']) @@ -1034,11 +1218,11 @@ def __init__(self, parent = None): self.block_till_controller_is_ready() print('M-061 Emergency referencing hack done') logger.info('M-061 Emergency referencing hack done') - + ''' Stage 5 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] self.f_stage.move_absolute({3: self.startfocus}) - #self.pidevice.MOV(5,self.startfocus/1000) + # self.pidevice.MOV(5,self.startfocus/1000) def __del__(self): try: @@ -1058,15 +1242,15 @@ def report_position(self): exceptional circumstances. ''' try: - self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() - _ , _ , self.f_pos = self.f_stage.read_position() + self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() + _, _, self.f_pos = self.f_stage.read_position() except: logger.info('Error while unpacking Galil stage position values') - + self.create_position_dict() - + self.theta_pos = positions['1'] - + self.int_x_pos = self.x_pos + self.int_x_pos_offset self.int_y_pos = self.y_pos + self.int_y_pos_offset self.int_z_pos = self.z_pos + self.int_z_pos_offset @@ -1076,7 +1260,7 @@ def report_position(self): self.create_internal_position_dict() self.sig_position.emit(self.int_position_dict) - #print(self.int_position_dict) + # print(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): ''' Galil move relative method @@ -1088,47 +1272,46 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - xyz_motion_dict.update({1:int(x_rel)}) + xyz_motion_dict.update({1: int(x_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - xyz_motion_dict.update({2:int(y_rel)}) + xyz_motion_dict.update({2: int(y_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - xyz_motion_dict.update({3:int(z_rel)}) + xyz_motion_dict.update({3: int(z_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) - + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) + if xyz_motion_dict != {}: self.xyz_stage.move_relative(xyz_motion_dict) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({1 : theta_rel}) + self.pidevice.MVR({1: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - self.f_stage.move_relative({3:int(f_rel)}) + self.f_stage.move_relative({3: int(f_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if wait_until_done == True: self.f_stage.wait_until_done('Z') self.xyz_stage.wait_until_done('XYZ') self.pitools.waitontarget(self.pidevice) - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -1142,41 +1325,41 @@ def move_absolute(self, dict, wait_until_done=False): if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - xyz_motion_dict.update({1:x_abs}) + xyz_motion_dict.update({1: x_abs}) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - xyz_motion_dict.update({2:y_abs}) - + xyz_motion_dict.update({2: y_abs}) + if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset - xyz_motion_dict.update({3:z_abs}) - + xyz_motion_dict.update({3: z_abs}) + if xyz_motion_dict != {}: self.xyz_stage.move_absolute(xyz_motion_dict) - + if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') - + if 'f_abs' in dict: f_abs = dict['f_abs'] f_abs = f_abs - self.int_f_pos_offset if self.f_min < f_abs and self.f_max > f_abs: ''' Conversion to mm and command emission''' - self.f_stage.move_absolute({3:int(f_abs)}) + self.f_stage.move_absolute({3: int(f_abs)}) else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset if self.theta_min < theta_abs and self.theta_max > theta_abs: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({1 : theta_abs}) + self.pidevice.MOV({1: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: self.pitools.waitontarget(self.pidevice) @@ -1187,13 +1370,13 @@ def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - self.move_absolute({'y_abs':self.cfg.stage_parameters['y_load_position']}) + self.move_absolute({'y_abs': self.cfg.stage_parameters['y_load_position']}) def unload_sample(self): - self.move_absolute({'y_abs':self.cfg.stage_parameters['y_unload_position']}) - + self.move_absolute({'y_abs': self.cfg.stage_parameters['y_unload_position']}) + def go_to_rotation_position(self, wait_until_done=False): - self.move_absolute({'x_abs':self.x_rot_position, 'y_abs':self.y_rot_position, 'z_abs':self.z_rot_position}) + self.move_absolute({'x_abs': self.x_rot_position, 'y_abs': self.y_rot_position, 'z_abs': self.z_rot_position}) if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') @@ -1214,32 +1397,20 @@ def execute_program(self): self.f_stage.execute_program() self.xyz_stage.execute_program() + class mesoSPIM_PI_rotz_and_Galil_xyf_Stages(mesoSPIM_Stage): ''' + Deprecated? Expects following microscope configuration: - - Sample XYF movement: Galil controller with 3 axes - Z-Movement and Rotation: PI C-884 mercury controller - It is expected that the parent class has the following signals: - sig_move_relative = pyqtSignal(dict) - sig_move_relative_and_wait_until_done = pyqtSignal(dict) - sig_move_absolute = pyqtSignal(dict) - sig_move_absolute_and_wait_until_done = pyqtSignal(dict) - sig_zero = pyqtSignal(list) - sig_unzero = pyqtSignal(list) - sig_stop_movement = pyqtSignal() - sig_mark_rotation_position = pyqtSignal() - - Also contains a QTimer that regularily sends position updates, e.g - during the execution of movements. - + Sample XYF movement: Galil controller with 3 axes + Z-Movement and Rotation: PI C-884 mercury controller ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) - #self.state = mesoSPIM_StateSingleton() + # self.state = mesoSPIM_StateSingleton() self.pos_timer = QtCore.QTimer(self) self.pos_timer.timeout.connect(self.report_position) @@ -1254,8 +1425,9 @@ def __init__(self, parent = None): self.f_encodercounts_per_um = self.cfg.xyf_galil_parameters['f_encodercounts_per_um'] ''' Setting up the Galil stages: XYZ ''' - self.xyf_stage = StageControlGalil(self.cfg.xyf_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.f_encodercounts_per_um]) + self.xyf_stage = StageControlGalil(self.cfg.xyf_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.f_encodercounts_per_um]) ''' PI-specific code ''' from pipython import GCSDevice, pitools @@ -1266,7 +1438,7 @@ def __init__(self, parent = None): self.pi = self.cfg.pi_parameters self.controllername = self.cfg.pi_parameters['controllername'] - self.pi_stages = self.cfg.pi_parameters['stages'] + self.pi_stages = list(self.cfg.pi_parameters['stages']) # ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE') self.refmode = self.cfg.pi_parameters['refmode'] # self.serialnum = ('118015439') # Wyss Geneva @@ -1281,7 +1453,7 @@ def __init__(self, parent = None): pitools.startup(self.pidevice, stages=self.pi_stages, refmode=self.refmode) ''' pitools.startup(self.pidevice, stages=self.pi_stages) - + ''' Setting PI velocities ''' self.pidevice.VEL(self.cfg.pi_parameters['velocity']) @@ -1291,7 +1463,7 @@ def __init__(self, parent = None): self.pidevice.FRF(2) print('M-406 Emergency referencing hack done') logger.info('M-406 Emergency referencing hack done') - + ''' Stage 5 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] self.xyf_stage.move_absolute({3: self.startfocus}) @@ -1313,15 +1485,15 @@ def report_position(self): exceptional circumstances. ''' try: - self.x_pos, self.y_pos, self.f_pos = self.xyf_stage.read_position() + self.x_pos, self.y_pos, self.f_pos = self.xyf_stage.read_position() except: logger.info('Error while unpacking Galil stage position values') - + self.create_position_dict() - self.z_pos = round(positions['2']*1000,2) + self.z_pos = round(positions['2'] * 1000, 2) self.theta_pos = positions['1'] - + self.int_x_pos = self.x_pos + self.int_x_pos_offset self.int_y_pos = self.y_pos + self.int_y_pos_offset self.int_z_pos = self.z_pos + self.int_z_pos_offset @@ -1331,7 +1503,7 @@ def report_position(self): self.create_internal_position_dict() self.sig_position.emit(self.int_position_dict) - #print(self.int_position_dict) + # print(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): ''' Galil move relative method @@ -1343,38 +1515,38 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - xyf_motion_dict.update({1:int(x_rel)}) + xyf_motion_dict.update({1: int(x_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - xyf_motion_dict.update({2:int(y_rel)}) + xyf_motion_dict.update({2: int(y_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - z_rel = z_rel/1000 - self.pidevice.MVR({2 : z_rel}) + z_rel = z_rel / 1000 + self.pidevice.MVR({2: z_rel}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) - + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) + if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({1 : theta_rel}) + self.pidevice.MVR({1: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - xyf_motion_dict.update({3:int(f_rel)}) + xyf_motion_dict.update({3: int(f_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) if xyf_motion_dict != {}: self.xyf_stage.move_relative(xyf_motion_dict) @@ -1383,7 +1555,6 @@ def move_relative(self, dict, wait_until_done=False): self.xyf_stage.wait_until_done('XYZ') self.pitools.waitontarget(self.pidevice) - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -1397,42 +1568,42 @@ def move_absolute(self, dict, wait_until_done=False): if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - xyf_motion_dict.update({1:x_abs}) + xyf_motion_dict.update({1: x_abs}) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - xyf_motion_dict.update({2:y_abs}) - + xyf_motion_dict.update({2: y_abs}) + if 'f_abs' in dict: f_abs = dict['f_abs'] f_abs = f_abs - self.int_f_pos_offset - xyf_motion_dict.update({3:f_abs}) - + xyf_motion_dict.update({3: f_abs}) + if xyf_motion_dict != {}: self.xyf_stage.move_absolute(xyf_motion_dict) - + if wait_until_done == True: self.xyf_stage.wait_until_done('XYZ') - + if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset if self.z_min < z_abs and self.z_max > z_abs: ''' Conversion to mm and command emission''' - z_abs= z_abs/1000 - self.pidevice.MOV({2 : z_abs}) + z_abs = z_abs / 1000 + self.pidevice.MOV({2: z_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset if self.theta_min < theta_abs and self.theta_max > theta_abs: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({1 : theta_abs}) + self.pidevice.MOV({1: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: self.xyf_stage.wait_until_done('XYZ') @@ -1443,22 +1614,22 @@ def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - self.xyf_stage.move_absolute({2:self.cfg.stage_parameters['y_load_position']}) - + self.xyf_stage.move_absolute({2: self.cfg.stage_parameters['y_load_position']}) + def unload_sample(self): - self.xyf_stage.move_absolute({2:self.cfg.stage_parameters['y_unload_position']}) - + self.xyf_stage.move_absolute({2: self.cfg.stage_parameters['y_unload_position']}) + def go_to_rotation_position(self, wait_until_done=False): ''' This has to be done in absolute coordinates of the stages to avoid problems with the internal position offset (when the stage is zeroed). ''' - xy_motion_dict = {1:self.x_rot_position, 2: self.y_rot_position} + xy_motion_dict = {1: self.x_rot_position, 2: self.y_rot_position} self.xyf_stage.move_absolute(xy_motion_dict) - self.pidevice.MOV({2 : self.z_rot_position/1000}) - + self.pidevice.MOV({2: self.z_rot_position / 1000}) + if wait_until_done == True: self.xyf_stage.wait_until_done('XYZ') self.pitools.waitontarget(self.pidevice) - + def block_till_controller_is_ready(self): ''' Blocks further execution (especially during referencing moves) @@ -1475,9 +1646,12 @@ def execute_program(self): '''Executes program stored on the Galil controller''' self.xyf_stage.execute_program() + ### Up for deletion --> also in mesoSPIM serial class mesoSPIM_PI_rot_and_Galil_xyzf_Stages(mesoSPIM_Stage): ''' + Deprecated? + Expects following microscope configuration: Sample XYZ movement: Galil controller with 3 axes @@ -1499,10 +1673,10 @@ class mesoSPIM_PI_rot_and_Galil_xyzf_Stages(mesoSPIM_Stage): ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) - #self.state = mesoSPIM_StateSingleton() + # self.state = mesoSPIM_StateSingleton() self.pos_timer = QtCore.QTimer(self) self.pos_timer.timeout.connect(self.report_position) @@ -1518,19 +1692,21 @@ def __init__(self, parent = None): self.f_encodercounts_per_um = self.cfg.f_galil_parameters['f_encodercounts_per_um'] ''' Setting up the Galil stages: XYZ ''' - self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.z_encodercounts_per_um]) + self.xyz_stage = StageControlGalil(self.cfg.xyz_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.z_encodercounts_per_um]) ''' Setting up the Galil stages: F with two dummy axes.''' - self.f_stage = StageControlGalil(self.cfg.f_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um,self.f_encodercounts_per_um]) + self.f_stage = StageControlGalil(self.cfg.f_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um, + self.f_encodercounts_per_um]) ''' self.f_stage = StageControlGalil(COMport = self.cfg.f_galil_parameters['COMport'], x_encodercounts_per_um = 0, y_encodercounts_per_um = 0, z_encodercounts_per_um = self.f_encodercounts_per_um) ''' - + ''' print('Galil: ', self.xyz_stage.read_position('x')) print('Galil: ', self.xyz_stage.read_position('y')) @@ -1546,11 +1722,10 @@ def __init__(self, parent = None): self.pi = self.cfg.pi_parameters self.controllername = self.cfg.pi_parameters['controllername'] - self.pi_stages = self.cfg.pi_parameters['stages'] + self.pi_stages = list(self.cfg.pi_parameters['stages']) # ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE') self.refmode = self.cfg.pi_parameters['refmode'] - # self.serialnum = ('118015439') # Wyss Geneva - self.serialnum = self.cfg.pi_parameters['serialnum'] # UZH Irchel H45 + self.serialnum = self.cfg.pi_parameters['serialnum'] self.pidevice = GCSDevice(self.controllername) self.pidevice.ConnectUSB(serialnum=self.serialnum) @@ -1561,7 +1736,7 @@ def __init__(self, parent = None): pitools.startup(self.pidevice, stages=self.pi_stages, refmode=self.refmode) ''' pitools.startup(self.pidevice, stages=self.pi_stages) - + ''' Setting PI velocities ''' self.pidevice.VEL(self.cfg.pi_parameters['velocity']) @@ -1571,11 +1746,11 @@ def __init__(self, parent = None): self.block_till_controller_is_ready() print('M-061 Emergency referencing hack done') logger.info('M-061 Emergency referencing hack done') - + ''' Stage 5 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] self.f_stage.move_absolute({3: self.startfocus}) - #self.pidevice.MOV(5,self.startfocus/1000) + # self.pidevice.MOV(5,self.startfocus/1000) def __del__(self): try: @@ -1595,15 +1770,15 @@ def report_position(self): exceptional circumstances. ''' try: - self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() - _ , _ , self.f_pos = self.f_stage.read_position() + self.x_pos, self.y_pos, self.z_pos = self.xyz_stage.read_position() + _, _, self.f_pos = self.f_stage.read_position() except: logger.info('Error while unpacking Galil stage position values') - + self.create_position_dict() - + self.theta_pos = positions['1'] - + self.int_x_pos = self.x_pos + self.int_x_pos_offset self.int_y_pos = self.y_pos + self.int_y_pos_offset self.int_z_pos = self.z_pos + self.int_z_pos_offset @@ -1613,7 +1788,7 @@ def report_position(self): self.create_internal_position_dict() self.sig_position.emit(self.int_position_dict) - #print(self.int_position_dict) + # print(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): ''' Galil move relative method @@ -1625,47 +1800,46 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - xyz_motion_dict.update({1:int(x_rel)}) + xyz_motion_dict.update({1: int(x_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - xyz_motion_dict.update({2:int(y_rel)}) + xyz_motion_dict.update({2: int(y_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - xyz_motion_dict.update({3:int(z_rel)}) + xyz_motion_dict.update({3: int(z_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) - + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) + if xyz_motion_dict != {}: self.xyz_stage.move_relative(xyz_motion_dict) if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({1 : theta_rel}) + self.pidevice.MVR({1: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - self.f_stage.move_relative({3:int(f_rel)}) + self.f_stage.move_relative({3: int(f_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if wait_until_done == True: self.f_stage.wait_until_done('Z') self.xyz_stage.wait_until_done('XYZ') self.pitools.waitontarget(self.pidevice) - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -1679,41 +1853,41 @@ def move_absolute(self, dict, wait_until_done=False): if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - xyz_motion_dict.update({1:x_abs}) + xyz_motion_dict.update({1: x_abs}) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - xyz_motion_dict.update({2:y_abs}) - + xyz_motion_dict.update({2: y_abs}) + if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset - xyz_motion_dict.update({3:z_abs}) - + xyz_motion_dict.update({3: z_abs}) + if xyz_motion_dict != {}: self.xyz_stage.move_absolute(xyz_motion_dict) - + if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') - + if 'f_abs' in dict: f_abs = dict['f_abs'] f_abs = f_abs - self.int_f_pos_offset if self.f_min < f_abs and self.f_max > f_abs: ''' Conversion to mm and command emission''' - self.f_stage.move_absolute({3:int(f_abs)}) + self.f_stage.move_absolute({3: int(f_abs)}) else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset if self.theta_min < theta_abs and self.theta_max > theta_abs: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({1 : theta_abs}) + self.pidevice.MOV({1: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: self.pitools.waitontarget(self.pidevice) @@ -1724,13 +1898,13 @@ def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - self.move_absolute({'y_abs':self.cfg.stage_parameters['y_load_position']}) + self.move_absolute({'y_abs': self.cfg.stage_parameters['y_load_position']}) def unload_sample(self): - self.move_absolute({'y_abs':self.cfg.stage_parameters['y_unload_position']}) - + self.move_absolute({'y_abs': self.cfg.stage_parameters['y_unload_position']}) + def go_to_rotation_position(self, wait_until_done=False): - self.move_absolute({'x_abs':self.x_rot_position, 'y_abs':self.y_rot_position, 'z_abs':self.z_rot_position}) + self.move_absolute({'x_abs': self.x_rot_position, 'y_abs': self.y_rot_position, 'z_abs': self.z_rot_position}) if wait_until_done == True: self.xyz_stage.wait_until_done('XYZ') @@ -1751,29 +1925,17 @@ def execute_program(self): self.f_stage.execute_program() self.xyz_stage.execute_program() + class mesoSPIM_PI_rotzf_and_Galil_xy_Stages(mesoSPIM_Stage): ''' + Deprecated? Expects following microscope configuration: Sample XY movement: Galil controller with 2 axes Z-Movement, F-Movement and Rotation: PI C-884 mercury controller - - It is expected that the parent class has the following signals: - sig_move_relative = pyqtSignal(dict) - sig_move_relative_and_wait_until_done = pyqtSignal(dict) - sig_move_absolute = pyqtSignal(dict) - sig_move_absolute_and_wait_until_done = pyqtSignal(dict) - sig_zero = pyqtSignal(list) - sig_unzero = pyqtSignal(list) - sig_stop_movement = pyqtSignal() - sig_mark_rotation_position = pyqtSignal() - - Also contains a QTimer that regularily sends position updates, e.g - during the execution of movements. - ''' - def __init__(self, parent = None): + def __init__(self, parent=None): super().__init__(parent) self.pos_timer = QtCore.QTimer(self) @@ -1788,8 +1950,8 @@ def __init__(self, parent = None): self.y_encodercounts_per_um = self.cfg.xy_galil_parameters['y_encodercounts_per_um'] ''' Setting up the Galil stages: XYZ ''' - self.xy_stage = StageControlGalil(self.cfg.xy_galil_parameters['port'],[self.x_encodercounts_per_um, - self.y_encodercounts_per_um]) + self.xy_stage = StageControlGalil(self.cfg.xy_galil_parameters['port'], [self.x_encodercounts_per_um, + self.y_encodercounts_per_um]) ''' PI-specific code ''' from pipython import GCSDevice, pitools @@ -1800,11 +1962,10 @@ def __init__(self, parent = None): self.pi = self.cfg.pi_parameters self.controllername = self.cfg.pi_parameters['controllername'] - self.pi_stages = self.cfg.pi_parameters['stages'] + self.pi_stages = list(self.cfg.pi_parameters['stages']) # ('M-112K033','L-406.40DG10','M-112K033','M-116.DG','M-406.4PD','NOSTAGE') self.refmode = self.cfg.pi_parameters['refmode'] - # self.serialnum = ('118015439') # Wyss Geneva - self.serialnum = self.cfg.pi_parameters['serialnum'] # UZH Irchel H45 + self.serialnum = self.cfg.pi_parameters['serialnum'] self.pidevice = GCSDevice(self.controllername) self.pidevice.ConnectUSB(serialnum=self.serialnum) @@ -1818,7 +1979,7 @@ def __init__(self, parent = None): ''' Setting PI velocities ''' self.pidevice.VEL(self.cfg.pi_parameters['velocity']) - + print('M-406 Emergency referencing hack: Waiting for referencing move') logger.info('M-406 Emergency referencing hack: Waiting for referencing move') self.pidevice.FRF(2) @@ -1835,7 +1996,7 @@ def __init__(self, parent = None): ''' Stage 3 close to good focus''' self.startfocus = self.cfg.stage_parameters['startfocus'] - self.pidevice.MOV(3,self.startfocus/1000) + self.pidevice.MOV(3, self.startfocus / 1000) def __del__(self): try: @@ -1854,17 +2015,16 @@ def report_position(self): exceptional circumstances. ''' try: - self.x_pos, self.y_pos = self.xy_stage.read_position() + self.x_pos, self.y_pos = self.xy_stage.read_position() except: logger.info('Error while unpacking Galil stage position values') - - - self.f_pos = round(positions['3']*1000,2) - self.z_pos = round(positions['2']*1000,2) + + self.f_pos = round(positions['3'] * 1000, 2) + self.z_pos = round(positions['2'] * 1000, 2) self.theta_pos = positions['1'] self.create_position_dict() - + self.int_x_pos = self.x_pos + self.int_x_pos_offset self.int_y_pos = self.y_pos + self.int_y_pos_offset self.int_z_pos = self.z_pos + self.int_z_pos_offset @@ -1874,7 +2034,7 @@ def report_position(self): self.create_internal_position_dict() self.sig_position.emit(self.int_position_dict) - #print(self.int_position_dict) + # print(self.int_position_dict) def move_relative(self, dict, wait_until_done=False): ''' Galil move relative method @@ -1886,39 +2046,39 @@ def move_relative(self, dict, wait_until_done=False): if 'x_rel' in dict: x_rel = dict['x_rel'] if self.x_min < self.x_pos + x_rel and self.x_max > self.x_pos + x_rel: - xy_motion_dict.update({1:int(x_rel)}) + xy_motion_dict.update({1: int(x_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: X Motion limit would be reached!', 1000) if 'y_rel' in dict: y_rel = dict['y_rel'] if self.y_min < self.y_pos + y_rel and self.y_max > self.y_pos + y_rel: - xy_motion_dict.update({2:int(y_rel)}) + xy_motion_dict.update({2: int(y_rel)}) else: - self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: Y Motion limit would be reached!', 1000) if 'z_rel' in dict: z_rel = dict['z_rel'] if self.z_min < self.z_pos + z_rel and self.z_max > self.z_pos + z_rel: - z_rel = z_rel/1000 - self.pidevice.MVR({2 : z_rel}) + z_rel = z_rel / 1000 + self.pidevice.MVR({2: z_rel}) else: - self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!',1000) - + self.sig_status_message.emit('Relative movement stopped: z Motion limit would be reached!', 1000) + if 'theta_rel' in dict: theta_rel = dict['theta_rel'] if self.theta_min < self.theta_pos + theta_rel and self.theta_max > self.theta_pos + theta_rel: - self.pidevice.MVR({1 : theta_rel}) + self.pidevice.MVR({1: theta_rel}) else: - self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: theta Motion limit would be reached!', 1000) if 'f_rel' in dict: f_rel = dict['f_rel'] if self.f_min < self.f_pos + f_rel and self.f_max > self.f_pos + f_rel: - f_rel = f_rel/1000 - self.pidevice.MVR({3 : f_rel}) + f_rel = f_rel / 1000 + self.pidevice.MVR({3: f_rel}) else: - self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!',1000) + self.sig_status_message.emit('Relative movement stopped: f Motion limit would be reached!', 1000) if xy_motion_dict != {}: self.xy_stage.move_relative(xy_motion_dict) @@ -1927,7 +2087,6 @@ def move_relative(self, dict, wait_until_done=False): self.xy_stage.wait_until_done('XY') self.pitools.waitontarget(self.pidevice) - def move_absolute(self, dict, wait_until_done=False): ''' Galil move absolute method @@ -1937,20 +2096,20 @@ def move_absolute(self, dict, wait_until_done=False): ''' xy_motion_dict = {} - if 'x_abs' or 'y_abs'in dict: + if 'x_abs' or 'y_abs' in dict: if 'x_abs' in dict: x_abs = dict['x_abs'] x_abs = x_abs - self.int_x_pos_offset - xy_motion_dict.update({1:x_abs}) + xy_motion_dict.update({1: x_abs}) if 'y_abs' in dict: y_abs = dict['y_abs'] y_abs = y_abs - self.int_y_pos_offset - xy_motion_dict.update({2:y_abs}) - + xy_motion_dict.update({2: y_abs}) + if xy_motion_dict != {}: self.xy_stage.move_absolute(xy_motion_dict) - + if wait_until_done == True: self.xy_stage.wait_until_done('XYZ') @@ -1959,29 +2118,29 @@ def move_absolute(self, dict, wait_until_done=False): f_abs = f_abs - self.int_f_pos_offset if self.f_min < f_abs and self.f_max > f_abs: ''' Conversion to mm and command emission''' - f_abs= f_abs/1000 - self.pidevice.MOV({3 : f_abs}) + f_abs = f_abs / 1000 + self.pidevice.MOV({3: f_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!',1000) - + self.sig_status_message.emit('Absolute movement stopped: F Motion limit would be reached!', 1000) + if 'z_abs' in dict: z_abs = dict['z_abs'] z_abs = z_abs - self.int_z_pos_offset if self.z_min < z_abs and self.z_max > z_abs: ''' Conversion to mm and command emission''' - z_abs= z_abs/1000 - self.pidevice.MOV({2 : z_abs}) + z_abs = z_abs / 1000 + self.pidevice.MOV({2: z_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Z Motion limit would be reached!', 1000) if 'theta_abs' in dict: theta_abs = dict['theta_abs'] theta_abs = theta_abs - self.int_theta_pos_offset if self.theta_min < theta_abs and self.theta_max > theta_abs: ''' No Conversion to mm !!!! and command emission''' - self.pidevice.MOV({1 : theta_abs}) + self.pidevice.MOV({1: theta_abs}) else: - self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!',1000) + self.sig_status_message.emit('Absolute movement stopped: Theta Motion limit would be reached!', 1000) if wait_until_done == True: self.xy_stage.wait_until_done('XY') @@ -1992,22 +2151,22 @@ def stop(self): self.pidevice.STP(noraise=True) def load_sample(self): - self.xy_stage.move_absolute({2:self.cfg.stage_parameters['y_load_position']}) - + self.xy_stage.move_absolute({2: self.cfg.stage_parameters['y_load_position']}) + def unload_sample(self): - self.xy_stage.move_absolute({2:self.cfg.stage_parameters['y_unload_position']}) - + self.xy_stage.move_absolute({2: self.cfg.stage_parameters['y_unload_position']}) + def go_to_rotation_position(self, wait_until_done=False): ''' This has to be done in absolute coordinates of the stages to avoid problems with the internal position offset (when the stage is zeroed). ''' - xy_motion_dict = {1:self.x_rot_position, 2: self.y_rot_position} + xy_motion_dict = {1: self.x_rot_position, 2: self.y_rot_position} self.xy_stage.move_absolute(xy_motion_dict) - self.pidevice.MOV({2 : self.z_rot_position/1000}) - + self.pidevice.MOV({2: self.z_rot_position / 1000}) + if wait_until_done == True: self.xy_stage.wait_until_done('XY') self.pitools.waitontarget(self.pidevice) - + def block_till_controller_is_ready(self): ''' Blocks further execution (especially during referencing moves) @@ -2022,4 +2181,4 @@ def block_till_controller_is_ready(self): def execute_program(self): '''Executes program stored on the Galil controller''' - self.xy_stage.execute_program() \ No newline at end of file + self.xy_stage.execute_program() diff --git a/mesoSPIM/src/utils/acquisition_builder.py b/mesoSPIM/src/utils/acquisition_builder.py deleted file mode 100644 index 10652cd..0000000 --- a/mesoSPIM/src/utils/acquisition_builder.py +++ /dev/null @@ -1,95 +0,0 @@ -''' Classes that define acquisition builders: - -Take a dict with information and return an acquisition list -''' -from .acquisitions import Acquisition, AcquisitionList - -class AcquisitionListBuilder(): - ''' - Generic Acquisition List Builder as parent class? - - TODO: Write this class - ''' - pass - -class TilingAcquisitionListBuilder(): - ''' - TODO: Filename generation - - self.dict['x_start'] - self.dict['x_end'] - self.dict['y_start'] - self.dict['y_end'] - self.dict['z_start'] - self.dict['z_end'] - self.dict['z_step'] - self.dict['theta_pos'] - self.dict['f_pos'] - self.dict['x_offset'] # Offset always larger than 0 - self.dict['y_offset'] # Offset always larger than 0 - self.dict['x_image_count'] - self.dict['y_image_count'] - ''' - - def __init__(self, dict): - self.acq_prelist = [] - - self.dict = dict - - self.x_start = self.dict['x_start'] - self.y_start = self.dict['y_start'] - self.x_end = self.dict['x_end'] - self.y_end = self.dict['y_end'] - - ''' - Reverse direction of the offset if pos_end < pos_start - ''' - if self.x_start < self.x_end: - self.x_offset = self.dict['x_offset'] - else: - self.x_offset = -self.dict['x_offset'] - - if self.y_start < self.y_end: - self.y_offset = self.dict['y_offset'] - else: - self.y_offset = -self.dict['y_offset'] - - ''' - Core loop: Create an acquisition list for all x & y values - ''' - tilecount = 0 - for i in range(0,self.dict['x_image_count']): - self.x_pos = round(self.x_start + i * self.x_offset,2) - for j in range(0,self.dict['y_image_count']): - self.y_pos = round(self.y_start + j * self.y_offset,2) - - - acq = Acquisition( x_pos=self.x_pos, - y_pos=self.y_pos, - z_start=self.dict['z_start'], - z_end=self.dict['z_end'], - z_step=self.dict['z_step'], - theta_pos=self.dict['theta_pos'], - f_start=round(self.dict['f_start'],2), - f_end=round(self.dict['f_end'],2), - laser=self.dict['laser'], - intensity=self.dict['intensity'], - filter=self.dict['filter'], - zoom=self.dict['zoom'], - shutterconfig=self.dict['shutterconfig'], - folder=self.dict['folder'], - filename='tiling_file_'+str(tilecount)+'.raw', - etl_l_offset=self.dict['etl_l_offset'], - etl_l_amplitude=self.dict['etl_l_amplitude'], - etl_r_offset=self.dict['etl_r_offset'], - etl_r_amplitude=self.dict['etl_r_amplitude'], - ) - ''' Update number of planes as this is not done by the acquisition - object itself ''' - acq['planes']=acq.get_image_count() - - self.acq_prelist.append(acq) - tilecount += 1 - - def get_acquisition_list(self): - return AcquisitionList(self.acq_prelist) diff --git a/mesoSPIM/src/utils/acquisition_wizards.py b/mesoSPIM/src/utils/acquisition_wizards.py deleted file mode 100644 index d4c3da4..0000000 --- a/mesoSPIM/src/utils/acquisition_wizards.py +++ /dev/null @@ -1,538 +0,0 @@ -''' -Contains Acquisition Wizard Classes: - -Widgets that take user input and create acquisition lists - -''' -import numpy as np -import pprint - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import pyqtProperty - -# from .config import config as cfg -from .acquisition_builder import TilingAcquisitionListBuilder - -from ..mesoSPIM_State import mesoSPIM_StateSingleton - -class TilingWizard(QtWidgets.QWizard): - ''' - Wizard to run - - The parent is the Window class of the microscope - ''' - wizard_done = QtCore.pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - - ''' By an instance variable, callbacks to window signals can be handed - through ''' - self.parent = parent - self.cfg = parent.cfg - self.state = mesoSPIM_StateSingleton() - - ''' Instance variables ''' - self.x_start = 0 - self.x_end = 0 - self.y_start = 0 - self.y_end = 0 - self.z_start = 0 - self.z_end = 0 - self.z_step = 1 - self.f_start = 0 - self.f_end = 0 - self.x_offset = 0 - self.y_offset = 0 - self.zoom = '1x' - self.x_fov = 1 - self.y_fov = 1 - self.laser = '' - self.intensity = 0 - self.filter = '' - self.shutterconfig = '' - self.theta_pos = 0 - self.f_pos = 0 - self.x_image_count = 1 - self.y_image_count = 1 - self.folder = '' - self.delta_x = 0.0 - self.delta_y = 0.0 - self.etl_l_offset = 0.0 - self.etl_l_amplitude = 0.0 - self.etl_r_offset = 0.0 - self.etl_r_amplitude = 0.0 - - self.acquisition_time = 0 - - self.setWindowTitle('Tiling Wizard') - - self.addPage(TilingWelcomePage(self)) - self.addPage(ZeroingXYStagePage(self)) - self.addPage(DefineXYPositionPage(self)) - # self.addPage(DefineXYStartPositionPage(self)) - # self.addPage(DefineXYEndPositionPage(self)) - self.addPage(DefineZPositionPage(self)) - self.addPage(OtherAcquisitionParametersPage(self)) - self.addPage(DefineFolderPage(self)) - self.addPage(CheckTilingPage(self)) - self.addPage(FinishedTilingPage(self)) - - self.show() - - def done(self, r): - ''' Reimplementation of the done function - - if r == 0: canceled - if r == 1: finished properly - ''' - if r == 0: - print("Wizard was canceled") - if r == 1: - print('Wizard was closed properly') - # self.print_dict() - self.update_model(self.parent.model, self.acq_list) - ''' Update state with this new list ''' - # self.parent.update_persistent_editors() - self.wizard_done.emit() - else: - print('Wizard provided return code: ', r) - - super().done(r) - - def update_model(self, model, table): - # if self.field('appendToTable'): - # current_acq_list = self.state['acq_list'] - # new_acq_list = current_acq_list.append(table) - # model.setTable(new_acq_list) - # self.state['acq_list']=new_acq_list - # else: - model.setTable(table) - self.state['acq_list']=self.acq_list - - def update_image_counts(self): - ''' - TODO: This needs some FOV information - ''' - self.delta_x = abs(self.x_end - self.x_start) - self.delta_y = abs(self.y_end - self.y_start) - - ''' Using the ceiling function to always create at least 1 image ''' - self.x_image_count = int(np.ceil(self.delta_x/self.x_offset)) - self.y_image_count = int(np.ceil(self.delta_y/self.y_offset)) - - ''' The first FOV is centered on the starting location - - therefore, add another image count to fully contain the end position - if necessary - ''' - if self.delta_x % self.x_offset > self.x_offset/2: - self.x_image_count = self.x_image_count + 1 - - if self.delta_y % self.y_offset > self.y_offset/2: - self.y_image_count = self.y_image_count + 1 - - - def update_fov(self): - - - pass - # zoom = self.zoom - # index = self.parent.cfg.zoom_options.index(zoom) - # self.x_fov = self.parent.cfg.zoom_options[index] - # self.y_fov = self.parent.cfg.zoom_options[index] - - def get_dict(self): - return {'x_start' : self.x_start, - 'x_end' : self.x_end, - 'y_start' : self.y_start, - 'y_end' : self.y_end, - 'z_start' : self.z_start, - 'z_end' : self.z_end, - 'z_step' : self.z_step, - 'theta_pos' : self.theta_pos, - 'f_start' : self.f_start, - 'f_end' : self.f_end, - 'x_offset' : self.x_offset, - 'y_offset' : self.y_offset, - 'x_fov' : self.x_fov, - 'y_fov' : self.y_fov, - 'x_image_count' : self.x_image_count, - 'y_image_count' : self.y_image_count, - 'zoom' : self.zoom, - 'laser' : self.laser, - 'intensity' : self.intensity, - 'filter' : self.filter, - 'shutterconfig' : self.shutterconfig, - 'folder' : self.folder, - 'etl_l_offset' : self.etl_l_offset, - 'etl_l_amplitude' : self.etl_l_amplitude, - 'etl_r_offset' : self.etl_r_offset, - 'etl_r_amplitude' : self.etl_r_amplitude, - } - - def update_acquisition_list(self): - self.update_image_counts() - self.update_fov() - - ''' If the ETL amplitude is set, update the acq list accordingly''' - if self.field('ETLCheckBox'): - self.etl_l_offset = self.state['etl_l_offset'] - self.etl_l_amplitude = self.state['etl_l_amplitude'] - self.etl_r_offset = self.state['etl_r_offset'] - self.etl_r_amplitude = self.state['etl_r_amplitude'] - - ''' Use the current rotation angle ''' - self.theta_pos = self.state['position']['theta_pos'] - - dict = self.get_dict() - self.acq_list = TilingAcquisitionListBuilder(dict).get_acquisition_list() - self.acquisition_time = self.acq_list.get_acquisition_time() - - # pprint.pprint(self.acq_list) - - def print_dict(self): - pprint.pprint(self.get_dict()) - - -class TilingWelcomePage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - - self.setTitle("Welcome to the tiling wizard") - self.setSubTitle("This wizard will guide you through the steps of creating a tiling acquisition.") - -class ZeroingXYStagePage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Zero stage positions") - self.setSubTitle("To aid in relative positioning, it is recommended to zero the XY stages.") - - # self.button = QtWidgets.QPushButton(self) - # self.button.setText('Zero XY stages') - # self.button.setCheckable(True) - - # self.registerField('stages_zeroed*', - # self.button, - # ) - - # try: - # ''' - # Pretty dirty approach, reaching up through the hierarchy: - - # The first level parent is the QWizard - # The second level parent is the Window - which can send zeroing signals - # The third level is the mesoSPIM MainWindow - # ''' - # self.button.toggled.connect(lambda: self.parent.parent.parent.sig_zero_axes.emit(['x','y'])) - # except: - # print('Zeroing connection failed') - -class DefineXYPositionPage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Define the corners of the tiling acquisition") - self.setSubTitle("Move XY stages to the starting corner position") - - self.button0 = QtWidgets.QPushButton(self) - self.button0.setText('Set XY Start Corner') - self.button0.setCheckable(True) - self.button0.toggled.connect(self.get_xy_start_position) - - self.button1 = QtWidgets.QPushButton(self) - self.button1.setText('Set XY End Corner') - self.button1.setCheckable(True) - self.button1.toggled.connect(self.get_xy_end_position) - - self.registerField('xy_start_position*', - self.button0, - ) - self.registerField('xy_end_position*', - self.button1, - ) - - self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.button0, 0, 0) - self.layout.addWidget(self.button1, 1, 1) - self.setLayout(self.layout) - - def get_xy_start_position(self): - self.parent.x_start = self.parent.state['position']['x_pos'] - self.parent.y_start = self.parent.state['position']['y_pos'] - - def get_xy_end_position(self): - self.parent.x_end = self.parent.state['position']['x_pos'] - self.parent.y_end = self.parent.state['position']['y_pos'] - -class DefineZPositionPage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Define start & end Z position") - self.setSubTitle("Move Z stages to the start & end position") - - self.ZStartButton = QtWidgets.QPushButton(self) - self.ZStartButton.setText('Set Z start') - self.ZStartButton.setCheckable(True) - self.ZStartButton.toggled.connect(self.update_z_start_position) - - self.ZEndButton = QtWidgets.QPushButton(self) - self.ZEndButton.setText('Set Z end') - self.ZEndButton.setCheckable(True) - self.ZEndButton.toggled.connect(self.update_z_end_position) - - self.ZSpinBoxLabel = QtWidgets.QLabel('Z stepsize') - - self.ZStepSpinBox = QtWidgets.QSpinBox(self) - self.ZStepSpinBox.setValue(1) - self.ZStepSpinBox.setMinimum(1) - self.ZStepSpinBox.setMaximum(1000) - self.ZStepSpinBox.valueChanged.connect(self.update_z_step) - - self.StartFocusButton = QtWidgets.QPushButton(self) - self.StartFocusButton.setText('Set start focus') - self.StartFocusButton.setCheckable(True) - self.StartFocusButton.toggled.connect(self.update_start_focus_position) - - self.EndFocusButton = QtWidgets.QPushButton(self) - self.EndFocusButton.setText('Set end focus') - self.EndFocusButton.setCheckable(True) - self.EndFocusButton.toggled.connect(self.update_end_focus_position) - - self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.ZStartButton, 0, 0) - self.layout.addWidget(self.ZEndButton, 0, 1) - self.layout.addWidget(self.ZSpinBoxLabel, 2, 0) - self.layout.addWidget(self.ZStepSpinBox, 2, 1) - self.layout.addWidget(self.StartFocusButton, 3, 0) - self.layout.addWidget(self.EndFocusButton, 4, 0) - self.setLayout(self.layout) - - self.registerField('z_start_position*', - self.ZStartButton, - ) - - self.registerField('z_end_position*', - self.ZEndButton, - ) - - self.registerField('start_focus_position*', - self.StartFocusButton, - ) - - self.registerField('end_focus_position*', - self.EndFocusButton, - ) - - def update_z_start_position(self): - self.parent.z_start = self.parent.state['position']['z_pos'] - - def update_z_end_position(self): - self.parent.z_end = self.parent.state['position']['z_pos'] - - def update_z_step(self): - self.parent.z_step = self.ZStepSpinBox.value() - - def update_start_focus_position(self): - self.parent.f_start = self.parent.state['position']['f_pos'] - - def update_end_focus_position(self): - self.parent.f_end = self.parent.state['position']['f_pos'] - -class OtherAcquisitionParametersPage(QtWidgets.QWizardPage): - ''' - - TODO: Needs a button: Take current parameters from Live or so - ''' - - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Define other parameters") - - self.zoomLabel = QtWidgets.QLabel('Zoom') - self.zoomComboBox = QtWidgets.QComboBox(self) - self.zoomComboBox.addItems(self.parent.cfg.zoomdict.keys()) - - self.laserLabel = QtWidgets.QLabel('Laser') - self.laserComboBox = QtWidgets.QComboBox(self) - self.laserComboBox.addItems(self.parent.cfg.laserdict.keys()) - - self.intensityLabel = QtWidgets.QLabel('Intensity') - self.intensitySlider = QtWidgets.QSlider(QtCore.Qt.Horizontal) - self.intensitySlider.setMinimum(0) - self.intensitySlider.setMaximum(100) - - self.filterLabel = QtWidgets.QLabel('Filter') - self.filterComboBox = QtWidgets.QComboBox(self) - self.filterComboBox.addItems(self.parent.cfg.filterdict.keys()) - - self.shutterLabel = QtWidgets.QLabel('Shutter') - self.shutterComboBox = QtWidgets.QComboBox(self) - self.shutterComboBox.addItems(self.parent.cfg.shutteroptions) - - self.xOffsetSpinBoxLabel = QtWidgets.QLabel('X Offset') - self.xOffsetSpinBox = QtWidgets.QSpinBox(self) - self.xOffsetSpinBox.setSuffix(' μm') - self.xOffsetSpinBox.setMinimum(1) - self.xOffsetSpinBox.setMaximum(20000) - self.xOffsetSpinBox.setValue(500) - - self.yOffsetSpinBoxLabel = QtWidgets.QLabel('Y Offset') - self.yOffsetSpinBox = QtWidgets.QSpinBox(self) - self.yOffsetSpinBox.setSuffix(' μm') - self.yOffsetSpinBox.setMinimum(1) - self.yOffsetSpinBox.setMaximum(20000) - self.yOffsetSpinBox.setValue(500) - - self.ETLCheckBoxLabel = QtWidgets.QLabel('ETL') - self.ETLCheckBox = QtWidgets.QCheckBox('Copy current ETL parameters', self) - self.ETLCheckBox.setChecked(True) - - self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.zoomLabel, 0, 0) - self.layout.addWidget(self.zoomComboBox, 0, 1) - self.layout.addWidget(self.laserLabel, 1, 0) - self.layout.addWidget(self.laserComboBox, 1, 1) - self.layout.addWidget(self.intensityLabel, 2, 0) - self.layout.addWidget(self.intensitySlider, 2, 1) - self.layout.addWidget(self.filterLabel, 3, 0) - self.layout.addWidget(self.filterComboBox, 3, 1) - self.layout.addWidget(self.shutterLabel, 4, 0) - self.layout.addWidget(self.shutterComboBox, 4, 1) - self.layout.addWidget(self.xOffsetSpinBoxLabel, 5, 0) - self.layout.addWidget(self.xOffsetSpinBox, 5, 1) - self.layout.addWidget(self.yOffsetSpinBoxLabel, 6, 0) - self.layout.addWidget(self.yOffsetSpinBox, 6, 1) - self.layout.addWidget(self.ETLCheckBoxLabel, 7, 0) - self.layout.addWidget(self.ETLCheckBox, 7, 1) - - self.registerField('ETLCheckBox', self.ETLCheckBox) - - self.setLayout(self.layout) - - self.update_page_from_state() - - def validatePage(self): - ''' The done function should update all the parent parameters ''' - self.update_other_acquisition_parameters() - return True - - def update_other_acquisition_parameters(self): - ''' Here, all the Tiling parameters are filled in the parent (TilingWizard) - - This method should be called when the "Next" Button is pressed - ''' - self.parent.zoom = self.zoomComboBox.currentText() - # self.parent.x_fov = self.parent.cfg.fov_options[self.zoomComboBox.currentIndex()] - # self.parent.y_fov = self.parent.cfg.fov_options[self.zoomComboBox.currentIndex()] - self.parent.x_offset = self.xOffsetSpinBox.value() - self.parent.y_offset = self.yOffsetSpinBox.value() - self.parent.laser = self.laserComboBox.currentText() - self.parent.intensity = self.intensitySlider.value() - self.parent.filter = self.filterComboBox.currentText() - self.parent.shutterconfig = self.shutterComboBox.currentText() - - def initializePage(self): - self.update_page_from_state() - - def update_page_from_state(self): - self.zoomComboBox.setCurrentText(self.parent.state['zoom']) - self.laserComboBox.setCurrentText(self.parent.state['laser']) - self.intensitySlider.setValue(self.parent.state['intensity']) - self.filterComboBox.setCurrentText(self.parent.state['filter']) - self.shutterComboBox.setCurrentText(self.parent.state['shutterconfig']) - -class DefineFolderPage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Select folder") - self.setSubTitle("Please select the folder in which the data should be saved.") - - self.Button = QtWidgets.QPushButton('Select Folder') - self.Button.setCheckable(True) - self.Button.setChecked(False) - self.Button.toggled.connect(self.choose_folder) - - self.TextEdit = QtWidgets.QLineEdit(self) - - self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.Button, 0, 0) - self.layout.addWidget(self.TextEdit, 1, 0) - self.setLayout(self.layout) - - def choose_folder(self): - ''' File dialog for choosing the save folder ''' - - path = QtWidgets.QFileDialog.getExistingDirectory(self.parent, 'Select Folder') - if path: - self.parent.folder = path - self.TextEdit.setText(path) - -class CheckTilingPage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Check Tiling Page") - self.setSubTitle("Here are your parameters") - - # self.timeLabel = QtWidgets.QLabel('Acquisition Time:') - # self.acqTime = QtWidgets.QLineEdit(self) - # self.acqTime.setReadOnly(True) - - self.xFOVLabel = QtWidgets.QLabel('X FOVs:') - self.xFOVs = QtWidgets.QLineEdit(self) - self.xFOVs.setReadOnly(True) - - self.yFOVLabel = QtWidgets.QLabel('Y FOVs:') - self.yFOVs = QtWidgets.QLineEdit(self) - self.yFOVs.setReadOnly(True) - - self.Button = QtWidgets.QPushButton('Values are ok?') - self.Button.setCheckable(True) - self.Button.setChecked(False) - - self.layout = QtWidgets.QGridLayout() - # self.layout.addWidget(self.timeLabel, 0, 0) - # self.layout.addWidget(self.acqTime, 0, 1) - self.layout.addWidget(self.xFOVLabel, 1, 0) - self.layout.addWidget(self.xFOVs, 1, 1) - self.layout.addWidget(self.yFOVLabel, 2, 0) - self.layout.addWidget(self.yFOVs, 2, 1) - self.layout.addWidget(self.Button, 3, 1) - self.setLayout(self.layout) - - self.registerField('finalCheck*',self.Button) - - def initializePage(self): - ''' Here, the acquisition list is created for further checking''' - self.parent.update_acquisition_list() - self.xFOVs.setText(str(self.parent.x_image_count)) - self.yFOVs.setText(str(self.parent.y_image_count)) - -class FinishedTilingPage(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super().__init__(parent) - self.parent = parent - - self.setTitle("Finished!") - self.setSubTitle("Attention: This will overwrite the Acquisition Table. Click 'Finished' to continue. To rename the files, use the filename wizard.") - - def validatePage(self): - print('Update parent table') - return True - - -if __name__ == '__main__': - import sys - app = QtWidgets.QApplication(sys.argv) - wizard = MyWizard() - sys.exit(app.exec_()) diff --git a/mesoSPIM/src/utils/acquisitions.py b/mesoSPIM/src/utils/acquisitions.py index fbca722..d5b0ff6 100644 --- a/mesoSPIM/src/utils/acquisitions.py +++ b/mesoSPIM/src/utils/acquisitions.py @@ -249,7 +249,7 @@ def get_capitalized_keylist(self): def get_keylist(self): ''' - Here, a list of capitalized keys is returnes for usage as a table header + Here, a list of capitalized keys is returned for usage as a table header ''' return self[0].get_keylist() @@ -258,7 +258,6 @@ def get_acquisition_time(self, framerate): Returns total time in seconds of a list of acquisitions ''' time = 0 - for i in range(len(self)): time += self[i].get_acquisition_time(framerate) @@ -324,23 +323,19 @@ def check_for_existing_filenames(self): def check_for_duplicated_filenames(self): ''' Returns a list of duplicated filenames ''' - duplicates = [] filenames = [] - - ''' Create a list of full file paths''' + # Create a list of full file paths for i in range(len(self)): - filename = self[i]['folder']+'/'+self[i]['filename'] - filenames.append(filename) - + if self[i]['filename'][-3:] != '.h5': + filename = self[i]['folder']+'/'+self[i]['filename'] + filenames.append(filename) duplicates = self.get_duplicates_in_list(filenames) return duplicates def check_for_nonexisting_folders(self): ''' Returns a list of nonexisting folders ''' - nonexisting_folders = [] - for i in range(len(self)): folder = self[i]['folder'] if not os.path.isdir(folder): @@ -348,11 +343,88 @@ def check_for_nonexisting_folders(self): return nonexisting_folders - def get_duplicates_in_list(self, list): + def get_duplicates_in_list(self, in_list): duplicates = [] - unique = set(list) + unique = set(in_list) for each in unique: - count = list.count(each) + count = in_list.count(each) if count > 1: duplicates.append(each) return duplicates + + def get_n_shutter_configs(self): + """Get the number of unique shutter configs (1 or 2)""" + sconfig_list = [a['shutterconfig'] for a in self] + sconfig_set = set(sconfig_list) + return len(sconfig_set) + + def get_n_angles(self): + """Get the number of unique angles""" + angle_list = [a['rot'] for a in self] + angle_set = set(angle_list) + return len(angle_set) + + def get_n_lasers(self): + """Get the number of unique laser lines""" + laser_list = [a['laser'] for a in self] + laser_set = set(laser_list) + return len(laser_set) + + def get_n_tiles(self): + """Get the number of tiles as unique (x,y,z_start,rot) combinations""" + tile_list = [] + for a in self: + tile_str = f"{a['x_pos']}{a['y_pos']}{a['z_start']}{a['rot']}" + if not tile_str in tile_list: + tile_list.append(tile_str) + return len(tile_list) + + def get_tile_index(self, acq): + """Get the the tile index for given acquisition""" + acq_str = f"{acq['x_pos']}{acq['y_pos']}{acq['z_start']}{acq['rot']}" + tile_list = [] + for a in self: + tile_str = f"{a['x_pos']}{a['y_pos']}{a['z_start']}{a['rot']}" + if not tile_str in tile_list: + tile_list.append(tile_str) + return tile_list.index(acq_str) + + def get_unique_attr_list(self, key: str = 'laser') -> list: + """Return ordered list of acquisition attributes. + + Parameters: + ----------- + key: str + One of ('laser', 'shutterconfig', 'rot') + + Returns: + -------- + List of strings, e.g. ('488', '561') for key='laser', in the order of acquisition. + """ + attributes = ('laser', 'shutterconfig', 'rot') + assert key in attributes, f'Key {key} must be one of {attributes}.' + unique_list = [] + for acq in self: + if acq[key] not in unique_list: + unique_list.append(acq[key]) + return unique_list + + def find_value_index(self, value: str = '488 nm', key: str = 'laser'): + """Find the attribute index in the acquisition list. + Example: + al = AcquisitionList([Acquisition(), Acquisition(), Acquisition(), Acquisition()]) + al[0]['laser'] = '488 nm' # + al[1]['laser'] = '488 nm' # gets removed because non-unique + al[2]['laser'] = '561 nm' # + al[3]['laser'] = '637 nm' # + Output: + al.find_value_index('488 nm', 'laser') # -> 0 + al.find_value_index('561 nm', 'laser') # -> 1 + al.find_value_index('637 nm', 'laser') # -> 2 + """ + unique_list = self.get_unique_attr_list(key) + assert value in unique_list, f"Value({value}) not found in list {unique_list}" + return unique_list.index(value) + + + diff --git a/mesoSPIM/src/utils/bigdataviewer_xml_creator.py b/mesoSPIM/src/utils/bigdataviewer_xml_creator.py deleted file mode 100644 index f94bd54..0000000 --- a/mesoSPIM/src/utils/bigdataviewer_xml_creator.py +++ /dev/null @@ -1,401 +0,0 @@ -''' -Classes to create XMLs for Bigstitcher out of mesoSPIM-Datasets. -''' -import os.path - -from ..mesoSPIM_State import mesoSPIM_StateSingleton - -from lxml import etree - -class mesoSPIM_XMLexporter: - ''' - Class to take a mesoSPIM acquisitionlist object and turn it into a Bigdataviewer/ - Bigstitcher XML file. - - TODO: - * a single stack goes into a single view setup - * everything has to be converted to string... - * have the correct image loader: tif - * 399 494 1256 - * if the coordinates match and the channels (laser or filter) are different: same tile, but different channels - * angle can be taken directly from acqlist - * every acq is a viewSetup - * acqs with matching X,Y,and Z_start, Z_end and angle positions positions are the same tile - * create a tile list? - * assign channels by order of appearance - * different lasers are definitely different channels - * same lasers: check if filters are different --> if yes then channels - * case: - * angles not supported - ''' - - def __init__(self, parent=None): - self.parent = parent - self.state = mesoSPIM_StateSingleton() - self.cfg = parent.cfg - - self.xmlwriter = mesoSPIM_BDVXMLwriter() - - self.xy_pixelsize = 1 - self.z_size = 1 - self.length_unit = 'micron' - - def generate_xml_from_acqlist(self, acqlist, path): - channeldict = self.generate_channeldict(acqlist) - tiledict = self.generate_tiledict(acqlist) - illuminationdict = self.generate_illuminationdict(acqlist) - - num_channels = len(channeldict) - num_tiles = len(tiledict) - num_illuminations = len(illuminationdict) - - if num_channels > 1: - layout_channels = 1 - else: - layout_channels = 0 - - if num_tiles > 1: - layout_tiles = 1 - else: - layout_tiles = 0 - - if num_illuminations > 1: - layout_illuminations = 1 - else: - layout_illuminations = 0 - - channellist = [c for c in range(num_channels)] - illuminationlist = [i for i in range(num_illuminations)] - tilelist = [t for t in range(num_tiles)] - anglelist = [0] - - self.xmlwriter.setLayout(filepattern='tiling_file_t{x}_c{c}.raw.tif', - timepoints=0, - channels=layout_channels, - illuminations=layout_illuminations, - angles = 0, - tiles = layout_tiles, - imglibcontainer="ArrayImgFactory") #or CellImgFactory - - id = 0 - for acq in acqlist: - channelstring = self.generate_channelstring(acq) - illuminationstring = self.generate_illuminationstring(acq) - tilestring = self.generate_tilestring(acq) - calibrationstring = self.create_calibration_string(acq) - - self.xmlwriter.addviewsetup(id=str(id), - name=str(acq['filename']), - size=self.create_size_string(acq), - vosize_unit = 'micron', - vosize = self.create_voxelsize_string(acq), - illumination = illuminationdict[illuminationstring], - channel = channeldict[channelstring], - tile = tiledict[tilestring], - angle = self.create_angle_string(acq)) - - self.xmlwriter.addCalibrationRegistration(tp='0', view=str(id), calibrationstring=calibrationstring) - id += 1 - - self.xmlwriter.addAttributes(illuminations=illuminationlist, - channels=channellist, - tiles=tilelist, - angles=anglelist) - - self.xmlwriter.addTimepoints('') - - self.xmlwriter.write(path) - - def generate_channeldict(self, acqlist): - ''' - Takes the acqlist and returns a dictionary of channels - Channels are defined as: - * different lasers in different acqs are definitely different channels - * same lasers: check if filters are different --> if yes then channels - ''' - channeldict = {} - c = 0 - - for acq in acqlist: - channelstring = self.generate_channelstring(acq) - if not channelstring in channeldict: - channeldict.update({channelstring:str(c)}) - c+=1 - - return channeldict - - - def generate_tiledict(self, acqlist): - ''' - Takes the acqlist and returns an assignment of - - Idea: Take an ACQ and create a hash/hashable datatype (e.g. string) - out of: - * X_pos - * Y_pos - * Z_start - * Z_end - * Angle - - Use this tilehash as keys for a dictionary - Later on, you can use it to assign tiles - ''' - tiledict = {} - t=0 - - for acq in acqlist: - tilestring = self.generate_tilestring(acq) - if not tilestring in tiledict: - tiledict.update({tilestring:str(t)}) - t+=1 - - return tiledict - - def generate_illuminationdict(self, acqlist): - illuminationdict = {} - i = 0 - - for acq in acqlist: - illuminationstring = self.generate_illuminationstring(acq) - if not illuminationstring in illuminationdict: - illuminationdict.update({illuminationstring: str(i)}) - i+=1 - - return illuminationdict - - def generate_channelstring(self, acq): - return str(acq['laser']) + ' ' + str(acq['filter']) - - def generate_tilestring(self, acq): - return str(acq['x_pos'])+' '+str(acq['y_pos'])+' '+str(acq['z_start'])+' '+str(acq['rot']) - - def generate_illuminationstring(self, acq): - return str(acq['shutterconfig']) - - def write(self, path): - self.xmlwriter.write(path) - - def create_size_string(self, acq): - ''' Creates the necessary XYZ #pixels string''' - - binning_string = self.cfg.camera_parameters['binning'] - x_binning = int(binning_string[0]) - y_binning = int(binning_string[2]) - - y_pixels = int(self.cfg.camera_parameters['y_pixels'] / y_binning) - x_pixels = int(self.cfg.camera_parameters['x_pixels'] / x_binning) - - z_pixels = acq['planes'] - - ''' X and Y flipped due to image rotation ''' - return str(y_pixels) + ' ' + str(x_pixels) + ' ' + str(z_pixels) - - def update_pixelsizes(self, acq): - self.xy_pixelsize = self.convert_zoom_to_pixelsize(acq['zoom']) - self.z_pixelsize = acq['z_step'] - - def create_voxelsize_string(self, acq): - ''' Assumes square pixels''' - self.update_pixelsizes(acq) - return str(self.xy_pixelsize) + ' ' + str(self.xy_pixelsize) + ' ' + str(self.z_pixelsize) - - def convert_zoom_to_pixelsize(self, zoom): - ''' Don't forget the binning!''' - return self.cfg.pixelsize[zoom] - - def create_angle_string(self, acq): - return str(int(acq['rot'])) - - def create_calibration_string(self, acq): - ''' - XY pixelsize: 15 - Z:pixelsize: 8 - 15/8 = 1.875 - - Result: - 1.875 0.0 0.0 0.0 0.0 1.875 0.0 0.0 0.0 0.0 1.0 0.0 - - if - XY pixelsize: 8 - Z:pixelsize: 15 - 15/8 = 1.875 - - Result: - 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.875 0.0 - ''' - self.update_pixelsizes(acq) - - if self.xy_pixelsize > self.z_pixelsize: - factor = self.xy_pixelsize/self.z_pixelsize - calibration_string = str(factor) + ' 0.0 0.0 0.0 0.0 ' + str(factor) + ' 0.0 0.0 0.0 0.0 1.0 0.0' - elif self.xy_pixelsize < self.z_pixelsize: - factor = self.z_pixelsize/self.xy_pixelsize - calibration_string = '1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 '+ str(factor) +' 0.0' - else: - calibration_string = '1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0' - - return calibration_string - -class mesoSPIM_BDVXMLwriter: - ''' - mesoSPIM bigdataviewer-XML-writer - - Based on the code posted by https://github.com/Xqua here: - https://github.com/bigdataviewer/bigdataviewer-core/issues/5 - ''' - - def __init__(self): - - self.xml = etree.Element('SpimData', version="0.2") - self.doc = etree.ElementTree(self.xml) - - self.BasePath = etree.SubElement(self.xml, 'BasePath', type="relative") - self.BasePath.text = "." - - self.SequenceDescription = etree.SubElement(self.xml, 'SequenceDescription') - self.ImageLoader = etree.SubElement(self.SequenceDescription, 'ImageLoader', format="spimreconstruction.stack.ij") - - self.ImageDirectory = etree.SubElement(self.ImageLoader, 'imagedirectory', type="relative") - - - - self.ViewSetups = etree.SubElement(self.SequenceDescription, 'ViewSetups') - self.ViewRegistrations = etree.SubElement(self.xml, 'ViewRegistrations') - - etree.SubElement(self.xml, "ViewInterestPoints") - etree.SubElement(self.xml, "BoundingBoxes") - etree.SubElement(self.xml, "PointSpreadFunctions") - etree.SubElement(self.xml, "StitchingResults") - etree.SubElement(self.xml, "IntensityAdjustments") - - def write(self, path): - out = str(etree.tostring(self.xml, pretty_print=True, encoding=str)) - # out = str(etree.tostring(self.xml, pretty_print=True, encoding='UTF-8')) - - - with open(path, 'w') as file: - file.write(out) - - def addFile(self, path): - image = etree.SubElement(self.ImageLoader, 'hdf5', type="relative") - image.text = path - - def addviewsetup(self, id, name, size, vosize_unit, vosize, illumination, channel, tile, angle): - V = etree.SubElement(self.ViewSetups, 'ViewSetup') - - Id = etree.SubElement(V, 'id') - Id.text = str(id) - Name = etree.SubElement(V, 'name') - Name.text = str(name) - Size = etree.SubElement(V, 'size') - Size.text = str(size) - - VoxelSize = etree.SubElement(V, 'voxelSize') - Unit = etree.SubElement(VoxelSize, 'unit') - Unit.text = str(vosize_unit) - Size = etree.SubElement(VoxelSize, 'size') - Size.text = str(vosize) - - Attributes = etree.SubElement(V, 'attributes') - Ilum = etree.SubElement(Attributes, 'illumination') - Ilum.text = str(illumination) - Chan = etree.SubElement(Attributes, 'channel') - Chan.text = str(channel) - Tile = etree.SubElement(Attributes, 'tile') - Tile.text = str(tile) - Ang = etree.SubElement(Attributes, 'angle') - Ang.text = str(angle) - - def setLayout(self, filepattern="tiling_file_{x}_c{c}.raw.tif", timepoints=0, channels=0, illuminations=0, angles=0, tiles=1,imglibcontainer="ArrayImgFactory"): - ''' - Layout entries according to: - https://scijava.org/javadoc.scijava.org/Fiji/spim/fiji/spimdata/imgloaders/LegacyStackImgLoader.html - - layoutTP - - 0 == one, 1 == one per file, 2 == all in one file - layoutChannels - - 0 == one, 1 == one per file, 2 == all in one file - layoutIllum - - 0 == one, 1 == one per file, 2 == all in one file - layoutAngles - - 0 == one, 1 == one per file, 2 == all in one file - ''' - self.FilePattern = etree.SubElement(self.ImageLoader,'filePattern') - self.FilePattern.text = filepattern - self.LayoutTimepoints = etree.SubElement(self.ImageLoader, 'layoutTimepoints') - self.LayoutTimepoints.text = str(timepoints) - self.LayoutChannels = etree.SubElement(self.ImageLoader, 'layoutChannels') - self.LayoutChannels.text = str(channels) - self.LayoutIlluminations = etree.SubElement(self.ImageLoader, 'layoutIlluminations') - self.LayoutIlluminations.text = str(illuminations) - self.LayoutAngles = etree.SubElement(self.ImageLoader, 'layoutAngles') - self.LayoutAngles.text = str(angles) - self.LayoutTiles = etree.SubElement(self.ImageLoader, 'layoutTiles') - self.LayoutTiles.text = str(tiles) - - self.Imglib2container = etree.SubElement(self.ImageLoader, 'imglib2container') - self.Imglib2container.text = imglibcontainer - - def setViewSize(self, Id, size): - trigger = False - for child in self.ViewSetups: - for el in child: - if el.tag == 'id': - if el.text == Id: - trigger = True - if el.tag == 'size' and trigger: - el.text = ' '.join(size) - trigger = False - return True - return False - - def addAttributes(self, illuminations, channels, tiles, angles): - illum = etree.SubElement(self.ViewSetups, 'Attributes', name="illumination") - chan = etree.SubElement(self.ViewSetups, 'Attributes', name="channel") - til = etree.SubElement(self.ViewSetups, 'Attributes', name="tile") - ang = etree.SubElement(self.ViewSetups, 'Attributes', name="angle") - - for illumination in illuminations: - I = etree.SubElement(illum, 'Illumination') - Id = etree.SubElement(I, 'id') - Id.text = str(illumination) - Name = etree.SubElement(I, 'name') - Name.text = str(illumination) - - for channel in channels: - I = etree.SubElement(chan, 'Channel') - Id = etree.SubElement(I, 'id') - Id.text = str(channel) - Name = etree.SubElement(I, 'name') - Name.text = str(channel) - - for tile in tiles: - I = etree.SubElement(til, 'Tile') - Id = etree.SubElement(I, 'id') - Id.text = str(tile) - Name = etree.SubElement(I, 'name') - Name.text = str(tile) - - for angle in angles: - I = etree.SubElement(ang, 'Angle') - Id = etree.SubElement(I, 'id') - Id.text = str(angle) - Name = etree.SubElement(I, 'name') - Name.text = str(angle) - - def addTimepoints(self, timepoints): - TP = etree.SubElement(self.SequenceDescription, 'Timepoints', type="pattern") - I = etree.SubElement(TP, 'integerpattern') - I.text = ', '.join(timepoints) - - def addRegistration(self, tp, view): - V = etree.SubElement(self.ViewRegistrations, 'ViewRegistration', timepoint=tp, setup=view) - VT = etree.SubElement(V, 'ViewTransform', type="affine") - name = etree.SubElement(VT, 'Name') - name.text = "calibration" - affine = etree.SubElement(VT, 'affine') - affine.text = '1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0' - - def addCalibrationRegistration(self, tp, view, calibrationstring): - V = etree.SubElement(self.ViewRegistrations, 'ViewRegistration', timepoint=str(tp), setup=str(view)) - VT = etree.SubElement(V, 'ViewTransform', type="affine") - name = etree.SubElement(VT, 'Name') - name.text = "calibration" - affine = etree.SubElement(VT, 'affine') - affine.text = calibrationstring \ No newline at end of file diff --git a/mesoSPIM/src/utils/delegates.py b/mesoSPIM/src/utils/delegates.py index 3e2daa7..2096740 100644 --- a/mesoSPIM/src/utils/delegates.py +++ b/mesoSPIM/src/utils/delegates.py @@ -158,9 +158,10 @@ def __init__(self, parent): super().__init__(parent) def createEditor(self, parent, option, index): - spinbox = QtWidgets.QSpinBox(parent) - spinbox.setMinimum(1) + spinbox = QtWidgets.QDoubleSpinBox(parent) + spinbox.setMinimum(0.1) spinbox.setMaximum(1000) + spinbox.setDecimals(1) spinbox.valueChanged.connect(lambda: self.commitData.emit(self.sender())) spinbox.setAutoFillBackground(True) return spinbox diff --git a/mesoSPIM/src/utils/demo_threads.py b/mesoSPIM/src/utils/demo_threads.py deleted file mode 100644 index 51d803d..0000000 --- a/mesoSPIM/src/utils/demo_threads.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging -logger = logging.getLogger(__name__) -import time - -from PyQt5 import QtWidgets, QtCore, QtGui - -class mesoSPIM_DemoThread(QtCore.QObject): - def __init__(self): - super().__init__() - - logger.info('Demo Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) - - @QtCore.pyqtSlot() - def report_thread_id(self): - logger.info('Demo Thread ID while running: '+str(int(QtCore.QThread.currentThreadId()))) - - diff --git a/mesoSPIM/src/utils/demo_threads1.py b/mesoSPIM/src/utils/demo_threads1.py deleted file mode 100644 index 4156bc8..0000000 --- a/mesoSPIM/src/utils/demo_threads1.py +++ /dev/null @@ -1,18 +0,0 @@ -import logging -logger = logging.getLogger(__name__) -import time - -from PyQt5 import QtWidgets, QtCore, QtGui - -class mesoSPIM_DemoThread(QtCore.QObject): - def __init__(self): - super().__init__() - - logger.info('Thread ID at Startup: '+str(int(QtCore.QThread.currentThreadId()))) - - def report_thread_id(self): - for i in range(0,101): - #time.sleep(0.01) - QtCore.QThread.msleep(10) - logger.info('Thread ID while running: '+str(int(QtCore.QThread.currentThreadId()))) - diff --git a/mesoSPIM/src/utils/filename_wizard.py b/mesoSPIM/src/utils/filename_wizard.py index 0e65c5e..a59f980 100644 --- a/mesoSPIM/src/utils/filename_wizard.py +++ b/mesoSPIM/src/utils/filename_wizard.py @@ -1,6 +1,5 @@ ''' -Contains Filename Wizard Class: autogenerates Filenames - +Contains Nonlinear Filename Wizard Class: autogenerates Filenames ''' from PyQt5 import QtWidgets, QtGui, QtCore @@ -8,19 +7,21 @@ from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import pyqtProperty -# from .config import config as cfg -# from .acquisition_builder import TilingAcquisitionListBuilder - from ..mesoSPIM_State import mesoSPIM_StateSingleton +import logging +logger = logging.getLogger(__name__) + class FilenameWizard(QtWidgets.QWizard): ''' Wizard to run - The parent is the Window class of the microscope ''' wizard_done = QtCore.pyqtSignal() + num_of_pages = 4 + (welcome, raw, single_hdf5, finished) = range(num_of_pages) + def __init__(self, parent=None): super().__init__(parent) @@ -28,12 +29,12 @@ def __init__(self, parent=None): through ''' self.parent = parent self.state = mesoSPIM_StateSingleton() - + self.file_format = None self.setWindowTitle('Filename Wizard') - - self.addPage(FilenameWizardWelcomePage(self)) - self.addPage(FilenameWizardCheckResultsPage(self)) - + self.setPage(0, FilenameWizardWelcomePage(self)) + self.setPage(1, FilenameWizardRawSelectionPage(self)) + self.setPage(2, FilenameWizardSingleHDF5SelectionPage(self)) + self.setPage(3, FilenameWizardCheckResultsPage(self)) self.show() def done(self, r): @@ -43,16 +44,12 @@ def done(self, r): if r == 1: finished properly ''' if r == 0: - print("Wizard was canceled") + logger.info('Filename Wizard was canceled') if r == 1: - print('Wizard was closed properly') - # print('Laser selected: ', self.field('Laser')) - # print('Filter selected: ', self.field('Filter')) - # print('Zoom selected: ', self.field('Zoom')) - # print('Shutter selected: ', self.field('Shutterconfig')) + logger.info('Filename Wizard was closed properly') self.update_filenames_in_model() else: - print('Wizard provided return code: ', r) + logger.info('Filename Wizard provided return code: ', r) super().done(r) @@ -62,72 +59,74 @@ def replace_spaces_with_underscores(self, string): def replace_dots_with_underscores(self, string): return string.replace('.','_') - def generate_filename_list(self): + def generate_filename_list(self, increment_number=True): ''' Go through the model, entry for entry and populate the filenames ''' row_count = self.parent.model.rowCount() - filename_column = self.parent.model.getFilenameColumn() - - print('Row count: ', row_count) - print('Filename column: ', filename_column) - num_string = '000000' - if self.field('StartNumber'): - start_number = self.field('StartNumberValue') - else: - start_number = 0 - + start_number = 0 start_number_string = str(start_number) - self.filename_list = [] - for row in range(0, row_count): filename = '' - if self.field('Description'): - descriptionstring = self.field('Description') + if self.field('DescriptionRaw'): + descriptionstring = self.field('DescriptionRaw') filename += self.replace_spaces_with_underscores(descriptionstring) filename += '_' - if self.field('xyPosition'): - '''Round to nearest integer ''' - x_position_string = str(int(round(self.parent.model.getXPosition(row)))) - y_position_string = str(int(round(self.parent.model.getYPosition(row)))) + if self.field('DescriptionHDF5'): + descriptionstring = self.field('DescriptionHDF5') + filename += self.replace_spaces_with_underscores(descriptionstring) + filename += '_' - filename += 'X' + x_position_string + '_' + 'Y' + y_position_string + '_' + if self.file_format == 'raw': + if self.field('xyPosition'): + '''Round to nearest integer ''' + x_position_string = str(int(round(self.parent.model.getXPosition(row)))) + y_position_string = str(int(round(self.parent.model.getYPosition(row)))) - if self.field('rotationPosition'): - rot_position_string = str(int(round(self.parent.model.getRotationPosition(row)))) - filename += 'rot_' + rot_position_string + '_' + filename += 'X' + x_position_string + '_' + 'Y' + y_position_string + '_' - if self.field('Laser'): - laserstring = self.parent.model.getLaser(row) - filename += self.replace_spaces_with_underscores(laserstring) - filename += '_' - - if self.field('Filter'): - filterstring = self.parent.model.getFilter(row) - filename += self.replace_spaces_with_underscores(filterstring) - filename += '_' - - if self.field('Zoom'): - zoomstring = self.parent.model.getZoom(row) - filename += self.replace_dots_with_underscores(zoomstring) - filename += '_' + if self.field('rotationPosition'): + rot_position_string = str(int(round(self.parent.model.getRotationPosition(row)))) + filename += 'rot_' + rot_position_string + '_' - if self.field('Shutterconfig'): - shutterstring = self.parent.model.getShutterconfig(row) - filename += shutterstring - filename += '_' + if self.field('Laser'): + laserstring = self.parent.model.getLaser(row) + filename += self.replace_spaces_with_underscores(laserstring) + filename += '_' - file_suffix = num_string[:-len(start_number_string)]+start_number_string + '.raw' + if self.field('Filter'): + filterstring = self.parent.model.getFilter(row) + filename += self.replace_spaces_with_underscores(filterstring) + filename += '_' + + if self.field('Zoom'): + zoomstring = self.parent.model.getZoom(row) + filename += self.replace_dots_with_underscores(zoomstring) + filename += '_' + + if self.field('Shutterconfig'): + shutterstring = self.parent.model.getShutterconfig(row) + filename += shutterstring + filename += '_' + + file_suffix = num_string[:-len(start_number_string)] + start_number_string + '.' + self.file_format + + if increment_number: + start_number += 1 + start_number_string = str(start_number) + + elif self.file_format == 'h5': + file_suffix = 'bdv.' + self.file_format + + else: + raise ValueError(f"file suffix invalid: {self.file_format}") - start_number += 1 - start_number_string = str(start_number) - filename += file_suffix - + self.filename_list.append(filename) def update_filenames_in_model(self): @@ -135,7 +134,7 @@ def update_filenames_in_model(self): filename_column = self.parent.model.getFilenameColumn() for row in range(0, row_count): - filename = self.filename_list[row] + filename = self.filename_list[row] index = self.parent.model.createIndex(row, filename_column) self.parent.model.setData(index, filename) @@ -145,9 +144,41 @@ def __init__(self, parent=None): self.parent = parent self.setTitle("Autogenerate filenames") + self.setSubTitle("How would you like to save your data?") + + self.raw_string = 'Individual Raw Files: ~.raw' + self.single_hdf5_string = 'Single HDF5-File: ~.h5' + + self.SaveAsComboBoxLabel = QtWidgets.QLabel('Save as:') + self.SaveAsComboBox = QtWidgets.QComboBox() + self.SaveAsComboBox.addItems([self.raw_string, self.single_hdf5_string]) + self.SaveAsComboBox.setCurrentIndex(0) + + self.registerField('SaveAs', self.SaveAsComboBox, 'currentIndex') + + self.layout = QtWidgets.QGridLayout() + self.layout.addWidget(self.SaveAsComboBoxLabel, 0, 0) + self.layout.addWidget(self.SaveAsComboBox, 0, 1) + self.setLayout(self.layout) + + def nextId(self): + if self.SaveAsComboBox.currentText() == self.raw_string: # is .raw + self.parent.file_format = 'raw' + return self.parent.raw + elif self.SaveAsComboBox.currentText() == self.single_hdf5_string: # is .h5 + self.parent.file_format = 'h5' + return self.parent.single_hdf5 + +class FilenameWizardRawSelectionPage(QtWidgets.QWizardPage): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + + self.setTitle("Autogenerate raw filenames") self.setSubTitle("Which properties would you like to use?") - self.DescriptionCheckBox = QtWidgets.QCheckBox('Description: ',self) + self.DescriptionCheckBox = QtWidgets.QCheckBox('Description: ', self) + self.DescriptionCheckBox.setChecked(True) self.DescriptionLineEdit = QtWidgets.QLineEdit(self) self.DescriptionCheckBox.toggled.connect(lambda boolean: self.DescriptionLineEdit.setEnabled(boolean)) @@ -156,31 +187,22 @@ def __init__(self, parent=None): self.RotationPositionCheckBox = QtWidgets.QCheckBox('Rotation angle') self.LaserCheckBox = QtWidgets.QCheckBox('Laser', self) + self.LaserCheckBox.setChecked(True) self.FilterCheckBox = QtWidgets.QCheckBox('Filter', self) + # self.FilterCheckBox.setChecked(True) self.ZoomCheckBox = QtWidgets.QCheckBox('Zoom', self) + self.ZoomCheckBox.setChecked(True) self.ShutterCheckBox = QtWidgets.QCheckBox('Shutterconfig', self) - self.StartNumberCheckBox = QtWidgets.QCheckBox('Start Number: ', self) - - self.StartNumberSpinBox = QtWidgets.QSpinBox(self) - self.StartNumberSpinBox.setEnabled(False) - self.StartNumberSpinBox.setValue(0) - self.StartNumberSpinBox.setSingleStep(1) - self.StartNumberSpinBox.setMinimum(0) - self.StartNumberSpinBox.setMaximum(999999) + self.ShutterCheckBox.setChecked(True) - self.StartNumberCheckBox.toggled.connect(lambda boolean: self.StartNumberSpinBox.setEnabled(boolean)) - - self.registerField('Description', self.DescriptionLineEdit) + self.registerField('DescriptionRaw', self.DescriptionLineEdit) self.registerField('xyPosition', self.xyPositionCheckBox) self.registerField('rotationPosition', self.RotationPositionCheckBox) - self.registerField('Laser',self.LaserCheckBox) + self.registerField('Laser', self.LaserCheckBox) self.registerField('Filter', self.FilterCheckBox) self.registerField('Zoom', self.ZoomCheckBox) self.registerField('Shutterconfig', self.ShutterCheckBox) - self.registerField('StartNumber', self.StartNumberCheckBox) - self.registerField('StartNumberValue', self.StartNumberSpinBox) - - + self.layout = QtWidgets.QGridLayout() self.layout.addWidget(self.DescriptionCheckBox, 0, 0) self.layout.addWidget(self.DescriptionLineEdit, 0, 1) @@ -190,14 +212,43 @@ def __init__(self, parent=None): self.layout.addWidget(self.FilterCheckBox, 4, 0) self.layout.addWidget(self.ZoomCheckBox, 5, 0) self.layout.addWidget(self.ShutterCheckBox, 6, 0) - self.layout.addWidget(self.StartNumberCheckBox, 7, 0) - self.layout.addWidget(self.StartNumberSpinBox, 7, 1) self.setLayout(self.layout) def validatePage(self): self.parent.generate_filename_list() return super().validatePage() + def nextId(self): + return self.parent.finished + + +class FilenameWizardSingleHDF5SelectionPage(QtWidgets.QWizardPage): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + + self.setTitle("Autogenerate hdf5 filename") + self.setSubTitle("This replaces all filenames with a single hdf5 file. \n Which properties would you like to use?") + + self.DescriptionCheckBox = QtWidgets.QCheckBox('Description: ', self) + self.DescriptionLineEdit = QtWidgets.QLineEdit(self) + self.DescriptionCheckBox.toggled.connect(lambda boolean: self.DescriptionLineEdit.setEnabled(boolean)) + + self.layout = QtWidgets.QGridLayout() + self.layout.addWidget(self.DescriptionCheckBox, 0, 0) + self.layout.addWidget(self.DescriptionLineEdit, 0, 1) + self.setLayout(self.layout) + + self.registerField('DescriptionHDF5', self.DescriptionLineEdit) + + def validatePage(self): + self.parent.generate_filename_list(increment_number=False) + return super().validatePage() + + def nextId(self): + return self.parent.finished + + class FilenameWizardCheckResultsPage(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) @@ -217,10 +268,18 @@ def __init__(self, parent=None): self.setLayout(self.layout) def initializePage(self): - for i in self.parent.filename_list: - self.mystring += str(i) + if self.parent.file_format == 'raw': + file_list = self.parent.filename_list + elif self.parent.file_format == 'h5': + file_list = [self.parent.filename_list[0]] + else: + raise ValueError(f"file_format must be in ('raw', 'h5'), received {self.parent.file_format}") + + for f in file_list: + self.mystring += f self.mystring += '\n' self.TextEdit.setPlainText(self.mystring) def cleanupPage(self): self.mystring = '' + diff --git a/mesoSPIM/src/utils/multicolor_acquisition_builder.py b/mesoSPIM/src/utils/multicolor_acquisition_builder.py index e79a721..73dcad9 100644 --- a/mesoSPIM/src/utils/multicolor_acquisition_builder.py +++ b/mesoSPIM/src/utils/multicolor_acquisition_builder.py @@ -58,45 +58,46 @@ def __init__(self, dict): Core loop: Create an acquisition list for all x & y & channel values ''' tilecount = 0 - - for i in range(0,self.dict['x_image_count']): - self.x_pos = round(self.x_start + i * self.x_offset,2) - - for j in range(0,self.dict['y_image_count']): - self.y_pos = round(self.y_start + j * self.y_offset,2) - + for i in range(0, self.dict['x_image_count']): + self.x_pos = round(self.x_start + i * self.x_offset, 2) + for j in range(0, self.dict['y_image_count']): + self.y_pos = round(self.y_start + j * self.y_offset, 2) channelcount = 0 for c in range(0, len(self.dict['channels'])): ''' Get a single channeldict out of the list of dicts ''' channeldict = self.dict['channels'][c] - - acq = Acquisition( x_pos=self.x_pos, - y_pos=self.y_pos, - z_start=self.dict['z_start'], - z_end=self.dict['z_end'], - z_step=self.dict['z_step'], - theta_pos=self.dict['theta_pos'], - f_start=round(channeldict['f_start'],2), - f_end=round(channeldict['f_end'],2), - laser=channeldict['laser'], - intensity=channeldict['intensity'], - filter=channeldict['filter'], - zoom=self.dict['zoom'], - shutterconfig=self.dict['shutterconfig'], - folder=self.dict['folder'], - filename='tiling_file_t'+str(tilecount)+'_c'+str(channelcount)+'.raw', - etl_l_offset=channeldict['etl_l_offset'], - etl_l_amplitude=channeldict['etl_l_amplitude'], - etl_r_offset=channeldict['etl_r_offset'], - etl_r_amplitude=channeldict['etl_r_amplitude'], - ) - ''' Update number of planes as this is not done by the acquisition - object itself ''' - acq['planes']=acq.get_image_count() - - self.acq_prelist.append(acq) - - channelcount +=1 + if self.dict['shutter_seq']: + n_shutter_configs = 2 + shutter_states = ["Left", "Right"] + else: + n_shutter_configs = 1 + shutter_states = [self.dict['shutterconfig']] + for i_shutter in range(n_shutter_configs): + acq = Acquisition( x_pos=self.x_pos, + y_pos=self.y_pos, + z_start=self.dict['z_start'], + z_end=self.dict['z_end'], + z_step=self.dict['z_step'], + theta_pos=self.dict['theta_pos'], + f_start=round(channeldict['f_start'],2), + f_end=round(channeldict['f_end'],2), + laser=channeldict['laser'], + intensity=channeldict['intensity'], + filter=channeldict['filter'], + zoom=self.dict['zoom'], + shutterconfig=shutter_states[i_shutter], + folder=self.dict['folder'], + filename='tiling_file_t'+str(tilecount)+'_c'+str(channelcount)+'.raw', + etl_l_offset=channeldict['etl_l_offset'], + etl_l_amplitude=channeldict['etl_l_amplitude'], + etl_r_offset=channeldict['etl_r_offset'], + etl_r_amplitude=channeldict['etl_r_amplitude'], + ) + ''' Update number of planes as this is not done by the acquisition + object itself ''' + acq['planes'] = acq.get_image_count() + self.acq_prelist.append(acq) + channelcount += 1 tilecount += 1 diff --git a/mesoSPIM/src/utils/multicolor_acquisition_wizard.py b/mesoSPIM/src/utils/multicolor_acquisition_wizard.py index 3089077..f54b41c 100644 --- a/mesoSPIM/src/utils/multicolor_acquisition_wizard.py +++ b/mesoSPIM/src/utils/multicolor_acquisition_wizard.py @@ -6,6 +6,7 @@ ''' import numpy as np import pprint +from functools import partial from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import pyqtProperty @@ -32,7 +33,7 @@ def __init__(self, parent=None): ''' By an instance variable, callbacks to window signals can be handed through ''' self.parent = parent - self.cfg = parent.cfg + self.cfg = parent.cfg if parent else None self.state = mesoSPIM_StateSingleton() ''' Instance variables ''' @@ -42,10 +43,12 @@ def __init__(self, parent=None): self.y_end = 0 self.z_start = 0 self.z_end = 0 - self.z_step = 1 + self.z_step = 10 self.x_offset = 0 self.y_offset = 0 self.zoom = '1x' + self.x_pixels = self.cfg.camera_parameters['x_pixels'] if self.cfg else 2048 + self.y_pixels = self.cfg.camera_parameters['y_pixels'] if self.cfg else 2048 self.x_fov = 1 self.y_fov = 1 self.channels = [] @@ -57,6 +60,7 @@ def __init__(self, parent=None): self.folder = '' self.delta_x = 0.0 self.delta_y = 0.0 + self.shutter_seq = False self.setWindowTitle('Tiling Wizard') @@ -70,7 +74,7 @@ def __init__(self, parent=None): self.setPage(7, ThirdChannelPage(self)) self.setPage(8, DefineFolderPage(self)) self.setPage(9, FinishedTilingPage(self)) - + self.setWizardStyle(QtWidgets.QWizard.ModernStyle) self.show() def done(self, r): @@ -85,7 +89,8 @@ def done(self, r): print('Wizard was closed properly') # self.print_dict() self.update_acquisition_list() - self.update_model(self.parent.model, self.acq_list) + if self.parent: + self.update_model(self.parent.model, self.acq_list) ''' Update state with this new list ''' # self.parent.update_persistent_editors() self.wizard_done.emit() @@ -106,32 +111,8 @@ def update_image_counts(self): self.delta_y = abs(self.y_end - self.y_start) ''' Using the ceiling function to always create at least 1 image ''' - self.x_image_count = int(np.ceil(self.delta_x/self.x_offset)) - self.y_image_count = int(np.ceil(self.delta_y/self.y_offset)) - - ''' Create at least 1 image even if delta_x or delta_y is 0 ''' - if self.x_image_count == 0: - self.x_image_count = 1 - if self.y_image_count == 0: - self.y_image_count = 1 - - ''' The first FOV is centered on the starting location - - therefore, add another image count to fully contain the end position - if necessary - ''' - if self.delta_x % self.x_offset > self.x_offset/2: - self.x_image_count = self.x_image_count + 1 - - if self.delta_y % self.y_offset > self.y_offset/2: - self.y_image_count = self.y_image_count + 1 - - - def update_fov(self): - pass - # zoom = self.zoom - # index = self.parent.cfg.zoom_options.index(zoom) - # self.x_fov = self.parent.cfg.zoom_options[index] - # self.y_fov = self.parent.cfg.zoom_options[index] + self.x_image_count = int(np.ceil(self.delta_x / self.x_offset)) + 1 + self.y_image_count = int(np.ceil(self.delta_y / self.y_offset)) + 1 def get_dict(self): return {'x_start' : self.x_start, @@ -150,14 +131,14 @@ def get_dict(self): 'y_image_count' : self.y_image_count, 'zoom' : self.zoom, 'shutterconfig' : self.shutterconfig, + 'shutter_seq': self.shutter_seq, 'folder' : self.folder, 'channels' : self.channels, } def update_acquisition_list(self): self.update_image_counts() - self.update_fov() - + ''' Use the current rotation angle ''' self.theta_pos = self.state['position']['theta_pos'] @@ -189,65 +170,108 @@ def __init__(self, parent=None): self.parent = parent self.setTitle("Define the bounding box of the tiling acquisition") - self.setSubTitle("Move XY stages to the starting corner position") - - self.button0 = QtWidgets.QPushButton(self) - self.button0.setText('Set XY Start Corner') - self.button0.setCheckable(True) - self.button0.toggled.connect(self.get_xy_start_position) - - self.button1 = QtWidgets.QPushButton(self) - self.button1.setText('Set XY End Corner') - self.button1.setCheckable(True) - self.button1.toggled.connect(self.get_xy_end_position) + self.setSubTitle("Define bounding box by corners OR edges. " + "Move XY stages to the positions before pressing the bounding box buttons.") + + self.button_xy_start = QtWidgets.QPushButton(self) + self.button_xy_start.setText('Set XY Start Corner') + self.button_xy_start.setCheckable(True) + self.button_xy_start.clicked.connect(partial(self.get_edge_position, key='xy-start')) + + self.button_x_start = QtWidgets.QPushButton(self) + self.button_x_start.setText('Set X start') + self.button_x_start.setCheckable(True) + self.button_x_start.clicked.connect(partial(self.get_edge_position, key='x-start')) + + self.button_x_end = QtWidgets.QPushButton(self) + self.button_x_end.setText('Set X end') + self.button_x_end.setCheckable(True) + self.button_x_end.clicked.connect(partial(self.get_edge_position, key='x-end')) + + self.button_y_start = QtWidgets.QPushButton(self) + self.button_y_start.setText('Set Y start') + self.button_y_start.setCheckable(True) + self.button_y_start.clicked.connect(partial(self.get_edge_position, key='y-start')) + + self.button_y_end = QtWidgets.QPushButton(self) + self.button_y_end.setText('Set Y end') + self.button_y_end.setCheckable(True) + self.button_y_end.clicked.connect(partial(self.get_edge_position, key='y-end')) + + self.button_xy_end = QtWidgets.QPushButton(self) + self.button_xy_end.setText('Set XY End Corner') + self.button_xy_end.setCheckable(True) + self.button_xy_end.clicked.connect(partial(self.get_edge_position, key='xy-end')) self.ZStartButton = QtWidgets.QPushButton(self) self.ZStartButton.setText('Set Z start') self.ZStartButton.setCheckable(True) - self.ZStartButton.toggled.connect(self.update_z_start_position) + self.ZStartButton.clicked.connect(partial(self.get_edge_position, key='z-start')) self.ZEndButton = QtWidgets.QPushButton(self) self.ZEndButton.setText('Set Z end') self.ZEndButton.setCheckable(True) - self.ZEndButton.toggled.connect(self.update_z_end_position) + self.ZEndButton.clicked.connect(partial(self.get_edge_position, key='z-end')) self.ZSpinBoxLabel = QtWidgets.QLabel('Z stepsize') - - self.ZStepSpinBox = QtWidgets.QSpinBox(self) - self.ZStepSpinBox.setValue(1) - self.ZStepSpinBox.setMinimum(1) + self.ZStepSpinBox = QtWidgets.QDoubleSpinBox(self) + self.ZStepSpinBox.setValue(10) + self.ZStepSpinBox.setDecimals(1) + self.ZStepSpinBox.setMinimum(0.1) self.ZStepSpinBox.setMaximum(1000) self.ZStepSpinBox.valueChanged.connect(self.update_z_step) - self.registerField('xy_start_position*', - self.button0, - ) - self.registerField('xy_end_position*', - self.button1, - ) + self.registerField('xy_start_position*', self.button_xy_start) + self.registerField('xy_end_position*', self.button_xy_end) + self.registerField('z_end_position*', self.ZEndButton) + self.update_z_step() self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.button0, 0, 0) - self.layout.addWidget(self.button1, 1, 1) - self.layout.addWidget(self.ZStartButton, 2, 0) - self.layout.addWidget(self.ZEndButton, 2, 1) - self.layout.addWidget(self.ZSpinBoxLabel, 3, 0) - self.layout.addWidget(self.ZStepSpinBox, 3, 1) + self.layout.addWidget(self.button_xy_start, 0, 0) + self.layout.addWidget(self.button_y_start, 0, 1) + self.layout.addWidget(self.button_x_start, 1, 0) + self.layout.addWidget(self.button_x_end, 1, 2) + self.layout.addWidget(self.button_y_end, 2, 1) + self.layout.addWidget(self.button_xy_end, 2, 2) + self.layout.addWidget(self.ZStartButton, 3, 0) + self.layout.addWidget(self.ZEndButton, 3, 2) + self.layout.addWidget(self.ZSpinBoxLabel, 4, 0) + self.layout.addWidget(self.ZStepSpinBox, 4, 2) self.setLayout(self.layout) - def get_xy_start_position(self): - self.parent.x_start = self.parent.state['position']['x_pos'] - self.parent.y_start = self.parent.state['position']['y_pos'] - - def get_xy_end_position(self): - self.parent.x_end = self.parent.state['position']['x_pos'] - self.parent.y_end = self.parent.state['position']['y_pos'] - - def update_z_start_position(self): - self.parent.z_start = self.parent.state['position']['z_pos'] - - def update_z_end_position(self): - self.parent.z_end = self.parent.state['position']['z_pos'] + def get_edge_position(self, key): + valid_keys = ('x-start', 'x-end', 'y-start', 'y-end', 'z-start', 'z-end', 'xy-start', 'xy-end') + assert key in valid_keys, f"Position key {key} is invalid" + if key == 'x-start': + self.parent.x_start = self.parent.state['position']['x_pos'] + if self.button_y_start.isChecked(): + self.button_xy_start.setChecked(True) + elif key == 'x-end': + self.parent.x_end = self.parent.state['position']['x_pos'] + if self.button_y_end.isChecked(): + self.button_xy_end.setChecked(True) + elif key == 'y-start': + self.parent.y_start = self.parent.state['position']['y_pos'] + if self.button_x_start.isChecked(): + self.button_xy_start.setChecked(True) + elif key == 'y-end': + self.parent.y_end = self.parent.state['position']['y_pos'] + if self.button_x_end.isChecked(): + self.button_xy_end.setChecked(True) + elif key == 'z-start': + self.parent.z_start = self.parent.state['position']['z_pos'] + elif key == 'z-end': + self.parent.z_end = self.parent.state['position']['z_pos'] + elif key == 'xy-start': + self.parent.x_start = self.parent.state['position']['x_pos'] + self.parent.y_start = self.parent.state['position']['y_pos'] + self.button_x_start.setChecked(True) + self.button_y_start.setChecked(True) + elif key == 'xy-end': + self.parent.x_end = self.parent.state['position']['x_pos'] + self.parent.y_end = self.parent.state['position']['y_pos'] + self.button_x_end.setChecked(True) + self.button_y_end.setChecked(True) def update_z_step(self): self.parent.z_step = self.ZStepSpinBox.value() @@ -260,9 +284,41 @@ def __init__(self, parent): self.setTitle("Define other parameters") + self.channelLabel = QtWidgets.QLabel('# Channels') + self.channelSpinBox = QtWidgets.QSpinBox(self) + self.channelSpinBox.setMinimum(1) + self.channelSpinBox.setMaximum(3) + self.zoomLabel = QtWidgets.QLabel('Zoom') self.zoomComboBox = QtWidgets.QComboBox(self) - self.zoomComboBox.addItems(self.parent.cfg.zoomdict.keys()) + if self.parent.cfg: + self.zoomComboBox.addItems(self.parent.cfg.zoomdict.keys()) + self.zoomComboBox.currentIndexChanged.connect(self.update_fov_size) + + self.shutterLabel = QtWidgets.QLabel('Shutter') + self.shutterComboBox = QtWidgets.QComboBox(self) + if self.parent.cfg: + self.shutterComboBox.addItems(self.parent.cfg.shutteroptions) + + self.shutterSequenceLabel = QtWidgets.QLabel('Left, then Right?') + self.shutterSeqCheckBox = QtWidgets.QCheckBox(self) + self.shutterSeqCheckBox.setChecked(False) + self.shutterSeqCheckBox.clicked.connect(self.update_shutt_seq) + + self.fovSizeLabel = QtWidgets.QLabel('FOV Size X ⨉ Y:') + self.fovSizeLineEdit = QtWidgets.QLineEdit(self) + self.fovSizeLineEdit.setReadOnly(True) + + self.overlapPercentageCheckBox = QtWidgets.QCheckBox('Overlap %', self) + self.overlapLabel = QtWidgets.QLabel('Overlap in %') + self.overlapPercentageSpinBox = QtWidgets.QSpinBox(self) + self.overlapPercentageSpinBox.setSuffix(' %') + self.overlapPercentageSpinBox.setMinimum(1) + self.overlapPercentageSpinBox.setMaximum(50) + self.overlapPercentageSpinBox.setValue(10) + self.overlapPercentageSpinBox.valueChanged.connect(self.update_x_and_y_offset) + + self.manualOverlapCheckBox = QtWidgets.QCheckBox('Set Offset Manually', self) self.xOffsetSpinBoxLabel = QtWidgets.QLabel('X Offset') self.xOffsetSpinBox = QtWidgets.QSpinBox(self) @@ -270,7 +326,7 @@ def __init__(self, parent): self.xOffsetSpinBox.setMinimum(1) self.xOffsetSpinBox.setMaximum(30000) self.xOffsetSpinBox.setValue(500) - + self.yOffsetSpinBoxLabel = QtWidgets.QLabel('Y Offset') self.yOffsetSpinBox = QtWidgets.QSpinBox(self) self.yOffsetSpinBox.setSuffix(' μm') @@ -278,26 +334,36 @@ def __init__(self, parent): self.yOffsetSpinBox.setMaximum(30000) self.yOffsetSpinBox.setValue(500) - self.shutterLabel = QtWidgets.QLabel('Shutter') - self.shutterComboBox = QtWidgets.QComboBox(self) - self.shutterComboBox.addItems(self.parent.cfg.shutteroptions) + self.overlapPercentageCheckBox.clicked.connect(lambda boolean: self.overlapPercentageSpinBox.setEnabled(boolean)) + self.overlapPercentageCheckBox.clicked.connect(self.update_x_and_y_offset) + self.overlapPercentageCheckBox.clicked.connect(lambda boolean: self.xOffsetSpinBox.setEnabled(not boolean)) + self.overlapPercentageCheckBox.clicked.connect(lambda boolean: self.yOffsetSpinBox.setEnabled(not boolean)) + self.overlapPercentageCheckBox.clicked.connect(lambda boolean: self.manualOverlapCheckBox.setChecked(not boolean)) - self.channelLabel = QtWidgets.QLabel('# Channels') - self.channelSpinBox = QtWidgets.QSpinBox(self) - self.channelSpinBox.setMinimum(1) - self.channelSpinBox.setMaximum(3) + self.manualOverlapCheckBox.clicked.connect(lambda boolean: self.overlapPercentageSpinBox.setEnabled(not boolean)) + self.manualOverlapCheckBox.clicked.connect(lambda boolean: self.xOffsetSpinBox.setEnabled(boolean)) + self.manualOverlapCheckBox.clicked.connect(lambda boolean: self.yOffsetSpinBox.setEnabled(boolean)) + self.manualOverlapCheckBox.clicked.connect(lambda boolean: self.overlapPercentageCheckBox.setChecked(not boolean)) self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.zoomLabel, 0, 0) - self.layout.addWidget(self.zoomComboBox, 0, 1) - self.layout.addWidget(self.shutterLabel, 1, 0) - self.layout.addWidget(self.shutterComboBox, 1, 1) - self.layout.addWidget(self.xOffsetSpinBoxLabel, 2, 0) - self.layout.addWidget(self.xOffsetSpinBox, 2, 1) - self.layout.addWidget(self.yOffsetSpinBoxLabel, 3, 0) - self.layout.addWidget(self.yOffsetSpinBox, 3, 1) - self.layout.addWidget(self.channelLabel, 4, 0) - self.layout.addWidget(self.channelSpinBox, 4, 1) + self.layout.addWidget(self.channelLabel, 0, 0) + self.layout.addWidget(self.channelSpinBox, 0, 1) + self.layout.addWidget(self.zoomLabel, 1, 0) + self.layout.addWidget(self.zoomComboBox, 1, 1) + self.layout.addWidget(self.shutterLabel, 2, 0) + self.layout.addWidget(self.shutterComboBox, 2, 1) + self.layout.addWidget(self.shutterSequenceLabel, 2, 2) + self.layout.addWidget(self.shutterSeqCheckBox, 2, 3) + self.layout.addWidget(self.fovSizeLabel, 3, 0) + self.layout.addWidget(self.fovSizeLineEdit, 3, 1) + self.layout.addWidget(self.overlapPercentageCheckBox, 4, 0) + self.layout.addWidget(self.overlapLabel, 5, 0) + self.layout.addWidget(self.overlapPercentageSpinBox, 5, 1) + self.layout.addWidget(self.manualOverlapCheckBox, 6, 0) + self.layout.addWidget(self.xOffsetSpinBoxLabel, 7, 0) + self.layout.addWidget(self.xOffsetSpinBox, 7, 1) + self.layout.addWidget(self.yOffsetSpinBoxLabel, 8, 0) + self.layout.addWidget(self.yOffsetSpinBox, 8, 1) self.setLayout(self.layout) def validatePage(self): @@ -305,9 +371,41 @@ def validatePage(self): self.update_other_acquisition_parameters() return True + @QtCore.pyqtSlot() + def update_fov_size(self): + ''' Should be invoked whenever the zoom selection is changed ''' + new_zoom = self.zoomComboBox.currentText() + pixelsize_in_um = self.parent.cfg.pixelsize[new_zoom] if self.parent.cfg else 6.5 + ''' X and Y are interchanged here to account for the camera rotation by 90°''' + new_x_fov_in_um = int(self.parent.y_pixels * pixelsize_in_um) + new_y_fov_in_um = int(self.parent.x_pixels * pixelsize_in_um) + self.parent.x_fov = new_x_fov_in_um + self.parent.y_fov = new_y_fov_in_um + + self.fovSizeLineEdit.setText(str(new_x_fov_in_um)+' ⨉ '+str(new_y_fov_in_um) + ' μm²') + + ''' If the zoom changes, the offset calculation should be redone''' + if self.overlapPercentageCheckBox.isChecked(): + self.update_x_and_y_offset() + + @QtCore.pyqtSlot() + def update_x_and_y_offset(self): + new_offset_percentage = self.overlapPercentageSpinBox.value() + x_offset = int(self.parent.x_fov * (1-new_offset_percentage / 100)) + y_offset = int(self.parent.y_fov * (1-new_offset_percentage / 100)) + self.xOffsetSpinBox.setValue(x_offset) + self.yOffsetSpinBox.setValue(y_offset) + + @QtCore.pyqtSlot() + def update_shutt_seq(self): + self.parent.shutter_seq = self.shutterSeqCheckBox.checkState() + if self.shutterSeqCheckBox.checkState(): + self.shutterComboBox.setEnabled(False) + else: + self.shutterComboBox.setEnabled(True) + def update_other_acquisition_parameters(self): ''' Here, all the Tiling parameters are filled in the parent (TilingWizard) - This method should be called when the "Next" Button is pressed ''' self.parent.zoom = self.zoomComboBox.currentText() @@ -318,7 +416,12 @@ def update_other_acquisition_parameters(self): def initializePage(self): self.update_page_from_state() - + self.update_fov_size() + self.update_x_and_y_offset() + self.overlapPercentageCheckBox.setChecked(True) + self.xOffsetSpinBox.setEnabled(False) + self.yOffsetSpinBox.setEnabled(False) + def update_page_from_state(self): self.zoomComboBox.setCurrentText(self.parent.state['zoom']) self.shutterComboBox.setCurrentText(self.parent.state['shutterconfig']) @@ -339,25 +442,49 @@ def __init__(self, parent=None): self.yFOVs = QtWidgets.QLineEdit(self) self.yFOVs.setReadOnly(True) + self.x_start_end_label = QtWidgets.QLabel('X start, end:') + self.x_start = QtWidgets.QLineEdit(self) + self.x_start.setReadOnly(True) + self.x_end = QtWidgets.QLineEdit(self) + self.x_end.setReadOnly(True) + + self.y_start_end_label = QtWidgets.QLabel('Y start, end:') + self.y_start = QtWidgets.QLineEdit(self) + self.y_start.setReadOnly(True) + self.y_end = QtWidgets.QLineEdit(self) + self.y_end.setReadOnly(True) + self.Button = QtWidgets.QPushButton('Values are ok?') self.Button.setCheckable(True) self.Button.setChecked(False) self.layout = QtWidgets.QGridLayout() - self.layout.addWidget(self.xFOVLabel, 1, 0) - self.layout.addWidget(self.xFOVs, 1, 1) + self.layout.addWidget(self.xFOVLabel, 0, 0) + self.layout.addWidget(self.xFOVs, 0, 1) + self.layout.addWidget(self.x_start_end_label, 1, 0) + self.layout.addWidget(self.x_start, 1, 1) + self.layout.addWidget(self.x_end, 1, 2) + self.layout.addWidget(self.yFOVLabel, 2, 0) self.layout.addWidget(self.yFOVs, 2, 1) - self.layout.addWidget(self.Button, 3, 1) - self.setLayout(self.layout) + self.layout.addWidget(self.y_start_end_label, 3, 0) + self.layout.addWidget(self.y_start, 3, 1) + self.layout.addWidget(self.y_end, 3, 2) - self.registerField('finalCheck*',self.Button) + self.layout.addWidget(self.Button, 4, 0) + self.setLayout(self.layout) + self.registerField('finalCheck*', self.Button) def initializePage(self): ''' Here, the acquisition list is created for further checking''' self.parent.update_image_counts() self.xFOVs.setText(str(self.parent.x_image_count)) self.yFOVs.setText(str(self.parent.y_image_count)) + self.x_start.setText(str(round(self.parent.x_start))) + self.y_start.setText(str(round(self.parent.y_start))) + self.x_end.setText(str(round(self.parent.x_end))) + self.y_end.setText(str(round(self.parent.y_end))) + class GenericChannelPage(QtWidgets.QWizardPage): def __init__(self, parent=None, channel_id=0): @@ -379,7 +506,8 @@ def __init__(self, parent=None, channel_id=0): self.laserLabel = QtWidgets.QLabel('Laser') self.laserComboBox = QtWidgets.QComboBox(self) - self.laserComboBox.addItems(self.parent.cfg.laserdict.keys()) + if self.parent.cfg: + self.laserComboBox.addItems(self.parent.cfg.laserdict.keys()) self.intensityLabel = QtWidgets.QLabel('Intensity') self.intensitySlider = QtWidgets.QSlider(QtCore.Qt.Horizontal) @@ -388,7 +516,8 @@ def __init__(self, parent=None, channel_id=0): self.filterLabel = QtWidgets.QLabel('Filter') self.filterComboBox = QtWidgets.QComboBox(self) - self.filterComboBox.addItems(self.parent.cfg.filterdict.keys()) + if self.parent.cfg: + self.filterComboBox.addItems(self.parent.cfg.filterdict.keys()) self.ETLCheckBoxLabel = QtWidgets.QLabel('ETL') self.ETLCheckBox = QtWidgets.QCheckBox('Copy current ETL parameters', self) @@ -540,7 +669,6 @@ def __init__(self, parent=None): def choose_folder(self): ''' File dialog for choosing the save folder ''' - path = QtWidgets.QFileDialog.getExistingDirectory(self.parent, 'Select Folder') if path: self.parent.folder = path @@ -552,13 +680,15 @@ def __init__(self, parent=None): self.parent = parent self.setTitle("Finished!") - self.setSubTitle("Attention: This will overwrite the Acquisition Table. Click 'Finished' to continue. To rename the files, use the filename wizard.") + self.setSubTitle("Attention: This will overwrite the Acquisition Table. Click 'Finished' to continue. " + "To rename the files, use the filename wizard.") def validatePage(self): return True + if __name__ == '__main__': import sys app = QtWidgets.QApplication(sys.argv) - wizard = MyWizard() + wizard = MulticolorTilingWizard() sys.exit(app.exec_()) diff --git a/mesoSPIM/src/utils/widgets.py b/mesoSPIM/src/utils/widgets.py index d6843d7..07ed72e 100644 --- a/mesoSPIM/src/utils/widgets.py +++ b/mesoSPIM/src/utils/widgets.py @@ -12,6 +12,10 @@ def __init__(self, parent=None): self.button = QtWidgets.QPushButton() self.button.setText("M") + font = QtGui.QFont() + font.setPointSize(14) + self.button.setFont(font) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -20,6 +24,7 @@ def __init__(self, parent=None): self.button.setMinimumSize(QtCore.QSize(25, 0)) self.lineEdit = QtWidgets.QLineEdit() + self.lineEdit.setFont(font) # self.lineEdit.setReadOnly(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -54,6 +59,7 @@ def __init__(self, parent=None): self.label.setSizePolicy(sizePolicy) self.slider = QtWidgets.QSlider() + self.slider.setOrientation(QtCore.Qt.Horizontal) self.slider.setTracking(False) self.slider.valueChanged.connect(self.setText) @@ -65,6 +71,7 @@ def __init__(self, parent=None): sizePolicy.setHeightForWidth(self.slider.sizePolicy().hasHeightForWidth()) self.slider.setSizePolicy(sizePolicy) + self.layout = QtWidgets.QHBoxLayout() self.layout.addWidget(self.label) self.layout.addWidget(self.slider) @@ -73,6 +80,10 @@ def __init__(self, parent=None): self.setLayout(self.layout) self.setAutoFillBackground(True) + font = QtGui.QFont() + font.setPointSize(14) + self.slider.setFont(font) + def setText(self, value): self.label.setText(str(value) + '%') diff --git a/mesoSPIM/start_mesoSPIM.bat b/mesoSPIM/start_mesoSPIM.bat new file mode 100644 index 0000000..9a5ce5f --- /dev/null +++ b/mesoSPIM/start_mesoSPIM.bat @@ -0,0 +1,10 @@ +rem Change the Anaconda path "C:\Users\Nikita\anaconda3\Scripts\" and environments path "C:\Users\Nikita\anaconda3\envs" to your own +rem To know your Anaconda path, run in Anaconda prompt: +rem $ where conda +rem +rem In a multi-user setting, you may want to create the py36 environment in a public folder accessible for all users: +rem $ conda create -p C:/full/public/path/to/envs/py36 python=3.6 +rem Make sure all users have right to execute python in the py36 environment (you may need admin rights for that). +echo off +"%windir%\System32\cmd.exe" /k ""C:\Users\Nikita\anaconda3\Scripts\activate.bat" "C:\Users\Nikita\anaconda3\envs\py36" && python "mesoSPIM_Control.py"" +pause \ No newline at end of file diff --git a/mesoSPIM/test/__init__.py b/mesoSPIM/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mesoSPIM/test/fixtures/beams/20210430-153343.tif b/mesoSPIM/test/fixtures/beams/20210430-153343.tif new file mode 100644 index 0000000..da9f232 Binary files /dev/null and b/mesoSPIM/test/fixtures/beams/20210430-153343.tif differ diff --git a/mesoSPIM/test/fixtures/beams/20210430-153343.tif_meta.txt b/mesoSPIM/test/fixtures/beams/20210430-153343.tif_meta.txt new file mode 100644 index 0000000..00a7262 --- /dev/null +++ b/mesoSPIM/test/fixtures/beams/20210430-153343.tif_meta.txt @@ -0,0 +1,34 @@ +[CFG] +[Laser] 561 nm +[Intensity (%)] 80 +[Zoom] 1.6x +[Pixelsize in um] 4.08 +[Filter] 405/488/561/640 m +[Shutter] Right + +[POSITION] +[x_pos] 25900.01 +[y_pos] 66034.93 +[z_pos] 9300.36 +[f_pos] 61350.0 + +[ETL PARAMETERS] +[ETL CFG File] C:/Users/mesoSPIM/Documents/GitHub/mesoSPIM-control/mesoSPIM/config/etl_parameters/ETL_NV_Fusion_40x40x40-OptGlass-BBPEG.csv +[etl_l_offset] 2.0980000000000008 +[etl_l_amplitude] 0.8 +[etl_r_offset] 2.4469999999999983 +[etl_r_amplitude] 0.8 + +[GALVO PARAMETERS] +[galvo_l_frequency] 99.9 +[galvo_l_amplitude] 0.0 +[galvo_l_offset] 0.47 +[galvo_r_amplitude] 0.0 +[galvo_r_offset] -0.45 + +[CAMERA PARAMETERS] +[camera_type] HamamatsuOrca +[camera_exposure] 0.05 +[camera_line_interval] 7.5e-05 +[x_pixels] 2304 +[y_pixels] 2304 diff --git a/mesoSPIM/test/fixtures/beams/20210430-153447.tif b/mesoSPIM/test/fixtures/beams/20210430-153447.tif new file mode 100644 index 0000000..fca6ea3 Binary files /dev/null and b/mesoSPIM/test/fixtures/beams/20210430-153447.tif differ diff --git a/mesoSPIM/test/fixtures/beams/20210430-153447.tif_meta.txt b/mesoSPIM/test/fixtures/beams/20210430-153447.tif_meta.txt new file mode 100644 index 0000000..a9653c3 --- /dev/null +++ b/mesoSPIM/test/fixtures/beams/20210430-153447.tif_meta.txt @@ -0,0 +1,34 @@ +[CFG] +[Laser] 561 nm +[Intensity (%)] 80 +[Zoom] 1.6x +[Pixelsize in um] 4.08 +[Filter] 405/488/561/640 m +[Shutter] Right + +[POSITION] +[x_pos] 25900.01 +[y_pos] 66034.93 +[z_pos] 9300.36 +[f_pos] 61350.0 + +[ETL PARAMETERS] +[ETL CFG File] C:/Users/mesoSPIM/Documents/GitHub/mesoSPIM-control/mesoSPIM/config/etl_parameters/ETL_NV_Fusion_40x40x40-OptGlass-BBPEG.csv +[etl_l_offset] 2.0980000000000008 +[etl_l_amplitude] 0.8 +[etl_r_offset] 2.4469999999999983 +[etl_r_amplitude] 0.0 + +[GALVO PARAMETERS] +[galvo_l_frequency] 99.9 +[galvo_l_amplitude] 0.0 +[galvo_l_offset] 0.47 +[galvo_r_amplitude] 0.0 +[galvo_r_offset] -0.45 + +[CAMERA PARAMETERS] +[camera_type] HamamatsuOrca +[camera_exposure] 0.05 +[camera_line_interval] 7.5e-05 +[x_pixels] 2304 +[y_pixels] 2304 diff --git a/mesoSPIM/test/fixtures/beams/20210430-153514.tif b/mesoSPIM/test/fixtures/beams/20210430-153514.tif new file mode 100644 index 0000000..2ef6e5f Binary files /dev/null and b/mesoSPIM/test/fixtures/beams/20210430-153514.tif differ diff --git a/mesoSPIM/test/fixtures/beams/20210430-153514.tif_meta.txt b/mesoSPIM/test/fixtures/beams/20210430-153514.tif_meta.txt new file mode 100644 index 0000000..f670c5c --- /dev/null +++ b/mesoSPIM/test/fixtures/beams/20210430-153514.tif_meta.txt @@ -0,0 +1,34 @@ +[CFG] +[Laser] 561 nm +[Intensity (%)] 80 +[Zoom] 1.6x +[Pixelsize in um] 4.08 +[Filter] 405/488/561/640 m +[Shutter] Right + +[POSITION] +[x_pos] 25900.01 +[y_pos] 66034.93 +[z_pos] 9300.36 +[f_pos] 61350.0 + +[ETL PARAMETERS] +[ETL CFG File] C:/Users/mesoSPIM/Documents/GitHub/mesoSPIM-control/mesoSPIM/config/etl_parameters/ETL_NV_Fusion_40x40x40-OptGlass-BBPEG.csv +[etl_l_offset] 2.0980000000000008 +[etl_l_amplitude] 0.8 +[etl_r_offset] 2.0510000000000073 +[etl_r_amplitude] 0.0 + +[GALVO PARAMETERS] +[galvo_l_frequency] 99.9 +[galvo_l_amplitude] 0.0 +[galvo_l_offset] 0.47 +[galvo_r_amplitude] 0.0 +[galvo_r_offset] -0.45 + +[CAMERA PARAMETERS] +[camera_type] HamamatsuOrca +[camera_exposure] 0.05 +[camera_line_interval] 7.5e-05 +[x_pixels] 2304 +[y_pixels] 2304 diff --git a/mesoSPIM/test/fixtures/beams/20210430-153549.tif b/mesoSPIM/test/fixtures/beams/20210430-153549.tif new file mode 100644 index 0000000..1414bff Binary files /dev/null and b/mesoSPIM/test/fixtures/beams/20210430-153549.tif differ diff --git a/mesoSPIM/test/fixtures/beams/20210430-153549.tif_meta.txt b/mesoSPIM/test/fixtures/beams/20210430-153549.tif_meta.txt new file mode 100644 index 0000000..3da6a54 --- /dev/null +++ b/mesoSPIM/test/fixtures/beams/20210430-153549.tif_meta.txt @@ -0,0 +1,34 @@ +[CFG] +[Laser] 561 nm +[Intensity (%)] 80 +[Zoom] 1.6x +[Pixelsize in um] 4.08 +[Filter] 405/488/561/640 m +[Shutter] Right + +[POSITION] +[x_pos] 25900.01 +[y_pos] 66034.93 +[z_pos] 9300.36 +[f_pos] 61350.0 + +[ETL PARAMETERS] +[ETL CFG File] C:/Users/mesoSPIM/Documents/GitHub/mesoSPIM-control/mesoSPIM/config/etl_parameters/ETL_NV_Fusion_40x40x40-OptGlass-BBPEG.csv +[etl_l_offset] 2.0980000000000008 +[etl_l_amplitude] 0.8 +[etl_r_offset] 2.8309999999999933 +[etl_r_amplitude] 0.0 + +[GALVO PARAMETERS] +[galvo_l_frequency] 99.9 +[galvo_l_amplitude] 0.0 +[galvo_l_offset] 0.47 +[galvo_r_amplitude] 0.0 +[galvo_r_offset] -0.45 + +[CAMERA PARAMETERS] +[camera_type] HamamatsuOrca +[camera_exposure] 0.05 +[camera_line_interval] 7.5e-05 +[x_pixels] 2304 +[y_pixels] 2304 diff --git a/mesoSPIM/test/test_serial.py b/mesoSPIM/test/test_serial.py new file mode 100644 index 0000000..2165d9d --- /dev/null +++ b/mesoSPIM/test/test_serial.py @@ -0,0 +1,80 @@ +# To run the test: +# python -m test.test_serial +import unittest +import src.devices.filter_wheels.ludlcontrol as ludl +import src.devices.zoom.mesoSPIM_Zoom as zoomlib + +""" +Filterwheel settings from config file +""" +filterwheel_parameters = {'filterwheel_type' : 'Ludl', + 'COMport' : 'COM6'} +filterdict = {'Empty-Alignment' : 0, + '405/50' : 1, + '480/40' : 2, + '525/50' : 3, + '535/30' : 4, + '590/50' : 5, + '585/40' : 6, + '405/488/561/640 m' : 7, + } + +''' +Zoom configuration from config file +''' +zoom_parameters = {'zoom_type' : 'Dynamixel', + 'servo_id' : 1, + 'COMport' : 'COM10', + 'baudrate' : 1000000} + +zoomdict = {'0.63x' : 3423, + '0.8x' : 3071, + '1x' : 2707, + '1.25x' : 2389, + '1.6x' : 2047, + '2x' : 1706, + '2.5x' : 1354, + '3.2x' : 967, + '4x' : 637, + '5x' : 318, + '6.3x' : 0} + +class TestFilterWheel(unittest.TestCase): + def setUp(self) -> None: + """"This will be called for EVERY test method of the class.""" + if filterwheel_parameters['filterwheel_type'] == 'Ludl': + self.fwheel = ludl.LudlFilterwheel(filterwheel_parameters['COMport'], filterdict) + else: + raise ValueError('Only Ludl filterwheel test is currently implemented') + + def test_multiple_filter_pos(self): + n_cycles = 3 + for i_cycle in range(n_cycles): + print(f"cycle {i_cycle}/{n_cycles}") + for filter in filterdict.keys(): + self.fwheel.set_filter(filter, wait_until_done=True) + print(f"filter {filter}") + + # def tearDown(self) -> None: + # """"Tidies up after EACH test method execution.""" + +class TestZoomServo(unittest.TestCase): + def setUp(self) -> None: + """"This will be called for EVERY test method of the class.""" + if zoom_parameters['zoom_type'] == 'Dynamixel': + self.zoom = zoomlib.DynamixelZoom(zoomdict, zoom_parameters['COMport'], + zoom_parameters['servo_id'], zoom_parameters['baudrate']) + else: + raise ValueError('Only Dynamixel zoom servo test is currently implemented') + + def test_multiple_zoom_pos(self): + n_cycles = 3 + for i_cycle in range(n_cycles): + print(f"cycle {i_cycle}/{n_cycles}") + for zoomratio in zoomdict.keys(): + self.zoom.set_zoom(zoomratio, wait_until_done=True) + print(f"zoom {zoomratio}") + + +if __name__ == '__main__': + unittest.main() diff --git a/mesoSPIM/test/test_tiling.py b/mesoSPIM/test/test_tiling.py new file mode 100644 index 0000000..1cfe002 --- /dev/null +++ b/mesoSPIM/test/test_tiling.py @@ -0,0 +1,33 @@ +# To run the test: +# python -m test.test_tiling +import unittest +from src.utils.multicolor_acquisition_wizard import MulticolorTilingWizard +from PyQt5 import QtWidgets +import sys + +class TestTilingWizard(unittest.TestCase): + def setUp(self) -> None: + """"This will automatically call for EVERY single test method below.""" + self.wiz = MulticolorTilingWizard() + + def test_image_counts(self): + # assuming FOV = 1000, overlap 20% + xy_start_fixtures = [(0, 0), (-100, -100), (0, 0), (0, -350), (4500, 4700)] + xy_end_fixtures = [(200, 200), (100, 100), (10000, 7900), (0, 350), (-3000, -5800)] + xy_offset_fixtures = [(800, 800), (800, 800), (800, 800), (752, 752), (2948, 2948)] + xy_counts_correct_answers = [(2, 2), (2, 2), (14, 11), (1, 2), (4, 5)] + for i in range(len(xy_start_fixtures)): + self.wiz.x_start, self.wiz.y_start = xy_start_fixtures[i] + self.wiz.x_end, self.wiz.y_end = xy_end_fixtures[i] + self.wiz.x_offset, self.wiz.y_offset = xy_offset_fixtures[i] + self.wiz.update_image_counts() + self.assertEqual((self.wiz.x_image_count, self.wiz.y_image_count), xy_counts_correct_answers[i], + f"Image count [{i}] {self.wiz.x_image_count, self.wiz.y_image_count} is incorrect," \ + f" must be {xy_counts_correct_answers[i]}") + + # def tearDown(self) -> None: + # """"Tidies up after EACH test method execution.""" + +if __name__ == '__main__': + app = QtWidgets.QApplication(sys.argv) + unittest.main() diff --git a/requirements-anaconda.txt b/requirements-anaconda.txt new file mode 100644 index 0000000..133b9e9 --- /dev/null +++ b/requirements-anaconda.txt @@ -0,0 +1,15 @@ +numpy==1.17.0 +scipy==1.2.1 +PyQt5==5.13.1 +PyQt5-sip==12.7.0 +nidaqmx==0.5.7 +indexed==1.2.1 +pipython==2.5.1.3 +pyserial==3.4 +pyqtgraph==0.11.1 +pywinusb==0.4.2 +tifffile==2019.7.26 +qdarkstyle==2.8.1 +npy2bdv==1.0.7 +future==0.18.2 + diff --git a/requirements-clean-python.txt b/requirements-clean-python.txt new file mode 100644 index 0000000..9d7687f --- /dev/null +++ b/requirements-clean-python.txt @@ -0,0 +1,20 @@ +csv +traceback +pprint +ctypes +importlib +argparse +glob +numpy==1.17.0 +scipy==1.2.1 +PyQt5==5.13.1 +nidaqmx==0.5.7 +indexed==1.2.1 +pipython==2.5.1.3 +pyserial==3.4 +pyqtgraph==0.11.1 +pywinusb==0.4.2 +tifffile==2019.7.26 +qdarkstyle==2.8.1 +npy2bdv==1.0.7 +future==0.18.2