├── module ├── VERSION ├── MANIFEST.in ├── README.md ├── docs │ ├── requirements.txt │ ├── Makefile │ ├── make.bat │ └── source │ │ ├── conf.py │ │ └── index.rst ├── .gitignore ├── setup.sh ├── test.dcm ├── requirements.txt ├── LICENSE ├── setup.py ├── test_script.py └── collageradiomics.py ├── slicer └── collageradiomics │ ├── CollageRadiomicsSlicer │ ├── Testing │ │ ├── CMakeLists.txt │ │ └── Python │ │ │ └── CMakeLists.txt │ ├── Screenshot.jpg │ ├── Resources │ │ ├── Icons │ │ │ └── CollageRadiomicsSlicer.png │ │ └── UI │ │ │ └── CollageRadiomicsSlicer.ui │ ├── CMakeLists.txt │ └── CollageRadiomicsSlicer.py │ ├── collageradiomics.png │ ├── Tutorials │ ├── CollageFullDemo.png │ └── CollageMultipleDemo.png │ ├── CMakeLists.txt │ └── README.md ├── sample_data ├── ImageMask.png ├── ImageSlice.png ├── ImageSlice2.png ├── BrainSliceTumor.png ├── BrainSliceTumorMask.png ├── ImageNonRectangularMask.png └── ImageNonRectangularMask2.png ├── .readthedocs.yml ├── Pipfile ├── .gitignore ├── docker └── dev_environment │ ├── collageradiomics_pip │ └── Dockerfile │ ├── collageradiomics_examples │ └── Dockerfile │ ├── ubuntu_base │ └── Dockerfile │ └── ml_base │ └── Dockerfile ├── requirements.txt ├── .github └── workflows │ └── python-publish.yml ├── cli ├── setup.py └── collageradiomicscli.py ├── LICENSE ├── examples └── isbi.py ├── README.md ├── jupyter └── examples │ └── example.ipynb └── Pipfile.lock /module/VERSION: -------------------------------------------------------------------------------- 1 | 0.3.8 2 | -------------------------------------------------------------------------------- /module/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include VERSION 2 | -------------------------------------------------------------------------------- /module/README.md: -------------------------------------------------------------------------------- 1 | Setup scripts and code for the collageradiomics pip module. -------------------------------------------------------------------------------- /module/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | matplotlib 3 | collageradiomics 4 | -------------------------------------------------------------------------------- /module/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all dcm files except test.dcm: 2 | *.dcm 3 | !test.dcm 4 | -------------------------------------------------------------------------------- /module/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python setup.py sdist bdist_wheel 3 | twine upload dist/* -------------------------------------------------------------------------------- /module/test.dcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/module/test.dcm -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /sample_data/ImageMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/ImageMask.png -------------------------------------------------------------------------------- /sample_data/ImageSlice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/ImageSlice.png -------------------------------------------------------------------------------- /sample_data/ImageSlice2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/ImageSlice2.png -------------------------------------------------------------------------------- /sample_data/BrainSliceTumor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/BrainSliceTumor.png -------------------------------------------------------------------------------- /sample_data/BrainSliceTumorMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/BrainSliceTumorMask.png -------------------------------------------------------------------------------- /sample_data/ImageNonRectangularMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/ImageNonRectangularMask.png -------------------------------------------------------------------------------- /sample_data/ImageNonRectangularMask2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/sample_data/ImageNonRectangularMask2.png -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /slicer/collageradiomics/collageradiomics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/slicer/collageradiomics/collageradiomics.png -------------------------------------------------------------------------------- /slicer/collageradiomics/Tutorials/CollageFullDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/slicer/collageradiomics/Tutorials/CollageFullDemo.png -------------------------------------------------------------------------------- /module/requirements.txt: -------------------------------------------------------------------------------- 1 | mahotas==1.4.13 2 | numpy>=1.21.6 3 | Pillow==9.3.0 4 | scikit-build>=0.15.0 5 | scikit-image>=0.19.3 6 | scipy>=1.7.3 7 | pydicom>=2.3.0 8 | sklearn 9 | -------------------------------------------------------------------------------- /slicer/collageradiomics/Tutorials/CollageMultipleDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/slicer/collageradiomics/Tutorials/CollageMultipleDemo.png -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/Screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/slicer/collageradiomics/CollageRadiomicsSlicer/Screenshot.jpg -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: module/docs/source/conf.py 5 | 6 | formats: 7 | - pdf 8 | 9 | python: 10 | version: 3.7 11 | install: 12 | - requirements: module/docs/requirements.txt 13 | -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/Resources/Icons/CollageRadiomicsSlicer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radxtools/collageradiomics/HEAD/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/Icons/CollageRadiomicsSlicer.png -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | numpy = "*" 8 | scikit-build = "*" 9 | scikit-image = "*" 10 | mahotas = "*" 11 | scipy = "*" 12 | jupyter = "*" 13 | SimpleITK = "*" 14 | 15 | [dev-packages] 16 | 17 | [requires] 18 | python_version = "3.8" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .local 2 | .ipynb_checkpoints 3 | */.ipynb_checkpoints/* 4 | profile_default/ 5 | ipython_config.py 6 | .cache/ 7 | .jupyter/ 8 | module/dist/* 9 | module/build/* 10 | output_data/ 11 | .bash_history 12 | .vscode/ 13 | module/collageradiomics.egg-info/* 14 | module/docs/build/* 15 | module/docs/source/_build/* 16 | .idea/* 17 | __pycache__ 18 | env_* 19 | -------------------------------------------------------------------------------- /docker/dev_environment/collageradiomics_pip/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nathanhillyer/ubuntu-base 2 | ENV LANG C.UTF-8 3 | RUN PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ 4 | $PIP_INSTALL \ 5 | collageradiomics==0.2.6 \ 6 | && \ 7 | ldconfig && \ 8 | apt-get clean && \ 9 | apt-get autoremove && \ 10 | rm -rf /var/lib/apt/lists/* /tmp/* ~/* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | contourpy==1.0.7 2 | cycler==0.11.0 3 | distro==1.8.0 4 | fonttools==4.38.0 5 | imageio==2.25.1 6 | joblib==1.2.0 7 | kiwisolver==1.4.4 8 | mahotas==1.4.13 9 | matplotlib==3.6.3 10 | networkx==3.0 11 | nibabel==5.0.1 12 | numpy==1.24.1 13 | packaging==23.0 14 | Pillow==9.4.0 15 | pyparsing==3.0.9 16 | python-dateutil==2.8.2 17 | PyWavelets==1.4.1 18 | scikit-build==0.16.6 19 | scikit-image==0.19.3 20 | scikit-learn==1.2.1 21 | scipy==1.10.0 22 | six==1.16.0 23 | threadpoolctl==3.1.0 24 | tifffile==2023.2.3 25 | -------------------------------------------------------------------------------- /docker/dev_environment/collageradiomics_examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nathanhillyer/ml-base 2 | ENV LANG C.UTF-8 3 | RUN PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ 4 | $PIP_INSTALL \ 5 | collageradiomics==0.2.6 \ 6 | Pillow \ 7 | && \ 8 | ldconfig && \ 9 | apt-get clean && \ 10 | apt-get autoremove && \ 11 | rm -rf /var/lib/apt/lists/* /tmp/* ~/* 12 | CMD JUPYTER_RUNTIME_DIR=/tmp jupyter notebook --no-browser --ip=0.0.0.0 --allow-root --NotebookApp.token= --notebook-dir='/root' 13 | EXPOSE 8888 14 | -------------------------------------------------------------------------------- /module/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | deploy: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.6' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install setuptools wheel twine 23 | - name: Build and publish 24 | env: 25 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 26 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 27 | run: | 28 | cd module 29 | sh setup.sh 30 | -------------------------------------------------------------------------------- /module/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME CollageRadiomicsSlicer) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ) 8 | 9 | set(MODULE_PYTHON_RESOURCES 10 | Resources/Icons/${MODULE_NAME}.png 11 | Resources/UI/${MODULE_NAME}.ui 12 | ) 13 | 14 | #----------------------------------------------------------------------------- 15 | slicerMacroBuildScriptedModule( 16 | NAME ${MODULE_NAME} 17 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 18 | RESOURCES ${MODULE_PYTHON_RESOURCES} 19 | WITH_GENERIC_TESTS 20 | ) 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | 25 | # Register the unittest subclass in the main script as a ctest. 26 | # Note that the test will also be available at runtime. 27 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 28 | 29 | # Additional build-time testing 30 | add_subdirectory(Testing) 31 | endif() 32 | -------------------------------------------------------------------------------- /cli/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | setup(name='collageradiomicscli', 7 | version='1.0.0', 8 | description='Get Collage features from an image and a binary mask', 9 | url='https://github.com/radxtools/collageradiomics', 10 | python_requires='>=3.6', 11 | author='Toth Technology', 12 | author_email='toth-tech@hillyer.me', 13 | license='BSD-3-Clause', 14 | zip_safe=False, 15 | install_requires=[ 16 | 'collageradiomics', 17 | 'setuptools>=47', 18 | 'SimpleITK==1.2.4', 19 | 'click' 20 | ], 21 | scripts=['collageradiomicscli.py'], 22 | classifiers=[ 23 | 'Intended Audience :: Science/Research', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: BSD License', 26 | 'Operating System :: OS Independent', 27 | 'Operating System :: POSIX :: Linux', 28 | 'Operating System :: Microsoft :: Windows :: Windows 10', 29 | 'Operating System :: MacOS', 30 | 'Programming Language :: Python :: 3', 31 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 BrIC Laboratory 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /module/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 BrIC Laboratory 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /module/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('requirements.txt', 'r') as f: 4 | requirements = f.read() 5 | 6 | with open('README.md', 'r') as fh: 7 | long_description = fh.read() 8 | 9 | with open('VERSION') as version_file: 10 | version = version_file.read().strip() 11 | 12 | setuptools.setup( 13 | name='collageradiomics', 14 | version=version, 15 | author='ThetaTech', 16 | author_email='robert.toth@thetatech.ai', 17 | description='Python implementation of COLLAGE texture features from BRIC & INVENTLAB.', 18 | long_description=long_description, 19 | long_description_content_type='text/markdown', 20 | url='https://github.com/ccipd/collageradiomics', 21 | project_urls={ 22 | 'Docker Examples': 'https://hub.docker.com/repository/docker/ccipd/collageradiomics-examples', 23 | 'Docker Module': 'https://hub.docker.com/repository/docker/ccipd/collageradiomics-pip', 24 | 'Github': 'https://github.com/ccipd/collageradiomics' 25 | }, 26 | py_modules=['collageradiomics'], 27 | classifiers=[ 28 | 'Development Status :: 3 - Alpha', 29 | 'Intended Audience :: Science/Research', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: BSD License', 32 | 'Operating System :: OS Independent', 33 | 'Operating System :: POSIX :: Linux', 34 | 'Operating System :: Microsoft :: Windows :: Windows 10', 35 | 'Operating System :: MacOS', 36 | 'Programming Language :: Python :: 3', 37 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 38 | ], 39 | install_requires=requirements, 40 | python_requires='>=3.6', 41 | keywords='radiomics cancerimaging medicalresearch computationalimaging', 42 | ) 43 | -------------------------------------------------------------------------------- /slicer/collageradiomics/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.4) 2 | 3 | project(CollageRadiomics) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://github.com/radxtools/collageradiomics/tree/master/slicer/collageradiomics") 8 | set(EXTENSION_CATEGORY "Informatics") 9 | set(EXTENSION_CONTRIBUTORS "Pallavi Tiwari (BriC), Satish Viswanath (BriC), Rob Toth (Toth Technology), Nathan Hillyer (Toth Technology)") 10 | set(EXTENSION_DESCRIPTION "The CollageRadiomics extension provides a 3D Slicer interface to the collageradiomics library. collageradiomics is an open-source python package for capturing subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood.") 11 | set(EXTENSION_ICONURL "https://github.com/radxtools/collageradiomics/raw/master/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/Icons/CollageRadiomicsSlicer.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://github.com/radxtools/collageradiomics/raw/master/slicer/collageradiomics/CollageRadiomicsSlicer/Screenshot.jpg") 13 | set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(CollageRadiomicsSlicer) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) -------------------------------------------------------------------------------- /module/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'collageradiomics' 21 | copyright = '2020, BrIC Laboratory' 22 | author = 'BrIC Laboratory' 23 | release = '0.2' 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'numpydoc', 27 | 'sphinx.ext.intersphinx', 28 | 'sphinx.ext.coverage', 29 | 'sphinx.ext.doctest', 30 | 'sphinx.ext.autosummary', 31 | 'sphinx.ext.graphviz', 32 | 'sphinx.ext.ifconfig', 33 | 'matplotlib.sphinxext.plot_directive', 34 | 'sphinx.ext.imgmath', 35 | 'sphinx_rtd_theme' 36 | ] 37 | imgmath_image_format = 'svg' 38 | templates_path = ['_templates'] 39 | exclude_patterns = [] 40 | html_theme = 'sphinx_rtd_theme' 41 | html_static_path = ['_static'] 42 | intersphinx_mapping = { 43 | 'numpy': ('https://numpy.org/doc/stable/', None), 44 | 'python': ('https://docs.python.org/dev', None), 45 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), 46 | 'matplotlib': ('https://matplotlib.org', None), 47 | 'imageio': ('https://imageio.readthedocs.io/en/stable', None), 48 | 'skimage': ('https://scikit-image.org/docs/stable', None), 49 | } 50 | master_doc = 'index' 51 | -------------------------------------------------------------------------------- /docker/dev_environment/ubuntu_base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | ENV LANG C.UTF-8 3 | RUN APT_INSTALL="apt-get install -y --no-install-recommends" && \ 4 | GIT_CLONE="git clone --depth 10" && \ 5 | rm -rf /var/lib/apt/lists/* \ 6 | /etc/apt/sources.list.d/cuda.list \ 7 | /etc/apt/sources.list.d/nvidia-ml.list && \ 8 | apt-get update && \ 9 | DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ 10 | software-properties-common \ 11 | && \ 12 | add-apt-repository "deb http://security.ubuntu.com/ubuntu xenial-security main" && \ 13 | apt-get update && \ 14 | # ================================================================== 15 | # tools 16 | # ------------------------------------------------------------------ 17 | DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ 18 | build-essential \ 19 | apt-utils \ 20 | ca-certificates \ 21 | wget \ 22 | git \ 23 | vim \ 24 | libssl-dev \ 25 | curl \ 26 | unzip \ 27 | unrar \ 28 | && \ 29 | $GIT_CLONE https://github.com/Kitware/CMake ~/cmake && \ 30 | cd ~/cmake && \ 31 | ./bootstrap && \ 32 | make -j"$(nproc)" install && \ 33 | add-apt-repository ppa:deadsnakes/ppa && \ 34 | apt-get update && \ 35 | DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ 36 | python3.8 \ 37 | python3.8-dev \ 38 | python3-distutils-extra \ 39 | && \ 40 | wget -O ~/get-pip.py \ 41 | https://bootstrap.pypa.io/get-pip.py && \ 42 | python3.8 ~/get-pip.py && \ 43 | ln -s /usr/bin/python3.8 /usr/local/bin/python3 && \ 44 | ln -s /usr/bin/python3.8 /usr/local/bin/python && \ 45 | # ================================================================== 46 | # config & cleanup 47 | # ------------------------------------------------------------------ 48 | ldconfig && \ 49 | apt-get clean && \ 50 | apt-get autoremove && \ 51 | rm -rf /var/lib/apt/lists/* /tmp/* ~/* 52 | 53 | EXPOSE 8888 -------------------------------------------------------------------------------- /module/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to collageradiomics's documentation! 2 | ============================================ 3 | 4 | CoLlAGe captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood. 5 | 6 | CoLlAGe is based on the hypothesis that disruption in tissue microarchitecture can be quantified on imaging by measuring the disorder in voxel-wise gradient orientations. CoLlAGe involves assigning every image voxel a ‘disorder value’ associated with the co-occurrence matrix of gradient orientations computed around every voxel. 7 | 8 | Details on extraction of CoLlAGe features are included in [1]. After feature extraction, the subsequent distribution or different statistics such as mean, median, variance etc can be computed and used in conjunction with a machine learning classifier to distinguish similar appearing pathologies. The feasibility of CoLlAGe in distinguishing cancer from treatment confounders/benign conditions and characterizing molecular subtypes of cancers has been demonstrated in the context of multiple challenging clinical problems. 9 | 10 | Helpful Links 11 | ============= 12 | Instructions: `README `_ 13 | 14 | RadxTools Website: ``_ 15 | 16 | Original Paper: `Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor `_ 17 | 18 | Code Documentation 19 | ================== 20 | 21 | Notes 22 | ----- 23 | The **attributes** below represent the public output intended to be available to consumers of this module. 24 | 25 | .. automodule:: collageradiomics 26 | :members: 27 | :undoc-members: 28 | :inherited-members: 29 | :show-inheritance: 30 | 31 | .. toctree:: 32 | :maxdepth: 3 33 | :caption: Contents: 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /docker/dev_environment/ml_base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nathanhillyer/ubuntu-base 2 | ENV LANG C.UTF-8 3 | RUN APT_INSTALL="apt-get install -y --no-install-recommends" && \ 4 | PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ 5 | GIT_CLONE="git clone --depth 10" && \ 6 | DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ 7 | software-properties-common \ 8 | && \ 9 | add-apt-repository ppa:deadsnakes/ppa && \ 10 | apt-get update && \ 11 | DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ 12 | python3.8 \ 13 | python3.8-dev \ 14 | python3-distutils-extra \ 15 | && \ 16 | wget -O ~/get-pip.py \ 17 | https://bootstrap.pypa.io/get-pip.py && \ 18 | python3.8 ~/get-pip.py && \ 19 | ln -s /usr/bin/python3.8 /usr/local/bin/python3 && \ 20 | ln -s /usr/bin/python3.8 /usr/local/bin/python && \ 21 | $PIP_INSTALL \ 22 | setuptools \ 23 | && \ 24 | $PIP_INSTALL \ 25 | numpy \ 26 | scipy \ 27 | pandas \ 28 | cloudpickle \ 29 | scikit-image>=0.14.2 \ 30 | scikit-learn \ 31 | matplotlib \ 32 | Cython \ 33 | tqdm \ 34 | imageio \ 35 | && \ 36 | $PIP_INSTALL \ 37 | pyradiomics \ 38 | mahotas \ 39 | eo-learn-features \ 40 | && \ 41 | $PIP_INSTALL -i https://test.pypi.org/simple/ \ 42 | rad-i \ 43 | # ================================================================== 44 | # jupyter 45 | # ------------------------------------------------------------------ 46 | $PIP_INSTALL \ 47 | jupyter \ 48 | && \ 49 | # ================================================================== 50 | # config & cleanup 51 | # ------------------------------------------------------------------ 52 | ldconfig && \ 53 | apt-get clean && \ 54 | apt-get autoremove && \ 55 | rm -rf /var/lib/apt/lists/* /tmp/* ~/* 56 | CMD jupyter notebook --no-browser --ip=0.0.0.0 --allow-root --NotebookApp.token= --notebook-dir='/root' 57 | EXPOSE 8888 -------------------------------------------------------------------------------- /examples/isbi.py: -------------------------------------------------------------------------------- 1 | """ 2 | The ISBI dataset can be found on https://theibsi.github.io/datasets 3 | This example computes the COLLAGE features for ibsi_1_ct_radiomics_phantom image. The path of the image and its mask should be modified based on your local dataset. 4 | 5 | Parameters: 6 | Path of the input image. 7 | Path of the input mask. 8 | Haralick window sizes. 9 | """ 10 | from pathlib import Path 11 | 12 | import nibabel as nib 13 | import numpy as np 14 | from scipy.stats import kurtosis, skew 15 | 16 | from collageradiomics import Collage 17 | 18 | path_image = Path( 19 | "/dataset/IBSI/ibsi_1_ct_radiomics_phantom/nifti/image/phantom.nii.gz" 20 | ) 21 | path_mask = Path("/dataset/IBSI/ibsi_1_ct_radiomics_phantom/nifti/mask/mask.nii.gz") 22 | 23 | image = nib.load(path_image).get_fdata() 24 | mask = nib.load(path_mask).get_fdata() 25 | 26 | image = np.swapaxes(image, 0, 2) 27 | mask = np.swapaxes(mask, 0, 2) 28 | 29 | path_result = Path("./examples") 30 | 31 | 32 | def compute_collage(image, mask, haralick_windows=[3, 5, 7, 9, 11]): 33 | """This method computes Collage features 34 | 35 | Args: 36 | image (path object): path of image 37 | mask (path object): path of mask 38 | haralick_windows (list, optional): Kernel window size. Defaults to [3, 5, 7, 9, 11]. 39 | 40 | Returns: 41 | numpy array: mean, standard deviation, skewness, and kurtosis of COLLAGE features based on kernel size and orientation 42 | """ 43 | windows_length = len(haralick_windows) 44 | 45 | feats = np.zeros((13 * windows_length * 2, 4), dtype=np.double) 46 | 47 | for window_idx, haralick_window_size in enumerate(haralick_windows): 48 | try: 49 | collage = Collage( 50 | image, 51 | mask, 52 | svd_radius=5, 53 | verbose_logging=True, 54 | num_unique_angles=64, 55 | haralick_window_size=haralick_window_size, 56 | ) 57 | 58 | collage_feats = collage.execute() 59 | np.save( 60 | rf"./{path_result}/COLLAGE_RAW_W{haralick_window_size}.npy", 61 | collage_feats, 62 | ) 63 | print("saved") 64 | 65 | for orientation in range(2): 66 | for collage_idx in range(13): 67 | k = window_idx * collage_idx * orientation 68 | feat = collage_feats[:, :, :, collage_idx, orientation].flatten() 69 | feat = feat[~np.isnan(feat)] 70 | 71 | feats[k, 0] = feat.mean() 72 | feats[k, 1] = feat.std() 73 | feats[k, 2] = skew(feat) 74 | feats[k, 3] = kurtosis(feat) 75 | 76 | except ValueError as err: 77 | print(f"VALUE ERROR- {err}") 78 | 79 | except Exception as err: 80 | print(f"EXCEPTION- {err}") 81 | 82 | return feats 83 | 84 | 85 | feats = compute_collage(image, mask, haralick_windows=[3]) 86 | 87 | np.save( 88 | rf"{path_result}/COLLAGE_STATS.npy", 89 | feats, 90 | ) 91 | -------------------------------------------------------------------------------- /module/test_script.py: -------------------------------------------------------------------------------- 1 | import collageradiomics 2 | import pydicom 3 | import logging 4 | from pydicom.pixel_data_handlers.util import apply_modality_lut, apply_voi_lut 5 | from skimage.exposure import equalize_hist 6 | import numpy as np 7 | from sklearn.preprocessing import minmax_scale 8 | from random import randint 9 | 10 | level = logging.INFO 11 | logging.basicConfig(level=level) 12 | logger = logging.getLogger() 13 | logger.setLevel(level) 14 | logger.info('Hello, world.') 15 | 16 | local_dcm_file = 'test.dcm' 17 | instance = pydicom.dcmread(local_dcm_file) 18 | slice_instance_uid = instance.SOPInstanceUID 19 | logger.debug(f'slice_instance_uid = {slice_instance_uid}') 20 | 21 | logger.info('Correcting image...') 22 | np_array = instance.pixel_array 23 | corrected = apply_modality_lut(np_array, instance) 24 | corrected = apply_voi_lut(corrected, instance) 25 | logger.debug(f'{np.histogram(corrected) = }') 26 | scaled_array = equalize_hist(corrected) 27 | logger.debug(f'np.histogram(scaled_array) = {np.histogram(scaled_array)}') 28 | logger.info('done.') 29 | 30 | width = 50 31 | height = 50 32 | min_row = randint(50,200) 33 | max_row = min_row + height 34 | min_col = randint(50,200) 35 | max_col = min_col + width 36 | 37 | original_shape = np_array.shape 38 | logger.debug(f'original_shape = {original_shape}') 39 | 40 | logger.info('Calculating collage features...') 41 | mask_array = np.zeros(original_shape, dtype='int') 42 | mask_array[min_row:max_row, min_col:max_col] = 1 43 | textures = collageradiomics.Collage(scaled_array, mask_array).execute() 44 | logger.info('Collage features calculated.') 45 | 46 | logger.debug(f'textures.shape = {textures.shape}') 47 | logger.debug(f'textures.dtype = {textures.dtype}') 48 | logger.debug(f'np.histogram(textures) = {np.histogram(textures, range=(np.nanmin(textures), np.nanmax(textures)))}') 49 | 50 | logger.info('Defining DICOM bit data to store unsigned greyscale texture output:') 51 | # http://dicomiseasy.blogspot.com/2012/08/chapter-12-pixel-data.html 52 | texture_bit_depth = 16 53 | texture_dtype = np.uint16 54 | 55 | instance.PhotometricInterpretation = 'MONOCHROME2' 56 | instance.SamplesPerPixel = 1 57 | instance.BitsAllocated = texture_bit_depth 58 | instance.BitsStored = texture_bit_depth 59 | instance.HighBit = texture_bit_depth - 1 60 | instance.PixelRepresentation = 0 # unsigned 61 | logger.info('DICOM bit data defined.') 62 | 63 | min_output_value = 0 64 | max_output_value = 2**texture_bit_depth-1 65 | 66 | for texture_index in range(textures.shape[2]): 67 | # https://stackoverflow.com/a/65964648 68 | texture_slice = textures[:,:,texture_index].copy() 69 | 70 | logger.info('Rescaling texture to full range of DICOM bit depth:') 71 | flattened = texture_slice.flatten() 72 | 73 | logger.info('Replacing NaN values with sentinel value...') 74 | flattened = np.nan_to_num(flattened, nan=np.nanmin(flattened)) 75 | 76 | scaled = minmax_scale(flattened, (min_output_value, max_output_value)) 77 | texture_slice = scaled.reshape(texture_slice.shape) 78 | #logger.debug(np.histogram(texture_slice, range=(np.nanmin(texture_slice), np.nanmax(texture_slice)))) 79 | logger.info('Rescaling texture to full range of DICOM bit depth done.') 80 | 81 | logger.info(f'Casting to {texture_dtype} and storing in instance...') 82 | output_pixel_data = texture_slice.astype(texture_dtype) 83 | #logger.debug(np.histogram(output_pixel_data, range=(np.nanmin(output_pixel_data), np.nanmax(output_pixel_data)))) 84 | #logger.debug(f'output_pixel_data.dtype = {output_pixel_data.dtype}') 85 | instance.PixelData = output_pixel_data 86 | logger.info('Storing in instance done.') 87 | 88 | feature_name = collageradiomics.HaralickFeature(texture_index).name 89 | filename = f'collage_feature_{texture_index:02d}_{feature_name}.dcm' 90 | instance.save_as(filename) 91 | logger.info(f'Saved {filename = }') 92 | -------------------------------------------------------------------------------- /slicer/collageradiomics/README.md: -------------------------------------------------------------------------------- 1 | [![doi](https://img.shields.io/badge/doi-10.1038/srep37241-brightgreen.svg)](https://doi.org/10.1038/srep37241) 2 | 3 | # CollageRadiomics Slicer Extension 4 | 5 | **CoLlAGe** captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood. 6 | 7 | **CoLlAGe** is based on the hypothesis that disruption in tissue microarchitecture can be quantified on imaging by measuring the disorder in voxel-wise gradient orientations. CoLlAGe involves assigning every image voxel a ‘disorder value’ associated with the co-occurrence matrix of gradient orientations computed around every voxel. 8 | 9 | Details on extraction of **CoLlAGe** features are included in [\[1\]](#references). After feature extraction, the subsequent distribution or different statistics such as mean, median, variance etc can be computed and used in conjunction with a machine learning classifier to distinguish similar appearing pathologies. The feasibility of CoLlAGe in distinguishing cancer from treatment confounders/benign conditions and characterizing molecular subtypes of cancers has been demonstrated in the context of multiple challenging clinical problems. 10 | 11 | # Table of Contents 12 | - [Slicer](#slicer) 13 | - [Overview](#overview) 14 | - [Tutorials](#tutorials) 15 | - [End to End Demo](#end-to-end-demo) 16 | - [Multiple Angles & Textures](#multiple-angles-&-textures) 17 | - [Contact](#contact) 18 | - [References](#references) 19 | 20 | # Slicer 21 | _[Back to **Table of Contents**](#table-of-contents)_ 22 | 23 | The collageradiomics Slicer 3D extension allows a user to run the collage algorithm on 3D images and then visualize and save the results. This is done by providing an input volume and a mask via segmentation. 24 | 25 | ## Overview 26 | _[Back to **Table of Contents**](#table-of-contents)_ 27 | 28 | The general operational flow for using the Slicer extension is to load a 3D image, segment a relavent portion, configure the collage parameters, and run the algorithm. 29 | 30 | Once this is completed, the user can take the output volume(s) and visualize them within Slicer. Slicer also supports exporting in various formats, e.g. `.mha`. 31 | 32 | # Tutorials 33 | _[Back to **Table of Contents**](#table-of-contents)_ 34 | 35 | Here are some tutorials to get you started. 36 | 37 | ## End to End Demo 38 | 39 | Here is a complete demonstration of loading sample data into Slicer, segmenting a tumor, visualizing the output, and saving it to a file. 40 | 41 | [![Collage Demonstration](Tutorials/CollageFullDemo.png?raw=true)](https://youtu.be/9om8FMpY1vA "Collage Demonstration") 42 | 43 | ## Multiple Angles & Textures 44 | _[Back to **Table of Contents**](#table-of-contents)_ 45 | 46 | Here is a demonstration of how to process and view multiple dominant directions and textures. 47 | 48 | [![Collage Multiple Angles & Textures Demonstration](Tutorials/CollageMultipleDemo.png?raw=true)](https://youtu.be/9om8FMpY1vA "Collage Multiple Angles & Textures Demonstration") 49 | 50 | # Contact 51 | _[Back to **Table of Contents**](#table-of-contents)_ 52 | 53 | Please report any issues or feature requests via the [Issue Tracker](https://github.com/radxtools/collageradiomics/issues). 54 | 55 | Additional information can be found on the [BrIC Lab](http://bric-lab.com) website. 56 | 57 | # References 58 | _[Back to **Table of Contents**](#table-of-contents)_ 59 | 60 | [![doi](https://img.shields.io/badge/doi-10.1038/srep37241-brightgreen.svg)](https://doi.org/10.1038/srep37241) 61 | 62 | 63 | 64 | If you make use of this implementation, please cite the following paper: 65 | 66 | [1] Prasanna, P., Tiwari, P., & Madabhushi, A. (2016). "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports", 6:37241. 67 | 68 | [2] R. M. Haralick, K. Shanmugam and I. Dinstein, "Textural Features for Image Classification," in IEEE Transactions on Systems, Man, and Cybernetics, vol. SMC-3, no. 6, pp. 610-621, Nov. 1973, [doi: 10.1109/TSMC.1973.4309314](https://doi.org/10.1109/TSMC.1973.4309314). 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Continuous Delivery](https://github.com/radxtools/collageradiomics/workflows/Continuous%20Delivery/badge.svg) [![Documentation Status](https://readthedocs.org/projects/collageradiomics/badge/?version=latest)](https://collageradiomics.readthedocs.io/en/latest/?badge=latest) [![doi](https://img.shields.io/badge/doi-10.1038/srep37241-brightgreen.svg)](https://doi.org/10.1038/srep37241) 2 | 3 | # Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe) 4 | 5 | # Table of Contents 6 | - [Science](#science) 7 | - [Overview](#overview) 8 | - [References](#references) 9 | - [Code](#code) 10 | - [Installation & Setup](#installation--setup) 11 | - [Pip Usage](#pip-usage) 12 | - [Docker Notebooks](#docker-notebooks) 13 | - [Docker Sandbox](#docker-sandbox) 14 | - [Python Usage](#python-usage) 15 | 16 | # Science 17 | ## Overview 18 | _[Back to **Table of Contents**](#table-of-contents)_ 19 | 20 | **CoLlAGe** captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood. 21 | 22 | **CoLlAGe** is based on the hypothesis that disruption in tissue microarchitecture can be quantified on imaging by measuring the disorder in voxel-wise gradient orientations. CoLlAGe involves assigning every image voxel a ‘disorder value’ associated with the co-occurrence matrix of gradient orientations computed around every voxel. 23 | 24 | Details on extraction of **CoLlAGe** features are included in [\[1\]](#references). After feature extraction, the subsequent distribution or different statistics such as mean, median, variance etc can be computed and used in conjunction with a machine learning classifier to distinguish similar appearing pathologies. The feasibility of CoLlAGe in distinguishing cancer from treatment confounders/benign conditions and characterizing molecular subtypes of cancers has been demonstrated in the context of multiple challenging clinical problems. 25 | 26 | Each of the 13 **CoLlAGe** correlate to one of the 13 Haralick texture features[\[2\]](#references): 27 | 1. _AngularSecondMoment_ 28 | 2. _Contrast_ 29 | 3. _Correlation_ 30 | 4. _SumOfSquareVariance_ 31 | 5. _SumAverage_ 32 | 6. _SumVariance_ 33 | 7. _SumEntropy_ 34 | 8. _Entropy_ 35 | 9. _DifferenceVariance_ 36 | 10. _DifferenceEntropy_ 37 | 11. _InformationMeasureOfCorrelation1_ 38 | 12. _InformationMeasureOfCorrelation2_ 39 | 13. _MaximalCorrelationCoefficient_ 40 | 41 | ## References 42 | _[Back to **Table of Contents**](#table-of-contents)_ 43 | 44 | 45 | 46 | If you make use of this implementation, please cite the following paper: 47 | 48 | [1] Prasanna, P., Tiwari, P., & Madabhushi, A. (2016). "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports", 6:37241. 49 | 50 | [2] R. M. Haralick, K. Shanmugam and I. Dinstein, "Textural Features for Image Classification," in IEEE Transactions on Systems, Man, and Cybernetics, vol. SMC-3, no. 6, pp. 610-621, Nov. 1973, [doi: 10.1109/TSMC.1973.4309314](https://doi.org/10.1109/TSMC.1973.4309314). 51 | 52 | # Code 53 | _[Back to **Table of Contents**](#table-of-contents)_ 54 | 55 | We made the collage object idempotent Our **CoLlAGe** module includes parameter tuning information in the output. It contains the image(s) and mask(s), and the settings applied upon them. This allows multiple fully reproducible runs without having to remember or find the original parameters. 56 | 57 | Documentation can be found at 58 | http://collageradiomics.rtfd.io/ 59 | 60 | We depend on the following libraries: 61 | - `matplotlib` 62 | - `numpy` 63 | - `scikit-learn` 64 | - `scikit-build` 65 | - `mahotas` 66 | - `scipy` 67 | 68 | # Installation & Setup 69 | _[Back to **Table of Contents**](#table-of-contents)_ 70 | 71 | ## Pip Usage 72 | ```console 73 | pip3 install --upgrade collageradiomics 74 | 75 | python3 -c 'import collageradiomics; print(collageradiomics.__name__)' 76 | ``` 77 | 78 | ## Local usage 79 | ```shell 80 | # get the code 81 | git clone https://github.com/radxtools/collageradiomics 82 | cd collageradiomics 83 | 84 | # set up virtual environment 85 | python3 -m venv collageenv 86 | source collageenv/bin/activate 87 | 88 | # install requirements 89 | sudo apt -y install build-essential gcc gfortran python-dev libopenblas-dev liblapack-dev cython libjpeg-dev zlib1g-dev 90 | pip3 install -r requirements.txt 91 | 92 | # run test script 93 | cd modules 94 | python3 test_script.py 95 | ``` 96 | 97 | ## Docker Sandbox 98 | _[Back to **Table of Contents**](#table-of-contents)_ 99 | 100 | This is the most straightforward way to start playing with the code. And it does not require the `git` commands that the **Jupyter** examples require. This is simply a pre-built container that lets you start trying out the module in **Python** immediately. 101 | 102 | ```console 103 | sudo docker pull radxtools/collageradiomics-pip:latest 104 | sudo docker run -it -v $PWD:/root radxtools/collageradiomics-pip 105 | ``` 106 | If your terminal prompt changes to `root@[random_string]:/#` then you are now working inside the standardized **Docker** sandbox container environment. 107 | 108 | Then you can try to import collage: 109 | ```shell 110 | python3 -c 'import collageradiomics; print(collageradiomics.__name__)' 111 | ``` 112 | 113 | ## Docker Notebooks 114 | 115 | ### Prepare Jupyter Notebooks 116 | To load the example jupyter notebooks, run the following commands (with or without `sudo` depending on your environment): 117 | ```console 118 | sudo docker pull radxtools/collageradiomics-pip:latest 119 | sudo docker run -it radxtools/collageradiomics-pip 120 | 121 | git clone https://github.com/radxtools/collageradiomics.git 122 | sudo docker pull radxtools/collageradiomics-examples:latest 123 | sudo docker run -it -p 8888:8888 -v $PWD:/root radxtools/collageradiomics-examples 124 | ``` 125 | 126 | ### Exploring The Examples 127 | _[Back to **Table of Contents**](#table-of-contents)_ 128 | 129 | 1. Open up a web browser to http://localhost:8888 130 | ![Jupyter Home](https://i.imgur.com/0XQ8OlT.png) 131 | 2. Navigate to the _Jupyter_ :arrow_right: _Examples_ directory. 132 | ![Jupyter Examples](https://i.imgur.com/NjdMlOr.png) 133 | 3. Click on one of the example `*.ipynb` files. 134 | 4. Run _Cell_ :arrow_right: _Run all_. 135 | ![Jupyter Run Cells](https://i.imgur.com/GaAaNAS.png) 136 | ![Jupyter Output](https://i.imgur.com/PapCcsg.png) 137 | 5. Feel free to add your own cells and run them to get familiar with the **CoLlAGe** code. 138 | 6. To stop the **Jupyter** notebook and exit the **Docker** image, press `Ctrl+C` twice: 139 | 140 | To run python code with collage: 141 | ```console 142 | root@12b12d2bff59:/# python 143 | Python 3.8.2 (default, Apr 27 2020, 15:53:34) 144 | [GCC 9.3.0] on linux 145 | Type "help", "copyright", "credits" or "license" for more information. 146 | >>> import collageradiomics 147 | >>> collageradiomics.__name__ 148 | 'collageradiomics' 149 | ``` 150 | 151 | # Python Usage 152 | _[Back to **Table of Contents**](#table-of-contents)_ 153 | ```python 154 | 155 | ################################################## 156 | # imports 157 | import collageradiomics 158 | import pydicom 159 | import logging 160 | from pydicom.pixel_data_handlers.util import apply_modality_lut, apply_voi_lut 161 | from skimage.exposure import equalize_hist 162 | import numpy as np 163 | from sklearn.preprocessing import minmax_scale 164 | from random import randint 165 | ################################################## 166 | 167 | 168 | ################################################## 169 | # logging 170 | level = logging.INFO 171 | logging.basicConfig(level=level) 172 | logger = logging.getLogger() 173 | logger.setLevel(level) 174 | logger.info('Hello, world.') 175 | ################################################## 176 | 177 | 178 | ################################################## 179 | # loading data 180 | local_dcm_file = 'test.dcm' 181 | instance = pydicom.dcmread(local_dcm_file) 182 | slice_instance_uid = instance.SOPInstanceUID 183 | logger.debug(f'slice_instance_uid = {slice_instance_uid}') 184 | ################################################## 185 | 186 | 187 | ################################################## 188 | # preprocessing 189 | logger.info('Correcting image...') 190 | np_array = instance.pixel_array 191 | corrected = apply_modality_lut(np_array, instance) 192 | corrected = apply_voi_lut(corrected, instance) 193 | scaled_array = equalize_hist(corrected) 194 | logger.debug(f'np.histogram(scaled_array) = {np.histogram(scaled_array)}') 195 | logger.info('done.') 196 | ################################################## 197 | 198 | 199 | ################################################## 200 | # rectangular selection 201 | width = 50 202 | height = 50 203 | min_row = randint(30,300) 204 | max_row = min_row + height 205 | min_col = randint(30,300) 206 | max_col = min_col + width 207 | 208 | original_shape = np_array.shape 209 | logger.debug(f'original_shape = {original_shape}') 210 | logger.info('Calculating collage features...') 211 | mask_array = np.zeros(original_shape, dtype='int') 212 | mask_array[min_row:max_row, min_col:max_col] = 1 213 | ################################################## 214 | 215 | 216 | ################################################## 217 | # run collage 218 | textures = collageradiomics.Collage(scaled_array, mask_array).execute() 219 | 220 | logger.debug(f'textures.shape = {textures.shape}') 221 | logger.debug(f'textures.dtype = {textures.dtype}') 222 | logger.debug(f'np.histogram(textures.flatten()) = {np.histogram(textures.flatten(), range=(np.nanmin(textures), np.nanmax(textures)))}') 223 | ################################################## 224 | ``` 225 | -------------------------------------------------------------------------------- /jupyter/examples/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import collageradiomics\n", 10 | "import numpy as np\n", 11 | "import SimpleITK as sitk\n", 12 | "import matplotlib.pyplot as plt\n", 13 | "from matplotlib.patches import Rectangle\n", 14 | "from matplotlib.gridspec import GridSpec\n", 15 | "from matplotlib.patches import Rectangle\n", 16 | "from mpl_toolkits.axes_grid1 import make_axes_locatable" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": { 23 | "scrolled": true 24 | }, 25 | "outputs": [], 26 | "source": [ 27 | "# Load user data in this cell\n", 28 | "\n", 29 | "# Jupyter user: set this to define whether to test 3D calculations or not\n", 30 | "use_3D = False\n", 31 | "\n", 32 | "# Read sample image (reads the grey image as an RGB array, so a 3D numpy array)\n", 33 | "image_sitk = sitk.ReadImage('../../sample_data/BrainSliceTumor.png')\n", 34 | "image_array = sitk.GetArrayFromImage(image_sitk)\n", 35 | "mask_image = sitk.ReadImage('../../sample_data/BrainSliceTumorMask.png')\n", 36 | "mask_array = sitk.GetArrayFromImage(mask_image)\n", 37 | "\n", 38 | "if use_3D:\n", 39 | " # flip the first and last slice so there's some gradient in the Z dimension\n", 40 | " image_array[:,:,0] = np.flip(image_array[:,:,0],0)\n", 41 | " image_array[:,:,2] = np.flip(image_array[:,:,1],1)\n", 42 | "else:\n", 43 | " # extract a single slice\n", 44 | " image_array = image_array[:,:,0]\n", 45 | " mask_array = mask_array [:,:,0]" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "# Helper functions for visualization\n", 55 | "\n", 56 | "def show_colored_image(figure, axis, image_data, colormap=plt.cm.jet):\n", 57 | " \"\"\"Helper method to show a colored image in matplotlib.\n", 58 | "\n", 59 | "\n", 60 | " :param figure: figure upon which to display\n", 61 | " :type figure: matplotlib.figure.Figure\n", 62 | " :param axis: axis upon which to display\n", 63 | " :type axis: matplotlib.axes.Axes\n", 64 | " :param image_data: image to display\n", 65 | " :type image_data: numpy.ndarray\n", 66 | " :param colormap: color map to convert for display. Defaults to plt.cm.jet.\n", 67 | " :type colormap: matplotlib.colors.Colormap, optional\n", 68 | " \"\"\"\n", 69 | "\n", 70 | " if image_data.ndim == 3:\n", 71 | " image_data = image_data[:,:,0]\n", 72 | " image = axis.imshow(image_data, cmap=colormap)\n", 73 | " divider = make_axes_locatable(axis)\n", 74 | " colorbar_axis = divider.append_axes(\"right\", size=\"5%\", pad=0.05)\n", 75 | " figure.colorbar(image, cax=colorbar_axis)\n", 76 | "\n", 77 | "\n", 78 | "def create_highlighted_rectangle(x, y, w, h):\n", 79 | " \"\"\"Creates a matplotlib Rectangle object for a highlight effect\n", 80 | "\n", 81 | "\n", 82 | " :param x: x location to start rectangle\n", 83 | " :type x: int\n", 84 | " :param y: y location to start rectangle\n", 85 | " :type y: int\n", 86 | " :param w: width of rectangle\n", 87 | " :type w: int\n", 88 | " :param h: height of rectangle\n", 89 | " :type h: int\n", 90 | "\n", 91 | " :returns: Rectangle used to highlight within a plot\n", 92 | " :rtype: matplotlib.patches.Rectangle\n", 93 | " \"\"\"\n", 94 | " return Rectangle((x, y), w, h, linewidth=3, edgecolor='cyan', facecolor='none')\n", 95 | "\n", 96 | "\n", 97 | "def highlight_rectangle_on_image(image_data, min_x, min_y, w, h, colormap=plt.cm.gray):\n", 98 | " \"\"\"Highlights a rectangle on an image at the passed in coordinate.\n", 99 | "\n", 100 | "\n", 101 | " :param image_data: image to highlight\n", 102 | " :type image_data: numpy.ndarray\n", 103 | " :param min_x: x location to start highlight\n", 104 | " :type min_x: int\n", 105 | " :param min_y: y location to start highlight\n", 106 | " :type min_y: int\n", 107 | " :param w: width of highlight rectangle\n", 108 | " :type w: int\n", 109 | " :param h: height of highlight rectangle\n", 110 | " :type h: int\n", 111 | " :param colormap: color map to convert for display. Defaults to plt.cm.jet.\n", 112 | " :type colormap: matplotlib.colors.Colormap, optional\n", 113 | "\n", 114 | " :returns: image array with highlighted rectangle\n", 115 | " :rtype: numpy.ndarray\n", 116 | " \"\"\"\n", 117 | " figure, axes = plt.subplots(1, 2, figsize=(15, 15))\n", 118 | "\n", 119 | " # Highlight window within image.\n", 120 | " show_colored_image(figure, axes[0], image_data, colormap)\n", 121 | " axes[0].add_patch(create_highlighted_rectangle(min_x, min_y, w, h))\n", 122 | "\n", 123 | " # Crop window.\n", 124 | " cropped_array = image_data[min_y:min_y + h, min_x:min_x + w]\n", 125 | " axes[1].set_title(f'Cropped Region ({w}x{h})')\n", 126 | " show_colored_image(figure, axes[1], cropped_array, colormap)\n", 127 | "\n", 128 | " plt.show()\n", 129 | "\n", 130 | " return cropped_array" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "# Show slice with mask\n", 140 | "figure = plt.figure(figsize = (10, 10))\n", 141 | "\n", 142 | "extent = 0, image_array.shape[1], 0, image_array.shape[0]\n", 143 | "\n", 144 | "# show the image\n", 145 | "plt.imshow(image_array[:,:,1] if use_3D else image_array, cmap = plt.cm.gray, extent=extent)\n", 146 | "\n", 147 | "# overlay the mask\n", 148 | "plt.imshow(mask_array[:,:,0] if use_3D else mask_array, cmap = plt.cm.jet, alpha=0.3, extent=extent)\n", 149 | "\n", 150 | "plt.title('Input image')\n", 151 | "\n", 152 | "figure.axes[0].get_xaxis().set_visible(False)\n", 153 | "figure.axes[0].get_yaxis().set_visible(False)\n", 154 | "\n", 155 | "print(image_array.shape)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "# Example of opti__init__.pyonal parameters\n", 165 | "collage = collageradiomics.Collage(\n", 166 | " image_array, \n", 167 | " mask_array, \n", 168 | " svd_radius=5, \n", 169 | " verbose_logging=True,\n", 170 | " num_unique_angles=64\n", 171 | ")" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "# Run CoLlage Algorithm.Prepare\n", 181 | "full_images = collage.execute()" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "# Display gradient\n", 191 | "figure, axes = plt.subplots(1, 3, figsize=(15, 15))\n", 192 | "show_colored_image(figure, axes[0], collage.dx)\n", 193 | "axes[0].set_title(f'Gx size={collage.dx.shape}')\n", 194 | "show_colored_image(figure, axes[1], collage.dy)\n", 195 | "axes[1].set_title(f'Gy size={collage.dy.shape}')\n", 196 | "show_colored_image(figure, axes[2], collage.dz)\n", 197 | "axes[2].set_title(f'Gz size={collage.dz.shape}')" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": { 204 | "scrolled": false 205 | }, 206 | "outputs": [], 207 | "source": [ 208 | "# Display dominant angles\n", 209 | "figure, axes = plt.subplots(1, 2, figsize=(15, 15))\n", 210 | "print(collage.dominant_angles.shape)\n", 211 | "show_colored_image(figure, axes[0], collage.dominant_angles[:,:,:,0])\n", 212 | "if use_3D:\n", 213 | " show_colored_image(figure, axes[1], collage.dominant_angles[:,:,:,1])\n", 214 | " axes[1].set_title('Secondary Angles: arctan(dz/(dx^2+dy^2))')\n", 215 | "else:\n", 216 | " axes[1].set_title('(Unused in 2D mode)')\n", 217 | "axes[0].set_title('Dominant Angles: arctan(dy/dx)')\n" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "# Display haralick\n", 227 | "figure, axes = plt.subplots(3, 5, figsize=(15,15))\n", 228 | "\n", 229 | "for row in range(3):\n", 230 | " for col in range(5):\n", 231 | " feature = row*5+col\n", 232 | " axis = axes[row][col]\n", 233 | " axis.set_axis_off()\n", 234 | " if feature>=13:\n", 235 | " continue\n", 236 | " collage_output = collage.get_single_feature_output(feature)\n", 237 | " if use_3D:\n", 238 | " collage_output = collage_output[:,:,:,0] # use dominant angle\n", 239 | " show_colored_image(figure, axis, collage_output)\n", 240 | " axis.set_title(f'Collage {feature+1}')" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "# Extract a single collage feature by name and overlay it\n", 250 | "which_feature = collageradiomics.HaralickFeature.Entropy\n", 251 | "\n", 252 | "alpha = 0.5 # transparency\n", 253 | "\n", 254 | "# extract the output\n", 255 | "collage_output = collage.get_single_feature_output(which_feature)\n", 256 | "print(collage_output.shape)\n", 257 | "if use_3D:\n", 258 | " collage_output = collage_output[:,:,1,1]\n", 259 | "\n", 260 | "# Show preview of larger version of image.\n", 261 | "figure = plt.figure(figsize = (15, 15))\n", 262 | "\n", 263 | "# show the image\n", 264 | "plt.imshow(image_array[:,:,1] if use_3D else image_array, cmap = plt.cm.gray, extent=extent)\n", 265 | "\n", 266 | "# overlay the collage output\n", 267 | "plt.imshow(collage_output, cmap = plt.cm.jet, alpha=alpha, extent=extent)\n", 268 | "\n", 269 | "figure.axes[0].get_xaxis().set_visible(False)\n", 270 | "figure.axes[0].get_yaxis().set_visible(False)\n", 271 | "\n", 272 | "plt.title('Collage Overlay')" 273 | ] 274 | } 275 | ], 276 | "metadata": { 277 | "kernelspec": { 278 | "display_name": "Python 3", 279 | "language": "python", 280 | "name": "python3" 281 | }, 282 | "language_info": { 283 | "codemirror_mode": { 284 | "name": "ipython", 285 | "version": 3 286 | }, 287 | "file_extension": ".py", 288 | "mimetype": "text/x-python", 289 | "name": "python", 290 | "nbconvert_exporter": "python", 291 | "pygments_lexer": "ipython3", 292 | "version": "3.7.5" 293 | } 294 | }, 295 | "nbformat": 4, 296 | "nbformat_minor": 4 297 | } 298 | -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/CollageRadiomicsSlicer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import logging 4 | import vtk, qt, ctk, slicer 5 | import numpy as np 6 | import datetime 7 | from slicer.ScriptedLoadableModule import * 8 | from slicer.util import VTKObservationMixin 9 | try: 10 | import collageradiomics 11 | except: 12 | slicer.util.pip_install('collageradiomics') 13 | slicer.util.pip_install('mahotas') 14 | 15 | from collageradiomics import HaralickFeature, DifferenceVarianceInterpretation, Collage 16 | # 17 | # CollageRadiomicsSlicer 18 | # 19 | 20 | class CollageRadiomicsSlicer(ScriptedLoadableModule): 21 | """Uses ScriptedLoadableModule base class, available at: 22 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 23 | """ 24 | 25 | def __init__(self, parent): 26 | ScriptedLoadableModule.__init__(self, parent) 27 | self.parent.title = "CollageRadiomics" 28 | self.parent.categories = ["Informatics"] 29 | self.parent.dependencies = [] 30 | self.parent.contributors = ["BriC Lab (Case Western University)"] 31 | self.parent.helpText = """ 32 | CoLlAGe captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood. 33 | """ 34 | self.parent.helpText += self.getDefaultModuleDocumentationLink() 35 | self.parent.acknowledgementText = """ 36 | If you make use of this implementation, please cite the following paper: 37 | 38 | [1] Prasanna, P., Tiwari, P., & Madabhushi, A. (2016). "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports", 6:37241. 39 | 40 | [2] R. M. Haralick, K. Shanmugam and I. Dinstein, "Textural Features for Image Classification," in IEEE Transactions on Systems, Man, and Cybernetics, vol. SMC-3, no. 6, pp. 610-621, Nov. 1973, doi: 10.1109/TSMC.1973.4309314. 41 | """ 42 | 43 | # 44 | # CollageRadiomicsSlicerWidget 45 | # 46 | 47 | class CollageRadiomicsSlicerWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 48 | """Uses ScriptedLoadableModuleWidget base class, available at: 49 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 50 | """ 51 | 52 | def __init__(self, parent=None): 53 | """ 54 | Called when the user opens the module the first time and the widget is initialized. 55 | """ 56 | ScriptedLoadableModuleWidget.__init__(self, parent) 57 | VTKObservationMixin.__init__(self) 58 | self.logic = None 59 | self._parameterNode = None 60 | 61 | def setup(self): 62 | """ 63 | Called when the user opens the module the first time and the widget is initialized. 64 | """ 65 | ScriptedLoadableModuleWidget.setup(self) 66 | 67 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/CollageRadiomicsSlicer.ui')) 68 | self.layout.addWidget(uiWidget) 69 | self.ui = slicer.util.childWidgetVariables(uiWidget) 70 | uiWidget.setMRMLScene(slicer.mrmlScene) 71 | self.layout.addStretch(1) 72 | 73 | self.ui.CheckBox.setVisible(False) 74 | advancedFormLayout = self.ui.featuresGridLayout 75 | 76 | self.individualFeatures = {} 77 | 78 | for i, feature in enumerate(HaralickFeature): 79 | checkBox = ctk.ctkCheckBox() 80 | checkBox.text = feature.name 81 | self.individualFeatures[feature] = checkBox 82 | checkBox.connect('clicked(bool)', self.onIndividualFeature) 83 | row = i / 2 84 | column = i % 2 85 | advancedFormLayout.addWidget(checkBox, row, column) 86 | 87 | self.allFeatures = ctk.ctkCheckBox() 88 | self.allFeatures.text = 'All' 89 | self.allFeatures.setChecked(True) 90 | self.allFeatures.connect('clicked(bool)', self.onAllFeature) 91 | row = len(HaralickFeature) / 2 92 | column = len(HaralickFeature) % 2 93 | advancedFormLayout.addWidget(self.allFeatures, row, column) 94 | 95 | self.ui.inputMaskSelector.nodeTypes = ['vtkMRMLLabelMapVolumeNode', 'vtkMRMLSegmentationNode'] 96 | self.ui.inputMaskSelector.selectNodeUponCreation = True 97 | self.ui.inputMaskSelector.addEnabled = False 98 | self.ui.inputMaskSelector.removeEnabled = False 99 | self.ui.inputMaskSelector.noneEnabled = True 100 | self.ui.inputMaskSelector.showHidden = False 101 | self.ui.inputMaskSelector.showChildNodeTypes = False 102 | self.ui.inputMaskSelector.setMRMLScene(slicer.mrmlScene) 103 | self.ui.inputMaskSelector.setToolTip('Pick the regions for feature calculation - defined by a segmentation or labelmap volume node.') 104 | 105 | for interpretation in DifferenceVarianceInterpretation: 106 | self.ui.differenceVarianceComboBox.addItem(interpretation.name) 107 | 108 | self.ui.phiCheckBox.connect('clicked(bool)', self.onPhi) 109 | self.ui.thetaCheckBox.connect('clicked(bool)', self.onTheta) 110 | 111 | self.logic = CollageRadiomicsSlicerLogic() 112 | self.ui.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", self.moduleName) 113 | self.setParameterNode(self.logic.getParameterNode()) 114 | 115 | self.ui.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode) 116 | self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) 117 | 118 | self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 119 | self.ui.inputMaskSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 120 | 121 | self.updateGUIFromParameterNode() 122 | 123 | def onIndividualFeature(self): 124 | self.allFeatures.setChecked(False) 125 | 126 | def onAllFeature(self): 127 | for feature in self.individualFeatures.values(): 128 | feature.setChecked(False) 129 | 130 | def onPhi(self): 131 | if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: 132 | self.ui.thetaCheckBox.setChecked(True) 133 | 134 | def onTheta(self): 135 | if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: 136 | self.ui.phiCheckBox.setChecked(True) 137 | 138 | def cleanup(self): 139 | """ 140 | Called when the application closes and the module widget is destroyed. 141 | """ 142 | self.removeObservers() 143 | 144 | def setParameterNode(self, inputParameterNode): 145 | """ 146 | Adds observers to the selected parameter node. Observation is needed because when the 147 | parameter node is changed then the GUI must be updated immediately. 148 | """ 149 | 150 | if inputParameterNode: 151 | self.logic.setDefaultParameters(inputParameterNode) 152 | 153 | # Set parameter node in the parameter node selector widget 154 | wasBlocked = self.ui.parameterNodeSelector.blockSignals(True) 155 | self.ui.parameterNodeSelector.setCurrentNode(inputParameterNode) 156 | self.ui.parameterNodeSelector.blockSignals(wasBlocked) 157 | 158 | if inputParameterNode == self._parameterNode: 159 | return 160 | 161 | if self._parameterNode is not None: 162 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 163 | if inputParameterNode is not None: 164 | self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 165 | self._parameterNode = inputParameterNode 166 | 167 | self.updateGUIFromParameterNode() 168 | 169 | def updateGUIFromParameterNode(self, caller=None, event=None): 170 | """ 171 | This method is called whenever parameter node is changed. 172 | The module GUI is updated to show the current state of the parameter node. 173 | """ 174 | self.ui.basicCollapsibleButton.enabled = self._parameterNode is not None 175 | self.ui.advancedCollapsibleButton.enabled = self._parameterNode is not None 176 | if self._parameterNode is None: 177 | return 178 | 179 | wasBlocked = self.ui.inputSelector.blockSignals(True) 180 | self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume")) 181 | self.ui.inputSelector.blockSignals(wasBlocked) 182 | 183 | wasBlocked = self.ui.inputMaskSelector.blockSignals(True) 184 | self.ui.inputMaskSelector.setCurrentNode(self._parameterNode.GetNodeReference("MaskVolume")) 185 | self.ui.inputMaskSelector.blockSignals(wasBlocked) 186 | 187 | if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("MaskVolume"): 188 | self.ui.applyButton.toolTip = "Compute collage" 189 | self.ui.applyButton.enabled = True 190 | else: 191 | self.ui.applyButton.toolTip = "Select input and mask volume nodes" 192 | self.ui.applyButton.enabled = False 193 | 194 | def updateParameterNodeFromGUI(self, caller=None, event=None): 195 | """ 196 | This method is called when the user makes any change in the GUI. 197 | The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). 198 | """ 199 | 200 | if self._parameterNode is None: 201 | return 202 | 203 | self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) 204 | self._parameterNode.SetNodeReferenceID("MaskVolume", self.ui.inputMaskSelector.currentNodeID) 205 | 206 | def onApplyButton(self): 207 | """ 208 | Run processing when user clicks "Apply" button. 209 | """ 210 | features = [] 211 | for feature, checkbox in self.individualFeatures.items(): 212 | if checkbox.checked or self.allFeatures.checked: 213 | features.append(feature) 214 | 215 | svd_radius = self.ui.svdSlider.value 216 | haralick_size = self.ui.windowSlider.value 217 | grey_levels = self.ui.graylevelsSlider.value 218 | dimensions = [] 219 | if self.ui.phiCheckBox.checked: 220 | dimensions.append(0) 221 | if self.ui.thetaCheckBox.checked: 222 | dimensions.append(1) 223 | 224 | try: 225 | self.logic.run(self.ui.inputSelector.currentNode(), self.ui.inputMaskSelector.currentNode(), features=features, window_size=haralick_size, grey_levels=grey_levels, dimensions=dimensions) 226 | except Exception as e: 227 | slicer.util.errorDisplay("Failed to compute results: "+str(e)) 228 | import traceback 229 | traceback.print_exc() 230 | 231 | 232 | # 233 | # CollageRadiomicsSlicerLogic 234 | # 235 | 236 | class CollageRadiomicsSlicerLogic(ScriptedLoadableModuleLogic): 237 | """This class should implement all the actual 238 | computation done by your module. The interface 239 | should be such that other python code can import 240 | this class and make use of the functionality without 241 | requiring an instance of the Widget. 242 | Uses ScriptedLoadableModuleLogic base class, available at: 243 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 244 | """ 245 | 246 | def setDefaultParameters(self, parameterNode): 247 | """ 248 | Initialize parameter node with default settings. 249 | """ 250 | # if not parameterNode.GetParameter("Threshold"): 251 | # parameterNode.SetParameter("Threshold", "50.0") 252 | # if not parameterNode.GetParameter("Invert"): 253 | # parameterNode.SetParameter("Invert", "false") 254 | 255 | def run(self, inputVolume, mask, dimensions=[0], invert=False, showResult=True, svd_radius=2, verbose_logging=True, features=[HaralickFeature.Contrast], window_size=-1, grey_levels=64): 256 | # This will convert the mask segmentation into a binary representation via LabelMapVolume 257 | # LabelMapVolume supports conversion to numpy arrays and that's what we need 258 | # to input to collage. 259 | labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode') 260 | slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode(mask, labelmapVolumeNode, inputVolume) 261 | inputMaskArray = slicer.util.arrayFromVolume(labelmapVolumeNode) 262 | # collage expects the image data to come first and arrayFromVolume returns it reversed order 263 | # as such, we switch the first and last axes 264 | inputMaskArray = np.swapaxes(inputMaskArray, 0, 2) 265 | 266 | # Input volume can be converted directly to a numpy array, unlike the mask. 267 | inputArray = slicer.util.arrayFromVolume(inputVolume) 268 | 269 | # same as above, need to swap the axes 270 | inputArray = np.swapaxes(inputArray, 0, 2) 271 | 272 | num_mask_values = np.count_nonzero(inputMaskArray) 273 | max_num_mask_before_warning = 5000 274 | max_num_mask_before_error = 50000 275 | presentable_num_mask_values = num_mask_values / float(1000) 276 | if num_mask_values > max_num_mask_before_warning: 277 | warning = 'briefly' 278 | if num_mask_values > max_num_mask_before_error: 279 | warning = 'EXTENSIVELY' 280 | response = slicer.util.confirmOkCancelDisplay(f'The masked area being passed to collage contains {presentable_num_mask_values:.1f} thousand voxels and will {warning} lock up the user interface while processing. Continue?') 281 | if response == False: 282 | logging.info('User cancelled collage from large mask warning.') 283 | return 284 | 285 | collage = Collage(inputArray, inputMaskArray, svd_radius=svd_radius, verbose_logging=True, haralick_window_size=window_size, num_unique_angles=int(grey_levels)) 286 | results = collage.execute() 287 | 288 | iso_time_string = datetime.datetime.now().isoformat() 289 | for dimension in dimensions: 290 | for feature in features: 291 | outputVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLScalarVolumeNode') 292 | dimension_name = 'theta' 293 | if dimension == 1: 294 | dimension_name = 'phi' 295 | outputVolumeNode.SetName(f'collage_{iso_time_string}_{feature.name.lower()}_{dimension_name}') 296 | node_data = results[:,:,:,feature,dimension] 297 | node_data = np.swapaxes(node_data, 0, 2) 298 | outputVolumeNode.CopyOrientation(inputVolume) 299 | slicer.util.updateVolumeFromArray(outputVolumeNode, node_data) 300 | 301 | 302 | logging.info('Processing completed') 303 | slicer.mrmlScene.RemoveNode(labelmapVolumeNode) 304 | 305 | # CollageRadiomicsSlicerTest 306 | # 307 | 308 | class CollageRadiomicsSlicerTest(ScriptedLoadableModuleTest): 309 | """ 310 | This is the test case for your scripted module. 311 | Uses ScriptedLoadableModuleTest base class, available at: 312 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 313 | """ 314 | 315 | def setUp(self): 316 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. 317 | """ 318 | slicer.mrmlScene.Clear(0) 319 | 320 | def runTest(self): 321 | """Run as few or as many tests as needed here. 322 | """ 323 | self.setUp() 324 | -------------------------------------------------------------------------------- /cli/collageradiomicscli.py: -------------------------------------------------------------------------------- 1 | #!usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import click 5 | import os 6 | import sys 7 | import SimpleITK as sitk 8 | import csv 9 | from scipy import stats 10 | import numpy as np 11 | 12 | import collageradiomics 13 | 14 | @click.command() 15 | @click.option('-i', '--input', required=True, help='Path to an input image from which features will be extracted or Path to an input csv (header: ID, Image, Mask) from which features will be extracted for several images and masks.') 16 | @click.option('-m', '--mask', help='Path to a mask that will be considered as binary. The highest pixel value will be considered as information and all other values will be considered outside the mask - Required if input is an image') 17 | @click.option('-o', '--outputfile', required=True, help='Path to the output CSV file.') 18 | @click.option('-v', '--verbose', default=True, help='Provides additional debug output.') 19 | @click.option('-d', '--dimensions', help='Optional number of dimensions upon which to run collage. Supported values are 2 and 3. If left out, we will default to the dimensionality of the image itself, which may not reflect expected behavior if the image has an alpha channel.', type=click.IntRange(2, 3, clamp=True)) 20 | @click.option('-s', '--svdradius', default=5, help='SVD radius is used for the dominant angle calculation pixel radius. DEFAULTS to 5 and is suggested to remain at the default.') 21 | @click.option('-h', '--haralickwindow', default=-1, help='Number of pixels around each pixel used to calculate the haralick texture. DEFAULTS to svdradius * 2 - 1.') 22 | @click.option('-b', '--binsize', default=64, help='Number of bins to use while calculating the grey level cooccurence matrix. DEFAULTS to 64.') 23 | @click.option('-l', '--label', default=-1, help='Some masks may have multiple labels. Select the label for which to calculate collage radiomics features. Use -1 if you want all labels in the mask as a value of True. DEFAULTS to -1.') 24 | @click.option('-e', '--extendstats', is_flag=True, help='Provides additional statistical metrics (mean and IQR) in the output.') 25 | 26 | def run(input, mask, outputfile, verbose, dimensions, svdradius, haralickwindow, binsize,label, extendstats): 27 | """CoLlAGe captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood.""" 28 | 29 | if input.endswith('.csv'): 30 | header = ['ID', 'Image', 'Mask', 'svdradius', 'haralickwindow', 'binsize', 'label'] 31 | features_list = [] 32 | list_failed_cases = [['ID', 'Image', 'Mask', 'Error']] 33 | if dimensions == 2: 34 | suffix = '' 35 | for feature in collageradiomics.HaralickFeature: 36 | if extendstats: 37 | features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'IQR'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Mean'+suffix, 'Collage'+feature.name+'Variance'+suffix]) 38 | else: 39 | features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Variance'+suffix]) 40 | header.append(features_list) 41 | output_list = [header] 42 | else: 43 | for suffix in ['Theta', 'Phi']: 44 | for feature in collageradiomics.HaralickFeature: 45 | if extendstats: 46 | features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'IQR'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Mean'+suffix, 'Collage'+feature.name+'Variance'+suffix]) 47 | else: 48 | features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Variance'+suffix]) 49 | header.extend(features_list) 50 | output_list = [header] 51 | 52 | with open(input, newline='') as csvfile: 53 | reader = csv.DictReader(csvfile) 54 | for row in reader: 55 | output_case = [] 56 | try: 57 | case_id = row['ID'] 58 | image_filepath = row['Image'] 59 | mask_filepath = row['Mask'] 60 | image = sitk.ReadImage(image_filepath) 61 | mask = sitk.ReadImage(mask_filepath) 62 | 63 | output_case.extend([case_id, image_filepath, mask_filepath, svdradius, haralickwindow, binsize, label]) 64 | 65 | # Check if user wants to select single label from the mask 66 | if label != -1: 67 | mask = sitk.BinaryThreshold(mask, lowerThreshold = label, upperThreshold = label, insideValue = 1, outsideValue = 0) 68 | 69 | image_array = sitk.GetArrayFromImage(image) 70 | mask_array = sitk.GetArrayFromImage(mask) 71 | 72 | # Collage is expecting array with x,y,z but sitk.GetArrayFromImage as z,y,x, so x show be swapped by z 73 | if dimensions != 2: 74 | image_array = np.swapaxes(image_array,0,2) 75 | mask_array = np.swapaxes(mask_array,0,2) 76 | 77 | # Remove any extra array dimensions if the user explicitly asks for 2D. 78 | if dimensions == 2: 79 | image_array = image_array[:,:,0] 80 | mask_array = mask_array [:,:,0] 81 | 82 | collage = collageradiomics.Collage( 83 | image_array, 84 | mask_array, 85 | svd_radius=svdradius, 86 | verbose_logging=verbose, 87 | num_unique_angles=binsize) 88 | 89 | collage.execute() 90 | 91 | for feature in collageradiomics.HaralickFeature: 92 | feature_output = collage.get_single_feature_output(feature) 93 | if image_array.ndim == 2: 94 | feature_output = feature_output[~np.isnan(feature_output)] 95 | 96 | # NumPy supports median natively, we'll use that. 97 | median = np.nanmedian(feature_output, axis=None) 98 | 99 | # Use SciPy for kurtosis, variance, and skewness. 100 | feature_stats = stats.describe(feature_output, axis=None) 101 | 102 | if extendstats: 103 | mean = feature_stats.mean #np.nanmean(feature_output, axis=None) 104 | iqr = stats.iqr(feature_output) 105 | 106 | output_case.extend([median, iqr, feature_stats.skewness, feature_stats.kurtosis, feature_stats.mean, feature_stats.variance]) 107 | else: 108 | output_case.extend([median, feature_stats.skewness, feature_stats.kurtosis, feature_stats.variance]) 109 | 110 | else: 111 | # Extract phi and theta angles. 112 | feature_output_theta = feature_output[:,:,:,0] 113 | feature_output_phi = feature_output[:,:,:,1] 114 | 115 | # Remove NaN for stat calculations. 116 | feature_output_theta = feature_output_theta[~np.isnan(feature_output_theta)] 117 | feature_output_phi = feature_output_phi[~np.isnan(feature_output_phi)] 118 | 119 | # NumPy supports median natively, we'll use that. 120 | median_theta = np.nanmedian(feature_output_theta, axis=None) 121 | median_phi = np.nanmedian(feature_output_phi, axis=None) 122 | 123 | # Use SciPy for kurtosis, variance, and skewness. 124 | feature_stats_theta = stats.describe(feature_output_theta.flatten(), axis=None) 125 | feature_stats_phi = stats.describe(feature_output_phi.flatten(), axis=None) 126 | 127 | if extendstats: 128 | mean_theta = feature_stats_theta.mean 129 | mean_phi = feature_stats_phi.mean 130 | iqr_theta = stats.iqr(feature_output_theta) 131 | iqr_phi = stats.iqr(feature_output_phi) 132 | 133 | output_case.extend([median_theta, iqr_theta, feature_stats_theta.skewness, feature_stats_theta.kurtosis, feature_stats_theta.mean, feature_stats_theta.variance, median_phi, iqr_phi, feature_stats_phi.skewness, feature_stats_phi.kurtosis, feature_stats_phi.mean, feature_stats_phi.variance]) 134 | else: 135 | output_case.extend([median_theta, feature_stats_theta.skewness, feature_stats_theta.kurtosis, feature_stats_theta.variance, median_phi, feature_stats_phi.skewness, feature_stats_phi.kurtosis, feature_stats_phi.variance]) 136 | output_list.append(output_case) 137 | except RuntimeError as err: 138 | list_failed_cases.append([case_id, image_filepath, mask_filepath, err]) 139 | except ValueError as err: 140 | list_failed_cases.append([case_id, image_filepath, mask_filepath, err]) 141 | 142 | # Create collage radiomic features output csv file 143 | with open(outputfile, 'w') as file: 144 | writer = csv.writer(file) 145 | writer.writerows(output_list) 146 | 147 | # Create errors output csv file 148 | with open(os.path.join(os.path.dirname(outputfile), 'errors_' + os.path.basename(outputfile)), 'w') as file: 149 | writer = csv.writer(file) 150 | writer.writerows(list_failed_cases) 151 | else: 152 | image = sitk.ReadImage(input) 153 | mask = sitk.ReadImage(mask) 154 | 155 | # Check if user wants to select single label from the mask 156 | if label != -1: 157 | mask = sitk.BinaryThreshold(mask, lowerThreshold = label, upperThreshold = label, insideValue = 1, outsideValue = 0) 158 | 159 | image_array = sitk.GetArrayFromImage(image) 160 | mask_array = sitk.GetArrayFromImage(mask) 161 | 162 | # Collage is expecting array with x,y,z but sitk.GetArrayFromImage as z,y,x, so x show be swapped by z 163 | if dimensions != 2: 164 | image_array = np.swapaxes(image_array,0,2) 165 | mask_array = np.swapaxes(mask_array,0,2) 166 | 167 | # Remove any extra array dimensions if the user explicitly asks for 2D. 168 | if dimensions == 2: 169 | image_array = image_array[:,:,0] 170 | mask_array = mask_array [:,:,0] 171 | 172 | collage = collageradiomics.Collage( 173 | image_array, 174 | mask_array, 175 | svd_radius=svdradius, 176 | verbose_logging=verbose, 177 | num_unique_angles=binsize) 178 | 179 | collage.execute() 180 | 181 | # Create a csv file at the passed in output file location. 182 | with open(outputfile, 'w', newline='') as csv_output_file: 183 | writer = csv.writer(csv_output_file) 184 | 185 | # Write the columns. 186 | writer.writerow(['FeatureName', 'Value']) 187 | for feature in collageradiomics.HaralickFeature: 188 | feature_output = collage.get_single_feature_output(feature) 189 | if image_array.ndim == 2: 190 | feature_output = feature_output[~np.isnan(feature_output)] 191 | 192 | # NumPy supports median natively, we'll use that. 193 | median = np.nanmedian(feature_output, axis=None) 194 | 195 | # Use SciPy for kurtosis, variance, and skewness. 196 | feature_stats = stats.describe(feature_output, axis=None) 197 | 198 | # Write CSV row for current feature. 199 | _write_csv_stats_row(writer, feature, median, feature_stats.skewness, feature_stats.kurtosis, feature_stats.variance) 200 | else: 201 | # Extract phi and theta angles. 202 | feature_output_theta = feature_output[:,:,:,0] 203 | feature_output_phi = feature_output[:,:,:,1] 204 | 205 | # Remove NaN for stat calculations. 206 | feature_output_theta = feature_output_theta[~np.isnan(feature_output_theta)] 207 | feature_output_phi = feature_output_phi[~np.isnan(feature_output_phi)] 208 | 209 | # NumPy supports median natively, we'll use that. 210 | median_theta = np.nanmedian(feature_output_theta, axis=None) 211 | median_phi = np.nanmedian(feature_output_phi, axis=None) 212 | 213 | # Use SciPy for kurtosis, variance, and skewness. 214 | feature_stats_theta = stats.describe(feature_output_theta.flatten(), axis=None) 215 | feature_stats_phi = stats.describe(feature_output_phi.flatten(), axis=None) 216 | 217 | if extendstats: 218 | mean_phi = feature_stats_phi.mean 219 | iqr_phi = stats.iqr(feature_output_phi.flatten()) 220 | 221 | mean_theta = feature_stats_theta.mean 222 | iqr_theta = stats.iqr(feature_output_theta.flatten()) 223 | 224 | _write_csv_extented_stats_row(writer, feature, median_theta, iqr_theta, feature_stats_theta.skewness, feature_stats_theta.kurtosis, mean_theta, feature_stats_theta.variance, 'Theta') 225 | _write_csv_extented_stats_row(writer, feature, median_phi, iqr_phi, feature_stats_phi.skewness, feature_stats_phi.kurtosis, mean_phi, feature_stats_phi.variance, 'Phi') 226 | else: 227 | # Write CSV rows for each angle. 228 | _write_csv_stats_row(writer, feature, median_theta, feature_stats_theta.skewness, feature_stats_theta.kurtosis, feature_stats_theta.variance, 'Theta') 229 | _write_csv_stats_row(writer, feature, median_phi, feature_stats_phi.skewness, feature_stats_phi.kurtosis, feature_stats_phi.variance, 'Phi') 230 | 231 | def _write_csv_stats_row(writer, feature, median, skewness, kurtosis, variance, suffix=''): 232 | writer.writerow([f'Collage{feature.name}Median{suffix}', f'{median:.10f}']) 233 | writer.writerow([f'Collage{feature.name}Skewness{suffix}', f'{skewness:.10f}']) 234 | writer.writerow([f'Collage{feature.name}Kurtosis{suffix}', f'{kurtosis:.10f}']) 235 | writer.writerow([f'Collage{feature.name}Variance{suffix}', f'{variance:.10f}']) 236 | 237 | def _write_csv_extented_stats_row(writer, feature, median, iqr, skewness, kurtosis, mean, variance, suffix=''): 238 | writer.writerow([f'Collage{feature.name}Median{suffix}', f'{median:.10f}']) 239 | writer.writerow([f'Collage{feature.name}IQR{suffix}', f'{iqr:.10f}']) 240 | writer.writerow([f'Collage{feature.name}Skewness{suffix}', f'{skewness:.10f}']) 241 | writer.writerow([f'Collage{feature.name}Kurtosis{suffix}', f'{kurtosis:.10f}']) 242 | writer.writerow([f'Collage{feature.name}Mean{suffix}', f'{mean:.10f}']) 243 | writer.writerow([f'Collage{feature.name}Variance{suffix}', f'{variance:.10f}']) 244 | 245 | if __name__ == '__main__': 246 | run() 247 | -------------------------------------------------------------------------------- /slicer/collageradiomics/CollageRadiomicsSlicer/Resources/UI/CollageRadiomicsSlicer.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CollageRadiomicsSlicer 4 | 5 | 6 | 7 | 0 8 | 0 9 | 458 10 | 909 11 | 12 | 13 | 14 | 15 | 16 | 17 | Paremeter set: 18 | 19 | 20 | 21 | 22 | 23 | 24 | Pick node to store parameter set 25 | 26 | 27 | 28 | vtkMRMLScriptedModuleNode 29 | 30 | 31 | 32 | true 33 | 34 | 35 | CollageRadiomicsSlicer 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | Input/Output 46 | 47 | 48 | 49 | 50 | 51 | Input Volume: 52 | 53 | 54 | 55 | 56 | 57 | 58 | Pick the input to the algorithm. 59 | 60 | 61 | 62 | vtkMRMLScalarVolumeNode 63 | 64 | 65 | 66 | false 67 | 68 | 69 | false 70 | 71 | 72 | false 73 | 74 | 75 | 76 | 77 | 78 | 79 | Input Mask: 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Unlike 2D features, two dominant directions (θ 3D, φ 3D) are computed for 3D CoLlAGe associated with each c ∈ C, which provide complimentary information about the degree of disorder of the principal gradient orientations in (X,Y) and (X,Y,Z) directions. 90 | 91 | 92 | Dominant Direction(s): 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Unlike 2D features, two dominant directions (θ 3D, φ 3D) are computed for 3D CoLlAGe associated with each c ∈ C, which provide complimentary information about the degree of disorder of the principal gradient orientations in (X,Y) and (X,Y,Z) directions. 102 | 103 | 104 | θ 105 | 106 | 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | Unlike 2D features, two dominant directions (θ 3D, φ 3D) are computed for 3D CoLlAGe associated with each c ∈ C, which provide complimentary information about the degree of disorder of the principal gradient orientations in (X,Y) and (X,Y,Z) directions. 115 | 116 | 117 | ϕ 118 | 119 | 120 | false 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | Basic 133 | 134 | 135 | 136 | 137 | 138 | Features: 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | Advanced 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | SVD Radius: 165 | 166 | 167 | 168 | 169 | 170 | 171 | 0 172 | 173 | 174 | 2.000000000000000 175 | 176 | 177 | 20.000000000000000 178 | 179 | 180 | 5.000000000000000 181 | 182 | 183 | 184 | 185 | 186 | 187 | Haralick Window Size (-1 is default): 188 | 189 | 190 | 191 | 192 | 193 | 194 | 0 195 | 196 | 197 | -1.000000000000000 198 | 199 | 200 | 50.000000000000000 201 | 202 | 203 | -1.000000000000000 204 | 205 | 206 | 207 | 208 | 209 | 210 | Difference Variance Interpretation: 211 | 212 | 213 | 214 | 215 | 216 | 217 | false 218 | 219 | 220 | 221 | 222 | 223 | 224 | Grey Levels Bin Size: 225 | 226 | 227 | 228 | 229 | 230 | 231 | 0 232 | 233 | 234 | 32.000000000000000 235 | 236 | 237 | 256.000000000000000 238 | 239 | 240 | 64.000000000000000 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | false 251 | 252 | 253 | Run the algorithm. 254 | 255 | 256 | Run 257 | 258 | 259 | 260 | 261 | 262 | 263 | QFrame::NoFrame 264 | 265 | 266 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 267 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 268 | p, li { white-space: pre-wrap; } 269 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 270 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CoLlAGe captures subtle anisotropic differences in disease pathologies by measuring entropy of co-occurrences of voxel-level gradient orientations on imaging computed within a local neighborhood.</p> 271 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> 272 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">CoLlAGe is based on the hypothesis that disruption in tissue microarchitecture can be quantified on imaging by measuring the disorder in voxel-wise gradient orientations. CoLlAGe involves assigning every image voxel a ‘disorder value’ associated with the co-occurrence matrix of gradient orientations computed around every voxel.</p> 273 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> 274 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">After feature extraction, the subsequent distribution or different statistics such as mean, median, variance etc can be computed and used in conjunction with a machine learning classifier to distinguish similar appearing pathologies. The feasibility of CoLlAGe in distinguishing cancer from treatment confounders/benign conditions and characterizing molecular subtypes of cancers has been demonstrated in the context of multiple challenging clinical problems.</p></body></html> 275 | 276 | 277 | false 278 | 279 | 280 | 281 | 282 | 283 | 284 | QFrame::NoFrame 285 | 286 | 287 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 288 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 289 | p, li { white-space: pre-wrap; } 290 | </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> 291 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Notes:</p> 292 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> 293 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The Haralick Window size default of -1 will result in a window size of SVD_RADIUS * 2 - 1.</p> 294 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> 295 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The Difference Variance Interpretation means feature 10 has two interpretations, as the variance of |x-y| or as the variance of P(|x-y|).]</p> 296 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> 297 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Note that unlike 2D features, two dominant directions (θ 3D, φ 3D) are computed for 3D CoLlAGe associated with each c ∈ C, which provide complimentary information about the degree of disorder of the principal gradient orientations in (X,Y) and (X,Y,Z) directions.</p></body></html> 298 | 299 | 300 | false 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ctkCheckBox 309 | QCheckBox 310 |
ctkCheckBox.h
311 |
312 | 313 | ctkCollapsibleButton 314 | QWidget 315 |
ctkCollapsibleButton.h
316 | 1 317 |
318 | 319 | ctkComboBox 320 | QComboBox 321 |
ctkComboBox.h
322 |
323 | 324 | ctkSliderWidget 325 | QWidget 326 |
ctkSliderWidget.h
327 |
328 | 329 | qMRMLNodeComboBox 330 | QWidget 331 |
qMRMLNodeComboBox.h
332 |
333 | 334 | qMRMLWidget 335 | QWidget 336 |
qMRMLWidget.h
337 | 1 338 |
339 |
340 | 341 | 342 | 343 | CollageRadiomicsSlicer 344 | mrmlSceneChanged(vtkMRMLScene*) 345 | inputSelector 346 | setMRMLScene(vtkMRMLScene*) 347 | 348 | 349 | 122 350 | 132 351 | 352 | 353 | 140 354 | 66 355 | 356 | 357 | 358 | 359 | CollageRadiomicsSlicer 360 | mrmlSceneChanged(vtkMRMLScene*) 361 | parameterNodeSelector 362 | setMRMLScene(vtkMRMLScene*) 363 | 364 | 365 | 28 366 | 267 367 | 368 | 369 | 192 370 | 18 371 | 372 | 373 | 374 | 375 | CollageRadiomicsSlicer 376 | mrmlSceneChanged(vtkMRMLScene*) 377 | inputMaskSelector 378 | setMRMLScene(vtkMRMLScene*) 379 | 380 | 381 | 228 382 | 454 383 | 384 | 385 | 285 386 | 300 387 | 388 | 389 | 390 | 391 |
392 | -------------------------------------------------------------------------------- /module/collageradiomics.py: -------------------------------------------------------------------------------- 1 | import math 2 | import logging 3 | from itertools import product 4 | import mahotas as mt 5 | import numpy as np 6 | from scipy import linalg 7 | from skimage.feature import graycomatrix 8 | from skimage.util.shape import view_as_windows 9 | from enum import Enum, IntEnum 10 | 11 | 12 | logger = logging.getLogger('collageradiomics') 13 | logger.info('Logging set up.') 14 | 15 | 16 | def _svd_dominant_angles(dx, dy, dz, svd_radius): 17 | """Calculate a new numpy image containing the dominant angles for each voxel. 18 | 19 | :param dx: 3D numpy array of the pixel gradient in the x directions 20 | :type dx: numpy.ndarray 21 | 22 | :returns: image array with the dominant angle calculated at each voxel 23 | :rtype: numpy.ndarray 24 | """ 25 | 26 | is_3D = dx.shape[2] > 1 27 | 28 | # create rolling windows 29 | svd_diameter = svd_radius * 2 + 1 30 | 31 | # the first (commented out) version would actually take a patch of one slice above and below, 32 | # but by convention, the third dimension is simply used for the gradient calculation; 33 | # the actual collection of nearby gradient values to run through the SVD calculation is 34 | # still done on a given 2D slice 35 | #window_shape = (svd_diameter, svd_diameter) + ((3 if is_3D else 1),) 36 | window_shape = (svd_diameter, svd_diameter, 1) 37 | logger.info(f'Window patch shape for dominant angle calculation = {window_shape}') 38 | dx_windows = view_as_windows(dx, window_shape) 39 | dy_windows = view_as_windows(dy, window_shape) 40 | dz_windows = view_as_windows(dz, window_shape) 41 | 42 | angles_shape = dx_windows.shape[0:3] 43 | dominant_angles_array = np.zeros(angles_shape + (2 if is_3D else 1,), np.single) 44 | 45 | # loop through each voxel and use SVD to calculate the dominant angle for that rolling window 46 | # centered on that x,y,z coordinate 47 | center_x_range = range(angles_shape[1]) 48 | center_y_range = range(angles_shape[0]) 49 | center_z_range = range(angles_shape[2]) 50 | for x, y, z in product(center_x_range, center_y_range, center_z_range): 51 | dominant_angles_array[y, x, z, :] = _svd_dominant_angle(x, y, z, dx_windows, dy_windows, dz_windows) 52 | 53 | return dominant_angles_array 54 | 55 | 56 | def _svd_dominant_angle(x, y, z, dx_windows, dy_windows, dz_windows): 57 | """Calculates the dominate angle at the coordinate within the windows. 58 | 59 | :param x: x value of coordinate 60 | :type x: int 61 | :param y: y value of coordinate 62 | :type y: int 63 | :param z: z value of coordinate 64 | :type z: int 65 | :param dx_windows: dx windows of x, y shape to run svd upon (shape = rows, cols, slices, row_radius, col_radius, slice_radius) 66 | :type dx_windows: numpy.ndarray 67 | :param dy_windows: dy windows of x, y shape to run svd upon 68 | :type dy_windows: numpy.ndarray 69 | :param dz_windows: dy windows of x, y shape to run svd upon 70 | :type dz_windows: numpy.ndarray 71 | 72 | :returns: dominant angle at x, y 73 | :rtype: float 74 | """ 75 | 76 | # extract the patch of pixel gradient values for this specific voxel 77 | dx_patch = dx_windows[y, x, z] 78 | dy_patch = dy_windows[y, x, z] 79 | dz_patch = dz_windows[y, x, z] 80 | 81 | is_3D = dx_windows.shape[2] > 1 82 | 83 | # flatten all N gradient values in this patch into an Nxd matrix to pass into svd 84 | window_area = dx_patch.size 85 | flattened_gradients = np.zeros((window_area, (3 if is_3D else 2))) 86 | matrix_order = 'F' # fortran-style to be consistent with original matlab implementation 87 | flattened_gradients[:, 0] = np.reshape(dx_patch, window_area, order=matrix_order) 88 | flattened_gradients[:, 1] = np.reshape(dy_patch, window_area, order=matrix_order) 89 | if is_3D: 90 | flattened_gradients[:, 2] = np.reshape(dz_patch, window_area, order=matrix_order) 91 | 92 | # calculate svd 93 | _, _, v = linalg.svd(flattened_gradients) 94 | 95 | # extract results from the first column (in matlab this would be the first row) 96 | dominant_y = v[0,0] 97 | dominant_x = v[1,0] 98 | 99 | # calculate the dominant angle for this voxel 100 | dominant_angle = math.atan2(dominant_y, dominant_x) 101 | 102 | if is_3D: 103 | # also include the secondary angle 104 | dominant_z = v[2,0] 105 | secondary_angle = math.atan2(dominant_z, math.sqrt(dominant_x ** 2 + dominant_y ** 2)) 106 | return (dominant_angle, secondary_angle) 107 | else: 108 | return dominant_angle 109 | 110 | class HaralickFeature(IntEnum): 111 | """Enumeration Helper For Haralick Features 112 | 113 | :param IntEnum: Enumeration Helper For Haralick Features 114 | :type IntEnum: HaralickFeature 115 | """ 116 | AngularSecondMoment = 0 117 | Contrast = 1 118 | Correlation = 2 119 | SumOfSquareVariance = 3 120 | SumAverage = 4 121 | SumVariance = 5 122 | SumEntropy = 6 123 | Entropy = 7 124 | DifferenceVariance = 8 125 | DifferenceEntropy = 9 126 | InformationMeasureOfCorrelation1 = 10 127 | InformationMeasureOfCorrelation2 = 11 128 | MaximalCorrelationCoefficient = 12 129 | 130 | 131 | class DifferenceVarianceInterpretation(Enum): 132 | """ Feature 10 has two interpretations, as the variance of |x-y| 133 | or as the variance of P(|x-y|). 134 | See: https://ieeexplore.ieee.org/document/4309314 135 | 136 | :param Enum: Enumeration Helper For Haralick Features 137 | :type Enum: DifferenceVarianceInterpretation 138 | """ 139 | XMinusYVariance = 0 140 | ProbabilityXMinusYVariance = 1 141 | 142 | 143 | class Collage: 144 | """This is the main object in the Collage calculation system. Usage: create a Collage object and then call the :py:meth:`execute` function. 145 | 146 | :param image_array: image to run collage upon 147 | :type image_array: numpy.ndarray 148 | :param mask_array: mask that correlates with the image 149 | :type mask_array: numpy.ndarray 150 | :param svd_radius: radius of svd. Defaults to 5. 151 | :type svd_radius: int, optional 152 | :param verbose_logging: This parameter is now ignored. Please use the python logging module. 153 | :type verbose_logging: bool, optional 154 | :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x*numpy.pi/4 for x in range(8)] 155 | :type cooccurence_angles: list, optional 156 | :param difference_variance_interpretation: Feature 10 has two interpretations, as the variance of |x-y| or as the variance of P(|x-y|).].Defaults to DifferenceVarianceInterpretation.XMinusYVariance. 157 | :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional 158 | :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. 159 | :type haralick_window_size: int, optional 160 | :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. 161 | :type num_unique_angles: int, optional 162 | """ 163 | 164 | 165 | @property 166 | def img_array(self): 167 | """ 168 | The original image. 169 | 170 | :getter: Returns the original image array. 171 | :setter: Sets the original image array. 172 | :type: np.ndarray 173 | """ 174 | return self._img_array 175 | 176 | @property 177 | def mask_array(self): 178 | """ 179 | Array passed into Collage. 180 | 181 | :getter: Returns the original mask array. 182 | :setter: Sets the original mask array. 183 | :type: np.ndarray 184 | """ 185 | return self._mask_array 186 | 187 | @property 188 | def is_3D(self): 189 | """ 190 | Whether we are using 3D collage calculations (True) or 2D (False) 191 | """ 192 | return self._is_3D 193 | 194 | @property 195 | def svd_radius(self): 196 | """ 197 | SVD radius is used to calculate the pixel radius 198 | for the dominant angle calculation. 199 | 200 | :getter: Returns the SVD radius. 201 | :setter: Sets the SVD radius. 202 | :type: int 203 | """ 204 | return self._svd_radius 205 | 206 | @property 207 | def verbose_logging(self): 208 | """ 209 | This parameter is now ignored. Please use the python logging module. 210 | 211 | :getter: Returns True if on. 212 | :setter: Turns verbose logging off or on. 213 | :type: bool 214 | """ 215 | return self._verbose_logging 216 | 217 | @property 218 | def cooccurence_angles(self): 219 | """ 220 | Iterable of angles that will be used in the cooccurence matrix. 221 | 222 | :getter: Returns the Iterable of cooccurence angles. 223 | :setter: Sets the angles to be used in the cooccurence matrix. 224 | :type: int 225 | """ 226 | return self._cooccurence_angles 227 | 228 | @property 229 | def difference_variance_interpretation(self): 230 | """ 231 | Feature 10 has two interpretations, as the variance of |x-y| or as the variance of P(|x-y|).]. 232 | Defaults to DifferenceVarianceInterpretation.XMinusYVariance. 233 | 234 | :getter: Returns requested variance interpretation. 235 | :setter: Sets requested variance interpretation. 236 | :type: DifferenceVarianceInterpretation 237 | """ 238 | return self._difference_variance_interpretation 239 | 240 | @property 241 | def haralick_window_size(self): 242 | """ 243 | Number of pixels around each pixel to calculate a haralick texture. 244 | 245 | :getter: Returns requested number of pixels. 246 | :setter: Sets requested number of pixels. 247 | :type: int 248 | """ 249 | return self._haralick_window_size 250 | 251 | @property 252 | def num_unique_angles(self): 253 | """ 254 | Number of bins to use for texture calculations. Defaults to 64. 255 | 256 | :getter: Returns requested number of unique angles to bin into. 257 | :type: int 258 | """ 259 | return self._num_unique_angles 260 | 261 | @property 262 | def collage_output(self): 263 | """ 264 | Array representing collage upon the mask within the full images. 265 | If the input was 2D, the output will be height×width×13 where "13" is the number of haralick textures. 266 | If the input was 3D, the output will be height×width×depth×13x2 where "2" is the primary angle (element 0) or the secondary angle (element 1) 267 | 268 | The output will have numpy.nan values everywhere outside the masked region. 269 | 270 | :getter: Returns array the same shape as the original image with collage in the mask region. 271 | :type: numpy.ndarray 272 | """ 273 | return self._collage_output 274 | 275 | @collage_output.setter 276 | def collage_output(self, value): 277 | self._collage_output = value 278 | 279 | 280 | def get_single_feature_output(self, which_feature): 281 | """ 282 | Output a single collage output feature. 283 | If this was a 3D calculation, the output will be of size height×width×depth×2 284 | where the "2" represents the collage calculation from the primary angle (0) or secondary angle (1). 285 | 286 | :param which_feature: Either an integer from 0 to 12 (inclusive) or a HaralickFeature enum value 287 | :type which_feature HaralickFeature 288 | """ 289 | if self.is_3D: 290 | return self.collage_output[:,:,:,which_feature,:] 291 | else: 292 | return self.collage_output[:,:,which_feature] 293 | 294 | 295 | def __init__(self, 296 | img_array, 297 | mask_array, 298 | svd_radius=5, 299 | verbose_logging=False, 300 | cooccurence_angles=[x * np.pi/4 for x in range(8)], 301 | difference_variance_interpretation=DifferenceVarianceInterpretation.XMinusYVariance, 302 | haralick_window_size=-1, 303 | num_unique_angles=64, 304 | ): 305 | """Designated initializer for Collage 306 | 307 | :param image_array: image to run collage upon 308 | :type image_array: numpy.ndarray 309 | :param mask_array: mask that correlates with the image 310 | :type mask_array: numpy.ndarray 311 | :param svd_radius: radius of svd. Defaults to 5. 312 | :type svd_radius: int, optional 313 | :param verbose_logging: This parameter is now ignored. Please use the python logging module. 314 | :type verbose_logging: bool, optional 315 | :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x * np.pi/4 for x in range(8)] 316 | :type cooccurence_angles: list, optional 317 | :param difference_variance_interpretation: Feature 10 has two interpretations, as the variance of |x-y| or as the variance of P(|x-y|).].Defaults to DifferenceVarianceInterpretation.XMinusYVariance. 318 | :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional 319 | :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. 320 | :type haralick_window_size: int, optional 321 | :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. 322 | :type num_unique_angles: int, optional 323 | """ 324 | 325 | logger.debug('Collage Module Initialized') 326 | 327 | # error checking 328 | if haralick_window_size == -1: 329 | self._haralick_window_size = svd_radius * 2 + 1 330 | else: 331 | self._haralick_window_size = haralick_window_size 332 | 333 | if self._haralick_window_size < 1: 334 | raise Exception('Haralick windows size must be at least 1 pixel.') 335 | 336 | if svd_radius < 1: 337 | raise Exception('SVD radius must be at least 1 pixel') 338 | 339 | if num_unique_angles < 1: 340 | raise Exception('num_unique_angles must contain at least 1 bin') 341 | 342 | if img_array.ndim < 2 or img_array.ndim > 3: 343 | raise Exception('Expected a 2D or 3D image.') 344 | 345 | if mask_array.shape != img_array.shape: 346 | raise Exception('Mask must be the same shape as image.') 347 | 348 | # Our minimum size for x & y is 50x50 349 | min_x_y_size = 50 350 | 351 | if img_array.shape[0] < min_x_y_size or img_array.shape[1] < min_x_y_size: 352 | raise Exception(f'Image size ({img_array.shape[0]}x{img_array.shape[1]}) unsupported. Image must be a minimum of a 50x50 for collage to run.') 353 | 354 | self._is_3D = img_array.ndim == 3 355 | logger.debug(f'Running 3D Collage = {self.is_3D}') 356 | 357 | self._img_array = img_array 358 | if not self.is_3D: 359 | # in the case of a single 2D slice, give it a third dimension of unit length 360 | self._img_array = self._img_array.reshape(self._img_array.shape + (1,)) 361 | 362 | min_3D_slices = 3 363 | if self._img_array.shape[0] < self._haralick_window_size or self._img_array.shape[1] < self._haralick_window_size or (self._is_3D and self._img_array.shape[2] < min_3D_slices): 364 | raise Exception( 365 | f'Image is too small for a window size of {self._haralick_window_size} pixels.') 366 | 367 | # threshold mask 368 | uniqueValues = np.unique(mask_array) 369 | numberOfValues = len(uniqueValues) 370 | if numberOfValues > 2: 371 | logger.info(f'Warning: Mask is not binary. Considering all {numberOfValues} nonzero values in the mask as a value of True.') 372 | thresholded_mask_array = (mask_array != 0) 373 | 374 | # make correct shape 375 | thresholded_mask_array = thresholded_mask_array.reshape(self.img_array.shape) 376 | 377 | # extract rectangular area of mask 378 | non_zero_indices = np.argwhere(thresholded_mask_array) 379 | min_mask_coordinates = non_zero_indices.min(0) 380 | max_mask_coordinates = non_zero_indices.max(0)+1 381 | self.mask_min_x = min_mask_coordinates[1] 382 | self.mask_min_y = min_mask_coordinates[0] 383 | self.mask_min_z = min_mask_coordinates[2] 384 | self.mask_max_x = max_mask_coordinates[1] 385 | self.mask_max_y = max_mask_coordinates[0] 386 | self.mask_max_z = max_mask_coordinates[2] 387 | 388 | cropped_mask_array = thresholded_mask_array[self.mask_min_y:self.mask_max_y, 389 | self.mask_min_x:self.mask_max_x, 390 | self.mask_min_z:self.mask_max_z] 391 | 392 | # store variables internally 393 | self._mask_array = cropped_mask_array 394 | 395 | self._svd_radius = svd_radius 396 | self._verbose_logging = verbose_logging 397 | 398 | self._cooccurence_angles = cooccurence_angles 399 | self._difference_variance_interpretation = difference_variance_interpretation 400 | 401 | self._num_unique_angles = num_unique_angles 402 | 403 | 404 | def _calculate_haralick_feature_values(self, img_array, center_x, center_y): 405 | 406 | """Gets the haralick texture feature values at the x, y, z coordinate. 407 | , pos[1] 408 | :param image_array: image to calculate texture 409 | :type image_array: numpy.ndarray 410 | :param center_x: x center of coordinate 411 | :type center_x: int 412 | :param center_y: y center of coordinate 413 | :type center_y: int 414 | :param window_size: size of window to pull for calculation 415 | :type window_size: int 416 | :param num_unique_angles: number of bins 417 | :type num_unique_angles: int 418 | :param haralick_feature: desired haralick feature 419 | :type haralick_feature: HaralickFeature 420 | 421 | :returns: A 13x1 vector of haralick texture at the coordinate. 422 | :rtype: numpy.ndarray 423 | """ 424 | # extract subpart of image (todo: pass in result from view_as_windows) 425 | window_size = self.haralick_window_size 426 | min_x = int(max(0, center_x - window_size / 2 - 1)) 427 | min_y = int(max(0, center_y - window_size / 2 - 1)) 428 | max_x = int(min(img_array.shape[1] - 1, center_x + window_size / 2 + 1)) 429 | max_y = int(min(img_array.shape[0] - 1, center_y + window_size / 2 + 1)) 430 | cropped_img_array = img_array[min_y:max_y, min_x:max_x] 431 | 432 | # co-occurence matrix of all 8 directions and sum them 433 | cooccurence_matrix = graycomatrix(cropped_img_array, [1], self.cooccurence_angles, levels=self.num_unique_angles) 434 | cooccurence_matrix = np.sum(cooccurence_matrix, axis=3) 435 | cooccurence_matrix = cooccurence_matrix[:, :, 0] 436 | 437 | # extract haralick using mahotas library 438 | return mt.features.texture.haralick_features([cooccurence_matrix], return_mean=True) 439 | 440 | 441 | def _calculate_haralick_textures(self, dominant_angles): 442 | """Gets haralick texture values 443 | 444 | :param dominant_angles_array: An image of the dominant angles at each voxel 445 | :type dominant_angles_[:,:,:,feature_index]array: numpy.ndarray 446 | :param desired_haralick_feature: which feature to calculate 447 | :type desired_haralick_feature: Haralick Feature 448 | :param num_unique_angles: number of bins 449 | :type num_unique_angles: int 450 | :param haralick_window_size: size of window around pixels to calculate haralick value 451 | 452 | :returns: An hxwxdx13 set of haralick texture. 453 | :rtype: numpy.ndarray 454 | """ 455 | 456 | # rescale from 0 to (num_unique_angles-1) 457 | num_unique_angles = self.num_unique_angles 458 | logger.debug(f'Rescaling dominant angles to {num_unique_angles} unique values.') 459 | dominant_angles_max = dominant_angles.max() 460 | dominant_angles_min = dominant_angles.min() 461 | dominant_angles_binned = (dominant_angles - dominant_angles_min) / (dominant_angles_max - dominant_angles_min + np.finfo(float).eps) * (num_unique_angles - 1) 462 | dominant_angles_binned = np.round(dominant_angles_binned).astype(int) 463 | logger.debug(f'Rescaling dominant angles done.') 464 | 465 | # prepare output 466 | shape = dominant_angles_binned.shape 467 | haralick_image = np.empty(shape + (13,)) 468 | haralick_image[:] = np.nan 469 | 470 | # the haralick is calculated for each slice separately 471 | height, width, depth = shape 472 | 473 | logger.debug(f'dominant_angles_binned shape is {shape} mask shape is {self.mask_array.shape}') 474 | 475 | # In 3D, we extended the dominant angles by one slice in each direction, so now we need to trim those off. 476 | for z in range(1, depth - 1) if self.is_3D else range(depth): 477 | for y,x in product(range(height), range(width)): 478 | if self.mask_array[y,x,z]: 479 | haralick_image[y,x,z,:] = self._calculate_haralick_feature_values(dominant_angles_binned[:,:,z], x, y) 480 | 481 | return haralick_image 482 | 483 | 484 | def execute(self): 485 | """Begins haralick calculation. 486 | 487 | :returns: An image at original size that only has the masked section filled in with collage calculations. 488 | :rtype: numpy.ndarray 489 | """ 490 | 491 | svd_radius = self.svd_radius 492 | 493 | # mask location 494 | mask_min_x = int(self.mask_min_x) 495 | mask_min_y = int(self.mask_min_y) 496 | mask_min_z = int(self.mask_min_z) 497 | mask_max_x = int(self.mask_max_x) 498 | mask_max_y = int(self.mask_max_y) 499 | mask_max_z = int(self.mask_max_z) 500 | 501 | mask_width = mask_max_x - mask_min_x 502 | mask_height = mask_max_y - mask_min_y 503 | mask_depth = mask_max_z - mask_min_z 504 | 505 | img_array = self.img_array 506 | 507 | # extend the mask outwards a bit (up to the edge of the image) to handle the svd radius 508 | cropped_min_x = max(mask_min_x - svd_radius, 0) 509 | cropped_min_y = max(mask_min_y - svd_radius, 0) 510 | cropped_min_z = max(mask_min_z - 1 , 0) # for 3D, we just extend 1 slice in both directions 511 | cropped_max_x = min(mask_max_x + svd_radius, img_array.shape[1]) 512 | cropped_max_y = min(mask_max_y + svd_radius, img_array.shape[0]) 513 | cropped_max_z = min(mask_max_z + 1 , img_array.shape[2]) 514 | 515 | extended_below = mask_min_z > 0 516 | extended_above = mask_max_z < img_array.shape[2] 517 | 518 | cropped_image = img_array[cropped_min_y:cropped_max_y, 519 | cropped_min_x:cropped_max_x, 520 | cropped_min_z:cropped_max_z] 521 | 522 | logger.debug(f'Image shape = {img_array.shape}') 523 | logger.debug(f'Mask size = {mask_height}x{mask_width}x{mask_depth}') 524 | logger.debug(f'Image shape (cropped and padded) = {cropped_image.shape}') 525 | 526 | # ensure the image values range from 0-1 527 | if cropped_image.max() > 1: 528 | logger.debug(f'Note: Dividing image values by {cropped_image.max()} to convert to 0-1 range') 529 | cropped_image = cropped_image / cropped_image.max() 530 | 531 | # calculate x,y,z gradients 532 | logger.debug('Calculating pixel gradients:') 533 | dx = np.gradient(cropped_image, axis=1) 534 | dy = np.gradient(cropped_image, axis=0) 535 | dz = np.gradient(cropped_image, axis=2) if self.is_3D else np.zeros(dx.shape) 536 | 537 | if extended_below: 538 | dx = dx[:,:,1:] 539 | dy = dy[:,:,1:] 540 | dz = dz[:,:,1:] 541 | 542 | if extended_above: 543 | dx = dx[:,:,:-1] 544 | dy = dy[:,:,:-1] 545 | dz = dz[:,:,:-1] 546 | 547 | self.dx = dx 548 | self.dy = dy 549 | self.dz = dz 550 | logger.debug('Calculating pixel gradients done.') 551 | 552 | # calculate dominant angles of each patch 553 | logger.debug(f'Calculating dominant gradient angles using SVD for each image patch of size {svd_radius}x{svd_radius}') 554 | dominant_angles = _svd_dominant_angles(dx, dy, dz, svd_radius) 555 | self.dominant_angles = dominant_angles 556 | angles_shape = dominant_angles.shape 557 | logger.debug('Calculating dominant gradient angles done.') 558 | logger.debug(f'Dominant angles shape = {angles_shape}') 559 | 560 | # calculate haralick features of the dominant angles 561 | logger.debug('Calculating haralick features of angles:') 562 | haralick_features = np.empty(angles_shape[0:3] + (13, 2 if self.is_3D else 1,)) 563 | for angle_index in range(angles_shape[3]): 564 | logger.info(f'Calculating features for angle {angle_index}:') 565 | haralick_features[:,:,:,:,angle_index] = self._calculate_haralick_textures(dominant_angles[:,:,:,angle_index]) 566 | logger.info(f'Calculating features for angle {angle_index} done.') 567 | logger.debug('Calculating haralick features of angles done.') 568 | 569 | # prepare an output full of "NaN's" 570 | collage_output = np.empty(img_array.shape + haralick_features.shape[3:5]) 571 | collage_output[:] = np.nan 572 | 573 | # if a mask covers the whole image, we'll offset the edges as nans 574 | if mask_height == img_array.shape[0] and mask_width == img_array.shape[1]: 575 | y_offset = int((img_array.shape[0] - dominant_angles.shape[0]) / 2) 576 | mask_min_y += y_offset 577 | mask_max_y -= y_offset 578 | x_offset = int((img_array.shape[1] - dominant_angles.shape[1]) / 2) 579 | mask_min_x += x_offset 580 | mask_max_x -= x_offset 581 | 582 | # insert the haralick output into the correct spot 583 | collage_output[mask_min_y:mask_max_y, 584 | mask_min_x:mask_max_x, 585 | mask_min_z:mask_max_z, 586 | :, :] = haralick_features 587 | 588 | # remove the singleton third dimension from the output 589 | if not self.is_3D: 590 | collage_output = np.squeeze(collage_output, 4) 591 | collage_output = np.squeeze(collage_output, 2) 592 | 593 | # output 594 | self.collage_output = collage_output 595 | logger.debug(f'Output shape = {collage_output.shape}') 596 | return collage_output 597 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "c9cc98dcbe8bef997326424a95f41a79c60724f7ac6da494e92a3322d0dc375c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "anyio": { 20 | "hashes": [ 21 | "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", 22 | "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" 23 | ], 24 | "markers": "python_full_version >= '3.6.2'", 25 | "version": "==3.6.2" 26 | }, 27 | "argon2-cffi": { 28 | "hashes": [ 29 | "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", 30 | "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==21.3.0" 34 | }, 35 | "argon2-cffi-bindings": { 36 | "hashes": [ 37 | "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", 38 | "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f", 39 | "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", 40 | "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194", 41 | "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", 42 | "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", 43 | "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", 44 | "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5", 45 | "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", 46 | "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7", 47 | "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", 48 | "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", 49 | "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", 50 | "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", 51 | "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", 52 | "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", 53 | "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", 54 | "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", 55 | "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", 56 | "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", 57 | "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351" 58 | ], 59 | "markers": "python_version >= '3.6'", 60 | "version": "==21.2.0" 61 | }, 62 | "asttokens": { 63 | "hashes": [ 64 | "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3", 65 | "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c" 66 | ], 67 | "version": "==2.2.1" 68 | }, 69 | "attrs": { 70 | "hashes": [ 71 | "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", 72 | "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" 73 | ], 74 | "markers": "python_version >= '3.6'", 75 | "version": "==22.2.0" 76 | }, 77 | "backcall": { 78 | "hashes": [ 79 | "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", 80 | "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" 81 | ], 82 | "version": "==0.2.0" 83 | }, 84 | "beautifulsoup4": { 85 | "hashes": [ 86 | "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39", 87 | "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106" 88 | ], 89 | "markers": "python_version >= '3.6'", 90 | "version": "==4.11.2" 91 | }, 92 | "bleach": { 93 | "hashes": [ 94 | "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", 95 | "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" 96 | ], 97 | "markers": "python_version >= '3.7'", 98 | "version": "==6.0.0" 99 | }, 100 | "cffi": { 101 | "hashes": [ 102 | "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", 103 | "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", 104 | "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", 105 | "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", 106 | "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", 107 | "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", 108 | "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", 109 | "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", 110 | "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", 111 | "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", 112 | "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", 113 | "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", 114 | "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", 115 | "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", 116 | "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", 117 | "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", 118 | "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", 119 | "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", 120 | "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", 121 | "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", 122 | "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", 123 | "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", 124 | "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", 125 | "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", 126 | "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", 127 | "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", 128 | "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", 129 | "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", 130 | "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", 131 | "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", 132 | "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", 133 | "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", 134 | "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", 135 | "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", 136 | "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", 137 | "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", 138 | "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", 139 | "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", 140 | "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", 141 | "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", 142 | "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", 143 | "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", 144 | "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", 145 | "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", 146 | "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", 147 | "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", 148 | "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", 149 | "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", 150 | "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", 151 | "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", 152 | "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", 153 | "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", 154 | "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", 155 | "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", 156 | "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", 157 | "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", 158 | "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", 159 | "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", 160 | "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", 161 | "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", 162 | "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", 163 | "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", 164 | "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", 165 | "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" 166 | ], 167 | "version": "==1.15.1" 168 | }, 169 | "comm": { 170 | "hashes": [ 171 | "sha256:3e2f5826578e683999b93716285b3b1f344f157bf75fa9ce0a797564e742f062", 172 | "sha256:9f3abf3515112fa7c55a42a6a5ab358735c9dccc8b5910a9d8e3ef5998130666" 173 | ], 174 | "markers": "python_version >= '3.6'", 175 | "version": "==0.1.2" 176 | }, 177 | "contourpy": { 178 | "hashes": [ 179 | "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98", 180 | "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772", 181 | "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2", 182 | "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc", 183 | "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803", 184 | "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051", 185 | "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc", 186 | "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4", 187 | "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436", 188 | "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5", 189 | "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5", 190 | "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3", 191 | "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80", 192 | "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1", 193 | "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0", 194 | "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae", 195 | "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556", 196 | "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02", 197 | "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566", 198 | "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350", 199 | "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967", 200 | "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4", 201 | "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66", 202 | "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69", 203 | "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd", 204 | "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2", 205 | "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810", 206 | "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50", 207 | "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc", 208 | "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2", 209 | "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0", 210 | "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3", 211 | "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6", 212 | "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac", 213 | "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d", 214 | "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6", 215 | "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f", 216 | "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd", 217 | "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566", 218 | "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa", 219 | "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414", 220 | "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a", 221 | "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c", 222 | "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693", 223 | "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d", 224 | "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161", 225 | "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e", 226 | "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2", 227 | "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f", 228 | "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71", 229 | "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd", 230 | "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9", 231 | "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8", 232 | "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab", 233 | "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad" 234 | ], 235 | "markers": "python_version >= '3.8'", 236 | "version": "==1.0.7" 237 | }, 238 | "cycler": { 239 | "hashes": [ 240 | "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", 241 | "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" 242 | ], 243 | "markers": "python_version >= '3.6'", 244 | "version": "==0.11.0" 245 | }, 246 | "debugpy": { 247 | "hashes": [ 248 | "sha256:0ea1011e94416e90fb3598cc3ef5e08b0a4dd6ce6b9b33ccd436c1dffc8cd664", 249 | "sha256:23363e6d2a04d726bbc1400bd4e9898d54419b36b2cdf7020e3e215e1dcd0f8e", 250 | "sha256:23c29e40e39ad7d869d408ded414f6d46d82f8a93b5857ac3ac1e915893139ca", 251 | "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe", 252 | "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32", 253 | "sha256:72687b62a54d9d9e3fb85e7a37ea67f0e803aaa31be700e61d2f3742a5683917", 254 | "sha256:78739f77c58048ec006e2b3eb2e0cd5a06d5f48c915e2fc7911a337354508110", 255 | "sha256:7aa7e103610e5867d19a7d069e02e72eb2b3045b124d051cfd1538f1d8832d1b", 256 | "sha256:87755e173fcf2ec45f584bb9d61aa7686bb665d861b81faa366d59808bbd3494", 257 | "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c", 258 | "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6", 259 | "sha256:b9c2130e1c632540fbf9c2c88341493797ddf58016e7cba02e311de9b0a96b67", 260 | "sha256:be596b44448aac14eb3614248c91586e2bc1728e020e82ef3197189aae556115", 261 | "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225", 262 | "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1", 263 | "sha256:dff595686178b0e75580c24d316aa45a8f4d56e2418063865c114eef651a982e", 264 | "sha256:f6383c29e796203a0bba74a250615ad262c4279d398e89d895a69d3069498305" 265 | ], 266 | "markers": "python_version >= '3.7'", 267 | "version": "==1.6.6" 268 | }, 269 | "decorator": { 270 | "hashes": [ 271 | "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", 272 | "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" 273 | ], 274 | "markers": "python_version >= '3.5'", 275 | "version": "==5.1.1" 276 | }, 277 | "defusedxml": { 278 | "hashes": [ 279 | "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", 280 | "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" 281 | ], 282 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 283 | "version": "==0.7.1" 284 | }, 285 | "distro": { 286 | "hashes": [ 287 | "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8", 288 | "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff" 289 | ], 290 | "markers": "python_version >= '3.6'", 291 | "version": "==1.8.0" 292 | }, 293 | "entrypoints": { 294 | "hashes": [ 295 | "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", 296 | "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f" 297 | ], 298 | "markers": "python_version >= '3.6'", 299 | "version": "==0.4" 300 | }, 301 | "executing": { 302 | "hashes": [ 303 | "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc", 304 | "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107" 305 | ], 306 | "version": "==1.2.0" 307 | }, 308 | "fastjsonschema": { 309 | "hashes": [ 310 | "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18", 311 | "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c" 312 | ], 313 | "version": "==2.16.2" 314 | }, 315 | "fonttools": { 316 | "hashes": [ 317 | "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1", 318 | "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb" 319 | ], 320 | "markers": "python_version >= '3.7'", 321 | "version": "==4.38.0" 322 | }, 323 | "idna": { 324 | "hashes": [ 325 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 326 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 327 | ], 328 | "markers": "python_version >= '3.5'", 329 | "version": "==3.4" 330 | }, 331 | "imageio": { 332 | "hashes": [ 333 | "sha256:5bce7f88eef7ee4e9aac798d3b218fea2e98cbbaa59a3e37b730a7aa5784eeac", 334 | "sha256:6021d42debd2187e9c781e494a49a30eba002fbac1eef43f491bbc731e7a6d2b" 335 | ], 336 | "markers": "python_version >= '3.7'", 337 | "version": "==2.25.1" 338 | }, 339 | "importlib-metadata": { 340 | "hashes": [ 341 | "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", 342 | "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" 343 | ], 344 | "markers": "python_version < '3.10'", 345 | "version": "==6.0.0" 346 | }, 347 | "importlib-resources": { 348 | "hashes": [ 349 | "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6", 350 | "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484" 351 | ], 352 | "markers": "python_version < '3.9'", 353 | "version": "==5.10.2" 354 | }, 355 | "ipykernel": { 356 | "hashes": [ 357 | "sha256:1893c5b847033cd7a58f6843b04a9349ffb1031bc6588401cadc9adb58da428e", 358 | "sha256:5d0675d5f48bf6a95fd517d7b70bcb3b2c5631b2069949b5c2d6e1d7477fb5a0" 359 | ], 360 | "markers": "python_version >= '3.8'", 361 | "version": "==6.20.2" 362 | }, 363 | "ipython": { 364 | "hashes": [ 365 | "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e", 366 | "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345" 367 | ], 368 | "markers": "python_version >= '3.8'", 369 | "version": "==8.10.0" 370 | }, 371 | "ipython-genutils": { 372 | "hashes": [ 373 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 374 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 375 | ], 376 | "version": "==0.2.0" 377 | }, 378 | "ipywidgets": { 379 | "hashes": [ 380 | "sha256:c0005a77a47d77889cafed892b58e33b4a2a96712154404c6548ec22272811ea", 381 | "sha256:ebb195e743b16c3947fe8827190fb87b4d00979c0fbf685afe4d2c4927059fa1" 382 | ], 383 | "markers": "python_version >= '3.7'", 384 | "version": "==8.0.4" 385 | }, 386 | "jedi": { 387 | "hashes": [ 388 | "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e", 389 | "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612" 390 | ], 391 | "markers": "python_version >= '3.6'", 392 | "version": "==0.18.2" 393 | }, 394 | "jinja2": { 395 | "hashes": [ 396 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 397 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 398 | ], 399 | "markers": "python_version >= '3.7'", 400 | "version": "==3.1.2" 401 | }, 402 | "jsonschema": { 403 | "hashes": [ 404 | "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", 405 | "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" 406 | ], 407 | "markers": "python_version >= '3.7'", 408 | "version": "==4.17.3" 409 | }, 410 | "jupyter": { 411 | "hashes": [ 412 | "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7", 413 | "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78", 414 | "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f" 415 | ], 416 | "index": "pypi", 417 | "version": "==1.0.0" 418 | }, 419 | "jupyter-client": { 420 | "hashes": [ 421 | "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7", 422 | "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392" 423 | ], 424 | "markers": "python_version >= '3.7'", 425 | "version": "==7.4.9" 426 | }, 427 | "jupyter-console": { 428 | "hashes": [ 429 | "sha256:172f5335e31d600df61613a97b7f0352f2c8250bbd1092ef2d658f77249f89fb", 430 | "sha256:756df7f4f60c986e7bc0172e4493d3830a7e6e75c08750bbe59c0a5403ad6dee" 431 | ], 432 | "markers": "python_version >= '3.7'", 433 | "version": "==6.4.4" 434 | }, 435 | "jupyter-core": { 436 | "hashes": [ 437 | "sha256:3815e80ec5272c0c19aad087a0d2775df2852cfca8f5a17069e99c9350cecff8", 438 | "sha256:c2909b9bc7dca75560a6c5ae78c34fd305ede31cd864da3c0d0bb2ed89aa9337" 439 | ], 440 | "index": "pypi", 441 | "version": "==4.11.2" 442 | }, 443 | "jupyter-server": { 444 | "hashes": [ 445 | "sha256:4ee4f311bd944bcf8060a8b746059571c40f6b8ada1d1e6e51239d26ab23b15c", 446 | "sha256:aa3398aeb5249d470ea53abcf81fca8a6876bb9dbdc652822e5bbbb0574a6e83" 447 | ], 448 | "markers": "python_version >= '3.7'", 449 | "version": "==1.23.4" 450 | }, 451 | "jupyterlab-pygments": { 452 | "hashes": [ 453 | "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f", 454 | "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d" 455 | ], 456 | "markers": "python_version >= '3.7'", 457 | "version": "==0.2.2" 458 | }, 459 | "jupyterlab-widgets": { 460 | "hashes": [ 461 | "sha256:a04a42e50231b355b7087e16a818f541e53589f7647144ea0344c4bf16f300e5", 462 | "sha256:eeaecdeaf6c03afc960ddae201ced88d5979b4ca9c3891bcb8f6631af705f5ef" 463 | ], 464 | "markers": "python_version >= '3.7'", 465 | "version": "==3.0.5" 466 | }, 467 | "kiwisolver": { 468 | "hashes": [ 469 | "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", 470 | "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", 471 | "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", 472 | "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", 473 | "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", 474 | "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", 475 | "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", 476 | "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", 477 | "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", 478 | "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", 479 | "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", 480 | "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", 481 | "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", 482 | "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", 483 | "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", 484 | "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", 485 | "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", 486 | "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", 487 | "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", 488 | "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", 489 | "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", 490 | "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", 491 | "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", 492 | "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", 493 | "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", 494 | "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", 495 | "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", 496 | "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", 497 | "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", 498 | "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", 499 | "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", 500 | "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", 501 | "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", 502 | "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", 503 | "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", 504 | "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", 505 | "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", 506 | "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", 507 | "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", 508 | "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", 509 | "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", 510 | "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", 511 | "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", 512 | "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", 513 | "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", 514 | "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", 515 | "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", 516 | "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", 517 | "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", 518 | "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", 519 | "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", 520 | "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", 521 | "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", 522 | "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", 523 | "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", 524 | "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", 525 | "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", 526 | "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", 527 | "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", 528 | "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", 529 | "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", 530 | "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", 531 | "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", 532 | "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", 533 | "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", 534 | "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", 535 | "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", 536 | "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" 537 | ], 538 | "markers": "python_version >= '3.7'", 539 | "version": "==1.4.4" 540 | }, 541 | "mahotas": { 542 | "hashes": [ 543 | "sha256:0f14399255c39f0f6cf00819e9c3d787f94d87d99c8f1a8ed78079a3c73824d2", 544 | "sha256:14dc57b5b2e01e3a3e832eb688a38cd245f8cdbdf382f29b6e07003d79d5f0e7", 545 | "sha256:22920ea2f5285f8a99a357960fdeed540863d94a47dcfa79bcc3885a29eafab7", 546 | "sha256:2df8091c037305398ae3e66990398b8f4502c49917536d28f562c88c1d1e53d6", 547 | "sha256:315cf7a3e5dd94f9014923fa611b36d999ce7e855b62ffe103613eeeb8cdc319", 548 | "sha256:3f0109f167fc9b599f77b4d30d57c45bcc34dea019ff79fe29eb43c866d87ce9", 549 | "sha256:4ef9a73062012aac935a4398d91edad4ccddd25f98f61f201cefbe20b6b0aa8b", 550 | "sha256:5a37ba53dda795f2534251df5887fbb37153b14a516f8a14a1c0c762b6513179", 551 | "sha256:849054bdc095749104f6e13cf3ada97f98f29f0ae2af0e0c05de740b3b532fd0", 552 | "sha256:a6d343b59c56fdc812ac2fa07e9e555ca1fb3fe9b555698e2f0bfae9fd9f2a75", 553 | "sha256:d200a8ebe10bd0c07f82d202d1713a96b7596f20adc644955685aafcfa7ae9d3", 554 | "sha256:d923b92f9a286626fe777464c8e6c684c24b2432cd6c6a97ddd5c8f6873ac006", 555 | "sha256:ed07b14c843900072de039c5ae925c9afcce7cafb6e1a4ffd5e1889502de38f8" 556 | ], 557 | "index": "pypi", 558 | "version": "==1.4.11" 559 | }, 560 | "markupsafe": { 561 | "hashes": [ 562 | "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", 563 | "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", 564 | "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", 565 | "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", 566 | "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", 567 | "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", 568 | "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", 569 | "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", 570 | "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", 571 | "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", 572 | "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", 573 | "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", 574 | "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", 575 | "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", 576 | "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", 577 | "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", 578 | "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", 579 | "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", 580 | "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", 581 | "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", 582 | "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", 583 | "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", 584 | "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", 585 | "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", 586 | "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", 587 | "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", 588 | "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", 589 | "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", 590 | "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", 591 | "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", 592 | "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", 593 | "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", 594 | "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", 595 | "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", 596 | "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", 597 | "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", 598 | "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", 599 | "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", 600 | "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", 601 | "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", 602 | "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", 603 | "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", 604 | "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", 605 | "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", 606 | "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", 607 | "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", 608 | "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", 609 | "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", 610 | "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", 611 | "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" 612 | ], 613 | "markers": "python_version >= '3.7'", 614 | "version": "==2.1.2" 615 | }, 616 | "matplotlib": { 617 | "hashes": [ 618 | "sha256:01b7f521a9a73c383825813af255f8c4485d1706e4f3e2ed5ae771e4403a40ab", 619 | "sha256:11011c97d62c1db7bc20509572557842dbb8c2a2ddd3dd7f20501aa1cde3e54e", 620 | "sha256:1183877d008c752d7d535396096c910f4663e4b74a18313adee1213328388e1e", 621 | "sha256:12f999661589981e74d793ee2f41b924b3b87d65fd929f6153bf0f30675c59b1", 622 | "sha256:1c235bf9be052347373f589e018988cad177abb3f997ab1a2e2210c41562cc0c", 623 | "sha256:1f4d69707b1677560cd952544ee4962f68ff07952fb9069ff8c12b56353cb8c9", 624 | "sha256:1fcc4cad498533d3c393a160975acc9b36ffa224d15a6b90ae579eacee5d8579", 625 | "sha256:2787a16df07370dcba385fe20cdd0cc3cfaabd3c873ddabca78c10514c799721", 626 | "sha256:29f17b7f2e068dc346687cbdf80b430580bab42346625821c2d3abf3a1ec5417", 627 | "sha256:38d38cb1ea1d80ee0f6351b65c6f76cad6060bbbead015720ba001348ae90f0c", 628 | "sha256:3f56a7252eee8f3438447f75f5e1148a1896a2756a92285fe5d73bed6deebff4", 629 | "sha256:5223affa21050fb6118353c1380c15e23aedfb436bf3e162c26dc950617a7519", 630 | "sha256:57ad1aee29043163374bfa8990e1a2a10ff72c9a1bfaa92e9c46f6ea59269121", 631 | "sha256:59400cc9451094b7f08cc3f321972e6e1db4cd37a978d4e8a12824bf7fd2f03b", 632 | "sha256:68d94a436f62b8a861bf3ace82067a71bafb724b4e4f9133521e4d8012420dd7", 633 | "sha256:6adc441b5b2098a4b904bbf9d9e92fb816fef50c55aa2ea6a823fc89b94bb838", 634 | "sha256:6d81b11ede69e3a751424b98dc869c96c10256b2206bfdf41f9c720eee86844c", 635 | "sha256:73b93af33634ed919e72811c9703e1105185cd3fb46d76f30b7f4cfbbd063f89", 636 | "sha256:77b384cee7ab8cf75ffccbfea351a09b97564fc62d149827a5e864bec81526e5", 637 | "sha256:79e501eb847f4a489eb7065bb8d3187117f65a4c02d12ea3a19d6c5bef173bcc", 638 | "sha256:809119d1cba3ece3c9742eb01827fe7a0e781ea3c5d89534655a75e07979344f", 639 | "sha256:80c166a0e28512e26755f69040e6bf2f946a02ffdb7c00bf6158cca3d2b146e6", 640 | "sha256:81b409b2790cf8d7c1ef35920f01676d2ae7afa8241844e7aa5484fdf493a9a0", 641 | "sha256:994637e2995b0342699b396a320698b07cd148bbcf2dd2fa2daba73f34dd19f2", 642 | "sha256:9ceebaf73f1a3444fa11014f38b9da37ff7ea328d6efa1652241fe3777bfdab9", 643 | "sha256:9fb8fb19d03abf3c5dab89a8677e62c4023632f919a62b6dd1d6d2dbf42cd9f5", 644 | "sha256:acc3b1a4bddbf56fe461e36fb9ef94c2cb607fc90d24ccc650040bfcc7610de4", 645 | "sha256:bbddfeb1495484351fb5b30cf5bdf06b3de0bc4626a707d29e43dfd61af2a780", 646 | "sha256:bbf269e1d24bc25247095d71c7a969813f7080e2a7c6fa28931a603f747ab012", 647 | "sha256:bebcff4c3ed02c6399d47329f3554193abd824d3d53b5ca02cf583bcd94470e2", 648 | "sha256:c3f08df2ac4636249b8bc7a85b8b82c983bef1441595936f62c2918370ca7e1d", 649 | "sha256:ca94f0362f6b6f424b555b956971dcb94b12d0368a6c3e07dc7a40d32d6d873d", 650 | "sha256:d00c248ab6b92bea3f8148714837937053a083ff03b4c5e30ed37e28fc0e7e56", 651 | "sha256:d2cfaa7fd62294d945b8843ea24228a27c8e7c5b48fa634f3c168153b825a21b", 652 | "sha256:d5f18430f5cfa5571ab8f4c72c89af52aa0618e864c60028f11a857d62200cba", 653 | "sha256:debeab8e2ab07e5e3dac33e12456da79c7e104270d2b2d1df92b9e40347cca75", 654 | "sha256:dfba7057609ca9567b9704626756f0142e97ec8c5ba2c70c6e7bd1c25ef99f06", 655 | "sha256:e0a64d7cc336b52e90f59e6d638ae847b966f68582a7af041e063d568e814740", 656 | "sha256:eb9421c403ffd387fbe729de6d9a03005bf42faba5e8432f4e51e703215b49fc", 657 | "sha256:faff486b36530a836a6b4395850322e74211cd81fc17f28b4904e1bd53668e3e", 658 | "sha256:ff2aa84e74f80891e6bcf292ebb1dd57714ffbe13177642d65fee25384a30894" 659 | ], 660 | "markers": "python_version >= '3.8'", 661 | "version": "==3.6.3" 662 | }, 663 | "matplotlib-inline": { 664 | "hashes": [ 665 | "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", 666 | "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" 667 | ], 668 | "markers": "python_version >= '3.5'", 669 | "version": "==0.1.6" 670 | }, 671 | "mistune": { 672 | "hashes": [ 673 | "sha256:0246113cb2492db875c6be56974a7c893333bf26cd92891c85f63151cee09d34", 674 | "sha256:bad7f5d431886fcbaf5f758118ecff70d31f75231b34024a1341120340a65ce8" 675 | ], 676 | "version": "==2.0.5" 677 | }, 678 | "nbclassic": { 679 | "hashes": [ 680 | "sha256:32c235e1f22f4048f3b877d354c198202898797cf9c2085856827598cead001b", 681 | "sha256:8e8ffce7582bb7a4baf11fa86a3d88b184e8e7df78eed4ead69f15aa4fc0e323" 682 | ], 683 | "markers": "python_version >= '3.7'", 684 | "version": "==0.5.1" 685 | }, 686 | "nbclient": { 687 | "hashes": [ 688 | "sha256:434c91385cf3e53084185334d675a0d33c615108b391e260915d1aa8e86661b8", 689 | "sha256:a1d844efd6da9bc39d2209bf996dbd8e07bf0f36b796edfabaa8f8a9ab77c3aa" 690 | ], 691 | "markers": "python_version >= '3.7'", 692 | "version": "==0.7.0" 693 | }, 694 | "nbconvert": { 695 | "hashes": [ 696 | "sha256:495638c5e06005f4a5ce828d8a81d28e34f95c20f4384d5d7a22254b443836e7", 697 | "sha256:a42c3ac137c64f70cbe4d763111bf358641ea53b37a01a5c202ed86374af5234" 698 | ], 699 | "markers": "python_version >= '3.7'", 700 | "version": "==7.2.9" 701 | }, 702 | "nbformat": { 703 | "hashes": [ 704 | "sha256:22a98a6516ca216002b0a34591af5bcb8072ca6c63910baffc901cfa07fefbf0", 705 | "sha256:4b021fca24d3a747bf4e626694033d792d594705829e5e35b14ee3369f9f6477" 706 | ], 707 | "markers": "python_version >= '3.7'", 708 | "version": "==5.7.3" 709 | }, 710 | "nest-asyncio": { 711 | "hashes": [ 712 | "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8", 713 | "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290" 714 | ], 715 | "markers": "python_version >= '3.5'", 716 | "version": "==1.5.6" 717 | }, 718 | "networkx": { 719 | "hashes": [ 720 | "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e", 721 | "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412" 722 | ], 723 | "markers": "python_version >= '3.8'", 724 | "version": "==3.0" 725 | }, 726 | "notebook": { 727 | "hashes": [ 728 | "sha256:c1897e5317e225fc78b45549a6ab4b668e4c996fd03a04e938fe5e7af2bfffd0", 729 | "sha256:e04f9018ceb86e4fa841e92ea8fb214f8d23c1cedfde530cc96f92446924f0e4" 730 | ], 731 | "markers": "python_version >= '3.7'", 732 | "version": "==6.5.2" 733 | }, 734 | "notebook-shim": { 735 | "hashes": [ 736 | "sha256:090e0baf9a5582ff59b607af523ca2db68ff216da0c69956b62cab2ef4fc9c3f", 737 | "sha256:9c6c30f74c4fbea6fce55c1be58e7fd0409b1c681b075dcedceb005db5026949" 738 | ], 739 | "markers": "python_version >= '3.7'", 740 | "version": "==0.2.2" 741 | }, 742 | "numpy": { 743 | "hashes": [ 744 | "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595", 745 | "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f", 746 | "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f", 747 | "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5", 748 | "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695", 749 | "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9", 750 | "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559", 751 | "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982", 752 | "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88", 753 | "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226", 754 | "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f", 755 | "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1", 756 | "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471", 757 | "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383", 758 | "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397", 759 | "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c", 760 | "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f", 761 | "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd", 762 | "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b", 763 | "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9", 764 | "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d", 765 | "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c" 766 | ], 767 | "index": "pypi", 768 | "version": "==1.22.0" 769 | }, 770 | "packaging": { 771 | "hashes": [ 772 | "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", 773 | "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" 774 | ], 775 | "markers": "python_version >= '3.7'", 776 | "version": "==23.0" 777 | }, 778 | "pandocfilters": { 779 | "hashes": [ 780 | "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38", 781 | "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f" 782 | ], 783 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 784 | "version": "==1.5.0" 785 | }, 786 | "parso": { 787 | "hashes": [ 788 | "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", 789 | "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" 790 | ], 791 | "markers": "python_version >= '3.6'", 792 | "version": "==0.8.3" 793 | }, 794 | "pexpect": { 795 | "hashes": [ 796 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 797 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 798 | ], 799 | "markers": "sys_platform != 'win32'", 800 | "version": "==4.8.0" 801 | }, 802 | "pickleshare": { 803 | "hashes": [ 804 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 805 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 806 | ], 807 | "version": "==0.7.5" 808 | }, 809 | "pillow": { 810 | "hashes": [ 811 | "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33", 812 | "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b", 813 | "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e", 814 | "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35", 815 | "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153", 816 | "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9", 817 | "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569", 818 | "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57", 819 | "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8", 820 | "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1", 821 | "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264", 822 | "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157", 823 | "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9", 824 | "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133", 825 | "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9", 826 | "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab", 827 | "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6", 828 | "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5", 829 | "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df", 830 | "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503", 831 | "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b", 832 | "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa", 833 | "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327", 834 | "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493", 835 | "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d", 836 | "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4", 837 | "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4", 838 | "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35", 839 | "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2", 840 | "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c", 841 | "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011", 842 | "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a", 843 | "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e", 844 | "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f", 845 | "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848", 846 | "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57", 847 | "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f", 848 | "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c", 849 | "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9", 850 | "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5", 851 | "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9", 852 | "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d", 853 | "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0", 854 | "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1", 855 | "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e", 856 | "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815", 857 | "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0", 858 | "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b", 859 | "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd", 860 | "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c", 861 | "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3", 862 | "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab", 863 | "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858", 864 | "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5", 865 | "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee", 866 | "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343", 867 | "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb", 868 | "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47", 869 | "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed", 870 | "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837", 871 | "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286", 872 | "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28", 873 | "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628", 874 | "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df", 875 | "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d", 876 | "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d", 877 | "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a", 878 | "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6", 879 | "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336", 880 | "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132", 881 | "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070", 882 | "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe", 883 | "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a", 884 | "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd", 885 | "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391", 886 | "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a", 887 | "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12" 888 | ], 889 | "markers": "python_version >= '3.7'", 890 | "version": "==9.4.0" 891 | }, 892 | "pkgutil-resolve-name": { 893 | "hashes": [ 894 | "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", 895 | "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" 896 | ], 897 | "markers": "python_version < '3.9'", 898 | "version": "==1.3.10" 899 | }, 900 | "prometheus-client": { 901 | "hashes": [ 902 | "sha256:0836af6eb2c8f4fed712b2f279f6c0a8bbab29f9f4aa15276b91c7cb0d1616ab", 903 | "sha256:a03e35b359f14dd1630898543e2120addfdeacd1a6069c1367ae90fd93ad3f48" 904 | ], 905 | "markers": "python_version >= '3.6'", 906 | "version": "==0.16.0" 907 | }, 908 | "prompt-toolkit": { 909 | "hashes": [ 910 | "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63", 911 | "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305" 912 | ], 913 | "markers": "python_full_version >= '3.6.2'", 914 | "version": "==3.0.36" 915 | }, 916 | "psutil": { 917 | "hashes": [ 918 | "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff", 919 | "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1", 920 | "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62", 921 | "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549", 922 | "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08", 923 | "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7", 924 | "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e", 925 | "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe", 926 | "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24", 927 | "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad", 928 | "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94", 929 | "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8", 930 | "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7", 931 | "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4" 932 | ], 933 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 934 | "version": "==5.9.4" 935 | }, 936 | "ptyprocess": { 937 | "hashes": [ 938 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 939 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 940 | ], 941 | "markers": "os_name != 'nt'", 942 | "version": "==0.7.0" 943 | }, 944 | "pure-eval": { 945 | "hashes": [ 946 | "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", 947 | "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" 948 | ], 949 | "version": "==0.2.2" 950 | }, 951 | "pycparser": { 952 | "hashes": [ 953 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 954 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 955 | ], 956 | "version": "==2.21" 957 | }, 958 | "pygments": { 959 | "hashes": [ 960 | "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", 961 | "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" 962 | ], 963 | "markers": "python_version >= '3.6'", 964 | "version": "==2.14.0" 965 | }, 966 | "pyparsing": { 967 | "hashes": [ 968 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 969 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 970 | ], 971 | "markers": "python_full_version >= '3.6.8'", 972 | "version": "==3.0.9" 973 | }, 974 | "pyrsistent": { 975 | "hashes": [ 976 | "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", 977 | "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", 978 | "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", 979 | "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", 980 | "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", 981 | "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", 982 | "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", 983 | "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", 984 | "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", 985 | "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", 986 | "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", 987 | "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", 988 | "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", 989 | "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", 990 | "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", 991 | "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", 992 | "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", 993 | "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", 994 | "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", 995 | "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", 996 | "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", 997 | "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", 998 | "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", 999 | "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", 1000 | "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", 1001 | "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", 1002 | "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" 1003 | ], 1004 | "markers": "python_version >= '3.7'", 1005 | "version": "==0.19.3" 1006 | }, 1007 | "python-dateutil": { 1008 | "hashes": [ 1009 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 1010 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 1011 | ], 1012 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1013 | "version": "==2.8.2" 1014 | }, 1015 | "pywavelets": { 1016 | "hashes": [ 1017 | "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b", 1018 | "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4", 1019 | "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4", 1020 | "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6", 1021 | "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de", 1022 | "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c", 1023 | "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa", 1024 | "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93", 1025 | "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875", 1026 | "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd", 1027 | "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356", 1028 | "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1", 1029 | "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c", 1030 | "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2", 1031 | "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784", 1032 | "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4", 1033 | "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b", 1034 | "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc", 1035 | "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966", 1036 | "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e", 1037 | "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426", 1038 | "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c", 1039 | "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202", 1040 | "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc", 1041 | "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd" 1042 | ], 1043 | "markers": "python_version >= '3.8'", 1044 | "version": "==1.4.1" 1045 | }, 1046 | "pyzmq": { 1047 | "hashes": [ 1048 | "sha256:00c94fd4c9dd3c95aace0c629a7fa713627a5c80c1819326b642adf6c4b8e2a2", 1049 | "sha256:01d53958c787cfea34091fcb8ef36003dbb7913b8e9f8f62a0715234ebc98b70", 1050 | "sha256:0282bba9aee6e0346aa27d6c69b5f7df72b5a964c91958fc9e0c62dcae5fdcdc", 1051 | "sha256:02f5cb60a7da1edd5591a15efa654ffe2303297a41e1b40c3c8942f8f11fc17c", 1052 | "sha256:0645b5a2d2a06fd8eb738018490c514907f7488bf9359c6ee9d92f62e844b76f", 1053 | "sha256:0a154ef810d44f9d28868be04641f837374a64e7449df98d9208e76c260c7ef1", 1054 | "sha256:0a90b2480a26aef7c13cff18703ba8d68e181facb40f78873df79e6d42c1facc", 1055 | "sha256:0e8d00228db627ddd1b418c7afd81820b38575f237128c9650365f2dd6ac3443", 1056 | "sha256:17e1cb97d573ea84d7cd97188b42ca6f611ab3ee600f6a75041294ede58e3d20", 1057 | "sha256:183e18742be3621acf8908903f689ec520aee3f08449bfd29f583010ca33022b", 1058 | "sha256:1f6116991568aac48b94d6d8aaed6157d407942ea385335a6ed313692777fb9d", 1059 | "sha256:20638121b0bdc80777ce0ec8c1f14f1ffec0697a1f88f0b564fa4a23078791c4", 1060 | "sha256:2754fa68da08a854f4816e05160137fa938a2347276471103d31e04bcee5365c", 1061 | "sha256:28bcb2e66224a7ac2843eb632e4109d6b161479e7a2baf24e37210461485b4f1", 1062 | "sha256:293a7c2128690f496057f1f1eb6074f8746058d13588389981089ec45d8fdc77", 1063 | "sha256:2a73af6504e0d2805e926abf136ebf536735a13c22f709be7113c2ec65b4bec3", 1064 | "sha256:2d05d904f03ddf1e0d83d97341354dfe52244a619b5a1440a5f47a5b3451e84e", 1065 | "sha256:2e7b87638ee30ab13230e37ce5331b3e730b1e0dda30120b9eeec3540ed292c8", 1066 | "sha256:3100dddcada66ec5940ed6391ebf9d003cc3ede3d320748b2737553019f58230", 1067 | "sha256:31e523d067ce44a04e876bed3ff9ea1ff8d1b6636d16e5fcace9d22f8c564369", 1068 | "sha256:3594c0ff604e685d7e907860b61d0e10e46c74a9ffca168f6e9e50ea934ee440", 1069 | "sha256:3670e8c5644768f214a3b598fe46378a4a6f096d5fb82a67dfd3440028460565", 1070 | "sha256:4046d03100aca266e70d54a35694cb35d6654cfbef633e848b3c4a8d64b9d187", 1071 | "sha256:4725412e27612f0d7d7c2f794d89807ad0227c2fc01dd6146b39ada49c748ef9", 1072 | "sha256:484c2c4ee02c1edc07039f42130bd16e804b1fe81c4f428e0042e03967f40c20", 1073 | "sha256:487305c2a011fdcf3db1f24e8814bb76d23bc4d2f46e145bc80316a59a9aa07d", 1074 | "sha256:4a1bc30f0c18444d51e9b0d0dd39e3a4e7c53ee74190bebef238cd58de577ea9", 1075 | "sha256:4c25c95416133942280faaf068d0fddfd642b927fb28aaf4ab201a738e597c1e", 1076 | "sha256:4cbb885f347eba7ab7681c450dee5b14aed9f153eec224ec0c3f299273d9241f", 1077 | "sha256:4d3d604fe0a67afd1aff906e54da557a5203368a99dcc50a70eef374f1d2abef", 1078 | "sha256:4e295f7928a31ae0f657e848c5045ba6d693fe8921205f408ca3804b1b236968", 1079 | "sha256:5049e75cc99db65754a3da5f079230fb8889230cf09462ec972d884d1704a3ed", 1080 | "sha256:5050f5c50b58a6e38ccaf9263a356f74ef1040f5ca4030225d1cb1a858c5b7b6", 1081 | "sha256:526f884a27e8bba62fe1f4e07c62be2cfe492b6d432a8fdc4210397f8cf15331", 1082 | "sha256:531866c491aee5a1e967c286cfa470dffac1e2a203b1afda52d62b58782651e9", 1083 | "sha256:5605621f2181f20b71f13f698944deb26a0a71af4aaf435b34dd90146092d530", 1084 | "sha256:58fc3ad5e1cfd2e6d24741fbb1e216b388115d31b0ca6670f894187f280b6ba6", 1085 | "sha256:60ecbfe7669d3808ffa8a7dd1487d6eb8a4015b07235e3b723d4b2a2d4de7203", 1086 | "sha256:610d2d112acd4e5501fac31010064a6c6efd716ceb968e443cae0059eb7b86de", 1087 | "sha256:6136bfb0e5a9cf8c60c6ac763eb21f82940a77e6758ea53516c8c7074f4ff948", 1088 | "sha256:62b9e80890c0d2408eb42d5d7e1fc62a5ce71be3288684788f74cf3e59ffd6e2", 1089 | "sha256:656281d496aaf9ca4fd4cea84e6d893e3361057c4707bd38618f7e811759103c", 1090 | "sha256:66509c48f7446b640eeae24b60c9c1461799a27b1b0754e438582e36b5af3315", 1091 | "sha256:6bf3842af37af43fa953e96074ebbb5315f6a297198f805d019d788a1021dbc8", 1092 | "sha256:731b208bc9412deeb553c9519dca47136b5a01ca66667cafd8733211941b17e4", 1093 | "sha256:75243e422e85a62f0ab7953dc315452a56b2c6a7e7d1a3c3109ac3cc57ed6b47", 1094 | "sha256:7877264aa851c19404b1bb9dbe6eed21ea0c13698be1eda3784aab3036d1c861", 1095 | "sha256:81f99fb1224d36eb91557afec8cdc2264e856f3464500b55749020ce4c848ef2", 1096 | "sha256:8539216173135e9e89f6b1cc392e74e6b935b91e8c76106cf50e7a02ab02efe5", 1097 | "sha256:85456f0d8f3268eecd63dede3b99d5bd8d3b306310c37d4c15141111d22baeaf", 1098 | "sha256:866eabf7c1315ef2e93e34230db7cbf672e0d7c626b37c11f7e870c8612c3dcc", 1099 | "sha256:926236ca003aec70574754f39703528947211a406f5c6c8b3e50eca04a9e87fc", 1100 | "sha256:930e6ad4f2eaac31a3d0c2130619d25db754b267487ebc186c6ad18af2a74018", 1101 | "sha256:94f0a7289d0f5c80807c37ebb404205e7deb737e8763eb176f4770839ee2a287", 1102 | "sha256:9a2d5e419bd39a1edb6cdd326d831f0120ddb9b1ff397e7d73541bf393294973", 1103 | "sha256:9ca6db34b26c4d3e9b0728841ec9aa39484eee272caa97972ec8c8e231b20c7e", 1104 | "sha256:9f72ea279b2941a5203e935a4588b9ba8a48aeb9a926d9dfa1986278bd362cb8", 1105 | "sha256:a0e7ef9ac807db50b4eb6f534c5dcc22f998f5dae920cc28873d2c1d080a4fc9", 1106 | "sha256:a1cd4a95f176cdc0ee0a82d49d5830f13ae6015d89decbf834c273bc33eeb3d3", 1107 | "sha256:a9c464cc508177c09a5a6122b67f978f20e2954a21362bf095a0da4647e3e908", 1108 | "sha256:ac97e7d647d5519bcef48dd8d3d331f72975afa5c4496c95f6e854686f45e2d9", 1109 | "sha256:af1fbfb7ad6ac0009ccee33c90a1d303431c7fb594335eb97760988727a37577", 1110 | "sha256:b055a1cddf8035966ad13aa51edae5dc8f1bba0b5d5e06f7a843d8b83dc9b66b", 1111 | "sha256:b6f75b4b8574f3a8a0d6b4b52606fc75b82cb4391471be48ab0b8677c82f9ed4", 1112 | "sha256:b90bb8dfbbd138558f1f284fecfe328f7653616ff9a972433a00711d9475d1a9", 1113 | "sha256:be05504af0619d1cffa500af1e0ede69fb683f301003851f5993b5247cc2c576", 1114 | "sha256:c21a5f4e54a807df5afdef52b6d24ec1580153a6bcf0607f70a6e1d9fa74c5c3", 1115 | "sha256:c48f257da280b3be6c94e05bd575eddb1373419dbb1a72c3ce64e88f29d1cd6d", 1116 | "sha256:cac602e02341eaaf4edfd3e29bd3fdef672e61d4e6dfe5c1d065172aee00acee", 1117 | "sha256:ccb3e1a863222afdbda42b7ca8ac8569959593d7abd44f5a709177d6fa27d266", 1118 | "sha256:e1081d7030a1229c8ff90120346fb7599b54f552e98fcea5170544e7c6725aab", 1119 | "sha256:e14df47c1265356715d3d66e90282a645ebc077b70b3806cf47efcb7d1d630cb", 1120 | "sha256:e4bba04ea779a3d7ef25a821bb63fd0939142c88e7813e5bd9c6265a20c523a2", 1121 | "sha256:e99629a976809fe102ef73e856cf4b2660acd82a412a51e80ba2215e523dfd0a", 1122 | "sha256:f330a1a2c7f89fd4b0aa4dcb7bf50243bf1c8da9a2f1efc31daf57a2046b31f2", 1123 | "sha256:f3f96d452e9580cb961ece2e5a788e64abaecb1232a80e61deffb28e105ff84a", 1124 | "sha256:fc7c1421c5b1c916acf3128bf3cc7ea7f5018b58c69a6866d70c14190e600ce9" 1125 | ], 1126 | "markers": "python_version >= '3.6'", 1127 | "version": "==25.0.0" 1128 | }, 1129 | "qtconsole": { 1130 | "hashes": [ 1131 | "sha256:57748ea2fd26320a0b77adba20131cfbb13818c7c96d83fafcb110ff55f58b35", 1132 | "sha256:be13560c19bdb3b54ed9741a915aa701a68d424519e8341ac479a91209e694b2" 1133 | ], 1134 | "markers": "python_version >= '3.7'", 1135 | "version": "==5.4.0" 1136 | }, 1137 | "qtpy": { 1138 | "hashes": [ 1139 | "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5", 1140 | "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408" 1141 | ], 1142 | "markers": "python_version >= '3.7'", 1143 | "version": "==2.3.0" 1144 | }, 1145 | "scikit-build": { 1146 | "hashes": [ 1147 | "sha256:da40dfd69b2456fad1349a894b90180b43712152b8a85d2a00f4ae2ce8ac9a5c", 1148 | "sha256:dd236b60330f243e79a9795952c6efeb6e28fd0bd7a35fd92eb490456ae29356" 1149 | ], 1150 | "index": "pypi", 1151 | "version": "==0.11.1" 1152 | }, 1153 | "scikit-image": { 1154 | "hashes": [ 1155 | "sha256:113bcacdfc839854f527a166a71768708328208e7b66e491050d6a57fa6727c7", 1156 | "sha256:11eec2e65cd4cd6487fe1089aa3538dbe25525aec7a36f5a0f14145df0163ce7", 1157 | "sha256:178210582cc62a5b25c633966658f1f2598615f9c3f27f36cf45055d2a74b401", 1158 | "sha256:1fda9109a19dc9d7a4ac152d1fc226fed7282ad186a099f14c0aa9151f0c758e", 1159 | "sha256:6b65a103edbc34b22640daf3b084dc9e470c358d3298c10aa9e3b424dcc02db6", 1160 | "sha256:7bedd3881ca4fea657a894815bcd5e5bf80944c26274f6b6417bb770c3f4f8e6", 1161 | "sha256:86a834f9a4d30201c0803a48a25364fe8f93f9feb3c58f2c483d3ce0a3e5fe4a", 1162 | "sha256:87ca5168c6fc36b7a298a1db2d185a8298f549854342020f282f747a4e4ddce9", 1163 | "sha256:bd954c0588f0f7e81d9763dc95e06950e68247d540476e06cb77bcbcd8c2d8b3", 1164 | "sha256:c0876e562991b0babff989ff4d00f35067a2ddef82e5fdd895862555ffbaec25", 1165 | "sha256:c5c277704b12e702e34d1f7b7a04d5ee8418735f535d269c74c02c6c9f8abee2", 1166 | "sha256:e99fa7514320011b250a21ab855fdd61ddcc05d3c77ec9e8f13edcc15d3296b5", 1167 | "sha256:ee3db438b5b9f8716a91ab26a61377a8a63356b186706f5b979822cc7241006d" 1168 | ], 1169 | "index": "pypi", 1170 | "version": "==0.17.2" 1171 | }, 1172 | "scipy": { 1173 | "hashes": [ 1174 | "sha256:168c45c0c32e23f613db7c9e4e780bc61982d71dcd406ead746c7c7c2f2004ce", 1175 | "sha256:213bc59191da2f479984ad4ec39406bf949a99aba70e9237b916ce7547b6ef42", 1176 | "sha256:25b241034215247481f53355e05f9e25462682b13bd9191359075682adcd9554", 1177 | "sha256:2c872de0c69ed20fb1a9b9cf6f77298b04a26f0b8720a5457be08be254366c6e", 1178 | "sha256:3397c129b479846d7eaa18f999369a24322d008fac0782e7828fa567358c36ce", 1179 | "sha256:368c0f69f93186309e1b4beb8e26d51dd6f5010b79264c0f1e9ca00cd92ea8c9", 1180 | "sha256:3d5db5d815370c28d938cf9b0809dade4acf7aba57eaf7ef733bfedc9b2474c4", 1181 | "sha256:4598cf03136067000855d6b44d7a1f4f46994164bcd450fb2c3d481afc25dd06", 1182 | "sha256:4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b", 1183 | "sha256:4f12d13ffbc16e988fa40809cbbd7a8b45bc05ff6ea0ba8e3e41f6f4db3a9e47", 1184 | "sha256:634568a3018bc16a83cda28d4f7aed0d803dd5618facb36e977e53b2df868443", 1185 | "sha256:65923bc3809524e46fb7eb4d6346552cbb6a1ffc41be748535aa502a2e3d3389", 1186 | "sha256:6b0ceb23560f46dd236a8ad4378fc40bad1783e997604ba845e131d6c680963e", 1187 | "sha256:8c8d6ca19c8497344b810b0b0344f8375af5f6bb9c98bd42e33f747417ab3f57", 1188 | "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62", 1189 | "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d", 1190 | "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437", 1191 | "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2", 1192 | "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54", 1193 | "sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474", 1194 | "sha256:e360cb2299028d0b0d0f65a5c5e51fc16a335f1603aa2357c25766c8dab56938", 1195 | "sha256:e98d49a5717369d8241d6cf33ecb0ca72deee392414118198a8e5b4c35c56340", 1196 | "sha256:ed572470af2438b526ea574ff8f05e7f39b44ac37f712105e57fc4d53a6fb660", 1197 | "sha256:f87b39f4d69cf7d7529d7b1098cb712033b17ea7714aed831b95628f483fd012", 1198 | "sha256:fa789583fc94a7689b45834453fec095245c7e69c58561dc159b5d5277057e4c" 1199 | ], 1200 | "index": "pypi", 1201 | "version": "==1.5.4" 1202 | }, 1203 | "send2trash": { 1204 | "hashes": [ 1205 | "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d", 1206 | "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08" 1207 | ], 1208 | "version": "==1.8.0" 1209 | }, 1210 | "setuptools": { 1211 | "hashes": [ 1212 | "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c", 1213 | "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48" 1214 | ], 1215 | "markers": "python_version >= '3'", 1216 | "version": "==67.2.0" 1217 | }, 1218 | "simpleitk": { 1219 | "hashes": [ 1220 | "sha256:09911709ae2d3e29115dce3e78fa437dd78836812306be0a252c2da120928435", 1221 | "sha256:13628c2b22679a8b36d209ebc1d0ace53fe4ab6c4e8654eff0bc96ee40f702c8", 1222 | "sha256:21bb06e640a8540d53b3ee00a8fcdcdf6e1a48aa4cf28172cc08bc153f012b76", 1223 | "sha256:46772fa3e6e167de0821b85e122debf90833d84daa23c5c66053cf28437f4573", 1224 | "sha256:4728c090a2e95b169ef8edab42f068fcb8657cac1d3d98df805ca468d9c45c38", 1225 | "sha256:5ab5ab167d27bb24e958f02df26b467603197b71e91b13b1635763cf64d2431f", 1226 | "sha256:6522c9e6a24301d0886f5e452724f165e0bd521548a9795b5bdc62031e950d1d", 1227 | "sha256:666b7b88bc53c07d012181fa5306134d2d837cc59bc99e5c6641954f81c6501a", 1228 | "sha256:6efe188b0f375f35734b272fe06863731d9ab08f9f0ff3e4bc342f941cf4ca08", 1229 | "sha256:73caad7fb842985abc0ccf3c2590fd4e66e2b66e846b2eb17b759ffe4cc0455c", 1230 | "sha256:8d795689bc613907196b1dc322e3b6247ec19c8e94cc12704a267d017551cb61", 1231 | "sha256:95070b5cd4293586aa788edf79593d1b9600d2c55ffe641920090f58dd69b995", 1232 | "sha256:a49b02bd405a3630be5388c0cafdcee951a8e8a81351fcca9e55b6b1b76f0ff9", 1233 | "sha256:cb969855ea1cd14839b10755a9b3cc1528cc912d4d5cc151c04f46fbcbd416c2", 1234 | "sha256:dc077928ab993170da5eed27f4737c9325a9fe705ff871945d0a6d6f15e24fab", 1235 | "sha256:e0be3a611b447fb923ebfaacc3ca1ce42d09aa31882086efdb792329e9ab123b", 1236 | "sha256:ed444d72a6a5f4870077a1ddf376974ac927afe0a2b7c8a34d359b074ac21c2d", 1237 | "sha256:f83fdde9eb91d0b01af73e64e22d65b26dbaf1edf6e6220a2ab21123229e0863", 1238 | "sha256:fa87ed3d73732a4668d8e216aac6e6d547ec92b566681a5d34ca5b7d86c569dc", 1239 | "sha256:fac923ee2c3c09dcee9b2a80c0d2bd886765a0727eba7a3befa3a36a8e17170f" 1240 | ], 1241 | "index": "pypi", 1242 | "version": "==2.0.2" 1243 | }, 1244 | "six": { 1245 | "hashes": [ 1246 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 1247 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 1248 | ], 1249 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1250 | "version": "==1.16.0" 1251 | }, 1252 | "sniffio": { 1253 | "hashes": [ 1254 | "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", 1255 | "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" 1256 | ], 1257 | "markers": "python_version >= '3.7'", 1258 | "version": "==1.3.0" 1259 | }, 1260 | "soupsieve": { 1261 | "hashes": [ 1262 | "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", 1263 | "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" 1264 | ], 1265 | "markers": "python_version >= '3.6'", 1266 | "version": "==2.3.2.post1" 1267 | }, 1268 | "stack-data": { 1269 | "hashes": [ 1270 | "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815", 1271 | "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8" 1272 | ], 1273 | "version": "==0.6.2" 1274 | }, 1275 | "terminado": { 1276 | "hashes": [ 1277 | "sha256:6ccbbcd3a4f8a25a5ec04991f39a0b8db52dfcd487ea0e578d977e6752380333", 1278 | "sha256:8650d44334eba354dd591129ca3124a6ba42c3d5b70df5051b6921d506fdaeae" 1279 | ], 1280 | "markers": "python_version >= '3.7'", 1281 | "version": "==0.17.1" 1282 | }, 1283 | "tifffile": { 1284 | "hashes": [ 1285 | "sha256:458df5ad9a5217f668edd636dc19fbc736062bf78aac823ab84cdbae9de8eaa6", 1286 | "sha256:6df45c1eab632eadb9384994c8ae8bc424cd3c26e9508481a345dbef1dcfd480" 1287 | ], 1288 | "markers": "python_version >= '3.8'", 1289 | "version": "==2023.2.3" 1290 | }, 1291 | "tinycss2": { 1292 | "hashes": [ 1293 | "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", 1294 | "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" 1295 | ], 1296 | "markers": "python_version >= '3.7'", 1297 | "version": "==1.2.1" 1298 | }, 1299 | "tornado": { 1300 | "hashes": [ 1301 | "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca", 1302 | "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72", 1303 | "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23", 1304 | "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8", 1305 | "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b", 1306 | "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9", 1307 | "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13", 1308 | "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75", 1309 | "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac", 1310 | "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e", 1311 | "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b" 1312 | ], 1313 | "markers": "python_version >= '3.7'", 1314 | "version": "==6.2" 1315 | }, 1316 | "traitlets": { 1317 | "hashes": [ 1318 | "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", 1319 | "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9" 1320 | ], 1321 | "markers": "python_version >= '3.7'", 1322 | "version": "==5.9.0" 1323 | }, 1324 | "wcwidth": { 1325 | "hashes": [ 1326 | "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", 1327 | "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" 1328 | ], 1329 | "version": "==0.2.6" 1330 | }, 1331 | "webencodings": { 1332 | "hashes": [ 1333 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 1334 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 1335 | ], 1336 | "version": "==0.5.1" 1337 | }, 1338 | "websocket-client": { 1339 | "hashes": [ 1340 | "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40", 1341 | "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e" 1342 | ], 1343 | "markers": "python_version >= '3.7'", 1344 | "version": "==1.5.1" 1345 | }, 1346 | "wheel": { 1347 | "hashes": [ 1348 | "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac", 1349 | "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8" 1350 | ], 1351 | "markers": "python_version >= '3.7'", 1352 | "version": "==0.38.4" 1353 | }, 1354 | "widgetsnbextension": { 1355 | "hashes": [ 1356 | "sha256:003f716d930d385be3fd9de42dd9bf008e30053f73bddde235d14fbeaeff19af", 1357 | "sha256:eaaaf434fb9b08bd197b2a14ffe45ddb5ac3897593d43c69287091e5f3147bf7" 1358 | ], 1359 | "markers": "python_version >= '3.7'", 1360 | "version": "==4.0.5" 1361 | }, 1362 | "zipp": { 1363 | "hashes": [ 1364 | "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", 1365 | "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" 1366 | ], 1367 | "markers": "python_version >= '3.7'", 1368 | "version": "==3.13.0" 1369 | } 1370 | }, 1371 | "develop": {} 1372 | } 1373 | --------------------------------------------------------------------------------