diff --git a/.gitattributes b/.gitattributes index 636a0b3..3f6730e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ xmidas/_version.py export-subst +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.gitignore b/.gitignore index 9a3e787..adc5511 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,6 @@ target/ *.png *.nor *.ipynb +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/AUTHORS.rst b/AUTHORS.rst old mode 100644 new mode 100755 index ca072bb..e9b117d --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,13 +1,17 @@ -======= -Credits -======= - -Maintainer ----------- - -* Brookhaven National Lab <> - -Contributors ------------- - -None yet. Why not be the first? See: CONTRIBUTING.rst +======= +Credits +======= + +Maintainers +---------- + +- Ajith Pattammattel +- Dmitri Gavrilov + +*Brookhaven National Laboratory* + + +Contributors +------------ + +None yet. Why not be the first? See: CONTRIBUTING.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst old mode 100644 new mode 100755 index 0b71b77..fbaf5b5 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,104 +1,104 @@ -============ -Contributing -============ - -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. - -You can contribute in many ways: - -Types of Contributions ----------------------- - -Report Bugs -~~~~~~~~~~~ - -Report bugs at https://github.com/dmgav/xmidas/issues. - -If you are reporting a bug, please include: - -* Any details about your local setup that might be helpful in troubleshooting. -* Detailed steps to reproduce the bug. - -Fix Bugs -~~~~~~~~ - -Look through the GitHub issues for bugs. Anything tagged with "bug" -is open to whoever wants to implement it. - -Implement Features -~~~~~~~~~~~~~~~~~~ - -Look through the GitHub issues for features. Anything tagged with "feature" -is open to whoever wants to implement it. - -Write Documentation -~~~~~~~~~~~~~~~~~~~ - -XMidas could always use more documentation, whether -as part of the official XMidas docs, in docstrings, -or even on the web in blog posts, articles, and such. - -Submit Feedback -~~~~~~~~~~~~~~~ - -The best way to send feedback is to file an issue at https://github.com/dmgav/xmidas/issues. - -If you are proposing a feature: - -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that contributions - are welcome :) - -Get Started! ------------- - -Ready to contribute? Here's how to set up `xmidas` for local development. - -1. Fork the `xmidas` repo on GitHub. -2. Clone your fork locally:: - - $ git clone git@github.com:your_name_here/xmidas.git - -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - - $ mkvirtualenv xmidas - $ cd xmidas/ - $ python setup.py develop - -4. Create a branch for local development:: - - $ git checkout -b name-of-your-bugfix-or-feature - - Now you can make your changes locally. - -5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: - - $ flake8 xmidas tests - $ python setup.py test - $ tox - - To get flake8 and tox, just pip install them into your virtualenv. - -6. Commit your changes and push your branch to GitHub:: - - $ git add . - $ git commit -m "Your detailed description of your changes." - $ git push origin name-of-your-bugfix-or-feature - -7. Submit a pull request through the GitHub website. - -Pull Request Guidelines ------------------------ - -Before you submit a pull request, check that it meets these guidelines: - -1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work for Python 2.7, 3.3, 3.4, 3.5 and for PyPy. Check - https://travis-ci.org/dmgav/xmidas/pull_requests - and make sure that the tests pass for all supported Python versions. - +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/dmgav/xmidas/issues. + +If you are reporting a bug, please include: + +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "feature" +is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +XMidas could always use more documentation, whether +as part of the official XMidas docs, in docstrings, +or even on the web in blog posts, articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/dmgav/xmidas/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `xmidas` for local development. + +1. Fork the `xmidas` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/xmidas.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv xmidas + $ cd xmidas/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: + + $ flake8 xmidas tests + $ python setup.py test + $ tox + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 2.7, 3.3, 3.4, 3.5 and for PyPy. Check + https://travis-ci.org/dmgav/xmidas/pull_requests + and make sure that the tests pass for all supported Python versions. + diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index 6853d64..c81fcd7 --- a/LICENSE +++ b/LICENSE @@ -1,29 +1,29 @@ -BSD 3-Clause License - -Copyright (c) 2021, Brookhaven National Lab -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BSD 3-Clause License + +Copyright (c) 2021, Brookhaven National Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in old mode 100644 new mode 100755 index d822171..0f75c6a --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,18 +1,18 @@ -include AUTHORS.rst -include CONTRIBUTING.rst -include LICENSE -include README.rst -include requirements.txt - -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] - -recursive-include docs *.rst conf.py Makefile make.bat - -include versioneer.py -include xmidas/_version.py -recursive-include xmidas/uis * -recursive-include xmidas/css * - -# If including data files in the package, add them like: -# include path/to/data_file +include AUTHORS.rst +include CONTRIBUTING.rst +include LICENSE +include README.rst +include requirements.txt + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include docs *.rst conf.py Makefile make.bat + +include versioneer.py +include xmidas/_version.py +recursive-include xmidas/uis * +recursive-include xmidas/css * + +# If including data files in the package, add them like: +# include path/to/data_file diff --git a/README.rst b/README.rst old mode 100644 new mode 100755 index 61a74e1..625b9bd --- a/README.rst +++ b/README.rst @@ -1,12 +1,14 @@ -====================================================== -XMidas (X-Ray Multimodal Image Data Analysis Software) -====================================================== - -.. image:: https://img.shields.io/pypi/v/xmidas.svg - :target: https://pypi.python.org/pypi/xmidas - -Software for analysis of imaging and spectrosocpy data collected at NSLS-II, -Brookhaven National Laboratory - -* Free software: 3-clause BSD license -* Documentation: https://nsls-ii.github.io/xmidas/. +====================================================== +XMidas (X-Ray Multimodal Image Data Analysis Software) +====================================================== + +.. image:: https://img.shields.io/pypi/v/xmidas.svg + :target: https://pypi.python.org/pypi/xmidas + +Software for analysis of imaging and spectrosocpy data collected at NSLS-II, +Brookhaven National Laboratory + +* Free software: 3-clause BSD license +* Documentation: https://nsls-ii.github.io/xmidas/. + +* Citation: Ajith Pattammattel, Ryan Tappero, Dmitri Gavrilov, Hongqiao Zhang, Paul Aronstein, Henry Jay Forman, Peggy A O'Day, Hanfei Yan, Yong S Chu, Multimodal X-ray nano-spectromicroscopy analysis of chemically heterogeneous systems, Metallomics, Volume 14, Issue 10, October 2022, mfac078, https://doi.org/10.1093/mtomcs/mfac078 diff --git a/SourceInstallationGuide.md b/SourceInstallationGuide.md new file mode 100644 index 0000000..cc2a333 --- /dev/null +++ b/SourceInstallationGuide.md @@ -0,0 +1,96 @@ +# XMIDAS - Windows Installation Guide + +This guide will walk Windows users through installing and running the **XMIDAS** (X-ray Multimodal Image Data Analysis Software) application from source. + +--- + +## 📦 Requirements + +* [Miniconda or Anaconda (64-bit)](https://docs.conda.io/en/latest/miniconda.html) +* (Optional) [Git for Windows](https://git-scm.com/download/win) + +--- + +## 🧰 Step-by-Step Installation + +### 1. Clone or Download the Repository + +**Option A – With Git (recommended):** + +```bash +git clone https://github.com/pattammattel/xmidas.git +cd xmidas +``` + +**Option B – Without Git:** + +1. Visit: [https://github.com/pattammattel/xmidas](https://github.com/pattammattel/xmidas) +2. Click **Code → Download ZIP** +3. Extract ZIP +4. **Open Anaconda Prompt and navigate into the extracted `xmidas/` folder**: + + ```bash + cd path\to\extracted\xmidas + ``` + +🚨 **IMPORTANT:** You **must** be inside the `xmidas` folder before continuing! All commands in the next steps assume this location. + +--- + +### 2. Create a Conda Environment + +```bash +conda create -n xmidas-env python=3.12 +conda activate xmidas-env +``` + +--- + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +``` + + +--- + +### 4. Run XMIDAS + +```bash +python -m xmidas.main +``` + + +--- + +## 📌 Troubleshooting + +* ✅ **Double-check that you're in the `xmidas/` folder** before running anything. +* If you see `ModuleNotFoundError: No module named 'xmidas'`, you are probably in the wrong directory. +* If PyQt-related errors occur, ensure `PyQt6` is installed and compatible with Python 3.12. + +--- + +## 🗂 Optional: Create a Desktop Shortcut (Windows only) + +Inside `MiscFiles-FindProperLocation/`, you’ll find a helper script: + +```bash +python create_shortcut.py +``` + +This will generate a Windows `.bat` shortcut to launch the GUI. + +--- + +## 🔗 Resources + +* 📖 Documentation: See `docs/source/index.rst` +* 🐞 Issues / Bugs: [GitHub Issues](https://github.com/pattammattel/xmidas/issues) + +--- + +Happy analyzing! + +*– XMIDAS Development Team* diff --git a/docs/Makefile b/docs/Makefile old mode 100644 new mode 100755 index b4a5893..e792a8c --- a/docs/Makefile +++ b/docs/Makefile @@ -1,20 +1,20 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = "-W" # This flag turns warnings into errors. -SPHINXBUILD = sphinx-build -SPHINXPROJ = PackagingScientificPython -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = "-W" # This flag turns warnings into errors. +SPHINXBUILD = sphinx-build +SPHINXPROJ = PackagingScientificPython +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/installation/windows_install_xmidas.bat b/docs/installation/windows_install_xmidas.bat new file mode 100644 index 0000000..e90580f --- /dev/null +++ b/docs/installation/windows_install_xmidas.bat @@ -0,0 +1,38 @@ +@echo off +setlocal + +REM --- Clone repo if not already present --- +IF NOT EXIST xmidas ( + echo Cloning XMIDAS from GitHub... + git clone https://github.com/pattammattel/xmidas.git +) + +REM --- Change to xmidas directory --- +cd xmidas + +REM --- Confirm location --- +IF NOT EXIST requirements.txt ( + echo You are not in the xmidas folder. Exiting. + pause + exit /b +) + +REM --- Create Conda env if it doesn't exist --- +echo Creating Conda environment xmidas-env... +conda info --envs | findstr "xmidas-env" >nul +IF %ERRORLEVEL% NEQ 0 ( + conda create -n xmidas-env python=3.12 -y +) + +REM --- Activate the environment --- +call conda activate xmidas-env + +REM --- Install dependencies --- +echo Installing Python dependencies... +pip install -r requirements.txt + +REM --- Launch XMIDAS --- +echo Starting XMIDAS... +python -m xmidas.main + +pause diff --git a/docs/make.bat b/docs/make.bat old mode 100644 new mode 100755 index 2be8306..ac53d5b --- a/docs/make.bat +++ b/docs/make.bat @@ -1,36 +1,36 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=PackagingScientificPython - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=PackagingScientificPython + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/Loading_Data.rst b/docs/source/Loading_Data.rst old mode 100644 new mode 100755 index 6a261e2..363b7e7 --- a/docs/source/Loading_Data.rst +++ b/docs/source/Loading_Data.rst @@ -1,46 +1,46 @@ - -================ -Loading an Image -================ - -Supported Formats -***************** - -XRF Data --------- - -MIDAS can unpack fluorescence .h5 data from NSLS-II beamlines, HXN, XFM, SRX, and TES.XRF data will be normalized with the I0 (scalar) value on the file. - -2D-XANES Data -------------- - -2D and 3D tiff images (stacks). The list of energies has to be loaded as a separate tiff file (see load energy section below) - -To open the above data types use the "Open Image Data" option in the File menu and select the file. - -Load Energy ------------ - -You can load it using the following methods, - -1. file menu option 'Load Energy'. - -2. if you name the energy txt file matching the image file name, the program loads it automatically (like 'test.tiff' , 'test.txt'). - -3. In the folder you exported xanes data from the beamline there should be a file named 'maps_log_tiff.txt'. Copy this to the same folder as the image file, then Midas automatically load energy when you open the image. - - -Create an image stack ---------------------- - -You can also create a 3D image stack from a selection of single tiff files. To do so choose "Open Multiple Files" -options from the File menu. - -.. image:: screenshots/FileOpen.jpg - -Once loaded correctly you should see the image on the top panel and the spectrum on the bottom panel. In case of -incorrect formatting or unsupported formats, the bottom left corner of the program shows the error. - -.. image:: screenshots/Midas_view.jpg - -Also see the `Video Tutorial `_ + +================ +Loading an Image +================ + +Supported Formats +***************** + +XRF Data +-------- + +XMidas can unpack fluorescence .h5 data from NSLS-II beamlines- HXN, XFM, SRX, and TES.XRF data will be normalized with the I0 (scalar) value on the file. + +2D-XANES Data +------------- + +2D and 3D tiff images (stacks). The list of energies has to be loaded as a separate tiff file (see load energy section below) + +To open the above data types use the "Open Image Data" option in the File menu and select the file. + +Load Energy +----------- + +You can load it using the following methods, + +1. file menu option 'Load Energy'. + +2. if you name the energy txt file matching the image file name, the program loads it automatically (like 'test.tiff' , 'test.txt'). + +3. In the folder you exported xanes data from the beamline there should be a log file named 'maps_log_tiff.txt'. Copy this to the same folder as the image file, then XMidas automatically load energy when you open the image. + + +Create an image stack +--------------------- + +You can also create a 3D image stack from a selection of single tiff files. To do so choose "Open Multiple Files" +options from the File menu. + +.. image:: screenshots/FileOpen.jpg + +Once loaded correctly you should see the image on the top panel and the spectrum on the bottom panel. In case of +incorrect formatting or unsupported formats, the bottom left corner of the program shows the error. + +.. image:: screenshots/Midas_view.jpg + +Also see the `Video Tutorial `_ diff --git a/docs/source/Nano-XANES_Workflow.rst b/docs/source/Nano-XANES_Workflow.rst new file mode 100755 index 0000000..dfb730a --- /dev/null +++ b/docs/source/Nano-XANES_Workflow.rst @@ -0,0 +1,51 @@ + +================ +Nano-XANES Processing +================ + +In short +***************** +The aim is to generate multi-color chemical state maps from a XANES stack + +What data are required? +-------- + +1. A 3D XANES stack or set of 2D tiff images +2. List of energy points (.txt files) +3. Reference spectrum library as simple colum files or .nor file from Athena (preferred) +Note : Make sure the athena file headers (spectrun names) are not too long. Parsing seems to fail otherwise but this problem will be addressed late. + + +2D-XANES Data +------------- + +2D and 3D tiff images (stacks). The list of energies has to be loaded as a separate tiff file (see load energy section below) + +To open the above data types use the "Open Image Data" option in the File menu and select the file. + +Load Energy +----------- + +You can load it using the following methods, + +1. file menu option 'Load Energy'. + +2. if you name the energy txt file matching the image file name, the program loads it automatically (like 'test.tiff' , 'test.txt'). + +3. In the folder you exported xanes data from the beamline there should be a log file named 'maps_log_tiff.txt'. Copy this to the same folder as the image file, then XMidas automatically load energy when you open the image. + + +Create an image stack +--------------------- + +You can also create a 3D image stack from a selection of single tiff files. To do so choose "Open Multiple Files" +options from the File menu. + +.. image:: screenshots/FileOpen.jpg + +Once loaded correctly you should see the image on the top panel and the spectrum on the bottom panel. In case of +incorrect formatting or unsupported formats, the bottom left corner of the program shows the error. + +.. image:: screenshots/Midas_view.jpg + +Also see the `Video Tutorial `_ diff --git a/docs/source/_static/.placeholder b/docs/source/_static/.placeholder old mode 100644 new mode 100755 diff --git a/docs/source/conf.py b/docs/source/conf.py old mode 100644 new mode 100755 index abd9ff0..e36fb2d --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,201 +1,201 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# XMidas documentation build configuration file, created by -# sphinx-quickstart on Thu Jun 28 12:35:56 2018. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.githubpages', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'IPython.sphinxext.ipython_directive', - 'IPython.sphinxext.ipython_console_highlighting', - 'matplotlib.sphinxext.plot_directive', - 'numpydoc', -] - -# Configuration options for plot_directive. See: -# https://github.com/matplotlib/matplotlib/blob/f3ed922d935751e08494e5fb5311d3050a3b637b/lib/matplotlib/sphinxext/plot_directive.py#L81 -plot_html_show_source_link = False -plot_html_show_formats = False - -# Generate the API documentation when building -autosummary_generate = True -numpydoc_show_class_members = False - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'XMidas' -copyright = '2021, Brookhaven National Lab' -author = 'Brookhaven National Lab' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -import xmidas -# The short X.Y version. -version = xmidas.__version__ -# The full version, including alpha/beta/rc tags. -release = xmidas.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' -import sphinx_rtd_theme -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'xmidas' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'xmidas.tex', 'XMidas Documentation', - 'Contributors', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'xmidas', 'XMidas Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'xmidas', 'XMidas Documentation', - author, 'xmidas', 'Multimodal Image Data Analysis Software', - 'Miscellaneous'), -] - - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), - 'matplotlib': ('https://matplotlib.org/stable', None), -} +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# XMidas documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 28 12:35:56 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.githubpages', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'IPython.sphinxext.ipython_directive', + 'IPython.sphinxext.ipython_console_highlighting', + 'matplotlib.sphinxext.plot_directive', + 'numpydoc', +] + +# Configuration options for plot_directive. See: +# https://github.com/matplotlib/matplotlib/blob/f3ed922d935751e08494e5fb5311d3050a3b637b/lib/matplotlib/sphinxext/plot_directive.py#L81 +plot_html_show_source_link = False +plot_html_show_formats = False + +# Generate the API documentation when building +autosummary_generate = True +numpydoc_show_class_members = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'XMidas' +copyright = '2021, Brookhaven National Lab' +author = 'Brookhaven National Lab' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +import xmidas +# The short X.Y version. +version = xmidas.__version__ +# The full version, including alpha/beta/rc tags. +release = xmidas.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' +import sphinx_rtd_theme +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'xmidas' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'xmidas.tex', 'XMidas Documentation', + 'Contributors', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'xmidas', 'XMidas Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'xmidas', 'XMidas Documentation', + author, 'xmidas', 'Multimodal Image Data Analysis Software', + 'Miscellaneous'), +] + + + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), + 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), + 'matplotlib': ('https://matplotlib.org/stable', None), +} diff --git a/docs/source/index.rst b/docs/source/index.rst old mode 100644 new mode 100755 index 41b2dae..0a7d5c3 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,14 +1,14 @@ -.. Packaging Scientific Python documentation master file, created by - sphinx-quickstart on Thu Jun 28 12:35:56 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -XMidas Documentation -==================== - -.. toctree:: - :maxdepth: 2 - - installation - Loading_Data - release-history +.. Packaging Scientific Python documentation master file, created by + sphinx-quickstart on Thu Jun 28 12:35:56 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +XMidas Documentation +==================== + +.. toctree:: + :maxdepth: 2 + + installation + Loading_Data + release-history diff --git a/docs/source/installation.rst b/docs/source/installation.rst old mode 100644 new mode 100755 index 4f0f468..df7c283 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,39 +1,35 @@ -============ -Installation -============ - -Create and activate Conda environment:: - - $ conda create -n xmidas_env python=3.9 -c conda-forge - $ conda activate xmidas_env - -Installation from *conda-forge*:: - - $ conda install xmidas -c conda-forge - -Installation from *PyPI* in a new environment:: - - $ conda install xraylarch -c conda-forge - $ pip install xmidas[all] - -Installation into an environment that already has ``PyQt`` and ``opencv`` installed, -for example an environment that has *XMidas* installed from *conda-forge*:: - - $ conda install xraylarch -c conda-forge - $ pip install xmidas - -Development installation from source is similar. In the new Conda environment use :: - - $ conda install xraylarch -c conda-forge - $ pip install -e .[all] - -and if the environment has ``PyQt`` and ``opencv`` installed use :: - - $ conda install xraylarch -c conda-forge - $ pip install -e . - -Start the program by typing :: - - $ xmidas - -in the command line. +============ +Installation +============ + +Create and activate Conda environment:: + + $ conda create -n xmidas_env python=3.9 -c conda-forge + $ conda activate xmidas_env + +Installation from *conda-forge*:: + + $ conda install xmidas -c conda-forge + +Installation from *PyPI* in a new environment:: + + $ pip install xmidas[all] + +Installation into an environment that already has ``PyQt`` and ``opencv`` installed, +for example an environment that has *XMidas* installed from *conda-forge*:: + + $ pip install xmidas + +Development installation from source is similar. In the new Conda environment use :: + + $ pip install -e .[all] + +and if the environment has ``PyQt`` and ``opencv`` installed use :: + + $ pip install -e . + +Start the program by typing :: + + $ xmidas + +in the command line. diff --git a/docs/source/release-history.rst b/docs/source/release-history.rst old mode 100644 new mode 100755 index 53707fb..06610d3 --- a/docs/source/release-history.rst +++ b/docs/source/release-history.rst @@ -1,6 +1,6 @@ -=============== -Release History -=============== - -Initial Release (YYYY-MM-DD) ----------------------------- +=============== +Release History +=============== + +Initial Release (YYYY-MM-DD) +---------------------------- diff --git a/docs/source/screenshots/FileOpen.jpg b/docs/source/screenshots/FileOpen.jpg old mode 100644 new mode 100755 diff --git a/docs/source/screenshots/Midas_Preview.jpg b/docs/source/screenshots/Midas_Preview.jpg old mode 100644 new mode 100755 diff --git a/docs/source/screenshots/Midas_view.jpg b/docs/source/screenshots/Midas_view.jpg old mode 100644 new mode 100755 diff --git a/docs/source/screenshots/midas1.mp4 b/docs/source/screenshots/midas1.mp4 old mode 100644 new mode 100755 diff --git a/docs/source/screenshots/midas_structure.jpg b/docs/source/screenshots/midas_structure.jpg old mode 100644 new mode 100755 diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..d647cd2 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,1499 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brunsli-0.1-he0dfb12_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-blosc2-2.23.0-hf9886e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/charls-2.4.2-h13dd4ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.15.1-nompi_py312h4eecd6b_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_had3affe_106.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/imagecodecs-2026.1.14-py312h04e8b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jxrlib-1.1-h93a5062_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.18-hdfa7624_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.5-h8664d51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libavif16-1.3.0-hde9513d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-5_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.18.0-he38603e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-h55c6f16_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_17.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_17.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_17.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h913acd8_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-5_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.54-h132b30e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzopfli-1.0.3-h9f76cd9_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.4.2-py312he281c53_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjph-0.26.0-h2a4d681_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.0-py312hae6be28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.1.0-py312h4e908a4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.12-h18782d2_2_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rav1e-0.7.1-h0716509_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-image-0.26.0-np2py312ha921b1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.8.0-np2py312he5ca3e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.17.0-py312h0f234b1_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-4.0.0-h0cb729a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.1.28-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zfp-1.0.1-ha86207d_5.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/3f/f073a980969aa485ef288eb2e3b94c223ba9c7ac9941543f19b51659b98d/pyqt6-6.10.2-cp39-abi3-macosx_10_14_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ce/c8/d99e65ab01c2402fb6bc4f77abef7244f7d5fb2f2e6d5b0abdf71bb2e4fc/pyqt6_qt6-6.10.2-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/53/a6/0e4d8fa7d6deb750bd0fdf89024e39c71fb127efb5eeedfab6830ad6679a/pyqt6_sip-13.11.0-cp312-cp312-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/00/5d8ddccbddc28871f40b5c84b55e121dac836e5efa407c11d3a00002e85d/pystackreg-0.2.8-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + build_number: 7 + sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd + md5: a44032f282e7d2acdeb1c240308052dd + depends: + - llvm-openmp >=9.0.1 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 8325 + timestamp: 1764092507920 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda + sha256: ec238f18ce8140485645252351a0eca9ef4f7a1c568a420f240a585229bc12ef + md5: 7adba36492a1bb22d98ffffe4f6fc6de + depends: + - __osx >=11.0 + - libcxx >=16 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 2235747 + timestamp: 1718551382432 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + sha256: c3fe902114b9a3ac837e1a32408cc2142c147ec054c1038d37aec6814343f48a + md5: 925acfb50a750aa178f7a0aced77f351 + depends: + - __osx >=11.0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.1,<1.3.0a0 + - zstd >=1.5.6,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 33602 + timestamp: 1733513285902 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brunsli-0.1-he0dfb12_2.conda + sha256: f32d7c6285601ac3f6baf0715b225fd017702cf24260c6f7f14f7b6e721d92bc + md5: 4cfe5258439d88ce2ef0b159007ee067 + depends: + - __osx >=11.0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcxx >=19 + license: MIT + license_family: MIT + purls: [] + size: 141089 + timestamp: 1761759272675 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 + md5: 58fd217444c2a5701a44244faf518206 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 125061 + timestamp: 1757437486465 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 + md5: bcb3cba70cf1eec964a03b4ba7775f01 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 180327 + timestamp: 1765215064054 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-blosc2-2.23.0-hf9886e1_0.conda + sha256: 81c20499764e5fe801319c0e89bb473749497bd2327d68fbfeee5d13ece30486 + md5: d834074f69cd126ef3a86100480c21e2 + depends: + - __osx >=11.0 + - libcxx >=19 + - lz4-c >=1.10.0,<1.11.0a0 + - zlib-ng >=2.3.2,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 254753 + timestamp: 1769992339050 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 + md5: bddacf101bb4dd0e51811cb69c7790e2 + depends: + - __unix + license: ISC + purls: [] + size: 146519 + timestamp: 1767500828366 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + noarch: python + sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 + md5: 9b347a7ec10940d3f7941ff6c460b551 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4134 + timestamp: 1615209571450 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 + md5: 576d629e47797577ab0f1b351297ef4a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=hash-mapping + size: 11065 + timestamp: 1615209567874 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/charls-2.4.2-h13dd4ca_0.conda + sha256: b9f79954e6d37ad59016b434abfdd096a75ff08c6de14e5198bcea497a10fae5 + md5: 6faf3cf8df25572c7f70138a45f37363 + depends: + - libcxx >=15.0.7 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 116255 + timestamp: 1684263271583 +- pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + name: colorama + version: 0.4.6 + sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda + sha256: 93e077b880a85baec8227e8c72199220c7f87849ad32d02c14fb3807368260b8 + md5: 5a74cdee497e6b65173e10d94582fae6 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 316394 + timestamp: 1685695959391 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda + sha256: 843b3f364ff844137e37d5c0a181f11f6d51adcedd216f019d074e5aa5d7e09c + md5: 95fa1486c77505330c20f7202492b913 + license: MIT + license_family: MIT + purls: [] + size: 71613 + timestamp: 1712692611426 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.15.1-nompi_py312h4eecd6b_101.conda + sha256: 914d4f00a4d8cb86a70ce60241acc631a0e9d0cd939c0091b06de2d6cef51a3b + md5: 1f19a033f9c3f388c8f3d3c1643d6611 + depends: + - __osx >=11.0 + - cached-property + - hdf5 >=1.14.6,<1.14.7.0a0 + - numpy >=1.23,<3 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/h5py?source=hash-mapping + size: 1139768 + timestamp: 1764017732485 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_had3affe_106.conda + sha256: e91c2b8fe62d73bb56bdb9b5adcdcbedbd164ced288e0f361b8eb3f017ddcd7b + md5: 2d1270d283403c542680e969bea70355 + depends: + - __osx >=11.0 + - libaec >=1.1.5,<2.0a0 + - libcurl >=8.18.0,<9.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3299759 + timestamp: 1770390513189 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.2-h38cb7af_0.conda + sha256: d4cefbca587429d1192509edc52c88de52bc96c2447771ddc1f8bee928aed5ef + md5: 1e93aca311da0210e660d2247812fa02 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12358010 + timestamp: 1767970350308 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/imagecodecs-2026.1.14-py312h04e8b9b_1.conda + sha256: 92eb5f53637ec0a658cb1f6ed4bb21589255b05bcfeb9ceb625a484aa4c9e03e + md5: 74b6cccaa4457a6eaf84b28b3ba59391 + depends: + - __osx >=11.0 + - blosc >=1.21.6,<2.0a0 + - brunsli >=0.1,<1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - c-blosc2 >=2.23.0,<2.24.0a0 + - charls >=2.4.2,<2.5.0a0 + - giflib >=5.2.2,<5.3.0a0 + - jxrlib >=1.1,<1.2.0a0 + - lcms2 >=2.18,<3.0a0 + - lerc >=4.0.0,<5.0a0 + - libaec >=1.1.5,<2.0a0 + - libavif16 >=1.3.0,<2.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - libjxl >=0.11,<1.0a0 + - liblzma >=5.8.2,<6.0a0 + - libpng >=1.6.54,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - libzopfli >=1.0.3,<1.1.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - numpy >=1.23,<3 + - openjpeg >=2.5.4,<3.0a0 + - openjph >=0.26.0,<0.27.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - snappy >=1.2.2,<1.3.0a0 + - zfp >=1.0.1,<2.0a0 + - zlib-ng >=2.3.2,<2.4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/imagecodecs?source=hash-mapping + size: 1552322 + timestamp: 1770036954956 +- conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.0-pyhfb79c49_0.conda + sha256: 8ef69fa00c68fad34a3b7b260ea774fda9bd9274fd706d3baffb9519fd0063fe + md5: b5577bc2212219566578fd5af9993af6 + depends: + - numpy + - pillow >=8.3.2 + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/imageio?source=hash-mapping + size: 293226 + timestamp: 1738273949742 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 + md5: 63ccfdc3a3ce25b027b8767eb722fca8 + depends: + - python >=3.9 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=hash-mapping + size: 34641 + timestamp: 1747934053147 +- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda + sha256: 301539229d7be6420c084490b8145583291123f0ce6b92f56be5948a2c83a379 + md5: 615de2a4d97af50c350e5cf160149e77 + depends: + - python >=3.10 + - setuptools + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/joblib?source=hash-mapping + size: 226448 + timestamp: 1765794135253 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jxrlib-1.1-h93a5062_3.conda + sha256: c9e0d3cf9255d4585fa9b3d07ace3bd934fdc6a67ef4532e5507282eff2364ab + md5: 879997fd868f8e9e4c2a12aec8583799 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 197843 + timestamp: 1703334079437 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b + md5: c6dc8a0fdec13a0565936655c33069a1 + depends: + - __osx >=11.0 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1155530 + timestamp: 1719463474401 +- conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.4-pyhd8ed1ab_2.conda + sha256: d7ea986507090fff801604867ef8e79c8fda8ec21314ba27c032ab18df9c3411 + md5: d10d9393680734a8febc4b362a4c94f2 + depends: + - importlib-metadata + - packaging + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/lazy-loader?source=hash-mapping + size: 16298 + timestamp: 1733636905835 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.18-hdfa7624_0.conda + sha256: d768da024ab74a4b30642401877fa914a68bdc238667f16b1ec2e0e98b2451a6 + md5: 6631a7bd2335bb9699b1dbc234b19784 + depends: + - __osx >=11.0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: MIT + license_family: MIT + purls: [] + size: 211756 + timestamp: 1768184994800 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8 + md5: a74332d9b60b62905e3d30709df08bf1 + depends: + - __osx >=11.0 + - libcxx >=18 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 188306 + timestamp: 1745264362794 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.5-h8664d51_0.conda + sha256: af9cd8db11eb719e38a3340c88bb4882cf19b5b4237d93845224489fc2a13b46 + md5: 13e6d9ae0efbc9d2e9a01a91f4372b41 + depends: + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 30390 + timestamp: 1769222133373 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libavif16-1.3.0-hde9513d_3.conda + sha256: 92eb3f8134586972145378b21ac789a65ee232d7c1ef01ce5bfc9e850d0e0e60 + md5: 6e9bcb90cfd20178a52f355030aba237 + depends: + - __osx >=11.0 + - aom >=3.9.1,<3.10.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - rav1e >=0.7.1,<0.8.0a0 + - svt-av1 >=4.0.0,<4.0.1.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 111474 + timestamp: 1769477399074 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-5_h51639a9_openblas.conda + build_number: 5 + sha256: 620a6278f194dcabc7962277da6835b1e968e46ad0c8e757736255f5ddbfca8d + md5: bcc025e2bbaf8a92982d20863fe1fb69 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - libcblas 3.11.0 5*_openblas + - liblapack 3.11.0 5*_openblas + - liblapacke 3.11.0 5*_openblas + - blas 2.305 openblas + - mkl <2026 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18546 + timestamp: 1765819094137 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 + depends: + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 + license: MIT + license_family: MIT + purls: [] + size: 290754 + timestamp: 1764018009077 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-5_hb0561ab_openblas.conda + build_number: 5 + sha256: 38809c361bbd165ecf83f7f05fae9b791e1baa11e4447367f38ae1327f402fc0 + md5: efd8bd15ca56e9d01748a3beab8404eb + depends: + - libblas 3.11.0 5_h51639a9_openblas + constrains: + - liblapacke 3.11.0 5*_openblas + - liblapack 3.11.0 5*_openblas + - blas 2.305 openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18548 + timestamp: 1765819108956 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.18.0-he38603e_0.conda + sha256: 11c78b3e89bc332933386f0a11ac60d9200afb7a811b9e3bec98aef8d4a6389b + md5: 36190179a799f3aee3c2d20a8a2b970d + depends: + - __osx >=11.0 + - krb5 >=1.21.3,<1.22.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.4,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + purls: [] + size: 402681 + timestamp: 1767822693908 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.8-h55c6f16_2.conda + sha256: 5fbeb2fc2673f0455af6079abf93faaf27f11a92574ad51565fa1ecac9a4e2aa + md5: 4cb5878bdb9ebfa65b7cdff5445087c5 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 570068 + timestamp: 1770238262922 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + sha256: 5e0b6961be3304a5f027a8c00bd0967fc46ae162cffb7553ff45c70f51b8314c + md5: a6130c709305cd9828b4e1bd9ba0000c + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 55420 + timestamp: 1761980066242 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.3-haf25636_0.conda + sha256: fce22610ecc95e6d149e42a42fbc3cc9d9179bd4eb6232639a60f06e080eec98 + md5: b79875dbb5b1db9a4a22a4520f918e1a + depends: + - __osx >=11.0 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 67800 + timestamp: 1763549994166 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + sha256: 9de25a86066f078822d8dd95a83048d7dc2897d5d655c0e04a8a54fca13ef1ef + md5: f35fb38e89e2776994131fbf961fa44b + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 7810 + timestamp: 1757947168537 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + sha256: cc4aec4c490123c0f248c1acd1aeab592afb6a44b1536734e20937cda748f7cd + md5: 6d4ede03e2a8e20eb51f7f681d2a2550 + depends: + - __osx >=11.0 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 346703 + timestamp: 1757947166116 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_17.conda + sha256: 07ba27f2ef1ce444ce5c99d0f9590772fc5b58ba73c993477bfad74b17dfaa79 + md5: 65c07cee234440ae4d5d340fc4b2e69a + depends: + - _openmp_mutex + constrains: + - libgomp 15.2.0 17 + - libgcc-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 402928 + timestamp: 1770254186829 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_17.conda + sha256: 7b96f428cb932df8d7c1aa4e433ed29b779dd9571934afdf4f9093a85155a142 + md5: 45ba22eb5381fb602a45233d89ba27ae + depends: + - libgfortran5 15.2.0 hdae7583_17 + constrains: + - libgfortran-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 139757 + timestamp: 1770254394473 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_17.conda + sha256: 9c41ff08f61c953cee13fc3df3c6245741e5a71e453b2c094a6d55b0eeda3669 + md5: c6329d871fb3207e9657c384128f5488 + depends: + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 599374 + timestamp: 1770254196706 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda + sha256: 837fe775ba8ec9f08655bb924e28dba390d917423350333a75fd5eeac0776174 + md5: 6375717f5fcd756de929a06d0e40fab0 + depends: + - __osx >=11.0 + - libcxx >=19 + license: Apache-2.0 OR BSD-3-Clause + purls: [] + size: 581579 + timestamp: 1758894814983 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.2-hc919400_0.conda + sha256: 6c061c56058bb10374daaef50e81b39cf43e8aee21f0037022c0c39c4f31872f + md5: f0695fbecf1006f27f4395d64bd0c4b8 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 551197 + timestamp: 1762095054358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h913acd8_8.conda + sha256: cb3713aa91c9271e1992d2a7234447f6da84ec0d59a5cf2f92ba850f808becb9 + md5: c41ad4bd5cb936fd7662426753ff1784 + depends: + - libcxx >=19 + - __osx >=11.0 + - libhwy >=1.3.0,<1.4.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1030574 + timestamp: 1768822131848 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-5_hd9741b5_openblas.conda + build_number: 5 + sha256: 735a6e6f7d7da6f718b6690b7c0a8ae4815afb89138aa5793abe78128e951dbb + md5: ca9d752201b7fa1225bca036ee300f2b + depends: + - libblas 3.11.0 5_h51639a9_openblas + constrains: + - libcblas 3.11.0 5*_openblas + - blas 2.305 openblas + - liblapacke 3.11.0 5*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18551 + timestamp: 1765819121855 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + sha256: 7bfc7ffb2d6a9629357a70d4eadeadb6f88fa26ebc28f606b1c1e5e5ed99dc7e + md5: 009f0d956d7bfb00de86901d16e486c7 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 92242 + timestamp: 1768752982486 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d + md5: a4b4dd73c67df470d091312ab87bf6ae + depends: + - __osx >=11.0 + - c-ares >=1.34.5,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.2,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 575454 + timestamp: 1756835746393 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_ha158390_4.conda + sha256: ebbbc089b70bcde87c4121a083c724330f02a690fb9d7c6cd18c30f1b12504fa + md5: a6f6d3a31bb29e48d37ce65de54e2df0 + depends: + - __osx >=11.0 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4284132 + timestamp: 1768547079205 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.54-h132b30e_0.conda + sha256: 1c271c0ec73b69f7570c5da67d0e47ddf7ff079bc1ca2dfaccd267ea39314b06 + md5: 1b80fd1eecb98f1cb7de4239f5d7dc15 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 288910 + timestamp: 1768285694469 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.51.2-h1ae2325_0.conda + sha256: 6e9b9f269732cbc4698c7984aa5b9682c168e2a8d1e0406e1ff10091ca046167 + md5: 4b0bf313c53c3e89692f020fb55d5f2c + depends: + - __osx >=11.0 + - icu >=78.2,<79.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 909777 + timestamp: 1768148320535 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + sha256: 8bfe837221390ffc6f111ecca24fa12d4a6325da0c8d131333d63d6c37f27e0a + md5: b68e8f66b94b44aaa8de4583d3d4cc40 + depends: + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 279193 + timestamp: 1745608793272 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h4030677_1.conda + sha256: e9248077b3fa63db94caca42c8dbc6949c6f32f94d1cafad127f9005d9b1507f + md5: e2a72ab2fa54ecb6abab2b26cde93500 + depends: + - __osx >=11.0 + - lerc >=4.0.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 373892 + timestamp: 1762022345545 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd + md5: e5e7d467f80da752be17796b87fe6385 + depends: + - __osx >=11.0 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 294974 + timestamp: 1752159906788 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + sha256: bd3816218924b1e43b275863e21a3e13a5db4a6da74cca8e60bc3c213eb62f71 + md5: af523aae2eca6dfa1c8eec693f5b9a79 + depends: + - __osx >=11.0 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + size: 323658 + timestamp: 1727278733917 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzopfli-1.0.3-h9f76cd9_0.tar.bz2 + sha256: e3003b8efe551902dc60b21c81d7164b291b26b7862704421368d26ba5c10fa0 + md5: a0758d74f57741aa0d9ede13fd592e56 + depends: + - libcxx >=11.0.0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 147901 + timestamp: 1607309166373 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.8-h4a912ad_0.conda + sha256: 56bcd20a0a44ddd143b6ce605700fdf876bcf5c509adc50bf27e76673407a070 + md5: 206ad2df1b5550526e386087bef543c7 + depends: + - __osx >=11.0 + constrains: + - openmp 21.1.8|21.1.8.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + purls: [] + size: 285974 + timestamp: 1765964756583 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + sha256: 94d3e2a485dab8bdfdd4837880bde3dd0d701e2b97d6134b8806b7c8e69c8652 + md5: 01511afc6cc1909c5303cf31be17b44f + depends: + - __osx >=11.0 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 148824 + timestamp: 1733741047892 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + sha256: f6a82172afc50e54741f6f84527ef10424326611503c64e359e25a19a8e4c1c6 + md5: a2c1eeadae7a309daed9d62c96012a2b + depends: + - python >=3.11 + - python + constrains: + - numpy >=1.25 + - scipy >=1.11.2 + - matplotlib-base >=3.8 + - pandas >=2.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/networkx?source=compressed-mapping + size: 1587439 + timestamp: 1765215107045 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.4.2-py312he281c53_1.conda + sha256: 7fd2f1a33b244129dcc2163304d103a7062fc38f01fe13945c9ea95cef12b954 + md5: 4afbe6ffff0335d25f3c5cc78b1350a4 + depends: + - python + - libcxx >=19 + - __osx >=11.0 + - python 3.12.* *_cpython + - libblas >=3.9.0,<4.0a0 + - python_abi 3.12.* *_cp312 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 6840961 + timestamp: 1770098400654 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + sha256: dd73e8f1da7dd6a5494c5586b835cbe2ec68bace55610b1c4bf927400fe9c0d7 + md5: 6bf3d24692c157a41c01ce0bd17daeea + depends: + - __osx >=11.0 + - libcxx >=19 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 319967 + timestamp: 1758489514651 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjph-0.26.0-h2a4d681_0.conda + sha256: 151443d08ec9149c3547eb1b066bfee8d15af6e8cf1724f100a1efe96ffba8be + md5: 165b7d49b821d421d057dbb35897df32 + depends: + - libcxx >=19 + - __osx >=11.0 + - libtiff >=4.7.1,<4.8.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 181565 + timestamp: 1766578508827 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + sha256: 361f5c5e60052abc12bdd1b50d7a1a43e6a6653aab99a2263bf2288d709dcf67 + md5: f4f6ad63f98f64191c3e77c5f5f29d76 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3104268 + timestamp: 1769556384749 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.0-py312hae6be28_0.conda + sha256: e77a102f752d66a4afbda7ba4746689ec3083723d5d6fc7d5af8ddef25ff5655 + md5: 2b38a1d070dff7f0f92641a5fa130e23 + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - __osx >=11.0 + - python 3.12.* *_cpython + - libcxx >=19 + - numpy >=1.23,<3 + - python_abi 3.12.* *_cp312 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 13876050 + timestamp: 1769076491884 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.1.0-py312h4e908a4_0.conda + sha256: 8cf9e79ad0ba1206f716dd3f6ca9c48e2864882e0c514d1fe4dbfebe63f25ac0 + md5: d831c4844e7a04eab4aa91a2c26dbbdd + depends: + - python + - __osx >=11.0 + - python 3.12.* *_cpython + - zlib-ng >=2.3.2,<2.4.0a0 + - python_abi 3.12.* *_cp312 + - tk >=8.6.13,<8.7.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - openjpeg >=2.5.4,<3.0a0 + - lcms2 >=2.17,<3.0a0 + - libxcb >=1.17.0,<2.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - libjpeg-turbo >=3.1.2,<4.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + license: HPND + purls: + - pkg:pypi/pillow?source=hash-mapping + size: 953450 + timestamp: 1767353279678 +- pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl + name: platformdirs + version: 4.5.1 + sha256: d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 + requires_dist: + - furo>=2025.9.25 ; extra == 'docs' + - proselint>=0.14 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.2 ; extra == 'docs' + - sphinx>=8.2.3 ; extra == 'docs' + - appdirs==1.4.4 ; extra == 'test' + - covdefaults>=2.3 ; extra == 'test' + - pytest-cov>=7 ; extra == 'test' + - pytest-mock>=3.15.1 ; extra == 'test' + - pytest>=8.4.2 ; extra == 'test' + - mypy>=1.18.2 ; extra == 'type' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + sha256: 8ed65e17fbb0ca944bfb8093b60086e3f9dd678c3448b5de212017394c247ee3 + md5: 415816daf82e0b23a736a069a75e9da7 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 8381 + timestamp: 1726802424786 +- pypi: https://files.pythonhosted.org/packages/fb/3f/f073a980969aa485ef288eb2e3b94c223ba9c7ac9941543f19b51659b98d/pyqt6-6.10.2-cp39-abi3-macosx_10_14_universal2.whl + name: pyqt6 + version: 6.10.2 + sha256: 37ae7c1183fe4dd0c6aefd2006a35731245de1cb6f817bb9e414a3e4848dfd6d + requires_dist: + - pyqt6-sip>=13.8,<14 + - pyqt6-qt6>=6.10.0,<6.11.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ce/c8/d99e65ab01c2402fb6bc4f77abef7244f7d5fb2f2e6d5b0abdf71bb2e4fc/pyqt6_qt6-6.10.2-py3-none-macosx_11_0_arm64.whl + name: pyqt6-qt6 + version: 6.10.2 + sha256: 6dda853a8db1b8d1a2ddbbe76cc6c3aa86614cad14056bd3c0435d8feea73b2d +- pypi: https://files.pythonhosted.org/packages/53/a6/0e4d8fa7d6deb750bd0fdf89024e39c71fb127efb5eeedfab6830ad6679a/pyqt6_sip-13.11.0-cp312-cp312-macosx_10_9_universal2.whl + name: pyqt6-sip + version: 13.11.0 + sha256: 6b3267cd93b7f4da6fdf9a6a26f3baed8faae06e5cdd76235f2acc2116c40a54 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl + name: pyqtgraph + version: 0.14.0 + sha256: 7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5 + requires_dist: + - numpy>=1.25.0 + - colorama + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c1/00/5d8ddccbddc28871f40b5c84b55e121dac836e5efa407c11d3a00002e85d/pystackreg-0.2.8-cp312-cp312-macosx_11_0_arm64.whl + name: pystackreg + version: 0.2.8 + sha256: cc82203cde0a1909180d0ab7067071ac13d9558632f602ca72cce40415a3f339 + requires_dist: + - numpy + - tqdm +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.12-h18782d2_2_cpython.conda + build_number: 2 + sha256: 765e5d0f92dabc8c468d078a4409490e08181a6f9be6f5d5802a4e3131b9a69c + md5: e198b8f74b12292d138eb4eceb004fa3 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 12953358 + timestamp: 1769472376612 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + build_number: 8 + sha256: 80677180dd3c22deb7426ca89d6203f1c7f1f256f2d5a94dc210f6e758229809 + md5: c3efd25ac4d74b1584d2f7a57195ddf1 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6958 + timestamp: 1752805918820 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rav1e-0.7.1-h0716509_3.conda + sha256: 65f862b2b31ef2b557990a82015cbd41e5a66041c2f79b4451dd14b4595d4c04 + md5: 7b37f30516100b86ea522350c8cab44c + depends: + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 856271 + timestamp: 1746622200646 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-image-0.26.0-np2py312ha921b1d_0.conda + sha256: 87f56777c91af0849bbcfc0f9f0d53cbf8eb651151975782fd4f744b7ccd1739 + md5: c582406829218aa701e9ce61aa57973e + depends: + - imageio >=2.33,!=2.35.0 + - lazy-loader >=0.4 + - networkx >=3.0 + - numpy >=1.24 + - packaging >=21.0 + - pillow >=10.1 + - python + - scipy >=1.11.4 + - tifffile >=2022.8.12 + - __osx >=11.0 + - libcxx >=19 + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 + - numpy >=1.23,<3 + constrains: + - astropy-base >=6.0 + - dask-core >=2023.2.0,!=2024.8.0 + - matplotlib-base >=3.7 + - pooch >=1.6.0 + - pyamg >=5.2 + - pywavelets >=1.6 + - scikit-learn >=1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scikit-image?source=hash-mapping + size: 17963697 + timestamp: 1766684388162 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.8.0-np2py312he5ca3e3_1.conda + sha256: 5f640a06e001666f9d4dca7cca992f1753e722e9f6e50899d7d250c02ddf7398 + md5: ed7887c51edfa304c69a424279cec675 + depends: + - python + - numpy >=1.24.1 + - scipy >=1.10.0 + - joblib >=1.3.0 + - threadpoolctl >=3.2.0 + - libcxx >=19 + - python 3.12.* *_cpython + - __osx >=11.0 + - llvm-openmp >=19.1.7 + - numpy >=1.23,<3 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scikit-learn?source=hash-mapping + size: 9124177 + timestamp: 1766550900752 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.17.0-py312h0f234b1_1.conda + sha256: a204b9b3a59a88a320d9da772eecda58242cfaaf785119927eb59c4bdc6fa66f + md5: 1f5a9253e1c3484a5c1df0b8145a9ce3 + depends: + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.7 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 13802410 + timestamp: 1768801119235 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda + sha256: fd7201e38e38bf7f25818d624ca8da97b8998957ca9ae3fb7fdc9c17e6b25fcd + md5: 1d00d46c634177fc8ede8b99d6089239 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=compressed-mapping + size: 637506 + timestamp: 1770634745653 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + sha256: cb9305ede19584115f43baecdf09a3866bfcd5bcca0d9e527bd76d9a1dbe2d8d + md5: fca4a2222994acd7f691e57f94b750c5 + depends: + - libcxx >=19 + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 38883 + timestamp: 1762948066818 +- pypi: https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.46 + sha256: 3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-4.0.0-h0cb729a_0.conda + sha256: f7fc5a18f4a6cf61fa4b6697c6aa28426b07a3df2b53a96b25dda28641991532 + md5: 3dc1d4f4c9829c82c7f6660878abcd32 + depends: + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 1455052 + timestamp: 1769280806030 +- conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda + sha256: 6016672e0e72c4cf23c0cf7b1986283bd86a9c17e8d319212d78d8e9ae42fdfd + md5: 9d64911b31d57ca443e9f1e36b04385f + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/threadpoolctl?source=hash-mapping + size: 23869 + timestamp: 1741878358548 +- conda: https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.1.28-pyhd8ed1ab_0.conda + sha256: efd8a9d2b57678e5efb41cc261dace3341ddd877ac514bfccab579f437efd826 + md5: e8d6529858171ac4babb28c334306c0d + depends: + - imagecodecs >=2025.11.11 + - numpy >=1.19.2 + - python >=3.11 + constrains: + - matplotlib-base >=3.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/tifffile?source=compressed-mapping + size: 183339 + timestamp: 1769690244546 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 +- pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + name: tqdm + version: 4.67.3 + sha256: ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf + requires_dist: + - colorama ; sys_platform == 'win32' + - importlib-metadata ; python_full_version < '3.8' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-asyncio>=0.24 ; extra == 'dev' + - nbval ; extra == 'dev' + - requests ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + name: typing-extensions + version: 4.15.0 + sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + sha256: adae11db0f66f86156569415ed79cda75b2dbf4bea48d1577831db701438164f + md5: 78b548eed8227a689f93775d5d23ae09 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 14105 + timestamp: 1762976976084 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + sha256: f7fa0de519d8da589995a1fe78ef74556bb8bc4172079ae3a8d20c3c81354906 + md5: 9d1299ace1924aa8f4e0bc8e71dd0cf7 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 19156 + timestamp: 1762977035194 +- pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + name: xraydb + version: 4.5.8 + sha256: 2215baafa6a03d00d0254a94525aafc6493c8c285e4ac4477fbd6271b25e6a51 + requires_dist: + - numpy>=1.19 + - scipy>=1.6 + - sqlalchemy>=2.0.1 + - platformdirs + - build ; extra == 'dev' + - twine ; extra == 'dev' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - xraydb[dev,doc,test] ; extra == 'all' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zfp-1.0.1-ha86207d_5.conda + sha256: 5b8bc86ca206f456ca9fe9e1a629f68b949ac47070211bccf4b44d29141c85d7 + md5: 581bd74656ccd460cf2bbe152292a1eb + depends: + - __osx >=11.0 + - libcxx >=19 + - llvm-openmp >=19.1.7 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 204043 + timestamp: 1766549790975 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + sha256: b4533f7d9efc976511a73ef7d4a2473406d7f4c750884be8e8620b0ce70f4dae + md5: 30cd29cb87d819caead4d55184c1d115 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=compressed-mapping + size: 24194 + timestamp: 1764460141901 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda + sha256: a339606a6b224bb230ff3d711e801934f3b3844271df9720165e0353716580d4 + md5: d99c2a23a31b0172e90f456f580b695e + depends: + - __osx >=11.0 + - libcxx >=19 + license: Zlib + license_family: Other + purls: [] + size: 94375 + timestamp: 1770168363685 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 433413 + timestamp: 1764777166076 diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..c77e500 --- /dev/null +++ b/pixi.toml @@ -0,0 +1,25 @@ +[workspace] +authors = ["Ajith Pattammattel "] +channels = ["conda-forge"] +name = "xmidas" +platforms = ["osx-arm64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = "3.12.*" +h5py = ">=3.15.1,<4" +numpy = ">=2.4.2,<3" +pandas = ">=3.0.0,<4" +scikit-image = ">=0.26.0,<0.27" +scikit-learn = ">=1.8.0,<2" +scipy = ">=1.17.0,<2" +tifffile = ">=2026.1.28,<2027" +packaging = ">=26.0,<27" + +[pypi-dependencies] +pyqt6 = ">=6.10.2, <7" +pystackreg = ">=0.2.8, <0.3" +xraydb = ">=4.5.8, <5" +pyqtgraph = ">=0.14.0, <0.15" diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index 56e0975..61323cb --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,21 @@ -[tool.black] -line-length = 115 -include = '\.pyi?$' -exclude = ''' -( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | \.pytest_cache - | _build - | buck-out - | build - | dist - | docs - | xmidas/uis - | versioneer.py -) -''' +[tool.black] +line-length = 115 +include = '\.pyi?$' +exclude = ''' +( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.pytest_cache + | _build + | buck-out + | build + | dist + | docs + | xmidas/uis + | versioneer.py +) +''' diff --git a/requirements-dev.txt b/requirements-dev.txt old mode 100644 new mode 100755 index 55894e2..355f546 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,17 +1,17 @@ -# These are required for developing the package (running the tests, building -# the documentation) but not necessarily required for _using_ it. -codecov -coverage -flake8 -pytest -sphinx -twine -pre-commit -black -nbstripout -# These are dependencies of various sphinx extensions for documentation. -ipython -matplotlib -numpydoc -sphinx-copybutton -sphinx_rtd_theme +# These are required for developing the package (running the tests, building +# the documentation) but not necessarily required for _using_ it. +codecov +coverage +flake8 +pytest +sphinx +twine +pre-commit +black +nbstripout +# These are dependencies of various sphinx extensions for documentation. +ipython +matplotlib +numpydoc +sphinx-copybutton +sphinx_rtd_theme diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index fa5c198..45351af --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -# List required packages in this file, one per line. -h5py -numpy -pandas -pyqtgraph>=0.12.2 -pystackreg -scikit-image -scikit-learn -scipy>=1.7 -tifffile -silx -xraylarch>=0.9.57 -packaging +# List required packages in this file, one per line. +h5py +numpy +pandas +pyqtgraph +pystackreg +scikit-image +scikit-learn +scipy +tifffile +packaging +pyqt6 +xraydb diff --git a/setup.cfg b/setup.cfg old mode 100644 new mode 100755 index e601645..b6c1992 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ -[versioneer] -VCS = git -style = pep440-post -versionfile_source = xmidas/_version.py -versionfile_build = xmidas/_version.py -tag_prefix = v +[versioneer] +VCS = git +style = pep440-post +versionfile_source = xmidas/_version.py +versionfile_build = xmidas/_version.py +tag_prefix = v diff --git a/versioneer.py b/versioneer.py old mode 100644 new mode 100755 index 64fea1c..7fcc43f --- a/versioneer.py +++ b/versioneer.py @@ -1,1822 +1,1822 @@ - -# Version: 0.18 - -"""The Versioneer - like a rocketeer, but for versions. - -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer -* Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -See [INSTALL.md](./INSTALL.md) for detailed installation instructions. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the - commit date in ISO 8601 format. This will be None if the date is not - available. - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See [details.md](details.md) in the Versioneer -source tree for descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Known Limitations - -Some situations are known to cause problems for Versioneer. This details the -most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). - -### Subprojects - -Versioneer has limited support for source trees in which `setup.py` is not in -the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are -two common reasons why `setup.py` might not be in the root: - -* Source trees which contain multiple subprojects, such as - [Buildbot](https://github.com/buildbot/buildbot), which contains both - "master" and "slave" subprojects, each with their own `setup.py`, - `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI - distributions (and upload multiple independently-installable tarballs). -* Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. - -Versioneer will look for `.git` in parent directories, and most operations -should get the right version string. However `pip` and `setuptools` have bugs -and implementation details which frequently cause `pip install .` from a -subproject directory to fail to find a correct version string (so it usually -defaults to `0+unknown`). - -`pip install --editable .` should work correctly. `setup.py install` might -work too. - -Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in -some later version. - -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking -this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the -issue from the Versioneer side in more detail. -[pip PR#3176](https://github.com/pypa/pip/pull/3176) and -[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve -pip to let Versioneer work correctly. - -Versioneer-0.16 and earlier only looked for a `.git` directory next to the -`setup.cfg`, so subprojects were completely unsupported with those releases. - -### Editable installs with setuptools <= 18.5 - -`setup.py develop` and `pip install --editable .` allow you to install a -project into a virtualenv once, then continue editing the source code (and -test) without re-installing after every change. - -"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a -convenient way to specify executable scripts that should be installed along -with the python package. - -These both work as expected when using modern setuptools. When using -setuptools-18.5 or earlier, however, certain operations will cause -`pkg_resources.DistributionNotFound` errors when running the entrypoint -script, which must be resolved by re-installing the package. This happens -when the install happens with one version, then the egg_info data is -regenerated while a different version is checked out. Many setup.py commands -cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into -a different virtualenv), so this can be surprising. - -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes -this one, but upgrading to a newer version of setuptools should probably -resolve it. - -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - - -## License - -To make Versioneer easier to embed, all its code is dedicated to the public -domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . - -""" - -from __future__ import print_function -try: - import configparser -except ImportError: - import ConfigParser as configparser -import errno -import json -import os -import re -import subprocess -import sys - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_root(): - """Get the project root directory. - - We require that all commands are run from the project root, i.e. the - directory that contains setup.py, setup.cfg, and versioneer.py . - """ - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) - vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root): - """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None - cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): - cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -LONG_VERSION_PY['git'] = ''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %%s but none started with prefix %%s" %% - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs - tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %%s not under git control" %% root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def do_vcs_install(manifest_in, versionfile_source, ipy): - """Git-specific installation logic for Versioneer. - - For Git, this means creating/changing .gitattributes to mark _version.py - for export-subst keyword substitution. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] - if ipy: - files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: - pass - if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename): - """Try to determine the version from _version.py if present.""" - try: - with open(filename) as f: - contents = f.read() - except EnvironmentError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename, versions): - """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -class VersioneerBadRootError(Exception): - """The project root directory is unknown or missing key files.""" - - -def get_versions(verbose=False): - """Get the project version from whatever source is available. - - Returns dict with two keys: 'version' and 'full'. - """ - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} - - -def get_version(): - """Get the short version string for this project.""" - return get_versions()["version"] - - -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 - - cmds = {} - - # we add "version" to both distutils and setuptools - from distutils.core import Command - - class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in both distutils and setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - # pip install: - # copies source tree to a tempdir before running egg_info/etc - # if .git isn't copied too, 'git describe' will fail - # then does setup.py bdist_wheel, or sometimes setup.py install - # setup.py egg_info -> ? - - # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py - else: - from distutils.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - # nczeczulin reports that py2exe won't like the pep440-style string - # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. - # setup(console=[{ - # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION - # "product_version": versioneer.get_version(), - # ... - - class cmd_build_exe(_build_exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - if 'py2exe' in sys.modules: # py2exe enabled? - try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 - except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 - - class cmd_py2exe(_py2exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _py2exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["py2exe"] = cmd_py2exe - - # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist - else: - from distutils.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self): - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -INIT_PY_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - - -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" - root = get_root() - try: - cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except EnvironmentError: - old = "" - if INIT_PY_SNIPPET not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-subst keyword - # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) - return 0 - - -def scan_setup_py(): - """Validate the contents of setup.py against Versioneer's expectations.""" - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) + +# Version: 0.19 + +"""The Versioneer - like a rocketeer, but for versions. + +The Versioneer +============== + +* like a rocketeer, but for versions! +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy +* [![Latest Version] +(https://pypip.in/version/versioneer/badge.svg?style=flat) +](https://pypi.python.org/pypi/versioneer/) +* [![Build Status] +(https://travis-ci.org/warner/python-versioneer.png?branch=master) +](https://travis-ci.org/warner/python-versioneer) + +This is a tool for managing a recorded version number in distutils-based +python projects. The goal is to remove the tedious and error-prone "update +the embedded version string" step from your release process. Making a new +release should be as easy as recording a new tag in your version-control +system, and maybe making new tarballs. + + +## Quick Install + +* `pip install versioneer` to somewhere to your $PATH +* add a `[versioneer]` section to your setup.cfg (see below) +* run `versioneer install` in your source tree, commit the results + +## Version Identifiers + +Source trees come from a variety of places: + +* a version-control system checkout (mostly used by developers) +* a nightly tarball, produced by build automation +* a snapshot tarball, produced by a web-based VCS browser, like github's + "tarball from tag" feature +* a release tarball, produced by "setup.py sdist", distributed through PyPI + +Within each source tree, the version identifier (either a string or a number, +this tool is format-agnostic) can come from a variety of places: + +* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows + about recent "tags" and an absolute revision-id +* the name of the directory into which the tarball was unpacked +* an expanded VCS keyword ($Id$, etc) +* a `_version.py` created by some earlier build step + +For released software, the version identifier is closely related to a VCS +tag. Some projects use tag names that include more than just the version +string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool +needs to strip the tag prefix to extract the version identifier. For +unreleased software (between tags), the version identifier should provide +enough information to help developers recreate the same tree, while also +giving them an idea of roughly how old the tree is (after version 1.2, before +version 1.3). Many VCS systems can report a description that captures this, +for example `git describe --tags --dirty --always` reports things like +"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the +0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has +uncommitted changes. + +The version identifier is used for multiple purposes: + +* to allow the module to self-identify its version: `myproject.__version__` +* to choose a name and prefix for a 'setup.py sdist' tarball + +## Theory of Operation + +Versioneer works by adding a special `_version.py` file into your source +tree, where your `__init__.py` can import it. This `_version.py` knows how to +dynamically ask the VCS tool for version information at import time. + +`_version.py` also contains `$Revision$` markers, and the installation +process marks `_version.py` to have this marker rewritten with a tag name +during the `git archive` command. As a result, generated tarballs will +contain enough information to get the proper version. + +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. + +## Installation + +See [INSTALL.md](./INSTALL.md) for detailed installation instructions. + +## Version-String Flavors + +Code which uses Versioneer can learn about its version string at runtime by +importing `_version` from your main `__init__.py` file and running the +`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can +import the top-level `versioneer.py` and run `get_versions()`. + +Both functions return a dictionary with different flavors of version +information: + +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the + commit date in ISO 8601 format. This will be None if the date is not + available. + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the +developers). `version` is suitable for display in an "about" box or a CLI +`--version` output: it can be easily compared against release notes and lists +of bugs fixed in various releases. + +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: + + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions + +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See [details.md](details.md) in the Versioneer +source tree for descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + +## Known Limitations + +Some situations are known to cause problems for Versioneer. This details the +most significant ones. More can be found on Github +[issues page](https://github.com/warner/python-versioneer/issues). + +### Subprojects + +Versioneer has limited support for source trees in which `setup.py` is not in +the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are +two common reasons why `setup.py` might not be in the root: + +* Source trees which contain multiple subprojects, such as + [Buildbot](https://github.com/buildbot/buildbot), which contains both + "master" and "slave" subprojects, each with their own `setup.py`, + `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI + distributions (and upload multiple independently-installable tarballs). +* Source trees whose main purpose is to contain a C library, but which also + provide bindings to Python (and perhaps other langauges) in subdirectories. + +Versioneer will look for `.git` in parent directories, and most operations +should get the right version string. However `pip` and `setuptools` have bugs +and implementation details which frequently cause `pip install .` from a +subproject directory to fail to find a correct version string (so it usually +defaults to `0+unknown`). + +`pip install --editable .` should work correctly. `setup.py install` might +work too. + +Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in +some later version. + +[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +this issue. The discussion in +[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +issue from the Versioneer side in more detail. +[pip PR#3176](https://github.com/pypa/pip/pull/3176) and +[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve +pip to let Versioneer work correctly. + +Versioneer-0.16 and earlier only looked for a `.git` directory next to the +`setup.cfg`, so subprojects were completely unsupported with those releases. + +### Editable installs with setuptools <= 18.5 + +`setup.py develop` and `pip install --editable .` allow you to install a +project into a virtualenv once, then continue editing the source code (and +test) without re-installing after every change. + +"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a +convenient way to specify executable scripts that should be installed along +with the python package. + +These both work as expected when using modern setuptools. When using +setuptools-18.5 or earlier, however, certain operations will cause +`pkg_resources.DistributionNotFound` errors when running the entrypoint +script, which must be resolved by re-installing the package. This happens +when the install happens with one version, then the egg_info data is +regenerated while a different version is checked out. Many setup.py commands +cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into +a different virtualenv), so this can be surprising. + +[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +this one, but upgrading to a newer version of setuptools should probably +resolve it. + +### Unicode version strings + +While Versioneer works (and is continually tested) with both Python 2 and +Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. +Newer releases probably generate unicode version strings on py2. It's not +clear that this is wrong, but it may be surprising for applications when then +write these strings to a network connection or include them in bytes-oriented +APIs like cryptographic checksums. + +[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates +this question. + + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` +* commit any changed files + +## Future Directions + +This tool is designed to make it easily extended to other version-control +systems: all VCS-specific components are in separate directories like +src/git/ . The top-level `versioneer.py` script is assembled from these +components by running make-versioneer.py . In the future, make-versioneer.py +will take a VCS name as an argument, and will construct a version of +`versioneer.py` that is specific to the given VCS. It might also take the +configuration arguments that are currently provided manually during +installation by editing setup.py . Alternatively, it might go the other +direction and include code from all supported VCS systems, reducing the +number of intermediate scripts. + + +## License + +To make Versioneer easier to embed, all its code is dedicated to the public +domain. The `_version.py` that it creates is also in the public domain. +Specifically, both are released under the Creative Commons "Public Domain +Dedication" license (CC0-1.0), as described in +https://creativecommons.org/publicdomain/zero/1.0/ . + +""" + +from __future__ import print_function +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import json +import os +import re +import subprocess +import sys + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_root(): + """Get the project root directory. + + We require that all commands are run from the project root, i.e. the + directory that contains setup.py, setup.cfg, and versioneer.py . + """ + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + me = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(me)[0]) + vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) + if me_dir != vsr_dir: + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root): + """Read the project setup.cfg file to determine Versioneer config.""" + # This might raise EnvironmentError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + setup_cfg = os.path.join(root, "setup.cfg") + parser = configparser.SafeConfigParser() + with open(setup_cfg, "r") as f: + parser.readfp(f) + VCS = parser.get("versioneer", "VCS") # mandatory + + def get(parser, name): + if parser.has_option("versioneer", name): + return parser.get("versioneer", name) + return None + cfg = VersioneerConfig() + cfg.VCS = VCS + cfg.style = get(parser, "style") or "" + cfg.versionfile_source = get(parser, "versionfile_source") + cfg.versionfile_build = get(parser, "versionfile_build") + cfg.tag_prefix = get(parser, "tag_prefix") + if cfg.tag_prefix in ("''", '""'): + cfg.tag_prefix = "" + cfg.parentdir_prefix = get(parser, "parentdir_prefix") + cfg.verbose = get(parser, "verbose") + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +LONG_VERSION_PY['git'] = ''' +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.18 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %%s" %% dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %%s" %% (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% dispcmd) + print("stdout was %%s" %% stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %%s but none started with prefix %%s" %% + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs - tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %%s not under git control" %% root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%%s*" %% tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%%d" %% pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} +''' + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def do_vcs_install(manifest_in, versionfile_source, ipy): + """Git-specific installation logic for Versioneer. + + For Git, this means creating/changing .gitattributes to mark _version.py + for export-subst keyword substitution. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + files = [manifest_in, versionfile_source] + if ipy: + files.append(ipy) + try: + me = __file__ + if me.endswith(".pyc") or me.endswith(".pyo"): + me = os.path.splitext(me)[0] + ".py" + versioneer_file = os.path.relpath(me) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + files.append(".gitattributes") + run_command(GITS, ["add", "--"] + files) + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.18) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +%s +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) +""" + + +def versions_from_file(filename): + """Try to determine the version from _version.py if present.""" + try: + with open(filename) as f: + contents = f.read() + except EnvironmentError: + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + + +def write_to_version_file(filename, versions): + """Write the given version number to the given _version.py file.""" + os.unlink(filename) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + + print("set %s to '%s'" % (filename, versions["version"])) + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +class VersioneerBadRootError(Exception): + """The project root directory is unknown or missing key files.""" + + +def get_versions(verbose=False): + """Get the project version from whatever source is available. + + Returns dict with two keys: 'version' and 'full'. + """ + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + + root = get_root() + cfg = get_config_from_root(root) + + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or cfg.verbose + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) + return ver + except NotThisMethod: + pass + + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) + return ver + except NotThisMethod: + pass + + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass + + if verbose: + print("unable to compute version") + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} + + +def get_version(): + """Get the short version string for this project.""" + return get_versions()["version"] + + +def get_cmdclass(): + """Get the custom setuptools/distutils subclasses used by Versioneer.""" + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/warner/python-versioneer/issues/52 + + cmds = {} + + # we add "version" to both distutils and setuptools + from distutils.core import Command + + class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + print(" date: %s" % vers.get("date")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in both distutils and setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + # pip install: + # copies source tree to a tempdir before running egg_info/etc + # if .git isn't copied too, 'git describe' will fail + # then does setup.py bdist_wheel, or sometimes setup.py install + # setup.py egg_info -> ? + + # we override different "build_py" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.build_py import build_py as _build_py + else: + from distutils.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string + # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. + # setup(console=[{ + # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION + # "product_version": versioneer.get_version(), + # ... + + class cmd_build_exe(_build_exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + except ImportError: + from py2exe.build_exe import py2exe as _py2exe # py2 + + class cmd_py2exe(_py2exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _py2exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["py2exe"] = cmd_py2exe + + # we override different "sdist" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.sdist import sdist as _sdist + else: + from distutils.command.sdist import sdist as _sdist + + class cmd_sdist(_sdist): + def run(self): + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + + +def do_setup(): + """Main VCS-independent setup function for installing Versioneer.""" + root = get_root() + try: + cfg = get_config_from_root(root) + except (EnvironmentError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + if os.path.exists(ipy): + try: + with open(ipy, "r") as f: + old = f.read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + with open(ipy, "a") as f: + f.write(INIT_PY_SNIPPET) + else: + print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + ipy = None + + # Make sure both the top-level "versioneer.py" and versionfile_source + # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so + # they'll be copied into source distributions. Pip won't be able to + # install the package without this. + manifest_in = os.path.join(root, "MANIFEST.in") + simple_includes = set() + try: + with open(manifest_in, "r") as f: + for line in f: + if line.startswith("include "): + for include in line.split()[1:]: + simple_includes.add(include) + except EnvironmentError: + pass + # That doesn't cover everything MANIFEST.in can do + # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # it might give some false negatives. Appending redundant 'include' + # lines is safe, though. + if "versioneer.py" not in simple_includes: + print(" appending 'versioneer.py' to MANIFEST.in") + with open(manifest_in, "a") as f: + f.write("include versioneer.py\n") + else: + print(" 'versioneer.py' already in MANIFEST.in") + if cfg.versionfile_source not in simple_includes: + print(" appending versionfile_source ('%s') to MANIFEST.in" % + cfg.versionfile_source) + with open(manifest_in, "a") as f: + f.write("include %s\n" % cfg.versionfile_source) + else: + print(" versionfile_source already in MANIFEST.in") + + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-subst keyword + # substitution. + do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + return 0 + + +def scan_setup_py(): + """Validate the contents of setup.py against Versioneer's expectations.""" + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + errors = do_setup() + errors += scan_setup_py() + if errors: + sys.exit(1) diff --git a/xmidas/__init__.py b/xmidas/__init__.py old mode 100644 new mode 100755 index 80edaf0..5c58ab0 --- a/xmidas/__init__.py +++ b/xmidas/__init__.py @@ -1,4 +1,3 @@ -from ._version import get_versions +# xmidas/__init__.py +__version__ = "1.0.0" -__version__ = get_versions()["version"] -del get_versions diff --git a/xmidas/_version.py b/xmidas/_version.py deleted file mode 100644 index 00d015b..0000000 --- a/xmidas/_version.py +++ /dev/null @@ -1,538 +0,0 @@ -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440-post" - cfg.tag_prefix = "v" - cfg.parentdir_prefix = "None" - cfg.versionfile_source = "xmidas/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, - cwd=cwd, - env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr else None), - ) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return { - "version": dirname[len(parentdir_prefix) :], - "full-revisionid": None, - "dirty": False, - "error": None, - "date": None, - } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] - if verbose: - print("picking %s" % r) - return { - "version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": None, - "date": date, - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return { - "version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": "no suitable tags", - "date": None, - } - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( - GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root - ) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[: git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return { - "version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None, - } - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return { - "version": rendered, - "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], - "error": None, - "date": pieces.get("date"), - } - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split("/"): - root = os.path.dirname(root) - except NameError: - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None, - } - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", - "date": None, - } diff --git a/xmidas/uis/ClusterView.ui b/xmidas/gui/layout/ClusterView.ui similarity index 96% rename from xmidas/uis/ClusterView.ui rename to xmidas/gui/layout/ClusterView.ui index 6f5fa2e..0fea754 100644 --- a/xmidas/uis/ClusterView.ui +++ b/xmidas/gui/layout/ClusterView.ui @@ -1,266 +1,266 @@ - - - MainWindow - - - - 0 - 0 - 979 - 815 - - - - - 0 - 0 - - - - MainWindow - - - background-color: rgb(240, 240, 240); -font: 10pt "Segoe UI"; - - - - QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -} - - - - - - - - - - - - - Composite Map - - - Qt::AlignCenter - - - - - - - Opan as RGBCMY Image - - - - - - - - - - - - - - - - - Nth Cluster - - - Qt::AlignCenter - - - - - - - - - - - - - 0 - 0 - - - - 0 - - - 20 - - - 1 - - - 1 - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - font: 12pt "Segoe UI"; - - - 1/3 - - - - - - - - - - - - - - - 100 - - - 100 - - - - - - - font: 75 12pt "MS Shell Dlg 2"; - - - Cluster Spectrum - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - - - Show all - - - - - - - - - QAbstractScrollArea::AdjustToContents - - - - - - - - - - - - - 0 - 0 - 979 - 23 - - - - - Image - - - - - - - - Spectrum - - - - - - - Options - - - - - - - - - - - Save All Clusters - - - - - Save Current Cluster - - - - - Save Composite Map as Masks - - - - - Save All Cluster Spectra - - - - - Save Current Spectrum - - - - - Save Results as a Folder - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 979 + 815 + + + + + 0 + 0 + + + + MainWindow + + + background-color: rgb(240, 240, 240); +font: 10pt "Segoe UI"; + + + + QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + + + + + + + + + + + + + Composite Map + + + Qt::AlignCenter + + + + + + + Opan as RGBCMY Image + + + + + + + + + + + + + + + + + Nth Cluster + + + Qt::AlignCenter + + + + + + + + + + + + + 0 + 0 + + + + 0 + + + 20 + + + 1 + + + 1 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + font: 12pt "Segoe UI"; + + + 1/3 + + + + + + + + + + + + + + + 100 + + + 100 + + + + + + + font: 75 12pt "MS Shell Dlg 2"; + + + Cluster Spectrum + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + Show all + + + + + + + + + QAbstractScrollArea::AdjustToContents + + + + + + + + + + + + + 0 + 0 + 979 + 23 + + + + + Image + + + + + + + + Spectrum + + + + + + + Options + + + + + + + + + + + Save All Clusters + + + + + Save Current Cluster + + + + + Save Composite Map as Masks + + + + + Save All Cluster Spectra + + + + + Save Current Spectrum + + + + + Save Results as a Folder + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/ComponentScatterPlot.ui b/xmidas/gui/layout/ComponentScatterPlot.ui similarity index 96% rename from xmidas/uis/ComponentScatterPlot.ui rename to xmidas/gui/layout/ComponentScatterPlot.ui index 2e4519b..ee95575 100644 --- a/xmidas/uis/ComponentScatterPlot.ui +++ b/xmidas/gui/layout/ComponentScatterPlot.ui @@ -1,195 +1,195 @@ - - - ScatterPlot - - - - 0 - 0 - 1034 - 859 - - - - Correlation Plot - - - font: 12pt "MS Shell Dlg 2"; - - - - - - - 10 - - - 10 - - - 10 - - - 10 - - - - - Image - - - Qt::AlignCenter - - - - - - - - - - - - 10 - - - 10 - - - 10 - - - 10 - - - - - Image - - - Qt::AlignCenter - - - - - - - - - - - - - - - Qt::LeftToRight - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - Apply - - - - - - - Scatter View of - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - Add A Line - - - - - - - Create A Mask - - - - - - - Reset Mask - - - - - - - Apply Mask - - - - - - - - - - - 0 - 0 - 1034 - 30 - - - - - File - - - - - - - - - - Save Plot - - - - - Save Images - - - - - - ImageView - QGraphicsView -
pyqtgraph
-
- - GraphicsLayoutWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + ScatterPlot + + + + 0 + 0 + 1034 + 859 + + + + Correlation Plot + + + font: 12pt "MS Shell Dlg 2"; + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Image + + + Qt::AlignCenter + + + + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Image + + + Qt::AlignCenter + + + + + + + + + + + + + + + Qt::LeftToRight + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Apply + + + + + + + Scatter View of + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + Add A Line + + + + + + + Create A Mask + + + + + + + Reset Mask + + + + + + + Apply Mask + + + + + + + + + + + 0 + 0 + 1034 + 30 + + + + + File + + + + + + + + + + Save Plot + + + + + Save Images + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+ + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/ComponentView.ui b/xmidas/gui/layout/ComponentView.ui similarity index 93% rename from xmidas/uis/ComponentView.ui rename to xmidas/gui/layout/ComponentView.ui index df74ea3..c7aca47 100644 --- a/xmidas/uis/ComponentView.ui +++ b/xmidas/gui/layout/ComponentView.ui @@ -1,296 +1,303 @@ - - - MainWindow - - - - 0 - 0 - 920 - 858 - - - - MainWindow - - - background-color: rgb(240, 240, 240); - - - - - - - - - - - - - - - - - - - - - Components - - - Qt::AlignCenter - - - - - - - - - - - - - - - 0 - 0 - - - - QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 0, 127); -} - - - 20 - - - 1 - - - 1 - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - font: 12pt "Segoe UI"; - - - 1/3 - - - - - - - - - - - - - - - - Component Masks - - - Qt::AlignCenter - - - - - - - - - - - - - Show All Spectra - - - - - - - Opan as RGBCMY Image - - - - - - - - - - - - - - - - Masked Spectrum - - - Qt::AlignCenter - - - - - - - - - - - - - - - - Component Spectrum - - - Qt::AlignCenter - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - Open ScatterPlot - - - - - - - - - - - - 0 - 0 - 920 - 26 - - - - - File - - - - - - - - - Save - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 920 + 858 + + + + MainWindow + + + background-color: rgb(240, 240, 240); + + + + + + + + + + + + + + + + + + + + + Components + + + Qt::AlignCenter + + + + + + + + + + + + + + + 0 + 0 + + + + QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 10px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #66e, stop: 1 #bbf); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #bbf, stop: 1 #55f); +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 13px; +margin-top: -2px; +margin-bottom: -2px; +border-radius: 4px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +border-radius: 2px; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +border-radius: 4px; +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 0, 127); +} + + + 20 + + + 1 + + + 1 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + font: 12pt "Segoe UI"; + + + 1/3 + + + + + + + + + + + + + + + + Component Masks + + + Qt::AlignCenter + + + + + + + + + + + + + Show All Spectra + + + + + + + Open as RGBCMY Image + + + + + + + + + + + + + + + + Masked Spectrum + + + Qt::AlignCenter + + + + + + + + + + + + + + + + Component Spectrum + + + Qt::AlignCenter + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + Open ScatterPlot + + + + + + + Open XANES Norm. View + + + + + + + + + + + + 0 + 0 + 920 + 26 + + + + + File + + + + + + + + + Save + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/gui/layout/MaskMaker.ui b/xmidas/gui/layout/MaskMaker.ui new file mode 100644 index 0000000..5209298 --- /dev/null +++ b/xmidas/gui/layout/MaskMaker.ui @@ -0,0 +1,307 @@ + + + MainWindow + + + + 0 + 0 + 979 + 513 + + + + Masked XANES + + + + + + + QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + + + + 15 + + + 15 + + + 15 + + + 15 + + + 10 + + + + + + + + + + + + + Create A mask + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + Binary Mask + + + Qt::AlignCenter + + + + + + + + + + + 3 + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 5 + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksAbove + + + 5 + + + + + + + 3 + + + 0.001000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + + + + + + + Send mask to XANESViewer + + + + + + + + + Use Binary Mask + + + + + + + + + + + 0 + 0 + 979 + 37 + + + + + File + + + + + + + + + Options + + + + + + + + + + + Save XANES Stack + + + + + Save XRF Map + + + + + Save Spectrum + + + + + Load Energy List + + + + + Load XRF Map + + + + + Load XANES Stack + + + + + Import Mask + + + + + Export Mask + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/gui/layout/MaskSpecViewer.ui b/xmidas/gui/layout/MaskSpecViewer.ui new file mode 100644 index 0000000..db4d916 --- /dev/null +++ b/xmidas/gui/layout/MaskSpecViewer.ui @@ -0,0 +1,367 @@ + + + MainWindow + + + + 0 + 0 + 900 + 695 + + + + MaskedXANES + + + + + + + QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + + + + 15 + + + 15 + + + 15 + + + 15 + + + 10 + + + + + + + 3 + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 5 + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksAbove + + + 5 + + + + + + + 3 + + + 0.001000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + + + + + + + Apply the mask to XANES + + + + + + Apply Mask + + + + + + + Send mask to XANESViewer + + + + + + + + + Use Binary Mask + + + + + + + + + + + + + + Create A mask + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + Masked Mean Spectrum + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + Binary Mask + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + XANES Stack + + + Qt::AlignCenter + + + + + + + + + + + + + + 0 + 0 + 900 + 37 + + + + + File + + + + + + + + + + + Options + + + + + + + + + + + Save XANES Stack + + + + + Save XRF Map + + + + + Save Spectrum + + + + + Load Energy List + + + + + Load XRF Map + + + + + Load XANES Stack + + + + + Import Mask + + + + + Export Mask + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+ + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/MaskedView.ui b/xmidas/gui/layout/MaskedView.ui similarity index 96% rename from xmidas/uis/MaskedView.ui rename to xmidas/gui/layout/MaskedView.ui index a8c40db..7a08678 100644 --- a/xmidas/uis/MaskedView.ui +++ b/xmidas/gui/layout/MaskedView.ui @@ -1,352 +1,352 @@ - - - MainWindow - - - - 0 - 0 - 930 - 779 - - - - MaskedXANES - - - - - - - - - - - - color: rgb(255, 0, 0); -font: 12pt "MS Shell Dlg 2"; - - - XANES Stack - - - Qt::AlignCenter - - - - - - - - - - - - - - font: 12pt "MS Shell Dlg 2"; -color: rgb(255, 0, 0); - - - Mask - - - Qt::AlignCenter - - - - - - - - - - - - 0 - - - - - A Tiff Mask with same dimensions - - - background-color: rgb(170, 255, 255); -font: 10pt "MS Shell Dlg 2"; - - - Import Mask - - - - - - - Save Mask tiff - - - background-color: rgb(170, 255, 255); -font: 10pt "MS Shell Dlg 2"; - - - Export Mask - - - - - - - Apply the mask to XANES - - - background-color: rgb(170, 255, 255); -font: 10pt "MS Shell Dlg 2"; - - - Apply - - - - - - - - - - - font: 75 12pt "MS Shell Dlg 2"; -color: rgb(255, 0, 0); - - - Mean Spectrum - - - Qt::AlignCenter - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - font: 12pt "MS Shell Dlg 2"; -color: rgb(255, 0, 0); - - - XRF Map - - - Qt::AlignCenter - - - - - - - font: 10pt "MS Shell Dlg 2"; -color: rgb(255, 0, 0); - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - Qt::NoFocus - - - 100 - - - 5 - - - 5 - - - 0 - - - 0 - - - true - - - Qt::Vertical - - - QSlider::TicksAbove - - - 5 - - - - - - - - - - - Qt::NoFocus - - - 100 - - - 5 - - - 5 - - - 100 - - - 100 - - - true - - - Qt::Vertical - - - false - - - false - - - QSlider::TicksBelow - - - 5 - - - - - - - - - - - - - - - - - 0 - 0 - 930 - 26 - - - - - File - - - - - - - - - - - - - - Save XANES Stack - - - - - Save XRF Map - - - - - Save Spectrum - - - - - Load Energy List - - - - - Load XRF Map - - - - - Load XANES Stack - - - - - - ImageView - QGraphicsView -
pyqtgraph
-
- - PlotWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 930 + 779 + + + + MaskedXANES + + + + + + + + + + + + color: rgb(255, 0, 0); +font: 12pt "MS Shell Dlg 2"; + + + XANES Stack + + + Qt::AlignCenter + + + + + + + + + + + + + + font: 12pt "MS Shell Dlg 2"; +color: rgb(255, 0, 0); + + + Mask + + + Qt::AlignCenter + + + + + + + + + + + + 0 + + + + + A Tiff Mask with same dimensions + + + background-color: rgb(170, 255, 255); +font: 10pt "MS Shell Dlg 2"; + + + Import Mask + + + + + + + Save Mask tiff + + + background-color: rgb(170, 255, 255); +font: 10pt "MS Shell Dlg 2"; + + + Export Mask + + + + + + + Apply the mask to XANES + + + background-color: rgb(170, 255, 255); +font: 10pt "MS Shell Dlg 2"; + + + Apply + + + + + + + + + + + font: 75 12pt "MS Shell Dlg 2"; +color: rgb(255, 0, 0); + + + Mean Spectrum + + + Qt::AlignCenter + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + font: 12pt "MS Shell Dlg 2"; +color: rgb(255, 0, 0); + + + XRF Map + + + Qt::AlignCenter + + + + + + + font: 10pt "MS Shell Dlg 2"; +color: rgb(255, 0, 0); + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Vertical + + + QSlider::TicksAbove + + + 5 + + + + + + + + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Vertical + + + false + + + false + + + QSlider::TicksBelow + + + 5 + + + + + + + + + + + + + + + + + 0 + 0 + 930 + 26 + + + + + File + + + + + + + + + + + + + + Save XANES Stack + + + + + Save XRF Map + + + + + Save Spectrum + + + + + Load Energy List + + + + + Load XRF Map + + + + + Load XANES Stack + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+ + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/gui/layout/MultiImageSpectrumView.ui b/xmidas/gui/layout/MultiImageSpectrumView.ui new file mode 100644 index 0000000..a418c19 --- /dev/null +++ b/xmidas/gui/layout/MultiImageSpectrumView.ui @@ -0,0 +1,734 @@ + + + MainWindow + + + + 0 + 0 + 1182 + 821 + + + + Chemical Map Spectrum View + + + font: 12pt "Segoe UI" + + + + QPushButton { +border: 1px solid #555; +border-radius: 5px; +background: qradialgradient(cx: 0.3, cy: -0.1, +fx: 0.7, fy: 0.1, +radius: 1, stop: 0 #fff, stop: 1 #888); +background-color: rgb(170, 255, 255); +} + +QPushButton:hover{ + background-color: rgb(255, 255, 0); + } + +QPushButton:pressed{ + background-color: rgb(0,255, 0); + } + +font: 12pt "Segoe UI"; + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + + + + Edit + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + 0 + 212 + 85 + + + + Thresholding + + + + + + + + + 0 + 0 + + + + 0,100 + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + click update after making changes + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::NoTicks + + + 5 + + + + + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + click update after making changes + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Horizontal + + + QSlider::NoTicks + + + 5 + + + + + + + + + + + + + + + 0 + 0 + 212 + 85 + + + + Opacity + + + + + + + + + 0 + 0 + + + + 1 + + + Qt::AlignCenter + + + + + + + click update after making changes + + + 100 + + + 10 + + + 100 + + + Qt::Horizontal + + + + + + + + + + + + + execute above changes to the selected item + + + Update + + + + + + + + + Show Selected + + + + + + + Show All + + + + + + + + + + 0 + 0 + + + + change properties of the selected item + + + font: 8pt "Segoe UI"; + + + QAbstractScrollArea::AdjustToContents + + + + + + + + + + 0 + 0 + + + + Change Selected To + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + + + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + XANES Normalization Parameters + + + + + + + 0 + 0 + + + + eV + + + -500.000000000000000 + + + 500.000000000000000 + + + -10.000000000000000 + + + + + + + + 0 + 0 + + + + eV + + + 0.000000000000000 + + + 1000.000000000000000 + + + 25.000000000000000 + + + + + + + + 0 + 0 + + + + 1 + + + 5 + + + + + + + + 0 + 0 + + + + Post-edge + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Norm. Order + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + eV + + + 2 + + + -500.000000000000000 + + + 500.000000000000000 + + + 1.000000000000000 + + + -50.000000000000000 + + + + + + + + 0 + 0 + + + + eV + + + 0.000000000000000 + + + 1500.000000000000000 + + + 75.000000000000000 + + + + + + + + 0 + 0 + + + + Pre-edge + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Eo + + + + + + + + 0 + 0 + + + + eV + + + 1000.000000000000000 + + + 20000.000000000000000 + + + 7125.000000000000000 + + + + + + + + 0 + 0 + + + + Plot Normalized Spectra + + + + + + + + + + + + + + + + + + 0 + 0 + 1182 + 27 + + + + + Image + + + true + + + + + + + + + + Spectrum + + + + + + + + + + Load Images + + + Select and load multiple tiff images to create a multi color view + + + + + Load 2 + + + + + Load 3 + + + + + Load 4 + + + + + Load 5 + + + + + Load 6 + + + + + Export Image + + + Export the image view as a sinle image file + + + + + Save State File + + + Save the current state of the view. Images and properties are saved + + + + + Load State File + + + Load a state (json file) saved previously. + + + + + Load Stack + + + Load images as a stack of tiff + + + + + Save Displayed Spectra(.CSV) + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/RefChooser.ui b/xmidas/gui/layout/RefChooser.ui similarity index 94% rename from xmidas/uis/RefChooser.ui rename to xmidas/gui/layout/RefChooser.ui index eea416f..26f9474 100644 --- a/xmidas/uis/RefChooser.ui +++ b/xmidas/gui/layout/RefChooser.ui @@ -1,247 +1,252 @@ - - - MainWindow - - - - 0 - 0 - 986 - 985 - - - - ArrowCursor - - - Select References - - - font: 10pt "MS Shell Dlg 2"; - - - - QCheckBox:checked { -color: rgb(255, 75, 52); - background-color: rgb(255, 248, 149); -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -} - -QPushButton:disabled { -background-color: rgb(255, 227, 213); -color: rgb(255, 255, 255); -} - - - - - - - 25 - - - 25 - - - 25 - - - 25 - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - Drag the line or double click on a point to select the parameters - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - - Max. No. of Refs - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 1 - - - 10 - - - 2 - - - - - - - - - - - Try All Cobinations - - - - - - - false - - - Fit With Selected - - - - - - - - - - color: rgb(255, 75, 52); -font: 10pt "MS Shell Dlg 2"; - - - N Combinations - - - Qt::AlignCenter - - - - - - - selection-color: rgb(0, 170, 127); - - - 1 - - - - - - - - - Sort by - - - - - - - - Fit Number - - - - - Reduced Chi-Square - - - - - R-Factor - - - - - R-Square - - - - - Chi-Square - - - - - - - - - - - - - - - 0 - 0 - 986 - 22 - - - - - File - - - - - - - - - Export Results (.csv) - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 986 + 985 + + + + ArrowCursor + + + Select References + + + font: 10pt "MS Shell Dlg 2"; + + + + QCheckBox:checked { +color: rgb(255, 75, 52); + background-color: rgb(255, 248, 149); +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + +QPushButton:disabled { +background-color: rgb(255, 227, 213); +color: rgb(255, 255, 255); +} + + + + + + + 25 + + + 25 + + + 25 + + + 25 + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + Drag the line or double click on a point to select the parameters + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + Max. No. of Refs + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 10 + + + 2 + + + + + + + + + + + Try All Cobinations + + + + + + + false + + + Fit With Selected + + + + + + + + + + color: rgb(255, 75, 52); +font: 10pt "MS Shell Dlg 2"; + + + N Combinations + + + Qt::AlignCenter + + + + + + + selection-color: rgb(0, 170, 127); + + + 1 + + + + + + + + + Sort by + + + + + + + + Reduced Chi-Square + + + + + Score + + + + + Fit Number + + + + + R-Factor + + + + + R-Square + + + + + Chi-Square + + + + + + + + + + + + + + + 0 + 0 + 986 + 27 + + + + + File + + + + + + + + + Export Results (.csv) + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/Scatter3D.ui b/xmidas/gui/layout/Scatter3D.ui similarity index 96% rename from xmidas/uis/Scatter3D.ui rename to xmidas/gui/layout/Scatter3D.ui index ca2ab8d..cefddb8 100644 --- a/xmidas/uis/Scatter3D.ui +++ b/xmidas/gui/layout/Scatter3D.ui @@ -1,130 +1,130 @@ - - - MainWindow - - - - 0 - 0 - 658 - 628 - - - - MainWindow - - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - 0 - 658 - 21 - - - - - View - - - - Plot Background - - - - - - - - - - - File - - - - - - - - - - - - Export - - - - - Save as PNG - - - - - Generate MultiColor Mask - - - - - Black - - - - - Charcol - - - - - White - - - - - Open Images - - - - - Open a Stack - - - - - Export Image - - - - - - GLViewWidget - QGraphicsView -
pyqtgraph.opengl
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 658 + 628 + + + + MainWindow + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + 658 + 21 + + + + + View + + + + Plot Background + + + + + + + + + + + File + + + + + + + + + + + + Export + + + + + Save as PNG + + + + + Generate MultiColor Mask + + + + + Black + + + + + Charcol + + + + + White + + + + + Open Images + + + + + Open a Stack + + + + + Export Image + + + + + + GLViewWidget + QGraphicsView +
pyqtgraph.opengl
+
+
+ + +
diff --git a/xmidas/uis/Scatter3D_plotly.ui b/xmidas/gui/layout/Scatter3D_plotly.ui similarity index 96% rename from xmidas/uis/Scatter3D_plotly.ui rename to xmidas/gui/layout/Scatter3D_plotly.ui index 1318496..df22d6d 100644 --- a/xmidas/uis/Scatter3D_plotly.ui +++ b/xmidas/gui/layout/Scatter3D_plotly.ui @@ -1,120 +1,120 @@ - - - MainWindow - - - - 0 - 0 - 856 - 628 - - - - MainWindow - - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - 0 - 856 - 21 - - - - - View - - - - Plot Background - - - - - - - - - - - - - - toolBar - - - TopToolBarArea - - - false - - - - - - - - Export - - - - - Save as PNG - - - - - Generate MultiColor Mask - - - - - Black - - - - - Grey - - - - - White - - - - - - QWebEngineView - QGraphicsView -
QtWebEngineWidgets
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 856 + 628 + + + + MainWindow + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + 856 + 21 + + + + + View + + + + Plot Background + + + + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Export + + + + + Save as PNG + + + + + Generate MultiColor Mask + + + + + Black + + + + + Grey + + + + + White + + + + + + QWebEngineView + QGraphicsView +
QtWebEngineWidgets
+
+
+ + +
diff --git a/xmidas/uis/ScatterView.ui b/xmidas/gui/layout/ScatterView.ui similarity index 96% rename from xmidas/uis/ScatterView.ui rename to xmidas/gui/layout/ScatterView.ui index f5394d3..4f0831e 100644 --- a/xmidas/uis/ScatterView.ui +++ b/xmidas/gui/layout/ScatterView.ui @@ -1,249 +1,249 @@ - - - CorrelationPlot - - - - 0 - 0 - 1034 - 859 - - - - Correlation Plot - - - font:10pt "Segoe UI"; - - - - QRadioButton::indicator { - border: 3px solid rgb(150, 150,150); - width: 15px; - height: 15px; - border-radius: 10px; - background: rgb(255, 255, 255); -} -QRadioButton::indicator:hover { - border: 3px solid rgb(58, 66, 81); -} -QRadioButton::indicator:checked { - background: 3px solid rgb(225, 75, 225); - border: 3px solid rgb(255, 252, 255); -} - - - - - - ROI Mask - - - - - - - - color: rgb(255, 0, 0); -background-color: rgb(20, 20, 20); -font: 87 10pt "Segoe UI Black"; - - - - ROI 1 - - - true - - - false - - - - - - - color: rgb(0, 255, 0); -background-color: rgb(20, 20, 20); -font: 87 10pt "Segoe UI Black"; - - - ROI 2 - - - false - - - - - - - color: rgb(0, 255, 255); -background-color: rgb(20, 20, 20); -font: 87 10pt "Segoe UI Black"; - - - ROI 3 - - - false - - - - - - - - - Create Composite Scatter Plot - - - - - - - Apply ROI Mask(s) - - - - - - - Clear Unchecked - - - - - - - Add Checked ROI(s) - - - - - - - - - - 10 - - - 10 - - - 10 - - - 10 - - - - - Image 1 (Blue Shade) - - - Qt::AlignCenter - - - - - - - - - - - - 10 - - - 10 - - - 10 - - - 10 - - - - - Image 2 (Green Shade) - - - Qt::AlignCenter - - - - - - - - - - - - - - - - - 0 - 0 - 1034 - 23 - - - - - File - - - - - - - Plot - - - - - - - - - - - - - Save Plot - - - - - Save Images - - - - - Swap Axes - - - - - - GraphicsLayoutWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + CorrelationPlot + + + + 0 + 0 + 1034 + 859 + + + + Correlation Plot + + + font:10pt "Segoe UI"; + + + + QRadioButton::indicator { + border: 3px solid rgb(150, 150,150); + width: 15px; + height: 15px; + border-radius: 10px; + background: rgb(255, 255, 255); +} +QRadioButton::indicator:hover { + border: 3px solid rgb(58, 66, 81); +} +QRadioButton::indicator:checked { + background: 3px solid rgb(225, 75, 225); + border: 3px solid rgb(255, 252, 255); +} + + + + + + ROI Mask + + + + + + + + color: rgb(255, 0, 0); +background-color: rgb(20, 20, 20); +font: 87 10pt "Segoe UI Black"; + + + + ROI 1 + + + true + + + false + + + + + + + color: rgb(0, 255, 0); +background-color: rgb(20, 20, 20); +font: 87 10pt "Segoe UI Black"; + + + ROI 2 + + + false + + + + + + + color: rgb(0, 255, 255); +background-color: rgb(20, 20, 20); +font: 87 10pt "Segoe UI Black"; + + + ROI 3 + + + false + + + + + + + + + Create Composite Scatter Plot + + + + + + + Apply ROI Mask(s) + + + + + + + Clear Unchecked + + + + + + + Add Checked ROI(s) + + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Image 1 (Blue Shade) + + + Qt::AlignCenter + + + + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Image 2 (Green Shade) + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + 0 + 0 + 1034 + 23 + + + + + File + + + + + + + Plot + + + + + + + + + + + + + Save Plot + + + + + Save Images + + + + + Swap Axes + + + + + + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/StackViewer.ui b/xmidas/gui/layout/StackViewer.ui similarity index 96% rename from xmidas/uis/StackViewer.ui rename to xmidas/gui/layout/StackViewer.ui index 62f2665..1d58ea9 100644 --- a/xmidas/uis/StackViewer.ui +++ b/xmidas/gui/layout/StackViewer.ui @@ -1,335 +1,335 @@ - - - MainWindow - - - - 0 - 0 - 931 - 815 - - - - MainWindow - - - - - - 11 - 30 - 701 - 326 - - - - - 0 - 0 - - - - - - - 10 - 390 - 891 - 371 - - - - - - - 720 - 80 - 181 - 171 - - - - - 0 - 0 - - - - Image ROI - - - - 11 - - - 11 - - - - - - - - - - - Y End - - - - - - - true - - - - - - - - - - - X Start - - - - - - - true - - - - - - - - - - - Y Start - - - - - - - true - - - - - - - - - - - X End - - - - - - - true - - - - - - - - - - - - - font: 75 10pt "MS Shell Dlg 2"; - - - Size - - - - - - - true - - - - - - - - - - - - - 720 - 250 - 181 - 139 - - - - - 0 - 0 - - - - Spectrum ROI - - - - - - Sync - - - - - - - - - Start - - - - - - - 10000 - - - 2 - - - - - - - - - - - End - - - - - - - 1 - - - 10000 - - - 2 - - - 10 - - - - - - - - - - - font: 75 10pt "MS Shell Dlg 2"; - - - Size - - - - - - - true - - - - - - - - - - - 730 - 10 - 171 - 70 - - - - - - - false - - - background-color: rgb(170, 255, 255); -font: 75 12pt "MS Shell Dlg 2"; - - - Play - - - - - - - Log View - - - - - - - Reset - - - - - - - - - - 0 - 0 - 931 - 26 - - - - - - - - ImageView - QGraphicsView -
pyqtgraph
-
- - PlotWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 931 + 815 + + + + MainWindow + + + + + + 11 + 30 + 701 + 326 + + + + + 0 + 0 + + + + + + + 10 + 390 + 891 + 371 + + + + + + + 720 + 80 + 181 + 171 + + + + + 0 + 0 + + + + Image ROI + + + + 11 + + + 11 + + + + + + + + + + + Y End + + + + + + + true + + + + + + + + + + + X Start + + + + + + + true + + + + + + + + + + + Y Start + + + + + + + true + + + + + + + + + + + X End + + + + + + + true + + + + + + + + + + + + + font: 75 10pt "MS Shell Dlg 2"; + + + Size + + + + + + + true + + + + + + + + + + + + + 720 + 250 + 181 + 139 + + + + + 0 + 0 + + + + Spectrum ROI + + + + + + Sync + + + + + + + + + Start + + + + + + + 10000 + + + 2 + + + + + + + + + + + End + + + + + + + 1 + + + 10000 + + + 2 + + + 10 + + + + + + + + + + + font: 75 10pt "MS Shell Dlg 2"; + + + Size + + + + + + + true + + + + + + + + + + + 730 + 10 + 171 + 70 + + + + + + + false + + + background-color: rgb(170, 255, 255); +font: 75 12pt "MS Shell Dlg 2"; + + + Play + + + + + + + Log View + + + + + + + Reset + + + + + + + + + + 0 + 0 + 931 + 26 + + + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+ + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/gui/layout/XANESViewer.ui b/xmidas/gui/layout/XANESViewer.ui new file mode 100644 index 0000000..2194045 --- /dev/null +++ b/xmidas/gui/layout/XANESViewer.ui @@ -0,0 +1,533 @@ + + + mainWindow + + + + 0 + 0 + 1182 + 602 + + + + XANES Fit View + + + + + + + QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + + + + + + + + + + + + + 0 + 0 + + + + + + + + XANES Stack + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + + + + + 0 + 0 + + + + + + + Chemical Map + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + + + 0 + 0 + + + + Live Fit + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + ROI Position + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + + + Reference Spectra + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Choose References + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 215 + + + + + + + + + + + + + 0 + 0 + + + + Update 2D Fit + + + + + + + + + + + E_shift + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + -50.000000000000000 + + + 50.000000000000000 + + + 0.500000000000000 + + + + + + + Regularizer: + + + + + + + useful for noisy data, higher if more noise, not applicable to NNLS + + + 10.000000000000000 + + + 0.250000000000000 + + + 0.100000000000000 + + + + + + + + + + + Fitting Model + + + + + + + + NNLS + + + + + ADMM + + + + + LASSO + + + + + RIDGE + + + + + + + + spectrum will be divided with its last point before fitting (similar to a post-edge normalization) + + + Normalize Fit + + + true + + + + + + + + + + + + + Normalize Chem. Map + + + + + + + + + + 0 + 0 + + + + font: 8pt "MS Shell Dlg 2"; +color: rgb(0, 85, 255); + + + Results Text + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + Open Mask Maker + + + + + + + + 0 + 0 + + + + Open Composite Chem Map Viewer + + + + + + + Open Cluster Mask Tool + + + + + + + Cluster Chem. map + + + + + + + + + + + + 0 + 0 + 1182 + 24 + + + + + File + + + + + + + + + Options + + + + + + + + + + Export Fit Stats + + + + + Export Ref. Plot + + + + + Export Chem Map + + + + + Export R-factor Image + + + + + false + + + Export References + + + + + Export Fit Stats + + + + + Export All Plotted Data + + + Ctrl+S + + + + + Export Sum Fit Data + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/align_test.ui b/xmidas/gui/layout/align_test.ui similarity index 95% rename from xmidas/uis/align_test.ui rename to xmidas/gui/layout/align_test.ui index da5604d..a5ab854 100644 --- a/xmidas/uis/align_test.ui +++ b/xmidas/gui/layout/align_test.ui @@ -1,71 +1,71 @@ - - - MainWindow - - - - 0 - 0 - 280 - 271 - - - - MainWindow - - - - - - 100 - 50 - 93 - 28 - - - - Load - - - - - - 100 - 100 - 93 - 28 - - - - Align - - - - - - 100 - 160 - 93 - 28 - - - - Save - - - - - - - 0 - 0 - 280 - 26 - - - - - - - - + + + MainWindow + + + + 0 + 0 + 280 + 271 + + + + MainWindow + + + + + + 100 + 50 + 93 + 28 + + + + Load + + + + + + 100 + 100 + 93 + 28 + + + + Align + + + + + + 100 + 160 + 93 + 28 + + + + Save + + + + + + + 0 + 0 + 280 + 26 + + + + + + + + diff --git a/xmidas/uis/animationWindow.ui b/xmidas/gui/layout/animationWindow.ui similarity index 96% rename from xmidas/uis/animationWindow.ui rename to xmidas/gui/layout/animationWindow.ui index b90a5e0..21e0c3a 100644 --- a/xmidas/uis/animationWindow.ui +++ b/xmidas/gui/layout/animationWindow.ui @@ -1,68 +1,68 @@ - - - Wait - - - - 0 - 0 - 200 - 200 - - - - Wait... - - - Qt::LeftToRight - - - -background-color: rgba(0, 0, 90, 125); - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - Qt::NoContextMenu - - - - - - - - - false - - - Qt::AlignCenter - - - - - - - - + + + Wait + + + + 0 + 0 + 200 + 200 + + + + Wait... + + + Qt::LeftToRight + + + +background-color: rgba(0, 0, 90, 125); + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::NoContextMenu + + + + + + + + + false + + + Qt::AlignCenter + + + + + + + + diff --git a/xmidas/css/darkStyle.css b/xmidas/gui/layout/css/darkStyle.css similarity index 95% rename from xmidas/css/darkStyle.css rename to xmidas/gui/layout/css/darkStyle.css index a5e9ddb..ea2c89f 100644 --- a/xmidas/css/darkStyle.css +++ b/xmidas/gui/layout/css/darkStyle.css @@ -1,68 +1,68 @@ -QWidget { -background-color: rgb(60, 60, 60); -color: rgb(255, 255, 255); -font: 10pt "Segoe UI"; -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -} - -QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - +QWidget { +background-color: rgb(60, 60, 60); +color: rgb(255, 255, 255); +font: 10pt "Segoe UI"; +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +} + +QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 10px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #66e, stop: 1 #bbf); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #bbf, stop: 1 #55f); +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 13px; +margin-top: -2px; +margin-bottom: -2px; +border-radius: 4px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +border-radius: 2px; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +border-radius: 4px; +} + diff --git a/xmidas/css/defaultStyle.css b/xmidas/gui/layout/css/defaultStyle.css similarity index 94% rename from xmidas/css/defaultStyle.css rename to xmidas/gui/layout/css/defaultStyle.css index 981a787..11c2467 100644 --- a/xmidas/css/defaultStyle.css +++ b/xmidas/gui/layout/css/defaultStyle.css @@ -1,72 +1,72 @@ - -QWidget { -font: 10pt "Segoe UI"; -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -font: 10pt "Segoe UI"; -} - -QLabel { -font: 10pt "Segoe UI"; -} - -QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - + +QWidget { +font: 10pt "Segoe UI"; +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +font: 10pt "Segoe UI"; +} + +QLabel { +font: 10pt "Segoe UI"; +} + +QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 10px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #66e, stop: 1 #bbf); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #bbf, stop: 1 #55f); +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 13px; +margin-top: -2px; +margin-bottom: -2px; +border-radius: 4px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +border-radius: 2px; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +border-radius: 4px; +} + diff --git a/xmidas/css/modern.css b/xmidas/gui/layout/css/modern.css similarity index 95% rename from xmidas/css/modern.css rename to xmidas/gui/layout/css/modern.css index 1bc5d20..a949b73 100644 --- a/xmidas/css/modern.css +++ b/xmidas/gui/layout/css/modern.css @@ -1,289 +1,289 @@ -/* ///////////////////////////////////////////////////////////////////////////////////////////////// - -SET APP STYLESHEET - FULL STYLES HERE -DARK THEME - DRACULA COLOR BASED - -///////////////////////////////////////////////////////////////////////////////////////////////// */ - -QWidget{ - background-color: rgb(23,23,37); - color: rgb(255, 255, 200); - font: 10pt "Segoe UI"; -} - - - -/* ///////////////////////////////////////////////////////////////////////////////////////////////// -Tooltip */ -QToolTip { - color: #ffffff; - background-color: rgba(33, 37, 43, 180); - border: 1px solid rgb(44, 49, 58); - background-image: none; - background-position: left center; - background-repeat: no-repeat; - border: none; - border-left: 2px solid rgb(255, 121, 198); - text-align: left; - padding-left: 8px; - margin: 0px; -} - - -QPushButton { - background-color: rgb(210, 210, 98); - color: rgb(23,23,37); - border: none; - border-radius: 5px; - padding: 4px; -} -QPushButton:hover { - background-color: rgb(211, 100, 211); -} -QPushButton:pressed { - background-color: rgb(189, 147, 249); - color: rgb(255, 255, 255); -} - - -QTabWidget { - color: rgb(44, 0, 0); -} -QTabWidget::item{ - color: rgb(24, 0, 0); -} -QTabBar::tab { - color:rgb(24, 0, 0); -} - -QLineEdit { - background-color: rgb(33, 37, 43); - border-radius: 5px; - border: 2px solid rgb(33, 37, 43); - padding-left: 10px; - color: rgb(255,255,255); - selection-color: rgb(255, 255, 255); - selection-background-color: rgb(255, 121, 198); -} -QLineEdit:hover { - border: 2px solid rgb(64, 71, 88); -} -QLineEdit:focus { - border: 2px solid rgb(91, 101, 124); -} - -/* ///////////////////////////////////////////////////////////////////////////////////////////////// -PlainTextEdit */ -QPlainTextEdit { - background-color: rgb(27, 29, 35); - border-radius: 5px; - padding: 10px; - selection-color: rgb(255, 255, 255); - selection-background-color: rgb(255, 121, 198); -} -QPlainTextEdit QScrollBar:vertical { - width: 8px; - } -QPlainTextEdit QScrollBar:horizontal { - height: 8px; - } -QPlainTextEdit:hover { - border: 2px solid rgb(64, 71, 88); -} -QPlainTextEdit:focus { - border: 2px solid rgb(91, 101, 124); -} - -/* ///////////////////////////////////////////////////////////////////////////////////////////////// -ScrollBars */ -QScrollBar:horizontal { - border: none; - background: rgb(52, 59, 72); - height: 8px; - margin: 0px 21px 0 21px; - border-radius: 0px; -} -QScrollBar::handle:horizontal { - background: rgb(189, 147, 249); - min-width: 25px; - border-radius: 4px -} -QScrollBar::add-line:horizontal { - border: none; - background: rgb(55, 63, 77); - width: 20px; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - subcontrol-position: right; - subcontrol-origin: margin; -} -QScrollBar::sub-line:horizontal { - border: none; - background: rgb(55, 63, 77); - width: 20px; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - subcontrol-position: left; - subcontrol-origin: margin; -} -QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal -{ - background: none; -} -QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal -{ - background: none; -} - QScrollBar:vertical { - border: none; - background: rgb(52, 59, 72); - width: 8px; - margin: 21px 0 21px 0; - border-radius: 0px; - } - QScrollBar::handle:vertical { - background: rgb(189, 147, 249); - min-height: 25px; - border-radius: 4px - } - QScrollBar::add-line:vertical { - border: none; - background: rgb(55, 63, 77); - height: 20px; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - QScrollBar::sub-line:vertical { - border: none; - background: rgb(55, 63, 77); - height: 20px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - subcontrol-position: top; - subcontrol-origin: margin; - } - QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { - background: none; - } - - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: none; - } - - -QRadioButton::indicator { - border: 3px solid rgb(52, 59, 72); - width: 15px; - height: 15px; - border-radius: 10px; - background: rgb(44, 4, 0); -} -QRadioButton::indicator:hover { - border: 3px solid rgb(58, 66, 81); -} -QRadioButton::indicator:checked { - background: 3px solid rgb(0, 255, 0); - border: 3px solid rgb(255, 252, 255); -} - -QCheckBox::indicator { - border: 3px solid rgb(52, 59, 72); - width: 15px; - height: 15px; - border-radius: 10px; - background: rgb(44, 4, 0); -} -QCheckBox::indicator:hover { - border: 3px solid rgb(58, 66, 81); -} -QCheckBox::indicator:checked { - background: 3px solid rgb(0, 255, 0); - border: 3px solid rgb(255, 252, 255); -} - -/* ///////////////////////////////////////////////////////////////////////////////////////////////// -ComboBox */ -QComboBox{ - background-color: rgb(138, 138, 138); - color: rgb(79, 27, 79); - border-radius: 5px; - border: 2px solid rgb(33, 37, 43); - padding: 5px; - padding-left: 10px; -} -QComboBox:hover{ - border: 2px solid rgb(64, 71, 88); -} -QComboBox::drop-down { - subcontrol-origin: padding; - subcontrol-position: top right; - width: 25px; - border-left-width: 3px; - border-left-color: rgba(39, 44, 54, 150); - border-left-style: solid; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; - background-image: url(:/icons/images/icons/cil-arrow-bottom.png); - background-position: center; - background-repeat: no-reperat; - } -QComboBox QAbstractItemView { - color: rgb(255, 121, 198); - background-color: rgb(33, 37, 43); - padding: 10px; - selection-background-color: rgb(39, 44, 54); -} - -/* ///////////////////////////////////////////////////////////////////////////////////////////////// -Sliders */ -QSlider::groove:horizontal { - border-radius: 5px; - height: 10px; - margin: 0px; - background-color: rgb(52, 59, 72); -} -QSlider::groove:horizontal:hover { - background-color: rgb(55, 62, 76); -} -QSlider::handle:horizontal { - background-color: rgb(189, 147, 249); - border: none; - height: 10px; - width: 10px; - margin: 0px; - border-radius: 5px; -} -QSlider::handle:horizontal:hover { - background-color: rgb(195, 155, 255); -} -QSlider::handle:horizontal:pressed { - background-color: rgb(255, 121, 198); -} - -QSlider::groove:vertical { - border-radius: 5px; - width: 10px; - margin: 0px; - background-color: rgb(52, 59, 72); -} -QSlider::groove:vertical:hover { - background-color: rgb(55, 62, 76); -} -QSlider::handle:vertical { - background-color: rgb(189, 147, 249); - border: none; - height: 10px; - width: 10px; - margin: 0px; - border-radius: 5px; -} -QSlider::handle:vertical:hover { - background-color: rgb(195, 155, 255); -} -QSlider::handle:vertical:pressed { - background-color: rgb(255, 121, 198); -} - - +/* ///////////////////////////////////////////////////////////////////////////////////////////////// + +SET APP STYLESHEET - FULL STYLES HERE +DARK THEME - DRACULA COLOR BASED + +///////////////////////////////////////////////////////////////////////////////////////////////// */ + +QWidget{ + background-color: rgb(23,23,37); + color: rgb(255, 255, 200); + font: 10pt "Segoe UI"; +} + + + +/* ///////////////////////////////////////////////////////////////////////////////////////////////// +Tooltip */ +QToolTip { + color: #ffffff; + background-color: rgba(33, 37, 43, 180); + border: 1px solid rgb(44, 49, 58); + background-image: none; + background-position: left center; + background-repeat: no-repeat; + border: none; + border-left: 2px solid rgb(255, 121, 198); + text-align: left; + padding-left: 8px; + margin: 0px; +} + + +QPushButton { + background-color: rgb(210, 210, 98); + color: rgb(23,23,37); + border: none; + border-radius: 5px; + padding: 4px; +} +QPushButton:hover { + background-color: rgb(211, 100, 211); +} +QPushButton:pressed { + background-color: rgb(189, 147, 249); + color: rgb(255, 255, 255); +} + + +QTabWidget { + color: rgb(44, 0, 0); +} +QTabWidget::item{ + color: rgb(24, 0, 0); +} +QTabBar::tab { + color:rgb(24, 0, 0); +} + +QLineEdit { + background-color: rgb(33, 37, 43); + border-radius: 5px; + border: 2px solid rgb(33, 37, 43); + padding-left: 10px; + color: rgb(255,255,255); + selection-color: rgb(255, 255, 255); + selection-background-color: rgb(255, 121, 198); +} +QLineEdit:hover { + border: 2px solid rgb(64, 71, 88); +} +QLineEdit:focus { + border: 2px solid rgb(91, 101, 124); +} + +/* ///////////////////////////////////////////////////////////////////////////////////////////////// +PlainTextEdit */ +QPlainTextEdit { + background-color: rgb(27, 29, 35); + border-radius: 5px; + padding: 10px; + selection-color: rgb(255, 255, 255); + selection-background-color: rgb(255, 121, 198); +} +QPlainTextEdit QScrollBar:vertical { + width: 8px; + } +QPlainTextEdit QScrollBar:horizontal { + height: 8px; + } +QPlainTextEdit:hover { + border: 2px solid rgb(64, 71, 88); +} +QPlainTextEdit:focus { + border: 2px solid rgb(91, 101, 124); +} + +/* ///////////////////////////////////////////////////////////////////////////////////////////////// +ScrollBars */ +QScrollBar:horizontal { + border: none; + background: rgb(52, 59, 72); + height: 8px; + margin: 0px 21px 0 21px; + border-radius: 0px; +} +QScrollBar::handle:horizontal { + background: rgb(189, 147, 249); + min-width: 25px; + border-radius: 4px +} +QScrollBar::add-line:horizontal { + border: none; + background: rgb(55, 63, 77); + width: 20px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + subcontrol-position: right; + subcontrol-origin: margin; +} +QScrollBar::sub-line:horizontal { + border: none; + background: rgb(55, 63, 77); + width: 20px; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + subcontrol-position: left; + subcontrol-origin: margin; +} +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal +{ + background: none; +} +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + QScrollBar:vertical { + border: none; + background: rgb(52, 59, 72); + width: 8px; + margin: 21px 0 21px 0; + border-radius: 0px; + } + QScrollBar::handle:vertical { + background: rgb(189, 147, 249); + min-height: 25px; + border-radius: 4px + } + QScrollBar::add-line:vertical { + border: none; + background: rgb(55, 63, 77); + height: 20px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + subcontrol-position: bottom; + subcontrol-origin: margin; + } + QScrollBar::sub-line:vertical { + border: none; + background: rgb(55, 63, 77); + height: 20px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + subcontrol-position: top; + subcontrol-origin: margin; + } + QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + background: none; + } + + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; + } + + +QRadioButton::indicator { + border: 3px solid rgb(52, 59, 72); + width: 15px; + height: 15px; + border-radius: 10px; + background: rgb(44, 4, 0); +} +QRadioButton::indicator:hover { + border: 3px solid rgb(58, 66, 81); +} +QRadioButton::indicator:checked { + background: 3px solid rgb(0, 255, 0); + border: 3px solid rgb(255, 252, 255); +} + +QCheckBox::indicator { + border: 3px solid rgb(52, 59, 72); + width: 15px; + height: 15px; + border-radius: 10px; + background: rgb(44, 4, 0); +} +QCheckBox::indicator:hover { + border: 3px solid rgb(58, 66, 81); +} +QCheckBox::indicator:checked { + background: 3px solid rgb(0, 255, 0); + border: 3px solid rgb(255, 252, 255); +} + +/* ///////////////////////////////////////////////////////////////////////////////////////////////// +ComboBox */ +QComboBox{ + background-color: rgb(138, 138, 138); + color: rgb(79, 27, 79); + border-radius: 5px; + border: 2px solid rgb(33, 37, 43); + padding: 5px; + padding-left: 10px; +} +QComboBox:hover{ + border: 2px solid rgb(64, 71, 88); +} +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 25px; + border-left-width: 3px; + border-left-color: rgba(39, 44, 54, 150); + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + background-image: url(:/icons/images/icons/cil-arrow-bottom.png); + background-position: center; + background-repeat: no-reperat; + } +QComboBox QAbstractItemView { + color: rgb(255, 121, 198); + background-color: rgb(33, 37, 43); + padding: 10px; + selection-background-color: rgb(39, 44, 54); +} + +/* ///////////////////////////////////////////////////////////////////////////////////////////////// +Sliders */ +QSlider::groove:horizontal { + border-radius: 5px; + height: 10px; + margin: 0px; + background-color: rgb(52, 59, 72); +} +QSlider::groove:horizontal:hover { + background-color: rgb(55, 62, 76); +} +QSlider::handle:horizontal { + background-color: rgb(189, 147, 249); + border: none; + height: 10px; + width: 10px; + margin: 0px; + border-radius: 5px; +} +QSlider::handle:horizontal:hover { + background-color: rgb(195, 155, 255); +} +QSlider::handle:horizontal:pressed { + background-color: rgb(255, 121, 198); +} + +QSlider::groove:vertical { + border-radius: 5px; + width: 10px; + margin: 0px; + background-color: rgb(52, 59, 72); +} +QSlider::groove:vertical:hover { + background-color: rgb(55, 62, 76); +} +QSlider::handle:vertical { + background-color: rgb(189, 147, 249); + border: none; + height: 10px; + width: 10px; + margin: 0px; + border-radius: 5px; +} +QSlider::handle:vertical:hover { + background-color: rgb(195, 155, 255); +} +QSlider::handle:vertical:pressed { + background-color: rgb(255, 121, 198); +} + + diff --git a/xmidas/gui/layout/decomposeViewer.ui b/xmidas/gui/layout/decomposeViewer.ui new file mode 100644 index 0000000..2b1f173 --- /dev/null +++ b/xmidas/gui/layout/decomposeViewer.ui @@ -0,0 +1,694 @@ + + + MainWindow + + + + 0 + 0 + 971 + 666 + + + + MainWindow + + + + + + + + + + + + + true + + + + 0 + 0 + + + + + + + View Stack + + + + + + + + 0 + 0 + + + + + + + Show Scree Plot + + + + + + + + + + 0 + 0 + + + + + + + Calculate Components + + + + + + + + + Method + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + PCA + + + + + NMF + + + + + FastICA + + + + + IncrementalPCA + + + + + TruncatedSVD + + + + + FactorAnalysis + + + + + DictionaryLearning + + + + + + + + + + + + Number of Components + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 4 + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + Cluster Stack + + + + + + + Use components + + + + + + + + + + + Method + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + KMeans + + + + + MiniBatchKMeans + + + + + MeanShift + + + + + Spectral Clustering + + + + + Correlation-Kmeans + + + + + Affinity Propagation + + + + + + + + + + + + Number of Clusters + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 4 + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + Eigen Values + + + Qt::AlignCenter + + + + + + + + + + + + + + Eigen Spectrum + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + Eigen Value Mask + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + Masked Spectrum + + + Qt::AlignCenter + + + + + + + + + + + + + + + + + 0 + 0 + + + + font: 12pt "Segoe UI"; + + + 1/3 + + + + + + + + 0 + 0 + + + + + + + 20 + + + 1 + + + 1 + + + Qt::Horizontal + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + + + Open ScatterPlot + + + + + + + + 0 + 0 + + + + + + + Show All Spectra + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Send Comp. as Mask + + + + + + + Use Binary Mask + + + true + + + + + + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksAbove + + + 5 + + + + + + + 3 + + + 0.001000000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + 3 + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 5 + + + + + + + Apply + + + + + + + + + + + Send mask and save results + + + + + + + + + + Browse + + + + + + + + + + + 0 + 0 + 971 + 37 + + + + + File + + + + + + + + + Save + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/Log.ui b/xmidas/gui/layout/log.ui similarity index 96% rename from xmidas/uis/Log.ui rename to xmidas/gui/layout/log.ui index 92ca4ec..6ff023b 100644 --- a/xmidas/uis/Log.ui +++ b/xmidas/gui/layout/log.ui @@ -1,68 +1,68 @@ - - - MainWindow - - - - 0 - 0 - 624 - 759 - - - - Log - - - - - - - font: 10pt "Segoe UI"; - - - QAbstractScrollArea::AdjustToContentsOnFirstShow - - - QPlainTextEdit::NoWrap - - - start typing here.. - - - - - - - - - Save - - - - - - - Clear - - - - - - - - - - - 0 - 0 - 624 - 26 - - - - - - - - + + + MainWindow + + + + 0 + 0 + 624 + 759 + + + + Log + + + + + + + font: 10pt "Segoe UI"; + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QPlainTextEdit::NoWrap + + + start typing here.. + + + + + + + + + Save + + + + + + + Clear + + + + + + + + + + + 0 + 0 + 624 + 26 + + + + + + + + diff --git a/xmidas/uis/maskedScatterPlotFit.ui b/xmidas/gui/layout/maskedScatterPlotFit.ui similarity index 96% rename from xmidas/uis/maskedScatterPlotFit.ui rename to xmidas/gui/layout/maskedScatterPlotFit.ui index e8bd894..8ad2145 100644 --- a/xmidas/uis/maskedScatterPlotFit.ui +++ b/xmidas/gui/layout/maskedScatterPlotFit.ui @@ -1,187 +1,187 @@ - - - CorrelationPlot - - - - 0 - 0 - 967 - 816 - - - - Correlation Plot - - - font: 12pt "MS Shell Dlg 2"; - - - - - 10 - - - 25 - - - 10 - - - 10 - - - - - Scatter Plot - - - - - - - 0 - 0 - - - - - - - - - - - Mask - - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 0 - - - - Fit Results - - - - - - Copy - - - - - - - - 0 - 0 - - - - font: 11pt "Consolas"; - - - true - - - - - - - Export - - - - - - - - - - Masked Image1 - - - - - - - 0 - 0 - - - - - - - - - - - - - - 0 - 0 - 967 - 25 - - - - - Export - - - - - - - - - - Scatter Plot+Fit - - - - - Mask - - - - - Masked Image - - - - - - ImageView - QGraphicsView -
pyqtgraph
-
- - GraphicsLayoutWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + CorrelationPlot + + + + 0 + 0 + 967 + 816 + + + + Correlation Plot + + + font: 12pt "MS Shell Dlg 2"; + + + + + 10 + + + 25 + + + 10 + + + 10 + + + + + Scatter Plot + + + + + + + 0 + 0 + + + + + + + + + + + Mask + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + Fit Results + + + + + + Copy + + + + + + + + 0 + 0 + + + + font: 11pt "Consolas"; + + + true + + + + + + + Export + + + + + + + + + + Masked Image1 + + + + + + + 0 + 0 + + + + + + + + + + + + + + 0 + 0 + 967 + 25 + + + + + Export + + + + + + + + + + Scatter Plot+Fit + + + + + Mask + + + + + Masked Image + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+ + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/midasMainwindow.ui b/xmidas/gui/layout/midasMainwindow.ui similarity index 67% rename from xmidas/uis/midasMainwindow.ui rename to xmidas/gui/layout/midasMainwindow.ui index eb98ef8..dcd166c 100644 --- a/xmidas/uis/midasMainwindow.ui +++ b/xmidas/gui/layout/midasMainwindow.ui @@ -1,2938 +1,3034 @@ - - - Ajith - MainWindow - - - true - - - - 0 - 0 - 1232 - 931 - - - - - 0 - 0 - - - - NSLS-II MIDAS - - - - pancake.icopancake.ico - - - font: 10pt "Segoe UI"; - - - - - true - - - - 0 - 0 - - - - QWidget { -font: 10pt "Segoe UI"; -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -font: 10pt "Segoe UI"; -} - -QLabel { -font: 10pt "Segoe UI"; -} - -QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - - - - 0 - 0 - - - - - - - 2 - - - - - 0 - 0 - 368 - 325 - - - - Adjust Image Dimensions - - - - - - - 0 - 0 - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - QLayout::SetMinimumSize - - - - - - 0 - 0 - - - - 0 - - - 500 - - - 0 - - - 10 - - - - - - - - 0 - 0 - - - - 1 - - - 5000 - - - 1200 - - - 10 - - - - - - - - 0 - 0 - - - - false - - - px - - - 1 - - - 10000 - - - 100 - - - - - - - - 0 - 0 - - - - false - - - px - - - 1 - - - 10000 - - - 100 - - - - - - - - 0 - 0 - - - - false - - - px - - - 0 - - - 10000 - - - 0 - - - - - - - - 0 - 0 - - - - Y Dimension - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - false - - - px - - - 0 - - - 10000 - - - 0 - - - - - - - - 0 - 0 - - - - X Dimension - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - Stack Range - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - - - 0 - 0 - - - - Adjust the dimensions of the image - - - - - - Update - - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - 0 - 0 - - - - Removes one column/row from all four edges - - - Remove Edges - - - - - - - - - true - - - - 0 - 0 - - - - rebin - - - - - - - true - - - - 0 - 0 - - - - Upscale - - - - - - - - - Ratio - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - true - - - - 0 - 0 - - - - font: 10pt "MS Shell Dlg 2"; - - - 2 - - - 16 - - - 2 - - - 2 - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - 0 - 0 - 300 - 392 - - - - Image Processing - - - - - - - - - reverses the image axes (ZXY) to (YXZ) - - - Transpose - - - - - - - swaps 2nd and 3rd axes (ZXY) to (ZYX) - - - Swap XY Axes - - - - - - - - - - - - 0 - 0 - - - - Covert image dat to log values. - - - - - - Log - - - false - - - - - - - - 0 - 0 - - - - Intensity Normalization with the last Frame. - - - Normalize to Max - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - Remove Outliers (NSigma) - - - - - - - false - - - - - - 200 - - - 3 - - - Qt::Horizontal - - - - - - - 1 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - - Thresholding - - - - - - - - - - - false - - - 100 - - - 5 - - - 5 - - - 5 - - - Qt::Horizontal - - - - - - - 5 - - - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - uses savgol_filter to smooth data - - - Smoothen - - - - - - - - - false - - - 3 - - - 12 - - - 2 - - - 2 - - - 3 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 2 - - - - - - - Window size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - - 0 - 0 - 379 - 309 - - - - Alignment - - - - - - - - - - - Transformations: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - TRANSLATION - - - - - RIGID_BODY - - - - - SCALED_ROTATION - - - - - AFFINE - - - - - BILINEAR - - - - - - - - - - - - Reference: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - previous - - - - - mean - - - - - first - - - - - - - - - - - - - 0 - 0 - - - - Iterative Mode - - - - - - - - - Max Iter. - - - - - - - - 0 - 0 - - - - 2 - - - 24 - - - - - - - - - - - - 0 - 0 - - - - Align - - - - - - - - - - 0 - 0 - - - - Load Reference Stack - - - - - - - No Ref. Available - - - true - - - - - - - - - - 0 - 0 - - - - Save Transformation File - - - - - - - Use - - - - - - - - 0 - 0 - - - - Load Transformation File - - - - - - - - - - - - - - - 0 - 0 - - - - - - - Reset Image - - - - - - - true - - - 2 - - - - - 0 - 0 - 379 - 321 - - - - Component Analysis - - - - - - - - - 0 - 0 - - - - - - - Calculate Components - - - - - - - - 0 - 0 - - - - - - - PCA Scree Plot - - - - - - - - - Method - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - PCA - - - - - NMF - - - - - FastICA - - - - - IncrementalPCA - - - - - TruncatedSVD - - - - - FactorAnalysis - - - - - DictionaryLearning - - - - - - - - - - - - Number of Components - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 1 - - - 4 - - - - - - - - - - - - - 0 - 0 - 379 - 321 - - - - Cluster Analysis - - - - - - - - - 0 - 0 - - - - - - - Calculate Clusters - - - - - - - - 0 - 0 - - - - - - - KMeans Variance Plot - - - - - - - Method - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - KMeans - - - - - MiniBatchKMeans - - - - - MeanShift - - - - - Spectral Clustering - - - - - Correlation-Kmeans - - - - - Affinity Propagation - - - - - - - - Number of Clusters - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 1 - - - 4 - - - - - - - - - - - 0 - 0 - 379 - 205 - - - - XANES Fitting - - - - - - - 11 - - - 11 - - - - - - - - 0 - 0 - - - - - - - Load Energy List - - - - - - - keV - - - - - - - - - - - - 0 - 0 - - - - - - - Load Ref. Spec. - - - - - - - - - - Plot - - - - - - - - - true - - - - 0 - 0 - - - - - - - Fit - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - QLayout::SetMaximumSize - - - - - 0 - - - - Live - - - - - - - 0 - 0 - - - - Send to Plot Collection - - - - - - - - 0 - 0 - - - - - - - Save - - - - - - - - 0 - 0 - - - - - - - - - Normalized - - - - - - Save - - - - - - - Clear - - - - - - - Norm. to Collector - - - - - - - - 0 - 0 - - - - - - - - - Collection - - - - - - - 0 - 0 - - - - - - - - Clear - - - - - - - Save - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - QLayout::SetDefaultConstraint - - - 11 - - - 11 - - - - - - 0 - 0 - - - - ROI Shape - - - - - - - 0 - 0 - - - - Rectangle - - - true - - - - - - - - 0 - 0 - - - - Ellipse - - - - - - - - 0 - 0 - - - - Polygon - - - false - - - - - - - - 0 - 0 - - - - Circle - - - - - - - - 0 - 0 - - - - Line - - - - - - - - 0 - 0 - - - - Zoom to ROI - - - - - - - - - - font: 9pt "Segoe UI"; - - - ROI Positions - - - - - - Spectrum ROI - - - - - - Range (eV): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - color: rgb(255, 0,0); - - - roi_size - - - - - - - color: rgb(255, 0,0); - - - roix, roiy - - - - - - - - - - Size (eV): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Image ROI - - - - - - color: rgb(255, 0,0); - - - roix, roiy - - - - - - - Position: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Size(pixels): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - color: rgb(255, 0, 0); - - - roi_size - - - - - - - - - - - - - - 0 - 0 - - - - 1 - - - - - 0 - 0 - 361 - 193 - - - - Image Calculations - - - - - - - - - - - - - - - Add ROI_2 - - - - - - - Calculation: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - Subtract - - - - - Divide - - - - - Add - - - - - Compare - - - - - - - - Apply - - - - - - - - - - - - 0 - 0 - 361 - 231 - - - - Spectrum Calculations - - - - - - - - - - - - - 0 - 0 - - - - Add ROI 2 - - - true - - - false - - - false - - - - - - - - - - 0 - 0 - - - - Calculation - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - Divide - - - - - Subtract - - - - - Add - - - - - Correlation Plot - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Apply - - - - - - - Use ROI for Correlations - - - - - - - - - - - - 0 - 0 - 386 - 273 - - - - XANES Normalization - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - 1 - - - 5 - - - - - - - - 0 - 0 - - - - Eo - - - - - - - - 0 - 0 - - - - eV - - - 1000.000000000000000 - - - 20000.000000000000000 - - - 7125.000000000000000 - - - - - - - - 0 - 0 - - - - calculate the energy point with maximum derivative - - - Auto Eo - - - - - - - - 0 - 0 - - - - eV - - - 2 - - - -500.000000000000000 - - - 500.000000000000000 - - - 1.000000000000000 - - - -50.000000000000000 - - - - - - - - 0 - 0 - - - - Pre-edge - - - - - - - - 0 - 0 - - - - eV - - - -500.000000000000000 - - - 500.000000000000000 - - - -10.000000000000000 - - - - - - - - 0 - 0 - - - - Post-edge - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Norm. Order - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - eV - - - 0.000000000000000 - - - 1000.000000000000000 - - - 25.000000000000000 - - - - - - - - 0 - 0 - - - - <html><head/><body><p> For mbak algorithm only, must be in <span style=" font-weight:600; font-style:italic;">element&lt;space&gt;edge</span> format</p></body></html> - - - Fe K - - - - - - - - 0 - 0 - - - - Element: - - - - - - - - 0 - 0 - - - - eV - - - 0.000000000000000 - - - 1500.000000000000000 - - - 75.000000000000000 - - - - - - - Initial Guess - - - - - - - - 0 - 0 - - - - Apply to Spectrum - - - - - - - Apply to Stack - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - 0 - 0 - 1232 - 29 - - - - - - - - Help - - - - - - - File - - - - - - - - - - - - true - - - Accessories - - - - - - - View - - - - Plot Background - - - - - - - - - - Window Background - - - - - - - - Change_Plot_Line_Width - - - - - - - - - - - - - - - - - Spectrum - - - - - - - - - Image - - - - - - - - - - - Batch - - - - - - - - - - - - - - - - - 0 - 0 - - - - toolBar - - - TopToolBarArea - - - false - - - - - - - - Open PDF - - - - - Open in GitHub (most updated) - - - - - true - - - Open Image Data - - - Support all tiff and specific h5 files - - - Ctrl+O - - - - - Close - - - - - Exit - - - Ctrl+Q - - - - - false - - - Export Tiff Stack - - - Save the displayed/Modified stack as a tiff file - - - Ctrl+S - - - - - Open PyXRF - - - - - Open Image J - - - - - Open TomViz - - - - - Open Mantis - - - - - Open Athena - - - - - DataBroker - - - - - Open HXN DB - - - - - false - - - Load Energy - - - Load list of energies for XANES stack. Supports only .txt fromat - - - Ctrl+E - - - - - Create a Virtual Stack - - - Create a stack from multiple tiff images of same shape - - - Ctrl+M - - - - - true - - - Open Mask Generator - - - A new window will be opened to creat threshold based masks - - - - - Create elist from log file - - - - - false - - - Export Energy List - - - - - true - - - MultiColorView - - - A new window will be opened to align images in a stack - - - - - White - - - - - true - - - true - - - Black - - - - - Red - - - - - Yellow - - - - - Blue - - - - - false - - - false - - - Dark Mode - - - - - Black - - - - - false - - - false - - - Default - - - - - Vivid - - - - - Export Sum Image (3D to 2D) - - - - - Subtract ROI as Background - - - - - Export Norm. Params - - - - - 2 - - - - - 4 - - - - - 6 - - - - - 10 - - - - - 2 - - - - - 3 - - - - - 4 - - - - - 5 - - - - - 6 - - - - - 8 - - - - - 10 - - - - - 2 - - - - - Import Norm. Params - - - - - 1 - - - - - Save Sum Spectrum - - - - - Save Mean Spectrum - - - - - Save Current Image as Mask - - - - - Stack to RGBCMY Image - - - - - Apply Current Crop to All images - - - all images in the folder will be cropped in reference to current - - - - - - - - - - - Save Current State - - - - - Normalize with another Stack - - - - - Stack Info - - - Diaplays details of the stack - - - - - Export Image - - - Saves displayed 2D image frame - - - - - - .. - - - Export Stack - - - Save current stack as is - - - false - - - - - Export Mean Image (3D to 2D) - - - - - Plot All Possible Image Correlations - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + Ajith + MainWindow + + + true + + + + 0 + 0 + 1287 + 842 + + + + + 0 + 0 + + + + NSLS-II MIDAS + + + + pancake.icopancake.ico + + + font: 10pt "Segoe UI"; + + + + + true + + + + 0 + 0 + + + + QWidget { +font: 10pt "Segoe UI"; +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 5,0); +font: 10pt "Segoe UI"; +} + +QLabel { +font: 10pt "Segoe UI"; +} + +QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 10px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #66e, stop: 1 #bbf); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #bbf, stop: 1 #55f); +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 13px; +margin-top: -2px; +margin-bottom: -2px; +border-radius: 4px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +border-radius: 2px; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +border-radius: 4px; +} + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + 0 + 0 + + + + + + + 0 + + + + + 0 + 0 + 302 + 288 + + + + Adjust Image Dimensions + + + + + + + 0 + 0 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + 0 + + + 500 + + + 0 + + + 10 + + + + + + + + 0 + 0 + + + + 1 + + + 5000 + + + 1200 + + + 10 + + + + + + + + 0 + 0 + + + + false + + + px + + + 1 + + + 10000 + + + 100 + + + + + + + + 0 + 0 + + + + false + + + px + + + 1 + + + 10000 + + + 100 + + + + + + + + 0 + 0 + + + + false + + + px + + + 0 + + + 10000 + + + 0 + + + + + + + + 0 + 0 + + + + Y Dimension + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + false + + + px + + + 0 + + + 10000 + + + 0 + + + + + + + + 0 + 0 + + + + X Dimension + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Stack Range + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + Adjust the dimensions of the image + + + + + + Update + + + + + + + Apply crop to all tiffs in directory + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + 0 + + + + Removes one column/row from all four edges + + + Remove Edges + + + + + + + + + true + + + + 0 + 0 + + + + rebin + + + + + + + true + + + + 0 + 0 + + + + Upscale + + + + + + + + + Ratio + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + 0 + 0 + + + + font: 10pt "MS Shell Dlg 2"; + + + 2 + + + 16 + + + 2 + + + 2 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + 233 + 365 + + + + Image Processing + + + + + + + + + reverses the image axes (ZXY) to (YXZ) + + + Transpose + + + + + + + swaps 2nd and 3rd axes (ZXY) to (ZYX) + + + Swap XY Axes + + + + + + + + + + + + 0 + 0 + + + + Covert image dat to log values. + + + + + + Log + + + false + + + + + + + + 0 + 0 + + + + Intensity Normalization with the last Frame. + + + Normalize to Max + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + Remove Outliers (NSigma) + + + + + + + false + + + + + + 200 + + + 3 + + + Qt::Horizontal + + + + + + + 1 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + Thresholding + + + + + + + + + + + false + + + 100 + + + 5 + + + 5 + + + 5 + + + Qt::Horizontal + + + + + + + 5 + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + uses savgol_filter to smooth data + + + Smoothen + + + + + + + + + false + + + 3 + + + 12 + + + 2 + + + 2 + + + 3 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 2 + + + + + + + Window size + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + 0 + 0 + 286 + 247 + + + + Alignment + + + + + + + + + + + Transformations: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + TRANSLATION + + + + + RIGID_BODY + + + + + SCALED_ROTATION + + + + + AFFINE + + + + + BILINEAR + + + + + + + + + + + + Reference: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + previous + + + + + mean + + + + + first + + + + + + + + + + + + + 0 + 0 + + + + Iterative Mode + + + + + + + + + Max Iter. + + + + + + + + 0 + 0 + + + + 2 + + + 24 + + + + + + + + + + + + 0 + 0 + + + + Align + + + + + + + + + + 0 + 0 + + + + Load Reference Stack + + + + + + + No Ref. Available + + + true + + + + + + + + + + 0 + 0 + + + + Save Transformation File + + + + + + + Use + + + + + + + + 0 + 0 + + + + Load Transformation File + + + + + + + + + + + + + + + 0 + 0 + + + + + + + Reset Image + + + + + + + true + + + 1 + + + + + 0 + 0 + 302 + 205 + + + + Component Analysis + + + + + + + + + 0 + 0 + + + + + + + Calculate Components + + + + + + + + 0 + 0 + + + + + + + PCA Scree Plot + + + + + + + + + Method + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + PCA + + + + + NMF + + + + + FastICA + + + + + IncrementalPCA + + + + + TruncatedSVD + + + + + FactorAnalysis + + + + + DictionaryLearning + + + + + + + + + + + + Number of Components + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 4 + + + + + + + + + + + + + 0 + 0 + 302 + 205 + + + + Cluster Analysis + + + + + + + + + 0 + 0 + + + + + + + Calculate Clusters + + + + + + + + 0 + 0 + + + + + + + KMeans Variance Plot + + + + + + + Method + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + KMeans + + + + + MiniBatchKMeans + + + + + MeanShift + + + + + Spectral Clustering + + + + + Correlation-Kmeans + + + + + Affinity Propagation + + + + + + + + Number of Clusters + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 4 + + + + + + + + + + + 0 + 0 + 302 + 205 + + + + XANES Fitting + + + + + + + 11 + + + 11 + + + + + + + + 0 + 0 + + + + + + + Load Energy List + + + + + + + keV + + + + + + + + + + + + 0 + 0 + + + + + + + Load Ref. Spec. + + + + + + + + + + Plot + + + + + + + + + true + + + + 0 + 0 + + + + + + + Fit + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + QLayout::SetMaximumSize + + + + + 0 + + + + Live + + + + + + + 0 + 0 + + + + Send to Plot Collection + + + + + + + + 0 + 0 + + + + + + + Save + + + + + + + + 0 + 0 + + + + + + + + + Normalized + + + + + + + 0 + 0 + + + + + + + + Norm. to Collector + + + + + + + Save + + + + + + + Clear + + + + + + + + Collection + + + + + + + 0 + 0 + + + + + + + + Clear + + + + + + + Save + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + + + font: 9pt "Segoe UI"; + + + ROI Positions + + + + + + Spectrum ROI + + + + + + Range (eV): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + color: rgb(255, 0,0); + + + roi_size + + + + + + + color: rgb(255, 0,0); + + + roix, roiy + + + + + + + + + + Size (eV): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Image ROI + + + + + + Position: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + color: rgb(255, 0,0); + + + roix, roiy + + + + + + + + + + Size(pixels): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + color: rgb(255, 0, 0); + + + roi_size + + + + + + + + + + + + + + 0 + 0 + + + + ROI Shape + + + + + + + 0 + 0 + + + + Ellipse + + + + + + + + 0 + 0 + + + + Rectangle + + + true + + + + + + + + 0 + 0 + + + + Polygon + + + false + + + + + + + + 0 + 0 + + + + Circle + + + + + + + + 0 + 0 + + + + Zoom to ROI + + + + + + + + 0 + 0 + + + + Line + + + + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + 2 + + + + + 0 + 0 + 305 + 384 + + + + Image Calculations + + + + + + + + + + + + + + + Add ROI_2 + + + + + + + Calculation: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + Subtract + + + + + Divide + + + + + Add + + + + + Compare + + + + + + + + Apply + + + + + + + + + + + + 0 + 0 + 323 + 384 + + + + Spectrum Calculations + + + + + + + + + + + + + 0 + 0 + + + + Add ROI 2 + + + true + + + false + + + false + + + + + + + + + + 0 + 0 + + + + Calculation + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + Divide + + + + + Subtract + + + + + Add + + + + + Correlation Plot + + + + + + + + + + Use ROI for Correlations + + + + + + + Apply + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + 365 + 384 + + + + XANES Normalization + + + + + + + + + + + + + + + 0 + 0 + + + + calculate the energy point with maximum derivative + + + Find Eo + + + + + + + + 0 + 0 + + + + Initial Guess + + + + + + + + + + + + 0 + 0 + + + + Element_Edge: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Pre-edge + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Post-edge + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p> For mbak algorithm only, must be in <span style=" font-weight:600; font-style:italic;">element&lt;space&gt;edge</span> format</p></body></html> + + + Fe_K + + + + + + + + 0 + 0 + + + + eV + + + 2 + + + -500.000000000000000 + + + 500.000000000000000 + + + 1.000000000000000 + + + -50.000000000000000 + + + + + + + + 0 + 0 + + + + eV + + + 0.000000000000000 + + + 1000.000000000000000 + + + 25.000000000000000 + + + + + + + + + + + + 0 + 0 + + + + Eo: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + to + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + eV + + + 1000.000000000000000 + + + 20000.000000000000000 + + + 7125.000000000000000 + + + + + + + + 0 + 0 + + + + eV + + + -500.000000000000000 + + + 500.000000000000000 + + + -10.000000000000000 + + + + + + + + 0 + 0 + + + + eV + + + 0.000000000000000 + + + 1500.000000000000000 + + + 75.000000000000000 + + + + + + + + + + + + + + + + 0 + 0 + + + + Norm. Order + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + 1 + + + 5 + + + + + + + + + + 0 + 0 + + + + Use Flattened + + + + + + + + 0 + 0 + + + + Apply to Spectrum + + + + + + + + 0 + 0 + + + + Apply to Stack + + + + + + + + 0 + 0 + + + + Ignore Post Edge (Stack Only) + + + + + + + MBAK Algorithm + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1287 + 24 + + + + + + + + Help + + + + + + + File + + + + + + + + + + + + true + + + Accessories + + + + + + + View + + + + Plot Background + + + + + + + + + + Window Background + + + + + + + + Change_Plot_Line_Width + + + + + + + + + + + + + + + + + Spectrum + + + + + + + + + Image + + + + + + + + + + + + Batch + + + + + + + + + + + + + + + + + 0 + 0 + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Open PDF + + + + + Open in GitHub (most updated) + + + + + true + + + Open Image Data + + + Support all tiff and specific h5 files + + + Ctrl+O + + + + + Close + + + + + Exit + + + Ctrl+Q + + + + + false + + + Export Tiff Stack + + + Save the displayed/Modified stack as a tiff file + + + Ctrl+S + + + + + Open PyXRF + + + + + Open Image J + + + + + Open TomViz + + + + + Open Mantis + + + + + Open Athena + + + + + DataBroker + + + + + Open HXN DB + + + + + false + + + Load Energy + + + Load list of energies for XANES stack. Supports only .txt fromat + + + Ctrl+E + + + + + Create a Virtual Stack + + + Create a stack from multiple tiff images of same shape + + + Ctrl+M + + + + + true + + + Open Mask Generator + + + A new window will be opened to creat threshold based masks + + + + + Create elist from log file + + + + + false + + + Export Energy List + + + + + true + + + MultiColorView + + + A new window will be opened to align images in a stack + + + + + White + + + + + true + + + true + + + Black + + + + + Red + + + + + Yellow + + + + + Blue + + + + + false + + + false + + + Dark Mode + + + + + Black + + + + + false + + + false + + + Default + + + + + Vivid + + + + + Export Sum Image (XYZ to sum(XY)) + + + + + Subtract ROI as Background + + + + + Export Norm. Params + + + + + 2 + + + + + 4 + + + + + 6 + + + + + 10 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 8 + + + + + 10 + + + + + 2 + + + + + Import Norm. Params + + + + + 1 + + + + + Save Sum Spectrum + + + + + Save Mean Spectrum + + + + + Save Current Image as Mask + + + + + Stack to RGBCMY Image + + + + + Apply Current Crop to All images + + + all images in the folder will be cropped in reference to current + + + + + + + + + + + Save Current State + + + + + Normalize with another Stack + + + + + Stack Info + + + Diaplays details of the stack + + + + + Export Image + + + Saves displayed 2D image frame + + + + + + .. + + + Export Stack + + + Save current stack as is + + + false + + + + + Export Mean Image (XYZ to mean(XY)) + + + + + Plot All Possible Image Correlations + + + + + Export Image as CSV (XYZ to Z,X*Y) + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+ + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/multipleScatterFit.ui b/xmidas/gui/layout/multipleScatterFit.ui similarity index 96% rename from xmidas/uis/multipleScatterFit.ui rename to xmidas/gui/layout/multipleScatterFit.ui index 7112364..64b9363 100644 --- a/xmidas/uis/multipleScatterFit.ui +++ b/xmidas/gui/layout/multipleScatterFit.ui @@ -1,114 +1,114 @@ - - - MainWindow - - - - 0 - 0 - 856 - 628 - - - - MainWindow - - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - 0 - 856 - 21 - - - - - View - - - - Plot Background - - - - - - - - - - - - - toolBar - - - TopToolBarArea - - - false - - - - - - - - Export - - - - - Save as PNG - - - - - Generate MultiColor Mask - - - - - Black - - - - - White - - - - - - GraphicsLayoutWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 856 + 628 + + + + MainWindow + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + 856 + 21 + + + + + View + + + + Plot Background + + + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Export + + + + + Save as PNG + + + + + Generate MultiColor Mask + + + + + Black + + + + + White + + + + + + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/mutlichannel.ui b/xmidas/gui/layout/mutlichannel.ui similarity index 95% rename from xmidas/uis/mutlichannel.ui rename to xmidas/gui/layout/mutlichannel.ui index 133f006..7315290 100644 --- a/xmidas/uis/mutlichannel.ui +++ b/xmidas/gui/layout/mutlichannel.ui @@ -1,458 +1,464 @@ - - - MainWindow - - - - 0 - 0 - 674 - 765 - - - - MainWindow - - - font: 12pt "Segoe UI" - - - - QPushButton { -border: 1px solid #555; -border-radius: 5px; -background: qradialgradient(cx: 0.3, cy: -0.1, -fx: 0.7, fy: 0.1, -radius: 1, stop: 0 #fff, stop: 1 #888); -background-color: rgb(170, 255, 255); -} - -QPushButton:hover{ - background-color: rgb(255, 255, 0); - } - -QPushButton:pressed{ - background-color: rgb(0,255, 0); - } - -font: 12pt "Segoe UI"; - - - - 25 - - - 25 - - - 25 - - - 25 - - - - - - 0 - 0 - - - - - - - - Edit - - - - - - - 0 - 0 - - - - 0 - - - - - 0 - 0 - 316 - 85 - - - - Thresholding - - - - - - - - - 0 - 0 - - - - 0,100 - - - Qt::AlignCenter - - - - - - - - - - - - 0 - 0 - - - - Qt::NoFocus - - - click update after making changes - - - 100 - - - 5 - - - 5 - - - 100 - - - 100 - - - true - - - Qt::Horizontal - - - false - - - false - - - QSlider::NoTicks - - - 5 - - - - - - - - - - - - 0 - 0 - - - - Qt::NoFocus - - - click update after making changes - - - 100 - - - 5 - - - 5 - - - 0 - - - 0 - - - true - - - Qt::Horizontal - - - QSlider::NoTicks - - - 5 - - - - - - - - - - - - - - - 0 - 0 - 316 - 85 - - - - Opacity - - - - - - - - - 0 - 0 - - - - 1 - - - Qt::AlignCenter - - - - - - - click update after making changes - - - 100 - - - 10 - - - 100 - - - Qt::Horizontal - - - - - - - - - - - - - execute above changes to the selected item - - - Update - - - - - - - - - Show Selected - - - - - - - Show All - - - - - - - - - - 0 - 0 - - - - change properties of the selected item - - - font: 8pt "Segoe UI"; - - - QAbstractScrollArea::AdjustToContents - - - - - - - - - - 0 - 0 - - - - Change Selected To - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - true - - - - - - - - - - - - - - - - - - 0 - 0 - 674 - 34 - - - - - File - - - true - - - - - - - - - - - - - Load Images - - - Select and load multiple tiff images to create a multi color view - - - - - Load 2 - - - - - Load 3 - - - - - Load 4 - - - - - Load 5 - - - - - Load 6 - - - - - Export Image - - - Export the image view as a sinle image file - - - - - Save State File - - - Save the current state of the view. Images and properties are saved - - - - - Load State File - - - Load a state (json file) saved previously. - - - - - Load Stack - - - Load images as a stack of tiff - - - - - - GraphicsLayoutWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 612 + 765 + + + + MainWindow + + + font: 12pt "Segoe UI" + + + + QPushButton { +border: 1px solid #555; +border-radius: 5px; +background: qradialgradient(cx: 0.3, cy: -0.1, +fx: 0.7, fy: 0.1, +radius: 1, stop: 0 #fff, stop: 1 #888); +background-color: rgb(170, 255, 255); +} + +QPushButton:hover{ + background-color: rgb(255, 255, 0); + } + +QPushButton:pressed{ + background-color: rgb(0,255, 0); + } + +font: 12pt "Segoe UI"; + + + + 25 + + + 25 + + + 25 + + + 25 + + + + + + 0 + 0 + + + + + + + + Edit + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + 0 + 254 + 85 + + + + Thresholding + + + + + + + + + 0 + 0 + + + + 0,100 + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + click update after making changes + + + 100 + + + 5 + + + 5 + + + 100 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::NoTicks + + + 5 + + + + + + + + + + + + 0 + 0 + + + + Qt::NoFocus + + + click update after making changes + + + 100 + + + 5 + + + 5 + + + 0 + + + 0 + + + true + + + Qt::Horizontal + + + QSlider::NoTicks + + + 5 + + + + + + + + + + + + + + + 0 + 0 + 98 + 81 + + + + Opacity + + + + + + + + + 0 + 0 + + + + 1 + + + Qt::AlignCenter + + + + + + + click update after making changes + + + 100 + + + 10 + + + 100 + + + Qt::Horizontal + + + + + + + + + + + + + execute above changes to the selected item + + + Update + + + + + + + + + Show Selected + + + + + + + Show All + + + + + + + + + + 0 + 0 + + + + change properties of the selected item + + + font: 8pt "Segoe UI"; + + + QAbstractScrollArea::AdjustToContents + + + + + + + + + + 0 + 0 + + + + Change Selected To + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + + + + + + + + + + + + + + + 0 + 0 + 612 + 34 + + + + + File + + + true + + + + + + + + + + + + + + Load Images + + + Select and load multiple tiff images to create a multi color view + + + + + Load 2 + + + + + Load 3 + + + + + Load 4 + + + + + Load 5 + + + + + Load 6 + + + + + Export Image + + + Export the image view as a sinle image file + + + + + Save State File + + + Save the current state of the view. Images and properties are saved + + + + + Load State File + + + Load a state (json file) saved previously. + + + + + Load Stack + + + Load images as a stack of tiff + + + + + Save Stack (.tiff) + + + + + + GraphicsLayoutWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/singleStackView.ui b/xmidas/gui/layout/singleStackView.ui similarity index 96% rename from xmidas/uis/singleStackView.ui rename to xmidas/gui/layout/singleStackView.ui index d832b04..22ee549 100644 --- a/xmidas/uis/singleStackView.ui +++ b/xmidas/gui/layout/singleStackView.ui @@ -1,151 +1,151 @@ - - - MainWindow - - - - 0 - 0 - 691 - 566 - - - - MainWindow - - - QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 0, 127); -} - - - - - - - - - - 0 - 0 - - - - - - - - - - Qt::Horizontal - - - - - - - font: 12pt "MS Shell Dlg 2"; - - - 10 - - - - - - - - - - - - - 0 - 0 - 691 - 26 - - - - - File - - - - - - - - - Save - - - - - - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 691 + 566 + + + + MainWindow + + + QSlider::groove:horizontal { +border: 1px solid #bbb; +background: white; +height: 10px; +border-radius: 4px; +} + +QSlider::sub-page:horizontal { +background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #66e, stop: 1 #bbf); +background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, + stop: 0 #bbf, stop: 1 #55f); +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::add-page:horizontal { +background: #fff; +border: 1px solid #777; +height: 10px; +border-radius: 4px; +} + +QSlider::handle:horizontal { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #eee, stop:1 #ccc); +border: 1px solid #777; +width: 13px; +margin-top: -2px; +margin-bottom: -2px; +border-radius: 4px; +} + +QSlider::handle:horizontal:hover { +background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #fff, stop:1 #ddd); +border: 1px solid #444; +border-radius: 2px; +} + +QSlider::sub-page:horizontal:disabled { +background: #bbb; +border-color: #999; +} + +QSlider::add-page:horizontal:disabled { +background: #eee; +border-color: #999; +} + +QSlider::handle:horizontal:disabled { +background: #eee; +border: 1px solid #aaa; +border-radius: 4px; +} + +QPushButton { +background-color: rgb(175, 236, 255); +color: rgb(255, 0, 127); +} + + + + + + + + + + 0 + 0 + + + + + + + + + + Qt::Horizontal + + + + + + + font: 12pt "MS Shell Dlg 2"; + + + 10 + + + + + + + + + + + + + 0 + 0 + 691 + 26 + + + + + File + + + + + + + + + Save + + + + + + ImageView + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/xanesFitStat.ui b/xmidas/gui/layout/xanesFitStat.ui similarity index 95% rename from xmidas/uis/xanesFitStat.ui rename to xmidas/gui/layout/xanesFitStat.ui index 25685bf..258e475 100644 --- a/xmidas/uis/xanesFitStat.ui +++ b/xmidas/gui/layout/xanesFitStat.ui @@ -1,47 +1,47 @@ - - - MainWindow - - - - 0 - 0 - 792 - 481 - - - - MainWindow - - - - - - - - - - - - - - - 0 - 0 - 792 - 26 - - - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
-
- - -
+ + + MainWindow + + + + 0 + 0 + 792 + 481 + + + + MainWindow + + + + + + + + + + + + + + + 0 + 0 + 792 + 26 + + + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/xmidas/uis/xrf_xanes_gui_3ID.ui b/xmidas/gui/layout/xrf_xanes_gui_3ID.ui similarity index 97% rename from xmidas/uis/xrf_xanes_gui_3ID.ui rename to xmidas/gui/layout/xrf_xanes_gui_3ID.ui index 8755841..18a9043 100644 --- a/xmidas/uis/xrf_xanes_gui_3ID.ui +++ b/xmidas/gui/layout/xrf_xanes_gui_3ID.ui @@ -1,756 +1,756 @@ - - - Ajith - MainWindow - - - - 0 - 0 - 820 - 553 - - - - HXN_Wizard - - - font: 12pt "MS Shell Dlg 2"; - - - - - - - - - - 0 - 0 - - - - - - - - 20 - - - - - - - - 0 - 0 - - - - - - - C:\Matrix\Blue Pill\Morpheus.csv - - - - - - - - 0 - 0 - - - - - - - C:\Matrix\Blue Pill - - - - - - - 0 - - - 10 - - - - - - 0 - 0 - - - - Select Working Directory - - - - - - - - 0 - 0 - - - - Select Paramater File - - - - - - - - 0 - 0 - - - - Select XANES Reference File (xanes only) - - - - - - - - - - 0 - 0 - - - - - - - C:\Matrix\Blue Pill\TheChosenOne.json - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - background-color: rgb(255, 221, 98); - - - Open PyXRF GUI - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - font: 12pt "MS Shell Dlg 2"; - - - 1 - - - false - - - - XRF - - - - - - XRF Batch Processing - - - Qt::AlignCenter - - - - - - 150 - - - 150 - - - - - 92158 - - - 00000 - - - - - - - 92102 - - - 00000 - - - - - - - Last Scan ID: - - - - - - - Scalar Name: - - - - - - - First Scan ID: - - - - - - - sclr1_ch4 - - - Fe_K - - - - - - - background-color: rgb(85, 255, 127); - - - Start Batch Processing - - - - - - - - - - - - 150 - - - 150 - - - - - XRF Live Processing - - - Qt::AlignCenter - - - - - - - - background-color: rgb(0, 170, 255); -background-color: rgb(67, 246, 255); - - - Initiate Live - - - - - - - background-color: rgb(255, 130, 67); -font: 75 12pt "MS Shell Dlg 2"; - - - - Start - - - - - - - Live Processing is not ready - - - Qt::AlignCenter - - - - - - - - - - - - - - - XANES - - - - - - 50 - - - 50 - - - - - - - First Scan ID: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 92102 - - - 00000 - - - - - - - XANES Element - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Fe_K - - - Fe_K - - - - - - - Alignemnt Element - - - - - - - P_K - - - Pt_L - - - - - - - - - 50 - - - 0 - - - - - Last Scan ID: - - - - - - - 92158 - - - 00000 - - - - - - - Scalar Name - - - - - - - sclr1_ch4 - - - Fe_K - - - - - - - Save All Elem tiff Stacks - - - true - - - - - - - - 0 - 0 - - - - Subtract Pre-edge - - - - - - - - - - - Qt::Horizontal - - - - 49 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 48 - - - - - - - - 20 - - - 20 - - - - - - 0 - 0 - - - - Fitting method - - - - - - - - 0 - 0 - - - - - nnls - - - - - admm - - - - - - - - false - - - - 0 - 0 - - - - Lambda for ADMM: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - false - - - - 0 - 0 - - - - Qt::StrongFocus - - - 5 - - - Lambda for ADMM - - - - - - - - 0 - 0 - - - - Energy Shift (eV) - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - 0 - - - Lambda for ADMM - - - - - - - Work Flow - - - - - - - - 0 - 0 - - - - - load_and_process - - - - - process - - - - - build_xanes_map - - - - - - - - font: 12pt "MS Shell Dlg 2"; -background-color: rgb(170, 255, 255); - - - Go - - - - - - - background-color: rgb(255, 77, 46); -color: rgb(0, 0, 0); -font: 75 10pt "MS Shell Dlg 2"; - - - Close All Plots - - - - - - - - - Qt::Horizontal - - - - 49 - 20 - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 52 - - - - - - - - - - 0 - 0 - 820 - 25 - - - - - HELP - - - - - - - - - + + + Ajith + MainWindow + + + + 0 + 0 + 820 + 553 + + + + HXN_Wizard + + + font: 12pt "MS Shell Dlg 2"; + + + + + + + + + + 0 + 0 + + + + + + + + 20 + + + + + + + + 0 + 0 + + + + + + + C:\Matrix\Blue Pill\Morpheus.csv + + + + + + + + 0 + 0 + + + + + + + C:\Matrix\Blue Pill + + + + + + + 0 + + + 10 + + + + + + 0 + 0 + + + + Select Working Directory + + + + + + + + 0 + 0 + + + + Select Paramater File + + + + + + + + 0 + 0 + + + + Select XANES Reference File (xanes only) + + + + + + + + + + 0 + 0 + + + + + + + C:\Matrix\Blue Pill\TheChosenOne.json + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + background-color: rgb(255, 221, 98); + + + Open PyXRF GUI + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + font: 12pt "MS Shell Dlg 2"; + + + 1 + + + false + + + + XRF + + + + + + XRF Batch Processing + + + Qt::AlignCenter + + + + + + 150 + + + 150 + + + + + 92158 + + + 00000 + + + + + + + 92102 + + + 00000 + + + + + + + Last Scan ID: + + + + + + + Scalar Name: + + + + + + + First Scan ID: + + + + + + + sclr1_ch4 + + + Fe_K + + + + + + + background-color: rgb(85, 255, 127); + + + Start Batch Processing + + + + + + + + + + + + 150 + + + 150 + + + + + XRF Live Processing + + + Qt::AlignCenter + + + + + + + + background-color: rgb(0, 170, 255); +background-color: rgb(67, 246, 255); + + + Initiate Live + + + + + + + background-color: rgb(255, 130, 67); +font: 75 12pt "MS Shell Dlg 2"; + + + + Start + + + + + + + Live Processing is not ready + + + Qt::AlignCenter + + + + + + + + + + + + + + + XANES + + + + + + 50 + + + 50 + + + + + + + First Scan ID: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 92102 + + + 00000 + + + + + + + XANES Element + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Fe_K + + + Fe_K + + + + + + + Alignemnt Element + + + + + + + P_K + + + Pt_L + + + + + + + + + 50 + + + 0 + + + + + Last Scan ID: + + + + + + + 92158 + + + 00000 + + + + + + + Scalar Name + + + + + + + sclr1_ch4 + + + Fe_K + + + + + + + Save All Elem tiff Stacks + + + true + + + + + + + + 0 + 0 + + + + Subtract Pre-edge + + + + + + + + + + + Qt::Horizontal + + + + 49 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 48 + + + + + + + + 20 + + + 20 + + + + + + 0 + 0 + + + + Fitting method + + + + + + + + 0 + 0 + + + + + nnls + + + + + admm + + + + + + + + false + + + + 0 + 0 + + + + Lambda for ADMM: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + + 0 + 0 + + + + Qt::StrongFocus + + + 5 + + + Lambda for ADMM + + + + + + + + 0 + 0 + + + + Energy Shift (eV) + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + 0 + + + Lambda for ADMM + + + + + + + Work Flow + + + + + + + + 0 + 0 + + + + + load_and_process + + + + + process + + + + + build_xanes_map + + + + + + + + font: 12pt "MS Shell Dlg 2"; +background-color: rgb(170, 255, 255); + + + Go + + + + + + + background-color: rgb(255, 77, 46); +color: rgb(0, 0, 0); +font: 75 10pt "MS Shell Dlg 2"; + + + Close All Plots + + + + + + + + + Qt::Horizontal + + + + 49 + 20 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 52 + + + + + + + + + + 0 + 0 + 820 + 25 + + + + + HELP + + + + + + + + + diff --git a/xmidas/uis/animation.gif b/xmidas/gui/resources/images/animation.gif old mode 100644 new mode 100755 similarity index 100% rename from xmidas/uis/animation.gif rename to xmidas/gui/resources/images/animation.gif diff --git a/xmidas/uis/animation_atom.gif b/xmidas/gui/resources/images/animation_atom.gif old mode 100644 new mode 100755 similarity index 100% rename from xmidas/uis/animation_atom.gif rename to xmidas/gui/resources/images/animation_atom.gif diff --git a/xmidas/uis/pancake.ico b/xmidas/gui/resources/images/pancake.ico old mode 100644 new mode 100755 similarity index 100% rename from xmidas/uis/pancake.ico rename to xmidas/gui/resources/images/pancake.ico diff --git a/xmidas/uis/splash.gif b/xmidas/gui/resources/images/splash.gif old mode 100644 new mode 100755 similarity index 100% rename from xmidas/uis/splash.gif rename to xmidas/gui/resources/images/splash.gif diff --git a/xmidas/gui/windows/decomposition_viewer.py b/xmidas/gui/windows/decomposition_viewer.py new file mode 100644 index 0000000..89c19d9 --- /dev/null +++ b/xmidas/gui/windows/decomposition_viewer.py @@ -0,0 +1,392 @@ +import logging +import sys +import os +import json +import scipy.stats as stats +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg +import pyqtgraph.exporters +from glob import glob +from scipy.stats import linregress +from packaging import version + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR + +from xmidas.gui.windows.multichannel_viewer import MultiChannelWindow +from xmidas.utils import * +from xmidas.utils.color_maps import * +from xmidas.models.encoders import jsonEncoder +from xmidas.gui.windows.singleStackViewer import * +cmap_dict = create_color_maps() + +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../layout" +)) + + + + +class DecomposeViewer(QtWidgets.QMainWindow): + mask_signal: pyqtSignal = QtCore.pyqtSignal(np.ndarray) + mask_and_path_signal: pyqtSignal = QtCore.pyqtSignal(list) + def __init__(self, to_decompose, im_stack, energy): + super(DecomposeViewer, self).__init__() + + # Load the UI Page + uic.loadUi(os.path.join(ui_dir, "decomposeViewer.ui"), self) + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.user_wd = os.path.abspath("~") + + self.to_decompose = to_decompose + if im_stack is None: + self.im_stack = self.to_decompose + else: + self.im_stack = im_stack + self.energy = energy + self.eigen_img = None + self.eigen_spectra = None + self.decon_spectra = None + self.decon_map = None + + if self.to_decompose.ndim==2: + self.to_decompose = np.expand_dims(self.to_decompose, axis=0) + + + (self.dim1, self.dim3, self.dim2) = self.to_decompose.shape + #self.hs_comp_number.setMaximum(self.dim1 - 1) + self.eigen_image_view.ui.menuBtn.hide() + self.eigen_image_view.ui.roiBtn.hide() + self.eigen_image_view.setPredefinedGradient("viridis") + + self.eigen_masked_spectrum_view.setLabel("bottom", "Energy") + self.eigen_masked_spectrum_view.setLabel("left", "Intensity", "A.U.") + + self.eigen_spectrum_view.setLabel("bottom", "Energy") + self.eigen_spectrum_view.setLabel("left", "Weight", "A.U.") + + self.pb_view_stack.clicked.connect(self.view_stack) + + + # connection + self.pb_show_all.clicked.connect(lambda:self.show_all_spec(norm_to_max = True, add_offset = True)) + self.hs_comp_number.valueChanged.connect(self.update_plots) + self.actionSave.triggered.connect(self.save_comp_data) + self.pb_send_mask.clicked.connect(self.send_mask) + #self.pb_openScatterPlot.clicked.connect(self.openScatterPlot) + # self.pb_showMultiColor.clicked.connect(lambda: self.generateMultiColorView(withSpectra=False)) + # self.pb_showMultiImageXANESView.clicked.connect(lambda: self.generateMultiColorView(withSpectra=True)) + self.pb_calc_components.clicked.connect(self.decompose_and_display) + self.pb_calc_cluster.clicked.connect(self.cluster_decomposed) + self.sldr_mask_low.valueChanged.connect(lambda value: self.dsb_low_threshold.setValue(value / 100.0)) + self.sldr_mask_high.valueChanged.connect(lambda value: self.dsb_high_threshold.setValue(value / 100.0)) + self.pb_apply_threshold.clicked.connect(lambda:self.create_mask(self.hs_comp_number.value())) + self.pb_browse_save_path.clicked.connect(self.browse_folder) + self.pb_send_all_and_save.clicked.connect(self.send_mask_and_save) + + + def view_stack(self): + self.newWindow = singleStackViewer(self.im_stack) + self.newWindow.show() + + def decompose_and_display(self): + self.eigen_img,self.eigen_spectra,self.decon_spectra,self.decon_map = decompose_stack( + self.to_decompose, + decompose_method=self.cb_comp_method.currentText(), + n_components_=self.sb_ncomp.value()) + + self.hs_comp_number.setMaximum(self.eigen_img.shape[0]-1) + self.update_plots(0) + + def cluster_decomposed(self): + + self.eigen_img, X_cluster, self.decon_spectra = cluster_stack( + self.to_decompose, + method=self.cb_clust_method.currentText(), + n_clusters_=self.sb_ncluster.value(), + decomposed=self.cb_use_comp_for_cluster.isChecked(), + decompose_method=self.cb_comp_method.currentText(), + decompose_comp=self.sb_ncomp.value()) + self.eigen_spectra = self.decon_spectra + self.hs_comp_number.setMaximum((self.eigen_img.shape)[0]-1) + self.update_plots(0) + + + def update_plots(self, im_index): + self.eigen_image_view.setImage(self.eigen_img[im_index]) + + try: + self.eigen_masked_spectrum_view.plot(self.energy, self.decon_spectra[:, im_index], clear=True) + self.eigen_spectrum_view.plot(self.energy, self.eigen_spectra[:, im_index], clear=True) + except: + pass + self.label_comp_number.setText(f"{im_index + 1}/{self.eigen_img.shape[0]}") + #print(f"{self.eigen_img.shape = }") + self.create_mask(im_index) + + def create_mask(self, im_index): + + self.threshold_low = self.dsb_low_threshold.value() + self.threshold_high = self.dsb_high_threshold.value() + self.dsb_low_threshold.setMaximum(self.threshold_high-1e-10) + self.dsb_high_threshold.setMinimum(self.threshold_low+1e-10) + + self.norm_mask = remove_nan_inf(self.eigen_img[im_index]) / np.nanmax(self.eigen_img[im_index]) + self.norm_mask[(self.norm_mask < self.threshold_low) | + (self.norm_mask > self.threshold_high)] = 0 + + self.mask = self.norm_mask + self.binary_mask = np.where(self.mask > 0, 1, 0) + self.eigen_mask_image_view.setImage(self.binary_mask) + + def send_mask(self): + + """ Send masked xanes viewer""" + + if self.cb_use_binary_mask.isChecked(): + self.mask_signal.emit(self.im_stack*self.binary_mask[np.newaxis,:,:]) + else: + self.mask_signal.emit(self.im_stack*self.mask[np.newaxis,:,:]) + + def browse_folder(self): + folder_path = QFileDialog.getExistingDirectory(self, "Select Folder") + if folder_path: + self.le_save_path.setText(folder_path) + + def send_mask_and_save(self): + + save_path = os.path.join(self.le_save_path.text(), f"Cluster_{self.hs_comp_number.value()}") + if self.cb_use_binary_mask.isChecked(): + self.mask_and_path_signal.emit([self.im_stack*self.binary_mask[np.newaxis,:,:], save_path]) + else: + self.mask_and_path_signal.emit([self.im_stack*self.mask[np.newaxis,:,:], save_path]) + + + def show_all_spec(self, norm_to_max = True, add_offset = True): + self.eigen_masked_spectrum_view.clear() + self.plt_colors = ["g", "b", "r", "c", "m", "y", "w"] * 10 + offsets = np.arange(0, 2, 0.2) + self.eigen_masked_spectrum_view.addLegend() + for ii in range(self.decon_spectra.shape[1]): + to_plot = self.decon_spectra[:, ii] + if norm_to_max: + to_plot = self.decon_spectra[:, ii] / self.decon_spectra[:, ii].max() + if add_offset: + to_plot = to_plot+ + offsets[ii] + + self.eigen_masked_spectrum_view.plot( + self.energy, + to_plot, + pen=self.plt_colors[ii], + name="component" + str(ii + 1), + ) + self.eigen_spectrum_view.clear() + self.eigen_spectrum_view.addLegend() + for ii in range(self.eigen_spectra.shape[1]): + to_plot = self.eigen_spectra[:, ii] + if norm_to_max: + to_plot = self.eigen_spectra[:, ii] / self.eigen_spectra[:, ii].max() + if add_offset: + to_plot = to_plot+ + offsets[ii] + self.eigen_spectrum_view.plot( + self.energy, + to_plot, + pen=self.plt_colors[ii], + name="eigen_vector" + str(ii + 1), + ) + + def save_comp_data(self): + file_name = QFileDialog().getSaveFileName(self, "save all data", self.user_wd, "data(*tiff *tif *txt *png )") + if file_name[0]: + self.show_all_spec(norm_to_max = False, add_offset = False) + tf.imwrite(file_name[0] + "_eigen_weights.tiff", np.float32(self.self.eigen_img)) + tf.imwrite(file_name[0] + "_eigen_masks.tiff", np.float32(self.decon_map)) + exporter_spec = pg.exporters.CSVExporter(self.eigen_masked_spectrum_view.plotItem) + exporter_spec.parameters()["columnMode"] = "(x,y) per plot" + exporter_spec.export(file_name[0] + "_deconv_spec.csv") + exporter_eigen = pg.exporters.CSVExporter(self.eigen_spectrum_view.plotItem) + exporter_eigen.parameters()["columnMode"] = "(x,y) per plot" + exporter_eigen.export(file_name[0] + "_eigen_vectors.csv") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def generateMultiColorView(self, withSpectra=False): + self.multichanneldict = {} + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.to_decompose.transpose(0, 1, 2))): + low, high = np.min(image), np.max(image) + self.multichanneldict[f'Image {n + 1}'] = {'ImageName': f'Image {n + 1}', + 'ImageDir': '.', + 'Image': image, + 'Color': colorName, + 'CmapLimits': (low, high), + 'Opacity': 1.0 + } + + if withSpectra: + compXanesSpetraAll = pd.DataFrame() + compXanesSpetraAll['Energy'] = self.energy + + for n, spec in enumerate(self.decon_spectra.T): + compXanesSpetraAll[f'Component_{n + 1}'] = spec + + # self.muli_color_window = MultiXANESWindow(image_dict=self.multichanneldict, + # spec_df=compXanesSpetraAll) + else: + self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) + + self.muli_color_window.show() + + # add energy column +class ComponentScatterPlot(QtWidgets.QMainWindow): + def __init__(self, decomp_stack, specs): + super(ComponentScatterPlot, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "ComponentScatterPlot.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.w1 = self.scatterViewer.addPlot() + self.decomp_stack = decomp_stack + self.specs = specs + (self.dim1, self.dim3, self.dim2) = self.decomp_stack.shape + # fill the combonbox depending in the number of components for scatter plot + for n, combs in enumerate(combinations(np.arange(self.dim1), 2)): + self.cb_scatter_comp.addItem(str(combs)) + self.cb_scatter_comp.setItemData(n, combs) + + self.s1 = pg.ScatterPlotItem(size=3, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 0, 120)) + + self.setImageAndScatterPlot() + # connections + self.actionSave_Plot.triggered.connect(self.pg_export_correlation) + self.actionSave_Images.triggered.connect(self.tiff_export_images) + self.pb_updateComponents.clicked.connect(self.setImageAndScatterPlot) + self.pb_define_mask.clicked.connect(self.createMask) + self.pb_apply_mask.clicked.connect(self.getMaskRegion) + self.pb_reset_mask.clicked.connect(self.resetMask) + self.pb_addALine.clicked.connect(lambda: self.createMask(Line=True)) + + def setImageAndScatterPlot(self): + + try: + self.s1.clear() + except Exception: + pass + + comp_tuple = self.cb_scatter_comp.currentData() + self.img1, self.img2 = self.decomp_stack[comp_tuple[0]], self.decomp_stack[comp_tuple[-1]] + self.image_view.setImage(self.decomp_stack[comp_tuple[0]]) + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + self.image_view.setPredefinedGradient("bipolar") + + self.image_view2.setImage(self.decomp_stack[comp_tuple[-1]]) + self.image_view2.ui.menuBtn.hide() + self.image_view2.ui.roiBtn.hide() + self.image_view2.setPredefinedGradient("bipolar") + + points = [] + for i, j in zip(self.img1.flatten(), self.img2.flatten()): + + points.append( + { + "pos": (i, j), + "data": "id", + "size": 5, + "pen": pg.mkPen(None), + "brush": pg.mkBrush(255, 255, 0, 160), + } + ) + + self.s1.addPoints(points) + self.w1.addItem(self.s1) + # self.s1.setData(self.specs[:, comp_tuple[0]], self.specs[:, comp_tuple[-1]]) + self.w1.setLabel("bottom", f"PC{comp_tuple[0]+1}") + self.w1.setLabel("left", f"PC{comp_tuple[-1]+1}") + self.label_im1.setText(f"PC{comp_tuple[0]+1}") + self.label_im2.setText(f"PC{comp_tuple[-1]+1}") + + def createMask(self, Line=False): + + self.size = self.img1.max() / 10 + self.pos = int(self.img1.mean()) + + if Line: + self.lineROI = pg.LineSegmentROI( + [0, 1], + pos=(self.pos, self.pos), + pen=pg.mkPen("r", width=4), + hoverPen=pg.mkPen("g", width=4), + removable=True, + ) + self.w1.addItem(self.lineROI) + + else: + + self.scatter_mask = pg.PolyLineROI( + [[0, 0], [0, self.size], [self.size, self.size], [self.size, 0]], + pos=(self.pos, self.pos), + pen=pg.mkPen("r", width=4), + hoverPen=pg.mkPen("g", width=4), + closed=True, + removable=True, + ) + + self.w1.addItem(self.scatter_mask) + + def resetMask(self): + self.clearMask() + self.createMask() + + def clearMask(self): + try: + self.w1.removeItem(self.scatter_mask) + except AttributeError: + pass + + def clearPgPlot(self): + try: + self.masked_img.close() + except Exception: + pass + + def getMaskRegion(self): + + # Ref : https://stackoverflow.com/questions/57719303/how-to-map-mouse-position-on-a-scatterplot + + roiShape = self.scatter_mask.mapToItem(self.s1, self.scatter_mask.shape()) + self._points = list() + logger.info("Building Scatter Plot Window; Please wait..") + for i in range(len(self.img1.flatten())): + self._points.append(QtCore.QPointF(self.img1.flatten()[i], self.img2.flatten()[i])) + + selected = [roiShape.contains(pt) for pt in self._points] + img_selected = np.reshape(selected, (self.img1.shape)) + + self.masked_img = singleStackViewer(img_selected * self.img1, gradient="bipolar") + self.masked_img.show() + + def pg_export_correlation(self): + + exporter = pg.exporters.CSVExporter(self.w1) + exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, "save correlation", "", "spectrum and fit (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + else: + pass + + def tiff_export_images(self): + file_name = QFileDialog().getSaveFileName(self, "save images", "", "spectrum and fit (*tiff)") + if file_name[0]: + tf.imwrite(file_name[0] + ".tiff", np.dstack([self.img1, self.img2]).T) + self.statusbar.showMessage(f"Images saved to {file_name[0]}") + else: + pass \ No newline at end of file diff --git a/xmidas/gui/windows/mask_maker.py b/xmidas/gui/windows/mask_maker.py new file mode 100644 index 0000000..84e3a0b --- /dev/null +++ b/xmidas/gui/windows/mask_maker.py @@ -0,0 +1,288 @@ +import argparse +import logging +import sys +import webbrowser +import traceback +import os +import json +import scipy.stats as stats +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg +import pyqtgraph.exporters +from glob import glob +from pyqtgraph import plot +from itertools import combinations +from scipy.stats import linregress +from packaging import version + + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR + +from xmidas.utils import * +from xmidas.utils.color_maps import * +from xmidas.models.encoders import jsonEncoder +cmap_dict = create_color_maps() +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../layout" +)) + + + +class MaskSpecViewer(QtWidgets.QMainWindow): + mask_signal: pyqtSignal = QtCore.pyqtSignal(np.ndarray) + def __init__(self, xanes_stack=None, mask_map=None, energy=[], refs = None): + super(MaskSpecViewer, self).__init__() + uic.loadUi(os.path.join(ui_dir, "MaskSpecViewer.ui"), self) + self.user_wd = os.path.abspath("~") + + self.xanes_stack = xanes_stack + self.mask_map = mask_map + self.energy = energy + #self.mask_map = self.xanes_stack[-1] + self.refs = refs #TODO fitting option + self.view_data() + + # connections + self.dsb_low_threshold.valueChanged.connect(self.create_mask) + self.dsb_high_threshold.valueChanged.connect(self.create_mask) + self.pb_apply_mask.clicked.connect(self.apply_mask_to_xanes) + self.action_export_mask.triggered.connect(self.export_mask) + self.action_import_mask.triggered.connect(self.import_a_mask) + self.actionLoad_Energy_List.triggered.connect(self.load_energy) + self.actionLoad_XANES_Stack.triggered.connect(self.load_xanes_stack) + self.actionLoad_XRF_Map.triggered.connect(lambda:self.load_mask_map()) + self.pb_send_mask.clicked.connect(self.send_mask) + self.sldr_mask_low.valueChanged.connect(lambda value: self.dsb_low_threshold.setValue(value / 100.0)) + self.sldr_mask_high.valueChanged.connect(lambda value: self.dsb_high_threshold.setValue(value / 100.0)) + + def view_data(self): + + self.xanes_view.setImage(self.xanes_stack) + self.xanes_view.ui.menuBtn.hide() + self.xanes_view.ui.roiBtn.hide() + (self.dim1, self.dim3, self.dim2) = self.xanes_stack.shape + self.xanes_view.setPredefinedGradient("viridis") + self.xanes_view.setCurrentIndex(self.dim1 // 2) + self.statusbar.showMessage("One image from the XANES stack is used as mask") + self.xrf_view.setImage(self.mask_map) + self.xrf_view.ui.menuBtn.hide() + self.xrf_view.ui.roiBtn.hide() + self.xrf_view.setPredefinedGradient("viridis") + + self.mask_view.ui.menuBtn.hide() + self.mask_view.ui.roiBtn.hide() + + def create_mask(self): + + self.threshold_low = self.dsb_low_threshold.value() + self.threshold_high = self.dsb_high_threshold.value() + self.dsb_low_threshold.setMaximum(self.threshold_high-0.001) + self.dsb_high_threshold.setMinimum(self.threshold_low+0.001) + + self.norm_mask = remove_nan_inf(self.mask_map) / np.nanmax(self.mask_map) + self.norm_mask[(self.norm_mask < self.threshold_low) | + (self.norm_mask > self.threshold_high)] = 0 + + # self.norm_mask[self.norm_mask < self.threshold_low] = 0 + # self.norm_mask[self.norm_mask > self.threshold_high] = 0 + self.xrf_view.setImage(self.norm_mask) + self.statusbar.showMessage("New Threshold Applied") + self.binary_mask = np.where(self.norm_mask > 0, 1, 0) + self.mask_view.setImage(self.binary_mask) + + + def load_xanes_stack(self): + """loading a new xanes stack""" + filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") + self.file_name = str(filename[0]) + self.xanes_stack = tf.imread(self.file_name).transpose(0, 2, 1) + self.view_data() + + def load_energy(self): + """To load energy list that will be used for plotting the spectra. + number of stack should match length of energy list""" + + file_name = QFileDialog().getOpenFileName(self, "Open energy list", "", "text file (*.txt)") + + try: + self.energy = np.loadtxt(file_name[0]) + logger.info("Energy file loaded") + assert len(self.energy) == self.dim1 + self.view_data() + + except OSError: + logger.error("No File selected") + pass + + def load_mask_map(self, z = -1): + """Array for masking.Z will be used if 3D """ + + filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") + self.xrf_file_name = str(filename[0]) + self.mask_map = tf.imread(self.xrf_file_name) + if self.mask_map.ndim == 3: + self.mask_map = self.mask_map[z] + + else: + self.mask_map = self.mask_map.T + + assert ( + self.dim3, + self.dim2, + ) == self.mask_map.shape, f"Unexpected image dimensions: {self.mask_map.shape} vs {(self.dim2,self.dim3)}" + + self.view_data() + self.create_mask() + + def send_mask(self): + + """ Apply mask to xanes viewer""" + + if self.cb_use_binary_mask.isChecked(): + self.mask_signal.emit(self.binary_mask) + else: + self.mask_signal.emit(self.norm_mask) + + def apply_mask_to_xanes(self): + + """Generates a mask with 0 and 1 from the choosen threshold and multply with the xanes stack. + A spectrum will be generated from the new masked stack""" + if self.cb_use_binary_mask.isChecked(): + self.masked_xanes = self.xanes_stack * self.binary_mask[np.newaxis,:,:] + + else: + self.masked_xanes = self.xanes_stack * self.norm_mask[np.newaxis,:,:] + + self.xanes_view.setImage(self.masked_xanes) + self.xanes_view.setCurrentIndex(self.dim1 // 2) + self.statusbar.showMessage("Mask Applied to XANES") + self.mask_spec = get_mean_spectra(self.masked_xanes) + + if len(self.energy) != 0: + self.xdata = self.energy + else: + self.xdata = np.arange(0, self.dim1) + self.statusbar.showMessage("No Energy List Available; Integer values are used for plotting") + + self.spectrum_view.plot(self.xdata, self.mask_spec, clear=True) + + def import_a_mask(self): + filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") + xrf_file_name = str(filename[0]) + self.binary_mask = tf.imread(xrf_file_name).T + self.statusbar.showMessage("A New Mask Imported") + self.mask_view.setImage(self.binary_mask) + self.apply_mask_to_xanes() + + def export_mask(self): + try: + file_name = QFileDialog().getSaveFileName(self, "Save image data", "", "image file(*tiff *tif )") + tf.imwrite(file_name[0] + ".tiff", self.binary_mask.T) + logger.info(f"Updated Image Saved: {file_name[0]}") + self.statusbar.showMessage("Mask Exported") + except Exception: + logger.error("No file to save") + pass + + +class MaskMaker(QtWidgets.QMainWindow): + mask_signal: pyqtSignal = QtCore.pyqtSignal(np.ndarray) + def __init__(self, im_stack = None, mask_map=None): + super(MaskMaker, self).__init__() + uic.loadUi(os.path.join(ui_dir, "MaskMaker.ui"), self) + self.user_wd = os.path.abspath("~") + + self.im_stack = im_stack + self.mask_map = mask_map + self.view_data() + + # connections + self.dsb_low_threshold.valueChanged.connect(self.create_mask) + self.dsb_high_threshold.valueChanged.connect(self.create_mask) + self.action_export_mask.triggered.connect(self.export_mask) + self.action_import_mask.triggered.connect(self.import_a_mask) + self.actionLoad_XRF_Map.triggered.connect(lambda:self.load_mask_map()) + self.pb_send_mask.clicked.connect(self.send_mask) + self.sldr_mask_low.valueChanged.connect(lambda value: self.dsb_low_threshold.setValue(value / 100.0)) + self.sldr_mask_high.valueChanged.connect(lambda value: self.dsb_high_threshold.setValue(value / 100.0)) + + def view_data(self): + self.xrf_view.setImage(self.mask_map) + self.xrf_view.ui.menuBtn.hide() + self.xrf_view.ui.roiBtn.hide() + self.xrf_view.setPredefinedGradient("viridis") + + self.mask_view.ui.menuBtn.hide() + self.mask_view.ui.roiBtn.hide() + + def create_mask(self): + + self.threshold_low = self.dsb_low_threshold.value() + self.threshold_high = self.dsb_high_threshold.value() + self.dsb_low_threshold.setMaximum(self.threshold_high-0.001) + self.dsb_high_threshold.setMinimum(self.threshold_low+0.001) + + self.norm_mask = remove_nan_inf(self.mask_map) / np.nanmax(self.mask_map) + self.norm_mask[(self.norm_mask < self.threshold_low) | + (self.norm_mask > self.threshold_high)] = 0 + + # self.norm_mask[self.norm_mask < self.threshold_low] = 0 + # self.norm_mask[self.norm_mask > self.threshold_high] = 0 + self.xrf_view.setImage(self.norm_mask) + self.statusbar.showMessage("New Threshold Applied") + self.binary_mask = np.where(self.norm_mask > 0, 1, 0) + self.mask_view.setImage(self.binary_mask) + + + def load_mask_map(self, z = -1): + """Array for masking.Z will be used if 3D """ + + filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") + self.xrf_file_name = str(filename[0]) + self.mask_map = tf.imread(self.xrf_file_name) + if self.mask_map.ndim == 3: + self.mask_map = self.mask_map[z] + + else: + self.mask_map = self.mask_map.T + + assert ( + self.dim3, + self.dim2, + ) == self.mask_map.shape, f"Unexpected image dimensions: {self.mask_map.shape} vs {(self.dim2,self.dim3)}" + + self.view_data() + self.create_mask() + + def send_mask(self): + + """ Apply mask to xanes viewer""" + + if self.cb_use_binary_mask.isChecked(): + self.mask_signal.emit(self.im_stack*self.binary_mask[np.newaxis,:,:]) + else: + self.mask_signal.emit(self.im_stack*self.norm_mask[np.newaxis,:,:]) + + def import_a_mask(self): + filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") + xrf_file_name = str(filename[0]) + self.binary_mask = tf.imread(xrf_file_name).T + self.statusbar.showMessage("A New Mask Imported") + self.mask_view.setImage(self.binary_mask) + self.apply_mask_to_xanes() + + def export_mask(self): + try: + file_name = QFileDialog().getSaveFileName(self, "Save image data", "", "image file(*tiff *tif )") + tf.imwrite(file_name[0] + ".tiff", self.binary_mask.T) + logger.info(f"Updated Image Saved: {file_name[0]}") + self.statusbar.showMessage("Mask Exported") + except Exception: + logger.error("No file to save") + pass diff --git a/xmidas/gui/windows/multichannel_viewer.py b/xmidas/gui/windows/multichannel_viewer.py new file mode 100644 index 0000000..4258543 --- /dev/null +++ b/xmidas/gui/windows/multichannel_viewer.py @@ -0,0 +1,319 @@ +import os,sys,json +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg +import pyqtgraph.exporters +from glob import glob +from pyqtgraph import plot +from itertools import combinations +from scipy.stats import linregress +from packaging import version + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../layout" +)) +from xmidas.utils.color_maps import * +from xmidas.models.encoders import jsonEncoder +cmap_dict = create_color_maps() + +class MultiChannelWindow(QtWidgets.QMainWindow): + def __init__(self, image_dict=None): + super(MultiChannelWindow, self).__init__() + if image_dict is None: + image_dict = {} + uic.loadUi(os.path.join(ui_dir, "mutlichannel.ui"), self) + self.user_wd = os.path.abspath("~") + + self.canvas = self.img_view.addPlot(title="") + self.canvas.getViewBox().invertY(True) + self.canvas.setAspectLocked(True) + self.cb_choose_color.addItems([i for i in cmap_dict.keys()]) + #self.canvas.set + self.sliderSetUp() + self.image_dict = image_dict + self.buildFromDictionary() + + # connections + self.actionLoad.triggered.connect(self.createMuliColorAndList) + self.actionLoad_Stack.triggered.connect(self.createMuliColorAndList) + self.actionSave_Stack_tiff.triggered.connect(self.saveTiffData) + self.cb_choose_color.currentTextChanged.connect(self.updateImageDictionary) + self.pb_update_low_high.clicked.connect(self.updateImageDictionary) + self.listWidget.itemClicked.connect(self.editImageProperties) + self.listWidget.itemDoubleClicked.connect(self.showOneImageOnly) + self.pb_show_selected.clicked.connect(self.showOneImageOnly) + self.pb_show_all.clicked.connect(self.showAllItems) + self.actionLoad_State_File.triggered.connect(self.importState) + self.actionSave_State.triggered.connect(self.exportState) + self.actionSave_View.triggered.connect(self.saveImage) + + def buildFromDictionary(self): + if self.image_dict is not None: + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + else: + pass + + def generateImageDictionary(self): + """Creates a dictionary contains image path, color scheme chosen, throshold limits etc. + when user edits the parameters dictionary will be updated and unwrapped for display later. + This dictionary is saved as json file while saving the state. Two image loading options are possible. + User can either select multiple 2D array images or one 3D array (stack)""" + + clickedAction = self.sender() + + if clickedAction.text() == "Load Images": + # multiple images are selected + self.loadMultipleImageFiles() + + elif clickedAction.text() == "Load Stack": + # an image stack is selected + self.loadAsStack() + + def loadMultipleImageFiles(self): + + filter = "TIFF (*.tiff);;TIF (*.tif)" + QtWidgets.QFileDialog().setFileMode(QtWidgets.QFileDialog.ExistingFiles) + # choose mutliple tiff files + names = QtWidgets.QFileDialog().getOpenFileNames(self, "Open files", " ", filter) + if names[0]: + self.image_dict = {} + # select the file directory. Image files are expected to be in the same folder + self.imageDir = os.path.dirname(names[0][0]) + + # create the dictionary + for colorName, image in zip(cmap_dict.keys(), names[0]): + # squeeze to allow with pseudo 3D axis from some tomo recon (eg. 1, 100,100 array) + im_array = np.squeeze(tf.imread(image)) + # set values for thresholding as image min and max + low, high = 0, 1 + # name of the tiff file is chosen as key for the dictionary, + # inner keys are properties set for that image + im_name = os.path.basename(image) + # construct the dictionary + self.image_dict[f"{os.path.basename(image)}"] = { + "ImageName": im_name, + "ImageDir": self.imageDir, + "Image": im_array, + "Color": colorName, + "CmapLimits": (low, high), + "Opacity": 1.0, + } + else: + pass + + def loadAsStack(self): + """construct the dictionary with image +number as the key. + All other steps are similar to the loadMultipleImageFiles function""" + + filter = "TIFF (*.tiff);;TIF (*.tif)" + file_name = QtWidgets.QFileDialog().getOpenFileName( + self, "Open a Stack", "", "TIFF(*tiff *tif);;all_files (*)", filter + ) + if file_name[0]: + self.imageDir = os.path.dirname(file_name[0]) + self.image_dict = {} + im_stack = np.squeeze(tf.imread(file_name[0])) + # asset the file is a stack + assert im_stack.ndim == 3, "Not a stack" + # construct the dictionary + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), im_stack)): + low, high = np.min(image), np.max(image) + self.image_dict[f"Image {n+1}"] = { + "ImageName": f"Image {n+1}", + "ImageDir": self.imageDir, + "Image": image, + "Color": colorName, + "CmapLimits": (low, high), + "Opacity": 1.0, + } + + def loadAnImage(self, image, colormap, cmap_limits, opacity=1): + """load single image and colorbar to the widget. This function will be looped for + multiple images later + """ + # get pg image item + img = pg.ImageItem() + # add image to the graphicsview widget + self.canvas.addItem(img) + # set the color map + cmap = pg.ColorMap(pos=np.linspace(0, 1, len(colormap)), color=colormap) + # image = np.squeeze(tf.imread(image_path)) + # set image to the image item with cmap + + lower_limit = np.nanmax(image)*cmap_limits[0] + upper_limit = np.nanmax(image)*cmap_limits[1] + img_ = np.where((image < lower_limit) | (image > upper_limit), 0, image) + + img.setImage(img_, + lut=cmap.getLookupTable(), + opacity=opacity) + + # set colorbar for thresholding + bar = pg.ColorBarItem(values=(lower_limit,upper_limit), cmap=cmap, + limits=None, orientation="vertical") + bar.setImageItem(img_) + # set composition mode to plus for overlaying + img.setCompositionMode(QtGui.QPainter.CompositionMode_Plus) + + def createMultiColorView(self, image_dictionary): + """Function creates multi color image view by taking image + data and parameters from the dictionary""" + + # clear the plots and list in case of re-loading + self.canvas.clear() + self.listWidget.clear() + + # display individual images in for loop + for path_and_color in image_dictionary.values(): + self.loadAnImage( + path_and_color["Image"], + cmap_dict[path_and_color["Color"]], + path_and_color["CmapLimits"], + path_and_color["Opacity"], + ) + + def showOneImageOnly(self): + editItem = self.listWidget.currentItem() + editRow = self.listWidget.currentRow() + for i in range(self.listWidget.count()): + if self.listWidget.item(i) == editItem: + editItemName = self.listWidget.item(i).text().split(",")[0] + self.image_dict[editItemName]["Opacity"] = 1 + + elif self.listWidget.item(i) != editItem: + editItemName = self.listWidget.item(i).text().split(",")[0] + self.image_dict[editItemName]["Opacity"] = 0 + + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + self.listWidget.setCurrentRow(editRow) + + def showAllItems(self): + + editItem = self.listWidget.currentItem() + editRow = self.listWidget.currentRow() + for i in range(self.listWidget.count()): + editItemName = self.listWidget.item(i).text().split(",")[0] + self.image_dict[editItemName]["Opacity"] = 1 + + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + self.listWidget.setCurrentRow(editRow) + + def displayImageNames(self, image_dictionary): + """Populate the list widget table with image name and color used to plot, + using image dictionary input""" + + for im_name, vals in image_dictionary.items(): + self.listWidget.addItem(f"{im_name},{vals['Color']}") + self.listWidget.setCurrentRow(0) + + def createMuliColorAndList(self): + """Finally Load Images and poplulate the list widget from the dictionary""" + with pg.BusyCursor(): # gives the circle showing gui is doing something + self.generateImageDictionary() + if self.image_dict: + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + + else: + pass + + def sliderSetUp(self): + """Setting the slider min and max from image values""" + self.sldr_low.setMaximum(1) + self.sldr_low.setMinimum(0) + self.sldr_high.setMaximum(1) + self.sldr_high.setMinimum(0) + + def editImageProperties(self, item): + """function to control the assigned properties such as color, + threshold limits, opacity etc of a single image selected using the list widget item""" + editItem = item.text() + # get the dictionary key from item text + editItemName = editItem.split(",")[0] + editItemColor = editItem.split(",")[1] + im_array = self.image_dict[editItemName]["Image"] + + self.cb_choose_color.setCurrentText(editItemColor) + + def updateImageDictionary(self): + newColor = self.cb_choose_color.currentText() + editItem = self.listWidget.currentItem().text() + editRow = self.listWidget.currentRow() + editItemName = editItem.split(",")[0] + self.imageDir = self.image_dict[editItemName]["ImageDir"] + im_array = self.image_dict[editItemName]["Image"] + cmap_limits = (self.sldr_low.value(),self.sldr_high.value()) + self.low_high_vals.setText(f"low:{cmap_limits[0]:.3f},high:{cmap_limits[1]:.3f}") + opacity = self.sldr_opacity.value() / 100 + self.opacity_val.setText(str(opacity)) + self.image_dict[editItemName] = { + "ImageName": editItemName, + "ImageDir": self.imageDir, + "Image": im_array, + "Color": newColor, + "CmapLimits": cmap_limits, + "Opacity": opacity, + } + + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + self.listWidget.setCurrentRow(editRow) + + def exportState(self): + + file_name = QtWidgets.QFileDialog().getSaveFileName( + self, "Save Current State", "multicolor_params.json", "json file(*json)" + ) + """ + for val in self.image_dict.values(): + val['CmapLimits'] = json.dumps(str(val['CmapLimits'])) + """ + + if file_name[0]: + + with open(f"{file_name[0]}", "w") as fp: + json.dump(self.image_dict, fp, indent=4, cls=jsonEncoder) + + else: + pass + + def importState(self): + file_name = QtWidgets.QFileDialog().getOpenFileName( + self, "Open a State File", "", "json file(*json);;all_files (*)" + ) + if file_name[0]: + with open(file_name[0], "r") as fp: + self.image_dict = json.load(fp) + + self.createMultiColorView(self.image_dict) + self.displayImageNames(self.image_dict) + else: + pass + + def saveImage(self): + file_name = QtWidgets.QFileDialog().getSaveFileName( + self, "Save Image", "multicolor_image.png", "PNG(*.png);; TIFF(*.tiff);; JPG(*.jpg)" + ) + exporter = pg.exporters.ImageExporter(self.canvas.getViewBox()) + exporter.export(file_name[0]) + + def saveTiffData(self): + file_name = QtWidgets.QFileDialog().getSaveFileName(self, "Save Image", 'stack_image_data.tiff', + 'TIFF(*.tiff)') + saveStack = [image_property['Image'] for image_property in self.image_dict.values()] + print(np.shape(saveStack)) + + if file_name[0]: + tf.imwrite(file_name[0], saveStack) + else: + return + diff --git a/xmidas/gui/windows/singleStackViewer.py b/xmidas/gui/windows/singleStackViewer.py new file mode 100644 index 0000000..af70c54 --- /dev/null +++ b/xmidas/gui/windows/singleStackViewer.py @@ -0,0 +1,95 @@ +import argparse +import logging +import datetime +import sys +import webbrowser +import traceback +import os +import json + +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg + + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR + +from xmidas.utils import * +from xmidas.utils.color_maps import * + +cmap_dict = create_color_maps() + +#from . import __version__ + +logger = logging.getLogger() +try: + import cv2 # noqa: F401 +except Exception: + logger.warning("openCV module not found") + pass +if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) + +if hasattr(QtCore.Qt, "AA_UseHighDpiPixmaps"): + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) + +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../layout" +)) + + +class singleStackViewer(QtWidgets.QMainWindow): + def __init__(self, img_stack, gradient="viridis"): + super(singleStackViewer, self).__init__() + + # Load the UI Page + uic.loadUi(os.path.join(ui_dir, "singleStackView.ui"), self) + self.user_wd = os.path.abspath("~") + + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + + self.img_stack = img_stack + self.gradient = gradient + self.image_view.setPredefinedGradient(gradient) + + if self.img_stack.ndim == 3: + self.dim1, self.dim3, self.dim2 = img_stack.shape + elif self.img_stack.ndim == 2: + self.dim3, self.dim2 = img_stack.shape + self.dim1 = 1 + self.hs_img_stack.setMaximum(self.dim1 - 1) + self.hs_img_stack.setValue(np.round(self.dim1 / 2)) + self.displayStack() + + # connections + self.hs_img_stack.valueChanged.connect(self.displayStack) + self.actionSave.triggered.connect(self.saveImageStackAsTIFF) + + def displayStack(self): + im_index = self.hs_img_stack.value() + if self.img_stack.ndim == 2: + self.image_view.setImage(self.img_stack) + else: + self.image_view.setImage(self.img_stack[im_index]) + self.label_img_count.setText(f"{im_index + 1}/{self.dim1}") + + def saveImageStackAsTIFF(self): + file_name = QFileDialog().getSaveFileName(self, + "Export Stack", + os.path.join(self.user_wd, "image_stack.tiff"), + "*.tiff;;*.tif") + if file_name[0]: + if self.img_stack.ndim == 3: + tf.imwrite(file_name[0], np.float32(self.img_stack.transpose(0, 2, 1))) + elif self.img_stack.ndim == 2: + tf.imwrite(file_name[0], np.float32(self.img_stack.T)) + + self.user_wd = os.path.dirname(file_name[0]) + else: + pass diff --git a/xmidas/gui/windows/xanes_viewer.py b/xmidas/gui/windows/xanes_viewer.py new file mode 100644 index 0000000..f155067 --- /dev/null +++ b/xmidas/gui/windows/xanes_viewer.py @@ -0,0 +1,1267 @@ +import argparse +import logging +import sys +import webbrowser +import traceback +import os +import json +import scipy.stats as stats +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg +import pyqtgraph.exporters +from glob import glob +from pyqtgraph import plot +from itertools import combinations +from scipy.stats import linregress +from packaging import version + + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication, QAbstractItemView, QTableWidgetItem +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR + +from xmidas.utils import * +from xmidas.utils.color_maps import * +from xmidas.models.encoders import jsonEncoder +from xmidas.utils.utils import xanes_fitting, xanes_fitting_1D, xanes_fitting_Binned, interploate_E, get_sum_spectra, get_mean_spectra, normalize_and_scale +from xmidas.gui.windows.multichannel_viewer import MultiChannelWindow +from xmidas.gui.windows.decomposition_viewer import * + +cmap_dict = create_color_maps() +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../layout" +)) + +class XANESViewer(QtWidgets.QMainWindow): + def __init__(self, im_stack=None, e_list=None, refs=None, ref_names=None): + super(XANESViewer, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "XANESViewer.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + + self.im_stack = im_stack + self.e_list = e_list + self.refs = refs + self.ref_names = ref_names + self.selected = self.ref_names + self.fitResultDict = {} + self.fit_method = self.cb_xanes_fit_model.currentText() + self.alphaForLM = self.dsb_alphaForLM.value() + self.xpixel = None + self.ypixel = None + self.xdata_eshifted= self.e_list + self.sb_e_shift.value() + self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( + self.im_stack, + self.xdata_eshifted, + self.refs, + method=self.fit_method, + alphaForLM=self.alphaForLM) + + self.add_roi() + self.add_point_item() + self.scrollBar_setup() + self.display_image_data() + self.display_references() + self.fit_roi_spectrum() + + # connections + self.sb_e_shift.valueChanged.connect(self.fit_roi_spectrum) + self.pb_re_fit.clicked.connect(self.re_fit_xanes) + self.pb_edit_refs.clicked.connect(self.choose_refs) + self.image_roi.sigRegionChanged.connect(self.fit_roi_spectrum) + self.hsb_xanes_stk.valueChanged.connect(self.display_image_data) + self.hsb_chem_map.valueChanged.connect(self.display_image_data) + # self.pb_showMultiColor.clicked.connect(self.generateMultiColorView) + # self.pb_showCompSpec.clicked.connect(self.showComponentXANES) + self.pb_showCompSpec.clicked.connect(self.generateCompoisteImageSpectrumView) + self.image_view.mousePressEvent = self.fit_point_spectrum + #self.image_view_maps.mousePressEvent = self.fit_point_spectrum + + # menu + self.actionSave_Chem_Map.triggered.connect(self.save_chem_map) + self.actionSave_R_factor_Image.triggered.connect(self.save_rfactor_img) + #self.actionSave_Live_Fit_Data.triggered.connect(self.pg_export_spec_fit) + self.actionSave_Live_Fit_Data.triggered.connect(lambda:self.export_data_and_params(folder=None)) + self.actionExport_Fit_Stats.triggered.connect(self.exportFitResults) + self.actionExport_Ref_Plot.triggered.connect(self.pg_export_references) + self.action_export_sum_fit_data.triggered.connect(lambda:self.plot_sum_spectrum_and_save(save_to=None)) + self.pb_open_mask_maker.clicked.connect(self.create_masked_xanes) + self.pb_open_cluster_mask.clicked.connect(self.create_decmpose_mask) + self.pb_norm_chem_map.clicked.connect(self.normalize_chem_map) + self.pb_cluster_chem_map.clicked.connect(self.cluster_chem_map) + + def add_roi(self): + (self.dim1, self.dim2, self.dim3) = self.im_stack.shape + self.cn = int(self.dim2 // 2) + self.sz = np.max([int(self.dim2 * 0.15), int(self.dim3 * 0.15)]) + self.stack_center = int(self.dim1 // 2) + self.image_roi = pg.RectROI( + [int(self.dim3 // 2), int(self.dim2 // 2)], + [self.sz, self.sz], + pen="w", + maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), + ) + self.image_roi.addTranslateHandle([0, 0], [2, 2]) + self.image_roi.addRotateHandle([0, 1], [2, 2]) + try: + self.image_view.removeItem(self.image_roi) + except: pass + + self.image_view.addItem(self.image_roi) + + def add_point_item(self): + self.clicked_point = pg.ScatterPlotItem( + size=9, + pen=pg.mkPen(None), + brush=pg.mkBrush(255, 255, 255), + hoverable=True, + hoverBrush=pg.mkBrush(255, 16, 240) + ) + self.clicked_point_for_map = pg.ScatterPlotItem( + size=9, + pen=pg.mkPen(None), + brush=pg.mkBrush(255, 255, 255), + hoverable=True, + hoverBrush=pg.mkBrush(255, 16, 240) + ) + try: + self.image_view.removeItem(self.clicked_point) + self.image_view_maps.removeItem(self.clicked_point_for_map) + except: pass + + self.image_view.addItem(self.clicked_point) + self.image_view_maps.addItem(self.clicked_point_for_map) + # Ensure scatterPlotItem is always at top + self.clicked_point.setZValue(2) + + + def scrollBar_setup(self): + self.hsb_xanes_stk.setValue(self.stack_center) + self.hsb_xanes_stk.setMaximum(self.dim1 - 1) + self.hsb_chem_map.setValue(0) + self.hsb_chem_map.setMaximum(self.decon_ims.shape[-1] - 1) + + + def import_stack(self): + filename = QFileDialog().getOpenFileName( + self, "Select image data", self.user_wd, "image file( *tiff *tif )" + ) + self.file_name = str(filename[0]) + self.user_wd = os.path.dirname(self.file_name) + + if filename[0]: + self.im_stack = tf.imread(filename[0]) + + #TODO import energy, look for default files first, if not open file dialogue, + #raise cannot proceed + + # if str(self.efilePath).endswith("log_tiff.txt"): + # self.energy = energy_from_logfile(logfile=str(self.efilePath)) + # logger.info("Log file from pyxrf processing") + + # else: + # self.energy = np.loadtxt(str(self.efilePath)) + # self.change_color_on_load(self.pb_elist_xanes) + # logger.info("Energy file loaded") + + self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting(self.im_stack, + self.xdata_eshifted, + self.refs, + method=self.fit_method, + alphaForLM=self.alphaForLM + ) + + self.add_roi() + self.add_point_item() + self.scrollBar_setup() + self.display_image_data() + self.display_references() + self.fit_roi_spectrum() + + def display_image_data(self): + + self.image_view.setImage(self.im_stack[self.hsb_xanes_stk.value()]) + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + self.image_view.setPredefinedGradient("viridis") + + self.image_view_maps.setImage(self.decon_ims.transpose(2, 0, 1)[self.hsb_chem_map.value()]) + self.image_view_maps.setPredefinedGradient("bipolar") + self.image_view_maps.ui.menuBtn.hide() + self.image_view_maps.ui.roiBtn.hide() + + def display_references(self): + + self.inter_ref = interploate_E(self.refs, self.xdata_eshifted) + self.plt_colors = ["c", "m", "y", "w"] * 10 + self.spectrum_view_refs.addLegend() + for ii in range(self.inter_ref.shape[0]): + if len(self.selected) != 0: + self.spectrum_view_refs.plot( + self.xdata_eshifted, + self.inter_ref[ii], + pen=pg.mkPen(self.plt_colors[ii], width=2), + name=self.selected[1:][ii], + ) + else: + self.spectrum_view_refs.plot( + self.xdata_eshifted, + self.inter_ref[ii], + pen=pg.mkPen(self.plt_colors[ii], width=2), + name="ref" + str(ii + 1), + ) + + def choose_refs(self): + "Interactively exclude some standards from the reference file" + self.ref_edit_window = RefChooser( + self.ref_names, + self.im_stack, + self.e_list, + self.refs, + self.sb_e_shift.value(), + self.cb_xanes_fit_model.currentText(), + ) + self.ref_edit_window.show() + # self.rf_plot = pg.plot(title="RFactor Tracker") + + # connections + self.ref_edit_window.choosenRefsSignal.connect(self.update_refs) + self.ref_edit_window.fitResultsSignal.connect(self.plotFitResults) + + + def create_masked_xanes(self): + "create mask and apply to xanes 3d array" + + self.mask_creator = MaskMaker(self.im_stack,self.decon_ims.transpose(2, 0, 1)[self.hsb_chem_map.value()]) + self.mask_creator.show() + + #connections + self.mask_creator.mask_signal.connect(self.apply_mask_to_xanes) + + def create_decmpose_mask(self): + + self.decomposer = DecomposeViewer(self.im_stack,None,self.xdata_eshifted) + self.decomposer.show() + #connections + self.decomposer.mask_signal.connect(self.apply_mask_to_xanes) + self.decomposer.mask_and_path_signal.connect(self.recieve_mask_and_save) + + def cluster_chem_map(self): + + self.decomposer2 = DecomposeViewer(self.decon_ims.transpose(2, 0, 1)[self.hsb_chem_map.value()], + self.im_stack, + self.xdata_eshifted) + + + # self.decomposer2 = DecomposeViewer(self.decon_ims.transpose(2, 0, 1), + # self.im_stack, + # self.xdata_eshifted) + self.decomposer2.show() + #connections + self.decomposer2.mask_signal.connect(self.apply_mask_to_xanes) + self.decomposer2.mask_and_path_signal.connect(self.recieve_mask_and_save) + + def plot_sum_spectrum(self): + self.xdata_eshifted = self.e_list + self.sb_e_shift.value() + self.ydata1 = get_sum_spectra(self.im_stack) + self.plot_data_and_fit() + self.re_fit_xanes() + + + def plot_sum_spectrum_and_save(self, save_to = None): + self.xdata_eshifted = self.e_list + self.sb_e_shift.value() + self.ydata1 = get_sum_spectra(self.im_stack) + self.plot_data_and_fit(plot_name="Sum Spectrum") + self.re_fit_xanes() + self.export_data_and_params(folder=save_to) + + def recieve_mask_and_save(self,signals): + self.apply_mask_to_xanes(signals[0]) + QtTest.QTest.qWait(5000) + self.plot_sum_spectrum_and_save(save_to = signals[1]) + QtTest.QTest.qWait(2000) + + + def normalize_chem_map(self): + + stack_ = normalize_and_scale(self.decon_ims.transpose(2, 0, 1)) + self.decon_ims = stack_.transpose(1, 2, 0) + self.display_image_data() + + + def apply_mask_to_xanes(self, masked_array): + + """ Apply the recieved mask and plot mean spectrum and fit """ + + #self.im_stack = self.im_stack*mask[np.newaxis,:,:] + self.im_stack = masked_array + self.display_image_data() + self.xdata_eshifted = self.e_list + self.sb_e_shift.value() + self.ydata1 = get_mean_spectra(self.im_stack) + self.plot_data_and_fit() + self.re_fit_xanes() + + def update_refs(self, list_): + self.selected = list_ # list_ is the signal from ref chooser + self.fit_roi_spectrum() + self.re_fit_xanes() + + def fit_point_spectrum(self, event): + if event.type() == QtCore.QEvent.Type.MouseButtonDblClick: + if event.button() == QtCore.Qt.LeftButton: + self.xpixel = int(self.image_view.view.mapSceneToView(event.pos()).x()) + zlim, ylim, xlim = self.im_stack.shape + + if self.xpixel > xlim: + self.xpixel = xlim + + self.ypixel = int(self.image_view.view.mapSceneToView(event.pos()).y()) + if self.ypixel > ylim: + self.ypixel = ylim + + self.clicked_point.setData(pos=[[self.xpixel+0.5,self.ypixel+0.5],]) + self.clicked_point_for_map.setData(pos=[[self.xpixel+0.5,self.ypixel+0.5],]) + self.xdata_eshifted = self.e_list + self.sb_e_shift.value() + self.ydata1 = self.im_stack[:, self.ypixel, self.xpixel] + + self.plot_data_and_fit(plot_name=f"Point Spectrum; x= {self.xpixel}, y= {self.ypixel}") + + def plot_data_and_fit(self, plot_name = "Data"): + + if self.cb_normalize_before_fit.isChecked(): + self.ydata1 =self.ydata1/self.ydata1[-1] + + self.fit_method = self.cb_xanes_fit_model.currentText() + self.spectrum_view.addLegend() + if len(self.selected) != 0: + + self.inter_ref = interploate_E(self.refs[self.selected], self.xdata_eshifted) + stats, coeffs = xanes_fitting_1D( + self.ydata1, + self.xdata_eshifted, + self.refs[self.selected], + method=self.fit_method, + alphaForLM=self.alphaForLM, + ) + + else: + self.inter_ref = interploate_E(self.refs, self.xdata_eshifted) + stats, coeffs = xanes_fitting_1D( + self.ydata1, self.xdata_eshifted, self.refs, method=self.fit_method, alphaForLM=self.alphaForLM + ) + + self.fit_ = np.dot(coeffs, self.inter_ref) + pen = pg.mkPen("b", width=1.5) + pen2 = pg.mkPen("r", width=1.5) + # pen3 = pg.mkPen("y", width=1.5) + self.spectrum_view.addLegend() + self.spectrum_view.setLabel("bottom", "Energy") + self.spectrum_view.setLabel("left", "Intensity", "A.U.") + self.spectrum_view.plot(self.xdata_eshifted, + self.ydata1, + pen=pen, + clear=True, + symbol="o", + symbolSize=6, + symbolBrush="b", + name=f"{plot_name}") + + self.spectrum_view.plot(self.xdata_eshifted, self.fit_, name="Fit", pen=pen2) + + for n, (coff, ref, plt_clr) in enumerate(zip(coeffs, self.inter_ref, self.plt_colors)): + + if len(self.selected) != 0: + + self.spectrum_view.plot(self.xdata_eshifted, np.dot(coff, ref), name=self.selected[1:][n], pen=plt_clr) + else: + self.spectrum_view.plot(self.xdata_eshifted, np.dot(coff, ref), name="ref" + str(n + 1), pen=plt_clr) + # set the rfactor value to the line edit slot + self.results = { + "ref_stds":self.selected[1:], + "Coefficients":coeffs, + "R-Factor": stats['R_Factor'], + "R-Square": stats['R_Square'], + "Chi-Square": stats['Chi_Square'], + "Reduced Chi-Square": stats['Reduced Chi_Square']} + + self.fit_results.setText("\n".join(f"{key}: {value}" for key, value in self.results.items())) + + def fit_roi_spectrum(self): + + self.roi_img = self.image_roi.getArrayRegion(self.im_stack, self.image_view.imageItem, axes=(1, 2)) + sizex, sizey = self.roi_img.shape[1], self.roi_img.shape[2] + posx, posy = self.image_roi.pos() + self.roi_info.setText(f"ROI_Pos: {int(posx)},{int(posy)} ROI_Size: {sizex},{sizey}") + + self.xdata_eshifted = self.e_list + self.sb_e_shift.value() + self.ydata1 = get_mean_spectra(self.roi_img) + + self.plot_data_and_fit(plot_name="ROI Spectrum") + + def re_fit_xanes(self): + self.fit_params = {"e_shit":self.sb_e_shift.value(), + "method":self.cb_xanes_fit_model.currentText(), + "regularization":self.dsb_alphaForLM.value()} + + + if len(self.selected) != 0: + self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( + self.im_stack, + self.e_list + self.sb_e_shift.value(), + self.refs[self.selected], + method=self.cb_xanes_fit_model.currentText(), + alphaForLM=self.alphaForLM, + ) + else: + # if non athena file with no header is loaded no ref file cannot be edited + self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( + self.im_stack, + self.e_list + self.sb_e_shift.value(), + self.refs, + method=self.cb_xanes_fit_model.currentText(), + alphaForLM=self.alphaForLM, + ) + + # rfactor is a list of all spectra so take the mean + self.rfactor_mean = np.mean(self.rfactor) + self.image_view_maps.setImage(self.decon_ims.transpose(2, 0, 1)) + self.scrollBar_setup() + + def plotFitResults(self, decon_ims, rfactor_mean, coeff_array): + # upadte the chem maps and scrollbar params + self.image_view_maps.setImage(decon_ims.transpose(2, 0, 1)) + self.hsb_chem_map.setValue(0) + self.hsb_chem_map.setMaximum(decon_ims.shape[-1]-1) + + # set the rfactor value to the line edit slot + self.le_r_sq.setText(f"{rfactor_mean :.4f}") + + def showComponentXANES(self): + compNum = self.hsb_chem_map.value() + currentComp = self.decon_ims.transpose(2, 0, 1)[compNum] + currentCompMask = currentComp > 0 + yData = applyMaskGetMeanSpectrum(self.im_stack, currentCompMask) + xanes_comp_plot = pg.plot( + self.e_list + self.sb_e_shift.value(), + yData, + title=f"Component_{compNum}", + pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), + symbol="o", + ) + xanes_comp_plot.setLabel("bottom", "Energy (keV)") + xanes_comp_plot.setLabel("left", "Intensity") + + def plotDeconvSpectrum(self, clusterSigma=0): + + try: + self.ref_plot.close() + + except: + pass + + self.ref_plot = plot(title="Deconvoluted XANES Spectra") + self.ref_plot.setLabel("bottom", "Energy") + self.ref_plot.setLabel("left", "Intensity") + self.ref_plot.addLegend() + + for n, compImage in enumerate(self.decon_ims.transpose(2, 0, 1)): + mask = np.where(compImage > clusterSigma * np.std(compImage), compImage, 0) + + self.ref_plot.plot( + self.xdata_eshifted, + get_mean_spectra(self.im_stack * mask), + pen=pg.mkPen(self.plt_colors[n], width=2), + name=f'Component_{n + 1}' + ) + + def generateCompoisteImageSpectrumView(self): + self.multichanneldict = {} + + spectrumDF = getDeconvolutedXANESSpectrum(self.im_stack, self.decon_ims.transpose(2, 0, 1), + self.xdata_eshifted, clusterSigma=0) + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.decon_ims.transpose((2, 0, 1)))): + low, high = np.min(image), np.max(image) + self.multichanneldict[f'Image {n + 1}'] = {'ImageName': f'Image {n + 1}', + 'ImageDir': '.', + 'Image': image, + 'Color': colorName, + 'CmapLimits': (low, high), + 'Opacity': 1.0 + } + self.muli_color_window = MultiXANESWindow(image_dict=self.multichanneldict, spec_df=spectrumDF) + self.muli_color_window.show() + + def generateMultiColorView(self): + self.multichanneldict = {} + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.decon_ims.transpose((2, 0, 1)))): + low, high = np.min(image), np.max(image) + self.multichanneldict[f"Image {n + 1}"] = { + "ImageName": f"Image {n + 1}", + "ImageDir": ".", + "Image": image, + "Color": colorName, + "CmapLimits": (low, high), + "Opacity": 1.0, + } + self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) + self.muli_color_window.show() + + def save_chem_map(self): + file_name = QFileDialog().getSaveFileName(self, + "save image", + os.path.join(self.user_wd,"chemical_map.tiff"), + "image data (*tiff)") + if file_name[0]: + tf.imwrite(file_name[0], np.float32(self.decon_ims.transpose(2, 0, 1))) + self.user_wd = os.path.dirname(file_name[0]) + else: + logger.error("No file to save") + pass + + def save_rfactor_img(self): + file_name = QFileDialog().getSaveFileName(self, + "save image", + os.path.join(self.user_wd,"r-factor_map.tiff"), + "image data (*tiff)") + if file_name[0]: + tf.imwrite(file_name[0], np.float32(self.rfactor), imagej=True) + self.user_wd = os.path.dirname(file_name[0]) + else: + logger.error("No file to save") + pass + + def save_spec_fit(self): + try: + to_save = np.column_stack([self.xdata_eshifted, self.ydata1, self.fit_]) + file_name = QFileDialog().getSaveFileName(self, + "save spectrum", + os.path.join(self.user_wd,"spec_fit.txt"), + "spectrum and fit (*txt)") + if file_name[0]: + np.savetxt(file_name[0] + ".txt", to_save) + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + except Exception: + logger.error("No file to save") + pass + + def pg_export_spec_fit(self): + + exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, + "save spectrum", + os.path.join(self.user_wd,"spec_fit.txt"), + "spectrum and fit (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def pg_export_references(self): + + exporter = pg.exporters.CSVExporter(self.spectrum_view_refs.plotItem) + exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, + "save references", + os.path.join(self.user_wd,"xanes_references.csv"), + "column data (*csv)" + ) + if file_name[0]: + exporter.export(file_name[0]) + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def exportFitResults(self): + file_name = QFileDialog().getSaveFileName(self, + "save txt", + os.path.join(self.user_wd,"xanes_1D_fit_results.json"), + "txt data (*txt)") + if file_name[0]: + with open(file_name[0], "w") as file: + json.dump(self.results, file, indent=4) + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def export_data_and_params(self, folder = None): + + if folder is None: + file_name,_ = QFileDialog().getSaveFileName(self, + "save to folder", + os.path.join(self.user_wd, "xanes_data"), + options = QFileDialog.ShowDirsOnly) + + else: + file_name = folder + exporter_csv = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + exporter_csv.parameters()["columnMode"] = '(x,y) per plot' + exporter_csv.parameters()["separator"] = 'comma' + + exporter_png = pg.exporters.ImageExporter(self.spectrum_view.getViewBox()) + exporter_img_png = pg.exporters.ImageExporter(self.image_view.getView()) + + exporter_svg = pg.exporters.SVGExporter(self.spectrum_view.getViewBox()) + exporter_img_svg = pg.exporters.SVGExporter(self.image_view.getView()) + + if file_name: + os.makedirs(file_name[0], exist_ok=True) + os.makedirs(os.path.join(file_name+"/csv_files"), exist_ok=True) + os.makedirs(os.path.join(file_name+"/png_files"), exist_ok=True) + os.makedirs(os.path.join(file_name+"/svg_files"), exist_ok=True) + os.makedirs(os.path.join(file_name+"/tiff_files"), exist_ok=True) + exporter_csv.export(os.path.join(file_name+"/csv_files/xanes_fit.csv")) + exporter_png.export(os.path.join(file_name+"/png_files/xanes_fit_image.png")) + exporter_img_png.export(os.path.join(file_name+"/png_files/stack_image.png")) + exporter_svg.export(os.path.join(file_name+"/svg_files/xanes_fit.svg")) + exporter_img_svg.export(os.path.join(file_name+"/svg_files/stack_image.svg")) + + tf.imwrite(os.path.join(file_name+"/tiff_files/chem_map.tiff"), np.float32(self.decon_ims.transpose(2, 0, 1))) + tf.imwrite(os.path.join(file_name+"/tiff_files/rfactor.tiff"), np.float32(self.rfactor), imagej=True) + tf.imwrite(os.path.join(file_name+"/tiff_files/stack_image.tiff"), np.float32(self.im_stack[self.hsb_xanes_stk.value()])) + + roi_and_pixel_params = {"sizex":self.roi_img.shape[1],"sizey":self.roi_img.shape[2], + "posx":self.image_roi.pos()[0],"posy":self.image_roi.pos()[1], + "pixelx":self.xpixel, "pixely":self.ypixel} + + self.fit_params = {"e_shit":self.sb_e_shift.value(), + "method":self.cb_xanes_fit_model.currentText(), + "regularization":self.dsb_alphaForLM.value()} + export_params = {"fit_params":self.fit_params, + "roi_or_pixel_params":roi_and_pixel_params, + "fit_results":self.results} + + with open(os.path.join(file_name+"/fit_params.json"), "w") as fp: + json.dump(export_params, fp, indent=4, cls = jsonEncoder) + self.user_wd = os.path.dirname(file_name) + QMessageBox.information(self,"Saved", f"data saved to {self.user_wd}") + + +class RefChooser(QtWidgets.QMainWindow): + choosenRefsSignal = QtCore.pyqtSignal(list) + fitResultsSignal = QtCore.pyqtSignal(np.ndarray, float, np.ndarray) + + def __init__(self, ref_names, im_stack, e_list, refs, e_shift, fit_model): + super().__init__() + uic.loadUi(os.path.join(ui_dir, "RefChooser.ui"), self) + + self.user_wd = os.path.abspath("~") + self.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + + self.ref_names = ref_names + self.refs = refs + self.im_stack = im_stack + self.e_list = e_list + self.e_shift = e_shift + self.fit_model = fit_model + + self.all_boxes = [] + self.rFactorList = [] + + self.setupUI() + + def setupUI(self): + self.displayCombinations() + + self.tableWidget.setStyleSheet( + "background-color: white; selection-background-color: rgb(200,0,0);" + ) + + self.selectionLine = pg.InfiniteLine( + pos=1, angle=90, pen=pg.mkPen("m", width=2.5), movable=True, label="Move Me!" + ) + self.stat_view.setLabel("bottom", "Fit ID") + self.stat_view.setLabel("left", "Reduced Chi^2") + + for n, name in enumerate(self.ref_names): + checkbox = QtWidgets.QCheckBox(self.ref_box_frame) + checkbox.setObjectName(name) + checkbox.setText(name) + checkbox.setChecked(n == 0) + checkbox.setEnabled(n != 0) + checkbox.toggled.connect(self.enableApply) + self.gridLayout_2.addWidget(checkbox, n, 0, 1, 1) + self.all_boxes.append(checkbox) + + self.pb_apply.clicked.connect(self.clickedWhichAre) + self.pb_combo.clicked.connect(self.tryAllCombo) + self.actionExport_Results_csv.triggered.connect(self.exportFitResults) + self.selectionLine.sigPositionChanged.connect(self.updateFitWithLine) + self.tableWidget.itemSelectionChanged.connect(self.updateWithTableSelection) + self.stat_view.mouseDoubleClickEvent = self.moveSelectionLine + self.sb_max_combo.valueChanged.connect(self.displayCombinations) + self.pb_sort_with_r.clicked.connect(self.sortTable) + self.cb_sorter.currentTextChanged.connect(self.sortTable) + + def populateChecked(self): + self.onlyCheckedBoxes = [cb.objectName() for cb in self.all_boxes if cb.isChecked()] + + @QtCore.pyqtSlot() + def clickedWhichAre(self): + self.populateChecked() + self.choosenRefsSignal.emit(self.onlyCheckedBoxes) + + def generateRefList(self, ref_list, maxCombo, minCombo=1): + if maxCombo > len(ref_list): + raise ValueError("Maximum combinations cannot exceed number of references") + combinations_list = [combo for i in range(minCombo, maxCombo + 1) for combo in combinations(ref_list, i)] + return len(combinations_list), combinations_list + + def displayCombinations(self): + niter, self.iter_list = self.generateRefList(self.ref_names[1:], self.sb_max_combo.value()) + self.label_nComb.setText(f"{niter} Combinations") + + @QtCore.pyqtSlot() + def tryAllCombo(self): + self.rfactor_list = [] + self.results_data = [] + + self.tableWidget.setRowCount(0) + self.tableWidget.setColumnCount(9) + self.tableWidget.setHorizontalHeaderLabels([ + "Fit Number", "References", "Coefficients", "Sum of Coefficients", + "R-Factor", "R^2", "chi^2", "red-chi^2", "Score" + ]) + + niter, self.iter_list = self.generateRefList(self.ref_names[1:], self.sb_max_combo.value()) + + for n, refs in enumerate(self.iter_list): + self.statusbar.showMessage(f"{n + 1}/{niter}") + selectedRefs = [self.ref_names[0]] + list(refs) + self.fit_combo_progress.setValue(int((n + 1) * 100 / niter)) + + self.stat, self.coeffs_arr = xanes_fitting_Binned( + self.im_stack, self.e_list + self.e_shift, self.refs[selectedRefs], method=self.fit_model + ) + + self.rfactor_list.append(self.stat["Reduced Chi_Square"]) + self.stat_view.plot( + x=np.arange(n + 1), y=self.rfactor_list, clear=True, + title="Reduced Chi^2", pen=pg.mkPen("y", width=2, style=QtCore.Qt.PenStyle.DotLine), symbol="o" + ) + + fit_score = (self.stat["R_Square"] + np.sum(self.coeffs_arr)) / ( + self.stat["R_Factor"] + self.stat["Reduced Chi_Square"]) + + resultsDict = { + "Fit Number": n, + "References": str(selectedRefs[1:]), + "Coefficients": str(np.around(self.coeffs_arr, 4)), + "Sum of Coefficients": str(np.around(np.sum(self.coeffs_arr), 4)), + "R-Factor": self.stat["R_Factor"], + "R^2": self.stat["R_Square"], + "chi^2": self.stat["Chi_Square"], + "red-chi^2": self.stat["Reduced Chi_Square"], + "Score": np.around(fit_score, 4), + } + + self.results_data.append(resultsDict) + self.appendToTableWidget(resultsDict) + QtTest.QTest.qWait(100) + + self.stat_view.addItem(self.selectionLine) + + def appendToTableWidget(self, row_dict): + row_position = self.tableWidget.rowCount() + self.tableWidget.insertRow(row_position) + for col, key in enumerate([ + "Fit Number", "References", "Coefficients", "Sum of Coefficients", + "R-Factor", "R^2", "chi^2", "red-chi^2", "Score" + ]): + item = QtWidgets.QTableWidgetItem(str(row_dict[key])) + self.tableWidget.setItem(row_position, col, item) + + def exportFitResults(self): + if not hasattr(self, "results_data"): + return + df = pd.DataFrame(self.results_data) + file_name, _ = QFileDialog.getSaveFileName(self, "Save CSV", "xanes_fit_results_log.csv", "CSV Files (*.csv)") + if file_name: + df.to_csv(file_name, index=False) + + def selectTableAndCheckBox(self, x): + nSelection = int(round(x)) + self.tableWidget.selectRow(nSelection) + refs_selected = self.iter_list[int(self.tableWidget.item(nSelection, 0).text())] + + for cb in self.findChildren(QtWidgets.QCheckBox): + if cb.isEnabled(): + cb.setChecked(False) + + for name in refs_selected: + cb = self.findChild(QtWidgets.QCheckBox, name=name) + if cb: + cb.setChecked(True) + + def updateFitWithLine(self): + pos_x, _ = self.selectionLine.pos() + x = int(round(pos_x)) + self.selectTableAndCheckBox(x) + + def updateWithTableSelection(self): + self.selectTableAndCheckBox(self.tableWidget.currentRow()) + + def moveSelectionLine(self, event): + if event.button() == QtCore.Qt.MouseButton.LeftButton: + pos = self.stat_view.plotItem.vb.mapSceneToView(event.pos()) + self.selectionLine.setPos(pos.x()) + + def sortTable(self): + key_map = { + "R-Factor": 4, + "R-Square": 5, + "Chi-Square": 6, + "Reduced Chi-Square": 7, + "Fit Number": 0, + } + col_index = key_map.get(self.cb_sorter.currentText(), 0) + self.tableWidget.sortItems(col_index, QtCore.Qt.SortOrder.AscendingOrder) + + def enableApply(self): + self.populateChecked() + self.pb_apply.setEnabled(len(self.onlyCheckedBoxes) > 1) + +class MultiXANESWindow(MultiChannelWindow): + + def __init__(self, image_dict=None, spec_df=None, references = None, ): + super().__init__(image_dict=None) + + self.image_dict = image_dict + self.spec_df = spec_df + + uic.loadUi(os.path.join(ui_dir, 'MultiImageSpectrumView.ui'), self) + self.user_wd = os.path.abspath("~") + # Copy from MultiChannelWindow Start here + self.canvas = self.img_view.addPlot(title="") + self.canvas.getViewBox().invertY(True) + #self.canvas.setZValue(-10) + self.canvas.setAspectLocked(True) + self.cb_choose_color.addItems([i for i in cmap_dict.keys()]) + #self.canvas.getViewBox().setBackgroundColor(pg.mkColor(222,222,222)) + #self.canvas.getViewBox().setOpacity(0.5) + + self.image_dict = image_dict + self.buildFromDictionary() + + self.actionLoad.triggered.connect(self.createMuliColorAndList) + self.actionLoad_Stack.triggered.connect(self.createMuliColorAndList) + self.cb_choose_color.currentTextChanged.connect(self.updateImageDictionary) + self.pb_update_low_high.clicked.connect(self.updateImageDictionary) + self.listWidget.itemClicked.connect(self.editImageProperties) + self.listWidget.itemDoubleClicked.connect(self.showOneImageOnly) + self.pb_show_selected.clicked.connect(self.showOneImageOnly) + self.pb_show_all.clicked.connect(self.showAllItems) + self.actionLoad_State_File.triggered.connect(self.importState) + self.actionSave_State.triggered.connect(self.exportState) + self.actionSave_View.triggered.connect(self.saveImage) + # Copy from MultiChannelWindow End here + self.actionSave_Spectrum_Data.triggered.connect(self.exportDisplayedSpectra) + self.listWidget_Spectrum.itemClicked.connect(self.plotNormSpectrum) + self.pb_apply_xanes_norm.clicked.connect(lambda: self.updateSpecData(plotNorm=True)) + self.createMultiSpectrumLibrary() + + [dsb.valueChanged.connect(lambda: self.updateSpecData()) for dsb in + [self.dsb_norm_Eo, self.dsb_norm_pre1, self.dsb_norm_pre2, self.dsb_norm_post1, + self.dsb_norm_post2, self.sb_norm_order]] + + def createSpectrumPropertyDict(self, specName, xdata, ydata, e0, pre1, pre2, norm1, norm2, normOrder): + SingleSpecProperty = {'Name': specName, + 'Data': (xdata, ydata), + 'NormParams': [e0, pre1, pre2, norm1, norm2, normOrder]} + + return SingleSpecProperty + + def createMultiSpectrumLibrary(self): + self.spec_dict = {} + column_names = self.spec_df.columns + spec_array = self.spec_df.to_numpy() + energy = spec_array[:, 0] + for i in range(self.spec_df.shape[1]): + if i != 0: + specData = spec_array[:, i] + e0_init = energy[np.argmax(np.gradient(specData))] + + pre1, pre2, post1, post2 = xanesNormalization( + energy, + specData, + e0=e0_init, + step=None, + nnorm=1, + nvict=0, + method = "guess" + ) + + self.spec_dict[column_names[i]] = self.createSpectrumPropertyDict(column_names[i], energy, specData, + e0_init, pre1, pre2, post1, post2, 1) + self.nomalizeSpectraAndPlot(self.spec_dict) + self.spectrumDictToListWidget() + + def nomalizeSpectraAndPlot(self, spectrumParamDict): + # print(self.spec_dict) + try: + self.spectrum_view.clear() + except: + pass + self.spectrum_view.setLabel("bottom", "Energy") + self.spectrum_view.setLabel("left", "Intensity") + self.spectrum_view.addLegend() + plt_colors = ['r', 'g', (31, 81, 255), 'c', 'm', 'y', 'w'] + for n, params in enumerate(spectrumParamDict.values()): + e0_ = params['NormParams'][0] + pre1_ = params['NormParams'][1] + pre2_ = params['NormParams'][2] + norm1_ = params['NormParams'][3] + norm2_ = params['NormParams'][4] + normOrder_ = params['NormParams'][5] + + preLine, postLine, self.normData = xanesNormalization( + params['Data'][0], + params['Data'][1], + e0=e0_, + step=None, + nnorm=normOrder_, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_ + ) + + # 'NormParams': (e0, pre1, pre2, norm1, norm2, normOrder)} + self.spectrum_view.plot(params['Data'][0], self.normData, + pen=pg.mkPen(plt_colors[n], width=2), + name=f"Norm._{params['Name']}") + + def loadAndPlotSpectrumData(self): + filter = 'txt (*.tiff);;csv (*.csv)' + file_name = QtWidgets.QFileDialog().getOpenFileName(self, "Open a spectrum file", '', + 'txt (*.tiff);;csv (*.csv);;all_files (*)', filter) + + if file_name[0]: + self.spec_df = pd.read_csv(file_name[0], index_col=None) + # print(self.spec_df.head()) + # print(self.spec_df.shape[1]) + self.createMultiSpectrumLibrary() + else: + return + + def spectrumDictToListWidget(self): + for params in self.spec_dict.values(): + # Creates a QListWidgetItem + specItem = QtWidgets.QListWidgetItem() + + # Setting QListWidgetItem Text + specItem.setText(params['Name']) + + # Setting your QListWidgetItem Data + specItem.setData(QtCore.Qt.UserRole, params) + + # Add the new rule to the QListWidget + self.listWidget_Spectrum.addItem(specItem) + + def plotNormSpectrum(self, item): + self.selectedItem = item + self.editItemName = self.selectedItem.text() + self.editItemData = self.selectedItem.data(QtCore.Qt.UserRole) + + e0_ = self.editItemData['NormParams'][0] + pre1_ = self.editItemData['NormParams'][1] + pre2_ = self.editItemData['NormParams'][2] + norm1_ = self.editItemData['NormParams'][3] + norm2_ = self.editItemData['NormParams'][4] + normOrder_ = self.editItemData['NormParams'][5] + + self.dsb_norm_Eo.setValue(e0_) # loop later + self.dsb_norm_pre1.setValue(pre1_) + self.dsb_norm_pre2.setValue(pre2_) + self.dsb_norm_post1.setValue(norm1_) + self.dsb_norm_post2.setValue(norm2_) + self.sb_norm_order.setValue(normOrder_) + + preLine, postLine, normSpec = xanesNormalization( + self.editItemData['Data'][0], + self.editItemData['Data'][1], + e0=e0_, + step=None, + nnorm=normOrder_, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_ + ) + + self.spectrum_view.clear() + self.spectrum_view.plot(self.editItemData['Data'][0], self.editItemData['Data'][1], + title=f"Normalization Plot_{self.editItemData['Name']}", + pen=pg.mkPen('y', width=2), name=self.editItemData['Name']) + self.spectrum_view.plot(self.editItemData['Data'][0], preLine, pen=pg.mkPen('c', width=2), name='Pre') + self.spectrum_view.plot(self.editItemData['Data'][0], postLine, pen=pg.mkPen('m', width=2), name='Norm') + + # def updateNormParamaters(self): + + def updateSpecData(self, plotNorm=False): + + # loop later + self.editItemData['NormParams'][0] = self.dsb_norm_Eo.value() + self.editItemData['NormParams'][1] = self.dsb_norm_pre1.value() + self.editItemData['NormParams'][2] = self.dsb_norm_pre2.value() + self.editItemData['NormParams'][3] = self.dsb_norm_post1.value() + self.editItemData['NormParams'][4] = self.dsb_norm_post2.value() + self.editItemData['NormParams'][5] = self.sb_norm_order.value() + + self.spec_dict[self.editItemName] = self.editItemData + self.selectedItem.setData(QtCore.Qt.UserRole, self.editItemData) + if plotNorm: + self.nomalizeSpectraAndPlot(self.spec_dict) + else: + self.plotNormSpectrum(self.selectedItem) + + def exportDisplayedSpectra(self): + exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + exporter.parameters()['columnMode'] = '(x,y,y,y) for all plots' + file_name = QFileDialog().getSaveFileName(self, "save spectra", 'xanes.csv', 'spectra (*csv)') + if file_name[0]: + exporter.export(file_name[0]) + else: + self.statusbar_main.showMessage('Saving cancelled') + pass + +class ChemMapViewer(MultiChannelWindow): + + def __init__(self, im_stack = None, + e_list=None, + refs = None, + ref_names = None, + chem_map = None, + fit_params = None): + + super().__init__(image_dict=None) + + self.im_stack = im_stack + self.e_list = e_list + self.refs = refs + self.ref_names = ref_names + self.chem_map = chem_map + self.fit_params = fit_params + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.chem_map.transpose((2, 0, 1)))): + low, high = np.min(image), np.max(image) + self.image_dict[f'Image {n + 1}'] = {'ImageName': self.ref_names[n], + 'ImageDir': '.', + 'Image': image, + 'Color': colorName, + 'CmapLimits': (low, high), + 'Opacity': 1.0 + } + + uic.loadUi(os.path.join(ui_dir, 'MultiImageSpectrumView.ui'), self) + self.user_wd = os.path.abspath("~") + # Copy from MultiChannelWindow Start here + self.canvas = self.img_view.addPlot(title="") + self.canvas.getViewBox().invertY(True) + #self.canvas.setZValue(-10) + self.canvas.setAspectLocked(True) + self.cb_choose_color.addItems([i for i in cmap_dict.keys()]) + #self.canvas.getViewBox().setBackgroundColor(pg.mkColor(222,222,222)) + #self.canvas.getViewBox().setOpacity(0.5) + + self.buildFromDictionary() + + self.actionLoad.triggered.connect(self.createMuliColorAndList) + self.actionLoad_Stack.triggered.connect(self.createMuliColorAndList) + self.cb_choose_color.currentTextChanged.connect(self.updateImageDictionary) + self.pb_update_low_high.clicked.connect(self.updateImageDictionary) + self.listWidget.itemClicked.connect(self.editImageProperties) + self.listWidget.itemDoubleClicked.connect(self.showOneImageOnly) + self.pb_show_selected.clicked.connect(self.showOneImageOnly) + self.pb_show_all.clicked.connect(self.showAllItems) + self.actionLoad_State_File.triggered.connect(self.importState) + self.actionSave_State.triggered.connect(self.exportState) + self.actionSave_View.triggered.connect(self.saveImage) + + + # Copy from MultiChannelWindow End here + self.actionSave_Spectrum_Data.triggered.connect(self.exportDisplayedSpectra) + self.listWidget_Spectrum.itemClicked.connect(self.plotNormSpectrum) + self.pb_apply_xanes_norm.clicked.connect(lambda: self.updateSpecData(plotNorm=True)) + self.createMultiSpectrumLibrary() + + [dsb.valueChanged.connect(lambda: self.updateSpecData()) for dsb in + [self.dsb_norm_Eo, self.dsb_norm_pre1, self.dsb_norm_pre2, self.dsb_norm_post1, + self.dsb_norm_post2, self.sb_norm_order]] + + def createSpectrumPropertyDict(self, specName, xdata, ydata, e0, pre1, pre2, norm1, norm2, normOrder): + SingleSpecProperty = {'Name': specName, + 'Data': (xdata, ydata), + 'NormParams': [e0, pre1, pre2, norm1, norm2, normOrder]} + + return SingleSpecProperty + + def createMultiSpectrumLibrary(self): + self.spec_dict = {} + column_names = self.spec_df.columns + spec_array = self.spec_df.to_numpy() + energy = spec_array[:, 0] + for i in range(self.spec_df.shape[1]): + if i != 0: + specData = spec_array[:, i] + e0_init = energy[np.argmax(np.gradient(specData))] + + pre1, pre2, post1, post2 = xanesNormalization( + energy, + specData, + e0=e0_init, + step=None, + nnorm=1, + nvict=0, + method = "guess" + ) + + self.spec_dict[column_names[i]] = self.createSpectrumPropertyDict(column_names[i], energy, specData, + e0_init, pre1, pre2, post1, post2, 1) + self.nomalizeSpectraAndPlot(self.spec_dict) + self.spectrumDictToListWidget() + + def nomalizeSpectraAndPlot(self, spectrumParamDict): + # print(self.spec_dict) + try: + self.spectrum_view.clear() + except: + pass + self.spectrum_view.setLabel("bottom", "Energy") + self.spectrum_view.setLabel("left", "Intensity") + self.spectrum_view.addLegend() + plt_colors = ['r', 'g', (31, 81, 255), 'c', 'm', 'y', 'w'] + for n, params in enumerate(spectrumParamDict.values()): + e0_ = params['NormParams'][0] + pre1_ = params['NormParams'][1] + pre2_ = params['NormParams'][2] + norm1_ = params['NormParams'][3] + norm2_ = params['NormParams'][4] + normOrder_ = params['NormParams'][5] + + preLine, postLine, self.normData = xanesNormalization( + params['Data'][0], + params['Data'][1], + e0=e0_, + step=None, + nnorm=normOrder_, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_ + ) + + # 'NormParams': (e0, pre1, pre2, norm1, norm2, normOrder)} + self.spectrum_view.plot(params['Data'][0], self.normData, + pen=pg.mkPen(plt_colors[n], width=2), + name=f"Norm._{params['Name']}") + + def loadAndPlotSpectrumData(self): + filter = 'txt (*.tiff);;csv (*.csv)' + file_name = QtWidgets.QFileDialog().getOpenFileName(self, "Open a spectrum file", '', + 'txt (*.tiff);;csv (*.csv);;all_files (*)', filter) + + if file_name[0]: + self.spec_df = pd.read_csv(file_name[0], index_col=None) + # print(self.spec_df.head()) + # print(self.spec_df.shape[1]) + self.createMultiSpectrumLibrary() + else: + return + + def spectrumDictToListWidget(self): + for params in self.spec_dict.values(): + # Creates a QListWidgetItem + specItem = QtWidgets.QListWidgetItem() + + # Setting QListWidgetItem Text + specItem.setText(params['Name']) + + # Setting your QListWidgetItem Data + specItem.setData(QtCore.Qt.UserRole, params) + + # Add the new rule to the QListWidget + self.listWidget_Spectrum.addItem(specItem) + + def plotNormSpectrum(self, item): + self.selectedItem = item + self.editItemName = self.selectedItem.text() + self.editItemData = self.selectedItem.data(QtCore.Qt.UserRole) + + e0_ = self.editItemData['NormParams'][0] + pre1_ = self.editItemData['NormParams'][1] + pre2_ = self.editItemData['NormParams'][2] + norm1_ = self.editItemData['NormParams'][3] + norm2_ = self.editItemData['NormParams'][4] + normOrder_ = self.editItemData['NormParams'][5] + + self.dsb_norm_Eo.setValue(e0_) # loop later + self.dsb_norm_pre1.setValue(pre1_) + self.dsb_norm_pre2.setValue(pre2_) + self.dsb_norm_post1.setValue(norm1_) + self.dsb_norm_post2.setValue(norm2_) + self.sb_norm_order.setValue(normOrder_) + + preLine, postLine, normSpec = xanesNormalization( + self.editItemData['Data'][0], + self.editItemData['Data'][1], + e0=e0_, + step=None, + nnorm=normOrder_, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_ + ) + + self.spectrum_view.clear() + self.spectrum_view.plot(self.editItemData['Data'][0], self.editItemData['Data'][1], + title=f"Normalization Plot_{self.editItemData['Name']}", + pen=pg.mkPen('y', width=2), name=self.editItemData['Name']) + self.spectrum_view.plot(self.editItemData['Data'][0], preLine, pen=pg.mkPen('c', width=2), name='Pre') + self.spectrum_view.plot(self.editItemData['Data'][0], postLine, pen=pg.mkPen('m', width=2), name='Norm') + + # def updateNormParamaters(self): + + def updateSpecData(self, plotNorm=False): + + # loop later + self.editItemData['NormParams'][0] = self.dsb_norm_Eo.value() + self.editItemData['NormParams'][1] = self.dsb_norm_pre1.value() + self.editItemData['NormParams'][2] = self.dsb_norm_pre2.value() + self.editItemData['NormParams'][3] = self.dsb_norm_post1.value() + self.editItemData['NormParams'][4] = self.dsb_norm_post2.value() + self.editItemData['NormParams'][5] = self.sb_norm_order.value() + + self.spec_dict[self.editItemName] = self.editItemData + self.selectedItem.setData(QtCore.Qt.UserRole, self.editItemData) + if plotNorm: + self.nomalizeSpectraAndPlot(self.spec_dict) + else: + self.plotNormSpectrum(self.selectedItem) + + def exportDisplayedSpectra(self): + exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + exporter.parameters()['columnMode'] = '(x,y,y,y) for all plots' + file_name = QFileDialog().getSaveFileName(self, "save spectra", 'xanes.csv', 'spectra (*csv)') + if file_name[0]: + exporter.export(file_name[0]) + else: + self.statusbar_main.showMessage('Saving cancelled') + pass + + diff --git a/xmidas/main.py b/xmidas/main.py index 9005597..f3c9353 100644 --- a/xmidas/main.py +++ b/xmidas/main.py @@ -1,4217 +1,2821 @@ -# -*- coding: utf-8 -*- - -# Author: Ajith Pattammattel -# First Version on:06-23-2020 - -import argparse -import logging -import sys -import webbrowser -import traceback -import os -import json -import h5py -import scipy.stats as stats -import numpy as np -import pandas as pd -import tifffile as tf -import pyqtgraph as pg -import pyqtgraph.exporters -import scipy.optimize as opt -import sklearn.decomposition as sd -import sklearn.cluster as sc - -from pyqtgraph import plot -from itertools import combinations -from scipy.stats import linregress -from scipy.signal import savgol_filter -from skimage.transform import resize -from skimage import filters -from sklearn import linear_model -from larch.xafs import preedge -from pystackreg import StackReg -from packaging import version - -from PyQt5 import QtWidgets, QtCore, QtGui, uic, QtTest -from PyQt5.QtGui import QMovie -from PyQt5.QtWidgets import QMessageBox, QFileDialog, QApplication -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR - -from . import __version__ - -# from MultiChannel import * - -logger = logging.getLogger() -try: - import cv2 # noqa: F401 -except Exception: - logger.warning("openCV module not found") - pass -if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): - QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) - -if hasattr(QtCore.Qt, "AA_UseHighDpiPixmaps"): - QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) - -ui_path = os.path.dirname(os.path.abspath(__file__)) - -# global settings for pyqtgraph plot and image colormaps -pg.setConfigOption("imageAxisOrder", "row-major") -cmap_names = ["CET-L13", "CET-L14", "CET-L15"] -cmap_combo = combinations(cmap_names, 2) -cmap_label1 = ["red", "green", "blue"] -cmap_label2 = ["yellow", "magenta", "cyan"] -cmap_dict = {} -for i, name in zip(cmap_names, cmap_label1): - cmap_dict[name] = pg.colormap.get(i).getLookupTable(alpha=True) - -for i, name in zip(cmap_combo, cmap_label2): - cmap_dict[name] = pg.colormap.get(i[0]).getLookupTable(alpha=True) + pg.colormap.get(i[1]).getLookupTable( - alpha=True - ) - cmap_dict[name][:, 3] = 255 - - grey = ( - pg.colormap.get("CET-L13").getLookupTable(alpha=True) - + pg.colormap.get("CET-L14").getLookupTable(alpha=True) - + pg.colormap.get("CET-L15").getLookupTable(alpha=True) - ) - - grey[:, 3] = 255 - cmap_dict["grey"] = grey - - -class jsonEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, np.integer): - return int(obj) - elif isinstance(obj, np.floating): - return float(obj) - elif isinstance(obj, np.ndarray): - return obj.tolist() - else: - return super(jsonEncoder, self).default(obj) - - -class midasWindow(QtWidgets.QMainWindow): - def __init__(self, im_stack=None, energy=[], refs=[]): - super(midasWindow, self).__init__() - uic.loadUi(os.path.join(ui_path, "uis/midasMainwindow.ui"), self) - self.im_stack = im_stack - self.energy = energy - self.refs = refs - self.loaded_tranform_file = [] - self.image_roi2_flag = False - self.refStackAvailable = False - self.isAReload = False - self.plotWidth = 2 - self.stackStatusDict = {} - - self.plt_colors = [ - "g", - "r", - "c", - "m", - "y", - "w", - "b", - pg.mkPen(70, 5, 80), - pg.mkPen(255, 85, 130), - pg.mkPen(0, 85, 130), - pg.mkPen(255, 170, 60), - ] * 3 - # window style - self.actionDarkMode.triggered.connect(self.darkMode) - self.actionDefault.triggered.connect(self.defaultMode) - self.actionModern.triggered.connect(self.modernMode) - - # self.setToolTipsVisible(True) - for menuItem in self.findChildren(QtWidgets.QMenu): - menuItem.setToolTipsVisible(True) - - # plotview options - self.actionWhite.triggered.connect(lambda: self.spectrum_view.setBackground("w")) - self.actionRed.triggered.connect(lambda: self.spectrum_view.setBackground("r")) - self.actionYellow.triggered.connect(lambda: self.spectrum_view.setBackground("y")) - self.actionBlue.triggered.connect(lambda: self.spectrum_view.setBackground("b")) - self.actionBlack.triggered.connect(lambda: self.spectrum_view.setBackground((0, 0, 0))) - - self.actn1.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn1.text()))) - self.actn2.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn2.text()))) - self.actn3.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn3.text()))) - self.actn4.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn4.text()))) - self.actn5.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn5.text()))) - self.actn6.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn6.text()))) - self.actn8.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn8.text()))) - self.actn10.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn10.text()))) - - self.actionOpen_Image_Data.triggered.connect(self.browse_file) - self.actionOpen_Multiple_Files.triggered.connect(self.createVirtualStack) - self.actionSave_as.triggered.connect(lambda: self.save_stack()) - self.actionExit.triggered.connect(lambda: QApplication.closeAllWindows()) - self.actionOpen_in_GitHub.triggered.connect(self.open_github_link) - self.actionLoad_Energy.triggered.connect(self.select_elist) - self.menuFile.setToolTipsVisible(True) - - # Accessories - self.actionOpen_Mask_Gen.triggered.connect(self.openMaskMaker) - self.actionMultiColor.triggered.connect(self.openMultiColorWindow) - - # calculations - self.pb_transpose_stack.clicked.connect(lambda: self.threadMaker(self.transposeStack)) - self.pb_swapXY_stack.clicked.connect(lambda: self.threadMaker(self.swapStackXY)) - self.pb_reset_img.clicked.connect(self.reloadImageStack) - self.pb_crop.clicked.connect(self.crop_to_dim) - self.pb_crop.clicked.connect(self.view_stack) - self.sb_scaling_factor.valueChanged.connect(self.view_stack) - self.pb_ref_xanes.clicked.connect(self.select_ref_file) - self.pb_elist_xanes.clicked.connect(self.select_elist) - - # batchjobs - self.actionPlotAllCorrelations.triggered.connect(self.plotCorrelationsAllCombinations) - - [ - uis.valueChanged.connect(self.replot_image) - for uis in [self.hs_smooth_size, self.hs_nsigma, self.hs_bg_threshold] - ] - - [ - uis.stateChanged.connect(self.replot_image) - for uis in [self.cb_remove_bg, self.cb_remove_outliers, self.cb_smooth, self.cb_norm, self.cb_log] - ] - - [ - uis.stateChanged.connect(self.view_stack) - for uis in [self.cb_remove_edges, self.cb_upscale, self.cb_rebin] - ] - - # ToolBar - self.actionStack_Info.triggered.connect(self.displayStackInfo) - self.actionSave_Image.triggered.connect(self.save_disp_img) - self.actionExport_Stack.triggered.connect(lambda: self.save_stack()) - - # ROI background - self.actionSubtract_ROI_BG.triggered.connect(lambda: self.threadMaker(self.removeROIBGStack)) - - # alignment - self.pb_load_align_ref.clicked.connect(self.loadAlignRefImage) - self.pb_loadAlignTranform.clicked.connect(self.importAlignTransformation) - self.pb_saveAlignTranform.clicked.connect(self.exportAlignTransformation) - self.pb_alignStack.clicked.connect(lambda: self.threadMaker(self.stackRegistration)) - # self.pb_alignStack.clicked.connect(self.stackRegistration) - - # save_options - self.actionSave_Sum_Image.triggered.connect(lambda: self.save_stack(method="sum")) - self.actionSave_Mean_Image.triggered.connect(lambda: self.save_stack(method="mean")) - self.pb_save_disp_spec.clicked.connect(self.save_disp_spec) - self.actionSave_Energy_List.triggered.connect(self.saveEnergyList) - self.pb_show_roi.clicked.connect(self.getROIMask) - self.pb_addToCollector.clicked.connect(self.addSpectrumToCollector) - self.pb_collect_clear.clicked.connect(lambda: self.spectrum_view_collect.clear()) - self.pb_saveCollectorPlot.clicked.connect(self.saveCollectorPlot) - - # XANES Normalization - self.pb_apply_xanes_norm.clicked.connect(self.nomalizeLiveSpec) - self.pb_auto_Eo.clicked.connect(self.findEo) - self.pb_xanes_norm_vals.clicked.connect(self.initNormVals) - self.pb_apply_norm_to_stack.clicked.connect(lambda: self.threadMaker(self.normalizeStack)) - self.actionExport_Norm_Params.triggered.connect(self.exportNormParams) - self.actionImport_Norm_Params.triggered.connect(self.importNormParams) - - # Analysis - self.pb_pca_scree.clicked.connect(self.pca_scree_) - self.pb_calc_components.clicked.connect(self.calc_comp_) - self.pb_kmeans_elbow.clicked.connect(self.kmeans_elbow) - self.pb_calc_cluster.clicked.connect(self.clustering_) - self.pb_xanes_fit.clicked.connect(self.fast_xanes_fitting) - self.pb_plot_refs.clicked.connect(self.plt_xanes_refs) - - self.show() - - self.threadpool = QThreadPool() - print(f"Multithreading with maximum {self.threadpool.maxThreadCount()} threads") - - # View Options - def darkMode(self): - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/darkStyle.css")).read()) - - def defaultMode(self): - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - - def modernMode(self): - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/modern.css")).read()) - - def setPlotLineWidth(self, width_input): - self.plotWidth = width_input - try: - self.update_spectrum() - except Exception: - pass - - def openMultiColorWindow(self): - self.multicolorwindow = MultiChannelWindow() - self.multicolorwindow.show() - - def openMaskMaker(self): - self.mask_window = MaskSpecViewer(xanes_stack=self.displayedStack, energy=self.energy) - self.mask_window.show() - - def open_github_link(self): - webbrowser.open("https://github.com/pattammattel/NSLS-II-MIDAS/wiki") - - def threadMaker(self, funct): - # Pass the function to execute - worker = Worker(funct) # Any other args, kwargs are passed to the run function - self.loadSplashScreen() - worker.signals.start.connect(self.splash.startAnimation) - worker.signals.result.connect(self.print_output) - - list( - map( - worker.signals.finished.connect, - [ - self.thread_complete, - self.splash.stopAnimation, - self.update_stack_info, - self.update_spectrum, - self.update_image_roi, - ], - ) - ) - - # Execute - self.threadpool.start(worker) - - # File Loading - - def createVirtualStack(self): - """User can load multiple/series of tiff images with same shape. - The 'self.load_stack()' recognizes 'self.filename as list and create the stack. - """ - self.energy = [] - filter = "TIFF (*.tiff);;TIF (*.tif);;all_files (*)" - file_name = QFileDialog() - file_name.setFileMode(QFileDialog.ExistingFiles) - names = file_name.getOpenFileNames(self, "Open files", " ", filter) - if names[0]: - self.file_name = names[0] - self.load_stack() - - else: - self.statusbar_main.showMessage("No file has selected") - pass - - def load_stack(self): - """load the image data from the selected file. - If the the choice is for multiple files stack will be created in a loop. - If single h5 file is selected the unpacking will be done with 'get_xrf_data' function in StackCalcs. - From the h5 the program can recognize the beamline. The exported stack will be normalized to I0. - - If the single tiff file is choosen tf.imread() is used. - - The output 'self.im_stack' is the unmodified data file - """ - - self.log_warning = False # for the Qmessage box in cb_log - self.image_roi2_flag = False - self.cb_log.setChecked(False) - self.cb_remove_edges.setChecked(False) - self.cb_norm.setChecked(False) - self.cb_smooth.setChecked(False) - self.cb_remove_outliers.setChecked(False) - self.cb_remove_bg.setChecked(False) - self.cb_rebin.setChecked(False) - self.cb_upscale.setChecked(False) - self.sb_xrange1.setValue(0) - self.sb_yrange1.setValue(0) - self.sb_zrange1.setValue(0) - - self.menuMask.setEnabled(True) - self.actionLoad_Energy.setEnabled(True) - self.actionSave_Energy_List.setEnabled(True) - self.actionSave_as.setEnabled(True) - - self.sb_zrange2.setMaximum(99999) - self.sb_xrange2.setMaximum(99999) - self.sb_yrange2.setMaximum(99999) - - self.statusbar_main.showMessage("Loading.. please wait...") - - if isinstance(self.file_name, list): - all_images = [] - - for im_file in self.file_name: - img = tf.imread(im_file) - all_images.append(img) # row major image - self.im_stack = np.dstack(all_images).transpose((2, 0, 1)) - self.avgIo = 1 # I0 is only applicable to XRF h5 files - self.sb_zrange2.setValue(self.im_stack.shape[0]) - - else: - if self.file_name.endswith(".h5"): - self.im_stack, mono_e, bl_name, self.avgIo = get_xrf_data(self.file_name) - self.statusbar_main.showMessage(f"Data from {bl_name}") - self.sb_zrange2.setValue(mono_e / 10) - self.energy = [] - - elif self.file_name.endswith(".tiff") or self.file_name.endswith(".tif"): - self.im_stack_ = tf.imread(self.file_name) - if self.im_stack_.ndim == 2: - self.im_stack = self.im_stack_.reshape(1, self.im_stack_.shape[0], self.im_stack_.shape[1]) - - else: - self.im_stack = self.im_stack_ - self.sb_zrange2.setValue(self.im_stack.shape[0]) - self.autoEnergyLoader() - self.energyUnitCheck() - self.avgIo = 1 - - else: - logger.error("Unknown data format") - - """ Fill the stack dimensions to the GUI and set the image dimensions as max values. - This prevent user from choosing higher image dimensions during a resizing event""" - - logger.info(f" loaded stack with {np.shape(self.im_stack)} from the file") - - try: - logger.info(f" Transposed to shape: {np.shape(self.im_stack)}") - self.init_dimZ, self.init_dimY, self.init_dimX = self.im_stack.shape - # Remove any previously set max value during a reload - - self.sb_xrange2.setValue(self.init_dimX) - self.sb_yrange2.setValue(self.init_dimY) - - except UnboundLocalError: - logger.error("No file selected") - pass - - self.view_stack() - logger.info("Stack displayed correctly") - self.update_stack_info() - - logger.info(f"completed image shape {np.shape(self.im_stack)}") - - try: - self.statusbar_main.showMessage(f"Loaded: {self.file_name}") - - except AttributeError: - self.statusbar_main.showMessage("New Stack is made from selected tiffs") - pass - - def browse_file(self): - """To open a file widow and choose the data file. - The filename will be used to load data using 'rest and load stack' function""" - - filename = QFileDialog().getOpenFileName( - self, "Select image data", "", "image file(*.hdf *.h5 *tiff *tif )" - ) - self.file_name = str(filename[0]) - - # if user decides to cancel the file window gui returns to original state - if self.file_name: - self.disconnectImageActions() - self.isAReload = False - self.load_stack() - - else: - self.statusbar_main.showMessage("No file has selected") - pass - - def autoEnergyLoader(self): - dir_, filename_ = os.path.split(self.file_name) - self.efilePath_name = os.path.join(dir_, os.path.splitext(filename_)[0] + ".txt") - self.efilePath_log = os.path.join(dir_, "maps_log_tiff.txt") - - if os.path.isfile(self.efilePath_name): - self.efilePath = self.efilePath_name - self.efileLoader() - self.statusbar_main.showMessage(f"Energy File detected {self.efilePath}") - - elif os.path.isfile(self.efilePath_log): - self.efilePath = self.efilePath_log - self.efileLoader() - self.statusbar_main.showMessage(f"Energy File detected {self.efilePath}") - - else: - self.efilePath = False - self.efileLoader() - - def update_stack_info(self): - z, y, x = np.shape(self.displayedStack) - self.sb_zrange2.setMaximum(z + self.sb_zrange1.value()) - self.sb_xrange2.setValue(x) - self.sb_xrange2.setMaximum(x) - self.sb_yrange2.setValue(y) - self.sb_yrange2.setMaximum(y) - logger.info("Stack info has been updated") - - # Image Transformations - - def crop_to_dim(self): - self.x1, self.x2 = self.sb_xrange1.value(), self.sb_xrange2.value() - self.y1, self.y2 = self.sb_yrange1.value(), self.sb_yrange2.value() - self.z1, self.z2 = self.sb_zrange1.value(), self.sb_zrange2.value() - - try: - self.displayedStack = remove_nan_inf( - self.displayedStack[self.z1 : self.z2, self.y1 : self.y2, self.x1 : self.x2] - ) - except Exception: - self.displayedStack = remove_nan_inf( - self.im_stack[self.z1 : self.z2, self.y1 : self.y2, self.x1 : self.x2] - ) - - def transpose_stack(self): - self.displayedStack = self.displayedStack.T - self.update_spectrum() - self.update_spec_image_roi() - - # Alignement - - def loadAlignRefImage(self): - filename = QFileDialog().getOpenFileName(self, "Image Data", "", "*.tiff *.tif") - file_name = str(filename[0]) - self.alignRefImage = tf.imread(file_name) - assert self.alignRefImage.shape == self.displayedStack.shape, "Image dimensions do not match" - self.refStackAvailable = True - self.rb_alignRefVoid.setChecked(False) - self.change_color_on_load(self.pb_load_align_ref) - - def stackRegistration(self): - self.transformations = { - "TRANSLATION": StackReg.TRANSLATION, - "RIGID_BODY": StackReg.RIGID_BODY, - "SCALED_ROTATION": StackReg.SCALED_ROTATION, - "AFFINE": StackReg.AFFINE, - "BILINEAR": StackReg.BILINEAR, - } - - self.transformType = self.transformations[self.cb_alignTransform.currentText()] - self.alignReferenceImage = self.cb_alignRef.currentText() - self.alignRefStackVoid = self.rb_alignRefVoid.isChecked() - self.alignMaxIter = self.sb_maxIterVal.value() - - if self.cb_use_tmatFile.isChecked(): - if len(self.loaded_tranform_file) > 0: - self.displayedStack = align_with_tmat( - self.displayedStack, tmat_file=self.loaded_tranform_file, transformation=self.transformType - ) - logger.info("Aligned to the tranform File") - - else: - logger.error("No Tranformation File Loaded") - - elif self.cb_iterAlign.isChecked(): - if not self.refStackAvailable: - self.alignRefImage = self.displayedStack - else: - pass - - self.displayedStack = align_stack_iter( - self.displayedStack, - ref_stack_void=False, - ref_stack=self.alignRefImage, - transformation=self.transformType, - method=("previous", "first"), - max_iter=self.alignMaxIter, - ) - - else: - if not self.refStackAvailable: - self.alignRefImage = self.displayedStack - - else: - pass - - self.displayedStack, self.tranform_file = align_stack( - self.displayedStack, - ref_image_void=True, - ref_stack=self.alignRefImage, - transformation=self.transformType, - reference=self.alignReferenceImage, - ) - logger.info("New Tranformation file available") - self.im_stack = self.displayedStack - - def exportAlignTransformation(self): - file_name = QFileDialog().getSaveFileName( - self, "Save Transformation File", "TranformationMatrix.npy", "text file (*.npy)" - ) - if file_name[0]: - np.save(file_name[0], self.tranform_file) - else: - pass - - def importAlignTransformation(self): - file_name = QFileDialog().getOpenFileName(self, "Open Transformation File", " ", "text file (*.npy)") - if file_name[0]: - self.loaded_tranform_file = np.load(file_name[0]) - self.cb_use_tmatFile.setChecked(True) - logger.info("Tranformation File Loaded") - else: - pass - - def loadSplashScreen(self): - self.splash = LoadingScreen() - - px = self.geometry().x() - py = self.geometry().y() - ph = self.geometry().height() - pw = self.geometry().width() - dw = self.splash.width() - dh = self.splash.height() - new_x, new_y = px + (0.5 * pw) - dw, py + (0.5 * ph) - dh - self.splash.setGeometry(new_x, new_y, dw, dh) - - self.splash.show() - - def reloadImageStack(self): - self.isAReload = True - self.load_stack() - - def update_stack(self): - self.displayedStack = self.im_stack - self.crop_to_dim() - - if self.cb_rebin.isChecked(): - self.cb_upscale.setChecked(False) - self.sb_scaling_factor.setEnabled(True) - self.displayedStack = resize_stack(self.displayedStack, scaling_factor=self.sb_scaling_factor.value()) - self.update_stack_info() - - elif self.cb_upscale.isChecked(): - self.cb_rebin.setChecked(False) - self.sb_scaling_factor.setEnabled(True) - self.displayedStack = resize_stack( - self.displayedStack, upscaling=True, scaling_factor=self.sb_scaling_factor.value() - ) - self.update_stack_info() - - if self.cb_remove_outliers.isChecked(): - self.hs_nsigma.setEnabled(True) - nsigma = self.hs_nsigma.value() / 10 - self.displayedStack = remove_hot_pixels(self.displayedStack, NSigma=nsigma) - self.label_nsigma.setText(str(nsigma)) - logger.info(f"Removing Outliers with NSigma {nsigma}") - - elif self.cb_remove_outliers.isChecked() is False: - self.hs_nsigma.setEnabled(False) - - if self.cb_remove_edges.isChecked(): - self.displayedStack = remove_edges(self.displayedStack) - logger.info(f"Removed edges, new shape {self.displayedStack.shape}") - self.update_stack_info() - - if self.cb_remove_bg.isChecked(): - self.hs_bg_threshold.setEnabled(True) - logger.info("Removing background") - bg_threshold = self.hs_bg_threshold.value() - self.label_bg_threshold.setText(str(bg_threshold) + "%") - self.displayedStack = clean_stack(self.displayedStack, auto_bg=False, bg_percentage=bg_threshold) - - elif self.cb_remove_bg.isChecked() is False: - self.hs_bg_threshold.setEnabled(False) - - if self.cb_log.isChecked(): - self.displayedStack = remove_nan_inf(np.log10(self.displayedStack)) - logger.info("Log Stack is in use") - - if self.cb_smooth.isChecked(): - self.hs_smooth_size.setEnabled(True) - window = self.hs_smooth_size.value() - if window % 2 == 0: - window = +1 - self.smooth_winow_size.setText("Window size: " + str(window)) - self.displayedStack = smoothen(self.displayedStack, w_size=window) - logger.info("Spectrum Smoothening Applied") - - elif self.cb_smooth.isChecked() is False: - self.hs_smooth_size.setEnabled(False) - - if self.cb_norm.isChecked(): - logger.info("Normalizing spectra") - self.displayedStack = normalize(self.displayedStack, norm_point=-1) - - logger.info("Updated image is in use") - - # ImageView - - def view_stack(self): - if not self.im_stack.ndim == 3: - raise ValueError("stack should be an ndarray with ndim == 3") - else: - self.update_stack() - # self.StackUpdateThread() - - try: - self.image_view.removeItem(self.image_roi_math) - except Exception: - pass - - (self.dim1, self.dim2, self.dim3) = self.displayedStack.shape - self.image_view.setImage(self.displayedStack) - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - self.image_view.setPredefinedGradient("viridis") - self.image_view.setCurrentIndex(self.dim1 // 2) - if len(self.energy) == 0: - self.energy = np.arange(self.z1, self.z2) * 10 - logger.info("Arbitary X-axis used in the plot for XANES") - self.sz = np.max( - [int(self.dim2 * 0.1), int(self.dim3 * 0.1)] - ) # size of the roi set to be 10% of the image area - - self.stack_center = self.energy[len(self.energy) // 2] - self.stack_width = (self.energy.max() - self.energy.min()) // 10 - self.spec_roi = pg.LinearRegionItem( - values=(self.stack_center - self.stack_width, self.stack_center + self.stack_width) - ) - - # a second optional ROI for calculations follow - self.image_roi_math = pg.PolyLineROI( - [[0, 0], [0, self.sz], [self.sz, self.sz], [self.sz, 0]], - pos=(int(self.dim3 // 3), int(self.dim2 // 3)), - pen="r", - closed=True, - removable=True, - ) - - self.spec_roi_math = pg.LinearRegionItem( - values=(self.stack_center - self.stack_width - 10, self.stack_center + self.stack_width - 10), - pen="r", - brush=QtGui.QColor(0, 255, 200, 50), - ) - self.spec_lo_m_idx = self.spec_hi_m_idx = 0 - - self.setImageROI() - self.update_spectrum() - self.update_image_roi() - - if not self.isAReload: - # image connections - self.image_view.mousePressEvent = self.getPointSpectrum - self.spec_roi.sigRegionChanged.connect(self.update_image_roi) - self.spec_roi_math.sigRegionChangeFinished.connect(self.spec_roi_calc) - self.pb_apply_spec_calc.clicked.connect(self.spec_roi_calc) - self.rb_math_roi.clicked.connect(self.update_spectrum) - self.pb_add_roi_2.clicked.connect(self.math_img_roi_flag) - self.image_roi_math.sigRegionChangeFinished.connect(self.image_roi_calc) - self.pb_apply_img_calc.clicked.connect(self.image_roi_calc) - - [ - rbs.clicked.connect(self.setImageROI) - for rbs in [self.rb_poly_roi, self.rb_elli_roi, self.rb_rect_roi, self.rb_line_roi, self.rb_circle_roi] - ] - - def disconnectImageActions(self): - for btns in [self.pb_apply_spec_calc, self.rb_math_roi, self.pb_add_roi_2, self.pb_apply_img_calc]: - try: - btns.disconnect() - except Exception: - pass - - def select_elist(self): - self.energyFileChooser() - self.efileLoader() - self.energyUnitCheck() - self.view_stack() - - def efileLoader(self): - if self.efilePath: - if str(self.efilePath).endswith("log_tiff.txt"): - self.energy = energy_from_logfile(logfile=str(self.efilePath)) - logger.info("Log file from pyxrf processing") - - else: - self.energy = np.loadtxt(str(self.efilePath)) - self.change_color_on_load(self.pb_elist_xanes) - logger.info("Energy file loaded") - - else: - self.statusbar_main.showMessage("No Energy List Selected, Setting Arbitary Axis") - self.energy = np.arange(self.im_stack.shape[0]) - logger.info("Arbitary Energy Axis") - - # assert len(self.energy) == self.dim1, "Number of Energy Points is not equal to stack length" - - def energyUnitCheck(self): - if np.max(self.energy) < 100: - self.cb_kev_flag.setChecked(True) - self.energy *= 1000 - - else: - self.cb_kev_flag.setChecked(False) - - def select_ref_file(self): - self.pb_xanes_fit.setEnabled(True) - self.ref_names = [] - file_name = QFileDialog().getOpenFileName(self, "Open reference file", "", "text file (*.txt *.nor)") - if file_name[0]: - if file_name[0].endswith(".nor"): - self.refs, self.ref_names = create_df_from_nor_try2(athenafile=str(file_name[0])) - self.change_color_on_load(self.pb_ref_xanes) - - elif file_name[0].endswith(".txt"): - self.refs = pd.read_csv(str(file_name[0]), header=None, delim_whitespace=True) - self.change_color_on_load(self.pb_ref_xanes) - - else: - logger.error("No file selected") - pass - - self.plt_xanes_refs() - - def plt_xanes_refs(self): - try: - self.ref_plot.close() - except Exception: - pass - - self.ref_plot = plot(title="Reference Standards") - self.ref_plot.setLabel("bottom", "Energy") - self.ref_plot.setLabel("left", "Intensity") - self.ref_plot.addLegend() - - for n in range(np.shape(self.refs)[1]): - if not n == 0: - self.ref_plot.plot( - self.refs.values[:, 0], - self.refs.values[:, n], - pen=pg.mkPen(self.plt_colors[n - 1], width=self.plotWidth), - name=self.ref_names[n], - ) - - def getPointSpectrum(self, event): - if event.type() == QtCore.QEvent.MouseButtonDblClick: - if event.button() == QtCore.Qt.LeftButton: - self.xpixel = int(self.image_view.view.mapSceneToView(event.pos()).x()) - 1 - zlim, ylim, xlim = self.displayedStack.shape - - if self.xpixel > xlim: - self.xpixel = xlim - 1 - - self.ypixel = int(self.image_view.view.mapSceneToView(event.pos()).y()) - 1 - if self.ypixel > ylim: - self.ypixel = ylim - 1 - self.spectrum_view.addLegend() - self.point_spectrum = self.displayedStack[:, self.ypixel, self.xpixel] - self.spectrum_view.plot( - self.xdata, - self.point_spectrum, - clear=True, - pen=pg.mkPen(pg.mkColor(0, 0, 255, 255), width=self.plotWidth), - symbol="o", - symbolSize=6, - symbolBrush="r", - name=f"Point Spectrum; x= {self.xpixel}, y= {self.ypixel}", - ) - - self.spectrum_view.addItem(self.spec_roi) - - self.statusbar_main.showMessage(f"{self.xpixel} and {self.ypixel}") - - def setImageROI(self): - self.lineROI = pg.LineSegmentROI([[int(self.dim3 // 2), int(self.dim2 // 2)], [self.sz, self.sz]], pen="r") - - self.rectROI = pg.RectROI( - [int(self.dim3 // 2), int(self.dim2 // 2)], - [self.sz, self.sz], - pen="w", - maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), - ) - - self.rectROI.addTranslateHandle([0, 0], [2, 2]) - self.rectROI.addRotateHandle([0, 1], [2, 2]) - - self.ellipseROI = pg.EllipseROI( - [int(self.dim3 // 2), int(self.dim2 // 2)], - [self.sz, self.sz], - pen="w", - maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), - ) - - self.circleROI = pg.CircleROI( - [int(self.dim3 // 2), int(self.dim2 // 2)], - [self.sz, self.sz], - pen="w", - maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), - ) # pos and size - - self.polyLineROI = pg.PolyLineROI( - [[0, 0], [0, self.sz], [self.sz, self.sz], [self.sz, 0]], - pos=(int(self.dim3 // 2), int(self.dim2 // 2)), - maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), - closed=True, - removable=True, - ) - - self.rois = { - "rb_line_roi": self.lineROI, - "rb_rect_roi": self.rectROI, - "rb_circle_roi": self.circleROI, - "rb_elli_roi": self.ellipseROI, - "rb_poly_roi": self.polyLineROI, - } - - button_name = self.sender() - - if button_name.objectName() in self.rois.keys(): - self.roi_preference = button_name.objectName() - - else: - self.roi_preference = "rb_rect_roi" # default - - try: - self.image_view.removeItem(self.image_roi) - - except Exception: - pass - - # ROI settings for image, used polyline roi with non rectangular shape - - self.image_roi = self.rois[self.roi_preference] - self.image_view.addItem(self.image_roi) - self.image_roi.sigRegionChanged.connect(self.update_spectrum) - - def replot_image(self): - self.update_stack() - self.update_spectrum() - self.update_image_roi() - - def update_spec_roi_values(self): - self.stack_center = int(self.energy[len(self.energy) // 2]) - self.stack_width = int((self.energy.max() - self.energy.min()) * 0.05) - self.spec_roi.setBounds([self.xdata[0], self.xdata[-1]]) # if want to set bounds for the spec roi - self.spec_roi_math.setBounds([self.xdata[0], self.xdata[-1]]) - - def update_spectrum(self): - # set x-axis values; array taken from energy values, if clipped z box values will update the array - self.xdata = self.energy[self.sb_zrange1.value() : self.sb_zrange2.value()] - - # get the cropped stack from ROI region; pyqtgraph function is used - self.roi_img_stk = self.image_roi.getArrayRegion( - self.displayedStack, self.image_view.imageItem, axes=(1, 2) - ) - - posx, posy = self.image_roi.pos() - self.le_roi.setText(str(int(posx)) + ":" + str(int(posy))) - - # display the ROI features in the line edit boxes - if self.roi_img_stk.ndim == 3: - sizex, sizey = self.roi_img_stk.shape[1], self.roi_img_stk.shape[2] - self.le_roi_size.setText(str(sizex) + "," + str(sizey)) - self.mean_spectra = get_mean_spectra(self.roi_img_stk) - - elif self.roi_img_stk.ndim == 2: - sizex, sizey = self.roi_img_stk.shape[0], self.roi_img_stk.shape[1] - self.le_roi_size.setText(str(sizex) + "," + str(sizey)) - self.mean_spectra = self.roi_img_stk.mean(-1) - - self.spectrum_view.addLegend() - - try: - self.spectrum_view.plot( - self.xdata, - self.mean_spectra, - pen=pg.mkPen(pg.mkColor(5, 255, 5, 255), width=self.plotWidth), - clear=True, - symbol="o", - symbolSize=6, - symbolBrush="r", - name="ROI Spectrum", - ) - except Exception: - self.spectrum_view.plot( - self.mean_spectra, - clear=True, - pen=pg.mkPen(pg.mkColor(5, 255, 5, 255), width=self.plotWidth), - symbol="o", - symbolSize=6, - symbolBrush="r", - name="ROI Spectrum", - ) - - if self.energy[-1] > 1000: - self.e_unit = "eV" - else: - self.e_unit = "keV" - - self.spectrum_view.setLabel("bottom", "Energy", self.e_unit) - self.spectrum_view.setLabel("left", "Intensity", "A.U.") - self.spectrum_view.addItem(self.spec_roi) - self.update_spec_roi_values() - self.math_roi_flag() - - def update_image_roi(self): - self.spec_lo, self.spec_hi = self.spec_roi.getRegion() - self.spec_lo_idx = (np.abs(self.energy - self.spec_lo)).argmin() - self.spec_hi_idx = (np.abs(self.energy - self.spec_hi)).argmin() - self.le_spec_roi.setText(str(int(self.spec_lo)) + ":" + str(int(self.spec_hi))) - self.le_spec_roi_size.setText(str(int(self.spec_hi - self.spec_lo))) - self.update_spec_roi_values() - self.stackIndexToNames() - - try: - if int(self.spec_lo_idx) == int(self.spec_hi_idx): - self.disp_img = self.displayedStack[int(self.spec_hi_idx), :, :] - - else: - self.disp_img = self.displayedStack[int(self.spec_lo_idx) : int(self.spec_hi_idx), :, :].mean(0) - - self.image_view.setImage(self.disp_img) - self.statusbar_main.showMessage(f"Image Display is {self.corrImg1}") - except Exception: - logger.warning("Indices are out of range; Image cannot be created") - pass - - def set_spec_roi(self): - self.spec_lo_, self.spec_hi_ = int(self.sb_roi_spec_s.value()), int(self.sb_roi_spec_e.value()) - self.spec_lo_idx_ = (np.abs(self.energy - self.spec_lo_)).argmin() - self.spec_hi_idx_ = (np.abs(self.energy - self.spec_hi_)).argmin() - self.spec_roi.setRegion((self.xdata[self.spec_lo_idx_], self.xdata[self.spec_hi_idx_])) - self.update_image_roi() - - def math_roi_flag(self): - if self.rb_math_roi.isChecked(): - self.rb_math_roi.setStyleSheet("color : green") - self.spectrum_view.addItem(self.spec_roi_math) - else: - self.spectrum_view.removeItem(self.spec_roi_math) - - def spec_roi_calc(self): - self.spec_lo_m, self.spec_hi_m = self.spec_roi_math.getRegion() - self.spec_lo_m_idx = (np.abs(self.energy - self.spec_lo_m)).argmin() - self.spec_hi_m_idx = (np.abs(self.energy - self.spec_hi_m)).argmin() - - if int(self.spec_lo_idx) == int(self.spec_hi_idx): - self.img1 = self.displayedStack[int(self.spec_hi_idx), :, :] - - else: - self.img1 = self.displayedStack[int(self.spec_lo_idx) : int(self.spec_hi_idx), :, :].mean(0) - - if int(self.spec_lo_m_idx) == int(self.spec_hi_m_idx): - self.img2 = self.displayedStack[int(self.spec_hi_m_idx), :, :] - - else: - self.img2 = self.displayedStack[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx), :, :].mean(0) - - if self.cb_roi_operation.currentText() == "Correlation Plot": - self.correlation_plot() - - else: - calc = {"Divide": np.divide, "Subtract": np.subtract, "Add": np.add} - self.disp_img = remove_nan_inf(calc[self.cb_roi_operation.currentText()](self.img1, self.img2)) - self.image_view.setImage(self.disp_img) - - def math_img_roi_flag(self): - button_name = self.sender().text() - logger.info(f"{button_name}") - if button_name == "Add ROI_2": - self.image_view.addItem(self.image_roi_math) - self.pb_add_roi_2.setText("Remove ROI_2") - self.image_roi2_flag = 1 - elif button_name == "Remove ROI_2": - self.image_view.removeItem(self.image_roi_math) - self.pb_add_roi_2.setText("Add ROI_2") - self.image_roi2_flag = 0 - - else: - pass - logger.error("Unknown signal for second ROI") - - def image_roi_calc(self): - if self.image_roi2_flag == 1: - self.calc = {"Divide": np.divide, "Subtract": np.subtract, "Add": np.add} - self.update_spec_image_roi() - else: - logger.error("No ROI2 found") - return - - def update_spec_image_roi(self): - self.math_roi_reg = self.image_roi_math.getArrayRegion( - self.displayedStack, self.image_view.imageItem, axes=(1, 2) - ) - if self.math_roi_reg.ndim == 3: - self.math_roi_spectra = get_mean_spectra(self.math_roi_reg) - - elif self.roi_img_stk.ndim == 2: - self.math_roi_spectra = self.math_roi_reg.mean(-1) - - if self.cb_img_roi_action.currentText() in self.calc.keys(): - calc_spec = self.calc[self.cb_img_roi_action.currentText()](self.mean_spectra, self.math_roi_spectra) - self.spectrum_view.addLegend() - self.spectrum_view.plot( - self.xdata, - calc_spec, - clear=True, - pen=pg.mkPen("m", width=2), - name=self.cb_img_roi_action.currentText() + "ed", - ) - self.spectrum_view.plot(self.xdata, self.math_roi_spectra, pen=pg.mkPen("y", width=2), name="ROI2") - self.spectrum_view.plot(self.xdata, self.mean_spectra, pen=pg.mkPen("g", width=2), name="ROI1") - - elif self.cb_img_roi_action.currentText() == "Compare": - self.spectrum_view.plot( - self.xdata, self.math_roi_spectra, pen=pg.mkPen("y", width=2), clear=True, name="ROI2" - ) - self.spectrum_view.plot(self.xdata, self.mean_spectra, pen=pg.mkPen("g", width=2), name="ROI1") - - self.spectrum_view.addItem(self.spec_roi) - - def displayStackInfo(self): - try: - if isinstance(self.file_name, list): - info = f"Folder; {os.path.dirname(self.file_name[0])} \n" - for n, name in enumerate(self.file_name): - info += f"{n}: {os.path.basename(name)} \n" - - # info = f'Stack order; {[name for name in enumerate(self.file_name)]}' - else: - info = f"Stack; {self.file_name}" - - self.infoWindow = StackInfo(str(info)) - self.infoWindow.show() - - except AttributeError: - self.statusbar_main.showMessage("Warning: No Image Data Loaded") - - def stackIndexToNames(self): - # create list of tiff file names for virtutal stack for plot axes - self.elemFileName = [] - - if isinstance(self.file_name, list): - for name in self.file_name: - self.elemFileName.append(os.path.basename(name).split(".")[0]) - - logger.info(f" Virtual Stack - list of image names; {self.elemFileName}") - - # if the roi focus on one frame, Note that this slicing excludes the last index - if int(self.spec_lo_idx) == int(self.spec_hi_idx): - self.corrImg1 = str(self.elemFileName[int(self.spec_lo_idx)]) - else: - self.corrImg1 = self.elemFileName[int(self.spec_lo_idx) : int(self.spec_hi_idx)] - if len(self.corrImg1) > 1: - self.corrImg1 = f"Sum of {self.corrImg1} " - - if int(self.spec_lo_m_idx) == int(self.spec_hi_m_idx): - self.corrImg2 = str(self.elemFileName[int(self.spec_lo_m_idx)]) - - else: - self.corrImg2 = self.elemFileName[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx)] - - if len(self.corrImg2) > 1: - self.corrImg2 = f"Sum of {self.corrImg2}" - - logger.info( - f"Correlation stack {int(self.spec_lo_idx)}:{int(self.spec_hi_idx)} with " - f"{int(self.spec_lo_m_idx)}:{int(self.spec_hi_m_idx)}" - ) - - logger.info(f" Virtual Stack; corrlation plot of {self.corrImg1} vs {self.corrImg2}") - else: - self.corrImg1 = ( - f" Sum of {os.path.basename(self.file_name).split('.')[0]}_{int(self.spec_lo_idx)} " - f"to {int(self.spec_hi_idx)}" - ) - self.corrImg2 = ( - f" Sum of {os.path.basename(self.file_name).split('.')[0]}_{int(self.spec_lo_m_idx)} " - f"to {int(self.spec_hi_m_idx)}" - ) - # logger.info(f" corrlation plot of {self.corrImg1} vs {self.corrImg2}") - - def correlation_plot(self): - self.stackIndexToNames() - - self.statusbar_main.showMessage(f"Correlation of {self.corrImg1} with {self.corrImg2}") - - if self.rb_roiRegionOnly.isChecked(): - self.roi_mask = self.image_roi.getArrayRegion( - self.displayedStack, self.image_view.imageItem, axes=(1, 2) - ) - self.roi_img1 = np.mean(self.roi_mask[int(self.spec_lo_idx) : int(self.spec_hi_idx)], axis=0) - self.roi_img2 = np.mean(self.roi_mask[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx)], axis=0) - self.scatter_window = ScatterPlot( - self.roi_img1, self.roi_img2, (str(self.corrImg1), str(self.corrImg2)) - ) - - else: - self.scatter_window = ScatterPlot(self.img1, self.img2, (str(self.corrImg1), str(self.corrImg2))) - - # ph = self.geometry().height() - # pw = self.geometry().width() - # px = self.geometry().x() - # py = self.geometry().y() - # dw = self.scatter_window.width() - # dh = self.scatter_window.height() - # self.scatter_window.setGeometry(px+0.65*pw, py + ph - 2*dh-5, dw, dh) - self.scatter_window.show() - - def plotCorrelationsAllCombinations(self): - self.stackIndexToNames() - allElemCombNum = list(combinations(np.arange(len(self.elemFileName)), 2)) - - self.scW1 = self.scW2 = self.scW3 = self.scW4 = self.scW5 = None - self.scW6 = self.scW7 = self.scW8 = self.scW9 = self.scW10 = None - - self.scWindowList = [ - self.scW1, - self.scW2, - self.scW3, - self.scW4, - self.scW5, - self.scW6, - self.scW7, - self.scW8, - self.scW9, - self.scW10, - ] - self.scWindowDict = { - 1: self.scW1, - 2: self.scW2, - 3: self.scW3, - 4: self.scW4, - 5: self.scW5, - 6: self.scW6, - 7: self.scW7, - 8: self.scW8, - 9: self.scW9, - 10: self.scW10, - } - - if len(allElemCombNum) > len(self.scWindowDict): - reply = QMessageBox.warning( - self, - "Plot Window Limit", - f"The number of combination exceeds " - f"maxiumum number of " - f"plot windows. First {len(self.scWindowDict)} " - f"combinations will be plotted. \n Proceed?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - - if reply == QMessageBox.Yes: - for i, pair in enumerate(allElemCombNum): - im1 = self.displayedStack[pair[0]] - im2 = self.displayedStack[pair[1]] - im1Name = self.elemFileName[pair[0]] - im2Name = self.elemFileName[pair[1]] - - self.scWindowDict[i] = ScatterPlot(im1, im2, (str(im1Name), str(im2Name))) - self.scWindowDict[i].show() - - if reply == QMessageBox.No: - return - - def getROIMask(self): - self.roi_mask = self.image_roi.getArrayRegion(self.displayedStack, self.image_view.imageItem, axes=(1, 2)) - self.newWindow = singleStackViewer(self.roi_mask) - self.newWindow.show() - - def save_stack(self, method="raw"): - # self.update_stack() - file_name = QFileDialog().getSaveFileName( - self, "Save image data", "image_data.tiff", "image file(*tiff *tif )" - ) - if file_name[0]: - if method == "raw": - tf.imsave(str(file_name[0]), self.displayedStack) - logger.info(f"Updated Image Saved: {str(file_name[0])}") - self.statusbar_main.showMessage(f"Updated Image Saved: {str(file_name[0])}") - elif method == "sum": - tf.imsave(str(file_name[0]), np.sum(self.displayedStack, axis=0)) - - elif method == "mean": - tf.imsave(str(file_name[0]), np.mean(self.displayedStack, axis=0)) - - else: - self.statusbar_main.showMessage("Saving cancelled") - logger.info(f"Save failed: {str(file_name[0])}") - pass - - def save_disp_img(self): - file_name = QFileDialog().getSaveFileName(self, "Save image data", "image.tiff", "image file(*tiff *tif )") - if file_name[0]: - tf.imsave(str(file_name[0]), self.disp_img) - self.statusbar_main.showMessage(f"Image Saved to {str(file_name[0])}") - logger.info(f"Updated Image Saved: {str(file_name[0])}") - - else: - logger.error("No file to save") - self.statusbar_main.showMessage("Saving cancelled") - pass - - def getLivePlotData(self): - try: - data = np.squeeze([c.getData() for c in self.spectrum_view.plotItem.curves]) - # print(np.shape(data)) - if data.ndim == 2: - self.mu_ = data[1] - self.e_ = data[0] - elif data.ndim == 3: - e_mu = data[0, :, :] - self.mu_ = e_mu[1] - self.e_ = e_mu[0] - - else: - logger.error(f" Data shape of {data.ndim} is not supported") - pass - except AttributeError: - logger.error("No data loaded") - pass - - def addSpectrumToCollector(self): - self.getLivePlotData() - self.spectrum_view_collect.plot(self.e_, self.mu_, name="ROI Spectrum") - self.spectrum_view_collect.setLabel("bottom", "Energy", self.e_unit) - self.spectrum_view_collect.setLabel("left", "Intensity", "A.U.") - - def findEo(self): - try: - self.getLivePlotData() - e0_init = self.e_[np.argmax(np.gradient(self.mu_))] - self.dsb_norm_Eo.setValue(e0_init) - - except AttributeError: - logger.error("No data loaded") - pass - - def initNormVals(self): - self.getLivePlotData() - e0_init = self.e_[np.argmax(np.gradient(self.mu_))] - pre1, pre2, post1, post2 = xanesNormalization( - self.e_, self.mu_, e0=e0_init, step=None, nnorm=1, nvict=0, guess=True - ) - self.dsb_norm_pre1.setValue(pre1) - self.dsb_norm_pre2.setValue(pre2) - self.dsb_norm_post1.setValue(post1) - self.dsb_norm_post2.setValue(post2) - self.dsb_norm_Eo.setValue(e0_init) - - def getNormParams(self): - self.getLivePlotData() - eo_ = self.dsb_norm_Eo.value() - pre1_, pre2_ = self.dsb_norm_pre1.value(), self.dsb_norm_pre2.value() - norm1_, norm2_ = self.dsb_norm_post1.value(), self.dsb_norm_post2.value() - norm_order = self.sb_norm_order.value() - - return eo_, pre1_, pre2_, norm1_, norm2_, norm_order - - def exportNormParams(self): - self.xanesNormParam = {} - eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() - self.xanesNormParam["E0"] = eo_ - self.xanesNormParam["pre1"] = pre1_ - self.xanesNormParam["pre2"] = pre2_ - self.xanesNormParam["post1"] = norm1_ - self.xanesNormParam["post2"] = norm2_ - self.xanesNormParam["norm_order"] = norm_order - - file_name = QtWidgets.QFileDialog().getSaveFileName( - self, "Save XANES Norm Params", "xanes_norm_params.csv", "csv file(*csv)" - ) - - if file_name[0]: - pd.DataFrame(self.xanesNormParam, index=[0]).to_csv(file_name[0]) - - else: - pass - - def importNormParams(self): - file_name = QtWidgets.QFileDialog().getOpenFileName( - self, "Open a XANES Norm File", "", "csv file(*csv);;all_files (*)" - ) - - if file_name[0]: - xanesNormParam = pd.read_csv(file_name[0]) - self.dsb_norm_Eo.setValue(xanesNormParam["E0"]) - self.dsb_norm_pre1.setValue(xanesNormParam["pre1"]) - self.dsb_norm_pre2.setValue(xanesNormParam["pre2"]) - self.dsb_norm_post1.setValue(xanesNormParam["post1"]) - self.dsb_norm_post2.setValue(xanesNormParam["post2"]) - self.sb_norm_order.setValue(xanesNormParam["norm_order"]) - - def nomalizeLiveSpec(self): - eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() - self.spectrum_view.clear() - - pre_line, post_line, normXANES = xanesNormalization( - self.e_, - self.mu_, - e0=eo_, - step=None, - nnorm=norm_order, - nvict=0, - pre1=pre1_, - pre2=pre2_, - norm1=norm1_, - norm2=norm2_, - ) - - names = np.array(("Spectrum", "Pre", "Post")) - data_array = np.array((self.mu_, pre_line, post_line)) - colors = np.array(("c", "r", "m")) - - for data, clr, name in zip(data_array, colors, names): - self.spectrum_view.plot(self.e_, data, pen=pg.mkPen(clr, width=self.plotWidth), name=name) - - self.spectrum_view_norm.plot( - self.e_, normXANES, clear=True, pen=pg.mkPen(self.plt_colors[-1], width=self.plotWidth) - ) - self.spectrum_view_norm.setLabel("bottom", "Energy", self.e_unit) - self.spectrum_view_norm.setLabel("left", "Norm. Intensity", "A.U.") - - def normalizeStack(self): - self.getLivePlotData() - eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() - - self.im_stack = self.displayedStack = xanesNormStack( - self.e_, - self.displayedStack, - e0=eo_, - step=None, - nnorm=norm_order, - nvict=0, - pre1=pre1_, - pre2=pre2_, - norm1=norm1_, - norm2=norm2_, - ) - # self.im_stack = self.displayedStack - - def transposeStack(self): - self.im_stack = self.displayedStack = np.transpose(self.displayedStack, (2, 1, 0)) - - def swapStackXY(self): - self.im_stack = self.displayedStack = np.transpose(self.displayedStack, (0, 2, 1)) - - def removeROIBGStack(self): - self.displayedStack = subtractBackground(self.displayedStack, self.mean_spectra) - - def resetCollectorSpec(self): - pass - - def saveCollectorPlot(self): - exporter = pg.exporters.CSVExporter(self.spectrum_view_collect.plotItem) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName(self, "save spectra", "", "spectra (*csv)") - if file_name[0]: - exporter.export(str(file_name[0]) + ".csv") - else: - self.statusbar_main.showMessage("Saving cancelled") - pass - - def save_disp_spec(self): - exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName(self, "save spectrum", "", "spectra (*csv)") - if file_name[0]: - exporter.export(str(file_name[0]) + ".csv") - else: - self.statusbar_main.showMessage("Saving cancelled") - pass - - def saveEnergyList(self): - file_name = QFileDialog().getSaveFileName(self, "save energy list", "energy_list.txt", "text file (*txt)") - if file_name[0]: - np.savetxt(file_name[0], self.xdata, fmt="%.4f") - else: - pass - - def pca_scree_(self): - logger.info("Process started..") - self.update_stack() - pca_scree(self.displayedStack) - logger.info("Process complete") - - def calc_comp_(self): - logger.info("Process started..") - - # self.update_stack() - n_components = self.sb_ncomp.value() - method_ = self.cb_comp_method.currentText() - - ims, comp_spec, decon_spec, decomp_map = decompose_stack( - self.displayedStack, decompose_method=method_, n_components_=n_components - ) - - self._new_window3 = ComponentViewer(ims, self.xdata, comp_spec, decon_spec, decomp_map) - self._new_window3.show() - - logger.info("Process complete") - - def kmeans_elbow(self): - logger.info("Process started..") - # self.update_stack() - - with pg.BusyCursor(): - try: - kmeans_variance(self.displayedStack) - logger.info("Process complete") - except OverflowError: - pass - logger.error("Overflow Error, values are too long") - - def kmeans_elbow_Thread(self): - # Pass the function to execute - worker = Worker(self.kmeans_elbow) # Any other args, kwargs are passed to the run function - worker.signals.result.connect(self.print_output) - worker.signals.finished.connect(self.thread_complete) - # Execute - self.threadpool.start(worker) - - def clustering_(self): - logger.info("Process started..") - # self.update_stack() - method_ = self.cb_clust_method.currentText() - - decon_images, X_cluster, decon_spectra = cluster_stack( - self.displayedStack, - method=method_, - n_clusters_=self.sb_ncluster.value(), - decomposed=False, - decompose_method=self.cb_comp_method.currentText(), - decompose_comp=self.sb_ncomp.value(), - ) - - self._new_window4 = ClusterViewer(decon_images, self.xdata, X_cluster, decon_spectra) - self._new_window4.show() - - logger.info("Process complete") - - def change_color_on_load(self, button_name): - button_name.setStyleSheet("background-color : rgb(0,150,0);" "color: rgb(255,255,255)") - - def energyFileChooser(self): - file_name = QFileDialog().getOpenFileName(self, "Open energy list", "", "text file (*.txt)") - self.efilePath = file_name[0] - - def fast_xanes_fitting(self): - self._new_window5 = XANESViewer(self.displayedStack, self.xdata, self.refs, self.ref_names) - self._new_window5.show() - - # Thread Signals - - def print_output(self, s): - print(s) - - def thread_complete(self): - print("THREAD COMPLETE!") - - def closeEvent(self, event): - reply = QMessageBox.question( - self, - "Window Close", - "Are you sure you want to close?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - - if reply == QMessageBox.Yes: - event.accept() - QApplication.closeAllWindows() - else: - event.ignore() - - -class WorkerSignals(QObject): - """ - Defines the signals available from a running worker thread. - Supported signals are: - - finished: No data - - error:`tuple` (exctype, value, traceback.format_exc() ) - - result: `object` data returned from processing, anything - - progress: `tuple` indicating progress metadata - """ - - start = pyqtSignal() - finished = pyqtSignal() - error = pyqtSignal(tuple) - result = pyqtSignal(object) - - -class Worker(QRunnable): - """ - Worker thread - Inherits from QRunnable to handler worker thread setup, signals and wrap-up. - """ - - def __init__(self, fn, *args, **kwargs): - super(Worker, self).__init__() - # Store constructor arguments (re-used for processing) - self.fn = fn - self.args = args - self.kwargs = kwargs - self.signals = WorkerSignals() - - @pyqtSlot() - def run(self): - """ - Initialise the runner function with passed args, kwargs. - """ - # Retrieve args/kwargs here; and fire processing using them - self.signals.start.emit() - try: - result = self.fn(*self.args, **self.kwargs) - except Exception: - traceback.print_exc() - exctype, value = sys.exc_info()[:2] - self.signals.error.emit((exctype, value, traceback.format_exc())) - else: - self.signals.result.emit(result) # Return the result of the processing - finally: - self.signals.finished.emit() # Done - - -class singleStackViewer(QtWidgets.QMainWindow): - def __init__(self, img_stack, gradient="viridis"): - super(singleStackViewer, self).__init__() - - # Load the UI Page - uic.loadUi(os.path.join(ui_path, "uis/singleStackView.ui"), self) - - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - - self.img_stack = img_stack - self.gradient = gradient - self.image_view.setPredefinedGradient(gradient) - - if self.img_stack.ndim == 3: - self.dim1, self.dim3, self.dim2 = img_stack.shape - elif self.img_stack.ndim == 2: - self.dim3, self.dim2 = img_stack.shape - self.dim1 = 1 - self.hs_img_stack.setMaximum(self.dim1 - 1) - self.hs_img_stack.setValue(np.round(self.dim1 / 2)) - self.displayStack() - - # connections - self.hs_img_stack.valueChanged.connect(self.displayStack) - self.actionSave.triggered.connect(self.saveImageStackAsTIFF) - - def displayStack(self): - im_index = self.hs_img_stack.value() - if self.img_stack.ndim == 2: - self.image_view.setImage(self.img_stack) - else: - self.image_view.setImage(self.img_stack[im_index]) - self.label_img_count.setText(f"{im_index + 1}/{self.dim1}") - - def saveImageStackAsTIFF(self): - file_name = QFileDialog().getSaveFileName(self, "", "", "*.tiff;;*.tif") - if file_name[0]: - if self.img_stack.ndim == 3: - tf.imsave(str(file_name[0]), np.float32(self.img_stack.transpose(0, 2, 1))) - elif self.img_stack.ndim == 2: - tf.imsave(str(file_name[0]), np.float32(self.img_stack.T)) - else: - pass - - -class ComponentViewer(QtWidgets.QMainWindow): - def __init__(self, comp_stack, energy, comp_spectra, decon_spectra, decomp_map): - super(ComponentViewer, self).__init__() - - # Load the UI Page - uic.loadUi(os.path.join(ui_path, "uis/ComponentView.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - - self.comp_stack = comp_stack - self.energy = energy - self.comp_spectra = comp_spectra - self.decon_spectra = decon_spectra - self.decomp_map = decomp_map - - (self.dim1, self.dim3, self.dim2) = self.comp_stack.shape - self.hs_comp_number.setMaximum(self.dim1 - 1) - - self.image_view.setImage(self.comp_stack) - self.image_view.setPredefinedGradient("viridis") - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - - self.image_view2.setImage(self.decomp_map) - self.image_view2.setPredefinedGradient("bipolar") - self.image_view2.ui.menuBtn.hide() - self.image_view2.ui.roiBtn.hide() - - # connection - self.update_image() - self.pb_show_all.clicked.connect(self.show_all_spec) - self.hs_comp_number.valueChanged.connect(self.update_image) - self.actionSave.triggered.connect(self.save_comp_data) - self.pb_openScatterPlot.clicked.connect(self.openScatterPlot) - self.pb_showMultiColor.clicked.connect(self.generateMultiColorView) - - def update_image(self): - im_index = self.hs_comp_number.value() - self.spectrum_view.setLabel("bottom", "Energy") - self.spectrum_view.setLabel("left", "Intensity", "A.U.") - self.spectrum_view.plot(self.energy, self.decon_spectra[:, im_index], clear=True) - self.component_view.setLabel("bottom", "Energy") - self.component_view.setLabel("left", "Weight", "A.U.") - self.component_view.plot(self.energy, self.comp_spectra[:, im_index], clear=True) - self.label_comp_number.setText(f"{im_index + 1}/{self.dim1}") - # self.image_view.setCurrentIndex(im_index-1) - self.image_view.setImage(self.comp_stack[im_index]) - - def openScatterPlot(self): - self.scatter_window = ComponentScatterPlot(self.comp_stack, self.comp_spectra) - - # ph = self.geometry().height() - # pw = self.geometry().width() - # px = self.geometry().x() - # py = self.geometry().y() - # dw = self.scatter_window.width() - # dh = self.scatter_window.height() - # self.scatter_window.setGeometry(px+0.65*pw, py + ph - 2*dh-5, dw, dh) - self.scatter_window.show() - - def show_all_spec(self): - self.spectrum_view.clear() - self.plt_colors = ["g", "b", "r", "c", "m", "y", "w"] * 10 - offsets = np.arange(0, 2, 0.2) - self.spectrum_view.addLegend() - for ii in range(self.decon_spectra.shape[1]): - self.spectrum_view.plot( - self.energy, - (self.decon_spectra[:, ii] / self.decon_spectra[:, ii].max()) + offsets[ii], - pen=self.plt_colors[ii], - name="component" + str(ii + 1), - ) - - def save_comp_data(self): - file_name = QFileDialog().getSaveFileName(self, "", "", "data(*tiff *tif *txt *png )") - if file_name[0]: - tf.imsave( - str(file_name[0]) + "_components.tiff", np.float32(self.comp_stack.transpose(0, 2, 1)), imagej=True - ) - tf.imsave(str(file_name[0]) + "_component_masks.tiff", np.float32(self.decomp_map.T), imagej=True) - np.savetxt(str(file_name[0]) + "_deconv_spec.txt", self.decon_spectra) - np.savetxt(str(file_name[0]) + "_component_spec.txt", self.comp_spectra) - else: - pass - - def generateMultiColorView(self): - self.multichanneldict = {} - - for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.comp_stack.transpose(0, 1, 2))): - low, high = np.min(image), np.max(image) - self.multichanneldict[f"Image {n + 1}"] = { - "ImageName": f"Image {n + 1}", - "ImageDir": ".", - "Image": image, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) - self.muli_color_window.show() - - # add energy column - - -class ClusterViewer(QtWidgets.QMainWindow): - def __init__(self, decon_images, energy, X_cluster, decon_spectra): - super(ClusterViewer, self).__init__() - - # Load the UI Page - uic.loadUi(os.path.join(ui_path, "uis/ClusterView.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - - self.decon_images = decon_images - self.energy = energy - self.X_cluster = X_cluster - self.decon_spectra = decon_spectra - (self.dim1, self.dim3, self.dim2) = self.decon_images.shape - self.hsb_cluster_number.setMaximum(self.dim1 - 1) - self.X_cluster = X_cluster - - self.image_view.setImage(self.decon_images, autoHistogramRange=True, autoLevels=True) - self.image_view.setPredefinedGradient("viridis") - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - - self.cluster_view.setImage(self.X_cluster, autoHistogramRange=True, autoLevels=True) - self.cluster_view.setPredefinedGradient("bipolar") - self.cluster_view.ui.histogram.hide() - self.cluster_view.ui.menuBtn.hide() - self.cluster_view.ui.roiBtn.hide() - - # connection - self.update_display() - self.hsb_cluster_number.valueChanged.connect(self.update_display) - self.actionSave.triggered.connect(self.save_clust_data) - self.pb_show_all_spec.clicked.connect(self.showAllSpec) - self.pb_showMultiColor.clicked.connect(self.generateMultiColorView) - - def update_display(self): - im_index = self.hsb_cluster_number.value() - self.component_view.setLabel("bottom", "Energy") - self.component_view.setLabel("left", "Intensity", "A.U.") - self.component_view.plot(self.energy, self.decon_spectra[:, im_index], clear=True) - # self.image_view.setCurrentIndex(im_index-1) - self.image_view.setImage(self.decon_images[im_index]) - self.label_comp_number.setText(f"{im_index + 1}/{self.dim1}") - - def save_clust_data(self): - file_name = QFileDialog().getSaveFileName(self, "", "", "data(*tiff *tif *txt *png )") - if file_name[0]: - tf.imsave( - str(file_name[0]) + "_cluster.tiff", np.float32(self.decon_images.transpose(0, 2, 1)), imagej=True - ) - tf.imsave(str(file_name[0]) + "_cluster_map.tiff", np.float32(self.X_cluster.T), imagej=True) - np.savetxt(str(file_name[0]) + "_deconv_spec.txt", self.decon_spectra) - - else: - logger.error("Saving Cancelled") - self.statusbar.showMessage("Saving Cancelled") - pass - - def showAllSpec(self): - self.component_view.clear() - self.plt_colors = ["g", "b", "r", "c", "m", "y", "w"] * 10 - offsets = np.arange(0, 2, 0.2) - self.component_view.addLegend() - for ii in range(self.decon_spectra.shape[1]): - self.component_view.plot( - self.energy, - (self.decon_spectra[:, ii] / self.decon_spectra[:, ii].max()) + offsets[ii], - pen=self.plt_colors[ii], - name="cluster" + str(ii + 1), - ) - - def generateMultiColorView(self): - self.multichanneldict = {} - - for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.decon_images.transpose(0, 1, 2))): - low, high = np.min(image), np.max(image) - self.multichanneldict[f"Image {n + 1}"] = { - "ImageName": f"Image {n + 1}", - "ImageDir": ".", - "Image": image, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) - self.muli_color_window.show() - - -class XANESViewer(QtWidgets.QMainWindow): - def __init__(self, im_stack=None, e_list=None, refs=None, ref_names=None): - super(XANESViewer, self).__init__() - - uic.loadUi(os.path.join(ui_path, "uis/XANESViewer.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - - self.im_stack = im_stack - self.e_list = e_list - self.refs = refs - self.ref_names = ref_names - self.selected = self.ref_names - self.fitResultDict = {} - self.fit_method = self.cb_xanes_fit_model.currentText() - self.alphaForLM = self.dsb_alphaForLM.value() - - self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( - self.im_stack, self.e_list, self.refs, method=self.fit_method, alphaForLM=self.alphaForLM - ) - - (self.dim1, self.dim2, self.dim3) = self.im_stack.shape - self.cn = int(self.dim2 // 2) - self.sz = np.max([int(self.dim2 * 0.15), int(self.dim3 * 0.15)]) - self.image_roi = pg.RectROI( - [int(self.dim3 // 2), int(self.dim2 // 2)], - [self.sz, self.sz], - pen="w", - maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), - ) - - self.image_roi.addTranslateHandle([0, 0], [2, 2]) - self.image_roi.addRotateHandle([0, 1], [2, 2]) - - # self.image_roi = pg.PolyLineROI([[0, 0], [0, self.sz], [self.sz, self.sz], [self.sz, 0]], - # pos=(int(self.dim2 // 2), int(self.dim3 // 2)), - # maxBounds=QtCore.QRect(0, 0, self.dim3, self.dim2), closed=True) - # self.image_roi.addTranslateHandle([self.sz // 2, self.sz // 2], [2, 2]) - - self.stack_center = int(self.dim1 // 2) - self.stack_width = int(self.dim1 * 0.05) - # self.image_view.setCurrentIndex(self.stack_center) - - self.image_view.addItem(self.image_roi) - self.xdata = self.e_list + self.sb_e_shift.value() - - self.scrollBar_setup() - self.display_image_data() - self.display_references() - self.update_spectrum() - - # connections - self.sb_e_shift.valueChanged.connect(self.update_spectrum) - self.pb_re_fit.clicked.connect(self.re_fit_xanes) - self.pb_edit_refs.clicked.connect(self.choose_refs) - self.image_roi.sigRegionChanged.connect(self.update_spectrum) - self.hsb_xanes_stk.valueChanged.connect(self.display_image_data) - self.hsb_chem_map.valueChanged.connect(self.display_image_data) - self.pb_showMultiColor.clicked.connect(self.generateMultiColorView) - self.pb_showCompSpec.clicked.connect(self.showComponentXANES) - - # menu - self.actionSave_Chem_Map.triggered.connect(self.save_chem_map) - self.actionSave_R_factor_Image.triggered.connect(self.save_rfactor_img) - self.actionSave_Live_Fit_Data.triggered.connect(self.pg_export_spec_fit) - self.actionExport_Fit_Stats.triggered.connect(self.exportFitResults) - self.actionExport_Ref_Plot.triggered.connect(self.pg_export_references) - - def scrollBar_setup(self): - self.hsb_xanes_stk.setValue(self.stack_center) - self.hsb_xanes_stk.setMaximum(self.dim1 - 1) - self.hsb_chem_map.setValue(0) - self.hsb_chem_map.setMaximum(self.decon_ims.shape[-1] - 1) - - def display_image_data(self): - self.image_view.setImage(self.im_stack[self.hsb_xanes_stk.value()]) - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - self.image_view.setPredefinedGradient("viridis") - - self.image_view_maps.setImage(self.decon_ims.transpose(2, 0, 1)[self.hsb_chem_map.value()]) - self.image_view_maps.setPredefinedGradient("bipolar") - self.image_view_maps.ui.menuBtn.hide() - self.image_view_maps.ui.roiBtn.hide() - - def display_references(self): - self.inter_ref = interploate_E(self.refs, self.xdata) - self.plt_colors = ["c", "m", "y", "w"] * 10 - self.spectrum_view_refs.addLegend() - for ii in range(self.inter_ref.shape[0]): - if len(self.selected) != 0: - self.spectrum_view_refs.plot( - self.xdata, - self.inter_ref[ii], - pen=pg.mkPen(self.plt_colors[ii], width=2), - name=self.selected[1:][ii], - ) - else: - self.spectrum_view_refs.plot( - self.xdata, - self.inter_ref[ii], - pen=pg.mkPen(self.plt_colors[ii], width=2), - name="ref" + str(ii + 1), - ) - - def choose_refs(self): - "Interactively exclude some standards from the reference file" - self.ref_edit_window = RefChooser( - self.ref_names, - self.im_stack, - self.e_list, - self.refs, - self.sb_e_shift.value(), - self.cb_xanes_fit_model.currentText(), - ) - self.ref_edit_window.show() - # self.rf_plot = pg.plot(title="RFactor Tracker") - - # connections - self.ref_edit_window.choosenRefsSignal.connect(self.update_refs) - self.ref_edit_window.fitResultsSignal.connect(self.plotFitResults) - - def update_refs(self, list_): - self.selected = list_ # list_ is the signal from ref chooser - self.update_spectrum() - self.re_fit_xanes() - - def update_spectrum(self): - self.roi_img = self.image_roi.getArrayRegion(self.im_stack, self.image_view.imageItem, axes=(1, 2)) - sizex, sizey = self.roi_img.shape[1], self.roi_img.shape[2] - posx, posy = self.image_roi.pos() - self.roi_info.setText(f"ROI_Pos: {int(posx)},{int(posy)} ROI_Size: {sizex},{sizey}") - - self.xdata1 = self.e_list + self.sb_e_shift.value() - self.ydata1 = get_sum_spectra(self.roi_img) - self.fit_method = self.cb_xanes_fit_model.currentText() - - if len(self.selected) != 0: - self.inter_ref = interploate_E(self.refs[self.selected], self.xdata1) - stats, coeffs = xanes_fitting_1D( - self.ydata1, - self.xdata1, - self.refs[self.selected], - method=self.fit_method, - alphaForLM=self.alphaForLM, - ) - - else: - self.inter_ref = interploate_E(self.refs, self.xdata1) - stats, coeffs = xanes_fitting_1D( - self.ydata1, self.xdata1, self.refs, method=self.fit_method, alphaForLM=self.alphaForLM - ) - - self.fit_ = np.dot(coeffs, self.inter_ref) - pen = pg.mkPen("g", width=1.5) - pen2 = pg.mkPen("r", width=1.5) - # pen3 = pg.mkPen("y", width=1.5) - self.spectrum_view.addLegend() - self.spectrum_view.setLabel("bottom", "Energy") - self.spectrum_view.setLabel("left", "Intensity", "A.U.") - self.spectrum_view.plot(self.xdata1, self.ydata1, pen=pen, name="Data", clear=True) - self.spectrum_view.plot(self.xdata1, self.fit_, name="Fit", pen=pen2) - - for n, (coff, ref, plt_clr) in enumerate(zip(coeffs, self.inter_ref, self.plt_colors)): - if len(self.selected) != 0: - self.spectrum_view.plot(self.xdata1, np.dot(coff, ref), name=self.selected[1:][n], pen=plt_clr) - else: - self.spectrum_view.plot(self.xdata1, np.dot(coff, ref), name="ref" + str(n + 1), pen=plt_clr) - # set the rfactor value to the line edit slot - self.results = ( - f"Coefficients: {coeffs} \n" - f"R-Factor: {stats['R_Factor']}, R-Square: {stats['R_Square']},\n " - f"Chi-Square: {stats['Chi_Square']}, " - f"Reduced Chi-Square: {stats['Reduced Chi_Square']}" - ) - - self.fit_results.setText(self.results) - - def re_fit_xanes(self): - if len(self.selected) != 0: - self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( - self.im_stack, - self.e_list + self.sb_e_shift.value(), - self.refs[self.selected], - method=self.cb_xanes_fit_model.currentText(), - alphaForLM=self.alphaForLM, - ) - else: - # if non athena file with no header is loaded no ref file cannot be edited - self.decon_ims, self.rfactor, self.coeffs_arr = xanes_fitting( - self.im_stack, - self.e_list + self.sb_e_shift.value(), - self.refs, - method=self.cb_xanes_fit_model.currentText(), - alphaForLM=self.alphaForLM, - ) - - # rfactor is a list of all spectra so take the mean - self.rfactor_mean = np.mean(self.rfactor) - self.image_view_maps.setImage(self.decon_ims.transpose(2, 0, 1)) - self.scrollBar_setup() - - def plotFitResults(self, decon_ims, rfactor_mean, coeff_array): - # upadte the chem maps and scrollbar params - self.image_view_maps.setImage(decon_ims.transpose(2, 0, 1)) - # self.hsb_chem_map.setValue(0) - # self.hsb_chem_map.setMaximum(decon_ims.shape[-1]-1) - - # set the rfactor value to the line edit slot - self.le_r_sq.setText(f"{rfactor_mean :.4f}") - - def showComponentXANES(self): - compNum = self.hsb_chem_map.value() - currentComp = self.decon_ims.transpose(2, 0, 1)[compNum] - currentCompMask = currentComp > 0 - yData = applyMaskGetMeanSpectrum(self.im_stack, currentCompMask) - xanes_comp_plot = pg.plot( - self.e_list + self.sb_e_shift.value(), - yData, - title=f"Component_{compNum}", - pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), - symbol="o", - ) - xanes_comp_plot.setLabel("bottom", "Energy (keV)") - xanes_comp_plot.setLabel("left", "Intensity") - - def generateMultiColorView(self): - self.multichanneldict = {} - - for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.decon_ims.transpose((2, 0, 1)))): - low, high = np.min(image), np.max(image) - self.multichanneldict[f"Image {n + 1}"] = { - "ImageName": f"Image {n + 1}", - "ImageDir": ".", - "Image": image, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) - self.muli_color_window.show() - - def save_chem_map(self): - file_name = QFileDialog().getSaveFileName(self, "save image", "chemical_map.tiff", "image data (*tiff)") - if file_name[0]: - tf.imsave(str(file_name[0]), np.float32(self.decon_ims.transpose(2, 0, 1)), imagej=True) - else: - logger.error("No file to save") - pass - - def save_rfactor_img(self): - file_name = QFileDialog().getSaveFileName(self, "save image", "r-factor_map.tiff", "image data (*tiff)") - if file_name[0]: - tf.imsave(str(file_name[0]), np.float32(self.rfactor), imagej=True) - else: - logger.error("No file to save") - pass - - def save_spec_fit(self): - try: - to_save = np.column_stack([self.xdata1, self.ydata1, self.fit_]) - file_name = QFileDialog().getSaveFileName(self, "save spectrum", "", "spectrum and fit (*txt)") - if file_name[0]: - np.savetxt(str(file_name[0]) + ".txt", to_save) - else: - pass - except Exception: - logger.error("No file to save") - pass - - def pg_export_spec_fit(self): - exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName(self, "save spectrum", "", "spectrum and fit (*csv)") - if file_name[0]: - exporter.export(str(file_name[0]) + ".csv") - else: - pass - - def pg_export_references(self): - exporter = pg.exporters.CSVExporter(self.spectrum_view_refs.plotItem) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName( - self, "save references", "xanes_references.csv", "column data (*csv)" - ) - if file_name[0]: - exporter.export(str(file_name[0])) - else: - pass - - def exportFitResults(self): - file_name = QFileDialog().getSaveFileName(self, "save txt", "xanes_1D_fit_results.txt", "txt data (*txt)") - if file_name[0]: - with open(file_name[0], "w") as file: - file.write(self.results) - else: - pass - - -class RefChooser(QtWidgets.QMainWindow): - choosenRefsSignal: pyqtSignal = QtCore.pyqtSignal(list) - fitResultsSignal: pyqtSignal = QtCore.pyqtSignal(np.ndarray, float, np.ndarray) - - def __init__(self, ref_names, im_stack, e_list, refs, e_shift, fit_model): - super(RefChooser, self).__init__() - uic.loadUi(os.path.join(ui_path, "uis/RefChooser.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - self.ref_names = ref_names - self.refs = refs - self.im_stack = im_stack - self.e_list = e_list - self.e_shift = e_shift - self.fit_model = fit_model - - self.all_boxes = [] - self.rFactorList = [] - - self.displayCombinations() - - # selection become more apparent than default with red-ish color - self.tableWidget.setStyleSheet("background-color: white; selection-background-color: rgb(200,0,0);") - - # add a line to the plot to walk through the table. Note that the table is not sorted - self.selectionLine = pg.InfiniteLine( - pos=1, angle=90, pen=pg.mkPen("m", width=2.5), movable=True, bounds=None, label="Move Me!" - ) - self.stat_view.setLabel("bottom", "Fit ID") - self.stat_view.setLabel("left", "Reduced Chi^2") - - for n, i in enumerate(self.ref_names): - self.cb_i = QtWidgets.QCheckBox(self.ref_box_frame) - if n == 0: - self.cb_i.setChecked(True) - self.cb_i.setEnabled(False) - self.cb_i.setObjectName(i) - self.cb_i.setText(i) - self.gridLayout_2.addWidget(self.cb_i, n, 0, 1, 1) - self.cb_i.toggled.connect(self.enableApply) - self.all_boxes.append(self.cb_i) - - # connections - self.pb_apply.clicked.connect(self.clickedWhichAre) - self.pb_combo.clicked.connect(self.tryAllCombo) - self.actionExport_Results_csv.triggered.connect(self.exportFitResults) - self.selectionLine.sigPositionChanged.connect(self.updateFitWithLine) - self.tableWidget.itemSelectionChanged.connect(self.updateWithTableSelection) - # self.stat_view.scene().sigMouseClicked.connect(self.moveSelectionLine) - self.stat_view.mouseDoubleClickEvent = self.moveSelectionLine - self.sb_max_combo.valueChanged.connect(self.displayCombinations) - # self.pb_sort_with_r.clicked.connect(lambda: self.tableWidget.sortItems(3, QtCore.Qt.AscendingOrder)) - self.pb_sort_with_r.clicked.connect(self.sortTable) - self.cb_sorter.currentTextChanged.connect(self.sortTable) - - # def clickedWhich(self): - # button_name = self.sender() - - def populateChecked(self): - self.onlyCheckedBoxes = [] - for names in self.all_boxes: - if names.isChecked(): - self.onlyCheckedBoxes.append(names.objectName()) - - QtCore.pyqtSlot() - - def clickedWhichAre(self): - self.populateChecked() - self.choosenRefsSignal.emit(self.onlyCheckedBoxes) - - def generateRefList(self, ref_list, maxCombo, minCombo=1): - """ - Creates a list of reference combinations for xanes fitting - - Paramaters; - - ref_list (list): list of ref names from the header - maxCombo (int): maximum number of ref lists in combination - minCombo (int): min number of ref lists in combination - - returns; - - 1. int: length of total number of combinations - 2. list: all the combinations - - """ - - if not maxCombo > len(ref_list): - iter_list = [] - while minCombo < maxCombo + 1: - iter_list += list(combinations(ref_list, minCombo)) - minCombo += 1 - return len(iter_list), iter_list - - else: - raise ValueError(" Maximum numbinations cannot be larger than number of list items") - - def displayCombinations(self): - niter, self.iter_list = self.generateRefList(self.ref_names[1:], self.sb_max_combo.value()) - self.label_nComb.setText(str(niter) + " Combinations") - - @QtCore.pyqtSlot() - def tryAllCombo(self): - # empty list to to keep track and plot of reduced chi2 of all the fits - self.rfactor_list = [] - - # create dataframe for the table - self.df = pd.DataFrame( - columns=["Fit Number", "References", "Coefficients", "R-Factor", "R^2", "chi^2", "red-chi^2", "Score"] - ) - - # df columns is the header for the table widget - self.tableWidget.setHorizontalHeaderLabels(self.df.columns) - # self.iter_list = list(combinations(self.ref_names[1:],self.sb_max_combo.value())) - - niter, self.iter_list = self.generateRefList(self.ref_names[1:], self.sb_max_combo.value()) - tot_combo = len(self.iter_list) - for n, refs in enumerate(self.iter_list): - self.statusbar.showMessage(f"{n + 1}/{tot_combo}") - selectedRefs = list((str(self.ref_names[0]),) + refs) - self.fit_combo_progress.setValue((n + 1) * 100 / tot_combo) - self.stat, self.coeffs_arr = xanes_fitting_Binned( - self.im_stack, self.e_list + self.e_shift, self.refs[selectedRefs], method=self.fit_model - ) - - self.rfactor_list.append(self.stat["Reduced Chi_Square"]) - self.stat_view.plot( - x=np.arange(n + 1), - y=self.rfactor_list, - clear=True, - title="Reduced Chi^2", - pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), - symbol="o", - ) - - # arbitary number to rank the best fit - fit_score = (self.stat["R_Square"] + np.sum(self.coeffs_arr)) / ( - self.stat["R_Factor"] + self.stat["Reduced Chi_Square"] - ) - - resultsDict = { - "Fit Number": n, - "References": str(selectedRefs[1:]), - "Coefficients": str(np.around(self.coeffs_arr, 4)), - "Sum of Coefficients": str(np.around(np.sum(self.coeffs_arr), 4)), - "R-Factor": self.stat["R_Factor"], - "R^2": self.stat["R_Square"], - "chi^2": self.stat["Chi_Square"], - "red-chi^2": self.stat["Reduced Chi_Square"], - "Score": np.around(fit_score, 4), - } - - self.df = pd.concat([self.df, pd.DataFrame([resultsDict])], ignore_index=True) - - self.dataFrametoQTable(self.df) - QtTest.QTest.qWait(0.1) # hepls with real time plotting - - self.stat_view.addItem(self.selectionLine) - - def dataFrametoQTable(self, df_: pd.DataFrame): - nRows = len(df_.index) - nColumns = len(df_.columns) - self.tableWidget.setRowCount(nRows) - self.tableWidget.setColumnCount(nColumns) - self.tableWidget.setHorizontalHeaderLabels(df_.columns) - - for i in range(nRows): - for j in range(nColumns): - cell = QtWidgets.QTableWidgetItem(str(df_.values[i][j])) - self.tableWidget.setItem(i, j, cell) - - # set the property of the table view. Size policy to make the contents justified - self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.tableWidget.resizeColumnsToContents() - - def exportFitResults(self): - file_name = QFileDialog().getSaveFileName(self, "save csv", "xanes_fit_results_log.csv", "txt data (*csv)") - if file_name[0]: - with open(str(file_name[0]), "w") as fp: - self.df.to_csv(fp) - else: - pass - - def selectTableAndCheckBox(self, x): - nSelection = int(round(x)) - self.tableWidget.selectRow(nSelection) - fit_num = int(self.tableWidget.item(nSelection, 0).text()) - refs_selected = self.iter_list[fit_num] - - # reset all the checkboxes to uncheck state, except the energy - for checkstate in self.findChildren(QtWidgets.QCheckBox): - if checkstate.isEnabled(): - checkstate.setChecked(False) - - for cb_names in refs_selected: - checkbox = self.findChild(QtWidgets.QCheckBox, name=cb_names) - checkbox.setChecked(True) - - def updateFitWithLine(self): - pos_x, pos_y = self.selectionLine.pos() - x = self.df.index[self.df[str("Fit Number")] == np.round(pos_x)][0] - self.selectTableAndCheckBox(x) - - def updateWithTableSelection(self): - x = self.tableWidget.currentRow() - self.selectTableAndCheckBox(x) - - def moveSelectionLine(self, event): - if event.button() == QtCore.Qt.LeftButton: - Pos = self.stat_view.plotItem.vb.mapSceneToView(event.pos()) - self.selectionLine.setPos(Pos.x()) - - def sortTable(self): - sorter_dict = { - "R-Factor": "R-Factor", - "R-Square": "R^2", - "Chi-Square": "chi^2", - "Reduced Chi-Square": "red-chi^2", - "Fit Number": "Fit Number", - } - sorter = sorter_dict[self.cb_sorter.currentText()] - self.df = self.df.sort_values(sorter, ignore_index=True) - self.dataFrametoQTable(self.df) - - def enableApply(self): - """ """ - self.populateChecked() - if len(self.onlyCheckedBoxes) > 1: - self.pb_apply.setEnabled(True) - else: - self.pb_apply.setEnabled(False) - - -class ScatterPlot(QtWidgets.QMainWindow): - def __init__(self, img1, img2, nameTuple): - super(ScatterPlot, self).__init__() - - uic.loadUi(os.path.join(ui_path, "uis/ScatterView.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - self.clearPgPlot() - self.w1 = self.scatterViewer.addPlot() - self.img1 = img1 - self.img2 = img2 - self.nameTuple = nameTuple - x, y = np.shape(self.img1) - self.s1 = pg.ScatterPlotItem(size=2, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 0, 255)) - # print(self.s1) - - # create three polyline ROIs for masking - Xsize = self.img1.max() / 6 - Ysize = self.img2.max() / 6 - - self.scatter_mask = pg.PolyLineROI( - [[0, 0], [0, Ysize], [Xsize / 2, Ysize * 1.5], [Xsize, Ysize], [Xsize, 0]], - pos=None, - pen=pg.mkPen("r", width=2), - hoverPen=pg.mkPen("w", width=2), - closed=True, - removable=True, - ) - - self.scatter_mask2 = pg.PolyLineROI( - [ - [Xsize * 1.2, 0], - [Xsize * 1.2, Ysize * 2], - [Xsize * 2, Ysize * 2], - [Xsize * 3, Ysize], - [Xsize * 2, 0], - ], - pos=None, - pen=pg.mkPen("g", width=2), - hoverPen=pg.mkPen("w", width=2), - closed=True, - removable=True, - ) - self.scatter_mask3 = pg.PolyLineROI( - [ - [Xsize * 2.5, 0], - [Xsize * 2.5, Ysize], - [Xsize * 4, Ysize], - [Xsize * 4, 0], - [Xsize * 3.7, Ysize * -0.5], - ], - pos=None, - pen=pg.mkPen("c", width=2), - hoverPen=pg.mkPen("w", width=2), - closed=True, - removable=True, - ) - - self.fitScatter = self.fitScatter2 = self.fitScatter3 = None - - self.rois = { - "ROI 1": (self.scatter_mask, self.rb_roi1.isChecked(), self.fitScatter), - "ROI 2": (self.scatter_mask2, self.rb_roi2.isChecked(), self.fitScatter2), - "ROI 3": (self.scatter_mask3, self.rb_roi3.isChecked(), self.fitScatter3), - } - - self.windowNames = {"ROI 1": self.fitScatter, "ROI 2": self.fitScatter2, "ROI 3": self.fitScatter3} - - self.s1.setData(self.img1.flatten(), self.img2.flatten()) - self.w1.setLabel("bottom", self.nameTuple[0], "counts") - self.label_img1.setText(self.nameTuple[0]) - self.w1.setLabel("left", self.nameTuple[1], "counts") - self.label_img2.setText(self.nameTuple[1]) - self.w1.addItem(self.s1) - - self.image_view.setImage(self.img1) - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - self.image_view.setPredefinedGradient("thermal") - - self.image_view2.setImage(self.img2) - self.image_view2.ui.menuBtn.hide() - self.image_view2.ui.roiBtn.hide() - self.image_view2.setPredefinedGradient("thermal") - - # connections - self.actionSave_Plot.triggered.connect(self.pg_export_correlation) - self.actionSave_Images.triggered.connect(self.tiff_export_images) - # self.pb_define_mask.clicked.connect(lambda:self.createMask(self.scatter_mask)) - self.pb_define_mask.clicked.connect(self.addMultipleROIs) - # self.pb_apply_mask.clicked.connect(lambda:self.getMaskRegion(self.scatter_mask)) - self.pb_apply_mask.clicked.connect(self.applyMultipleROIs) - self.pb_clear_mask.clicked.connect(self.clearMultipleROIs) - self.pb_compositeScatter.clicked.connect(self.createCompositeScatter) - [rbs.clicked.connect(self.updateROIDict) for rbs in [self.rb_roi1, self.rb_roi2, self.rb_roi3]] - - def pg_export_correlation(self): - exporter = pg.exporters.CSVExporter(self.w1) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName(self, "save correlation", "", "spectrum and fit (*csv)") - if file_name[0]: - exporter.export(str(file_name[0]) + ".csv") - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - pass - - def tiff_export_images(self): - file_name = QFileDialog().getSaveFileName(self, "save images", "", "spectrum and fit (*tiff)") - if file_name[0]: - tf.imsave(str(file_name[0]) + ".tiff", np.dstack([self.img1, self.img2]).T) - self.statusbar.showMessage(f"Images saved to {str(file_name[0])}") - else: - pass - - def createMask(self, ROIName): - try: - self.w1.removeItem(ROIName) - except Exception: - pass - self.w1.addItem(ROIName) - - def clearMask(self, ROIName): - self.w1.removeItem(ROIName) - - def clearPgPlot(self): - try: - self.masked_img.close() - except Exception: - pass - - def getMaskRegion(self, ROIName, generateSeperateWindows=True): - """filter scatterplot points using polylineROI region""" - - # Ref : https://stackoverflow.com/questions/57719303/how-to-map-mouse-position-on-a-scatterplot - - # get the roi region:QPaintPathObject - roiShape = self.rois[ROIName][0].mapToItem(self.s1, self.rois[ROIName][0].shape()) - - # get data in the scatter plot - scatterData = np.array(self.s1.getData()) - - # generate a binary mask for points inside or outside the roishape - selected = [roiShape.contains(QtCore.QPointF(pt[0], pt[1])) for pt in scatterData.T] - - # reshape the mask to image dimensions - self.mask2D = np.reshape(selected, (self.img1.shape)) - - # get masked image1 - self.maskedImage = self.mask2D * self.img1 - - # get rid of the (0,0) values in the masked array - self.xData, self.yData = np.compress(selected, scatterData[0]), np.compress(selected, scatterData[1]) - - # linear regeression of the filtered X,Y data - result = linregress(self.xData, self.yData) - - # Pearson's correlation of the filtered X,Y data - pr, pp = stats.pearsonr(self.xData, self.yData) - - # apply the solved equation to xData to generate the fit line - self.yyData = result.intercept + result.slope * self.xData - - # Prepare strings for fit results and stats - self.fitLineEqn = ( - f" y = x*{result.slope :.3e} + {result.intercept :.3e}, " - "R^2 = {result.rvalue**2 :.3f}, r = {pr :.3f}\n" - ) - FitStats1 = f" Slope Error = {result.stderr :.3e}, Intercept Error = {result.intercept_stderr :.3e}\n" - FitStats2 = f" Pearson’s correlation coefficient = {pr :.3f}" - refs = "\n\n ***References****\n\n scipy.stats.linregress, scipy.stats.pearsonr " - fitStats = ( - f"\n ***{ROIName} Fit Results***\n\n" + " Equation: " + self.fitLineEqn + FitStats1 + FitStats2 + refs - ) - - # generate new window to plot the results - - if generateSeperateWindows: - self.windowNames[ROIName] = MaskedScatterPlotFit( - [self.xData, self.yData], - [self.xData, self.yyData], - self.mask2D, - self.maskedImage, - fitStats, - self.fitLineEqn, - self.nameTuple, - ) - self.windowNames[ROIName].show() - - """ - from scipy.linalg import lstsq - M = xData[:, np.newaxis]**[0, 1] #use >1 for polynomial fits - p, res, rnk, s = lstsq(M, yData) - yyData = p[0] + p[1]*xData - """ - - def updateROIDict(self): - self.rois = { - "ROI 1": (self.scatter_mask, self.rb_roi1.isChecked()), - "ROI 2": (self.scatter_mask2, self.rb_roi2.isChecked()), - "ROI 3": (self.scatter_mask3, self.rb_roi3.isChecked()), - } - - def applyMultipleROIs(self): - with pg.BusyCursor(): - self.updateROIDict() - for key in self.rois.keys(): - if self.rois[key][1]: - self.getMaskRegion(key) - else: - pass - - def addMultipleROIs(self): - self.updateROIDict() - for key in self.rois.keys(): - if self.rois[key][1]: - self.createMask(self.rois[key][0]) - else: - self.clearMask(self.rois[key][0]) - - def clearMultipleROIs(self): - self.updateROIDict() - for key in self.rois.keys(): - if not self.rois[key][1]: - self.clearMask(self.rois[key][0]) - else: - pass - - def createCompositeScatter(self): - points = [] - fitLine = [] - masks = [] - roiFitEqn = {} - - self.updateROIDict() - for n, key in enumerate(self.rois.keys()): - if self.rois[key][1]: - self.getMaskRegion(key, generateSeperateWindows=False) - points.append(np.column_stack([self.xData, self.yData])) - fitLine.append(np.column_stack([self.xData, self.yyData])) - masks.append(self.mask2D) - roiFitEqn[key] = self.fitLineEqn - else: - pass - - logger.info(f" fitline shape: {np.shape(fitLine)}") - logger.info(f" points shape: {np.shape(points)}") - logger.info(f" maks shape: {np.shape(masks)}") - self.compositeScatterWindow = CompositeScatterPlot( - np.array(points), np.array(fitLine), np.array(masks), roiFitEqn, self.nameTuple - ) - self.compositeScatterWindow.show() - - def _createCompositeScatter(self): - self.scatterColors = ["w", "c", "y", "k", "m"] - points = [] - fitLine = [] - - self.updateROIDict() - for n, key in enumerate(self.rois.keys()): - if self.rois[key][1]: - self.getMaskRegion(key, generateSeperateWindows=False) - - for x, y, yy in zip(self.xData, self.yData, self.yyData): - points.append( - { - "pos": (x, y), - "data": "id", - "size": 3, - "pen": pg.mkPen(None), - "brush": self.scatterColors[n], - } - ) - fitLine.extend(np.column_stack((self.xData, self.yyData))) - else: - pass - - logger.info(f" fitline shape: {np.shape(fitLine)}") - self.compositeScatterWindow = CompositeScatterPlot(points, np.array(fitLine)) - self.compositeScatterWindow.show() - - def getROIParams(self): - print(np.array(self.scatter_mask.getSceneHandlePositions())) - - -class MaskedScatterPlotFit(QtWidgets.QMainWindow): - def __init__(self, scatterData, fitData, mask, maskedImage, fitString, fitEquation, nameTuple): - super(MaskedScatterPlotFit, self).__init__() - - uic.loadUi(os.path.join(ui_path, "uis/maskedScatterPlotFit.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - self.scatterData = scatterData - self.fitData = fitData - self.mask = mask - self.maskedImage = maskedImage - self.fitString = fitString - self.fitEquation = fitEquation - self.nameTuple = nameTuple - - # set the graphicslayoutwidget in the ui as canvas - self.canvas = self.scatterViewer.addPlot() - self.canvas.addLegend() - self.canvas.setLabel("bottom", self.nameTuple[0], "counts") - self.canvas.setLabel("left", self.nameTuple[1], "counts") - self.gb_maskedImage1.setTitle(f" Masked {self.nameTuple[0]}") - - # generate a scatter plot item - self.scattered = pg.ScatterPlotItem(size=3.5, pen=pg.mkPen(None), brush=pg.mkBrush(5, 214, 255, 200)) - - # set scatter plot data - self.scattered.setData(scatterData[0], scatterData[1], name="Data") - - # set z value negative to show scatter data behind the fit line - self.scattered.setZValue(-10) - - # add scatter plot to the canvas - self.canvas.addItem(self.scattered) - - # generate plotitem for fit line - self.fitLinePlot = pg.PlotDataItem(pen=pg.mkPen(pg.mkColor(220, 20, 60), width=3.3)) - - # set line plot data - self.fitLinePlot.setData(fitData[0], fitData[1], name="Linear Fit") - - # add line plot to the canvas - self.canvas.addItem(self.fitLinePlot) - - # display Mask - self.imageView_mask.setImage(self.mask) - self.imageView_mask.ui.menuBtn.hide() - self.imageView_mask.ui.roiBtn.hide() - self.imageView_mask.setPredefinedGradient("plasma") - - # display masked Image - self.imageView_maskedImage.setImage(self.maskedImage) - self.imageView_maskedImage.ui.menuBtn.hide() - self.imageView_maskedImage.ui.roiBtn.hide() - self.imageView_maskedImage.setPredefinedGradient("viridis") - - # display Fit stats - self.text_fit_results.setPlainText(fitString) - self.canvas.setTitle(self.fitEquation, color="r") - - # connections - self.pb_copy_results.clicked.connect(self.copyFitResults) - self.pb_save_results.clicked.connect(self.saveFitResults) - self.actionSave_Plot.triggered.connect(self.pg_export_correlation) - self.actionSaveMask.triggered.connect(self.saveMask) - self.actionSaveMaskedImage.triggered.connect(self.saveImage) - - def saveFitResults(self): - S__File = QFileDialog.getSaveFileName(self, "save txt", "correlationPlotFit.txt", "txt data (*txt)") - - Text = self.text_fit_results.toPlainText() - if S__File[0]: - with open(S__File[0], "w") as file: - file.write(Text) - - def copyFitResults(self): - self.text_fit_results.selectAll() - self.text_fit_results.copy() - self.statusbar.showMessage("text copied to clipboard") - - def pg_export_correlation(self): - exporter = pg.exporters.CSVExporter(self.canvas) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName( - self, "save correlation", "scatterData.csv", "spectrum and fit (*csv)" - ) - if file_name[0]: - exporter.export(str(file_name[0])) - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - pass - - def saveImage(self): - file_name = QFileDialog().getSaveFileName(self, "Save image data", "image.tiff", "image file(*tiff *tif )") - if file_name[0]: - tf.imsave(str(file_name[0]), self.maskedImage) - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - self.statusbar.showMessage("Saving cancelled") - pass - - def saveMask(self): - file_name = QFileDialog().getSaveFileName(self, "Save image data", "mask.tiff", "image file(*tiff *tif )") - if file_name[0]: - tf.imsave(str(file_name[0]), self.mask) - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - self.statusbar.showMessage("Saving cancelled") - pass - - -class ComponentScatterPlot(QtWidgets.QMainWindow): - def __init__(self, decomp_stack, specs): - super(ComponentScatterPlot, self).__init__() - - uic.loadUi(os.path.join(ui_path, "uis/ComponentScatterPlot.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - self.w1 = self.scatterViewer.addPlot() - self.decomp_stack = decomp_stack - self.specs = specs - (self.dim1, self.dim3, self.dim2) = self.decomp_stack.shape - # fill the combonbox depending in the number of components for scatter plot - for n, combs in enumerate(combinations(np.arange(self.dim1), 2)): - self.cb_scatter_comp.addItem(str(combs)) - self.cb_scatter_comp.setItemData(n, combs) - - self.s1 = pg.ScatterPlotItem(size=3, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 0, 120)) - - self.setImageAndScatterPlot() - # connections - self.actionSave_Plot.triggered.connect(self.pg_export_correlation) - self.actionSave_Images.triggered.connect(self.tiff_export_images) - self.pb_updateComponents.clicked.connect(self.setImageAndScatterPlot) - self.pb_define_mask.clicked.connect(self.createMask) - self.pb_apply_mask.clicked.connect(self.getMaskRegion) - self.pb_reset_mask.clicked.connect(self.resetMask) - self.pb_addALine.clicked.connect(lambda: self.createMask(Line=True)) - - def setImageAndScatterPlot(self): - try: - self.s1.clear() - except Exception: - pass - - comp_tuple = self.cb_scatter_comp.currentData() - self.img1, self.img2 = self.decomp_stack[comp_tuple[0]], self.decomp_stack[comp_tuple[-1]] - self.image_view.setImage(self.decomp_stack[comp_tuple[0]]) - self.image_view.ui.menuBtn.hide() - self.image_view.ui.roiBtn.hide() - self.image_view.setPredefinedGradient("bipolar") - - self.image_view2.setImage(self.decomp_stack[comp_tuple[-1]]) - self.image_view2.ui.menuBtn.hide() - self.image_view2.ui.roiBtn.hide() - self.image_view2.setPredefinedGradient("bipolar") - - points = [] - for i, j in zip(self.img1.flatten(), self.img2.flatten()): - points.append( - { - "pos": (i, j), - "data": "id", - "size": 5, - "pen": pg.mkPen(None), - "brush": pg.mkBrush(255, 255, 0, 160), - } - ) - - self.s1.addPoints(points) - self.w1.addItem(self.s1) - # self.s1.setData(self.specs[:, comp_tuple[0]], self.specs[:, comp_tuple[-1]]) - self.w1.setLabel("bottom", f"PC{comp_tuple[0]+1}") - self.w1.setLabel("left", f"PC{comp_tuple[-1]+1}") - self.label_im1.setText(f"PC{comp_tuple[0]+1}") - self.label_im2.setText(f"PC{comp_tuple[-1]+1}") - - def createMask(self, Line=False): - self.size = self.img1.max() / 10 - self.pos = int(self.img1.mean()) - - if Line: - self.lineROI = pg.LineSegmentROI( - [0, 1], - pos=(self.pos, self.pos), - pen=pg.mkPen("r", width=4), - hoverPen=pg.mkPen("g", width=4), - removable=True, - ) - self.w1.addItem(self.lineROI) - - else: - self.scatter_mask = pg.PolyLineROI( - [[0, 0], [0, self.size], [self.size, self.size], [self.size, 0]], - pos=(self.pos, self.pos), - pen=pg.mkPen("r", width=4), - hoverPen=pg.mkPen("g", width=4), - closed=True, - removable=True, - ) - - self.w1.addItem(self.scatter_mask) - - def resetMask(self): - self.clearMask() - self.createMask() - - def clearMask(self): - try: - self.w1.removeItem(self.scatter_mask) - except AttributeError: - pass - - def clearPgPlot(self): - try: - self.masked_img.close() - except Exception: - pass - - def getMaskRegion(self): - # Ref : https://stackoverflow.com/questions/57719303/how-to-map-mouse-position-on-a-scatterplot - - roiShape = self.scatter_mask.mapToItem(self.s1, self.scatter_mask.shape()) - self._points = list() - logger.info("Building Scatter Plot Window; Please wait..") - for i in range(len(self.img1.flatten())): - self._points.append(QtCore.QPointF(self.img1.flatten()[i], self.img2.flatten()[i])) - - selected = [roiShape.contains(pt) for pt in self._points] - img_selected = np.reshape(selected, (self.img1.shape)) - - self.masked_img = singleStackViewer(img_selected * self.img1, gradient="bipolar") - self.masked_img.show() - - def pg_export_correlation(self): - exporter = pg.exporters.CSVExporter(self.w1) - exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" - file_name = QFileDialog().getSaveFileName(self, "save correlation", "", "spectrum and fit (*csv)") - if file_name[0]: - exporter.export(str(file_name[0]) + ".csv") - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - pass - - def tiff_export_images(self): - file_name = QFileDialog().getSaveFileName(self, "save images", "", "spectrum and fit (*tiff)") - if file_name[0]: - tf.imsave(str(file_name[0]) + ".tiff", np.dstack([self.img1, self.img2]).T) - self.statusbar.showMessage(f"Images saved to {str(file_name[0])}") - else: - pass - - -class LoadingScreen(QtWidgets.QSplashScreen): - def __init__(self): - super(LoadingScreen, self).__init__() - uic.loadUi(os.path.join(ui_path, "uis/animationWindow.ui"), self) - self.setWindowOpacity(0.65) - self.movie = QMovie("uis/animation.gif") - self.label.setMovie(self.movie) - - def mousePressEvent(self, event): - # disable default "click-to-dismiss" behaviour - pass - - def startAnimation(self): - self.movie.start() - self.show() - - def stopAnimation(self): - self.movie.stop() - self.hide() - - -class CompositeScatterPlot(QtWidgets.QMainWindow): - def __init__(self, scatterPoints, fitLine, maskImages, fitEquations, nameTuple): - super(CompositeScatterPlot, self).__init__() - - uic.loadUi(os.path.join(ui_path, "uis/multipleScatterFit.ui"), self) - self.centralwidget.setStyleSheet(open(os.path.join(ui_path, "css/defaultStyle.css")).read()) - - self.scatterPoints = scatterPoints - self.fitLine = fitLine - self.scatterColors = ["r", (0, 115, 0), (4, 186, 186), "c", "w", "k"] - self.fitColors = ["b", "r", "m", "k", "b"] - self.roiNames = list(fitEquations.keys()) - self.fitEqns = list(fitEquations.values()) - self.nameTuple = nameTuple - self.maskImages = maskImages - - # self.scatterViewer.setBackground('w') - # set the graphicslayoutwidget in the ui as canvas - self.canvas = self.scatterViewer.addPlot() - self.canvas.addLegend() - self.canvas.setLabel("bottom", self.nameTuple[0], "counts") - self.canvas.setLabel("left", self.nameTuple[1], "counts") - - # connections - self.actionExport.triggered.connect(self.exportData) - self.actionSave_as_PNG.triggered.connect(self.exportAsPNG) - self.actionGenerate_MultiColor_Mask.triggered.connect(self.generateMultiColorView) - self.actionWhite.triggered.connect(lambda: self.scatterViewer.setBackground("w")) - self.actionBlack.triggered.connect(lambda: self.scatterViewer.setBackground("k")) - - with pg.BusyCursor(): - for arr, fitline, clr, fitClr, rname, feqn in zip( - self.scatterPoints, self.fitLine, self.scatterColors, self.fitColors, self.roiNames, self.fitEqns - ): - sctrPoints = [] - for pt in arr: - sctrPoints.append( - {"pos": (pt[0], pt[1]), "data": "id", "size": 3, "pen": pg.mkPen(None), "brush": clr} - ) - - # generate a scatter plot item - self.scattered = pg.ScatterPlotItem(size=4.5, pen=clr, brush=pg.mkBrush(5, 214, 255, 200)) - # set scatter plot data - self.scattered.setPoints(sctrPoints, name=rname) - - # set z value negative to show scatter data behind the fit line - self.scattered.setZValue(-10) - - # add scatter plot to the canvas - self.canvas.addItem(self.scattered) - - # generate plotitem for fit line - self.fitLinePlot = pg.PlotDataItem(pen=pg.mkPen(fitClr, width=4.5)) - - # set line plot data - self.fitLinePlot.setData(fitline, name=feqn) - - # add line plot to the canvas - self.canvas.addItem(self.fitLinePlot) - - def generateMultiColorView(self): - self.multichanneldict = {} - - for n, (colorName, image, rname) in enumerate(zip(cmap_dict.keys(), self.maskImages, self.roiNames)): - low, high = np.min(image), np.max(image) - self.multichanneldict[rname] = { - "ImageName": rname, - "ImageDir": ".", - "Image": image, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - - # print( self.multichanneldict) - self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) - self.muli_color_window.show() - - def exportData(self): - exporter = pg.exporters.CSVExporter(self.canvas) - # exporter.parameters()['columnMode'] = '(x,y,y,y) for all plots' - file_name = QFileDialog().getSaveFileName(self, "Save CSV Data", "scatter.csv", "image file (*csv)") - if file_name[0]: - exporter.export(str(file_name[0])) - self.statusbar.showMessage(f"Data saved to {str(file_name[0])}") - else: - pass - - def exportAsPNG(self): - file_name = QtWidgets.QFileDialog().getSaveFileName( - self, "Save Image", "image.png", "PNG(*.png);; TIFF(*.tiff);; JPG(*.jpg)" - ) - exporter = pg.exporters.ImageExporter(self.canvas) - - if file_name[0]: - exporter.export(str(file_name[0])) - self.statusbar.showMessage(f"Image saved to {str(file_name[0])}") - else: - pass - - -class MaskSpecViewer(QtWidgets.QMainWindow): - def __init__(self, xanes_stack=None, xrf_map=None, energy=[]): - super(MaskSpecViewer, self).__init__() - uic.loadUi(os.path.join(ui_path, "uis/MaskedView.ui"), self) - - self.xanes_stack = xanes_stack - self.xrf_map = xrf_map - self.energy = energy - self.xrf_map = self.xanes_stack[-1] - self.view_data() - - # connections - self.sldr_xrf_low.valueChanged.connect(self.create_mask) - self.sldr_xrf_high.valueChanged.connect(self.create_mask) - self.pb_apply_mask.clicked.connect(self.apply_mask_to_xanes) - self.pb_export_mask.clicked.connect(self.export_mask) - self.pb_import_mask.clicked.connect(self.import_a_mask) - self.actionLoad_Energy_List.triggered.connect(self.load_energy) - self.actionLoad_XANES_Stack.triggered.connect(self.load_xanes_stack) - self.actionLoad_XRF_Map.triggered.connect(self.load_xrf_map) - - def view_data(self): - self.xanes_view.setImage(self.xanes_stack) - self.xanes_view.ui.menuBtn.hide() - self.xanes_view.ui.roiBtn.hide() - (self.dim1, self.dim3, self.dim2) = self.xanes_stack.shape - self.xanes_view.setPredefinedGradient("viridis") - self.xanes_view.setCurrentIndex(self.dim1 // 2) - self.statusbar.showMessage("One image from the XANES stack is used as mask") - self.xrf_view.setImage(self.xrf_map) - self.xrf_view.ui.menuBtn.hide() - self.xrf_view.ui.roiBtn.hide() - self.xrf_view.setPredefinedGradient("viridis") - - self.mask_view.ui.menuBtn.hide() - self.mask_view.ui.roiBtn.hide() - - def create_mask(self): - self.threshold_low = np.around(self.sldr_xrf_low.value() * 0.01, 3) - self.threshold_high = np.around(self.sldr_xrf_high.value() * 0.01, 3) - self.sldr_xrf_low.setMaximum(self.sldr_xrf_high.value() + 1) - self.sldr_xrf_high.setMinimum(self.sldr_xrf_low.value() + 1) - self.norm_xrf_map = remove_nan_inf(self.xrf_map) / remove_nan_inf(self.xrf_map.max()) - self.norm_xrf_map[self.norm_xrf_map < self.threshold_low] = 0 - self.norm_xrf_map[self.norm_xrf_map > self.threshold_high] = 0 - self.xrf_view.setImage(self.norm_xrf_map) - self.le_sldr_vals.setText(str(self.threshold_low) + " to " + str(self.threshold_high)) - self.statusbar.showMessage("New Threshold Applied") - self.xrf_mask = np.where(self.norm_xrf_map > 0, self.norm_xrf_map, 0) - self.xrf_mask[self.xrf_mask > 0] = 1 - self.mask_view.setImage(self.xrf_mask) - - def load_xanes_stack(self): - """loading a new xanes stack""" - filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") - self.file_name = str(filename[0]) - self.xanes_stack = tf.imread(self.file_name).transpose(0, 2, 1) - self.view_data() - - def load_energy(self): - """To load energy list that will be used for plotting the spectra. - number of stack should match length of energy list""" - - file_name = QFileDialog().getOpenFileName(self, "Open energy list", "", "text file (*.txt)") - - try: - self.energy = np.loadtxt(str(file_name[0])) - logger.info("Energy file loaded") - assert len(self.energy) == self.dim1 - self.view_data() - - except OSError: - logger.error("No File selected") - pass - - def load_xrf_map(self): - """To xrf map for masking. If 3D mean will be taken""" - - filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") - self.xrf_file_name = str(filename[0]) - self.xrf_map = tf.imread(self.xrf_file_name) - if self.xrf_map.ndim == 3: - self.xrf_map = self.xrf_map.mean(0).T - - else: - self.xrf_map = self.xrf_map.T - - assert ( - self.dim3, - self.dim2, - ) == self.xrf_map.shape, f"Unexpected image dimensions: {self.xrf_map.shape} vs {(self.dim2,self.dim3)}" - - self.view_data() - self.create_mask() - - def apply_mask_to_xanes(self): - """Generates a mask with 0 and 1 from the choosen threshold and multply with the xanes stack. - A spectrum will be generated from the new masked stack""" - - self.masked_xanes = self.xanes_stack * self.xrf_mask - self.xanes_view.setImage(self.masked_xanes) - self.xanes_view.setCurrentIndex(self.dim1 // 2) - self.statusbar.showMessage("Mask Applied to XANES") - self.mask_spec = get_mean_spectra(self.masked_xanes) - - if len(self.energy) != 0: - self.xdata = self.energy - else: - self.xdata = np.arange(0, self.dim1) - self.statusbar.showMessage("No Energy List Available; Integer values are used for plotting") - - self.spectrum_view.plot(self.xdata, self.mask_spec, clear=True) - - def import_a_mask(self): - filename = QFileDialog().getOpenFileName(self, "Select image data", "", "image file(*tiff *tif )") - xrf_file_name = str(filename[0]) - self.xrf_mask = tf.imread(xrf_file_name).T - self.statusbar.showMessage("A New Mask Imported") - self.mask_view.setImage(self.xrf_mask) - self.apply_mask_to_xanes() - - def export_mask(self): - try: - file_name = QFileDialog().getSaveFileName(self, "Save image data", "", "image file(*tiff *tif )") - tf.imsave(str(file_name[0]) + ".tiff", self.xrf_mask.T) - logger.info(f"Updated Image Saved: {str(file_name[0])}") - self.statusbar.showMessage("Mask Exported") - except Exception: - logger.error("No file to save") - pass - - -class StackInfo(QtWidgets.QMainWindow): - def __init__(self, text_to_write: str = " "): - super(StackInfo, self).__init__() - uic.loadUi(os.path.join(ui_path, "uis/log.ui"), self) - - self.text_to_write = text_to_write - self.pte_run_cmd.setPlainText(self.text_to_write) - - # connections - self.pb_save_cmd.clicked.connect(self.save_file) - self.pb_clear_cmd.clicked.connect(self.clear_text) - - def save_file(self): - S__File = QFileDialog.getSaveFileName(None, "SaveFile", "/", "txt Files (*.txt)") - - Text = self.pte_run_cmd.toPlainText() - if S__File[0]: - with open(S__File[0], "w") as file: - file.write(Text) - - def clear_text(self): - self.pte_run_cmd.clear() - - -class MultiChannelWindow(QtWidgets.QMainWindow): - def __init__(self, image_dict=None): - super(MultiChannelWindow, self).__init__() - if image_dict is None: - image_dict = {} - uic.loadUi(os.path.join(ui_path, "uis/mutlichannel.ui"), self) - - self.canvas = self.img_view.addPlot(title="") - self.canvas.getViewBox().invertY(True) - self.canvas.setAspectLocked(True) - self.cb_choose_color.addItems([i for i in cmap_dict.keys()]) - - self.image_dict = image_dict - self.buildFromDictionary() - - # connections - self.actionLoad.triggered.connect(self.createMuliColorAndList) - self.actionLoad_Stack.triggered.connect(self.createMuliColorAndList) - self.cb_choose_color.currentTextChanged.connect(self.updateImageDictionary) - self.pb_update_low_high.clicked.connect(self.updateImageDictionary) - self.listWidget.itemClicked.connect(self.editImageProperties) - self.listWidget.itemDoubleClicked.connect(self.showOneImageOnly) - self.pb_show_selected.clicked.connect(self.showOneImageOnly) - self.pb_show_all.clicked.connect(self.showAllItems) - self.actionLoad_State_File.triggered.connect(self.importState) - self.actionSave_State.triggered.connect(self.exportState) - self.actionSave_View.triggered.connect(self.saveImage) - - def buildFromDictionary(self): - if self.image_dict is not None: - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - else: - pass - - def generateImageDictionary(self): - """Creates a dictionary contains image path, color scheme chosen, throshold limits etc. - when user edits the parameters dictionary will be updated and unwrapped for display later. - This dictionary is saved as json file while saving the state. Two image loading options are possible. - User can either select multiple 2D array images or one 3D array (stack)""" - - clickedAction = self.sender() - - if clickedAction.text() == "Load Images": - # multiple images are selected - self.loadMultipleImageFiles() - - elif clickedAction.text() == "Load Stack": - # an image stack is selected - self.loadAsStack() - - def loadMultipleImageFiles(self): - filter = "TIFF (*.tiff);;TIF (*.tif)" - QtWidgets.QFileDialog().setFileMode(QtWidgets.QFileDialog.ExistingFiles) - # choose mutliple tiff files - names = QtWidgets.QFileDialog().getOpenFileNames(self, "Open files", " ", filter) - if names[0]: - self.image_dict = {} - # select the file directory. Image files are expected to be in the same folder - self.imageDir = os.path.dirname(names[0][0]) - - # create the dictionary - for colorName, image in zip(cmap_dict.keys(), names[0]): - # squeeze to allow with pseudo 3D axis from some tomo recon (eg. 1, 100,100 array) - im_array = np.squeeze(tf.imread(image)) - # set values for thresholding as image min and max - low, high = np.min(im_array), np.max(im_array) - # name of the tiff file is chosen as key for the dictionary, - # inner keys are properties set for that image - im_name = os.path.basename(image) - # construct the dictionary - self.image_dict[f"{os.path.basename(image)}"] = { - "ImageName": im_name, - "ImageDir": self.imageDir, - "Image": im_array, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - else: - pass - - def loadAsStack(self): - """construct the dictionary with image +number as the key. - All other steps are similar to the loadMultipleImageFiles function""" - - filter = "TIFF (*.tiff);;TIF (*.tif)" - file_name = QtWidgets.QFileDialog().getOpenFileName( - self, "Open a Stack", "", "TIFF(*tiff *tif);;all_files (*)", filter - ) - if file_name[0]: - self.imageDir = os.path.dirname(file_name[0]) - self.image_dict = {} - im_stack = np.squeeze(tf.imread(file_name[0])) - # asset the file is a stack - assert im_stack.ndim == 3, "Not a stack" - # construct the dictionary - for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), im_stack)): - low, high = np.min(image), np.max(image) - self.image_dict[f"Image {n+1}"] = { - "ImageName": f"Image {n+1}", - "ImageDir": self.imageDir, - "Image": image, - "Color": colorName, - "CmapLimits": (low, high), - "Opacity": 1.0, - } - - def loadAnImage(self, image, colormap, cmap_limits, opacity=1): - """load single image and colorbar to the widget. This function will be looped for - multiple images later - """ - # get pg image item - img = pg.ImageItem() - # add image to the graphicsview widget - self.canvas.addItem(img) - # set the color map - cmap = pg.ColorMap(pos=np.linspace(0, 1, len(colormap)), color=colormap) - # image = np.squeeze(tf.imread(image_path)) - # set image to the image item with cmap - img.setImage(np.array(image), lut=cmap.getLookupTable(), opacity=opacity) - - # set colorbar for thresholding - bar = pg.ColorBarItem(values=cmap_limits, cmap=cmap, limits=(0, None), orientation="vertical") - bar.setImageItem(img) - # set composition mode to plus for overlaying - img.setCompositionMode(QtGui.QPainter.CompositionMode_Plus) - - def createMultiColorView(self, image_dictionary): - """Function creates multi color image view by taking image - data and parameters from the dictionary""" - - # clear the plots and list in case of re-loading - self.canvas.clear() - self.listWidget.clear() - - # display individual images in for loop - for path_and_color in image_dictionary.values(): - self.loadAnImage( - path_and_color["Image"], - cmap_dict[path_and_color["Color"]], - path_and_color["CmapLimits"], - path_and_color["Opacity"], - ) - - def showOneImageOnly(self): - editItem = self.listWidget.currentItem() - editRow = self.listWidget.currentRow() - for i in range(self.listWidget.count()): - if self.listWidget.item(i) == editItem: - editItemName = self.listWidget.item(i).text().split(",")[0] - self.image_dict[editItemName]["Opacity"] = 1 - - elif self.listWidget.item(i) != editItem: - editItemName = self.listWidget.item(i).text().split(",")[0] - self.image_dict[editItemName]["Opacity"] = 0 - - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - self.listWidget.setCurrentRow(editRow) - - def showAllItems(self): - # editItem = self.listWidget.currentItem() - editRow = self.listWidget.currentRow() - for i in range(self.listWidget.count()): - editItemName = self.listWidget.item(i).text().split(",")[0] - self.image_dict[editItemName]["Opacity"] = 1 - - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - self.listWidget.setCurrentRow(editRow) - - def displayImageNames(self, image_dictionary): - """Populate the list widget table with image name and color used to plot, - using image dictionary input""" - - for im_name, vals in image_dictionary.items(): - self.listWidget.addItem(f"{im_name},{vals['Color']}") - self.listWidget.setCurrentRow(0) - - def createMuliColorAndList(self): - """Finally Load Images and poplulate the list widget from the dictionary""" - with pg.BusyCursor(): # gives the circle showing gui is doing something - self.generateImageDictionary() - if self.image_dict: - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - - else: - pass - - def sliderSetUp(self, im_array): - """Setting the slider min and max from image values""" - - low = (np.min(im_array) / np.max(im_array)) * 100 - self.sldr_low.setMaximum(100) - self.sldr_low.setMinimum(low) - self.sldr_high.setMaximum(100) - self.sldr_high.setMinimum(low) - - def editImageProperties(self, item): - """function to control the assigned properties such as color, - threshold limits, opacity etc of a single image selected using the list widget item""" - - editItem = item.text() - # get the dictionary key from item text - editItemName = editItem.split(",")[0] - editItemColor = editItem.split(",")[1] - im_array = self.image_dict[editItemName]["Image"] - self.sliderSetUp(im_array) - setValLow = (self.image_dict[editItemName]["CmapLimits"][0] * 100) / np.max(im_array) - setValHigh = (self.image_dict[editItemName]["CmapLimits"][1] * 100) / np.max(im_array) - setOpacity = self.image_dict[editItemName]["Opacity"] * 100 - self.sldr_low.setValue(int(setValLow)) - self.sldr_high.setValue(int(setValHigh)) - self.sldr_opacity.setValue(int(setOpacity)) - self.low_high_vals.setText(f"low:{self.sldr_low.value()}," f"high:{self.sldr_high.value()}") - self.cb_choose_color.setCurrentText(editItemColor) - - def updateImageDictionary(self): - newColor = self.cb_choose_color.currentText() - editItem = self.listWidget.currentItem().text() - editRow = self.listWidget.currentRow() - editItemName = editItem.split(",")[0] - self.imageDir = self.image_dict[editItemName]["ImageDir"] - im_array = self.image_dict[editItemName]["Image"] - self.sliderSetUp(im_array) - cmap_limits = ( - self.sldr_low.value() * np.max(im_array) / 100, - self.sldr_high.value() * np.max(im_array) / 100, - ) - self.low_high_vals.setText(f"low:{cmap_limits[0]:.3f},high:{cmap_limits[1]:.3f}") - opacity = self.sldr_opacity.value() / 100 - self.opacity_val.setText(str(opacity)) - self.image_dict[editItemName] = { - "ImageName": editItemName, - "ImageDir": self.imageDir, - "Image": im_array, - "Color": newColor, - "CmapLimits": cmap_limits, - "Opacity": opacity, - } - - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - self.listWidget.setCurrentRow(editRow) - - def exportState(self): - file_name = QtWidgets.QFileDialog().getSaveFileName( - self, "Save Current State", "multicolor_params.json", "json file(*json)" - ) - """ - for val in self.image_dict.values(): - val['CmapLimits'] = json.dumps(str(val['CmapLimits'])) - """ - - if file_name[0]: - with open(f"{file_name[0]}", "w") as fp: - json.dump(self.image_dict, fp, indent=4, cls=jsonEncoder) - - else: - pass - - def importState(self): - file_name = QtWidgets.QFileDialog().getOpenFileName( - self, "Open a State File", "", "json file(*json);;all_files (*)" - ) - if file_name[0]: - with open(file_name[0], "r") as fp: - self.image_dict = json.load(fp) - - self.createMultiColorView(self.image_dict) - self.displayImageNames(self.image_dict) - else: - pass - - def saveImage(self): - file_name = QtWidgets.QFileDialog().getSaveFileName( - self, "Save Image", "multicolor_image.png", "PNG(*.png);; TIFF(*.tiff);; JPG(*.jpg)" - ) - exporter = pg.exporters.ImageExporter(self.canvas.getViewBox()) - exporter.export(file_name[0]) - - -""" Helper Functions""" - - -def get_xrf_data(h="h5file"): - """ - get xrf stack from h5 data generated at NSLS-II beamlines - - Arguments: - h5/hdf5 file - - Returns: - norm_xrf_stack - xrf stack image normalized with Io - mono_e - excitation enegy used for xrf - beamline - identity of the beamline - Io_avg - an average Io value, used before taking log - - """ - - f = h5py.File(h, "r") - - if list(f.keys())[0] == "xrfmap": - logger.info("Data from HXN/TES/SRX") - beamline = f["xrfmap/scan_metadata"].attrs["scan_instrument_id"] - - try: - beamline_scalar = {"HXN": 2, "SRX": 0, "TES": 0} - - if beamline in beamline_scalar.keys(): - Io = np.array(f["xrfmap/scalers/val"])[:, :, beamline_scalar[beamline]] - raw_xrf_stack = np.array(f["xrfmap/detsum/counts"]) - norm_xrf_stack = raw_xrf_stack - Io_avg = int(remove_nan_inf(Io).mean()) - else: - logger.error("Unknown Beamline Scalar") - except Exception: - logger.warning("Unknown Scalar: Raw Detector count in use") - norm_xrf_stack = np.array(f["xrfmap/detsum/counts"]) - - elif list(f.keys())[0] == "xrmmap": - logger.info("Data from XFM") - beamline = "XFM" - raw_xrf_stack = np.array(f["xrmmap/mcasum/counts"]) - Io = np.array(f["xrmmap/scalars/I0"]) - norm_xrf_stack = raw_xrf_stack - Io_avg = int(remove_nan_inf(Io).mean()) - - else: - logger.error("Unknown Data Format") - - try: - mono_e = int(f["xrfmap/scan_metadata"].attrs["instrument_mono_incident_energy"] * 1000) - logger.info("Excitation energy was taken from the h5 data") - - except Exception: - mono_e = 12000 - logger.info(f"Unable to get Excitation energy from the h5 data; using default value {mono_e} KeV") - - return remove_nan_inf(norm_xrf_stack.transpose((2, 0, 1))), mono_e + 1500, beamline, Io_avg - - -def remove_nan_inf(im): - im = np.array(im, dtype=np.float32) - im[np.isnan(im)] = 0 - im[np.isinf(im)] = 0 - return im - - -def rebin_image(im, bin_factor): - arrx, arry = np.shape(im) - if arrx / bin_factor != int or arrx / bin_factor != int: - logger.error("Invalid Binning") - - else: - shape = (arrx / bin_factor, arry / bin_factor) - return im.reshape(shape).mean(-1).mean(1) - - -def remove_hot_pixels(image_array, NSigma=5): - image_array = remove_nan_inf(image_array) - a, b, c = np.shape(image_array) - img_stack2 = np.zeros((a, b, c)) - for i in range(a): - im = image_array[i, :, :] - im[abs(im) > np.std(im) * NSigma] = im.mean() - img_stack2[i, :, :] = im - return img_stack2 - - -def smoothen(image_array, w_size=5): - a, b, c = np.shape(image_array) - spec2D_Matrix = np.reshape(image_array, (a, (b * c))) - smooth2D_Matrix = savgol_filter(spec2D_Matrix, w_size, w_size - 2, axis=0) - return remove_nan_inf(np.reshape(smooth2D_Matrix, (a, b, c))) - - -def resize_stack(image_array, upscaling=False, scaling_factor=2): - en, im1, im2 = np.shape(image_array) - - if upscaling: - im1_ = im1 * scaling_factor - im2_ = im2 * scaling_factor - img_stack_resized = resize(image_array, (en, im1_, im2_)) - - else: - im1_ = int(im1 / scaling_factor) - im2_ = int(im2 / scaling_factor) - img_stack_resized = resize(image_array, (en, im1_, im2_)) - - return img_stack_resized - - -def normalize(image_array, norm_point=-1): - norm_stack = image_array / image_array[norm_point] - return remove_nan_inf(norm_stack) - - -def remove_edges(image_array): - # z, x, y = np.shape(image_array) - return image_array[:, 1:-1, 1:-1] - - -def background_value(image_array): - img = image_array.mean(0) - img_h = img.mean(0) - img_v = img.mean(1) - h = np.gradient(img_h) - v = np.gradient(img_v) - bg = np.min([img_h[h == h.max()], img_v[v == v.max()]]) - return bg - - -def background_subtraction(img_stack, bg_percentage=10): - img_stack = remove_nan_inf(img_stack) - a, b, c = np.shape(img_stack) - ref_image = np.reshape(img_stack.mean(0), (b * c)) - bg_ratio = int((b * c) * 0.01 * bg_percentage) - bg_ = np.max(sorted(ref_image)[0:bg_ratio]) - bged_img_stack = img_stack - bg_[:, np.newaxis, np.newaxis] - return bged_img_stack - - -def background_subtraction2(img_stack, bg_percentage=10): - img_stack = remove_nan_inf(img_stack) - a, b, c = np.shape(img_stack) - bg_ratio = int((b * c) * 0.01 * bg_percentage) - bged_img_stack = img_stack.copy() - - for n, img in enumerate(img_stack): - bg_ = np.max(sorted(img.flatten())[0:bg_ratio]) - print(bg_) - bged_img_stack[n] = img - bg_ - - return remove_nan_inf(bged_img_stack) - - -def background1(img_stack): - img = img_stack.sum(0) - img_h = img.mean(0) - img_v = img.mean(1) - h = np.gradient(img_h) - v = np.gradient(img_v) - bg = np.min([img_h[h == h.max()], img_v[v == v.max()]]) - return bg - - -def get_sum_spectra(image_array): - spec = np.sum(image_array, axis=(1, 2)) - return spec - - -def get_mean_spectra(image_array): - spec = np.mean(image_array, axis=(1, 2)) - return spec - - -def flatten_(image_array): - z, x, y = np.shape(image_array) - flat_array = np.reshape(image_array, (x * y, z)) - return flat_array - - -def image_to_pandas(image_array): - a, b, c = np.shape(image_array) - im_array = np.reshape(image_array, ((b * c), a)) - a, b = im_array.shape - df = pd.DataFrame( - data=im_array[:, :], columns=["e" + str(i) for i in range(b)], index=["s" + str(i) for i in range(a)] - ) - return df - - -def image_to_pandas2(image_array): - a, b, c = np.shape(image_array) - im_array = np.reshape(image_array, (a, (b * c))) - a, b = im_array.shape - df = pd.DataFrame( - data=im_array[:, :], index=["e" + str(i) for i in range(a)], columns=["s" + str(i) for i in range(b)] - ) - return df - - -def neg_log(image_array): - absorb = -1 * np.log(image_array) - return remove_nan_inf(absorb) - - -def clean_stack(img_stack, auto_bg=False, bg_percentage=5): - a, b, c = np.shape(img_stack) - - if auto_bg is True: - bg_ = background1(img_stack) - - else: - sum_spec = (img_stack.sum(1)).sum(1) - ref_stk_num = np.where(sum_spec == sum_spec.max())[-1] - - ref_image = np.reshape(img_stack[ref_stk_num], (b * c)) - bg_ratio = int((b * c) * 0.01 * bg_percentage) - bg_ = np.max(sorted(ref_image)[0:bg_ratio]) - - bg = np.where(img_stack[ref_stk_num] > bg_, img_stack[ref_stk_num], 0) - bg2 = np.where(bg < bg_, bg, 1) - - bged_img_stack = img_stack * bg2 - - return remove_nan_inf(bged_img_stack) - - -def subtractBackground(im_stack, bg_region): - if bg_region.ndim == 3: - bg_region_ = np.mean(bg_region, axis=(1, 2)) - - elif bg_region.ndim == 2: - bg_region_ = np.mean(bg_region, axis=1) - - else: - bg_region_ = bg_region - - return im_stack - bg_region_[:, np.newaxis, np.newaxis] - - -def classify(img_stack, correlation="Pearson"): - img_stack_ = img_stack - a, b, c = np.shape(img_stack_) - norm_img_stack = normalize(img_stack_) - f = np.reshape(norm_img_stack, (a, (b * c))) - - max_x, max_y = np.where(norm_img_stack.sum(0) == (norm_img_stack.sum(0)).max()) - ref = norm_img_stack[:, int(max_x), int(max_y)] - corr = np.zeros(len(f.T)) - for s in range(len(f.T)): - if correlation == "Kendall": - r, p = stats.kendalltau(ref, f.T[s]) - elif correlation == "Pearson": - r, p = stats.pearsonr(ref, f.T[s]) - - corr[s] = r - - cluster_image = np.reshape(corr, (b, c)) - return (cluster_image**3), img_stack_ - - -def correlation_kmeans(img_stack, n_clusters, correlation="Pearson"): - img, bg_image = classify(img_stack, correlation) - img[np.isnan(img)] = -99999 - X = img.reshape((-1, 1)) - k_means = sc.KMeans(n_clusters) - k_means.fit(X) - - X_cluster = k_means.labels_ - X_cluster = X_cluster.reshape(img.shape) + 1 - - return X_cluster - - -def cluster_stack( - im_array, method="KMeans", n_clusters_=4, decomposed=False, decompose_method="PCA", decompose_comp=2 -): - a, b, c = im_array.shape - - if method == "Correlation-Kmeans": - X_cluster = correlation_kmeans(im_array, n_clusters_, correlation="Pearson") - - else: - methods = { - "MiniBatchKMeans": sc.MiniBatchKMeans, - "KMeans": sc.KMeans, - "MeanShift": sc.MeanShift, - "Spectral Clustering": sc.SpectralClustering, - "Affinity Propagation": sc.AffinityPropagation, - } - - if decomposed: - im_array = denoise_with_decomposition(im_array, method_=decompose_method, n_components=decompose_comp) - - flat_array = np.reshape(im_array, (a, (b * c))) - init_cluster = methods[method](n_clusters=n_clusters_) - init_cluster.fit(np.transpose(flat_array)) - X_cluster = init_cluster.labels_.reshape(b, c) + 1 - - decon_spectra = np.zeros((a, n_clusters_)) - decon_images = np.zeros((n_clusters_, b, c)) - - for i in range(n_clusters_): - mask_i = np.where(X_cluster == (i + 1), X_cluster, 0) - spec_i = get_sum_spectra(im_array * mask_i) - decon_spectra[:, i] = spec_i - decon_images[i] = im_array.sum(0) * mask_i - - return decon_images, X_cluster, decon_spectra - - -def kmeans_variance(im_array): - a, b, c = im_array.shape - flat_array = np.reshape(im_array, (a, (b * c))) - var = np.arange(24) - clust_n = np.arange(24) + 2 - - for clust in var: - init_cluster = sc.KMeans(n_clusters=int(clust + 2)) - init_cluster.fit(np.transpose(flat_array)) - var_ = init_cluster.inertia_ - var[clust] = np.float64(var_) - - kmeans_var_plot = pg.plot( - clust_n, var, title="KMeans Variance", pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), symbol="o" - ) - kmeans_var_plot.setLabel("bottom", "Cluster Number") - kmeans_var_plot.setLabel("left", "Sum of squared distances") - - -def pca_scree(im_stack): - new_image = im_stack.transpose(2, 1, 0) - x, y, z = np.shape(new_image) - img_ = np.reshape(new_image, (x * y, z)) - pca = sd.PCA(z) - pca.fit(img_) - # var = pca.explained_variance_ratio_ - var = pca.singular_values_ - - pca_scree_plot = pg.plot( - var[:24], title="PCA Scree Plot", pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), symbol="o" - ) - pca_scree_plot.addLine(y=0) - pca_scree_plot.setLabel("bottom", "Component Number") - pca_scree_plot.setLabel("left", "Singular Values") - - -def decompose_stack(im_stack, decompose_method="PCA", n_components_=3): - new_image = im_stack.transpose(2, 1, 0) - x, y, z = np.shape(new_image) - img_ = np.reshape(new_image, (x * y, z)) - methods_dict = { - "PCA": sd.PCA, - "IncrementalPCA": sd.IncrementalPCA, - "NMF": sd.NMF, - "FastICA": sd.FastICA, - "DictionaryLearning": sd.MiniBatchDictionaryLearning, - "FactorAnalysis": sd.FactorAnalysis, - "TruncatedSVD": sd.TruncatedSVD, - } - - _mdl = methods_dict[decompose_method](n_components=n_components_) - - ims = (_mdl.fit_transform(img_).reshape(x, y, n_components_)).transpose(2, 1, 0) - spcs = _mdl.components_.transpose() - decon_spetra = np.zeros((z, n_components_)) - decom_map = np.zeros((ims.shape)) - - for i in range(n_components_): - f = ims.copy()[i] - f[f < 0] = 0 - spec_i = ((new_image.T * f).sum(1)).sum(1) - decon_spetra[:, i] = spec_i - - f[f > 0] = i + 1 - decom_map[i] = f - decom_map = decom_map.sum(0) - - return np.float32(ims), spcs, decon_spetra, decom_map - - -def denoise_with_decomposition(img_stack, method_="PCA", n_components=4): - new_image = img_stack.transpose(2, 1, 0) - x, y, z = np.shape(new_image) - img_ = np.reshape(new_image, (x * y, z)) - - methods_dict = { - "PCA": sd.PCA, - "IncrementalPCA": sd.IncrementalPCA, - "NMF": sd.NMF, - "FastICA": sd.FastICA, - "DictionaryLearning": sd.DictionaryLearning, - "FactorAnalysis": sd.FactorAnalysis, - "TruncatedSVD": sd.TruncatedSVD, - } - - decomposed = methods_dict[method_](n_components=n_components) - - ims = (decomposed.fit_transform(img_).reshape(x, y, n_components)).transpose(2, 1, 0) - ims[ims < 0] = 0 - ims[ims > 0] = 1 - mask = ims.sum(0) - mask[mask > 1] = 1 - # mask = uniform_filter(mask) - filtered = img_stack * mask - # plt.figure() - # plt.imshow(filtered.sum(0)) - # plt.title('background removed') - # plt.show() - return remove_nan_inf(filtered) - - -def interploate_E(refs, e): - n = np.shape(refs)[1] - refs = np.array(refs) - ref_e = refs[:, 0] - ref = refs[:, 1:n] - all_ref = [] - for i in range(n - 1): - ref_i = np.interp(e, ref_e, ref[:, i]) - all_ref.append(ref_i) - return np.array(all_ref) - - -def getStats(spec, fit, num_refs=2): - stats = {} - - r_factor = (np.sum(spec - fit) ** 2) / np.sum(spec**2) - stats["R_Factor"] = np.around(r_factor, 5) - - y_mean = np.sum(spec) / len(spec) - SS_tot = np.sum((spec - y_mean) ** 2) - SS_res = np.sum((spec - fit) ** 2) - r_square = 1 - (SS_res / SS_tot) - stats["R_Square"] = np.around(r_square, 4) - - chisq = np.sum((spec - fit) ** 2) - stats["Chi_Square"] = np.around(chisq, 5) - - red_chisq = chisq / (len(spec) - num_refs) - stats["Reduced Chi_Square"] = red_chisq - - return stats - - -def xanes_fitting_1D(spec, e_list, refs, method="NNLS", alphaForLM=0.01): - """Linear combination fit of image data with reference standards""" - - int_refs = interploate_E(refs, e_list) - - if method == "NNLS": - coeffs, r = opt.nnls(int_refs.T, spec) - - elif method == "LASSO": - lasso = linear_model.Lasso(positive=True, alpha=alphaForLM) # lowering alpha helps with 1D fits - fit_results = lasso.fit(int_refs.T, spec) - coeffs = fit_results.coef_ - - elif method == "RIDGE": - ridge = linear_model.Ridge(alpha=alphaForLM) - fit_results = ridge.fit(int_refs.T, spec) - coeffs = fit_results.coef_ - - fit = coeffs @ int_refs - stats = getStats(spec, fit, num_refs=np.min(np.shape(int_refs.T))) - - return stats, coeffs - - -def xanes_fitting(im_stack, e_list, refs, method="NNLS", alphaForLM=0.1, binStack=False): - """Linear combination fit of image data with reference standards""" - - if binStack: - im_stack = resize_stack(im_stack, scaling_factor=4) - - en, im1, im2 = np.shape(im_stack) - im_array = im_stack.reshape(en, im1 * im2) - coeffs_arr = [] - r_factor_arr = [] - # lasso = linear_model.Lasso(positive=True, alpha=alphaForLM) - for n, i in enumerate(range(im1 * im2)): - stats, coeffs = xanes_fitting_1D(im_array[:, i], e_list, refs, method=method, alphaForLM=alphaForLM) - coeffs_arr.append(coeffs) - r_factor_arr.append(stats["R_Factor"]) - - abundance_map = np.reshape(coeffs_arr, (im1, im2, -1)) - r_factor_im = np.reshape(r_factor_arr, (im1, im2)) - - return abundance_map, r_factor_im, np.mean(coeffs_arr, axis=0) - - -def xanes_fitting_Line(im_stack, e_list, refs, method="NNLS", alphaForLM=0.05): - """Linear combination fit of image data with reference standards""" - en, im1, im2 = np.shape(im_stack) - im_array = np.mean(im_stack, 2) - coeffs_arr = [] - meanStats = {"R_Factor": 0, "R_Square": 0, "Chi_Square": 0, "Reduced Chi_Square": 0} - - for i in range(im1): - stats, coeffs = xanes_fitting_1D(im_array[:, i], e_list, refs, method=method, alphaForLM=alphaForLM) - coeffs_arr.append(coeffs) - for key in stats.keys(): - meanStats[key] += stats[key] - - for key, vals in meanStats.items(): - meanStats[key] = np.around((vals / im1), 5) - - return meanStats, np.mean(coeffs_arr, axis=0) - - -def xanes_fitting_Binned(im_stack, e_list, refs, method="NNLS", alphaForLM=0.05): - """Linear combination fit of image data with reference standards""" - - im_stack = resize_stack(im_stack, scaling_factor=10) - # use a simple filter to find threshold value - val = filters.threshold_otsu(im_stack[-1]) - en, im1, im2 = np.shape(im_stack) - im_array = im_stack.reshape(en, im1 * im2) - coeffs_arr = [] - meanStats = {"R_Factor": 0, "R_Square": 0, "Chi_Square": 0, "Reduced Chi_Square": 0} - - specs_fitted = 0 - total_spec = im1 * im2 - for i in range(total_spec): - spec = im_array[:, i] - # do not fit low intensity/background regions - if spec[-1] > val: - specs_fitted += 1 - stats, coeffs = xanes_fitting_1D(spec / spec[-1], e_list, refs, method=method, alphaForLM=alphaForLM) - coeffs_arr.append(coeffs) - for key in stats.keys(): - meanStats[key] += stats[key] - else: - pass - - for key, vals in meanStats.items(): - meanStats[key] = np.around((vals / specs_fitted), 6) - # print(f"{specs_fitted}/{total_spec}") - return meanStats, np.mean(coeffs_arr, axis=0) - - -def create_df_from_nor(athenafile="fe_refs.nor"): - """create pandas dataframe from athena nor file, first column - is energy and headers are sample names""" - - refs = np.loadtxt(athenafile) - n_refs = refs.shape[-1] - skip_raw_n = n_refs + 6 - - df = pd.read_table( - athenafile, delim_whitespace=True, skiprows=skip_raw_n, header=None, usecols=np.arange(0, n_refs) - ) - df2 = pd.read_table( - athenafile, delim_whitespace=True, skiprows=skip_raw_n - 1, usecols=np.arange(0, n_refs + 1) - ) - new_col = df2.columns.drop("#") - df.columns = new_col - return df, list(new_col) - - -def create_df_from_nor_try2(athenafile="fe_refs.nor"): - """create pandas dataframe from athena nor file, first column - is energy and headers are sample names""" - - refs = np.loadtxt(athenafile) - n_refs = refs.shape[-1] - df_refs = pd.DataFrame(refs) - - df = pd.read_csv(athenafile, header=None) - new_col = list((str(df.iloc[n_refs + 5].values)).split(" ")[2::2]) - df_refs.columns = new_col - - return df_refs, list(new_col) - - -def energy_from_logfile(logfile="maps_log_tiff.txt"): - df = pd.read_csv(logfile, header=None, delim_whitespace=True, skiprows=9) - return df[9][df[7] == "energy"].values.astype(float) - - -def xanesNormalization( - e, mu, e0=7125, step=None, nnorm=2, nvict=0, pre1=None, pre2=-50, norm1=100, norm2=None, guess=False -): - if guess: - result = preedge(e, mu, e0, step=step, nnorm=nnorm, nvict=nvict) - - return result["pre1"], result["pre2"], result["norm1"], result["norm2"] - - else: - result = preedge(e, mu, e0, step, nnorm, nvict, pre1, pre2, norm1, norm2) - - return result["pre_edge"], result["post_edge"], result["norm"] - - -def xanesNormStack( - e_list, im_stack, e0=7125, step=None, nnorm=2, nvict=0, pre1=None, pre2=-50, norm1=100, norm2=None -): - en, im1, im2 = np.shape(im_stack) - im_array = im_stack.reshape(en, im1 * im2) - normedStackArray = np.zeros_like(im_array) - - for i in range(im1 * im2): - pre_line, post_line, normXANES = xanesNormalization( - e_list, - im_array[:, i], - e0=e0, - step=step, - nnorm=nnorm, - nvict=nvict, - pre1=pre1, - pre2=pre2, - norm1=norm1, - norm2=norm2, - guess=False, - ) - normedStackArray[:, i] = normXANES - - return remove_nan_inf(np.reshape(normedStackArray, (en, im1, im2))) - - -def align_stack( - stack_img, ref_image_void=True, ref_stack=None, transformation=StackReg.TRANSLATION, reference="previous" -): - """Image registration flow using pystack reg""" - - # all the options are in one function - - sr = StackReg(transformation) - - if ref_image_void: - tmats_ = sr.register_stack(stack_img, reference=reference) - - else: - tmats_ = sr.register_stack(ref_stack, reference=reference) - # out_ref = sr.transform_stack(ref_stack) - - out_stk = sr.transform_stack(stack_img, tmats=tmats_) - return np.float32(out_stk), tmats_ - - -def align_simple(stack_img, transformation=StackReg.TRANSLATION, reference="previous"): - sr = StackReg(transformation) - tmats_ = sr.register_stack(stack_img, reference="previous") - for i in range(10): - out_stk = sr.transform_stack(stack_img, tmats=tmats_) - import time - - time.sleep(2) - return np.float32(out_stk) - - -def align_with_tmat(stack_img, tmat_file, transformation=StackReg.TRANSLATION): - sr = StackReg(transformation) - out_stk = sr.transform_stack(stack_img, tmats=tmat_file) - return np.float32(out_stk) - - -def align_stack_iter( - stack, - ref_stack_void=True, - ref_stack=None, - transformation=StackReg.TRANSLATION, - method=("previous", "first"), - max_iter=2, -): - if ref_stack_void: - ref_stack = stack - - for i in range(max_iter): - sr = StackReg(transformation) - for ii in range(len(method)): - print(ii, method[ii]) - tmats = sr.register_stack(ref_stack, reference=method[ii]) - ref_stack = sr.transform_stack(ref_stack) - stack = sr.transform_stack(stack, tmats=tmats) - - return np.float32(stack) - - -def applyMaskGetMeanSpectrum(im_stack, mask): - """A 2d mask to multiply with the 3d xanes stack and returns mean spectrum""" - - masked_stack = im_stack * mask - return get_mean_spectra(masked_stack) - - -def modifyStack( - raw_stack, - normalizeStack=False, - normToPoint=-1, - applySmooth=False, - smoothWindowSize=3, - applyThreshold=False, - thresholdValue=0, - removeOutliers=False, - nSigmaOutlier=3, - applyTranspose=False, - transposeVals=(0, 1, 2), - applyCrop=False, - cropVals=(0, 1, 2), - removeEdges=False, - resizeStack=False, - upScaling=False, - binFactor=2, -): - """A giant function to modify the stack with many possible operations. - all the changes can be saved to a jason file as a config file. Enabling and - distabling the sliders is a problem""" - - """ - normStack = normalize(raw_stack, norm_point=normToPoint) - smoothStack = smoothen(raw_stack, w_size= smoothWindowSize) - thresholdStack = clean_stack(raw_stack, auto_bg=False, bg_percentage = thresholdValue) - outlierStack = remove_hot_pixels(raw_stack, NSigma=nSigmaOutlier) - transposeStack = np.transpose(raw_stack, transposeVals) - croppedStack = raw_stack[cropVals] - edgeStack = remove_edges(raw_stack) - binnedStack = resize_stack(raw_stack,upscaling=upScaling,scaling_factor=binFactor) - - """ - - if removeOutliers: - modStack = remove_hot_pixels(raw_stack, NSigma=nSigmaOutlier) - - else: - modStack = raw_stack - - if applyThreshold: - modStack = clean_stack(modStack, auto_bg=False, bg_percentage=thresholdValue) - - else: - pass - - if applySmooth: - modStack = smoothen(modStack, w_size=smoothWindowSize) - - else: - pass - - if applyTranspose: - modStack = np.transpose(modStack, transposeVals) - - else: - pass - - if applyCrop: - modStack = modStack[cropVals] - - else: - pass - - if normalizeStack: - modStack = normalize(raw_stack, norm_point=normToPoint) - else: - pass - - -def start_xmidas(): - def formatter(prog): - # Set maximum width such that printed help mostly fits in the RTD theme code block (documentation). - return argparse.RawDescriptionHelpFormatter(prog, max_help_position=20, width=90) - - parser = argparse.ArgumentParser( - description=f"XMidas: v{__version__}", - formatter_class=formatter, - ) - parser.parse_args() - - logger.setLevel(logging.INFO) - formatter = logging.Formatter(fmt="%(asctime)s : %(levelname)s : %(message)s") - stream_handler = logging.StreamHandler() - stream_handler.setFormatter(formatter) - stream_handler.setLevel(logging.INFO) - if logger.hasHandlers(): - logger.handlers.clear() - logger.addHandler(stream_handler) - - if version.parse(PYQT_VERSION_STR) >= version.parse("5.14"): - QApplication.setAttribute(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) - - app = QtWidgets.QApplication(sys.argv) - # app.setAttribute(QtCore.Qt.AA_Use96Dpi) - window = midasWindow() - window.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - start_xmidas() +# -*- coding: utf-8 -*- + +# Author: Ajith Pattammattel +# First Version on:06-23-2020 +#python 3.12 update on July 2025 +__version__ = "1.0.0" + +import argparse +import logging +import datetime +import sys +import webbrowser +import traceback +import os +import json +import scipy.stats as stats +import numpy as np +import pandas as pd +import tifffile as tf +import pyqtgraph as pg +import pyqtgraph.exporters +import faulthandler +faulthandler.enable() + + +from glob import glob +from pyqtgraph import plot +from itertools import combinations +from scipy.stats import linregress +from packaging import version + +from PyQt6 import QtWidgets, QtCore, QtGui, uic, QtTest +from PyQt6.QtGui import QMovie +from PyQt6.QtWidgets import QMessageBox, QFileDialog, QApplication +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool, PYQT_VERSION_STR, QTimer + + +from xmidas.utils.utils import * +from xmidas.utils.color_maps import create_color_maps +from xmidas.models.encoders import jsonEncoder + +from xmidas.gui.windows.xanes_viewer import XANESViewer +from xmidas.gui.windows.multichannel_viewer import MultiChannelWindow +from xmidas.gui.windows.mask_maker import MaskSpecViewer +from xmidas.gui.windows.singleStackViewer import * + +cmap_dict = create_color_maps() + +#from . import __version__ + +logger = logging.getLogger() +try: + import cv2 # noqa: F401 +except Exception: + logger.warning("openCV module not found") + pass +if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) + +if hasattr(QtCore.Qt, "AA_UseHighDpiPixmaps"): + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) + +ui_dir = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "gui/layout" +)) + +print(ui_dir) + +# global settings for pyqtgraph plot and image colormaps +pg.setConfigOption("imageAxisOrder", "row-major") + +class midasWindow(QtWidgets.QMainWindow): + def __init__(self, im_stack=None, energy=[], refs=[]): + super(midasWindow, self).__init__() + uic.loadUi(os.path.join(ui_dir, "midasMainwindow.ui"), self) + self.im_stack = im_stack + self.energy = energy + self.refs = refs + self.loaded_tranform_file = [] + self.image_roi2_flag = False + self.refStackAvailable = False + self.isAReload = False + self.plotWidth = 2 + self.stackStatusDict = {} + + # Performance optimization: debounce timer and cache + self.update_timer = QTimer() + self.update_timer.setSingleShot(True) + self.update_timer.timeout.connect(self._do_update_stack) + self._pending_update = False + + # Cache for expensive operations + self._cache = { + 'resize': {}, # {scaling_factor: processed_stack} + 'outliers': {}, # {nsigma: processed_stack} + 'smooth': {}, # {window_size: processed_stack} + 'last_params': {} # Track last used parameters + } + + self.user_wd = os.path.expanduser("~") + # self.user_config_path = os.path.join(ui_path,"user_config.json") + + # if not os.path.exists(self.user_config_path): + + # with open(f"{self.user_config_path}", "w") as fp: + # json.dump(self.user_config, fp, indent=4) + + self.plt_colors = [ + "g", + "r", + "c", + "m", + "y", + "w", + "b", + pg.mkPen(70, 5, 80), + pg.mkPen(255, 85, 130), + pg.mkPen(0, 85, 130), + pg.mkPen(255, 170, 60), + ] * 3 + # window style + self.actionDarkMode.triggered.connect(self.darkMode) + self.actionDefault.triggered.connect(self.defaultMode) + self.actionModern.triggered.connect(self.modernMode) + + # self.setToolTipsVisible(True) + for menuItem in self.findChildren(QtWidgets.QMenu): + menuItem.setToolTipsVisible(True) + + # plotview options + self.actionWhite.triggered.connect(lambda: self.spectrum_view.setBackground("w")) + self.actionRed.triggered.connect(lambda: self.spectrum_view.setBackground("r")) + self.actionYellow.triggered.connect(lambda: self.spectrum_view.setBackground("y")) + self.actionBlue.triggered.connect(lambda: self.spectrum_view.setBackground("b")) + self.actionBlack.triggered.connect(lambda: self.spectrum_view.setBackground((0, 0, 0))) + + self.actn1.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn1.text()))) + self.actn2.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn2.text()))) + self.actn3.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn3.text()))) + self.actn4.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn4.text()))) + self.actn5.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn5.text()))) + self.actn6.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn6.text()))) + self.actn8.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn8.text()))) + self.actn10.triggered.connect(lambda: self.setPlotLineWidth(int(self.actn10.text()))) + + self.actionOpen_Image_Data.triggered.connect(self.browse_file) + self.actionOpen_Multiple_Files.triggered.connect(self.createVirtualStack) + self.actionSave_as.triggered.connect(lambda: self.save_stack()) + self.actionExit.triggered.connect(lambda: QApplication.closeAllWindows()) + self.actionOpen_in_GitHub.triggered.connect(self.open_github_link) + self.actionLoad_Energy.triggered.connect(self.select_elist) + self.menuFile.setToolTipsVisible(True) + + # Accessories + self.actionOpen_Mask_Gen.triggered.connect(self.openMaskMaker) + self.actionMultiColor.triggered.connect(self.openMultiColorWindow) + + # calculations + self.pb_transpose_stack.clicked.connect(lambda: self.threadMaker(self.transposeStack)) + self.pb_swapXY_stack.clicked.connect(lambda: self.threadMaker(self.swapStackXY)) + self.pb_reset_img.clicked.connect(self.reloadImageStack) + self.pb_crop.clicked.connect(self.crop_to_dim) + self.pb_apply_crop_to_all.clicked.connect(self.apply_crop_to_all) + self.pb_crop.clicked.connect(self.view_stack) + self.sb_scaling_factor.valueChanged.connect(self.view_stack) + self.pb_ref_xanes.clicked.connect(self.select_ref_file) + self.pb_elist_xanes.clicked.connect(self.select_elist) + + # batchjobs + self.actionPlotAllCorrelations.triggered.connect(self.plotCorrelationsAllCombinations) + + # Use debounced updates for sliders to prevent rapid successive processing + [ + uis.valueChanged.connect(self.schedule_update) + for uis in [self.hs_smooth_size, self.hs_nsigma, self.hs_bg_threshold] + ] + + [ + uis.stateChanged.connect(self.replot_image) + for uis in [self.cb_remove_bg, self.cb_remove_outliers, self.cb_smooth, self.cb_norm, self.cb_log] + ] + + [ + uis.stateChanged.connect(self.view_stack) + for uis in [self.cb_remove_edges, self.cb_upscale, self.cb_rebin] + ] + + # ToolBar + self.actionStack_Info.triggered.connect(self.displayStackInfo) + self.actionSave_Image.triggered.connect(self.save_disp_img) + self.actionExport_Stack.triggered.connect(lambda: self.save_stack()) + + # ROI background + self.actionSubtract_ROI_BG.triggered.connect(lambda: self.threadMaker(self.removeROIBGStack)) + + # alignment + self.pb_load_align_ref.clicked.connect(self.loadAlignRefImage) + self.pb_loadAlignTranform.clicked.connect(self.importAlignTransformation) + self.pb_saveAlignTranform.clicked.connect(self.exportAlignTransformation) + self.pb_alignStack.clicked.connect(lambda: self.threadMaker(self.stackRegistration)) + # self.pb_alignStack.clicked.connect(self.stackRegistration) + + # save_options + self.actionSave_Sum_Image.triggered.connect(lambda: self.save_stack(method="sum")) + self.actionSave_Mean_Image.triggered.connect(lambda: self.save_stack(method="mean")) + self.actionExport_Image_to_CSV.triggered.connect(self.stackToCSV) + self.pb_save_disp_spec.clicked.connect(self.save_disp_spec) + self.actionSave_Energy_List.triggered.connect(self.saveEnergyList) + self.pb_show_roi.clicked.connect(self.getROIMask) + self.pb_addToCollector.clicked.connect(self.addSpectrumToCollector) + self.pb_collect_clear.clicked.connect(lambda: self.spectrum_view_collect.clear()) + self.pb_saveCollectorPlot.clicked.connect(self.saveCollectorPlot) + + # XANES Normalization + self.pb_apply_xanes_norm.clicked.connect(self.nomalizeLiveSpec) + self.pb_auto_Eo.clicked.connect(self.findEo) + self.pb_xanes_norm_vals.clicked.connect(self.initNormVals) + self.pb_apply_norm_to_stack.clicked.connect(lambda: self.threadMaker(self.normalizeStack)) + self.actionExport_Norm_Params.triggered.connect(self.exportNormParams) + self.actionImport_Norm_Params.triggered.connect(self.importNormParams) + + # Analysis + self.pb_pca_scree.clicked.connect(self.pca_scree_) + self.pb_calc_components.clicked.connect(self.calc_comp_) + self.pb_kmeans_elbow.clicked.connect(self.kmeans_elbow) + self.pb_calc_cluster.clicked.connect(self.clustering_) + self.pb_xanes_fit.clicked.connect(self.fast_xanes_fitting) + self.pb_plot_refs.clicked.connect(self.plt_xanes_refs) + + self.show() + + self.threadpool = QThreadPool() + print(f"Multithreading with maximum {self.threadpool.maxThreadCount()} threads") + + # View Options + def darkMode(self): + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/darkStyle.css")).read()) + + def defaultMode(self): + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + + def modernMode(self): + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/modern.css")).read()) + + def setPlotLineWidth(self, width_input): + self.plotWidth = width_input + try: + self.update_spectrum() + except Exception: + pass + + def openMultiColorWindow(self): + self.multicolorwindow = MultiChannelWindow() + self.multicolorwindow.show() + + def openMaskMaker(self): + self.mask_window = MaskSpecViewer(xanes_stack=self.displayedStack, energy=self.energy) + self.mask_window.show() + + def open_github_link(self): + webbrowser.open("https://github.com/pattammattel/NSLS-II-MIDAS/wiki") + + def threadMaker(self, funct): + # Pass the function to execute + worker = Worker(funct) # Any other args, kwargs are passed to the run function + self.loadSplashScreen() + worker.signals.start.connect(self.splash.startAnimation) + worker.signals.result.connect(self.print_output) + + list( + map( + worker.signals.finished.connect, + [ + self.thread_complete, + self.splash.stopAnimation, + self.update_stack_info, + self.update_spectrum, + self.update_image_roi, + self.setImageROI + ], + ) + ) + + # Execute + self.threadpool.start(worker) + + + # File Loading + + def createVirtualStack(self): + """User can load multiple/series of tiff images with same shape. + The 'self.load_stack()' recognizes 'self.filename as list and create the stack. + """ + self.energy = [] + filter = "TIFF (*.tiff);;TIF (*.tif);;all_files (*)" + file_name = QFileDialog() + file_name.setFileMode(QFileDialog.FileMode.ExistingFiles) + names = file_name.getOpenFileNames(self, "Open files", self.user_wd, filter) + if names[0]: + + self.file_name = names[0] + self.user_wd = os.path.dirname(self.file_name[0]) + self.load_stack() + + else: + self.statusbar_main.showMessage("No file has selected") + pass + + def load_stack(self): + + """load the image data from the selected file. + If the the choice is for multiple files stack will be created in a loop. + If single h5 file is selected the unpacking will be done with 'get_xrf_data' function in StackCalcs. + From the h5 the program can recognize the beamline. The exported stack will be normalized to I0. + + If the single tiff file is choosen tf.imread() is used. + + The output 'self.im_stack' is the unmodified data file + """ + + # Invalidate cache when loading new data + self.invalidate_cache() + + self.log_warning = False # for the Qmessage box in cb_log + self.image_roi2_flag = False + self.cb_log.setChecked(False) + self.cb_remove_edges.setChecked(False) + self.cb_norm.setChecked(False) + self.cb_smooth.setChecked(False) + self.cb_remove_outliers.setChecked(False) + self.cb_remove_bg.setChecked(False) + self.cb_rebin.setChecked(False) + self.cb_upscale.setChecked(False) + self.sb_xrange1.setValue(0) + self.sb_yrange1.setValue(0) + self.sb_zrange1.setValue(0) + + self.menuMask.setEnabled(True) + self.actionLoad_Energy.setEnabled(True) + self.actionSave_Energy_List.setEnabled(True) + self.actionSave_as.setEnabled(True) + + self.sb_zrange2.setMaximum(99999) + self.sb_xrange2.setMaximum(99999) + self.sb_yrange2.setMaximum(99999) + + self.statusbar_main.showMessage("Loading.. please wait...") + + if isinstance(self.file_name, list): + + all_images = [] + + for im_file in self.file_name: + img = tf.imread(im_file) + all_images.append(img) # row major image + self.im_stack = np.dstack(all_images).transpose((2, 0, 1)) + self.avgIo = 1 # I0 is only applicable to XRF h5 files + self.sb_zrange2.setValue(self.im_stack.shape[0]) + + else: + + if self.file_name.endswith(".h5"): + self.im_stack, mono_e, bl_name, self.avgIo = get_xrf_data(self.file_name) + self.statusbar_main.showMessage(f"Data from {bl_name}") + self.sb_zrange2.setValue(mono_e / 10) + self.energy = [] + + elif self.file_name.endswith(".tiff") or self.file_name.endswith(".tif"): + self.im_stack_ = tf.imread(self.file_name) + if self.im_stack_.ndim == 2: + self.im_stack = self.im_stack_.reshape(1, self.im_stack_.shape[0], self.im_stack_.shape[1]) + + else: + self.im_stack = self.im_stack_ + self.sb_zrange2.setValue(self.im_stack.shape[0]) + self.autoEnergyLoader() + self.energyUnitCheck() + self.avgIo = 1 + + else: + logger.error("Unknown data format") + + """ Fill the stack dimensions to the GUI and set the image dimensions as max values. + This prevent user from choosing higher image dimensions during a resizing event""" + + logger.info(f" loaded stack with {np.shape(self.im_stack)} from the file") + + try: + logger.info(f" Transposed to shape: {np.shape(self.im_stack)}") + self.init_dimZ, self.init_dimY, self.init_dimX = self.im_stack.shape + # Remove any previously set max value during a reload + + self.sb_xrange2.setValue(self.init_dimX) + self.sb_yrange2.setValue(self.init_dimY) + + except UnboundLocalError: + logger.error("No file selected") + pass + + self.view_stack() + logger.info("Stack displayed correctly") + self.update_stack_info() + + logger.info(f"completed image shape {np.shape(self.im_stack)}") + + try: + self.statusbar_main.showMessage(f"Loaded: {self.file_name}") + + except AttributeError: + self.statusbar_main.showMessage("New Stack is made from selected tiffs") + pass + + def browse_file(self): + """To open a file widow and choose the data file. + The filename will be used to load data using 'rest and load stack' function""" + + filename = QFileDialog().getOpenFileName( + self, "Select image data", self.user_wd, "image file(*.hdf *.h5 *tiff *tif )" + ) + self.file_name = str(filename[0]) + self.user_wd = os.path.dirname(self.file_name) + + # if user decides to cancel the file window gui returns to original state + if self.file_name: + self.disconnectImageActions() + self.isAReload = False + self.load_stack() + + else: + self.statusbar_main.showMessage("No file has selected") + pass + + def autoEnergyLoader(self): + + dir_, filename_ = os.path.split(self.file_name) + self.efilePath_name = os.path.join(dir_, os.path.splitext(filename_)[0] + ".txt") + self.efilePath_log = os.path.join(dir_, "maps_log_tiff.txt") + + if os.path.isfile(self.efilePath_name): + self.efilePath = self.efilePath_name + self.efileLoader() + self.statusbar_main.showMessage(f"Energy File detected {self.efilePath}") + + elif os.path.isfile(self.efilePath_log): + self.efilePath = self.efilePath_log + self.efileLoader() + self.statusbar_main.showMessage(f"Energy File detected {self.efilePath}") + + else: + self.efilePath = False + self.efileLoader() + + def update_stack_info(self): + z, y, x = np.shape(self.displayedStack) + self.sb_zrange2.setMaximum(z + self.sb_zrange1.value()) + self.sb_xrange2.setValue(x) + self.sb_xrange2.setMaximum(x) + self.sb_yrange2.setValue(y) + self.sb_yrange2.setMaximum(y) + logger.info("Stack info has been updated") + + # Image Transformations + + def crop_to_dim(self): + self.x1, self.x2 = self.sb_xrange1.value(), self.sb_xrange2.value() + self.y1, self.y2 = self.sb_yrange1.value(), self.sb_yrange2.value() + self.z1, self.z2 = self.sb_zrange1.value(), self.sb_zrange2.value() + + try: + self.displayedStack = remove_nan_inf( + self.displayedStack[self.z1 : self.z2, self.y1 : self.y2, self.x1 : self.x2] + ) + except Exception: + self.displayedStack = remove_nan_inf( + self.im_stack[self.z1 : self.z2, self.y1 : self.y2, self.x1 : self.x2] + ) + + def apply_crop_to_all(self): + dir_ = os.path.dirname(self.file_name) + tiffs = glob(dir_+"/*.tiff") + + self.x1, self.x2 = self.sb_xrange1.value(), self.sb_xrange2.value() + self.y1, self.y2 = self.sb_yrange1.value(), self.sb_yrange2.value() + self.z1, self.z2 = self.sb_zrange1.value(), self.sb_zrange2.value() + + #print(tiffs) + save_str = ' ' + for fname in tiffs: + print(fname) + im_array = tf.imread(fname) + im_name = os.path.join(dir_,os.path.basename(fname).split('.')[0]+"_cropped.tiff") + save_path = os.path.relpath(im_name, os.path.expanduser('~')) + if np.ndim(im_array) == 3: + tf.imwrite(im_name, im_array[self.z1 : self.z2, self.y1 : self.y2, self.x1 : self.x2]) + save_str+=f"\n{save_path} cropped from {im_array.shape} to (z,y,x):{self.z1}:{self.z2},{self.y1}:{self.y2},{self.x1}:{self.x2}" + logger.info(f"{save_path} saved") + elif np.ndim(im_array) == 2: + tf.imwrite(im_name, im_array[self.y1 : self.y2, self.x1 : self.x2]) + logger.info(f"{save_path} saved") + save_str+=f"\n{save_path} cropped from {im_array.shape} to (z,y,x):{self.y1}:{self.y2},{self.x1}:{self.x2}" + else: + pass + + print(f"{save_str = }") + + # Get the current date and time + current_datetime = datetime.datetime.now() + formatted_date_time = current_datetime.strftime("%Y-%m-%d_%H-%M-%S") + + # save crop settings + file_name = os.path.join(dir_, f"crop_log_{formatted_date_time}.txt") + + with open(file_name, 'w') as file: + file.write(save_str) + + #print(f"log saved as '{file_name}'") + + def transpose_stack(self): + self.displayedStack = self.displayedStack.T + self.update_spectrum() + self.update_spec_image_roi() + + # Alignement + + def loadAlignRefImage(self): + filename = QFileDialog().getOpenFileName(self, "Image Data", self.user_wd, "*.tiff *.tif") + file_name = str(filename[0]) + self.user_wd = os.path.dirname(file_name) + self.alignRefImage = tf.imread(file_name) + assert self.alignRefImage.shape == self.displayedStack.shape, "Image dimensions do not match" + self.refStackAvailable = True + self.rb_alignRefVoid.setChecked(False) + self.change_color_on_load(self.pb_load_align_ref) + + def stackRegistration(self): + + self.transformations = { + "TRANSLATION": StackReg.TRANSLATION, + "RIGID_BODY": StackReg.RIGID_BODY, + "SCALED_ROTATION": StackReg.SCALED_ROTATION, + "AFFINE": StackReg.AFFINE, + "BILINEAR": StackReg.BILINEAR, + } + + self.transformType = self.transformations[self.cb_alignTransform.currentText()] + self.alignReferenceImage = self.cb_alignRef.currentText() + self.alignRefStackVoid = self.rb_alignRefVoid.isChecked() + self.alignMaxIter = self.sb_maxIterVal.value() + + if self.cb_use_tmatFile.isChecked(): + + if len(self.loaded_tranform_file) > 0: + + self.displayedStack = align_with_tmat( + self.displayedStack, tmat_file=self.loaded_tranform_file, transformation=self.transformType + ) + logger.info("Aligned to the tranform File") + + else: + logger.error("No Tranformation File Loaded") + + elif self.cb_iterAlign.isChecked(): + + if not self.refStackAvailable: + self.alignRefImage = self.displayedStack + else: + pass + + self.displayedStack = align_stack_iter( + self.displayedStack, + ref_stack_void=False, + ref_stack=self.alignRefImage, + transformation=self.transformType, + method=("previous", "first"), + max_iter=self.alignMaxIter, + ) + + else: + if not self.refStackAvailable: + self.alignRefImage = self.displayedStack + + else: + pass + + self.displayedStack, self.tranform_file = align_stack( + self.displayedStack, + ref_image_void=True, + ref_stack=self.alignRefImage, + transformation=self.transformType, + reference=self.alignReferenceImage, + ) + logger.info("New Tranformation file available") + self.im_stack = self.displayedStack + + def exportAlignTransformation(self): + + + + file_name = QFileDialog().getSaveFileName( + self, + "Save Transformation File", + os.path.join(self.user_wd,"TranformationMatrix.npy"), + "text file (*.npy)" + ) + if file_name[0]: + np.save(file_name[0], self.tranform_file) + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def importAlignTransformation(self): + file_name = QFileDialog().getOpenFileName(self, "Open Transformation File", self.user_wd, "text file (*.npy)") + if file_name[0]: + self.loaded_tranform_file = np.load(file_name[0]) + self.cb_use_tmatFile.setChecked(True) + self.user_wd = os.path.dirname(file_name[0]) + logger.info("Transformation File Loaded") + else: + pass + + def loadSplashScreen(self): + self.splash = LoadingScreen() + + px = self.geometry().x() + py = self.geometry().y() + ph = self.geometry().height() + pw = self.geometry().width() + dw = self.splash.width() + dh = self.splash.height() + new_x, new_y = px + (0.5 * pw) - dw, py + (0.5 * ph) - dh + self.splash.setGeometry(int(new_x), int(new_y), int(dw), int(dh)) + + self.splash.show() + + def reloadImageStack(self): + self.isAReload = True + self.load_stack() + + def schedule_update(self): + """Schedule a debounced update to prevent rapid successive processing.""" + self.update_timer.stop() + self.update_timer.start(300) # 300ms delay + + def _do_update_stack(self): + """Internal method called by timer to perform the actual update.""" + self.replot_image() + + def invalidate_cache(self): + """Clear all cached results when base stack changes.""" + self._cache = { + 'resize': {}, + 'outliers': {}, + 'smooth': {}, + 'last_params': {} + } + logger.info("Cache invalidated") + + def update_stack(self): + """Update the displayed stack with all selected modifications. + + Uses caching to avoid redundant processing of expensive operations. + + OPERATION ORDER (FIXED): + The operations are applied in a specific order optimized for: + 1. Performance (reduce data size early) + 2. Correctness (irreversible operations like log/edge removal in proper sequence) + + Order: + 1. Resize/Rebin - Reduces data size for faster subsequent operations + 2. Remove Edges - Reduces data size, IRREVERSIBLE + 3. Remove Outliers - Pixel-level operation + 4. Remove Background - Threshold-based filtering + 5. Log Transform - IRREVERSIBLE, changes data scale + 6. Smoothing - Works on any scale + 7. Normalization - Final scaling step + + Note: If you need a different order, you'll need to apply operations + manually in sequence rather than using checkboxes simultaneously. + """ + # Show busy cursor during processing + QApplication.setOverrideCursor(QtCore.Qt.CursorShape.WaitCursor) + + try: + self.displayedStack = self.im_stack + self.crop_to_dim() + + # OPTIMIZATION 1: Resize/rebin first to reduce data size for subsequent operations + if self.cb_rebin.isChecked(): + self.cb_upscale.setChecked(False) + self.sb_scaling_factor.setEnabled(True) + scaling_factor = self.sb_scaling_factor.value() + + # Check cache + cache_key = f"rebin_{scaling_factor}" + if cache_key in self._cache['resize']: + self.displayedStack = self._cache['resize'][cache_key] + logger.info(f"Using cached rebin result (factor={scaling_factor})") + else: + self.displayedStack = resize_stack(self.displayedStack, scaling_factor=scaling_factor) + self._cache['resize'][cache_key] = self.displayedStack.copy() + self.update_stack_info() + + elif self.cb_upscale.isChecked(): + self.cb_rebin.setChecked(False) + self.sb_scaling_factor.setEnabled(True) + scaling_factor = self.sb_scaling_factor.value() + + # Check cache + cache_key = f"upscale_{scaling_factor}" + if cache_key in self._cache['resize']: + self.displayedStack = self._cache['resize'][cache_key] + logger.info(f"Using cached upscale result (factor={scaling_factor})") + else: + self.displayedStack = resize_stack( + self.displayedStack, upscaling=True, scaling_factor=scaling_factor + ) + self._cache['resize'][cache_key] = self.displayedStack.copy() + self.update_stack_info() + + # OPTIMIZATION 2: Remove edges early to reduce data size + if self.cb_remove_edges.isChecked(): + self.displayedStack = remove_edges(self.displayedStack) + logger.info(f"Removed edges, new shape {self.displayedStack.shape}") + self.update_stack_info() + + # Outlier removal with caching + if self.cb_remove_outliers.isChecked(): + self.hs_nsigma.setEnabled(True) + nsigma = self.hs_nsigma.value() / 10 + + # Only cache if this is the ONLY operation (no subsequent ops that would be skipped) + # Otherwise the cache would bypass operations like log transform + self.displayedStack = remove_hot_pixels(self.displayedStack, NSigma=nsigma) + + self.label_nsigma.setText(str(nsigma)) + logger.info(f"Removing Outliers with NSigma {nsigma}") + else: + self.hs_nsigma.setEnabled(False) + + # Background removal + if self.cb_remove_bg.isChecked(): + self.hs_bg_threshold.setEnabled(True) + logger.info("Removing background") + bg_threshold = self.hs_bg_threshold.value() + self.label_bg_threshold.setText(str(bg_threshold) + "%") + self.displayedStack = clean_stack(self.displayedStack, auto_bg=False, bg_percentage=bg_threshold) + else: + self.hs_bg_threshold.setEnabled(False) + + # Log transform + if self.cb_log.isChecked(): + self.displayedStack = remove_nan_inf(np.log10(self.displayedStack)) + logger.info("Log Stack is in use") + + # Smoothing (removed caching to prevent pipeline issues) + if self.cb_smooth.isChecked(): + self.hs_smooth_size.setEnabled(True) + window = self.hs_smooth_size.value() + if window % 2 == 0: + window += 1 + + self.displayedStack = smoothen(self.displayedStack, w_size=window) + + self.smooth_winow_size.setText("Window size: " + str(window)) + logger.info("Spectrum Smoothening Applied") + else: + self.hs_smooth_size.setEnabled(False) + + # Normalization (fast operation, no caching needed) + if self.cb_norm.isChecked(): + logger.info("Normalizing spectra") + self.displayedStack = normalize(self.displayedStack, norm_point=-1) + + logger.info("Updated image is in use") + + finally: + # Always restore cursor + QApplication.restoreOverrideCursor() + + # ImageView + + def view_stack(self): + + if not self.im_stack.ndim == 3: + raise ValueError("stack should be an ndarray with ndim == 3") + else: + self.update_stack() + # self.StackUpdateThread() + + try: + self.image_view.removeItem(self.image_roi_math) + except Exception: + pass + + (self.dim1, self.dim2, self.dim3) = self.displayedStack.shape + self.image_view.setImage(self.displayedStack) + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + self.image_view.setPredefinedGradient("viridis") + self.image_view.setCurrentIndex(self.dim1 // 2) + if len(self.energy) == 0: + self.energy = np.arange(self.z1, self.z2) * 10 + logger.info("Arbitary X-axis used in the plot for XANES") + self.sz = np.max( + [int(self.dim2 * 0.1), int(self.dim3 * 0.1)] + ) # size of the roi set to be 10% of the image area + + self.stack_center = self.energy[len(self.energy) // 2] + self.stack_width = (self.energy.max() - self.energy.min()) // 10 + self.spec_roi = pg.LinearRegionItem( + values=(self.stack_center - self.stack_width, self.stack_center + self.stack_width) + ) + + # a second optional ROI for calculations follow + self.image_roi_math = pg.PolyLineROI( + [[0, 0], [0, self.sz], [self.sz, self.sz], [self.sz, 0]], + pos=(int(self.dim3 // 3), int(self.dim2 // 3)), + pen="r", + closed=True, + removable=True, + ) + + self.spec_roi_math = pg.LinearRegionItem( + values=(self.stack_center - self.stack_width - 10, self.stack_center + self.stack_width - 10), + pen="r", + brush=QtGui.QColor(0, 255, 200, 50), + ) + self.spec_lo_m_idx = self.spec_hi_m_idx = 0 + + self.setImageROI() + self.update_spectrum() + self.update_image_roi() + + if not self.isAReload: + # image connections + self.image_view.mousePressEvent = self.getPointSpectrum + self.pb_apply_spec_calc.clicked.connect(self.spec_roi_calc) + self.rb_math_roi.clicked.connect(self.update_spectrum) + self.pb_add_roi_2.clicked.connect(self.math_img_roi_flag) + self.image_roi_math.sigRegionChangeFinished.connect(self.image_roi_calc) + self.pb_apply_img_calc.clicked.connect(self.image_roi_calc) + + self.spec_roi.sigRegionChanged.connect(self.update_image_roi) + self.spec_roi_math.sigRegionChangeFinished.connect(self.spec_roi_calc) + + [ + rbs.clicked.connect(self.setImageROI) + for rbs in [self.rb_poly_roi, self.rb_elli_roi, self.rb_rect_roi, self.rb_line_roi, self.rb_circle_roi] + ] + + def disconnectImageActions(self): + for btns in [self.pb_apply_spec_calc, self.rb_math_roi, self.pb_add_roi_2, self.pb_apply_img_calc]: + try: + btns.disconnect() + except Exception: + pass + + def select_elist(self): + self.energyFileChooser() + self.efileLoader() + self.energyUnitCheck() + self.view_stack() + + def efileLoader(self): + + if self.efilePath: + + if str(self.efilePath).endswith("log_tiff.txt"): + self.energy = energy_from_logfile(logfile=str(self.efilePath)) + logger.info("Log file from pyxrf processing") + + else: + self.energy = np.loadtxt(str(self.efilePath)) + self.change_color_on_load(self.pb_elist_xanes) + logger.info("Energy file loaded") + + else: + self.statusbar_main.showMessage("No Energy List Selected, Setting Arbitary Axis") + self.energy = np.arange(self.im_stack.shape[0]) + logger.info("Arbitary Energy Axis") + + # assert len(self.energy) == self.dim1, "Number of Energy Points is not equal to stack length" + + def energyUnitCheck(self): + + if np.max(self.energy) < 100: + self.cb_kev_flag.setChecked(True) + self.energy *= 1000 + + else: + self.cb_kev_flag.setChecked(False) + + def select_ref_file(self): + self.pb_xanes_fit.setEnabled(True) + self.ref_names = [] + file_name = QFileDialog().getOpenFileName(self, "Open reference file", self.user_wd, "text file (*.csv *.nor)") + if file_name[0]: + if file_name[0].endswith(".nor"): + self.refs, self.ref_names = create_df_from_nor_try2(athenafile=file_name[0]) + self.change_color_on_load(self.pb_ref_xanes) + + elif file_name[0].endswith(".csv"): + self.refs = pd.read_csv(file_name[0]) + self.ref_names = list(self.refs.keys()) + + self.change_color_on_load(self.pb_ref_xanes) + + self.user_wd = os.path.dirname(file_name[0]) + + else: + logger.error("No file selected") + pass + + logger.info(f"{self.refs.shape = }") + + self.plt_xanes_refs() + + def plt_xanes_refs(self): + + try: + self.ref_plot.close() + except Exception: + pass + + self.ref_plot = plot(title="Reference Standards") + self.ref_plot.setLabel("bottom", "Energy") + self.ref_plot.setLabel("left", "Intensity") + self.ref_plot.addLegend() + + for n in range(np.shape(self.refs)[1]): + + if not n == 0: + self.ref_plot.plot( + self.refs.values[:, 0], + self.refs.values[:, n], + pen=pg.mkPen(self.plt_colors[n - 1], width=self.plotWidth), + name=self.ref_names[n], + ) + + def getPointSpectrum(self, event): + if event.type() == QtCore.QEvent.Type.MouseButtonDblClick: + if event.button() == QtCore.Qt.MouseButton.LeftButton: + self.xpixel = int(self.image_view.view.mapSceneToView(event.pos().toPointF()).x()) - 1 + zlim, ylim, xlim = self.displayedStack.shape + + if self.xpixel > xlim: + self.xpixel = xlim - 1 + + self.ypixel = int(self.image_view.view.mapSceneToView(event.pos().toPointF()).y()) - 1 + if self.ypixel > ylim: + self.ypixel = ylim - 1 + self.spectrum_view.addLegend() + self.point_spectrum = self.displayedStack[:, self.ypixel, self.xpixel] + self.spectrum_view.plot( + self.xdata, + self.point_spectrum, + clear=True, + pen=pg.mkPen(pg.mkColor(0, 0, 255, 255), width=self.plotWidth), + symbol="o", + symbolSize=6, + symbolBrush="r", + name=f"Point Spectrum; x= {self.xpixel}, y= {self.ypixel}", + ) + + self.spectrum_view.addItem(self.spec_roi) + + self.statusbar_main.showMessage(f"{self.xpixel} and {self.ypixel}") + + def setImageROI(self): + + self.lineROI = pg.LineSegmentROI([[int(self.dim3 // 2), int(self.dim2 // 2)], [self.sz, self.sz]], pen="r") + + self.rectROI = pg.RectROI( + [int(self.dim3 // 2), int(self.dim2 // 2)], + [self.sz, self.sz], + pen="w", + maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), + ) + + self.rectROI.addTranslateHandle([0, 0], [2, 2]) + self.rectROI.addRotateHandle([0, 1], [2, 2]) + + self.ellipseROI = pg.EllipseROI( + [int(self.dim3 // 2), int(self.dim2 // 2)], + [self.sz, self.sz], + pen="w", + maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), + ) + + self.circleROI = pg.CircleROI( + [int(self.dim3 // 2), int(self.dim2 // 2)], + [self.sz, self.sz], + pen="w", + maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), + ) # pos and size + + self.polyLineROI = pg.PolyLineROI( + [[0, 0], [0, self.sz], [self.sz, self.sz], [self.sz, 0]], + pos=(int(self.dim3 // 2), int(self.dim2 // 2)), + maxBounds=QtCore.QRectF(0, 0, self.dim3, self.dim2), + closed=True, + removable=True, + ) + + self.rois = { + "rb_line_roi": self.lineROI, + "rb_rect_roi": self.rectROI, + "rb_circle_roi": self.circleROI, + "rb_elli_roi": self.ellipseROI, + "rb_poly_roi": self.polyLineROI, + } + + button_name = self.sender() + + if button_name.objectName() in self.rois.keys(): + self.roi_preference = button_name.objectName() + + else: + self.roi_preference = "rb_rect_roi" # default + + try: + self.image_view.removeItem(self.image_roi) + + except Exception: + pass + + # ROI settings for image, used polyline roi with non rectangular shape + + self.image_roi = self.rois[self.roi_preference] + self.image_view.addItem(self.image_roi) + self.image_roi.sigRegionChanged.connect(self.update_spectrum) + + def replot_image(self): + self.update_stack() + self.update_spectrum() + self.update_image_roi() + + def update_spec_roi_values(self): + self.stack_center = int(self.energy[len(self.energy) // 2]) + self.stack_width = int((self.energy.max() - self.energy.min()) * 0.05) + self.spec_roi.setBounds([self.xdata[0], self.xdata[-1]]) # if want to set bounds for the spec roi + self.spec_roi_math.setBounds([self.xdata[0], self.xdata[-1]]) + + def update_spectrum(self): + + # set x-axis values; array taken from energy values, if clipped z box values will update the array + self.xdata = self.energy[self.sb_zrange1.value() : self.sb_zrange2.value()] + + # get the cropped stack from ROI region; pyqtgraph function is used + self.roi_img_stk = self.image_roi.getArrayRegion( + self.displayedStack, self.image_view.imageItem, axes=(1, 2) + ) + + posx, posy = self.image_roi.pos() + self.le_roi.setText(str(int(posx)) + ":" + str(int(posy))) + + # display the ROI features in the line edit boxes + if self.roi_img_stk.ndim == 3: + sizex, sizey = self.roi_img_stk.shape[1], self.roi_img_stk.shape[2] + self.le_roi_size.setText(str(sizex) + "," + str(sizey)) + self.mean_spectra = get_mean_spectra(self.roi_img_stk) + + elif self.roi_img_stk.ndim == 2: + sizex, sizey = self.roi_img_stk.shape[0], self.roi_img_stk.shape[1] + self.le_roi_size.setText(str(sizex) + "," + str(sizey)) + self.mean_spectra = self.roi_img_stk.mean(-1) + + self.spectrum_view.addLegend() + + try: + self.spectrum_view.plot( + self.xdata, + self.mean_spectra, + pen=pg.mkPen(pg.mkColor(5, 255, 5, 255), width=self.plotWidth), + clear=True, + symbol="o", + symbolSize=6, + symbolBrush="r", + name="ROI Spectrum", + ) + except Exception: + self.spectrum_view.plot( + self.mean_spectra, + clear=True, + pen=pg.mkPen(pg.mkColor(5, 255, 5, 255), width=self.plotWidth), + symbol="o", + symbolSize=6, + symbolBrush="r", + name="ROI Spectrum", + ) + + if self.energy[-1] > 1000: + self.e_unit = "eV" + else: + self.e_unit = "keV" + + self.spectrum_view.setLabel("bottom", "Energy", self.e_unit) + self.spectrum_view.setLabel("left", "Intensity", "A.U.") + self.spectrum_view.addItem(self.spec_roi) + self.update_spec_roi_values() + self.math_roi_flag() + + def update_image_roi(self): + self.spec_lo, self.spec_hi = self.spec_roi.getRegion() + self.spec_lo_idx = (np.abs(self.energy - self.spec_lo)).argmin() + self.spec_hi_idx = (np.abs(self.energy - self.spec_hi)).argmin() + self.le_spec_roi.setText(str(int(self.spec_lo)) + ":" + str(int(self.spec_hi))) + self.le_spec_roi_size.setText(str(int(self.spec_hi - self.spec_lo))) + self.update_spec_roi_values() + self.stackIndexToNames() + + try: + if int(self.spec_lo_idx) == int(self.spec_hi_idx): + self.disp_img = self.displayedStack[int(self.spec_hi_idx), :, :] + + else: + self.disp_img = self.displayedStack[int(self.spec_lo_idx) : int(self.spec_hi_idx), :, :].mean(0) + + self.image_view.setImage(self.disp_img) + self.statusbar_main.showMessage(f"Image Display is {self.corrImg1}") + except Exception: + logger.warning("Indices are out of range; Image cannot be created") + pass + + def set_spec_roi(self): + self.spec_lo_, self.spec_hi_ = int(self.sb_roi_spec_s.value()), int(self.sb_roi_spec_e.value()) + self.spec_lo_idx_ = (np.abs(self.energy - self.spec_lo_)).argmin() + self.spec_hi_idx_ = (np.abs(self.energy - self.spec_hi_)).argmin() + self.spec_roi.setRegion((self.xdata[self.spec_lo_idx_], self.xdata[self.spec_hi_idx_])) + self.update_image_roi() + + def math_roi_flag(self): + if self.rb_math_roi.isChecked(): + self.rb_math_roi.setStyleSheet("color : green") + self.spectrum_view.addItem(self.spec_roi_math) + else: + self.spectrum_view.removeItem(self.spec_roi_math) + + def spec_roi_calc(self): + + self.spec_lo_m, self.spec_hi_m = self.spec_roi_math.getRegion() + self.spec_lo_m_idx = (np.abs(self.energy - self.spec_lo_m)).argmin() + self.spec_hi_m_idx = (np.abs(self.energy - self.spec_hi_m)).argmin() + + if int(self.spec_lo_idx) == int(self.spec_hi_idx): + self.img1 = self.displayedStack[int(self.spec_hi_idx), :, :] + + else: + self.img1 = self.displayedStack[int(self.spec_lo_idx) : int(self.spec_hi_idx), :, :].mean(0) + + if int(self.spec_lo_m_idx) == int(self.spec_hi_m_idx): + self.img2 = self.displayedStack[int(self.spec_hi_m_idx), :, :] + + else: + self.img2 = self.displayedStack[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx), :, :].mean(0) + + if self.cb_roi_operation.currentText() == "Correlation Plot": + self.correlation_plot() + + else: + calc = {"Divide": np.divide, "Subtract": np.subtract, "Add": np.add} + self.disp_img = remove_nan_inf(calc[self.cb_roi_operation.currentText()](self.img1, self.img2)) + self.image_view.setImage(self.disp_img) + + def math_img_roi_flag(self): + + button_name = self.sender().text() + logger.info(f"{button_name}") + if button_name == "Add ROI_2": + self.image_view.addItem(self.image_roi_math) + self.pb_add_roi_2.setText("Remove ROI_2") + self.image_roi2_flag = 1 + elif button_name == "Remove ROI_2": + self.image_view.removeItem(self.image_roi_math) + self.pb_add_roi_2.setText("Add ROI_2") + self.image_roi2_flag = 0 + + else: + pass + logger.error("Unknown signal for second ROI") + + def image_roi_calc(self): + + if self.image_roi2_flag == 1: + self.calc = {"Divide": np.divide, "Subtract": np.subtract, "Add": np.add} + self.update_spec_image_roi() + else: + logger.error("No ROI2 found") + return + + def update_spec_image_roi(self): + + self.math_roi_reg = self.image_roi_math.getArrayRegion( + self.displayedStack, self.image_view.imageItem, axes=(1, 2) + ) + if self.math_roi_reg.ndim == 3: + + self.math_roi_spectra = get_mean_spectra(self.math_roi_reg) + + elif self.roi_img_stk.ndim == 2: + self.math_roi_spectra = self.math_roi_reg.mean(-1) + + if self.cb_img_roi_action.currentText() in self.calc.keys(): + + calc_spec = self.calc[self.cb_img_roi_action.currentText()](self.mean_spectra, self.math_roi_spectra) + self.spectrum_view.addLegend() + self.spectrum_view.plot( + self.xdata, + calc_spec, + clear=True, + pen=pg.mkPen("m", width=2), + name=self.cb_img_roi_action.currentText() + "ed", + ) + self.spectrum_view.plot(self.xdata, self.math_roi_spectra, pen=pg.mkPen("y", width=2), name="ROI2") + self.spectrum_view.plot(self.xdata, self.mean_spectra, pen=pg.mkPen("g", width=2), name="ROI1") + + elif self.cb_img_roi_action.currentText() == "Compare": + self.spectrum_view.plot( + self.xdata, self.math_roi_spectra, pen=pg.mkPen("y", width=2), clear=True, name="ROI2" + ) + self.spectrum_view.plot(self.xdata, self.mean_spectra, pen=pg.mkPen("g", width=2), name="ROI1") + + self.spectrum_view.addItem(self.spec_roi) + + def displayStackInfo(self): + + try: + + if isinstance(self.file_name, list): + info = f"Folder; {os.path.dirname(self.file_name[0])} \n" + for n, name in enumerate(self.file_name): + info += f"{n}: {os.path.basename(name)} \n" + + # info = f'Stack order; {[name for name in enumerate(self.file_name)]}' + else: + info = f"Stack; {self.file_name}" + + self.infoWindow = StackInfo(str(info)) + self.infoWindow.show() + + except AttributeError: + self.statusbar_main.showMessage("Warning: No Image Data Loaded") + + def stackIndexToNames(self): + # create list of tiff file names for virtutal stack for plot axes + self.elemFileName = [] + + if isinstance(self.file_name, list): + for name in self.file_name: + self.elemFileName.append(os.path.basename(name).split(".")[0]) + + logger.info(f" Virtual Stack - list of image names; {self.elemFileName}") + + # if the roi focus on one frame, Note that this slicing excludes the last index + if int(self.spec_lo_idx) == int(self.spec_hi_idx): + self.corrImg1 = str(self.elemFileName[int(self.spec_lo_idx)]) + else: + self.corrImg1 = self.elemFileName[int(self.spec_lo_idx) : int(self.spec_hi_idx)] + if len(self.corrImg1) > 1: + self.corrImg1 = f"Sum of {self.corrImg1} " + + if int(self.spec_lo_m_idx) == int(self.spec_hi_m_idx): + self.corrImg2 = str(self.elemFileName[int(self.spec_lo_m_idx)]) + + else: + self.corrImg2 = self.elemFileName[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx)] + + if len(self.corrImg2) > 1: + self.corrImg2 = f"Sum of {self.corrImg2}" + + logger.info( + f"Correlation stack {int(self.spec_lo_idx)}:{int(self.spec_hi_idx)} with " + f"{int(self.spec_lo_m_idx)}:{int(self.spec_hi_m_idx)}" + ) + + logger.info(f" Virtual Stack; corrlation plot of {self.corrImg1} vs {self.corrImg2}") + else: + self.corrImg1 = ( + f" Sum of {os.path.basename(self.file_name).split('.')[0]}_{int(self.spec_lo_idx)} " + f"to {int(self.spec_hi_idx)}" + ) + self.corrImg2 = ( + f" Sum of {os.path.basename(self.file_name).split('.')[0]}_{int(self.spec_lo_m_idx)} " + f"to {int(self.spec_hi_m_idx)}" + ) + # logger.info(f" corrlation plot of {self.corrImg1} vs {self.corrImg2}") + + def stackToCSV(self): + + self.stackIndexToNames() + self.imageDf = pd.DataFrame() + if len(self.elemFileName) == len(self.displayedStack): + for name, image in zip(self.elemFileName, self.displayedStack): + self.imageDf[f'{name}'] = image.flatten() + # print(self.imageDf.head()) + else: + self.imageDf = image_to_pandas2(self.displayedStack) + + file_name = QFileDialog().getSaveFileName(self, + "Save CSV Data", + os.path.join(self.user_wd,'image_2DArray.csv'), + 'file (*csv)') + if file_name[0]: + self.imageDf.to_csv(path_or_buf=file_name[0]) + self.user_wd = os.path.dirname(file_name[0]) + self.statusbar_main.showMessage(f"Data saved to {file_name[0]}") + else: + pass + + def correlation_plot(self): + self.stackIndexToNames() + + self.statusbar_main.showMessage(f"Correlation of {self.corrImg1} with {self.corrImg2}") + + if self.rb_roiRegionOnly.isChecked(): + self.roi_mask = self.image_roi.getArrayRegion( + self.displayedStack, self.image_view.imageItem, axes=(1, 2) + ) + self.roi_img1 = np.mean(self.roi_mask[int(self.spec_lo_idx) : int(self.spec_hi_idx)], axis=0) + self.roi_img2 = np.mean(self.roi_mask[int(self.spec_lo_m_idx) : int(self.spec_hi_m_idx)], axis=0) + self.scatter_window = ScatterPlot( + self.roi_img1, self.roi_img2, (str(self.corrImg1), str(self.corrImg2)) + ) + + else: + + self.scatter_window = ScatterPlot(self.img1, self.img2, (str(self.corrImg1), str(self.corrImg2))) + + self.scatter_window.show() + + def plotCorrelationsAllCombinations(self): + + print("Plotting all correlations") + self.stackIndexToNames() + allElemCombNum = list(combinations(np.arange(len(self.elemFileName)), 2)) + + self.scW1 = self.scW2 = self.scW3 = self.scW4 = self.scW5 = None + self.scW6 = self.scW7 = self.scW8 = self.scW9 = self.scW10 = None + + self.scWindowList = [ + self.scW1, + self.scW2, + self.scW3, + self.scW4, + self.scW5, + self.scW6, + self.scW7, + self.scW8, + self.scW9, + self.scW10, + ] + self.scWindowDict = { + 1: self.scW1, + 2: self.scW2, + 3: self.scW3, + 4: self.scW4, + 5: self.scW5, + 6: self.scW6, + 7: self.scW7, + 8: self.scW8, + 9: self.scW9, + 10: self.scW10, + } + + if len(allElemCombNum) > len(self.scWindowDict): + + reply = QMessageBox.warning( + self, + "Plot Window Limit", + f"The number of combination exceeds " + f"maxiumum number of " + f"plot windows. First {len(self.scWindowDict)} " + f"combinations will be plotted. \n Proceed?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + + if reply == QMessageBox.StandardButton.Yes: + + for i, pair in enumerate(allElemCombNum): + im1 = self.displayedStack[pair[0]] + im2 = self.displayedStack[pair[1]] + im1Name = self.elemFileName[pair[0]] + im2Name = self.elemFileName[pair[1]] + + self.scWindowDict[i] = ScatterPlot(im1, im2, (str(im1Name), str(im2Name))) + self.scWindowDict[i].show() + + if reply == QMessageBox.StandardButton.No: + return + + else: + + for i, pair in enumerate(allElemCombNum): + im1 = self.displayedStack[pair[0]] + im2 = self.displayedStack[pair[1]] + im1Name = self.elemFileName[pair[0]] + im2Name = self.elemFileName[pair[1]] + + self.scWindowDict[i] = ScatterPlot(im1, im2, (str(im1Name), str(im2Name))) + self.scWindowDict[i].show() + + def getROIMask(self): + self.roi_mask = self.image_roi.getArrayRegion(self.displayedStack, self.image_view.imageItem, axes=(1, 2)) + self.newWindow = singleStackViewer(self.roi_mask) + self.newWindow.show() + + def save_stack(self, method="raw"): + + # self.update_stack() + file_name = QFileDialog().getSaveFileName( + self, + "Save image data", + os.path.join(self.user_wd,"image_data.tiff"), + "image file(*tiff *tif )" + ) + if file_name[0]: + if method == "raw": + + tf.imwrite(file_name[0], self.displayedStack) + logger.info(f"Updated Image Saved: {file_name[0]}") + self.statusbar_main.showMessage(f"Updated Image Saved: {file_name[0]}") + elif method == "sum": + tf.imwrite(file_name[0], np.sum(self.displayedStack, axis=0)) + + elif method == "mean": + tf.imwrite(file_name[0], np.mean(self.displayedStack, axis=0)) + + self.user_wd = os.path.dirname(file_name[0]) + + else: + self.statusbar_main.showMessage("Saving cancelled") + logger.info(f"Save failed: {file_name[0]}") + pass + + def save_disp_img(self): + file_name = QFileDialog().getSaveFileName(self, + "Save image data", + os.path.join(self.user_wd,"image.tiff"), + "image file(*tiff *tif )") + if file_name[0]: + tf.imwrite(file_name[0], self.disp_img) + self.statusbar_main.showMessage(f"Image Saved to {file_name[0]}") + self.user_wd = os.path.dirname(file_name[0]) + logger.info(f"Updated Image Saved: {file_name[0]}") + + else: + logger.error("No file to save") + self.statusbar_main.showMessage("Saving cancelled") + pass + + def getLivePlotData(self): + try: + + data = np.squeeze([c.getData() for c in self.spectrum_view.plotItem.curves]) + # print(np.shape(data)) + if data.ndim == 2: + self.mu_ = data[1] + self.e_ = data[0] + elif data.ndim == 3: + e_mu = data[0, :, :] + self.mu_ = e_mu[1] + self.e_ = e_mu[0] + + else: + logger.error(f" Data shape of {data.ndim} is not supported") + pass + except AttributeError: + logger.error("No data loaded") + pass + + def addSpectrumToCollector(self): + self.getLivePlotData() + self.spectrum_view_collect.plot(self.e_, self.mu_, name="ROI Spectrum") + self.spectrum_view_collect.setLabel("bottom", "Energy", self.e_unit) + self.spectrum_view_collect.setLabel("left", "Intensity", "A.U.") + + def findEo(self): + try: + self.getLivePlotData() + e0_init = self.e_[np.argmax(np.gradient(self.mu_))] + self.dsb_norm_Eo.setValue(e0_init) + + except AttributeError: + logger.error("No data loaded") + pass + + def initNormVals(self): + self.getLivePlotData() + e0_init = self.e_[np.argmax(np.gradient(self.mu_))] + pre_edge, post_edge, norm = xanesNormalization( + self.e_, + self.mu_, + e0=e0_init, + nnorm=1, + nvict=0, + ) + # xanesNormalization now returns arrays, not scalar bounds + # Set reasonable default bounds for the UI based on standard XANES practice + self.dsb_norm_pre1.setValue(-50) # 50 eV before edge + self.dsb_norm_pre2.setValue(-20) # 20 eV before edge + self.dsb_norm_post1.setValue(50) # 50 eV after edge + self.dsb_norm_post2.setValue(150) # 150 eV after edge + self.dsb_norm_Eo.setValue(e0_init) + + def getNormParams(self): + self.getLivePlotData() + eo_ = self.dsb_norm_Eo.value() + pre1_, pre2_ = self.dsb_norm_pre1.value(), self.dsb_norm_pre2.value() + norm1_, norm2_ = self.dsb_norm_post1.value(), self.dsb_norm_post2.value() + norm_order = self.sb_norm_order.value() + + return eo_, pre1_, pre2_, norm1_, norm2_, norm_order + + def exportNormParams(self): + self.xanesNormParam = {} + eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() + self.xanesNormParam["E0"] = eo_ + self.xanesNormParam["pre1"] = pre1_ + self.xanesNormParam["pre2"] = pre2_ + self.xanesNormParam["post1"] = norm1_ + self.xanesNormParam["post2"] = norm2_ + self.xanesNormParam["norm_order"] = norm_order + + file_name = QtWidgets.QFileDialog().getSaveFileName( + self, + "Save XANES Norm Params", + os.path.join(self.user_wd,"xanes_norm_params.csv"), + "csv file(*csv)" + ) + if file_name[0]: + pd.DataFrame(self.xanesNormParam, index=[0]).to_csv(file_name[0]) + self.user_wd = os.path.dirname(file_name[0]) + + else: + pass + + def importNormParams(self): + + file_name = QtWidgets.QFileDialog().getOpenFileName( + self, "Open a XANES Norm File", self.user_wd, "csv file(*csv);;all_files (*)" + ) + + if file_name[0]: + xanesNormParam = pd.read_csv(file_name[0]) + self.dsb_norm_Eo.setValue(xanesNormParam["E0"]) + self.dsb_norm_pre1.setValue(xanesNormParam["pre1"]) + self.dsb_norm_pre2.setValue(xanesNormParam["pre2"]) + self.dsb_norm_post1.setValue(xanesNormParam["post1"]) + self.dsb_norm_post2.setValue(xanesNormParam["post2"]) + self.sb_norm_order.setValue(xanesNormParam["norm_order"]) + self.user_wd = os.path.dirname(file_name[0]) + + def nomalizeLiveSpec(self): + eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() + self.spectrum_view.clear() + colors = np.array(("c", "r", "m")) + + if self.cb_mback.isChecked(): + pass + # f2, normXANES = xanesNormalization( + # self.e_, + # self.mu_, + # e0=eo_, + # nnorm=norm_order, + # nvict=0, + # pre1=pre1_, + # pre2=pre2_, + # norm1=norm1_, + # norm2=norm2_, + # useFlattened=self.cb_xanes_flat.isChecked(), + # ) + + # names = np.array(("matched mu(E)", "f2")) + # data_array = np.array((normXANES, f2)) + + else: + pre_line, post_line, normXANES = xanesNormalization( + self.e_, + self.mu_, + e0=eo_, + nnorm=norm_order, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_, + useFlattened=self.cb_xanes_flat.isChecked() + ) + + names = np.array(("Spectrum", "Pre", "Post")) + data_array = np.array((self.mu_, pre_line, post_line)) + + + for data, clr, name in zip(data_array, colors, names): + self.spectrum_view.plot(self.e_, data, pen=pg.mkPen(clr, width=self.plotWidth), name=name) + + self.spectrum_view_norm.plot( + self.e_, normXANES, clear=True, pen=pg.mkPen(self.plt_colors[-1], width=self.plotWidth)) + + self.spectrum_view_norm.setLabel("bottom", "Energy", self.e_unit) + self.spectrum_view_norm.setLabel("left", "Norm. Intensity", "A.U.") + + def normalizeStack(self): + self.getLivePlotData() + eo_, pre1_, pre2_, norm1_, norm2_, norm_order = self.getNormParams() + + self.im_stack = self.displayedStack = xanesNormStack( + self.e_, + self.displayedStack, + e0=eo_, + nnorm=norm_order, + nvict=0, + pre1=pre1_, + pre2=pre2_, + norm1=norm1_, + norm2=norm2_, + ignorePostEdgeNorm=self.cb_xanes_postedge.isChecked() + ) + # self.im_stack = self.displayedStack + + def transposeStack(self): + self.im_stack = self.displayedStack = np.transpose(self.displayedStack, (2, 1, 0)) + + def swapStackXY(self): + self.im_stack = self.displayedStack = np.transpose(self.displayedStack, (0, 2, 1)) + + def removeROIBGStack(self): + self.displayedStack = subtractBackground(self.displayedStack, self.mean_spectra) + + def resetCollectorSpec(self): + pass + + def saveCollectorPlot(self): + exporter = pg.exporters.CSVExporter(self.spectrum_view_collect.plotItem) + #exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, "save spectra", self.user_wd, "spectra (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.user_wd = os.path.dirname(file_name[0]) + else: + self.statusbar_main.showMessage("Saving cancelled") + pass + + def save_disp_spec(self): + + exporter = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + #exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, + "save spectrum", + os.path.join(self.user_wd,"spectrum.csv"), + "spectra (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.user_wd = os.path.dirname(file_name[0]) + else: + self.statusbar_main.showMessage("Saving cancelled") + pass + + def saveEnergyList(self): + file_name = QFileDialog().getSaveFileName(self, + "save energy list", + os.path.join(self.user_wd,"energy_list.txt"), + "text file (*txt)") + if file_name[0]: + np.savetxt(file_name[0], self.xdata, fmt="%.4f") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def pca_scree_(self): + logger.info("Process started..") + self.update_stack() + var = pca_scree(self.displayedStack) + + pca_scree_plot = pg.plot( + var[:24], title="Scree Plot", pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), symbol="o" + ) + pca_scree_plot.addLine(y=0) + pca_scree_plot.setLabel("bottom", "Component Number") + pca_scree_plot.setLabel("left", "Singular Values") + + logger.info("Process complete") + + def calc_comp_(self): + + logger.info("Process started..") + + # self.update_stack() + n_components = self.sb_ncomp.value() + method_ = self.cb_comp_method.currentText() + + ims, comp_spec, decon_spec, decomp_map = decompose_stack( + self.displayedStack, decompose_method=method_, n_components_=n_components + ) + + self._new_window3 = ComponentViewer(ims, self.xdata, comp_spec, decon_spec, decomp_map) + self._new_window3.show() + + logger.info("Process complete") + + def kmeans_elbow(self): + logger.info("Process started..") + # self.update_stack() + + with pg.BusyCursor(): + try: + clust_n, var = kmeans_variance(self.displayedStack) + kmeans_var_plot = pg.plot( + clust_n, var, title="KMeans Variance", pen=pg.mkPen("y", width=2, style=QtCore.Qt.DotLine), + symbol="o" + ) + kmeans_var_plot.setLabel("bottom", "Cluster Number") + kmeans_var_plot.setLabel("left", "Sum of squared distances") + logger.info("Process complete") + except OverflowError: + pass + logger.error("Overflow Error, values are too long") + + def kmeans_elbow_Thread(self): + # Pass the function to execute + worker = Worker(self.kmeans_elbow) # Any other args, kwargs are passed to the run function + worker.signals.result.connect(self.print_output) + worker.signals.finished.connect(self.thread_complete) + # Execute + self.threadpool.start(worker) + + def clustering_(self): + + logger.info("Process started..") + # self.update_stack() + method_ = self.cb_clust_method.currentText() + + decon_images, X_cluster, decon_spectra = cluster_stack( + self.displayedStack, + method=method_, + n_clusters_=self.sb_ncluster.value(), + decomposed=False, + decompose_method=self.cb_comp_method.currentText(), + decompose_comp=self.sb_ncomp.value(), + ) + + self._new_window4 = ClusterViewer(decon_images, self.xdata, X_cluster, decon_spectra) + self._new_window4.show() + + logger.info("Process complete") + + def change_color_on_load(self, button_name): + button_name.setStyleSheet("background-color : rgb(0,150,0);" "color: rgb(255,255,255)") + + def energyFileChooser(self): + file_name = QFileDialog().getOpenFileName(self, + "Open energy list", + self.user_wd, + "text file (*.txt)") + self.efilePath = file_name[0] + + def fast_xanes_fitting(self): + + self._new_window5 = XANESViewer(self.displayedStack, self.xdata, self.refs, self.ref_names) + self._new_window5.show() + + # Thread Signals + + def print_output(self, s): + print(s) + + def thread_complete(self): + print("THREAD COMPLETE!") + + def closeEvent(self, event): + reply = QMessageBox.question( + self, + "Window Close", + "Are you sure you want to close?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + + if reply == QMessageBox.StandardButton.Yes: + event.accept() + QApplication.closeAllWindows() + else: + event.ignore() + + +class WorkerSignals(QObject): + """ + Defines the signals available from a running worker thread. + Supported signals are: + - finished: No data + - error:`tuple` (exctype, value, traceback.format_exc() ) + - result: `object` data returned from processing, anything + - progress: `tuple` indicating progress metadata + """ + + start = pyqtSignal() + finished = pyqtSignal() + error = pyqtSignal(tuple) + result = pyqtSignal(object) + + +class Worker(QRunnable): + """ + Worker thread + Inherits from QRunnable to handler worker thread setup, signals and wrap-up. + """ + + def __init__(self, fn, *args, **kwargs): + super(Worker, self).__init__() + # Store constructor arguments (re-used for processing) + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + @pyqtSlot() + def run(self): + """ + Initialise the runner function with passed args, kwargs. + """ + # Retrieve args/kwargs here; and fire processing using them + self.signals.start.emit() + try: + result = self.fn(*self.args, **self.kwargs) + except Exception: + traceback.print_exc() + exctype, value = sys.exc_info()[:2] + self.signals.error.emit((exctype, value, traceback.format_exc())) + else: + self.signals.result.emit(result) # Return the result of the processing + finally: + self.signals.finished.emit() # Done + + + + +class ComponentViewer(QtWidgets.QMainWindow): + def __init__(self, comp_stack, energy, comp_spectra, decon_spectra, decomp_map): + super(ComponentViewer, self).__init__() + + # Load the UI Page + uic.loadUi(os.path.join(ui_dir, "ComponentView.ui"), self) + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.user_wd = os.path.abspath("~") + + self.comp_stack = comp_stack + self.energy = energy + self.comp_spectra = comp_spectra + self.decon_spectra = decon_spectra + self.decomp_map = decomp_map + + (self.dim1, self.dim3, self.dim2) = self.comp_stack.shape + self.hs_comp_number.setMaximum(self.dim1 - 1) + + self.image_view.setImage(self.comp_stack) + self.image_view.setPredefinedGradient("viridis") + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + + self.image_view2.setImage(self.decomp_map) + self.image_view2.setPredefinedGradient("bipolar") + self.image_view2.ui.menuBtn.hide() + self.image_view2.ui.roiBtn.hide() + + # connection + self.update_image() + self.pb_show_all.clicked.connect(lambda:self.show_all_spec(norm_to_max = True, add_offset = True)) + self.hs_comp_number.valueChanged.connect(self.update_image) + self.actionSave.triggered.connect(self.save_comp_data) + self.pb_openScatterPlot.clicked.connect(self.openScatterPlot) + self.pb_showMultiColor.clicked.connect(lambda: self.generateMultiColorView(withSpectra=False)) + self.pb_showMultiImageXANESView.clicked.connect(lambda: self.generateMultiColorView(withSpectra=True)) + + def update_image(self): + im_index = self.hs_comp_number.value() + self.spectrum_view.setLabel("bottom", "Energy") + self.spectrum_view.setLabel("left", "Intensity", "A.U.") + self.spectrum_view.plot(self.energy, self.decon_spectra[:, im_index], clear=True) + self.component_view.setLabel("bottom", "Energy") + self.component_view.setLabel("left", "Weight", "A.U.") + self.component_view.plot(self.energy, self.comp_spectra[:, im_index], clear=True) + self.label_comp_number.setText(f"{im_index + 1}/{self.dim1}") + # self.image_view.setCurrentIndex(im_index-1) + self.image_view.setImage(self.comp_stack[im_index]) + + def openScatterPlot(self): + self.scatter_window = ComponentScatterPlot(self.comp_stack, self.comp_spectra) + + # ph = self.geometry().height() + # pw = self.geometry().width() + # px = self.geometry().x() + # py = self.geometry().y() + # dw = self.scatter_window.width() + # dh = self.scatter_window.height() + # self.scatter_window.setGeometry(px+0.65*pw, py + ph - 2*dh-5, dw, dh) + self.scatter_window.show() + + def show_all_spec(self, norm_to_max = True, add_offset = True): + self.spectrum_view.clear() + self.plt_colors = ["g", "b", "r", "c", "m", "y", "w"] * 10 + offsets = np.arange(0, 2, 0.2) + self.spectrum_view.addLegend() + for ii in range(self.decon_spectra.shape[1]): + to_plot = self.decon_spectra[:, ii] + if norm_to_max: + to_plot = self.decon_spectra[:, ii] / self.decon_spectra[:, ii].max() + if add_offset: + to_plot = to_plot+ + offsets[ii] + + self.spectrum_view.plot( + self.energy, + to_plot, + pen=self.plt_colors[ii], + name="component" + str(ii + 1), + ) + self.component_view.clear() + self.component_view.addLegend() + for ii in range(self.comp_spectra.shape[1]): + to_plot = self.comp_spectra[:, ii] + if norm_to_max: + to_plot = self.comp_spectra[:, ii] / self.comp_spectra[:, ii].max() + if add_offset: + to_plot = to_plot+ + offsets[ii] + self.component_view.plot( + self.energy, + to_plot, + pen=self.plt_colors[ii], + name="eigen_vector" + str(ii + 1), + ) + + def save_comp_data(self): + file_name = QFileDialog().getSaveFileName(self, "save all data", self.user_wd, "data(*tiff *tif *txt *png )") + if file_name[0]: + self.show_all_spec(norm_to_max = False, add_offset = False) + tf.imwrite(file_name[0] + "_components.tiff", np.float32(self.comp_stack)) + tf.imwrite(file_name[0] + "_component_masks.tiff", np.float32(self.decomp_map)) + exporter_spec = pg.exporters.CSVExporter(self.spectrum_view.plotItem) + exporter_spec.parameters()["columnMode"] = "(x,y) per plot" + exporter_spec.export(file_name[0] + "_deconv_spec.csv") + exporter_eigen = pg.exporters.CSVExporter(self.component_view.plotItem) + exporter_eigen.parameters()["columnMode"] = "(x,y) per plot" + exporter_eigen.export(file_name[0] + "_eigen_vectors.csv") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def generateMultiColorView(self, withSpectra=False): + self.multichanneldict = {} + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.comp_stack.transpose(0, 1, 2))): + low, high = np.min(image), np.max(image) + self.multichanneldict[f'Image {n + 1}'] = {'ImageName': f'Image {n + 1}', + 'ImageDir': '.', + 'Image': image, + 'Color': colorName, + 'CmapLimits': (low, high), + 'Opacity': 1.0 + } + + if withSpectra: + compXanesSpetraAll = pd.DataFrame() + compXanesSpetraAll['Energy'] = self.energy + + for n, spec in enumerate(self.decon_spectra.T): + compXanesSpetraAll[f'Component_{n + 1}'] = spec + + self.muli_color_window = MultiXANESWindow(image_dict=self.multichanneldict, + spec_df=compXanesSpetraAll) + else: + self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) + + self.muli_color_window.show() + + # add energy column + + +class ClusterViewer(QtWidgets.QMainWindow): + def __init__(self, decon_images, energy, X_cluster, decon_spectra): + super(ClusterViewer, self).__init__() + + # Load the UI Page + uic.loadUi(os.path.join(ui_dir, "ClusterView.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + + self.decon_images = decon_images + self.energy = energy + self.X_cluster = X_cluster + self.decon_spectra = decon_spectra + (self.dim1, self.dim3, self.dim2) = self.decon_images.shape + self.hsb_cluster_number.setMaximum(self.dim1 - 1) + self.X_cluster = X_cluster + + self.image_view.setImage(self.decon_images, autoHistogramRange=True, autoLevels=True) + self.image_view.setPredefinedGradient("viridis") + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + + self.cluster_view.setImage(self.X_cluster, autoHistogramRange=True, autoLevels=True) + self.cluster_view.setPredefinedGradient("bipolar") + self.cluster_view.ui.histogram.hide() + self.cluster_view.ui.menuBtn.hide() + self.cluster_view.ui.roiBtn.hide() + + # connection + self.update_display() + self.hsb_cluster_number.valueChanged.connect(self.update_display) + self.actionSave.triggered.connect(self.save_clust_data) + self.pb_show_all_spec.clicked.connect(self.showAllSpec) + self.pb_showMultiColor.clicked.connect(self.generateMultiColorView) + + def update_display(self): + im_index = self.hsb_cluster_number.value() + self.component_view.setLabel("bottom", "Energy") + self.component_view.setLabel("left", "Intensity", "A.U.") + self.component_view.plot(self.energy, self.decon_spectra[:, im_index], clear=True) + # self.image_view.setCurrentIndex(im_index-1) + self.image_view.setImage(self.decon_images[im_index]) + self.label_comp_number.setText(f"{im_index + 1}/{self.dim1}") + + def save_clust_data(self): + file_name = QFileDialog().getSaveFileName(self, "", "", "data(*tiff *tif *txt *png )") + if file_name[0]: + + tf.imwrite( + file_name[0] + "_cluster.tiff", np.float32(self.decon_images.transpose(0, 2, 1)), imagej=True + ) + tf.imwrite(file_name[0] + "_cluster_map.tiff", np.float32(self.X_cluster.T), imagej=True) + np.savetxt(file_name[0] + "_deconv_spec.txt", self.decon_spectra) + + else: + logger.error("Saving Cancelled") + self.statusbar.showMessage("Saving Cancelled") + pass + + def showAllSpec(self): + self.component_view.clear() + self.plt_colors = ["g", "b", "r", "c", "m", "y", "w"] * 10 + offsets = np.arange(0, 2, 0.2) + self.component_view.addLegend() + for ii in range(self.decon_spectra.shape[1]): + self.component_view.plot( + self.energy, + (self.decon_spectra[:, ii] / self.decon_spectra[:, ii].max()) + offsets[ii], + pen=self.plt_colors[ii], + name="cluster" + str(ii + 1), + ) + + def generateMultiColorView(self): + self.multichanneldict = {} + + for n, (colorName, image) in enumerate(zip(cmap_dict.keys(), self.decon_images.transpose(0, 1, 2))): + low, high = np.min(image), np.max(image) + self.multichanneldict[f"Image {n + 1}"] = { + "ImageName": f"Image {n + 1}", + "ImageDir": ".", + "Image": image, + "Color": colorName, + "CmapLimits": (low, high), + "Opacity": 1.0, + } + self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) + self.muli_color_window.show() + + + +class ScatterPlot(QtWidgets.QMainWindow): + def __init__(self, img1, img2, nameTuple): + super(ScatterPlot, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "ScatterView.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.clearPgPlot() + self.w1 = self.scatterViewer.addPlot() + self.img1 = img1 + self.img2 = img2 + self.nameTuple = nameTuple + x, y = np.shape(self.img1) + self.s1 = pg.ScatterPlotItem(size=2, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 0, 255)) + # print(self.s1) + + # create three polyline ROIs for masking + Xsize = self.img1.max() / 6 + Ysize = self.img2.max() / 6 + + self.scatter_mask = pg.PolyLineROI( + [[0, 0], [0, Ysize], [Xsize / 2, Ysize * 1.5], [Xsize, Ysize], [Xsize, 0]], + pos=None, + pen=pg.mkPen("r", width=2), + hoverPen=pg.mkPen("w", width=2), + closed=True, + removable=True, + ) + + self.scatter_mask2 = pg.PolyLineROI( + [ + [Xsize * 1.2, 0], + [Xsize * 1.2, Ysize * 2], + [Xsize * 2, Ysize * 2], + [Xsize * 3, Ysize], + [Xsize * 2, 0], + ], + pos=None, + pen=pg.mkPen("g", width=2), + hoverPen=pg.mkPen("w", width=2), + closed=True, + removable=True, + ) + self.scatter_mask3 = pg.PolyLineROI( + [ + [Xsize * 2.5, 0], + [Xsize * 2.5, Ysize], + [Xsize * 4, Ysize], + [Xsize * 4, 0], + [Xsize * 3.7, Ysize * -0.5], + ], + pos=None, + pen=pg.mkPen("c", width=2), + hoverPen=pg.mkPen("w", width=2), + closed=True, + removable=True, + ) + + self.fitScatter = self.fitScatter2 = self.fitScatter3 = None + + self.rois = { + "ROI 1": (self.scatter_mask, self.rb_roi1.isChecked(), self.fitScatter), + "ROI 2": (self.scatter_mask2, self.rb_roi2.isChecked(), self.fitScatter2), + "ROI 3": (self.scatter_mask3, self.rb_roi3.isChecked(), self.fitScatter3), + } + + self.windowNames = {"ROI 1": self.fitScatter, "ROI 2": self.fitScatter2, "ROI 3": self.fitScatter3} + + self.s1.setData(self.img1.flatten(), self.img2.flatten()) + self.w1.setLabel("bottom", self.nameTuple[0], "counts") + self.label_img1.setText(self.nameTuple[0]) + self.w1.setLabel("left", self.nameTuple[1], "counts") + self.label_img2.setText(self.nameTuple[1]) + self.w1.addItem(self.s1) + + self.image_view.setImage(self.img1) + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + self.image_view.setPredefinedGradient("thermal") + + self.image_view2.setImage(self.img2) + self.image_view2.ui.menuBtn.hide() + self.image_view2.ui.roiBtn.hide() + self.image_view2.setPredefinedGradient("thermal") + + # connections + self.actionSave_Plot.triggered.connect(self.pg_export_correlation) + self.actionSave_Images.triggered.connect(self.tiff_export_images) + # self.pb_define_mask.clicked.connect(lambda:self.createMask(self.scatter_mask)) + self.pb_define_mask.clicked.connect(self.addMultipleROIs) + # self.pb_apply_mask.clicked.connect(lambda:self.getMaskRegion(self.scatter_mask)) + self.pb_apply_mask.clicked.connect(self.applyMultipleROIs) + self.pb_clear_mask.clicked.connect(self.clearMultipleROIs) + self.pb_compositeScatter.clicked.connect(self.createCompositeScatter) + [rbs.clicked.connect(self.updateROIDict) for rbs in [self.rb_roi1, self.rb_roi2, self.rb_roi3]] + + def pg_export_correlation(self): + + exporter = pg.exporters.CSVExporter(self.w1) + #exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, + "save correlation", + os.path.join(self.user_wd,"correlation.csv"), + "spectrum and fit (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def tiff_export_images(self): + file_name = QFileDialog().getSaveFileName(self, + "save images", + os.path.join(self.user_wd,"image.txt"), + "spectrum and fit (*tiff)") + if file_name[0]: + tf.imwrite(file_name[0] + ".tiff", np.dstack([self.img1, self.img2]).T) + self.statusbar.showMessage(f"Images saved to {file_name[0]}") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def createMask(self, ROIName): + + try: + self.w1.removeItem(ROIName) + except Exception: + pass + self.w1.addItem(ROIName) + + def clearMask(self, ROIName): + self.w1.removeItem(ROIName) + + def clearPgPlot(self): + try: + self.masked_img.close() + except Exception: + pass + + def getMaskRegion(self, ROIName, generateSeperateWindows=True): + + """filter scatterplot points using polylineROI region""" + + # Ref : https://stackoverflow.com/questions/57719303/how-to-map-mouse-position-on-a-scatterplot + + # get the roi region:QPaintPathObject + roiShape = self.rois[ROIName][0].mapToItem(self.s1, self.rois[ROIName][0].shape()) + + # get data in the scatter plot + scatterData = np.array(self.s1.getData()) + + # generate a binary mask for points inside or outside the roishape + selected = [roiShape.contains(QtCore.QPointF(pt[0], pt[1])) for pt in scatterData.T] + + # reshape the mask to image dimensions + self.mask2D = np.reshape(selected, (self.img1.shape)) + + # get masked image1 + self.maskedImage = self.mask2D * self.img1 + + # get rid of the (0,0) values in the masked array + self.xData, self.yData = np.compress(selected, scatterData[0]), np.compress(selected, scatterData[1]) + + # linear regeression of the filtered X,Y data + result = linregress(self.xData, self.yData) + + # Pearson's correlation of the filtered X,Y data + pr, pp = stats.pearsonr(self.xData, self.yData) + + # apply the solved equation to xData to generate the fit line + self.yyData = result.intercept + result.slope * self.xData + + # Prepare strings for fit results and stats + self.fitLineEqn = ( + f" y = x*{result.slope :.3e} + {result.intercept :.3e}," + f"\n R^2 = {result.rvalue**2 :.3f}, r = {pr :.3f}" + ) + FitStats1 = f" Slope Error = {result.stderr :.3e}, Intercept Error = {result.intercept_stderr :.3e}\n" + FitStats2 = f" Pearson's correlation coefficient = {pr :.3f}" + refs = "\n\n ***References****\n\n scipy.stats.linregress, scipy.stats.pearsonr " + fitStats = ( + f"\n ***{ROIName} Fit Results***\n\n" + " Equation: " + self.fitLineEqn + FitStats1 + FitStats2 + refs + ) + + # generate new window to plot the results + + if generateSeperateWindows: + self.windowNames[ROIName] = MaskedScatterPlotFit( + [self.xData, self.yData], + [self.xData, self.yyData], + self.mask2D, + self.maskedImage, + fitStats, + self.fitLineEqn, + self.nameTuple, + ) + self.windowNames[ROIName].show() + + """ + from scipy.linalg import lstsq + M = xData[:, np.newaxis]**[0, 1] #use >1 for polynomial fits + p, res, rnk, s = lstsq(M, yData) + yyData = p[0] + p[1]*xData + """ + + def updateROIDict(self): + self.rois = { + "ROI 1": (self.scatter_mask, self.rb_roi1.isChecked()), + "ROI 2": (self.scatter_mask2, self.rb_roi2.isChecked()), + "ROI 3": (self.scatter_mask3, self.rb_roi3.isChecked()), + } + + def applyMultipleROIs(self): + with pg.BusyCursor(): + self.updateROIDict() + for key in self.rois.keys(): + if self.rois[key][1]: + self.getMaskRegion(key) + else: + pass + + def addMultipleROIs(self): + self.updateROIDict() + for key in self.rois.keys(): + if self.rois[key][1]: + self.createMask(self.rois[key][0]) + else: + self.clearMask(self.rois[key][0]) + + def clearMultipleROIs(self): + self.updateROIDict() + for key in self.rois.keys(): + if not self.rois[key][1]: + self.clearMask(self.rois[key][0]) + else: + pass + + def createCompositeScatter(self): + + points = [] + fitLine = [] + masks = [] + roiFitEqn = {} + + self.updateROIDict() + for n, key in enumerate(self.rois.keys()): + if self.rois[key][1]: + self.getMaskRegion(key, generateSeperateWindows=False) + points.append(np.column_stack([self.xData, self.yData])) + fitLine.append(np.column_stack([self.xData, self.yyData])) + masks.append(self.mask2D) + roiFitEqn[key] = self.fitLineEqn + else: + pass + + self.compositeScatterWindow = CompositeScatterPlot( + points, + fitLine, + np.array(masks), + roiFitEqn, + self.nameTuple + ) + self.compositeScatterWindow.show() + + def _createCompositeScatter(self): + self.scatterColors = ["w", "c", "y", "k", "m"] + points = [] + fitLine = [] + + self.updateROIDict() + for n, key in enumerate(self.rois.keys()): + if self.rois[key][1]: + self.getMaskRegion(key, generateSeperateWindows=False) + + for x, y, yy in zip(self.xData, self.yData, self.yyData): + + points.append( + { + "pos": (x, y), + "data": "id", + "size": 3, + "pen": pg.mkPen(None), + "brush": self.scatterColors[n], + } + ) + fitLine.extend(np.column_stack((self.xData, self.yyData))) + else: + pass + + logger.info(f" fitline shape: {np.shape(fitLine)}") + self.compositeScatterWindow = CompositeScatterPlot(points, np.array(fitLine)) + self.compositeScatterWindow.show() + + def getROIParams(self): + print(np.array(self.scatter_mask.getSceneHandlePositions())) + +class MaskedScatterPlotFit(QtWidgets.QMainWindow): + def __init__(self, scatterData, fitData, mask, maskedImage, fitString, fitEquation, nameTuple): + super(MaskedScatterPlotFit, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "maskedScatterPlotFit.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.scatterData = scatterData + self.fitData = fitData + self.mask = mask + self.maskedImage = maskedImage + self.fitString = fitString + self.fitEquation = fitEquation + self.nameTuple = nameTuple + + # set the graphicslayoutwidget in the ui as canvas + self.canvas = self.scatterViewer.addPlot() + self.canvas.addLegend() + self.canvas.setLabel("bottom", self.nameTuple[0], "counts") + self.canvas.setLabel("left", self.nameTuple[1], "counts") + self.gb_maskedImage1.setTitle(f" Masked {self.nameTuple[0]}") + + # generate a scatter plot item + self.scattered = pg.ScatterPlotItem(size=3.5, pen=pg.mkPen(None), brush=pg.mkBrush(5, 214, 255, 200)) + + # set scatter plot data + self.scattered.setData(scatterData[0], scatterData[1], name="Data") + + # set z value negative to show scatter data behind the fit line + self.scattered.setZValue(-10) + + # add scatter plot to the canvas + self.canvas.addItem(self.scattered) + + # generate plotitem for fit line + self.fitLinePlot = pg.PlotDataItem(pen=pg.mkPen(pg.mkColor(220, 20, 60), width=3.3)) + + # set line plot data + self.fitLinePlot.setData(fitData[0], fitData[1], name="Linear Fit") + + # add line plot to the canvas + self.canvas.addItem(self.fitLinePlot) + + # display Mask + self.imageView_mask.setImage(self.mask) + self.imageView_mask.ui.menuBtn.hide() + self.imageView_mask.ui.roiBtn.hide() + self.imageView_mask.setPredefinedGradient("plasma") + + # display masked Image + self.imageView_maskedImage.setImage(self.maskedImage) + self.imageView_maskedImage.ui.menuBtn.hide() + self.imageView_maskedImage.ui.roiBtn.hide() + self.imageView_maskedImage.setPredefinedGradient("viridis") + + # display Fit stats + self.text_fit_results.setPlainText(fitString) + self.canvas.setTitle(self.fitEquation, color="r") + + # connections + self.pb_copy_results.clicked.connect(self.copyFitResults) + self.pb_save_results.clicked.connect(self.saveFitResults) + self.actionSave_Plot.triggered.connect(self.pg_export_correlation) + self.actionSaveMask.triggered.connect(self.saveMask) + self.actionSaveMaskedImage.triggered.connect(self.saveImage) + + def saveFitResults(self): + S__File = QFileDialog.getSaveFileName(self, "save txt", "correlationPlotFit.txt", "txt data (*txt)") + + Text = self.text_fit_results.toPlainText() + if S__File[0]: + with open(S__File[0], "w") as file: + file.write(Text) + + def copyFitResults(self): + self.text_fit_results.selectAll() + self.text_fit_results.copy() + self.statusbar.showMessage("text copied to clipboard") + + def pg_export_correlation(self): + + exporter = pg.exporters.CSVExporter(self.canvas) + #exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName( + self, "save correlation", "scatterData.csv", "spectrum and fit (*csv)" + ) + if file_name[0]: + exporter.export(file_name[0]) + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + else: + pass + + def saveImage(self): + + file_name = QFileDialog().getSaveFileName(self, "Save image data", "image.tiff", "image file(*tiff *tif )") + if file_name[0]: + tf.imwrite(file_name[0], self.maskedImage) + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + else: + self.statusbar.showMessage("Saving cancelled") + pass + + def saveMask(self): + + file_name = QFileDialog().getSaveFileName(self, "Save image data", "mask.tiff", "image file(*tiff *tif )") + if file_name[0]: + tf.imwrite(file_name[0], self.mask) + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + else: + self.statusbar.showMessage("Saving cancelled") + pass + +class ComponentScatterPlot(QtWidgets.QMainWindow): + def __init__(self, decomp_stack, specs): + super(ComponentScatterPlot, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "ComponentScatterPlot.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + self.w1 = self.scatterViewer.addPlot() + self.decomp_stack = decomp_stack + self.specs = specs + (self.dim1, self.dim3, self.dim2) = self.decomp_stack.shape + # fill the combonbox depending in the number of components for scatter plot + for n, combs in enumerate(combinations(np.arange(self.dim1), 2)): + self.cb_scatter_comp.addItem(str(combs)) + self.cb_scatter_comp.setItemData(n, combs) + + self.s1 = pg.ScatterPlotItem(size=3, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 0, 120)) + + self.setImageAndScatterPlot() + # connections + self.actionSave_Plot.triggered.connect(self.pg_export_correlation) + self.actionSave_Images.triggered.connect(self.tiff_export_images) + self.pb_updateComponents.clicked.connect(self.setImageAndScatterPlot) + self.pb_define_mask.clicked.connect(self.createMask) + self.pb_apply_mask.clicked.connect(self.getMaskRegion) + self.pb_reset_mask.clicked.connect(self.resetMask) + self.pb_addALine.clicked.connect(lambda: self.createMask(Line=True)) + + def setImageAndScatterPlot(self): + + try: + self.s1.clear() + except Exception: + pass + + comp_tuple = self.cb_scatter_comp.currentData() + self.img1, self.img2 = self.decomp_stack[comp_tuple[0]], self.decomp_stack[comp_tuple[-1]] + self.image_view.setImage(self.decomp_stack[comp_tuple[0]]) + self.image_view.ui.menuBtn.hide() + self.image_view.ui.roiBtn.hide() + self.image_view.setPredefinedGradient("bipolar") + + self.image_view2.setImage(self.decomp_stack[comp_tuple[-1]]) + self.image_view2.ui.menuBtn.hide() + self.image_view2.ui.roiBtn.hide() + self.image_view2.setPredefinedGradient("bipolar") + + points = [] + for i, j in zip(self.img1.flatten(), self.img2.flatten()): + + points.append( + { + "pos": (i, j), + "data": "id", + "size": 5, + "pen": pg.mkPen(None), + "brush": pg.mkBrush(255, 255, 0, 160), + } + ) + + self.s1.addPoints(points) + self.w1.addItem(self.s1) + # self.s1.setData(self.specs[:, comp_tuple[0]], self.specs[:, comp_tuple[-1]]) + self.w1.setLabel("bottom", f"PC{comp_tuple[0]+1}") + self.w1.setLabel("left", f"PC{comp_tuple[-1]+1}") + self.label_im1.setText(f"PC{comp_tuple[0]+1}") + self.label_im2.setText(f"PC{comp_tuple[-1]+1}") + + def createMask(self, Line=False): + + self.size = self.img1.max() / 10 + self.pos = int(self.img1.mean()) + + if Line: + self.lineROI = pg.LineSegmentROI( + [0, 1], + pos=(self.pos, self.pos), + pen=pg.mkPen("r", width=4), + hoverPen=pg.mkPen("g", width=4), + removable=True, + ) + self.w1.addItem(self.lineROI) + + else: + + self.scatter_mask = pg.PolyLineROI( + [[0, 0], [0, self.size], [self.size, self.size], [self.size, 0]], + pos=(self.pos, self.pos), + pen=pg.mkPen("r", width=4), + hoverPen=pg.mkPen("g", width=4), + closed=True, + removable=True, + ) + + self.w1.addItem(self.scatter_mask) + + def resetMask(self): + self.clearMask() + self.createMask() + + def clearMask(self): + try: + self.w1.removeItem(self.scatter_mask) + except AttributeError: + pass + + def clearPgPlot(self): + try: + self.masked_img.close() + except Exception: + pass + + def getMaskRegion(self): + + # Ref : https://stackoverflow.com/questions/57719303/how-to-map-mouse-position-on-a-scatterplot + + roiShape = self.scatter_mask.mapToItem(self.s1, self.scatter_mask.shape()) + self._points = list() + logger.info("Building Scatter Plot Window; Please wait..") + for i in range(len(self.img1.flatten())): + self._points.append(QtCore.QPointF(self.img1.flatten()[i], self.img2.flatten()[i])) + + selected = [roiShape.contains(pt) for pt in self._points] + img_selected = np.reshape(selected, (self.img1.shape)) + + self.masked_img = singleStackViewer(img_selected * self.img1, gradient="bipolar") + self.masked_img.show() + + def pg_export_correlation(self): + + exporter = pg.exporters.CSVExporter(self.w1) + exporter.parameters()["columnMode"] = "(x,y,y,y) for all plots" + file_name = QFileDialog().getSaveFileName(self, "save correlation", "", "spectrum and fit (*csv)") + if file_name[0]: + exporter.export(file_name[0] + ".csv") + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + else: + pass + + def tiff_export_images(self): + file_name = QFileDialog().getSaveFileName(self, "save images", "", "spectrum and fit (*tiff)") + if file_name[0]: + tf.imwrite(file_name[0] + ".tiff", np.dstack([self.img1, self.img2]).T) + self.statusbar.showMessage(f"Images saved to {file_name[0]}") + else: + pass + +class LoadingScreen(QtWidgets.QSplashScreen): + def __init__(self): + super(LoadingScreen, self).__init__() + uic.loadUi(os.path.join(ui_dir, "animationWindow.ui"), self) + self.setWindowOpacity(0.65) + self.movie = QMovie("uis/animation.gif") + self.label.setMovie(self.movie) + + def mousePressEvent(self, event): + # disable default "click-to-dismiss" behaviour + pass + + def startAnimation(self): + self.movie.start() + self.show() + + def stopAnimation(self): + self.movie.stop() + self.hide() + +class CompositeScatterPlot(QtWidgets.QMainWindow): + def __init__(self, scatterPoints, fitLine, maskImages, fitEquations, nameTuple): + super(CompositeScatterPlot, self).__init__() + + uic.loadUi(os.path.join(ui_dir, "multipleScatterFit.ui"), self) + self.user_wd = os.path.abspath("~") + self.centralwidget.setStyleSheet(open(os.path.join(ui_dir, "css/defaultStyle.css")).read()) + + self.scatterPoints = scatterPoints + # print(f"{np.shape(self.scatterPoints) = }") + self.fitLine = fitLine + #print(f"{np.shape(self.fitLine) = }") + self.scatterColors = ["r", (0, 115, 0), (4, 186, 186), "c", "w", "k"] + self.fitColors = ["b", "r", "m", "k", "b"] + self.roiNames = list(fitEquations.keys()) + self.fitEqns = list(fitEquations.values()) + # print(f"{np.shape(self.roiNames) = }") + # print(f"{np.shape(self.fitEqns) = }") + self.nameTuple = nameTuple + self.maskImages = maskImages + + # self.scatterViewer.setBackground('w') + # set the graphicslayoutwidget in the ui as canvas + self.canvas = self.scatterViewer.addPlot() + self.canvas.addLegend() + self.canvas.setLabel("bottom", self.nameTuple[0], "counts") + self.canvas.setLabel("left", self.nameTuple[1], "counts") + + # connections + self.actionExport.triggered.connect(self.exportData) + self.actionSave_as_PNG.triggered.connect(self.exportAsPNG) + self.actionGenerate_MultiColor_Mask.triggered.connect(self.generateMultiColorView) + self.actionWhite.triggered.connect(lambda: self.scatterViewer.setBackground("w")) + self.actionBlack.triggered.connect(lambda: self.scatterViewer.setBackground("k")) + + with pg.BusyCursor(): + + for arr, fitline, clr, fitClr, rname, feqn in zip( + self.scatterPoints, self.fitLine, self.scatterColors, self.fitColors, self.roiNames, self.fitEqns + ): + + sctrPoints = [] + for pt in arr: + sctrPoints.append( + {"pos": (pt[0], pt[1]), "data": "id", "size": 3, "pen": pg.mkPen(None), "brush": clr} + ) + + # generate a scatter plot item + self.scattered = pg.ScatterPlotItem(size=4.5, pen=clr, brush=pg.mkBrush(5, 214, 255, 200)) + # set scatter plot data + self.scattered.addPoints(sctrPoints, name=rname) + + # set z value negative to show scatter data behind the fit line + self.scattered.setZValue(-10) + + # add scatter plot to the canvas + self.canvas.addItem(self.scattered) + + # generate plotitem for fit line + self.fitLinePlot = pg.PlotDataItem(pen=pg.mkPen(fitClr, width=4.5)) + + # set line plot data + self.fitLinePlot.setData(fitline, name=feqn) + + # add line plot to the canvas + self.canvas.addItem(self.fitLinePlot) + + def generateMultiColorView(self): + self.multichanneldict = {} + + for n, (colorName, image, rname) in enumerate(zip(cmap_dict.keys(), self.maskImages, self.roiNames)): + low, high = np.min(image), np.max(image) + self.multichanneldict[rname] = { + "ImageName": rname, + "ImageDir": ".", + "Image": image, + "Color": colorName, + "CmapLimits": (low, high), + "Opacity": 1.0, + } + + # print( self.multichanneldict) + self.muli_color_window = MultiChannelWindow(image_dict=self.multichanneldict) + self.muli_color_window.show() + + def exportData(self): + + exporter = pg.exporters.CSVExporter(self.canvas) + # exporter.parameters()['columnMode'] = '(x,y,y,y) for all plots' + file_name = QFileDialog().getSaveFileName(self, + "Save CSV Data", + os.path.join(self.user_wd,"scatter.csv"), + "image file (*csv)") + if file_name[0]: + exporter.export(file_name[0]) + self.statusbar.showMessage(f"Data saved to {file_name[0]}") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + def exportAsPNG(self): + file_name = QFileDialog().getSaveFileName(self, + "Save Image", + os.path.join(self.user_wd,"image.png"), + "PNG(*.png);; TIFF(*.tiff);; JPG(*.jpg)" + ) + exporter = pg.exporters.ImageExporter(self.canvas) + + if file_name[0]: + exporter.export(file_name[0]) + self.statusbar.showMessage(f"Image saved to {file_name[0]}") + self.user_wd = os.path.dirname(file_name[0]) + else: + pass + + +class StackInfo(QtWidgets.QMainWindow): + def __init__(self, text_to_write: str = " "): + super(StackInfo, self).__init__() + uic.loadUi(os.path.join(ui_dir, "log.ui"), self) + self.user_wd = os.path.abspath("~") + + self.text_to_write = text_to_write + self.pte_run_cmd.setPlainText(self.text_to_write) + + # connections + self.pb_save_cmd.clicked.connect(self.save_file) + self.pb_clear_cmd.clicked.connect(self.clear_text) + + def save_file(self): + S__File = QFileDialog.getSaveFileName(None, "SaveFile", "/", "txt Files (*.txt)") + + Text = self.pte_run_cmd.toPlainText() + if S__File[0]: + with open(S__File[0], "w") as file: + file.write(Text) + + def clear_text(self): + self.pte_run_cmd.clear() + + +def start_xmidas(): + def formatter(prog): + # Set maximum width such that printed help mostly fits in the RTD theme code block (documentation). + return argparse.RawDescriptionHelpFormatter(prog, max_help_position=20, width=90) + + parser = argparse.ArgumentParser( + description=f"XMidas: v{__version__}", + formatter_class=formatter, + ) + parser.parse_args() + + logger.setLevel(logging.INFO) + formatter = logging.Formatter(fmt="%(asctime)s : %(levelname)s : %(message)s") + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + stream_handler.setLevel(logging.INFO) + if logger.hasHandlers(): + logger.handlers.clear() + logger.addHandler(stream_handler) + if version.parse(PYQT_VERSION_STR) >= version.parse("5.14"): + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough + ) + + + app = QtWidgets.QApplication(sys.argv) + # app.setAttribute(QtCore.Qt.AA_Use96Dpi) + window = midasWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + start_xmidas() + +__version__ = "1.0.0" # or whatever version you want diff --git a/xmidas/models/encoders.py b/xmidas/models/encoders.py new file mode 100644 index 0000000..4f5dcc0 --- /dev/null +++ b/xmidas/models/encoders.py @@ -0,0 +1,13 @@ +import json +import numpy as np + +class jsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + else: + return super(jsonEncoder, self).default(obj) \ No newline at end of file diff --git a/xmidas/tests/__init__.py b/xmidas/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/xmidas/tests/conftest.py b/xmidas/tests/conftest.py deleted file mode 100644 index e69de29..0000000 diff --git a/xmidas/tests/test_general.py b/xmidas/tests/test_general.py deleted file mode 100644 index 8bdb023..0000000 --- a/xmidas/tests/test_general.py +++ /dev/null @@ -1,15 +0,0 @@ -import subprocess - - -def test_package_installed(): - """ - Test if the package is installed (imports work) - """ - from xmidas.main import start_xmidas # noqa: F401 - - -def test_entry_point_installed(): - """ - Test that the entry point is installed and the application can be started. - """ - assert subprocess.call(["xmidas", "-h"]) == 0 diff --git a/xmidas/uis/SVGs/Arrow Down.svg b/xmidas/uis/SVGs/Arrow Down.svg deleted file mode 100644 index cce1eec..0000000 --- a/xmidas/uis/SVGs/Arrow Down.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Arrow Left.svg b/xmidas/uis/SVGs/Arrow Left.svg deleted file mode 100644 index 93a3cd4..0000000 --- a/xmidas/uis/SVGs/Arrow Left.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Arrow Right.svg b/xmidas/uis/SVGs/Arrow Right.svg deleted file mode 100644 index fb1f0e1..0000000 --- a/xmidas/uis/SVGs/Arrow Right.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Arrow Up.svg b/xmidas/uis/SVGs/Arrow Up.svg deleted file mode 100644 index 40dbd57..0000000 --- a/xmidas/uis/SVGs/Arrow Up.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Basket.svg b/xmidas/uis/SVGs/Basket.svg deleted file mode 100644 index e33eea3..0000000 --- a/xmidas/uis/SVGs/Basket.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Book.svg b/xmidas/uis/SVGs/Book.svg deleted file mode 100644 index 27d79f2..0000000 --- a/xmidas/uis/SVGs/Book.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Calendar.svg b/xmidas/uis/SVGs/Calendar.svg deleted file mode 100644 index 8e6ee76..0000000 --- a/xmidas/uis/SVGs/Calendar.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Camera.svg b/xmidas/uis/SVGs/Camera.svg deleted file mode 100644 index 80b9e35..0000000 --- a/xmidas/uis/SVGs/Camera.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Case.svg b/xmidas/uis/SVGs/Case.svg deleted file mode 100644 index 173d397..0000000 --- a/xmidas/uis/SVGs/Case.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Change View.svg b/xmidas/uis/SVGs/Change View.svg deleted file mode 100644 index 9332cfc..0000000 --- a/xmidas/uis/SVGs/Change View.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Check V2.svg b/xmidas/uis/SVGs/Check V2.svg deleted file mode 100644 index c8a5e40..0000000 --- a/xmidas/uis/SVGs/Check V2.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Check.svg b/xmidas/uis/SVGs/Check.svg deleted file mode 100644 index dadeaed..0000000 --- a/xmidas/uis/SVGs/Check.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Chervon Right Circle.svg b/xmidas/uis/SVGs/Chervon Right Circle.svg deleted file mode 100644 index 1492a0b..0000000 --- a/xmidas/uis/SVGs/Chervon Right Circle.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Down Circle.svg b/xmidas/uis/SVGs/Chevron Down Circle.svg deleted file mode 100644 index c7ce17b..0000000 --- a/xmidas/uis/SVGs/Chevron Down Circle.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Down.svg b/xmidas/uis/SVGs/Chevron Down.svg deleted file mode 100644 index 56a3576..0000000 --- a/xmidas/uis/SVGs/Chevron Down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Left Circle.svg b/xmidas/uis/SVGs/Chevron Left Circle.svg deleted file mode 100644 index a0e84b0..0000000 --- a/xmidas/uis/SVGs/Chevron Left Circle.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Left.svg b/xmidas/uis/SVGs/Chevron Left.svg deleted file mode 100644 index f296cd7..0000000 --- a/xmidas/uis/SVGs/Chevron Left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Right.svg b/xmidas/uis/SVGs/Chevron Right.svg deleted file mode 100644 index f468a3f..0000000 --- a/xmidas/uis/SVGs/Chevron Right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Up Circle.svg b/xmidas/uis/SVGs/Chevron Up Circle.svg deleted file mode 100644 index 3ebfe80..0000000 --- a/xmidas/uis/SVGs/Chevron Up Circle.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Chevron Up.svg b/xmidas/uis/SVGs/Chevron Up.svg deleted file mode 100644 index fc00cf6..0000000 --- a/xmidas/uis/SVGs/Chevron Up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Clock.svg b/xmidas/uis/SVGs/Clock.svg deleted file mode 100644 index 2bc6f3f..0000000 --- a/xmidas/uis/SVGs/Clock.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Close.svg b/xmidas/uis/SVGs/Close.svg deleted file mode 100644 index 5e22958..0000000 --- a/xmidas/uis/SVGs/Close.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Cog.svg b/xmidas/uis/SVGs/Cog.svg deleted file mode 100644 index 9a39f32..0000000 --- a/xmidas/uis/SVGs/Cog.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Cross Circle.svg b/xmidas/uis/SVGs/Cross Circle.svg deleted file mode 100644 index b4d5ebb..0000000 --- a/xmidas/uis/SVGs/Cross Circle.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Download.svg b/xmidas/uis/SVGs/Download.svg deleted file mode 100644 index deeac83..0000000 --- a/xmidas/uis/SVGs/Download.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Edit V2.svg b/xmidas/uis/SVGs/Edit V2.svg deleted file mode 100644 index 49c8796..0000000 --- a/xmidas/uis/SVGs/Edit V2.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Edit.svg b/xmidas/uis/SVGs/Edit.svg deleted file mode 100644 index 7494aef..0000000 --- a/xmidas/uis/SVGs/Edit.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/xmidas/uis/SVGs/File.svg b/xmidas/uis/SVGs/File.svg deleted file mode 100644 index 9dadb9b..0000000 --- a/xmidas/uis/SVGs/File.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Grid.svg b/xmidas/uis/SVGs/Grid.svg deleted file mode 100644 index b9792e5..0000000 --- a/xmidas/uis/SVGs/Grid.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Heart.svg b/xmidas/uis/SVGs/Heart.svg deleted file mode 100644 index 5f71778..0000000 --- a/xmidas/uis/SVGs/Heart.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Image.svg b/xmidas/uis/SVGs/Image.svg deleted file mode 100644 index 8399140..0000000 --- a/xmidas/uis/SVGs/Image.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Link.svg b/xmidas/uis/SVGs/Link.svg deleted file mode 100644 index fba631d..0000000 --- a/xmidas/uis/SVGs/Link.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Location Cursor.svg b/xmidas/uis/SVGs/Location Cursor.svg deleted file mode 100644 index 65858b3..0000000 --- a/xmidas/uis/SVGs/Location Cursor.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Location.svg b/xmidas/uis/SVGs/Location.svg deleted file mode 100644 index 7626878..0000000 --- a/xmidas/uis/SVGs/Location.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Logout.svg b/xmidas/uis/SVGs/Logout.svg deleted file mode 100644 index 04d2dde..0000000 --- a/xmidas/uis/SVGs/Logout.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Mail.svg b/xmidas/uis/SVGs/Mail.svg deleted file mode 100644 index c64f596..0000000 --- a/xmidas/uis/SVGs/Mail.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Menu.svg b/xmidas/uis/SVGs/Menu.svg deleted file mode 100644 index af3578c..0000000 --- a/xmidas/uis/SVGs/Menu.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Message.svg b/xmidas/uis/SVGs/Message.svg deleted file mode 100644 index 0dc9f24..0000000 --- a/xmidas/uis/SVGs/Message.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Microphone.svg b/xmidas/uis/SVGs/Microphone.svg deleted file mode 100644 index d3bdff4..0000000 --- a/xmidas/uis/SVGs/Microphone.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/More.svg b/xmidas/uis/SVGs/More.svg deleted file mode 100644 index ad2af87..0000000 --- a/xmidas/uis/SVGs/More.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Plus Circle.svg b/xmidas/uis/SVGs/Plus Circle.svg deleted file mode 100644 index be293d9..0000000 --- a/xmidas/uis/SVGs/Plus Circle.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Plus.svg b/xmidas/uis/SVGs/Plus.svg deleted file mode 100644 index 9a2a645..0000000 --- a/xmidas/uis/SVGs/Plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Power Button.svg b/xmidas/uis/SVGs/Power Button.svg deleted file mode 100644 index 2541ea6..0000000 --- a/xmidas/uis/SVGs/Power Button.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Printer.svg b/xmidas/uis/SVGs/Printer.svg deleted file mode 100644 index 5ac0097..0000000 --- a/xmidas/uis/SVGs/Printer.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Settings.svg b/xmidas/uis/SVGs/Settings.svg deleted file mode 100644 index 28474d0..0000000 --- a/xmidas/uis/SVGs/Settings.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Share.svg b/xmidas/uis/SVGs/Share.svg deleted file mode 100644 index 3fe3197..0000000 --- a/xmidas/uis/SVGs/Share.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Shutter.svg b/xmidas/uis/SVGs/Shutter.svg deleted file mode 100644 index 92d99e7..0000000 --- a/xmidas/uis/SVGs/Shutter.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Star.svg b/xmidas/uis/SVGs/Star.svg deleted file mode 100644 index facd65d..0000000 --- a/xmidas/uis/SVGs/Star.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/Trash.svg b/xmidas/uis/SVGs/Trash.svg deleted file mode 100644 index ec862c9..0000000 --- a/xmidas/uis/SVGs/Trash.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/xmidas/uis/SVGs/Trophy.svg b/xmidas/uis/SVGs/Trophy.svg deleted file mode 100644 index 050c08d..0000000 --- a/xmidas/uis/SVGs/Trophy.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/xmidas/uis/SVGs/User.svg b/xmidas/uis/SVGs/User.svg deleted file mode 100644 index 71bb60b..0000000 --- a/xmidas/uis/SVGs/User.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/xmidas/uis/SVGs/Video.svg b/xmidas/uis/SVGs/Video.svg deleted file mode 100644 index 6d8869f..0000000 --- a/xmidas/uis/SVGs/Video.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/xmidas/uis/XANESViewer.ui b/xmidas/uis/XANESViewer.ui deleted file mode 100644 index 1209c09..0000000 --- a/xmidas/uis/XANESViewer.ui +++ /dev/null @@ -1,431 +0,0 @@ - - - mainWindow - - - - 0 - 0 - 1089 - 854 - - - - XANES View - - - font: 10pt "Verdana"; - - - - - - QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -} - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - 0 - 0 - - - - Fit 2D - - - - - - - - - - - E_shift - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - -10.000000000000000 - - - 10.000000000000000 - - - 0.500000000000000 - - - - - - - Alpha: - - - - - - - 10.000000000000000 - - - 0.250000000000000 - - - 0.100000000000000 - - - - - - - - - - - Fitting Model - - - - - - - - NNLS - - - - - LASSO - - - - - RIDGE - - - - - - - - - - - - - - - - - - 0 - 0 - - - - font: 8pt "MS Shell Dlg 2"; -color: rgb(0, 85, 255); - - - Results Text - - - Qt::AlignCenter - - - - - - - - - - - - - - 0 - 0 - - - - ROI Position - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - Opan as RGBCMY Image - - - - - - - - 0 - 0 - - - - - - - XANES Maps - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - Show Component Spectrum - - - - - - - - - - - Live ROI Fit - - - Qt::AlignCenter - - - - - - - - - - - - - - - 0 - 0 - - - - Choose References - - - - - - - - - - References - - - Qt::AlignCenter - - - - - - - - - - - - - - - - 0 - 0 - 1089 - 22 - - - - - Image - - - - - - - Spectrum - - - - - - - Fit - - - - - - - - - - - Export Fit Stats - - - - - Export Ref. Plot - - - - - Save Chem Map - - - - - Save R-factor Image - - - - - false - - - Export References - - - - - Export Fit Stats - - - - - Save Live Fit Data - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
diff --git a/xmidas/uis/__init__.py b/xmidas/uis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/xmidas/uis/animation2.gif b/xmidas/uis/animation2.gif deleted file mode 100644 index b5261ca..0000000 Binary files a/xmidas/uis/animation2.gif and /dev/null differ diff --git a/xmidas/uis/mainwindow_admin.ui b/xmidas/uis/mainwindow_admin.ui deleted file mode 100644 index a7d17b9..0000000 --- a/xmidas/uis/mainwindow_admin.ui +++ /dev/null @@ -1,2814 +0,0 @@ - - - Ajith - MainWindow - - - true - - - - 0 - 0 - 1364 - 1060 - - - - - 0 - 0 - - - - NSLS-II MIDAS - - - - ../pancake.ico../pancake.ico - - - font: 12pt "Segoe UI"; - - - - - true - - - - 0 - 0 - - - - font: 10pt "Segoe UI"; - - -QPushButton { -background-color: rgb(175, 236, 255); -color: rgb(255, 5,0); -} - -QSlider::groove:horizontal { -border: 1px solid #bbb; -background: white; -height: 10px; -border-radius: 4px; -} - -QSlider::sub-page:horizontal { -background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #66e, stop: 1 #bbf); -background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, - stop: 0 #bbf, stop: 1 #55f); -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::add-page:horizontal { -background: #fff; -border: 1px solid #777; -height: 10px; -border-radius: 4px; -} - -QSlider::handle:horizontal { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #eee, stop:1 #ccc); -border: 1px solid #777; -width: 13px; -margin-top: -2px; -margin-bottom: -2px; -border-radius: 4px; -} - -QSlider::handle:horizontal:hover { -background: qlineargradient(x1:0, y1:0, x2:1, y2:1, - stop:0 #fff, stop:1 #ddd); -border: 1px solid #444; -border-radius: 2px; -} - -QSlider::sub-page:horizontal:disabled { -background: #bbb; -border-color: #999; -} - -QSlider::add-page:horizontal:disabled { -background: #eee; -border-color: #999; -} - -QSlider::handle:horizontal:disabled { -background: #eee; -border: 1px solid #aaa; -border-radius: 4px; -} - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - - - - 0 - 0 - - - - - - - 0 - - - - - 0 - 0 - 411 - 355 - - - - Adjust Image Dimensions - - - - - - - 0 - 0 - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - QLayout::SetMinimumSize - - - - - - 0 - 0 - - - - 0 - - - 500 - - - 0 - - - 10 - - - - - - - - 0 - 0 - - - - 1 - - - 5000 - - - 1200 - - - 10 - - - - - - - - 0 - 0 - - - - false - - - px - - - 1 - - - 10000 - - - 100 - - - - - - - - 0 - 0 - - - - false - - - px - - - 1 - - - 10000 - - - 100 - - - - - - - - 0 - 0 - - - - false - - - px - - - 0 - - - 10000 - - - 0 - - - - - - - - 0 - 0 - - - - Y Dimension - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - false - - - px - - - 0 - - - 10000 - - - 0 - - - - - - - - 0 - 0 - - - - X Dimension - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - Stack Range - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - - - 0 - 0 - - - - Adjust the dimensions of the image - - - - - - Update - - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - 0 - 0 - - - - Removes one column/row from all four edges - - - Remove Edges - - - - - - - - - true - - - - 0 - 0 - - - - rebin - - - - - - - true - - - - 0 - 0 - - - - Upscale - - - - - - - - - Ratio - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - true - - - - 0 - 0 - - - - font: 10pt "MS Shell Dlg 2"; - - - 2 - - - 16 - - - 2 - - - 2 - - - - - - - - - - - - - - - - - - 0 - 0 - 334 - 423 - - - - Image Processing - - - - - - - - - - - - - - 0 - 0 - - - - Covert image dat to log values. - - - - - - Log - - - false - - - - - - - - 0 - 0 - - - - Intensity Normalization with the last Frame. - - - Normalize - - - - - - - - - - 0 - 0 - - - - Reverse the arrays. 012 will be 210 - - - Qt::LeftToRight - - - Transpose - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - Remove Outliers (NSigma) - - - - - - - false - - - - - - 200 - - - 3 - - - Qt::Horizontal - - - - - - - 1 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - - Thresholding - - - - - - - - - - - false - - - 100 - - - 5 - - - 5 - - - 5 - - - Qt::Horizontal - - - - - - - 5 - - - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - uses savgol_filter to smooth data - - - Smoothen - - - - - - - - - false - - - 3 - - - 12 - - - 2 - - - 2 - - - 3 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 2 - - - - - - - Window size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - - - - 0 - 0 - 420 - 344 - - - - Alignment - - - - - - - - - - - Transformations: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - TRANSLATION - - - - - RIGID_BODY - - - - - SCALED_ROTATION - - - - - AFFINE - - - - - BILINEAR - - - - - - - - - - - - Reference: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - previous - - - - - mean - - - - - first - - - - - - - - - - - - - 0 - 0 - - - - Iterative Mode - - - - - - - - - Max Iter. - - - - - - - - 0 - 0 - - - - 2 - - - 24 - - - - - - - - - - - - 0 - 0 - - - - Align - - - - - - - - - - 0 - 0 - - - - Load Reference Stack - - - - - - - No Ref. Available - - - true - - - - - - - - - - 0 - 0 - - - - Save Transformation File - - - - - - - Use - - - - - - - - 0 - 0 - - - - Load Transformation File - - - - - - - - - - - - - - - 0 - 0 - - - - - - - Reset Image - - - - - - - true - - - 2 - - - - - 0 - 0 - 411 - 318 - - - - Component Analysis - - - - - - - - - 0 - 0 - - - - - - - Calculate Components - - - - - - - - 0 - 0 - - - - - - - PCA Scree Plot - - - - - - - - - Method - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - PCA - - - - - NMF - - - - - FastICA - - - - - IncrementalPCA - - - - - TruncatedSVD - - - - - FactorAnalysis - - - - - DictionaryLearning - - - - - - - - - - - - Number of Components - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 1 - - - 4 - - - - - - - - - - - - - 0 - 0 - 411 - 318 - - - - Cluster Analysis - - - - - - - - - 0 - 0 - - - - - - - Calculate Clusters - - - - - - - - 0 - 0 - - - - - - - KMeans Variance Plot - - - - - - - Method - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - KMeans - - - - - MiniBatchKMeans - - - - - MeanShift - - - - - Spectral Clustering - - - - - Correlation-Kmeans - - - - - Affinity Propagation - - - - - - - - Number of Clusters - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - 1 - - - 4 - - - - - - - - - - - 0 - 0 - 411 - 318 - - - - XANES Fitting - - - - - - - 11 - - - 11 - - - - - - - - 0 - 0 - - - - - - - Load Energy List - - - - - - - keV - - - - - - - - - - - - 0 - 0 - - - - - - - Load Ref. Spec. - - - - - - - - - - Plot - - - - - - - - - true - - - - 0 - 0 - - - - - - - Fit - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - 0 - 0 - - - - font:12pt "Segoe UI"; - - - - - - - QLayout::SetMaximumSize - - - - - - 0 - 0 - - - - - - - Save Image (.tiff) - - - - - - - 0 - - - - Live - - - - - - - 0 - 0 - - - - Send to Collector - - - - - - - - 0 - 0 - - - - - - - Save - - - - - - - - 0 - 0 - - - - - - - - - Normalized - - - - - - Save - - - - - - - Clear - - - - - - - Norm. to Collector - - - - - - - - 0 - 0 - - - - - - - - - Collector - - - - - - - 0 - 0 - - - - - - - - Clear - - - - - - - Save - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - Stack Info - - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - QLayout::SetDefaultConstraint - - - 11 - - - 11 - - - - - - 0 - 0 - - - - ROI Shape - - - - - - - 0 - 0 - - - - Rectangle - - - true - - - - - - - - 0 - 0 - - - - Ellipse - - - - - - - - 0 - 0 - - - - Polygon - - - false - - - - - - - - 0 - 0 - - - - Circle - - - - - - - - 0 - 0 - - - - Line - - - - - - - - 0 - 0 - - - - Show ROI Stack - - - - - - - - - - font: 9pt "Segoe UI"; - - - ROI Positions - - - - - - Spectrum ROI - - - - - - color: rgb(255, 0,0); - - - roi_size - - - - - - - Range (eV): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Size (eV): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - color: rgb(255, 0,0); - - - roix, roiy - - - - - - - - - - Image ROI - - - - - - Position: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - color: rgb(255, 0,0); - - - roix, roiy - - - - - - - - - - Size(pixels): - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - color: rgb(255, 0, 0); - - - roi_size - - - - - - - - - - - - - - 0 - 0 - - - - 2 - - - - - 0 - 0 - 361 - 193 - - - - Image Calculations - - - - - - - - - - - - - - - Add ROI_2 - - - - - - - Calculation: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - Subtract - - - - - Divide - - - - - Add - - - - - Compare - - - - - - - - - - - - - 0 - 0 - 361 - 193 - - - - Spectrum Calculations - - - - - - - - - - - - - 0 - 0 - - - - Add ROI 2 - - - true - - - false - - - - - - - - - - 0 - 0 - - - - Calculation - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - Divide - - - - - Subtract - - - - - Add - - - - - Correlation Plot - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - 0 - 0 - 433 - 308 - - - - XANES Normalization - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - 1 - - - 5 - - - - - - - - 0 - 0 - - - - Eo - - - - - - - - 0 - 0 - - - - eV - - - 1000.000000000000000 - - - 20000.000000000000000 - - - 7125.000000000000000 - - - - - - - - 0 - 0 - - - - calculate the energy point with maximum derivative - - - Auto Eo - - - - - - - - 0 - 0 - - - - eV - - - 2 - - - -500.000000000000000 - - - 500.000000000000000 - - - 1.000000000000000 - - - -50.000000000000000 - - - - - - - - 0 - 0 - - - - Pre-edge - - - - - - - - 0 - 0 - - - - eV - - - -500.000000000000000 - - - 500.000000000000000 - - - -10.000000000000000 - - - - - - - - 0 - 0 - - - - Post-edge - - - - - - - - 0 - 0 - - - - to - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Norm. Order - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - eV - - - 0.000000000000000 - - - 1000.000000000000000 - - - 25.000000000000000 - - - - - - - - 0 - 0 - - - - <html><head/><body><p> For mbak algorithm only, must be in <span style=" font-weight:600; font-style:italic;">element&lt;space&gt;edge</span> format</p></body></html> - - - Fe K - - - - - - - - 0 - 0 - - - - Element: - - - - - - - - 0 - 0 - - - - eV - - - 0.000000000000000 - - - 1500.000000000000000 - - - 75.000000000000000 - - - - - - - Initial Guess - - - - - - - - 0 - 0 - - - - Apply to Spectrum - - - - - - - Apply to Stack - - - - - - - - - - - - - - Available Stacks - - - - - - - - - - - - 0 - 0 - 1364 - 34 - - - - - Help - - - - - - - File - - - - - - - - - - - true - - - Tools - - - - - - - View - - - - Plot Background - - - - - - - - - - Window Background - - - - - - - - Change_Plot_Line_Width - - - - - - - - - - - - - - - - - Spectrum - - - - - - - - - Image - - - - - - - - - - - - - - - - Open PDF - - - - - Open in GitHub (most updated) - - - - - true - - - Open Image Data - - - Support all tiff and specific h5 files - - - Ctrl+O - - - - - Close - - - - - Exit - - - Ctrl+Q - - - - - false - - - Export Tiff Stack - - - Save the displayed/Modified stack as a tiff file - - - Ctrl+S - - - - - Open PyXRF - - - - - Open Image J - - - - - Open TomViz - - - - - Open Mantis - - - - - Open Athena - - - - - DataBroker - - - - - Open HXN DB - - - - - false - - - Load Energy - - - Load list of energies for XANES stack. Supports only .txt fromat - - - Ctrl+E - - - - - Create a Virtual Stack - - - Create a stack from multiple tiff images of same shape - - - Ctrl+M - - - - - true - - - Open Mask Generator - - - A new window will be opened to creat threshold based masks - - - - - Create elist from log file - - - - - false - - - Save Energy List - - - - - true - - - MultiColorView - - - A new window will be opened to align images in a stack - - - - - White - - - - - true - - - true - - - Black - - - - - Red - - - - - Yellow - - - - - Blue - - - - - false - - - false - - - Dark Mode - - - - - Black - - - - - false - - - false - - - Default - - - - - Vivid - - - - - Save Sum Image - - - - - Subtract ROI as Background - - - - - Export Norm. Params - - - - - 2 - - - - - 4 - - - - - 6 - - - - - 10 - - - - - 2 - - - - - 3 - - - - - 4 - - - - - 5 - - - - - 6 - - - - - 8 - - - - - 10 - - - - - 2 - - - - - Import Norm. Params - - - - - 1 - - - - - Save Sum Spectrum - - - - - Save Mean Spectrum - - - - - Save Current Image as Mask - - - - - - PlotWidget - QGraphicsView -
pyqtgraph
-
- - ImageView - QGraphicsView -
pyqtgraph
-
-
- - -
diff --git a/xmidas/uis/midas.py b/xmidas/uis/midas.py deleted file mode 100644 index 4b0ea40..0000000 --- a/xmidas/uis/midas.py +++ /dev/null @@ -1,1048 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'mainwindow_admin.ui' -# -# Created by: PyQt5 UI code generator 5.14.1 -# -# WARNING! All changes made in this file will be lost! - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.setEnabled(True) - MainWindow.resize(1246, 936) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) - MainWindow.setSizePolicy(sizePolicy) - MainWindow.setStyleSheet("font: 10pt \"Segoe UI\";\n" -"\n" -"\n" -"QSlider::groove:horizontal {\n" -"border: 1px solid #bbb;\n" -"background: white;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::sub-page:horizontal {\n" -"background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,\n" -" stop: 0 #66e, stop: 1 #bbf);\n" -"background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1,\n" -" stop: 0 #bbf, stop: 1 #55f);\n" -"border: 1px solid #777;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::add-page:horizontal {\n" -"background: #fff;\n" -"border: 1px solid #777;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::handle:horizontal {\n" -"background: qlineargradient(x1:0, y1:0, x2:1, y2:1,\n" -" stop:0 #eee, stop:1 #ccc);\n" -"border: 1px solid #777;\n" -"width: 13px;\n" -"margin-top: -2px;\n" -"margin-bottom: -2px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::handle:horizontal:hover {\n" -"background: qlineargradient(x1:0, y1:0, x2:1, y2:1,\n" -" stop:0 #fff, stop:1 #ddd);\n" -"border: 1px solid #444;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::sub-page:horizontal:disabled {\n" -"background: #bbb;\n" -"border-color: #999;\n" -"}\n" -"\n" -"QSlider::add-page:horizontal:disabled {\n" -"background: #eee;\n" -"border-color: #999;\n" -"}\n" -"\n" -"QSlider::handle:horizontal:disabled {\n" -"background: #eee;\n" -"border: 1px solid #aaa;\n" -"border-radius: 4px;\n" -"}") - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) - self.centralwidget.setSizePolicy(sizePolicy) - self.centralwidget.setStyleSheet("QSlider::groove:horizontal {\n" -"border: 1px solid #bbb;\n" -"background: white;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::sub-page:horizontal {\n" -"background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,\n" -" stop: 0 #66e, stop: 1 #bbf);\n" -"background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1,\n" -" stop: 0 #bbf, stop: 1 #55f);\n" -"border: 1px solid #777;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::add-page:horizontal {\n" -"background: #fff;\n" -"border: 1px solid #777;\n" -"height: 10px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::handle:horizontal {\n" -"background: qlineargradient(x1:0, y1:0, x2:1, y2:1,\n" -" stop:0 #eee, stop:1 #ccc);\n" -"border: 1px solid #777;\n" -"width: 13px;\n" -"margin-top: -2px;\n" -"margin-bottom: -2px;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::handle:horizontal:hover {\n" -"background: qlineargradient(x1:0, y1:0, x2:1, y2:1,\n" -" stop:0 #fff, stop:1 #ddd);\n" -"border: 1px solid #444;\n" -"border-radius: 4px;\n" -"}\n" -"\n" -"QSlider::sub-page:horizontal:disabled {\n" -"background: #bbb;\n" -"border-color: #999;\n" -"}\n" -"\n" -"QSlider::add-page:horizontal:disabled {\n" -"background: #eee;\n" -"border-color: #999;\n" -"}\n" -"\n" -"QSlider::handle:horizontal:disabled {\n" -"background: #eee;\n" -"border: 1px solid #aaa;\n" -"border-radius: 4px;\n" -"}") - self.centralwidget.setObjectName("centralwidget") - self.gridLayout_16 = QtWidgets.QGridLayout(self.centralwidget) - self.gridLayout_16.setObjectName("gridLayout_16") - self.scrollArea = QtWidgets.QScrollArea(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy) - self.scrollArea.setStyleSheet("font:12pt \"Segoe UI\";") - self.scrollArea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setAlignment(QtCore.Qt.AlignCenter) - self.scrollArea.setObjectName("scrollArea") - self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 321, 855)) - self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") - self.gridLayout_9 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents) - self.gridLayout_9.setObjectName("gridLayout_9") - self.gridLayout_13 = QtWidgets.QGridLayout() - self.gridLayout_13.setObjectName("gridLayout_13") - self.pb_reset_img = QtWidgets.QPushButton(self.scrollAreaWidgetContents) - self.pb_reset_img.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_reset_img.setObjectName("pb_reset_img") - self.gridLayout_13.addWidget(self.pb_reset_img, 0, 0, 1, 1) - self.gridLayout_9.addLayout(self.gridLayout_13, 1, 0, 1, 1) - self.toolBox = QtWidgets.QToolBox(self.scrollAreaWidgetContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.toolBox.sizePolicy().hasHeightForWidth()) - self.toolBox.setSizePolicy(sizePolicy) - self.toolBox.setStyleSheet("font: 10pt \"Segoe UI\";") - self.toolBox.setObjectName("toolBox") - self.page = QtWidgets.QWidget() - self.page.setGeometry(QtCore.QRect(0, 0, 368, 331)) - self.page.setObjectName("page") - self.gridLayout_31 = QtWidgets.QGridLayout(self.page) - self.gridLayout_31.setObjectName("gridLayout_31") - self.frame_7 = QtWidgets.QFrame(self.page) - self.frame_7.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_7.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_7.setObjectName("frame_7") - self.gridLayout_30 = QtWidgets.QGridLayout(self.frame_7) - self.gridLayout_30.setObjectName("gridLayout_30") - self.gridLayout_17 = QtWidgets.QGridLayout() - self.gridLayout_17.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) - self.gridLayout_17.setObjectName("gridLayout_17") - self.sb_zrange1 = QtWidgets.QSpinBox(self.frame_7) - self.sb_zrange1.setMinimum(0) - self.sb_zrange1.setMaximum(500) - self.sb_zrange1.setProperty("value", 0) - self.sb_zrange1.setDisplayIntegerBase(10) - self.sb_zrange1.setObjectName("sb_zrange1") - self.gridLayout_17.addWidget(self.sb_zrange1, 0, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(self.frame_7) - self.label_5.setAlignment(QtCore.Qt.AlignCenter) - self.label_5.setObjectName("label_5") - self.gridLayout_17.addWidget(self.label_5, 1, 2, 1, 1) - self.label_9 = QtWidgets.QLabel(self.frame_7) - self.label_9.setAlignment(QtCore.Qt.AlignCenter) - self.label_9.setObjectName("label_9") - self.gridLayout_17.addWidget(self.label_9, 2, 2, 1, 1) - self.sb_yrange2 = QtWidgets.QSpinBox(self.frame_7) - self.sb_yrange2.setReadOnly(False) - self.sb_yrange2.setMinimum(1) - self.sb_yrange2.setMaximum(10000) - self.sb_yrange2.setProperty("value", 100) - self.sb_yrange2.setObjectName("sb_yrange2") - self.gridLayout_17.addWidget(self.sb_yrange2, 2, 3, 1, 1) - self.sb_zrange2 = QtWidgets.QSpinBox(self.frame_7) - self.sb_zrange2.setMinimum(1) - self.sb_zrange2.setMaximum(5000) - self.sb_zrange2.setProperty("value", 1200) - self.sb_zrange2.setDisplayIntegerBase(10) - self.sb_zrange2.setObjectName("sb_zrange2") - self.gridLayout_17.addWidget(self.sb_zrange2, 0, 3, 1, 1) - self.sb_xrange1 = QtWidgets.QSpinBox(self.frame_7) - self.sb_xrange1.setReadOnly(False) - self.sb_xrange1.setMinimum(0) - self.sb_xrange1.setMaximum(10000) - self.sb_xrange1.setProperty("value", 0) - self.sb_xrange1.setObjectName("sb_xrange1") - self.gridLayout_17.addWidget(self.sb_xrange1, 1, 1, 1, 1) - self.sb_xrange2 = QtWidgets.QSpinBox(self.frame_7) - self.sb_xrange2.setReadOnly(False) - self.sb_xrange2.setMinimum(1) - self.sb_xrange2.setMaximum(10000) - self.sb_xrange2.setProperty("value", 100) - self.sb_xrange2.setObjectName("sb_xrange2") - self.gridLayout_17.addWidget(self.sb_xrange2, 1, 3, 1, 1) - self.label_22 = QtWidgets.QLabel(self.frame_7) - self.label_22.setAlignment(QtCore.Qt.AlignCenter) - self.label_22.setObjectName("label_22") - self.gridLayout_17.addWidget(self.label_22, 0, 2, 1, 1) - self.label_19 = QtWidgets.QLabel(self.frame_7) - self.label_19.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_19.setObjectName("label_19") - self.gridLayout_17.addWidget(self.label_19, 1, 0, 1, 1) - self.sb_yrange1 = QtWidgets.QSpinBox(self.frame_7) - self.sb_yrange1.setReadOnly(False) - self.sb_yrange1.setMinimum(0) - self.sb_yrange1.setMaximum(10000) - self.sb_yrange1.setProperty("value", 0) - self.sb_yrange1.setObjectName("sb_yrange1") - self.gridLayout_17.addWidget(self.sb_yrange1, 2, 1, 1, 1) - self.label_15 = QtWidgets.QLabel(self.frame_7) - self.label_15.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_15.setObjectName("label_15") - self.gridLayout_17.addWidget(self.label_15, 2, 0, 1, 1) - self.label_20 = QtWidgets.QLabel(self.frame_7) - self.label_20.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_20.setObjectName("label_20") - self.gridLayout_17.addWidget(self.label_20, 0, 0, 1, 1) - self.gridLayout_30.addLayout(self.gridLayout_17, 0, 0, 1, 1) - self.pb_crop = QtWidgets.QPushButton(self.frame_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_crop.sizePolicy().hasHeightForWidth()) - self.pb_crop.setSizePolicy(sizePolicy) - self.pb_crop.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_crop.setObjectName("pb_crop") - self.gridLayout_30.addWidget(self.pb_crop, 1, 0, 1, 1) - self.gridLayout_31.addWidget(self.frame_7, 1, 0, 1, 1) - self.frame_6 = QtWidgets.QFrame(self.page) - self.frame_6.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_6.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_6.setObjectName("frame_6") - self.gridLayout_7 = QtWidgets.QGridLayout(self.frame_6) - self.gridLayout_7.setObjectName("gridLayout_7") - self.horizontalLayout_9 = QtWidgets.QHBoxLayout() - self.horizontalLayout_9.setObjectName("horizontalLayout_9") - self.cb_rebin = QtWidgets.QCheckBox(self.frame_6) - self.cb_rebin.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_rebin.sizePolicy().hasHeightForWidth()) - self.cb_rebin.setSizePolicy(sizePolicy) - self.cb_rebin.setObjectName("cb_rebin") - self.horizontalLayout_9.addWidget(self.cb_rebin) - self.horizontalLayout_10 = QtWidgets.QHBoxLayout() - self.horizontalLayout_10.setObjectName("horizontalLayout_10") - self.cb_upscale = QtWidgets.QCheckBox(self.frame_6) - self.cb_upscale.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_upscale.sizePolicy().hasHeightForWidth()) - self.cb_upscale.setSizePolicy(sizePolicy) - self.cb_upscale.setObjectName("cb_upscale") - self.horizontalLayout_10.addWidget(self.cb_upscale) - self.horizontalLayout_9.addLayout(self.horizontalLayout_10) - self.sb_scaling_factor = QtWidgets.QSpinBox(self.frame_6) - self.sb_scaling_factor.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sb_scaling_factor.sizePolicy().hasHeightForWidth()) - self.sb_scaling_factor.setSizePolicy(sizePolicy) - self.sb_scaling_factor.setStyleSheet("font: 10pt \"MS Shell Dlg 2\";") - self.sb_scaling_factor.setMinimum(2) - self.sb_scaling_factor.setMaximum(16) - self.sb_scaling_factor.setSingleStep(2) - self.sb_scaling_factor.setProperty("value", 2) - self.sb_scaling_factor.setObjectName("sb_scaling_factor") - self.horizontalLayout_9.addWidget(self.sb_scaling_factor) - self.gridLayout_7.addLayout(self.horizontalLayout_9, 2, 0, 1, 1) - self.cb_remove_edges = QtWidgets.QCheckBox(self.frame_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_remove_edges.sizePolicy().hasHeightForWidth()) - self.cb_remove_edges.setSizePolicy(sizePolicy) - self.cb_remove_edges.setObjectName("cb_remove_edges") - self.gridLayout_7.addWidget(self.cb_remove_edges, 0, 0, 1, 1) - self.gridLayout_31.addWidget(self.frame_6, 2, 0, 1, 1) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_31.addItem(spacerItem, 0, 0, 1, 1) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_31.addItem(spacerItem1, 3, 0, 1, 1) - self.toolBox.addItem(self.page, "") - self.page_2 = QtWidgets.QWidget() - self.page_2.setGeometry(QtCore.QRect(0, 0, 299, 462)) - self.page_2.setObjectName("page_2") - self.gridLayout_8 = QtWidgets.QGridLayout(self.page_2) - self.gridLayout_8.setObjectName("gridLayout_8") - self.groupBox = QtWidgets.QGroupBox(self.page_2) - self.groupBox.setTitle("") - self.groupBox.setObjectName("groupBox") - self.gridLayout_28 = QtWidgets.QGridLayout(self.groupBox) - self.gridLayout_28.setObjectName("gridLayout_28") - self.frame = QtWidgets.QFrame(self.groupBox) - self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame.setObjectName("frame") - self.gridLayout_27 = QtWidgets.QGridLayout(self.frame) - self.gridLayout_27.setObjectName("gridLayout_27") - self.groupBox_9 = QtWidgets.QGroupBox(self.frame) - self.groupBox_9.setObjectName("groupBox_9") - self.gridLayout_24 = QtWidgets.QGridLayout(self.groupBox_9) - self.gridLayout_24.setObjectName("gridLayout_24") - self.verticalLayout_3 = QtWidgets.QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.horizontalLayout_7 = QtWidgets.QHBoxLayout() - self.horizontalLayout_7.setObjectName("horizontalLayout_7") - self.cb_log = QtWidgets.QCheckBox(self.groupBox_9) - self.cb_log.setStyleSheet("") - self.cb_log.setChecked(False) - self.cb_log.setObjectName("cb_log") - self.horizontalLayout_7.addWidget(self.cb_log) - self.cb_norm = QtWidgets.QCheckBox(self.groupBox_9) - self.cb_norm.setObjectName("cb_norm") - self.horizontalLayout_7.addWidget(self.cb_norm) - self.verticalLayout_3.addLayout(self.horizontalLayout_7) - self.cb_transpose = QtWidgets.QCheckBox(self.groupBox_9) - self.cb_transpose.setLayoutDirection(QtCore.Qt.LeftToRight) - self.cb_transpose.setObjectName("cb_transpose") - self.verticalLayout_3.addWidget(self.cb_transpose) - self.gridLayout_24.addLayout(self.verticalLayout_3, 0, 0, 1, 1) - self.gridLayout_27.addWidget(self.groupBox_9, 0, 0, 1, 1) - self.gridLayout_28.addWidget(self.frame, 0, 0, 1, 1) - self.frame_4 = QtWidgets.QFrame(self.groupBox) - self.frame_4.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_4.setObjectName("frame_4") - self.gridLayout_25 = QtWidgets.QGridLayout(self.frame_4) - self.gridLayout_25.setObjectName("gridLayout_25") - self.gridLayout_21 = QtWidgets.QGridLayout() - self.gridLayout_21.setObjectName("gridLayout_21") - self.cb_remove_outliers = QtWidgets.QCheckBox(self.frame_4) - self.cb_remove_outliers.setObjectName("cb_remove_outliers") - self.gridLayout_21.addWidget(self.cb_remove_outliers, 0, 0, 1, 1) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.hs_nsigma = QtWidgets.QSlider(self.frame_4) - self.hs_nsigma.setEnabled(False) - self.hs_nsigma.setStyleSheet("border-color: rgb(0, 85, 255);") - self.hs_nsigma.setMaximum(200) - self.hs_nsigma.setProperty("value", 3) - self.hs_nsigma.setOrientation(QtCore.Qt.Horizontal) - self.hs_nsigma.setObjectName("hs_nsigma") - self.horizontalLayout.addWidget(self.hs_nsigma) - self.label_nsigma = QtWidgets.QLabel(self.frame_4) - self.label_nsigma.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_nsigma.setObjectName("label_nsigma") - self.horizontalLayout.addWidget(self.label_nsigma) - self.gridLayout_21.addLayout(self.horizontalLayout, 1, 0, 1, 1) - self.gridLayout_25.addLayout(self.gridLayout_21, 0, 0, 1, 1) - self.gridLayout_28.addWidget(self.frame_4, 1, 0, 1, 1) - self.frame_3 = QtWidgets.QFrame(self.groupBox) - self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_3.setObjectName("frame_3") - self.gridLayout_26 = QtWidgets.QGridLayout(self.frame_3) - self.gridLayout_26.setObjectName("gridLayout_26") - self.gridLayout_5 = QtWidgets.QGridLayout() - self.gridLayout_5.setObjectName("gridLayout_5") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.cb_remove_bg = QtWidgets.QCheckBox(self.frame_3) - self.cb_remove_bg.setObjectName("cb_remove_bg") - self.horizontalLayout_3.addWidget(self.cb_remove_bg) - self.gridLayout_5.addLayout(self.horizontalLayout_3, 0, 0, 1, 1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.hs_bg_threshold = QtWidgets.QSlider(self.frame_3) - self.hs_bg_threshold.setEnabled(False) - self.hs_bg_threshold.setMaximum(100) - self.hs_bg_threshold.setSingleStep(5) - self.hs_bg_threshold.setPageStep(5) - self.hs_bg_threshold.setProperty("value", 5) - self.hs_bg_threshold.setOrientation(QtCore.Qt.Horizontal) - self.hs_bg_threshold.setObjectName("hs_bg_threshold") - self.horizontalLayout_2.addWidget(self.hs_bg_threshold) - self.label_bg_threshold = QtWidgets.QLabel(self.frame_3) - self.label_bg_threshold.setObjectName("label_bg_threshold") - self.horizontalLayout_2.addWidget(self.label_bg_threshold) - self.gridLayout_5.addLayout(self.horizontalLayout_2, 1, 0, 1, 1) - self.gridLayout_26.addLayout(self.gridLayout_5, 0, 0, 1, 1) - self.gridLayout_28.addWidget(self.frame_3, 2, 0, 1, 1) - self.frame_2 = QtWidgets.QFrame(self.groupBox) - self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_2.setObjectName("frame_2") - self.gridLayout_3 = QtWidgets.QGridLayout(self.frame_2) - self.gridLayout_3.setObjectName("gridLayout_3") - self.cb_smooth = QtWidgets.QCheckBox(self.frame_2) - self.cb_smooth.setObjectName("cb_smooth") - self.gridLayout_3.addWidget(self.cb_smooth, 0, 0, 1, 1) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.hs_smooth_size = QtWidgets.QSlider(self.frame_2) - self.hs_smooth_size.setEnabled(False) - self.hs_smooth_size.setMinimum(3) - self.hs_smooth_size.setMaximum(12) - self.hs_smooth_size.setSingleStep(2) - self.hs_smooth_size.setPageStep(2) - self.hs_smooth_size.setProperty("value", 3) - self.hs_smooth_size.setOrientation(QtCore.Qt.Horizontal) - self.hs_smooth_size.setTickPosition(QtWidgets.QSlider.TicksBelow) - self.hs_smooth_size.setTickInterval(2) - self.hs_smooth_size.setObjectName("hs_smooth_size") - self.horizontalLayout_4.addWidget(self.hs_smooth_size) - self.smooth_winow_size = QtWidgets.QLabel(self.frame_2) - self.smooth_winow_size.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.smooth_winow_size.setObjectName("smooth_winow_size") - self.horizontalLayout_4.addWidget(self.smooth_winow_size) - self.gridLayout_3.addLayout(self.horizontalLayout_4, 1, 0, 1, 1) - self.gridLayout_28.addWidget(self.frame_2, 3, 0, 1, 1) - self.gridLayout_8.addWidget(self.groupBox, 0, 0, 1, 1) - self.toolBox.addItem(self.page_2, "") - self.gridLayout_9.addWidget(self.toolBox, 0, 0, 1, 1) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.gridLayout_9.addItem(spacerItem2, 2, 0, 1, 1) - self.toolBox_2 = QtWidgets.QToolBox(self.scrollAreaWidgetContents) - self.toolBox_2.setObjectName("toolBox_2") - self.page_5 = QtWidgets.QWidget() - self.page_5.setGeometry(QtCore.QRect(0, 0, 298, 189)) - self.page_5.setObjectName("page_5") - self.gridLayout_15 = QtWidgets.QGridLayout(self.page_5) - self.gridLayout_15.setObjectName("gridLayout_15") - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.pb_calc_components = QtWidgets.QPushButton(self.page_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_calc_components.sizePolicy().hasHeightForWidth()) - self.pb_calc_components.setSizePolicy(sizePolicy) - self.pb_calc_components.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_calc_components.setObjectName("pb_calc_components") - self.gridLayout.addWidget(self.pb_calc_components, 0, 0, 1, 1) - self.pb_pca_scree = QtWidgets.QPushButton(self.page_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_pca_scree.sizePolicy().hasHeightForWidth()) - self.pb_pca_scree.setSizePolicy(sizePolicy) - self.pb_pca_scree.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_pca_scree.setObjectName("pb_pca_scree") - self.gridLayout.addWidget(self.pb_pca_scree, 1, 0, 1, 1) - self.horizontalLayout_5 = QtWidgets.QHBoxLayout() - self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.label_11 = QtWidgets.QLabel(self.page_5) - self.label_11.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_11.setObjectName("label_11") - self.horizontalLayout_5.addWidget(self.label_11) - self.cb_comp_method = QtWidgets.QComboBox(self.page_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_comp_method.sizePolicy().hasHeightForWidth()) - self.cb_comp_method.setSizePolicy(sizePolicy) - self.cb_comp_method.setObjectName("cb_comp_method") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.cb_comp_method.addItem("") - self.horizontalLayout_5.addWidget(self.cb_comp_method) - self.gridLayout.addLayout(self.horizontalLayout_5, 2, 0, 1, 1) - self.horizontalLayout_6 = QtWidgets.QHBoxLayout() - self.horizontalLayout_6.setObjectName("horizontalLayout_6") - self.label = QtWidgets.QLabel(self.page_5) - self.label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label.setObjectName("label") - self.horizontalLayout_6.addWidget(self.label) - self.sb_ncomp = QtWidgets.QSpinBox(self.page_5) - self.sb_ncomp.setMinimum(1) - self.sb_ncomp.setProperty("value", 4) - self.sb_ncomp.setObjectName("sb_ncomp") - self.horizontalLayout_6.addWidget(self.sb_ncomp) - self.gridLayout.addLayout(self.horizontalLayout_6, 3, 0, 1, 1) - self.gridLayout_15.addLayout(self.gridLayout, 0, 0, 1, 1) - self.toolBox_2.addItem(self.page_5, "") - self.page_6 = QtWidgets.QWidget() - self.page_6.setGeometry(QtCore.QRect(0, 0, 308, 185)) - self.page_6.setObjectName("page_6") - self.gridLayout_18 = QtWidgets.QGridLayout(self.page_6) - self.gridLayout_18.setObjectName("gridLayout_18") - self.gridLayout_4 = QtWidgets.QGridLayout() - self.gridLayout_4.setObjectName("gridLayout_4") - self.pb_calc_cluster = QtWidgets.QPushButton(self.page_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_calc_cluster.sizePolicy().hasHeightForWidth()) - self.pb_calc_cluster.setSizePolicy(sizePolicy) - self.pb_calc_cluster.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_calc_cluster.setObjectName("pb_calc_cluster") - self.gridLayout_4.addWidget(self.pb_calc_cluster, 0, 0, 1, 3) - self.pb_kmeans_elbow = QtWidgets.QPushButton(self.page_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_kmeans_elbow.sizePolicy().hasHeightForWidth()) - self.pb_kmeans_elbow.setSizePolicy(sizePolicy) - self.pb_kmeans_elbow.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_kmeans_elbow.setObjectName("pb_kmeans_elbow") - self.gridLayout_4.addWidget(self.pb_kmeans_elbow, 1, 0, 1, 3) - self.label_10 = QtWidgets.QLabel(self.page_6) - self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_10.setObjectName("label_10") - self.gridLayout_4.addWidget(self.label_10, 2, 0, 1, 1) - self.cb_clust_method = QtWidgets.QComboBox(self.page_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_clust_method.sizePolicy().hasHeightForWidth()) - self.cb_clust_method.setSizePolicy(sizePolicy) - self.cb_clust_method.setObjectName("cb_clust_method") - self.cb_clust_method.addItem("") - self.cb_clust_method.addItem("") - self.cb_clust_method.addItem("") - self.cb_clust_method.addItem("") - self.cb_clust_method.addItem("") - self.cb_clust_method.addItem("") - self.gridLayout_4.addWidget(self.cb_clust_method, 2, 1, 1, 2) - self.label_2 = QtWidgets.QLabel(self.page_6) - self.label_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_2.setObjectName("label_2") - self.gridLayout_4.addWidget(self.label_2, 3, 0, 1, 2) - self.sb_ncluster = QtWidgets.QSpinBox(self.page_6) - self.sb_ncluster.setMinimum(1) - self.sb_ncluster.setProperty("value", 4) - self.sb_ncluster.setObjectName("sb_ncluster") - self.gridLayout_4.addWidget(self.sb_ncluster, 3, 2, 1, 1) - self.gridLayout_18.addLayout(self.gridLayout_4, 0, 0, 1, 1) - self.toolBox_2.addItem(self.page_6, "") - self.page_7 = QtWidgets.QWidget() - self.page_7.setGeometry(QtCore.QRect(0, 0, 299, 156)) - self.page_7.setObjectName("page_7") - self.gridLayout_20 = QtWidgets.QGridLayout(self.page_7) - self.gridLayout_20.setObjectName("gridLayout_20") - self.gridLayout_2 = QtWidgets.QGridLayout() - self.gridLayout_2.setObjectName("gridLayout_2") - self.pb_elist_xanes = QtWidgets.QPushButton(self.page_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_elist_xanes.sizePolicy().hasHeightForWidth()) - self.pb_elist_xanes.setSizePolicy(sizePolicy) - self.pb_elist_xanes.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_elist_xanes.setObjectName("pb_elist_xanes") - self.gridLayout_2.addWidget(self.pb_elist_xanes, 0, 0, 1, 2) - self.cb_kev_flag = QtWidgets.QCheckBox(self.page_7) - self.cb_kev_flag.setObjectName("cb_kev_flag") - self.gridLayout_2.addWidget(self.cb_kev_flag, 0, 2, 1, 1) - self.pb_ref_xanes = QtWidgets.QPushButton(self.page_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_ref_xanes.sizePolicy().hasHeightForWidth()) - self.pb_ref_xanes.setSizePolicy(sizePolicy) - self.pb_ref_xanes.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_ref_xanes.setObjectName("pb_ref_xanes") - self.gridLayout_2.addWidget(self.pb_ref_xanes, 1, 0, 1, 2) - self.pb_plot_refs = QtWidgets.QPushButton(self.page_7) - self.pb_plot_refs.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);") - self.pb_plot_refs.setObjectName("pb_plot_refs") - self.gridLayout_2.addWidget(self.pb_plot_refs, 1, 2, 1, 1) - self.cb_xanes_fitting_method = QtWidgets.QComboBox(self.page_7) - self.cb_xanes_fitting_method.setObjectName("cb_xanes_fitting_method") - self.cb_xanes_fitting_method.addItem("") - self.gridLayout_2.addWidget(self.cb_xanes_fitting_method, 2, 0, 1, 1) - self.pb_xanes_fit = QtWidgets.QPushButton(self.page_7) - self.pb_xanes_fit.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pb_xanes_fit.sizePolicy().hasHeightForWidth()) - self.pb_xanes_fit.setSizePolicy(sizePolicy) - self.pb_xanes_fit.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);\n" -"") - self.pb_xanes_fit.setObjectName("pb_xanes_fit") - self.gridLayout_2.addWidget(self.pb_xanes_fit, 2, 1, 1, 2) - self.gridLayout_20.addLayout(self.gridLayout_2, 0, 0, 1, 1) - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_20.addItem(spacerItem3, 1, 0, 1, 1) - self.toolBox_2.addItem(self.page_7, "") - self.gridLayout_9.addWidget(self.toolBox_2, 3, 0, 1, 1) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.gridLayout_16.addWidget(self.scrollArea, 0, 0, 1, 1) - self.groupBox_6 = QtWidgets.QGroupBox(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.groupBox_6.sizePolicy().hasHeightForWidth()) - self.groupBox_6.setSizePolicy(sizePolicy) - self.groupBox_6.setStyleSheet("font:12pt \"Segoe UI\";") - self.groupBox_6.setTitle("") - self.groupBox_6.setObjectName("groupBox_6") - self.gridLayout_29 = QtWidgets.QGridLayout(self.groupBox_6) - self.gridLayout_29.setObjectName("gridLayout_29") - self.frame_5 = QtWidgets.QFrame(self.groupBox_6) - self.frame_5.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_5.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_5.setObjectName("frame_5") - self.gridLayout_23 = QtWidgets.QGridLayout(self.frame_5) - self.gridLayout_23.setObjectName("gridLayout_23") - self.groupBox_8 = QtWidgets.QGroupBox(self.frame_5) - self.groupBox_8.setStyleSheet("font: 9pt \"Segoe UI\";") - self.groupBox_8.setObjectName("groupBox_8") - self.gridLayout_19 = QtWidgets.QGridLayout(self.groupBox_8) - self.gridLayout_19.setObjectName("gridLayout_19") - self.groupBox_5 = QtWidgets.QGroupBox(self.groupBox_8) - self.groupBox_5.setObjectName("groupBox_5") - self.gridLayout_11 = QtWidgets.QGridLayout(self.groupBox_5) - self.gridLayout_11.setObjectName("gridLayout_11") - self.le_spec_roi_size = QtWidgets.QLabel(self.groupBox_5) - self.le_spec_roi_size.setStyleSheet("color: rgb(255, 0,0);") - self.le_spec_roi_size.setObjectName("le_spec_roi_size") - self.gridLayout_11.addWidget(self.le_spec_roi_size, 1, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(self.groupBox_5) - self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_8.setObjectName("label_8") - self.gridLayout_11.addWidget(self.label_8, 0, 0, 1, 1) - self.label_29 = QtWidgets.QLabel(self.groupBox_5) - self.label_29.setStyleSheet("") - self.label_29.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_29.setObjectName("label_29") - self.gridLayout_11.addWidget(self.label_29, 1, 0, 1, 1) - self.le_spec_roi = QtWidgets.QLabel(self.groupBox_5) - self.le_spec_roi.setStyleSheet("color: rgb(255, 0,0);") - self.le_spec_roi.setObjectName("le_spec_roi") - self.gridLayout_11.addWidget(self.le_spec_roi, 0, 1, 1, 1) - self.gridLayout_19.addWidget(self.groupBox_5, 0, 1, 1, 1) - self.groupBox_4 = QtWidgets.QGroupBox(self.groupBox_8) - self.groupBox_4.setObjectName("groupBox_4") - self.gridLayout_12 = QtWidgets.QGridLayout(self.groupBox_4) - self.gridLayout_12.setObjectName("gridLayout_12") - self.label_4 = QtWidgets.QLabel(self.groupBox_4) - self.label_4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_4.setObjectName("label_4") - self.gridLayout_12.addWidget(self.label_4, 0, 0, 1, 1) - self.le_roi = QtWidgets.QLabel(self.groupBox_4) - self.le_roi.setStyleSheet("color: rgb(255, 0,0);") - self.le_roi.setObjectName("le_roi") - self.gridLayout_12.addWidget(self.le_roi, 0, 1, 1, 1) - self.label_28 = QtWidgets.QLabel(self.groupBox_4) - self.label_28.setStyleSheet("") - self.label_28.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_28.setObjectName("label_28") - self.gridLayout_12.addWidget(self.label_28, 1, 0, 1, 1) - self.le_roi_size = QtWidgets.QLabel(self.groupBox_4) - self.le_roi_size.setStyleSheet("color: rgb(255, 0, 0);") - self.le_roi_size.setObjectName("le_roi_size") - self.gridLayout_12.addWidget(self.le_roi_size, 1, 1, 1, 1) - self.gridLayout_19.addWidget(self.groupBox_4, 0, 0, 1, 1) - self.gridLayout_23.addWidget(self.groupBox_8, 0, 0, 1, 1) - self.groupBox_3 = QtWidgets.QGroupBox(self.frame_5) - self.groupBox_3.setObjectName("groupBox_3") - self.gridLayout_6 = QtWidgets.QGridLayout(self.groupBox_3) - self.gridLayout_6.setObjectName("gridLayout_6") - self.rb_math_roi_img = QtWidgets.QRadioButton(self.groupBox_3) - self.rb_math_roi_img.setLayoutDirection(QtCore.Qt.LeftToRight) - self.rb_math_roi_img.setObjectName("rb_math_roi_img") - self.gridLayout_6.addWidget(self.rb_math_roi_img, 0, 0, 1, 1) - self.pb_reset_roi_2 = QtWidgets.QPushButton(self.groupBox_3) - self.pb_reset_roi_2.setStyleSheet("background-color: rgb(175, 236, 255);") - self.pb_reset_roi_2.setObjectName("pb_reset_roi_2") - self.gridLayout_6.addWidget(self.pb_reset_roi_2, 0, 1, 1, 1) - self.label_14 = QtWidgets.QLabel(self.groupBox_3) - self.label_14.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_14.setObjectName("label_14") - self.gridLayout_6.addWidget(self.label_14, 1, 0, 1, 1) - self.cb_img_roi_action = QtWidgets.QComboBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_img_roi_action.sizePolicy().hasHeightForWidth()) - self.cb_img_roi_action.setSizePolicy(sizePolicy) - self.cb_img_roi_action.setObjectName("cb_img_roi_action") - self.cb_img_roi_action.addItem("") - self.cb_img_roi_action.addItem("") - self.cb_img_roi_action.addItem("") - self.cb_img_roi_action.addItem("") - self.gridLayout_6.addWidget(self.cb_img_roi_action, 1, 1, 1, 1) - self.gridLayout_23.addWidget(self.groupBox_3, 3, 0, 1, 1) - self.groupBox_7 = QtWidgets.QGroupBox(self.frame_5) - self.groupBox_7.setObjectName("groupBox_7") - self.gridLayout_14 = QtWidgets.QGridLayout(self.groupBox_7) - self.gridLayout_14.setObjectName("gridLayout_14") - self.rb_math_roi = QtWidgets.QRadioButton(self.groupBox_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.rb_math_roi.sizePolicy().hasHeightForWidth()) - self.rb_math_roi.setSizePolicy(sizePolicy) - self.rb_math_roi.setCheckable(True) - self.rb_math_roi.setChecked(False) - self.rb_math_roi.setObjectName("rb_math_roi") - self.gridLayout_14.addWidget(self.rb_math_roi, 0, 0, 1, 2) - self.label_13 = QtWidgets.QLabel(self.groupBox_7) - self.label_13.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_13.setObjectName("label_13") - self.gridLayout_14.addWidget(self.label_13, 1, 0, 1, 1) - self.cb_roi_operation = QtWidgets.QComboBox(self.groupBox_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cb_roi_operation.sizePolicy().hasHeightForWidth()) - self.cb_roi_operation.setSizePolicy(sizePolicy) - self.cb_roi_operation.setObjectName("cb_roi_operation") - self.cb_roi_operation.addItem("") - self.cb_roi_operation.addItem("") - self.cb_roi_operation.addItem("") - self.cb_roi_operation.addItem("") - self.gridLayout_14.addWidget(self.cb_roi_operation, 1, 1, 1, 1) - self.gridLayout_23.addWidget(self.groupBox_7, 10, 0, 1, 1) - self.pb_save_disp_img = QtWidgets.QPushButton(self.frame_5) - self.pb_save_disp_img.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);") - self.pb_save_disp_img.setObjectName("pb_save_disp_img") - self.gridLayout_23.addWidget(self.pb_save_disp_img, 13, 0, 1, 1) - spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_23.addItem(spacerItem4, 4, 0, 1, 1) - self.pb_save_disp_spec = QtWidgets.QPushButton(self.frame_5) - self.pb_save_disp_spec.setStyleSheet("background-color: rgb(175, 236, 255);\n" -"color: rgb(255, 0, 127);") - self.pb_save_disp_spec.setObjectName("pb_save_disp_spec") - self.gridLayout_23.addWidget(self.pb_save_disp_spec, 14, 0, 1, 1) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_23.addItem(spacerItem5, 12, 0, 1, 1) - self.groupBox_2 = QtWidgets.QGroupBox(self.frame_5) - self.groupBox_2.setObjectName("groupBox_2") - self.gridLayout_22 = QtWidgets.QGridLayout(self.groupBox_2) - self.gridLayout_22.setObjectName("gridLayout_22") - self.verticalLayout = QtWidgets.QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.rb_poly_roi = QtWidgets.QRadioButton(self.groupBox_2) - self.rb_poly_roi.setChecked(True) - self.rb_poly_roi.setObjectName("rb_poly_roi") - self.verticalLayout.addWidget(self.rb_poly_roi) - self.rb_rect_roi = QtWidgets.QRadioButton(self.groupBox_2) - self.rb_rect_roi.setObjectName("rb_rect_roi") - self.verticalLayout.addWidget(self.rb_rect_roi) - self.gridLayout_22.addLayout(self.verticalLayout, 0, 0, 1, 2) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.rb_elli_roi = QtWidgets.QRadioButton(self.groupBox_2) - self.rb_elli_roi.setObjectName("rb_elli_roi") - self.verticalLayout_2.addWidget(self.rb_elli_roi) - self.rb_circle_roi = QtWidgets.QRadioButton(self.groupBox_2) - self.rb_circle_roi.setObjectName("rb_circle_roi") - self.verticalLayout_2.addWidget(self.rb_circle_roi) - self.gridLayout_22.addLayout(self.verticalLayout_2, 0, 2, 1, 1) - self.rb_line_roi = QtWidgets.QRadioButton(self.groupBox_2) - self.rb_line_roi.setObjectName("rb_line_roi") - self.gridLayout_22.addWidget(self.rb_line_roi, 1, 0, 1, 1) - self.gridLayout_23.addWidget(self.groupBox_2, 1, 0, 1, 1) - spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout_23.addItem(spacerItem6, 2, 0, 1, 1) - self.gridLayout_29.addWidget(self.frame_5, 0, 2, 2, 1) - self.image_view = ImageView(self.groupBox_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.image_view.sizePolicy().hasHeightForWidth()) - self.image_view.setSizePolicy(sizePolicy) - self.image_view.setObjectName("image_view") - self.gridLayout_29.addWidget(self.image_view, 0, 1, 1, 1) - self.spectrum_view = PlotWidget(self.groupBox_6) - self.spectrum_view.setObjectName("spectrum_view") - self.gridLayout_29.addWidget(self.spectrum_view, 1, 1, 1, 1) - self.gridLayout_16.addWidget(self.groupBox_6, 0, 1, 1, 1) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1246, 29)) - self.menubar.setObjectName("menubar") - self.menuManual = QtWidgets.QMenu(self.menubar) - self.menuManual.setObjectName("menuManual") - self.menuFile = QtWidgets.QMenu(self.menubar) - self.menuFile.setObjectName("menuFile") - self.menuMask = QtWidgets.QMenu(self.menubar) - self.menuMask.setObjectName("menuMask") - MainWindow.setMenuBar(self.menubar) - self.statusbar_main = QtWidgets.QStatusBar(MainWindow) - self.statusbar_main.setObjectName("statusbar_main") - MainWindow.setStatusBar(self.statusbar_main) - self.actionOpen_PDF = QtWidgets.QAction(MainWindow) - self.actionOpen_PDF.setObjectName("actionOpen_PDF") - self.actionOpen_in_GitHub = QtWidgets.QAction(MainWindow) - self.actionOpen_in_GitHub.setObjectName("actionOpen_in_GitHub") - self.actionOpen_Image_Data = QtWidgets.QAction(MainWindow) - self.actionOpen_Image_Data.setEnabled(True) - self.actionOpen_Image_Data.setObjectName("actionOpen_Image_Data") - self.actionClose = QtWidgets.QAction(MainWindow) - self.actionClose.setObjectName("actionClose") - self.actionExit = QtWidgets.QAction(MainWindow) - self.actionExit.setObjectName("actionExit") - self.actionSave_as = QtWidgets.QAction(MainWindow) - self.actionSave_as.setObjectName("actionSave_as") - self.actionOpen_PyXRF = QtWidgets.QAction(MainWindow) - self.actionOpen_PyXRF.setObjectName("actionOpen_PyXRF") - self.actionOpen_Image_J = QtWidgets.QAction(MainWindow) - self.actionOpen_Image_J.setObjectName("actionOpen_Image_J") - self.actionOpen_TomViz = QtWidgets.QAction(MainWindow) - self.actionOpen_TomViz.setObjectName("actionOpen_TomViz") - self.actionOpen_Mantis = QtWidgets.QAction(MainWindow) - self.actionOpen_Mantis.setObjectName("actionOpen_Mantis") - self.actionOpen_Athena = QtWidgets.QAction(MainWindow) - self.actionOpen_Athena.setObjectName("actionOpen_Athena") - self.actionDataBroker = QtWidgets.QAction(MainWindow) - self.actionDataBroker.setObjectName("actionDataBroker") - self.actionOpen_HXN_DB = QtWidgets.QAction(MainWindow) - self.actionOpen_HXN_DB.setObjectName("actionOpen_HXN_DB") - self.actionLoad_Energy = QtWidgets.QAction(MainWindow) - self.actionLoad_Energy.setObjectName("actionLoad_Energy") - self.actionOpen_Multiple_Files = QtWidgets.QAction(MainWindow) - self.actionOpen_Multiple_Files.setObjectName("actionOpen_Multiple_Files") - self.actionOpen_Mask_Gen = QtWidgets.QAction(MainWindow) - self.actionOpen_Mask_Gen.setObjectName("actionOpen_Mask_Gen") - self.actionCreate_elist_from_log = QtWidgets.QAction(MainWindow) - self.actionCreate_elist_from_log.setObjectName("actionCreate_elist_from_log") - self.actionSave_Energy_List = QtWidgets.QAction(MainWindow) - self.actionSave_Energy_List.setObjectName("actionSave_Energy_List") - self.actionAlign_Stack = QtWidgets.QAction(MainWindow) - self.actionAlign_Stack.setObjectName("actionAlign_Stack") - self.menuManual.addSeparator() - self.menuManual.addAction(self.actionOpen_in_GitHub) - self.menuFile.addAction(self.actionOpen_Image_Data) - self.menuFile.addAction(self.actionOpen_Multiple_Files) - self.menuFile.addAction(self.actionLoad_Energy) - self.menuFile.addAction(self.actionSave_Energy_List) - self.menuFile.addAction(self.actionSave_as) - self.menuFile.addAction(self.actionExit) - self.menuMask.addAction(self.actionOpen_Mask_Gen) - self.menuMask.addAction(self.actionAlign_Stack) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuMask.menuAction()) - self.menubar.addAction(self.menuManual.menuAction()) - - self.retranslateUi(MainWindow) - self.toolBox.setCurrentIndex(1) - self.toolBox_2.setCurrentIndex(2) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "NSLS-II MIDAS")) - self.pb_reset_img.setText(_translate("MainWindow", "Reset Image")) - self.label_5.setText(_translate("MainWindow", "to")) - self.label_9.setText(_translate("MainWindow", "to")) - self.sb_yrange2.setSuffix(_translate("MainWindow", " px")) - self.sb_xrange1.setSuffix(_translate("MainWindow", " px")) - self.sb_xrange2.setSuffix(_translate("MainWindow", " px")) - self.label_22.setText(_translate("MainWindow", "to")) - self.label_19.setText(_translate("MainWindow", "X Dimension")) - self.sb_yrange1.setSuffix(_translate("MainWindow", " px")) - self.label_15.setText(_translate("MainWindow", "Y Dimension")) - self.label_20.setText(_translate("MainWindow", "Stack Range ")) - self.pb_crop.setToolTip(_translate("MainWindow", "Adjust the dimensions of the image")) - self.pb_crop.setText(_translate("MainWindow", "Update")) - self.cb_rebin.setText(_translate("MainWindow", "rebin")) - self.cb_upscale.setText(_translate("MainWindow", "Upscale")) - self.cb_remove_edges.setToolTip(_translate("MainWindow", "Removes one column/row from all four edges")) - self.cb_remove_edges.setText(_translate("MainWindow", "Remove Edges")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page), _translate("MainWindow", "Adjust Image Dimensions")) - self.groupBox_9.setTitle(_translate("MainWindow", "Transform")) - self.cb_log.setToolTip(_translate("MainWindow", "Covert image dat to log values.")) - self.cb_log.setText(_translate("MainWindow", "Log ")) - self.cb_norm.setToolTip(_translate("MainWindow", "Intensity Normalization with the last Frame.")) - self.cb_norm.setText(_translate("MainWindow", "Normalize")) - self.cb_transpose.setToolTip(_translate("MainWindow", "Reverse the arrays. 012 will be 210")) - self.cb_transpose.setText(_translate("MainWindow", "Transpose")) - self.cb_remove_outliers.setText(_translate("MainWindow", "Remove Outliers (NSigma)")) - self.label_nsigma.setText(_translate("MainWindow", "1")) - self.cb_remove_bg.setText(_translate("MainWindow", "Thresholding")) - self.label_bg_threshold.setText(_translate("MainWindow", "5")) - self.cb_smooth.setToolTip(_translate("MainWindow", "uses savgol_filter to smooth data")) - self.cb_smooth.setText(_translate("MainWindow", "Smoothen")) - self.smooth_winow_size.setText(_translate("MainWindow", "Window size")) - self.toolBox.setItemText(self.toolBox.indexOf(self.page_2), _translate("MainWindow", "Image Processing")) - self.pb_calc_components.setText(_translate("MainWindow", "Calculate Components")) - self.pb_pca_scree.setText(_translate("MainWindow", "PCA Scree Plot")) - self.label_11.setText(_translate("MainWindow", "Method")) - self.cb_comp_method.setItemText(0, _translate("MainWindow", "PCA")) - self.cb_comp_method.setItemText(1, _translate("MainWindow", "NMF")) - self.cb_comp_method.setItemText(2, _translate("MainWindow", "FastICA")) - self.cb_comp_method.setItemText(3, _translate("MainWindow", "IncrementalPCA")) - self.cb_comp_method.setItemText(4, _translate("MainWindow", "TruncatedSVD")) - self.cb_comp_method.setItemText(5, _translate("MainWindow", "FactorAnalysis")) - self.cb_comp_method.setItemText(6, _translate("MainWindow", "DictionaryLearning")) - self.label.setText(_translate("MainWindow", "Number of Components")) - self.toolBox_2.setItemText(self.toolBox_2.indexOf(self.page_5), _translate("MainWindow", "Component Analysis")) - self.pb_calc_cluster.setText(_translate("MainWindow", "Calculate Clusters")) - self.pb_kmeans_elbow.setText(_translate("MainWindow", "KMeans Variance Plot")) - self.label_10.setText(_translate("MainWindow", "Method")) - self.cb_clust_method.setItemText(0, _translate("MainWindow", "KMeans")) - self.cb_clust_method.setItemText(1, _translate("MainWindow", "MiniBatchKMeans")) - self.cb_clust_method.setItemText(2, _translate("MainWindow", "MeanShift")) - self.cb_clust_method.setItemText(3, _translate("MainWindow", "Spectral Clustering")) - self.cb_clust_method.setItemText(4, _translate("MainWindow", "Correlation-Kmeans")) - self.cb_clust_method.setItemText(5, _translate("MainWindow", "Affinity Propagation")) - self.label_2.setText(_translate("MainWindow", "Number of Clusters")) - self.toolBox_2.setItemText(self.toolBox_2.indexOf(self.page_6), _translate("MainWindow", "Cluster Analysis")) - self.pb_elist_xanes.setText(_translate("MainWindow", "Load Energy List")) - self.cb_kev_flag.setText(_translate("MainWindow", "keV")) - self.pb_ref_xanes.setText(_translate("MainWindow", "Load Ref. Spec.")) - self.pb_plot_refs.setText(_translate("MainWindow", "Plot")) - self.cb_xanes_fitting_method.setItemText(0, _translate("MainWindow", "NNLS")) - self.pb_xanes_fit.setText(_translate("MainWindow", " Fit")) - self.toolBox_2.setItemText(self.toolBox_2.indexOf(self.page_7), _translate("MainWindow", "XANES Fitting")) - self.groupBox_8.setTitle(_translate("MainWindow", "ROI Positions")) - self.groupBox_5.setTitle(_translate("MainWindow", "Spectrum ROI")) - self.le_spec_roi_size.setText(_translate("MainWindow", "roi_size")) - self.label_8.setText(_translate("MainWindow", "Range (eV):")) - self.label_29.setText(_translate("MainWindow", "Size (eV):")) - self.le_spec_roi.setText(_translate("MainWindow", "roix, roiy")) - self.groupBox_4.setTitle(_translate("MainWindow", "Image ROI")) - self.label_4.setText(_translate("MainWindow", "Position:")) - self.le_roi.setText(_translate("MainWindow", "roix, roiy")) - self.label_28.setText(_translate("MainWindow", "Size(pixels):")) - self.le_roi_size.setText(_translate("MainWindow", "roi_size")) - self.groupBox_3.setTitle(_translate("MainWindow", "Image Calculations")) - self.rb_math_roi_img.setText(_translate("MainWindow", "Add Math ROI")) - self.pb_reset_roi_2.setText(_translate("MainWindow", "Reset")) - self.label_14.setText(_translate("MainWindow", "Action:")) - self.cb_img_roi_action.setItemText(0, _translate("MainWindow", "Subtract")) - self.cb_img_roi_action.setItemText(1, _translate("MainWindow", "Divide")) - self.cb_img_roi_action.setItemText(2, _translate("MainWindow", "Add")) - self.cb_img_roi_action.setItemText(3, _translate("MainWindow", "Compare")) - self.groupBox_7.setTitle(_translate("MainWindow", "Spectrum Calculations")) - self.rb_math_roi.setText(_translate("MainWindow", "Add Math ROI")) - self.label_13.setText(_translate("MainWindow", "Action:")) - self.cb_roi_operation.setItemText(0, _translate("MainWindow", "Divide")) - self.cb_roi_operation.setItemText(1, _translate("MainWindow", "Subtract")) - self.cb_roi_operation.setItemText(2, _translate("MainWindow", "Add")) - self.cb_roi_operation.setItemText(3, _translate("MainWindow", "Correlation Plot")) - self.pb_save_disp_img.setText(_translate("MainWindow", "Save Current Image (.tiff)")) - self.pb_save_disp_spec.setText(_translate("MainWindow", "Save Current Spectrum (.txt)")) - self.groupBox_2.setTitle(_translate("MainWindow", "ROI Shape")) - self.rb_poly_roi.setText(_translate("MainWindow", "Polygon (default)")) - self.rb_rect_roi.setText(_translate("MainWindow", "Rectangle")) - self.rb_elli_roi.setText(_translate("MainWindow", "Ellipse")) - self.rb_circle_roi.setText(_translate("MainWindow", "Circle")) - self.rb_line_roi.setText(_translate("MainWindow", "Line")) - self.menuManual.setTitle(_translate("MainWindow", "Help")) - self.menuFile.setTitle(_translate("MainWindow", "File")) - self.menuMask.setTitle(_translate("MainWindow", "Tools")) - self.actionOpen_PDF.setText(_translate("MainWindow", "Open PDF")) - self.actionOpen_in_GitHub.setText(_translate("MainWindow", "Open in GitHub (most updated)")) - self.actionOpen_Image_Data.setText(_translate("MainWindow", "Open Image Data")) - self.actionOpen_Image_Data.setToolTip(_translate("MainWindow", "Support all tiff and specific h5 files")) - self.actionOpen_Image_Data.setShortcut(_translate("MainWindow", "Ctrl+O")) - self.actionClose.setText(_translate("MainWindow", "Close")) - self.actionExit.setText(_translate("MainWindow", "Exit")) - self.actionExit.setShortcut(_translate("MainWindow", "Ctrl+Q")) - self.actionSave_as.setText(_translate("MainWindow", "Export Tiff Stack")) - self.actionSave_as.setToolTip(_translate("MainWindow", "Save the displayed/Modified stack as a tiff file")) - self.actionSave_as.setShortcut(_translate("MainWindow", "Ctrl+S")) - self.actionOpen_PyXRF.setText(_translate("MainWindow", "Open PyXRF")) - self.actionOpen_Image_J.setText(_translate("MainWindow", "Open Image J")) - self.actionOpen_TomViz.setText(_translate("MainWindow", "Open TomViz")) - self.actionOpen_Mantis.setText(_translate("MainWindow", "Open Mantis")) - self.actionOpen_Athena.setText(_translate("MainWindow", "Open Athena")) - self.actionDataBroker.setText(_translate("MainWindow", "DataBroker")) - self.actionOpen_HXN_DB.setText(_translate("MainWindow", "Open HXN DB")) - self.actionLoad_Energy.setText(_translate("MainWindow", "Load Energy")) - self.actionLoad_Energy.setToolTip(_translate("MainWindow", "Load list of energies for XANES stack. Supports only .txt fromat")) - self.actionLoad_Energy.setShortcut(_translate("MainWindow", "Ctrl+E")) - self.actionOpen_Multiple_Files.setText(_translate("MainWindow", "Open Multiple Files")) - self.actionOpen_Multiple_Files.setToolTip(_translate("MainWindow", "Create a stack from multiple tiff images of same shape")) - self.actionOpen_Multiple_Files.setShortcut(_translate("MainWindow", "Ctrl+M")) - self.actionOpen_Mask_Gen.setText(_translate("MainWindow", "Open Mask Generator")) - self.actionOpen_Mask_Gen.setToolTip(_translate("MainWindow", "A new window will be opened to creat threshold based masks")) - self.actionCreate_elist_from_log.setText(_translate("MainWindow", "Create elist from log file")) - self.actionSave_Energy_List.setText(_translate("MainWindow", "Save Energy List")) - self.actionAlign_Stack.setText(_translate("MainWindow", "Align Stack")) - self.actionAlign_Stack.setToolTip(_translate("MainWindow", "A new window will be opened to align images in a stack")) -from pyqtgraph import ImageView, PlotWidget - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - sys.exit(app.exec_()) diff --git a/xmidas/utils/color_maps.py b/xmidas/utils/color_maps.py new file mode 100644 index 0000000..9c9742c --- /dev/null +++ b/xmidas/utils/color_maps.py @@ -0,0 +1,28 @@ +from itertools import combinations +import pyqtgraph as pg + +def create_color_maps(): + cmap_names = ["CET-L13", "CET-L14", "CET-L15"] + cmap_combo = combinations(cmap_names, 2) + cmap_label1 = ["red", "green", "blue"] + cmap_label2 = ["yellow", "magenta", "cyan"] + cmap_dict = {} + for i, name in zip(cmap_names, cmap_label1): + cmap_dict[name] = pg.colormap.get(i).getLookupTable(alpha=True) + + for i, name in zip(cmap_combo, cmap_label2): + cmap_dict[name] = pg.colormap.get(i[0]).getLookupTable(alpha=True) + pg.colormap.get(i[1]).getLookupTable( + alpha=True + ) + cmap_dict[name][:, 3] = 255 + + grey = ( + pg.colormap.get("CET-L13").getLookupTable(alpha=True) + + pg.colormap.get("CET-L14").getLookupTable(alpha=True) + + pg.colormap.get("CET-L15").getLookupTable(alpha=True) + ) + + grey[:, 3] = 255 + cmap_dict["grey"] = grey + + return cmap_dict \ No newline at end of file diff --git a/xmidas/utils/larch_norm.py b/xmidas/utils/larch_norm.py new file mode 100644 index 0000000..37dda6c --- /dev/null +++ b/xmidas/utils/larch_norm.py @@ -0,0 +1,119 @@ +import numpy as np + +def preedge(energy, mu, e0=None, npre=1, pre1=None, pre2=None, nvict=0): + energy = np.asarray(energy) + mu = np.asarray(mu) + + # 1) determine e0 + if e0 is None: + deriv = np.gradient(mu, energy) + ie0 = np.nanargmax(np.abs(deriv)) + e0 = energy[ie0] + else: + ie0 = np.argmin(np.abs(energy - e0)) + e0 = energy[ie0] + + # 2) default regions + dE = energy[1] - energy[0] + pre1 = -2 * dE if pre1 is None else pre1 + pre2 = -0.5 * dE if pre2 is None else pre2 + + # 3) ensure ordering + if pre1 > pre2: + pre1, pre2 = pre2, pre1 + + low, high = e0 + pre1, e0 + pre2 + mask = (energy >= low) & (energy <= high) + + # 4) fallback if too few points + if np.sum(mask) < (npre + 1): + idx0 = max(ie0 - (npre + 1), 0) + mask = np.zeros_like(mask, bool) + mask[idx0:ie0] = True + + x = energy[mask] + y = mu[mask] * x**nvict + + # 5) fit baseline + if npre == 0 or x.size == 0: + val = np.mean(y) if y.size else np.mean(mu[:2]) + pre_edge = np.full_like(mu, val) + else: + coef = np.polyfit(x, y, 1) + baseline = np.polyval(coef, energy) + pre_edge = baseline * energy**(-nvict) + + return e0, ie0, pre_edge + + +def normalize(energy, mu, pre_edge, e0, + norm1=None, norm2=None, nnorm=1): + energy = np.asarray(energy) + mu = np.asarray(mu) + dE = energy[1] - energy[0] + + # 1) default norms + norm1 = 2 * dE if norm1 is None else norm1 + norm2 = 10 * dE if norm2 is None else norm2 + + # 2) ensure ordering + if norm1 > norm2: + norm1, norm2 = norm2, norm1 + + low, high = e0 + norm1, e0 + norm2 + mask = (energy >= low) & (energy <= high) + + # 3) fallback to points just above edge + min_pts = nnorm + 1 + if np.sum(mask) < min_pts: + i0 = np.argmin(np.abs(energy - e0)) + start = min(i0 + 1, len(energy) - min_pts) + mask = np.zeros_like(mask, bool) + mask[start:start + min_pts] = True + + x = energy[mask] + y = (mu - pre_edge)[mask] + + # 4) fit poly + coef = ([0] * (nnorm + 1) if x.size == 0 + else np.polyfit(x, y, nnorm)) + poly = np.polyval(coef, energy) + post_edge = pre_edge + poly + + # 5) compute step & norm + ie0 = np.argmin(np.abs(energy - e0)) + edge_step = post_edge[ie0] - pre_edge[ie0] + edge_step = abs(edge_step) if edge_step != 0 else 1e-12 + norm = (mu - pre_edge) / edge_step + + return post_edge, norm, edge_step + + + +def pre_edge_simple(energy, mu, **kwargs): + """ + Combined pre-edge subtraction and normalization. + + Returns + ------- + dict with keys: e0, edge_step, pre_edge, post_edge, norm + """ + e0, ie0, pre_edge_arr = preedge(energy, mu, + e0=kwargs.get('e0', None), + npre=kwargs.get('npre', 1), + pre1=kwargs.get('pre1', None), + pre2=kwargs.get('pre2', None), + nvict=kwargs.get('nvict', 0)) + post_edge, norm_arr, edge_step = normalize( + energy, mu, pre_edge_arr, e0, + norm1=kwargs.get('norm1', None), + norm2=kwargs.get('norm2', None), + nnorm=kwargs.get('nnorm', 1) + ) + return { + 'e0': e0, + 'edge_step': edge_step, + 'pre_edge': pre_edge_arr, + 'post_edge': post_edge, + 'norm': norm_arr + } diff --git a/xmidas/utils/utils.py b/xmidas/utils/utils.py new file mode 100644 index 0000000..ceba360 --- /dev/null +++ b/xmidas/utils/utils.py @@ -0,0 +1,1156 @@ +""" Helper Functions (make a class later)""" + + +import h5py +import logging +import numpy as np +import pandas as pd +import os +import scipy.optimize as opt +import scipy.stats as stats + +import sklearn.decomposition as sd +import sklearn.cluster as sc +from scipy.signal import savgol_filter +from skimage.transform import resize +from skimage import filters +from sklearn import linear_model + +# from larch.xafs import pre_edge, preedge, mback +# from larch.io import read_ascii, read_athena +# from larch import Group +import xraydb +from pystackreg import StackReg + +from xmidas.utils.larch_norm import pre_edge_simple + + +logger = logging.getLogger() + +def get_xrf_data(h='h5file'): + """ + get xrf stack from h5 data generated at NSLS-II beamlines + + Arguments: + h5/hdf5 file + + Returns: + norm_xrf_stack - xrf stack image normalized with Io + mono_e - excitation enegy used for xrf + beamline - identity of the beamline + Io_avg - an average Io value, used before taking log + + """ + + f = h5py.File(h, "r") + + if list(f.keys())[0] == "xrfmap": + logger.info("Data from HXN/TES/SRX") + beamline = f["xrfmap/scan_metadata"].attrs["scan_instrument_id"] + + try: + + beamline_scalar = {"HXN": 2, "SRX": 0, "TES": 0} + + if beamline in beamline_scalar.keys(): + + Io = np.array(f["xrfmap/scalers/val"])[:, :, beamline_scalar[beamline]] + raw_xrf_stack = np.array(f["xrfmap/detsum/counts"]) + norm_xrf_stack = raw_xrf_stack + Io_avg = int(remove_nan_inf(Io).mean()) + else: + logger.error("Unknown Beamline Scalar") + except Exception: + logger.warning("Unknown Scalar: Raw Detector count in use") + norm_xrf_stack = np.array(f["xrfmap/detsum/counts"]) + + elif list(f.keys())[0] == "xrmmap": + logger.info("Data from XFM") + beamline = "XFM" + raw_xrf_stack = np.array(f["xrmmap/mcasum/counts"]) + Io = np.array(f["xrmmap/scalars/I0"]) + norm_xrf_stack = raw_xrf_stack + Io_avg = int(remove_nan_inf(Io).mean()) + + elif list(f.keys())[0] == "MAPS": + logger.info("MAPS file") + beamline = "APS" + raw_xrf_stack = np.array(f["MAPS/Spectra/mca_arr"]) + Io = 1 #have to find the name of the scalar + norm_xrf_stack = raw_xrf_stack.transpose((1, 2, 0)) + Io_avg = int(remove_nan_inf(Io).mean()) + + else: + logger.error("Unknown Data Format") + + try: + mono_e = int(f["xrfmap/scan_metadata"].attrs["instrument_mono_incident_energy"] * 1000) + logger.info("Excitation energy was taken from the h5 data") + + except Exception: + mono_e = 12000 + logger.info(f"Unable to get Excitation energy from the h5 data; using default value {mono_e} KeV") + + return remove_nan_inf(norm_xrf_stack.transpose((2, 0, 1))), mono_e + 1500, beamline, Io_avg + + +def remove_nan_inf(im): + im = np.array(im, dtype=np.float32) + im[np.isnan(im)] = 0 + im[np.isinf(im)] = 0 + return im + + +def rebin_image(im, bin_factor): + arrx, arry = np.shape(im) + if arrx / bin_factor != int or arrx / bin_factor != int: + logger.error("Invalid Binning") + + else: + shape = (arrx / bin_factor, arry / bin_factor) + return im.reshape(shape).mean(-1).mean(1) + + +def remove_hot_pixels(image_array, NSigma=5): + image_array = remove_nan_inf(image_array) + a, b, c = np.shape(image_array) + img_stack2 = np.zeros((a, b, c)) + for i in range(a): + im = image_array[i, :, :] + im[abs(im) > np.std(im) * NSigma] = im.mean() + img_stack2[i, :, :] = im + return img_stack2 + + +def smoothen(image_array, w_size=5): + a, b, c = np.shape(image_array) + spec2D_Matrix = np.reshape(image_array, (a, (b * c))) + smooth2D_Matrix = savgol_filter(spec2D_Matrix, w_size, w_size - 2, axis=0) + return remove_nan_inf(np.reshape(smooth2D_Matrix, (a, b, c))) + + +def resize_stack(image_array, upscaling=False, scaling_factor=2): + en, im1, im2 = np.shape(image_array) + + if upscaling: + im1_ = im1 * scaling_factor + im2_ = im2 * scaling_factor + img_stack_resized = resize(image_array, (en, im1_, im2_)) + + else: + im1_ = int(im1 / scaling_factor) + im2_ = int(im2 / scaling_factor) + img_stack_resized = resize(image_array, (en, im1_, im2_)) + + return img_stack_resized + + +def normalize(image_array, norm_point=-1): + norm_stack = image_array / image_array[norm_point] + return remove_nan_inf(norm_stack) + + +def remove_edges(image_array): + # z, x, y = np.shape(image_array) + return image_array[:, 1:-1, 1:-1] + + +def background_value(image_array): + img = image_array.mean(0) + img_h = img.mean(0) + img_v = img.mean(1) + h = np.gradient(img_h) + v = np.gradient(img_v) + bg = np.min([img_h[h == h.max()], img_v[v == v.max()]]) + return bg + + +def background_subtraction(img_stack, bg_percentage=10): + img_stack = remove_nan_inf(img_stack) + a, b, c = np.shape(img_stack) + ref_image = np.reshape(img_stack.mean(0), (b * c)) + bg_ratio = int((b * c) * 0.01 * bg_percentage) + bg_ = np.max(sorted(ref_image)[0:bg_ratio]) + bged_img_stack = img_stack - bg_[:, np.newaxis, np.newaxis] + return bged_img_stack + + +def background_subtraction2(img_stack, bg_percentage=10): + img_stack = remove_nan_inf(img_stack) + a, b, c = np.shape(img_stack) + bg_ratio = int((b * c) * 0.01 * bg_percentage) + bged_img_stack = img_stack.copy() + + for n, img in enumerate(img_stack): + bg_ = np.max(sorted(img.flatten())[0:bg_ratio]) + print(bg_) + bged_img_stack[n] = img - bg_ + + return remove_nan_inf(bged_img_stack) + + +def background1(img_stack): + img = img_stack.sum(0) + img_h = img.mean(0) + img_v = img.mean(1) + h = np.gradient(img_h) + v = np.gradient(img_v) + bg = np.min([img_h[h == h.max()], img_v[v == v.max()]]) + return bg + + +def get_sum_spectra(image_array): + spec = np.nansum(image_array, axis=(1, 2)) + return spec + + +def get_mean_spectra(image_array): + spec = np.nanmean(image_array, axis=(1, 2)) + return spec + + +def flatten_(image_array): + z, x, y = np.shape(image_array) + flat_array = np.reshape(image_array, (x * y, z)) + return flat_array + + +def image_to_pandas(image_array): + a, b, c = np.shape(image_array) + im_array = np.reshape(image_array, ((b * c), a)) + a, b = im_array.shape + df = pd.DataFrame( + data=im_array[:, :], columns=["e" + str(i) for i in range(b)], + index=["s" + str(i) for i in range(a)] + ) + return df + + +def image_to_pandas2(image_array): + a, b, c = np.shape(image_array) + im_array = np.reshape(image_array, (a, (b * c))) + a, b = im_array.shape + df = pd.DataFrame( + data=im_array[:, :], index=["e" + str(i) for i in range(a)], + columns=["s" + str(i) for i in range(b)] + ) + return df + + +def neg_log(image_array): + absorb = -1 * np.log(image_array) + return remove_nan_inf(absorb) + + +def clean_stack(img_stack, auto_bg=False, bg_percentage=5): + a, b, c = np.shape(img_stack) + + if auto_bg is True: + bg_ = background1(img_stack) + + else: + sum_spec = (img_stack.sum(1)).sum(1) + ref_stk_num = np.where(sum_spec == sum_spec.max())[-1] + + ref_image = np.reshape(img_stack[ref_stk_num], (b * c)) + bg_ratio = int((b * c) * 0.01 * bg_percentage) + bg_ = np.max(sorted(ref_image)[0:bg_ratio]) + + bg = np.where(img_stack[ref_stk_num] > bg_, img_stack[ref_stk_num], 0) + bg2 = np.where(bg < bg_, bg, 1) + + bged_img_stack = img_stack * bg2 + + return remove_nan_inf(bged_img_stack) + + +def subtractBackground(im_stack, bg_region): + if bg_region.ndim == 3: + bg_region_ = np.mean(bg_region, axis=(1, 2)) + + elif bg_region.ndim == 2: + bg_region_ = np.mean(bg_region, axis=1) + + else: + bg_region_ = bg_region + + return im_stack - bg_region_[:, np.newaxis, np.newaxis] + + +def classify(img_stack, correlation="Pearson"): + img_stack_ = img_stack + a, b, c = np.shape(img_stack_) + norm_img_stack = normalize(img_stack_) + f = np.reshape(norm_img_stack, (a, (b * c))) + + max_x, max_y = np.where(norm_img_stack.sum(0) == (norm_img_stack.sum(0)).max()) + ref = norm_img_stack[:, int(max_x), int(max_y)] + corr = np.zeros(len(f.T)) + for s in range(len(f.T)): + if correlation == "Kendall": + r, p = stats.kendalltau(ref, f.T[s]) + elif correlation == "Pearson": + r, p = stats.pearsonr(ref, f.T[s]) + + corr[s] = r + + cluster_image = np.reshape(corr, (b, c)) + return (cluster_image ** 3), img_stack_ + + +def correlation_kmeans(img_stack, n_clusters, correlation="Pearson"): + img, bg_image = classify(img_stack, correlation) + img[np.isnan(img)] = -99999 + X = img.reshape((-1, 1)) + k_means = sc.KMeans(n_clusters) + k_means.fit(X) + + X_cluster = k_means.labels_ + X_cluster = X_cluster.reshape(img.shape) + 1 + + return X_cluster + + +def cluster_stack( + im_array, method="KMeans", n_clusters_=4, decomposed=False, + decompose_method="PCA", decompose_comp=2 + ): + a, b, c = im_array.shape + + if method == "Correlation-Kmeans": + + X_cluster = correlation_kmeans(im_array, n_clusters_, correlation="Pearson") + + else: + + methods = { + "MiniBatchKMeans": sc.MiniBatchKMeans, + "KMeans": sc.KMeans, + "MeanShift": sc.MeanShift, + "Spectral Clustering": sc.SpectralClustering, + "Affinity Propagation": sc.AffinityPropagation, + } + + if decomposed: + im_array = denoise_with_decomposition(im_array, method_=decompose_method, + n_components=decompose_comp) + + flat_array = np.reshape(im_array, (a, (b * c))) + init_cluster = methods[method](n_clusters=n_clusters_) + init_cluster.fit(np.transpose(flat_array)) + X_cluster = init_cluster.labels_.reshape(b, c) + 1 + + decon_spectra = np.zeros((a, n_clusters_)) + decon_images = np.zeros((n_clusters_, b, c)) + + for i in range(n_clusters_): + mask_i = np.where(X_cluster == (i + 1), X_cluster, 0) + spec_i = get_sum_spectra(im_array * mask_i) + decon_spectra[:, i] = spec_i + decon_images[i] = im_array.sum(0) * mask_i + + return decon_images, X_cluster, decon_spectra + +def kmeans_variance(im_array): + a, b, c = im_array.shape + flat_array = np.reshape(im_array, (a, (b * c))) + var = np.arange(24) + clust_n = np.arange(24) + 2 + + for clust in var: + init_cluster = sc.KMeans(n_clusters=int(clust + 2)) + init_cluster.fit(np.transpose(flat_array)) + var_ = init_cluster.inertia_ + var[clust] = np.float64(var_) + + return clust_n, var + + +def pca_scree(im_stack): + new_image = im_stack.transpose(2, 1, 0) + x, y, z = np.shape(new_image) + img_ = np.reshape(new_image, (x * y, z)) + # pca = sd.PCA(z) + # pca.fit(img_) + pca = sd.TruncatedSVD(z - 1) + pca.fit(img_) + var = pca.singular_values_ + # var = pca.singular_values_ + return var + + +def decompose_stack(im_stack, decompose_method="PCA", + n_components_=3, generate_label_img = True): + + new_image = im_stack.transpose(2, 1, 0) + new_image[new_image<0] = 0 + + x, y, z = np.shape(new_image) + img_ = np.reshape(new_image, (x * y, z)) + methods_dict = { + "PCA": sd.PCA, + "IncrementalPCA": sd.IncrementalPCA, + "NMF": sd.NMF, + "FastICA": sd.FastICA, + "DictionaryLearning": sd.MiniBatchDictionaryLearning, + "FactorAnalysis": sd.FactorAnalysis, + "TruncatedSVD": sd.TruncatedSVD, + } + + _mdl = methods_dict[decompose_method](n_components=n_components_) + + ims = (_mdl.fit_transform(img_).reshape(x, y, n_components_)).transpose(2, 1, 0) + spcs = _mdl.components_.transpose() + decon_spetra = np.zeros((z, n_components_)) + decom_map = np.zeros((ims.shape)) + + if generate_label_img: + for i in range(n_components_): + f = ims.copy()[i] + f[f < 0] = 0 + f = np.where(f > 0 * np.std(f), f, 0) + spec_i = ((new_image.T * f).sum(1)).sum(1) + decon_spetra[:, i] = spec_i + + f[f > 0] = i + 1 + decom_map[i] = f + decom_map = decom_map.sum(0) + + return np.float32(ims), spcs, decon_spetra, decom_map + + + +def denoise_with_decomposition(img_stack, method_="PCA", n_components=4): + new_image = img_stack.transpose(2, 1, 0) + x, y, z = np.shape(new_image) + img_ = np.reshape(new_image, (x * y, z)) + + methods_dict = { + "PCA": sd.PCA, + "IncrementalPCA": sd.IncrementalPCA, + "NMF": sd.NMF, + "FastICA": sd.FastICA, + "DictionaryLearning": sd.DictionaryLearning, + "FactorAnalysis": sd.FactorAnalysis, + "TruncatedSVD": sd.TruncatedSVD, + } + + decomposed = methods_dict[method_](n_components=n_components) + + ims = (decomposed.fit_transform(img_).reshape(x, y, n_components)).transpose(2, 1, 0) + ims[ims < 0] = 0 + ims[ims > 0] = 1 + mask = ims.sum(0) + mask[mask > 1] = 1 + # mask = uniform_filter(mask) + filtered = img_stack * mask + # plt.figure() + # plt.imshow(filtered.sum(0)) + # plt.title('background removed') + # plt.show() + return remove_nan_inf(filtered) + + +def interploate_E(refs, e): + n = np.shape(refs)[1] + refs = np.array(refs) + ref_e = refs[:, 0] + ref = refs[:, 1:n] + all_ref = [] + for i in range(n - 1): + ref_i = np.interp(e, ref_e, ref[:, i]) + all_ref.append(ref_i) + return np.array(all_ref) + +def rfactor(spectrum_experimental, spectrum_fit): + r""" + Computes R-factor based on two spectra + + Parameters + ---------- + spectrum_experimental : ndarray + spectrum data on which fitting is performed (N elements) + + spectrum_fit : ndarray + fitted spectrum (weighted sum of spectrum components, N elements) + + Returns + ------- + float, the value of R-factor + """ + + # Compute R-factor + dif = spectrum_experimental - spectrum_fit + dif_sum = np.sum(np.abs(dif), axis=0) + data_sum = np.sum(np.abs(spectrum_experimental), axis=0) + + # Avoid accidental division by zero (or a very small number) + data_sum = np.clip(data_sum, a_min=1e-30, a_max=None) + + return dif_sum / data_sum + +def rfactor_compute(spectrum, fit_results, ref_spectra): + r""" + Computes R-factor for the fitting results + + Parameters + ---------- + spectrum : ndarray + spectrum data on which fitting is performed (N elements) + + fit_results : ndarray + results of fitting (coefficients, K elements) + + ref_spectra : 2D ndarray + reference spectra used for fitting (NxK element array) + + Returns + ------- + float, the value of R-factor + """ + + # Check if input parameters are valid + assert ( + spectrum.ndim == 1 or spectrum.ndim == 2 + ), "Parameter 'spectrum' must be 1D or 2D array, ({spectrum.ndim})" + assert spectrum.ndim == fit_results.ndim, ( + f"Spectrum data (ndim = {spectrum.ndim}) and fitting results " + f"(ndim = {fit_results.ndim}) must have the same number of dimensions" + ) + assert ref_spectra.ndim == 2, "Parameter 'ref_spectra' must be 2D array, ({ref_spectra.ndim})" + assert spectrum.shape[0] == ref_spectra.shape[0], ( + f"Arrays 'spectrum' ({spectrum.shape}) and 'ref_spectra' ({ref_spectra.shape}) " + "must have the same number of data points" + ) + assert fit_results.shape[0] == ref_spectra.shape[1], ( + f"Arrays 'fit_results' ({fit_results.shape}) and 'ref_spectra' ({ref_spectra.shape}) " + "must have the same number of spectrum points" + ) + if spectrum.ndim == 2: # Only if multiple spectra are processed + assert spectrum.shape[1] == fit_results.shape[1], ( + f"Arrays 'spectrum' {spectrum.shape} and 'fit_results' {fit_results.shape}" + "must have the same number of columns" + ) + + spectrum_fit = np.matmul(ref_spectra, fit_results) + return rfactor(spectrum, spectrum_fit) + + +def fitting_admm(data, ref_spectra, *, rate=0.2, maxiter=100, epsilon=1e-30, + non_negative=True, weight_to_whiteline = False): + r""" + Fitting of multiple spectra using ADMM method. + + Parameters + ---------- + + data : ndarray(float), 2D + array holding multiple observed spectra, shape (K, N), where K is the number of energy points, + and N is the number of spectra + + absorption_refs : ndarray(float), 2D + array of references, shape (K, Q), where Q is the number of references. + + maxiter : int + maximum number of iterations. Optimization may stop prematurely if convergence criteria are met. + + rate : float + descent rate for optimization algorithm. Currently is used only for ADMM fitting (1/lambda). + + epsilon : float + small value used in stopping criterion of ADMM optimization algorithm. + + non_negative : bool + if True, then the solution is guaranteed to be non-negative + + Returns + ------- + + map_data_fitted : ndarray(float), 2D + fitting results, shape (Q, N), where Q is the number of references and N is the number of spectra. + + map_rfactor : ndarray(float), 2D + map that represents R-factor for the fitting, shape (M,N). + + convergence : ndarray(float), 1D + convergence data returned by ADMM algorithm + + feasibility : ndarray(float), 1D + feasibility data returned by ADMM algorithm + + The prototype for the ADMM fitting function was implemented by Hanfei Yan in Matlab. + """ + #print(type(data)) + if data.ndim == 1: + data = np.expand_dims(data, axis=1) #"Data array 'data' must have 2 dimensions" + #print(type(data)) + assert ref_spectra.ndim == 2, "Data array 'ref_spectra' must have 2 dimensions" + + n_pts = data.shape[0] + n_pixels = data.shape[1] + n_pts_2 = ref_spectra.shape[0] + n_refs = ref_spectra.shape[1] + + assert ( + n_pts == n_pts_2 + ), f"ADMM fitting: number of spectrum points in data ({n_pts}) and references ({n_pts_2}) do not match." + + assert rate > 0.0, f"ADMM fitting: parameter 'rate' is zero or negative ({rate:.6g})" + + assert maxiter > 0, f"ADMM fitting: parameter 'maxiter' is zero or negative ({rate})" + + assert epsilon > 0.0, f"ADMM fitting: parameter 'epsilon' is zero or negative ({rate:.6g})" + + wgt = np.ones(len(data)) + + #print(wgt) + + if weight_to_whiteline: + wgt[0:-15] = 0.4 + wgt[-25:-1]=0.5 + + #print(wgt) + + y = data + # Calculate some quantity to be used in the iteration + A = ref_spectra + At = np.transpose(A) + #print(np.shape(At), np.shape(y), np.shape(np.diag(wgt))) + #print(np.diag(wgt)) + + z = A.T @ np.diag(wgt) @ y + c = A.T @ np.diag(wgt) @ A + + # Initialize variables + w = np.ones(shape=[n_refs, n_pixels]) + u = np.zeros(shape=[n_refs, n_pixels]) + + # Feasibility test: x == w + convergence = np.zeros(shape=[maxiter]) + feasibility = np.zeros(shape=[maxiter]) + + dg = np.eye(n_refs, dtype=float) * rate + m1 = np.linalg.inv((c + dg)) + + n_iter = 0 + for i in range(maxiter): + m2 = z + (w - u) * rate + x = np.matmul(m1, m2) + w_updated = x + u + if non_negative: + w_updated = w_updated.clip(min=0) + u = u + x - w_updated + + conv = np.linalg.norm(w_updated - w) / np.linalg.norm(w_updated) + convergence[i] = conv + feasibility[i] = np.linalg.norm(x - w_updated) + + w = w_updated + + if conv < epsilon: + n_iter = i + 1 + break + + # Compute R-factor + rfactor = rfactor_compute(data, w, ref_spectra) + #rfactor = 1 + + convergence = convergence[:n_iter] + feasibility = feasibility[:n_iter] + + return w, rfactor, convergence, feasibility + + +def getStats(spec, fit, num_refs=2): + stats = {} + + SS_tot = np.sum((spec - np.mean(spec))**2) + SS_res = np.sum((spec - fit) ** 2) + #r_factor = (np.sum(spec - fit) ** 2) / np.sum(spec ** 2) + r_factor = 1 - (SS_res / SS_tot) #temp to get r2 array for paper + stats["R_Factor"] = np.around(r_factor, 5) + + y_mean = np.sum(spec) / len(spec) + #SS_tot = np.sum((spec - y_mean) ** 2) + + r_square = 1 - (SS_res / SS_tot) + stats["R_Square"] = np.around(r_square, 4) + + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chisquare.html + # https://en.wikipedia.org/wiki/Chi-squared_distribution + # https://ned.ipac.caltech.edu/level5/Leo/Stats2_4.html + chisq = np.sum(((spec - fit) ** 2) / np.var(spec)) + # chisq = np.sum((spec - fit) ** 2) + stats["Chi_Square"] = np.around(chisq, 5) + + red_chisq = chisq / (len(spec) - num_refs) + stats["Reduced Chi_Square"] = np.around(red_chisq, 5) + + return stats + + +def xanes_fitting_1D(spec, e_list, refs, + method="NNLS", alphaForLM=0.01): + + """Linear combination fit of image data with reference standards""" + + #check for energy unit in e_list vs ref + #print(f"at 1D fit function{e_list[0]}") + + + # #energy list should be corrected for shifts before passing here + # if len(str(int(e_list[0]))) <3: + # e_list *=1000 + # #print(e_list) + + # #print(refs[0][0]) + # if len(str(int(refs[0][0]))) <3: + # refs[:,0] =refs[:,0]*1000 + # #print(refs[0]) + + spec = np.nan_to_num(spec) + refs = np.nan_to_num(refs) + int_refs = interploate_E(refs, e_list) + + + if method == "NNLS": + coeffs, r = opt.nnls(int_refs.T, spec) + #print(f"{coeffs}: nnls") + + elif method == "LASSO": + lasso = linear_model.Lasso(positive=True, alpha=alphaForLM) # lowering alpha helps with 1D fits + fit_results = lasso.fit(int_refs.T, spec) + coeffs = fit_results.coef_ + + elif method == "RIDGE": + ridge = linear_model.Ridge(alpha=alphaForLM) + fit_results = ridge.fit(int_refs.T, spec) + coeffs = fit_results.coef_ + + elif method == "ADMM": + coeffs,r_factor,convergence, feasibility = fitting_admm(spec, + int_refs.T, + maxiter=100, + rate=alphaForLM, + epsilon=1e-30) + coeffs = np.squeeze(coeffs.T) + # print(f"{coeffs}; ADMM") + + fit = np.dot(coeffs , int_refs) + stats = getStats(spec, fit, num_refs=np.min(np.shape(int_refs.T))) + + return stats, coeffs + + +def xanes_fitting(im_stack, e_list, refs, method="NNLS", alphaForLM=0.1, binStack=False): + """Linear combination fit of image data with reference standards""" + + if binStack: + im_stack = resize_stack(im_stack, scaling_factor=4) + + en, im1, im2 = np.shape(im_stack) + im_array = im_stack.reshape(en, im1 * im2) + coeffs_arr = [] + r_factor_arr = [] + lasso = linear_model.Lasso(positive=True, alpha=alphaForLM) + if not method=="ADMM": + for n, i in enumerate(range(im1 * im2)): + stats, coeffs = xanes_fitting_1D(im_array[:, i], + e_list, + refs, + method=method, + alphaForLM=alphaForLM) + + coeffs_arr.append(coeffs) + r_factor_arr.append(stats["R_Factor"]) + + abundance_map = np.reshape(coeffs_arr, (im1, im2, -1)) + r_factor_im = np.reshape(r_factor_arr, (im1, im2)) + + elif method=="ADMM": + int_refs = interploate_E(refs,e_list) + coeffs_arr,r_factor_im,convergence, feasibility = fitting_admm(im_array, + int_refs.T, + maxiter=100, + rate=alphaForLM, + epsilon=1e-30) + # print(coeffs_arr.shape) + + #plt.imshow(coeffs_arr[1].reshape(im1, im2)) + + abundance_map = np.reshape((coeffs_arr.T), (im1, im2, -1)) + + return abundance_map, r_factor_im, np.mean(coeffs_arr, axis=0) + + +def xanes_fitting_Line(im_stack, e_list, refs, method="NNLS", alphaForLM=0.05): + """Linear combination fit of image data with reference standards""" + en, im1, im2 = np.shape(im_stack) + im_array = np.mean(im_stack, 2) + coeffs_arr = [] + meanStats = {"R_Factor": 0, "R_Square": 0, "Chi_Square": 0, "Reduced Chi_Square": 0} + + for i in range(im1): + stats, coeffs = xanes_fitting_1D(im_array[:, i], e_list, refs, + method=method, alphaForLM=alphaForLM) + coeffs_arr.append(coeffs) + for key in stats.keys(): + meanStats[key] += stats[key] + + for key, vals in meanStats.items(): + meanStats[key] = np.around((vals / im1), 5) + + return meanStats, np.mean(coeffs_arr, axis=0) + +#TODO ADMM maybe faster here +def xanes_fitting_Binned(im_stack, e_list, refs, method="NNLS", alphaForLM=0.05): + """Linear combination fit of image data with reference standards""" + + im_stack = resize_stack(im_stack, scaling_factor=10) + # use a simple filter to find threshold value + val = filters.threshold_otsu(im_stack[-1]) + en, im1, im2 = np.shape(im_stack) + im_array = im_stack.reshape(en, im1 * im2) + coeffs_arr = [] + meanStats = {"R_Factor": 0, "R_Square": 0, "Chi_Square": 0, "Reduced Chi_Square": 0} + + specs_fitted = 0 + total_spec = im1 * im2 + for i in range(total_spec): + spec = im_array[:, i] + # do not fit low intensity/background regions + if spec[-1] > val: + specs_fitted += 1 + stats, coeffs = xanes_fitting_1D(spec / spec[-1], e_list, refs, + method=method, alphaForLM=alphaForLM) + coeffs_arr.append(coeffs) + for key in stats.keys(): + meanStats[key] += stats[key] + else: + pass + + for key, vals in meanStats.items(): + meanStats[key] = np.around((vals / specs_fitted), 6) + # print(f"{specs_fitted}/{total_spec}") + return meanStats, np.mean(coeffs_arr, axis=0) + + +def create_df_from_nor(athenafile="fe_refs.nor"): + """create pandas dataframe from athena nor file, first column + is energy and headers are sample names""" + + refs = np.loadtxt(athenafile) + n_refs = refs.shape[-1] + skip_raw_n = n_refs + 6 + + df = pd.read_table( + athenafile, sep=r'\s+', skiprows=skip_raw_n, header=None, usecols=np.arange(0, n_refs) + ) + df2 = pd.read_table( + athenafile, sep=r'\s+', skiprows=skip_raw_n - 1, usecols=np.arange(0, n_refs + 1) + ) + new_col = df2.columns.drop("#") + df.columns = new_col + return df, list(new_col) + + +def create_df_from_nor_try2(athenafile="fe_refs.nor"): + """create pandas dataframe from athena nor file, first column + is energy and headers are sample names""" + + refs = np.loadtxt(athenafile) + n_refs = refs.shape[-1] + df_refs = pd.DataFrame(refs) + + df = pd.read_csv(athenafile, header=None) + new_col = list((str(df.iloc[n_refs + 5].values)).split(" ")[2::2]) + df_refs.columns = new_col + + return df_refs, list(new_col) + + +def energy_from_logfile(logfile="maps_log_tiff.txt"): + df = pd.read_csv(logfile, header=None, sep=r'\s+', skiprows=9) + return df[9][df[7] == "energy"].values.astype(float) + + +# def xanesNormalization(e, mu, e0=7125, step=None, +# nnorm=2, nvict=0, pre1=None, pre2=-50, +# norm1=100, norm2=None, method="pre_edge", +# useFlattened=False, Elemline = "Fe_K"): + +# elem, line = Elemline.split('_') +# elemZ = xraydb.atomic_number(elem) +# dat = Group(name='larchgroup', col1=e, col2=mu) + + +# if method == "guess": +# result = preedge(e, mu, e0, step=step, nnorm=nnorm, nvict=nvict) + +# return result["pre1"], result["pre2"], result["norm1"], result["norm2"] + +# elif method == "mback": +# mback(e,mu, group=dat, z=elemZ, edge=line, e0=e0,fit_erfc=False) +# return dat.f2, dat.fpp + +# else: +# pre_edge(e, mu,group=dat,e0=e0, step=step, nnorm=nnorm,nvict=nvict, pre1=pre1, +# pre2=pre2, norm1=norm1,norm2=norm2, make_flat = True) + +# if useFlattened: +# normSpec = dat.flat +# else: +# normSpec = dat.norm + +# return dat.pre_edge, dat.post_edge, normSpec + +def xanesNormalization(e, mu, e0=7125, nnorm=2, nvict=0, + pre1=None, pre2=-50, norm1=None, norm2=None, + useFlattened=False, Elemline="Fe_K"): + """ + e, mu : energy & absorption arrays + e0 : edge energy (float) + nnorm : degree of post-edge polynomial + nvict : exponent for pre-edge fit weighting + pre1, pre2 : relative pre-edge fitting bounds + norm1, norm2: relative post-edge fitting bounds + useFlattened: ignored in this simple version + Elemline : placeholder, no longer used + """ + # perform pre-edge subtraction + normalization + res = pre_edge_simple( + e, mu, + e0=e0, nnorm=nnorm, nvict=nvict, + pre1=pre1, pre2=pre2, + norm1=norm1, norm2=norm2 + ) + + pre_edge_arr = res["pre_edge"] + post_edge_arr = res["post_edge"] + norm_arr = res["norm"] + + return pre_edge_arr, post_edge_arr, norm_arr + +# def xanesNormStack(e_list, im_stack, e0=7125, step=None, +# nnorm=2, nvict=0, pre1=None, pre2=-50, +# norm1=100, norm2=None, useFlattened=False, ignorePostEdgeNorm=False): +# en, im1, im2 = np.shape(im_stack) +# im_array = im_stack.reshape(en, im1 * im2) +# normedStackArray = np.zeros_like(im_array) +# dat = Group(name='larchgroup', col1=e_list, col2=get_mean_spectra(im_stack)) + + +# for i in range(im1 * im2): +# pre_edge(e_list, im_array[:, i], e0=e0, group=dat, step=step, nnorm=nnorm, +# nvict=nvict, pre1=pre1, pre2=pre2, norm1=norm1, norm2=norm2,make_flat = True) + +# if useFlattened: +# normSpec = dat.flat +# else: +# normSpec = dat.norm + +# if ignorePostEdgeNorm: +# normedStackArray[:, i] = normSpec * dat.post_edge +# else: +# normedStackArray[:, i] = normSpec + +# return remove_nan_inf(np.reshape(normedStackArray, (en, im1, im2))) + + + +def xanesNormStack(e_list, im_stack, + e0=7125, nnorm=2, nvict=0, + pre1=None, pre2=-50, + norm1=None, norm2=None, + ignorePostEdgeNorm=False): + """ + Apply XANES normalization to every spectrum in a 3D stack. + + Parameters + ---------- + e_list : 1D array + Energy grid. + im_stack : 3D array, shape (nE, nX, nY) + Raw absorption stack. + e0, nnorm, nvict, pre1, pre2, norm1, norm2 : fit params + Passed to pre_edge_simple. + ignorePostEdgeNorm : bool + If True, multiply normalized spec by post-edge baseline. + + Returns + ------- + normed_stack : 3D array, same shape as im_stack + """ + nE, nX, nY = im_stack.shape + flat_in = im_stack.reshape(nE, -1) + flat_out = np.zeros_like(flat_in) + + for i in range(flat_in.shape[1]): + spec = flat_in[:, i] + res = pre_edge_simple( + e_list, spec, + e0=e0, nnorm=nnorm, nvict=nvict, + pre1=pre1, pre2=pre2, + norm1=norm1, norm2=norm2 + ) + norm_spec = res["norm"] + post_edge = res["post_edge"] + + if ignorePostEdgeNorm: + flat_out[:, i] = norm_spec * post_edge + else: + flat_out[:, i] = norm_spec + + # reshape back and remove any NaN/inf + return remove_nan_inf(flat_out.reshape(nE, nX, nY)) + +def getDeconvolutedXANESSpectrum(xanesStack, chemMapStack, energy, clusterSigma=1): + compXanesSpetraAll = pd.DataFrame() + compXanesSpetraAll['Energy'] = energy + + for n, compImage in enumerate(chemMapStack): + mask = np.where(compImage > clusterSigma * np.std(compImage), compImage, 0) + compXanesSpetraAll[f'Component_{n + 1}'] = get_mean_spectra(xanesStack * mask) + return compXanesSpetraAll + + +def align_stack( + stack_img, ref_image_void=True, ref_stack=None, + transformation=StackReg.TRANSLATION, reference="previous" +): + + """Image registration flow using pystack reg""" + + # all the options are in one function + + sr = StackReg(transformation) + + if ref_image_void: + tmats_ = sr.register_stack(stack_img, reference=reference) + + else: + tmats_ = sr.register_stack(ref_stack, reference=reference) + out_ref = sr.transform_stack(ref_stack) + + out_stk = sr.transform_stack(stack_img, tmats=tmats_) + return np.float32(out_stk), tmats_ + + +def align_simple(stack_img, transformation=StackReg.TRANSLATION, reference="previous"): + + sr = StackReg(transformation) + tmats_ = sr.register_stack(stack_img, reference="previous") + for i in range(10): + out_stk = sr.transform_stack(stack_img, tmats=tmats_) + return np.float32(out_stk) + + +def align_with_tmat(stack_img, tmat_file, transformation=StackReg.TRANSLATION): + + sr = StackReg(transformation) + out_stk = sr.transform_stack(stack_img, tmats=tmat_file) + return np.float32(out_stk) + + +def align_stack_iter( + stack, + ref_stack_void=True, + ref_stack=None, + transformation=StackReg.TRANSLATION, + method=("previous", "first"), + max_iter=2, +): + if ref_stack_void: + ref_stack = stack + + for i in range(max_iter): + sr = StackReg(transformation) + for ii in range(len(method)): + print(ii, method[ii]) + tmats = sr.register_stack(ref_stack, reference=method[ii]) + ref_stack = sr.transform_stack(ref_stack) + stack = sr.transform_stack(stack, tmats=tmats) + + return np.float32(stack) + +def normalize_and_scale(stack): + return (stack**2/stack.sum(0)[np.newaxis,:,:])/stack.max() + + +def applyMaskGetMeanSpectrum(im_stack, mask): + """A 2d mask to multiply with the 3d xanes stack and returns mean spectrum""" + + masked_stack = im_stack * mask + return get_mean_spectra(masked_stack) + + +def modifyStack( + raw_stack, + normalizeStack=False, + normToPoint=-1, + applySmooth=False, + smoothWindowSize=3, + applyThreshold=False, + thresholdValue=0, + removeOutliers=False, + nSigmaOutlier=3, + applyTranspose=False, + transposeVals=(0, 1, 2), + applyCrop=False, + cropVals=(0, 1, 2), + removeEdges=False, + resizeStack=False, + upScaling=False, + binFactor=2, +): + + """A giant function to modify the stack with many possible operations. + all the changes can be saved to a jason file as a config file. Enabling and + distabling the sliders is a problem""" + + """ + normStack = normalize(raw_stack, norm_point=normToPoint) + smoothStack = smoothen(raw_stack, w_size= smoothWindowSize) + thresholdStack = clean_stack(raw_stack, auto_bg=False, bg_percentage = thresholdValue) + outlierStack = remove_hot_pixels(raw_stack, NSigma=nSigmaOutlier) + transposeStack = np.transpose(raw_stack, transposeVals) + croppedStack = raw_stack[cropVals] + edgeStack = remove_edges(raw_stack) + binnedStack = resize_stack(raw_stack,upscaling=upScaling,scaling_factor=binFactor) + + """ + + if removeOutliers: + modStack = remove_hot_pixels(raw_stack, NSigma=nSigmaOutlier) + + else: + modStack = raw_stack + + if applyThreshold: + modStack = clean_stack(modStack, auto_bg=False, bg_percentage=thresholdValue) + + else: + pass + + if applySmooth: + modStack = smoothen(modStack, w_size=smoothWindowSize) + + else: + pass + + if applyTranspose: + modStack = np.transpose(modStack, transposeVals) + + else: + pass + + if applyCrop: + modStack = modStack[cropVals] + + else: + pass + + if normalizeStack: + modStack = normalize(raw_stack, norm_point=normToPoint) + else: + pass