diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a169a76 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.agents +.codex +.aws +.local +.env +**/.env +.venv +env_* +.pytest_cache +.mypy_cache +.ruff_cache +**/__pycache__ +**/*.egg-info +**/build +**/dist +output_data diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml new file mode 100644 index 0000000..0a72e39 --- /dev/null +++ b/.github/workflows/conda.yml @@ -0,0 +1,46 @@ +name: Conda + +on: + push: + paths: ['environment.yml', 'packaging/conda/**', 'module/**', 'tests/**', '.github/workflows/conda.yml'] + pull_request: + paths: ['environment.yml', 'packaging/conda/**', 'module/**', 'tests/**', '.github/workflows/conda.yml'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + environment: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - uses: mamba-org/setup-micromamba@v3 + with: + environment-file: environment.yml + generate-run-shell: true + - name: Test the editable environment + shell: micromamba-shell {0} + run: | + python -m pip check + python -m pytest + package: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - uses: mamba-org/setup-micromamba@v3 + with: + environment-name: conda-build + create-args: python=3.12 conda-build rattler-build + generate-run-shell: true + - name: Build and test the package + shell: micromamba-shell {0} + run: | + conda build --override-channels -c conda-forge --python 3.12 --output-folder conda-dist packaging/conda + rattler-build build --recipe packaging/conda/conda-forge/recipe.yaml --channel conda-forge --output-dir conda-dist --log-style plain + - uses: actions/upload-artifact@v7 + with: + name: conda-package + path: conda-dist/noarch/*.conda diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7d69680 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,57 @@ +name: Containers + +on: + push: + paths: ['docker/**', 'module/**', 'jupyter/examples/**', 'tests/container_smoke.py', '.dockerignore', '.github/workflows/docker.yml'] + pull_request: + paths: ['docker/**', 'module/**', 'jupyter/examples/**', 'tests/container_smoke.py', '.dockerignore', '.github/workflows/docker.yml'] + workflow_dispatch: + inputs: + publish: + description: Publish tested images to GitHub Container Registry + type: boolean + default: false + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v7 + - name: Build both targets + run: | + docker build -f docker/Dockerfile --target cli -t collage-test:cli . + docker build -f docker/Dockerfile --target notebook -t collage-test:notebook . + - name: Test the installed CLI + run: | + docker run --rm collage-test:cli --help + docker run --rm --entrypoint python -v "$PWD/tests/container_smoke.py:/tmp/container_smoke.py:ro" collage-test:cli /tmp/container_smoke.py + - name: Execute the bundled notebook + run: | + docker run --rm collage-test:notebook python -m nbconvert --to notebook --execute /opt/collage/examples/example.ipynb --output /tmp/executed.ipynb --ExecutePreprocessor.timeout=120 + publish: + if: github.event_name == 'workflow_dispatch' && inputs.publish + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + - name: Authenticate to the registry + env: + REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: printf '%s' "$REGISTRY_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + - name: Build and publish verified targets + env: + IMAGE_BASE: ghcr.io/${{ github.repository }} + REVISION: ${{ github.sha }} + run: | + for target in cli notebook; do + docker build -f docker/Dockerfile --target "$target" -t "$IMAGE_BASE-$target:$REVISION" -t "$IMAGE_BASE-$target:latest" . + docker push "$IMAGE_BASE-$target:$REVISION" + docker push "$IMAGE_BASE-$target:latest" + done diff --git a/.github/workflows/docs-examples.yml b/.github/workflows/docs-examples.yml new file mode 100644 index 0000000..d286d7e --- /dev/null +++ b/.github/workflows/docs-examples.yml @@ -0,0 +1,27 @@ +name: Documentation and examples + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +jobs: + documentation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: | + module/requirements.txt + module/setup.py + module/docs/requirements.txt + - name: Install documentation and example dependencies + run: python -m pip install -r module/docs/requirements.txt './module[examples]' + - name: Build documentation with warnings as errors + run: python -m sphinx -W -b html module/docs/source "$RUNNER_TEMP/docs" + - name: Execute the notebook without a desktop + run: | + python -m nbconvert --to notebook --execute jupyter/examples/example.ipynb --output "$RUNNER_TEMP/example.executed.ipynb" --ExecutePreprocessor.timeout=120 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1541a3f..67b86be 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,29 +1,66 @@ -name: Continuous Delivery +name: Release packages on: push: - tags: - - 'v*' + paths: ['module/**', 'cli/**', 'tests/**', 'requirements-test.txt', '.github/workflows/python-publish.yml'] + pull_request: + paths: ['module/**', 'cli/**', 'tests/**', 'requirements-test.txt', '.github/workflows/python-publish.yml'] + release: + types: [published] + workflow_dispatch: -jobs: - deploy: +permissions: + contents: read +jobs: + build: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.6' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - cd module - sh setup.sh + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + - name: Validate release version + if: github.event_name == 'release' + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + test "$RELEASE_TAG" = "v$(cat module/VERSION)" + - name: Install and test + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-test.txt twine + python -m pip install --no-build-isolation './module[cli]' + python -m pip check + python -m pytest + - name: Build distributions from source archives + run: | + python -m build --outdir dist/core module + python -m build --outdir dist/cli cli + python -m twine check --strict dist/core/* dist/cli/* + - uses: actions/upload-artifact@v7 + with: + name: python-distributions + path: dist/ + publish: + if: github.event_name == 'release' && !github.event.release.prerelease + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/collageradiomics + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v8 + with: + name: python-distributions + path: dist + - name: Publish the core with trusted publishing + uses: pypa/gh-action-pypi-publish@v1.14.2 + with: + packages-dir: dist/core/ + - name: Publish the compatibility CLI with trusted publishing + uses: pypa/gh-action-pypi-publish@v1.14.2 + with: + packages-dir: dist/cli/ diff --git a/.github/workflows/slicer-tests.yml b/.github/workflows/slicer-tests.yml new file mode 100644 index 0000000..eafe92c --- /dev/null +++ b/.github/workflows/slicer-tests.yml @@ -0,0 +1,39 @@ +name: Slicer tests + +on: + push: + paths: ['module/**', 'slicer/**', 'CMakeLists.txt', 'packaging/catalogs/**', 'tests/run_slicer.py', '.github/workflows/slicer-tests.yml'] + pull_request: + paths: ['module/**', 'slicer/**', 'CMakeLists.txt', 'packaging/catalogs/**', 'tests/run_slicer.py', '.github/workflows/slicer-tests.yml'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + slicer: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + env: + SLICER_SHA512: ac76c5061a8fff20d4e59732855f74360930fd4891450085bcbc6a8eb1a1f4ade9a1ca4fc633142661b714dbe8c4604306079f9546e52053faa5e6f99410cf76 + LIBGL_ALWAYS_SOFTWARE: '1' + steps: + - uses: actions/checkout@v7 + - name: Install application prerequisites + run: | + sudo apt-get update + sudo apt-get install -y libglu1-mesa libpulse-mainloop-glib0 libnss3 libasound2t64 qt5dxcb-plugin xvfb + - name: Download verified Slicer 5.12.4 + run: | + curl --fail --location --retry 3 https://download.slicer.org/bitstream/6aa1beb7ce9de556d3010204 -o "$RUNNER_TEMP/slicer.tar.gz" + printf '%s %s\n' "$SLICER_SHA512" "$RUNNER_TEMP/slicer.tar.gz" | sha512sum --check - + mkdir "$RUNNER_TEMP/slicer" + tar -xzf "$RUNNER_TEMP/slicer.tar.gz" --strip-components=1 -C "$RUNNER_TEMP/slicer" + - name: Install the checked-out library + run: '"$RUNNER_TEMP/slicer/bin/PythonSlicer" -m pip install ./module' + - name: Run native extension tests + env: + XDG_CONFIG_HOME: ${{ runner.temp }}/slicer-config + XDG_CACHE_HOME: ${{ runner.temp }}/slicer-cache + run: | + xvfb-run -a "$RUNNER_TEMP/slicer/Slicer" --ignore-slicerrc --testing --no-main-window --no-splash --python-script tests/run_slicer.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c681239 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Tests + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.10" + constraints: "-c constraints-test.txt" + - python-version: "3.11" + constraints: "" + - python-version: "3.12" + constraints: "" + - python-version: "3.13" + constraints: "" + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + module/pyproject.toml + module/requirements.txt + requirements-test.txt + constraints-test.txt + - name: Install test tools and package + run: | + python -m pip install --upgrade pip + python -m pip install ${{ matrix.constraints }} -r requirements-test.txt + python -m pip install --no-build-isolation ${{ matrix.constraints }} -e './module[cli]' + python -m pip check + - name: Run core and distribution tests + run: python -m pytest + - name: Check types and style + if: matrix.python-version == '3.11' + run: | + python -m mypy + python -m ruff check module/collageradiomics module/_collageradiomics tests cli + python -m ruff format --check module/collageradiomics module/_collageradiomics tests cli diff --git a/.gitignore b/.gitignore index b32ac30..266241a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,11 @@ module/docs/build/* module/docs/source/_build/* .idea/* __pycache__ +.pytest_cache/ env_* +.venv/ +.mypy_cache/ +.ruff_cache/ +*.egg-info/ +cli/build/ +cli/dist/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 1e08141..4ffe4dc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,15 @@ version: 2 - +build: + os: ubuntu-24.04 + tools: + python: '3.12' sphinx: configuration: module/docs/source/conf.py - + fail_on_warning: true formats: - pdf - python: - version: 3.7 install: + - method: pip + path: ./module - requirements: module/docs/requirements.txt diff --git a/BACKLOG.rst b/BACKLOG.rst new file mode 100644 index 0000000..92404c1 --- /dev/null +++ b/BACKLOG.rst @@ -0,0 +1,141 @@ +Backlog resolution +=================== + +This record covers the original 28 issues and three dependency PRs. Implementation +and decisions are in `PR #129 `_. +The code work is complete; four issues retain external completion steps below. +Version 0.4 changes numerical results: recompute cached 0.3.x features and read the +numerical migration guide. No clinical validation is implied by these tests. + +Completed work +-------------- + +.. list-table:: + :header-rows: 1 + :widths: 12 88 + + * - Issue + - Resolution and evidence + * - #11 + - 110 Python tests cover validation, geometry, independent numerical values, + CLI, examples, and sdist-to-wheel installation. CI tests Python 3.10–3.13; + eight additional tests run in native Slicer 5.12.4. + * - #24 + - README and CONTRIBUTING document virtual environments and checkout installs. + * - #43 + - Both Docker targets include the CLI and SimpleITK. Container CI runs an + actual image/mask extraction and checks 52 finite summary values. + * - #52 + - The deterministic notebook uses inline Matplotlib and executes headlessly. + * - #53 + - Typed helpers reject invalid array types, dimensions, shape, nonfinite + values, empty masks, invalid enums, and malformed numerical parameters. + Boundary and small-mask cases have regression tests. + * - #54 + - CONTRIBUTING specifies Python syntax, formatting, logging, types, and + numerical-test expectations. Ruff checks run in CI. + * - #55 + - CONTRIBUTING documents setup, tests, Slicer testing, documentation, and PRs. + * - #59 + - Core and I/O paths use TypeError, ValueError, RuntimeError, and I/O-specific + failures. Tests check exception types and useful messages. + * - #60 + - Public and internal packages are annotated, ship py.typed, and pass mypy + across nine source files. CI checks types. + * - #62 + - Validation, orientation, texture mapping, I/O, statistics, progress, and CLI + live in the internal _collageradiomics package. + * - #63 + - Keep major.minor.patch versioning. Numerical/API changes before 1.0 require + a minor version and migration notes; dates do not convey compatibility. + The maintenance guide records this decision. + * - #68 + - Container CI builds and tests CLI/notebook targets. GHCR publication is an + explicit manual input after successful builds. + * - #69 + - Sphinx reads module/VERSION; Read the Docs installs the checkout. The docs + build passes with warnings treated as errors. + * - #77 + - The library uses the collageradiomics logger and leaves root configuration + to callers. The legacy verbose_logging parameter is accepted and ignored. + * - #80 + - The sdist includes VERSION, requirements, the DICOM sample, and its script. + Distribution tests rebuild and run outside the checkout. The example + locates its sample relative to its own file. + * - #81 + - Keep shared helpers in this repository. There is no independent external + consumer requiring a separate common-code repository. The maintenance + guide records this decision. + * - #84 + - NIfTI I/O validates geometry and converts axes consistently. Core geometry + tests cover thin volumes and all masked slices; both angles remain finite. + * - #85 + - Retain maintained numerical dependencies. Use NumPy rolling views, remove + unused core dependencies, and keep GUI/I/O/example packages optional. The + maintenance guide explains the decision against vendoring upstream code. + * - #86 + - The CLI supports single/batch cases, label selection, all extraction + parameters, consistent summaries, relative paths, and per-case error CSVs. + Twenty CLI tests and container extraction cover its behavior. + * - #90 + - Slicer creates separate theta (0) and phi (1) output volumes. Native tests + select both angles and two features and verify all four outputs. + * - #91 + - Throttled progress callbacks and INFO logging report stages; Slicer shows + cancellable progress and removes temporary/incomplete nodes on cancellation. + * - #97 + - Every masked 3D slice and both angles are calculated, including constant + images and small/edge ROIs. Geometry/numerical tests require finite masked + values. The original privately shared volume was unavailable for testing. + * - #121 + - The original attached 512x512 image and mask now complete in about 30 seconds: + all 92,918 masked pixels have finite thirteen-feature outputs, and background + pixels remain NaN. Small boundary regressions preserve the shape fix. + * - #124 + - Slicer accepts labelmaps directly and exports visible segmentations into + the input geometry. Native tests cover both inputs and geometry/transform + preservation. + +External completion steps +------------------------- + +These issues remain open so repository changes are not confused with third-party +acceptance or account ownership. + +.. list-table:: + :header-rows: 1 + :widths: 12 88 + + * - Issue + - Prepared work and remaining action + * - #25 + - environment.yml and a native-tested local Conda recipe support checkout + development and local packages. A separate conda-forge v1 recipe uses an + official commit archive and verified SHA256, builds with rattler-build, and + passes installed-package tests. `Staged-recipes PR #35264 + `_ remains in draft + for release-source substitution, maintainer confirmation, and acceptance. + * - #73 + - `MRHub PR #119 `_ adds a validated + entry and original logo. MRHub maintainers must accept the listing. + * - #78 + - The release guide specifies PyPI ownership, trusted publishers for both + packages, and Read the Docs roles. A service owner must verify/add the + maintainers and configure those accounts. Legacy GitHub secret names do + not establish service ownership or working publication credentials. + * - #83 + - `Slicer index PR #2424 `_ + supplies catalog metadata. The extension retries dependency imports after + an explicit Install action and performs no package installation at startup. + The source changes must merge, core 0.4 must reach PyPI, and Slicer + maintainers must accept/build the catalog entry. + +Dependency PR disposition +------------------------- + +PRs #123 (SciPy 1.10.0), #125 (Pygments 2.15.0), and #126 (Tornado 6.3.3) are +superseded by the coordinated refresh in #129. The final Python 3.11 lock uses +SciPy 1.17.1, Pygments 2.21.0, and Tornado 6.5.10. Applying the old lockfile diffs +would conflict and restore unrelated outdated versions. Close them after #129 +merges. The refreshed lock passes pip-audit with no known vulnerabilities as of +2026-10-20, including a fixed pytest version. diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..e5dc4c8 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,29 @@ +Changelog +========= + +0.4.0 (unreleased) +------------------ + +Recompute cached features from 0.3.x. This release corrects principal-gradient +selection, SVD signs, centered texture windows, image/mask boundary handling, +3D end slices, the public Haralick feature mapping, maximal correlation, and +difference-variance selection. The thirteen feature names, public import, and +output axis order remain compatible. See the numerical migration guide. + +* Require Python 3.10 or newer and use current numerical dependencies. Image I/O, + plotting, DICOM, and notebook dependencies are optional extras. +* Validate arrays and parameters, including empty masks and nonfinite inputs. + Small and edge-touching masks are supported. Expose throttled progress and use + standard logging. +* Ship a working single/batch CLI with consistent summary statistics, mask label + selection, physical-geometry checks, and per-case error reporting. Explicit 2D + mode rejects multislice images. Constant-feature moments are finite. +* Repair the Slicer extension's labelmaps, segmentations, theta/phi outputs, + parameter forwarding, cancellation, geometry, and temporary-node cleanup. +* Add numerical, CLI, distribution, example, and native Slicer tests; test the + minimum Python 3.10 stack and current Python 3.11–3.13 stacks in CI. +* Replace external-viewer examples with a reproducible inline notebook. Fix + derived DICOM serialization and the IBSI example's feature indexing. +* Add typed shared helpers, contributor/style guides, automatic documentation + versions, CLI/notebook Docker builds, a Conda environment and package recipe, + and validated release artifacts with PyPI trusted publishing. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f97e32f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.13.4) +project(CollageRadiomics) + +# Extension metadata is kept at the repository root for catalog validation. +set(EXTENSION_HOMEPAGE "https://github.com/radxtools/collageradiomics/tree/master/slicer/collageradiomics") +set(EXTENSION_CATEGORY "Informatics") +set(EXTENSION_CONTRIBUTORS "Pallavi Tiwari (BriC), Satish Viswanath (BriC), Rob Toth (Toth Technology), Nathan Hillyer (Toth Technology)") +set(EXTENSION_DESCRIPTION "Voxel-wise CoLlAGe radiomics from local gradient orientations, with labelmap and segmentation inputs and thirteen texture features.") +set(EXTENSION_ICONURL "https://raw.githubusercontent.com/radxtools/collageradiomics/master/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/Icons/CollageRadiomicsSlicer.png") +set(EXTENSION_SCREENSHOTURLS "https://raw.githubusercontent.com/radxtools/collageradiomics/master/slicer/collageradiomics/CollageRadiomicsSlicer/Screenshot.jpg") +set(EXTENSION_DEPENDS "NA") + +find_package(Slicer REQUIRED) +include(${Slicer_USE_FILE}) +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/slicer/collageradiomics/CollageRadiomicsSlicer" CollageRadiomicsSlicer) +include(${Slicer_EXTENSION_GENERATE_CONFIG}) +include(${Slicer_EXTENSION_CPACK}) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..b1c19ae --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,53 @@ +Contributing +============ + +Work in a virtual environment from the repository root:: + + python3 -m venv env_test + source env_test/bin/activate + python -m pip install -r requirements-test.txt + python -m pip install --no-build-isolation -e './module[cli]' + python -m pytest + python -m mypy + python -m ruff check module/collageradiomics module/_collageradiomics tests cli + python -m ruff format --check module/collageradiomics module/_collageradiomics tests cli + +Add ``-c constraints-test.txt`` to both install commands to reproduce the minimum +stack on Python 3.10. CI also tests current dependencies on 3.11, 3.12, and 3.13. +Pipenv users can run ``pipenv sync --dev`` using the checked-in Python 3.11 lock. + +Code style +---------- + +Use Python 3.10 syntax, four spaces, descriptive names, and Ruff's 100-column +formatter. Keep the public import and feature order stable. Annotate the core and +shared helpers, then run mypy. Give errors specific types and useful parameter +names. Use the ``collageradiomics`` logger; library code does not configure root +logging or print progress. Keep shared calculations in ``_collageradiomics`` so +Python, CLI, examples, and Slicer use the same behavior. Retain upstream numerical +dependencies rather than copying their implementations into the repository. + +Tests and changes +----------------- + +Reproduce a reported problem with a small deterministic fixture. Numerical tests +should include independent expected values, shapes, mask placement, and NaN +behavior. Test batch and single-case CSV output together. A strict ``xfail`` is +appropriate only while a confirmed bug is being fixed; remove it with the fix. +Distribution tests must rebuild a wheel from its sdist and run outside the checkout. +Slicer changes must pass ``tests/run_slicer.py`` in the application, including GUI +parameter forwarding and temporary-node cleanup. Containers build in GitHub CI. + +Use a short imperative commit subject. Explain changed numerical behavior and +validation in the PR. Prefer focused commits and include migration notes for +changes that affect cached features. Do not mix package publishing with tests. + +Documentation +------------- + +Install the documentation tools and build with warnings treated as errors:: + + python -m pip install -r module/docs/requirements.txt + python -m sphinx -W -b html module/docs/source /tmp/collage-docs + +See the maintenance guide for versioning and release responsibilities. diff --git a/Pipfile b/Pipfile index bb298e8..08e88c7 100644 --- a/Pipfile +++ b/Pipfile @@ -4,15 +4,13 @@ verify_ssl = true name = "pypi" [packages] -numpy = "*" -scikit-build = "*" -scikit-image = "*" -mahotas = "*" -scipy = "*" -jupyter = "*" -SimpleITK = "*" +collageradiomics = {path = "./module", editable = true, extras = ["examples", "cli"]} [dev-packages] +pytest = ">=9.0.3,<10" +build = ">=1.2,<2" +mypy = ">=1.13,<2" +ruff = ">=0.11,<1" [requires] -python_version = "3.8" +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index aa1d176..7652044 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "c9cc98dcbe8bef997326424a95f41a79c60724f7ac6da494e92a3322d0dc375c" + "sha256": "83069d400dca479329804f1e64ecb8579c8a3fa7ee91556df84fa42cb2da8968" }, "pipfile-spec": 6, "requires": { - "python_version": "3.8" + "python_version": "3.11" }, "sources": [ { @@ -18,1355 +18,2617 @@ "default": { "anyio": { "hashes": [ - "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421", - "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3" + "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", + "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94" ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.2" + "markers": "python_version >= '3.10'", + "version": "==4.15.1" }, "argon2-cffi": { "hashes": [ - "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", - "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b" + "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", + "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741" ], - "markers": "python_version >= '3.6'", - "version": "==21.3.0" + "markers": "python_version >= '3.8'", + "version": "==25.1.0" }, "argon2-cffi-bindings": { "hashes": [ - "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", - "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f", - "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", - "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194", - "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", - "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", - "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", - "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5", - "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", - "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7", - "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", - "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", - "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", - "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", - "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", - "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", - "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", - "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", - "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", - "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", - "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351" + "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", + "sha256:0cc40f7b4050bb93eb67de95d2d759322fc7ce4930b9d645581ecf4913ec651e", + "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", + "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", + "sha256:19b562b1de4b9052ef1214a2821c44b6e6f22945daa102c32ae4eff929d8b6d8", + "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", + "sha256:1af817e84578ef8b7295ad17de0f9896e4c8520dbf2233c7aa5aa3d487256fc4", + "sha256:1b0bcac4d490a237e18cf91f57352920c29f77f2fa39efd0813fb81298bf17ba", + "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", + "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", + "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", + "sha256:242bb0cda2ae3650764fc194593d9ea45fc9e72729acd89778c7cfe184cec2a5", + "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", + "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", + "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", + "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", + "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", + "sha256:49d525938467d52c923a890153c99087c9d5a937d1f6b585dbdba34ec82e397a", + "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", + "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", + "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", + "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", + "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", + "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", + "sha256:7014ab7e6f5d8511af92544667a0346ea6dfc314ea9a7cad1dba9fdb5c9a6e33", + "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", + "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", + "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", + "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", + "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", + "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", + "sha256:b70225b5fd1e0d2ef4f7fd30d24658454535f0924dff0caca5dc08efbbbadfbb", + "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", + "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", + "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", + "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", + "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", + "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", + "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", + "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", + "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4" + ], + "markers": "python_version >= '3.10'", + "version": "==26.1.0" + }, + "arrow": { + "hashes": [ + "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", + "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7" ], - "markers": "python_version >= '3.6'", - "version": "==21.2.0" + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "asttokens": { "hashes": [ - "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3", - "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c" + "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", + "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.2" + }, + "async-lru": { + "hashes": [ + "sha256:1b46a96419ef91fe11431d58db98d792c9b4250001427191a40faf1031934212", + "sha256:b0b0077d9b7596615b3c12d0aad4f6aeccaaa0ac75ceae472608ede89e764c08" ], - "version": "==2.2.1" + "markers": "python_version >= '3.10'", + "version": "==2.4.0" }, "attrs": { "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" + "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", + "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" + "markers": "python_version >= '3.9'", + "version": "==26.1.0" }, - "backcall": { + "babel": { "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", + "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" ], - "version": "==0.2.0" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "beautifulsoup4": { "hashes": [ - "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39", - "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106" + "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", + "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9" ], - "markers": "python_version >= '3.6'", - "version": "==4.11.2" + "markers": "python_full_version >= '3.7.0'", + "version": "==4.15.0" }, "bleach": { + "extras": [ + "css" + ], + "hashes": [ + "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", + "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081" + ], + "markers": "python_version >= '3.10'", + "version": "==6.4.0" + }, + "certifi": { "hashes": [ - "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", - "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" + "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", + "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b" ], "markers": "python_version >= '3.7'", - "version": "==6.0.0" + "version": "==2026.10.19" }, "cffi": { "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" + "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", + "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", + "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", + "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", + "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", + "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", + "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", + "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", + "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", + "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", + "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", + "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", + "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", + "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", + "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", + "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", + "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", + "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", + "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", + "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", + "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", + "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", + "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", + "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", + "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", + "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", + "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", + "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", + "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", + "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", + "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", + "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", + "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", + "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", + "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", + "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", + "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", + "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", + "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", + "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", + "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", + "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", + "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", + "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", + "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", + "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", + "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", + "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", + "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", + "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", + "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", + "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", + "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", + "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", + "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", + "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", + "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", + "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", + "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", + "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", + "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", + "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", + "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", + "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", + "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", + "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", + "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", + "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", + "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", + "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", + "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", + "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", + "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", + "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", + "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", + "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", + "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", + "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", + "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", + "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", + "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", + "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", + "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", + "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", + "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", + "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", + "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", + "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", + "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", + "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", + "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", + "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", + "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", + "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", + "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", + "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", + "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", + "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", + "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", + "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264" + ], + "markers": "python_version >= '3.10'", + "version": "==2.1.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", + "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", + "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", + "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", + "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", + "sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848", + "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", + "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", + "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640", + "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", + "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", + "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", + "sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275", + "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", + "sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787", + "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", + "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", + "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", + "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3", + "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", + "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", + "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", + "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", + "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", + "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f", + "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", + "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1", + "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d", + "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", + "sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320", + "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", + "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", + "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", + "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", + "sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc", + "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", + "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", + "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac", + "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", + "sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c", + "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", + "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", + "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", + "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", + "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", + "sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa", + "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6", + "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", + "sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2", + "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", + "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", + "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", + "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", + "sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9", + "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c", + "sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c", + "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab", + "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", + "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", + "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", + "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", + "sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be", + "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", + "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", + "sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2", + "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", + "sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a", + "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", + "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", + "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", + "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37", + "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", + "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", + "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", + "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", + "sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e", + "sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5", + "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", + "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", + "sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0", + "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", + "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", + "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", + "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", + "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", + "sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac", + "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", + "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", + "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", + "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", + "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", + "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", + "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", + "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", + "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", + "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", + "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", + "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", + "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96", + "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", + "sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249", + "sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4", + "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", + "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", + "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", + "sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03", + "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e", + "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", + "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", + "sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849", + "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", + "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a", + "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", + "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", + "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", + "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", + "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", + "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", + "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", + "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", + "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", + "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", + "sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45", + "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", + "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", + "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", + "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", + "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400", + "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", + "sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c", + "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", + "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", + "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74", + "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", + "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", + "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", + "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", + "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", + "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", + "sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a", + "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", + "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5", + "sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f", + "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", + "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", + "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", + "sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5", + "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", + "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", + "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", + "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", + "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", + "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", + "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", + "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", + "sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50", + "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", + "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", + "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", + "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", + "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288", + "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", + "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", + "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", + "sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4", + "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", + "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", + "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd", + "sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341", + "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", + "sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d", + "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f" + ], + "markers": "python_version >= '3.7'", + "version": "==3.5.2" }, - "comm": { + "click": { "hashes": [ - "sha256:3e2f5826578e683999b93716285b3b1f344f157bf75fa9ce0a797564e742f062", - "sha256:9f3abf3515112fa7c55a42a6a5ab358735c9dccc8b5910a9d8e3ef5998130666" + "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", + "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34" ], - "markers": "python_version >= '3.6'", - "version": "==0.1.2" + "markers": "python_version >= '3.10'", + "version": "==8.5.0" }, - "contourpy": { + "collageradiomics": { + "editable": true, + "extras": [ + "cli", + "examples" + ], + "path": "./module" + }, + "comm": { "hashes": [ - "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98", - "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772", - "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2", - "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc", - "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803", - "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051", - "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc", - "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4", - "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436", - "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5", - "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5", - "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3", - "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80", - "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1", - "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0", - "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae", - "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556", - "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02", - "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566", - "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350", - "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967", - "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4", - "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66", - "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69", - "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd", - "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2", - "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810", - "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50", - "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc", - "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2", - "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0", - "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3", - "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6", - "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac", - "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d", - "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6", - "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f", - "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd", - "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566", - "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa", - "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414", - "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a", - "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c", - "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693", - "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d", - "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161", - "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e", - "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2", - "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f", - "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71", - "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd", - "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9", - "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8", - "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab", - "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad" + "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", + "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417" ], "markers": "python_version >= '3.8'", - "version": "==1.0.7" + "version": "==0.2.3" }, - "cycler": { + "contourpy": { "hashes": [ - "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3", - "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f" - ], - "markers": "python_version >= '3.6'", - "version": "==0.11.0" + "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", + "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", + "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", + "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", + "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", + "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", + "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", + "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", + "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", + "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", + "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", + "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", + "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", + "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", + "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", + "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", + "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", + "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", + "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", + "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", + "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", + "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", + "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", + "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", + "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", + "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", + "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", + "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", + "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", + "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", + "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", + "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", + "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", + "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", + "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", + "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", + "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", + "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", + "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", + "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", + "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", + "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", + "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", + "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", + "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", + "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", + "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", + "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", + "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", + "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", + "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", + "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", + "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", + "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", + "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", + "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", + "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", + "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", + "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", + "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", + "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", + "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", + "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", + "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", + "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", + "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", + "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", + "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", + "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", + "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", + "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", + "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a" + ], + "markers": "python_version >= '3.11'", + "version": "==1.3.3" }, - "debugpy": { + "cycler": { "hashes": [ - "sha256:0ea1011e94416e90fb3598cc3ef5e08b0a4dd6ce6b9b33ccd436c1dffc8cd664", - "sha256:23363e6d2a04d726bbc1400bd4e9898d54419b36b2cdf7020e3e215e1dcd0f8e", - "sha256:23c29e40e39ad7d869d408ded414f6d46d82f8a93b5857ac3ac1e915893139ca", - "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe", - "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32", - "sha256:72687b62a54d9d9e3fb85e7a37ea67f0e803aaa31be700e61d2f3742a5683917", - "sha256:78739f77c58048ec006e2b3eb2e0cd5a06d5f48c915e2fc7911a337354508110", - "sha256:7aa7e103610e5867d19a7d069e02e72eb2b3045b124d051cfd1538f1d8832d1b", - "sha256:87755e173fcf2ec45f584bb9d61aa7686bb665d861b81faa366d59808bbd3494", - "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c", - "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6", - "sha256:b9c2130e1c632540fbf9c2c88341493797ddf58016e7cba02e311de9b0a96b67", - "sha256:be596b44448aac14eb3614248c91586e2bc1728e020e82ef3197189aae556115", - "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225", - "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1", - "sha256:dff595686178b0e75580c24d316aa45a8f4d56e2418063865c114eef651a982e", - "sha256:f6383c29e796203a0bba74a250615ad262c4279d398e89d895a69d3069498305" + "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", + "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c" ], - "markers": "python_version >= '3.7'", - "version": "==1.6.6" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, - "decorator": { + "debugpy": { "hashes": [ - "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", - "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" - ], - "markers": "python_version >= '3.5'", - "version": "==5.1.1" + "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9", + "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b", + "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35", + "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c", + "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3", + "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0", + "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3", + "sha256:3b7c328cb47b4e2f2b40801936daf8150f88dcb4ffef5080ff119270f0d23626", + "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0", + "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f", + "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c", + "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a", + "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee", + "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb", + "sha256:92fc425308a08f601f3c69afb4719767f9b898276f44b97e6942a713c4c32740", + "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947", + "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07", + "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a", + "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5", + "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337", + "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39", + "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54", + "sha256:cba7b99573ee41f9510c6deeff1cdd0c333748b9dd63d5016413dd57ba9a4593", + "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e", + "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e", + "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d", + "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2", + "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735", + "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294", + "sha256:ffeeaf4dfe1534375902040df32a4c48f721fd35a4d4d337ac72a44d4176075b" + ], + "markers": "python_version >= '3.10'", + "version": "==1.8.22" }, "defusedxml": { "hashes": [ "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3' and python_version != '3.4'", "version": "==0.7.1" }, - "distro": { + "executing": { "hashes": [ - "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8", - "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff" + "sha256:15919cb5d667e5cb4e099511971d00d659573fff2dd5c4e6cd8b71636c7858d2", + "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122" ], - "markers": "python_version >= '3.6'", - "version": "==1.8.0" + "markers": "python_version >= '3.8'", + "version": "==2.3.0" }, - "entrypoints": { + "fastjsonschema": { "hashes": [ - "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", - "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f" + "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", + "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf" ], - "markers": "python_version >= '3.6'", - "version": "==0.4" + "markers": "python_version >= '3.10'", + "version": "==2.22.2" }, - "executing": { + "fonttools": { "hashes": [ - "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc", - "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107" + "sha256:0781fe22583529e1e98bb8a3a33040632e202a4c427ed7e65412c41a21b8ebcb", + "sha256:07a2f36b3263faadf5b7b548f62fd3cac401e490189c82b16f7139ac0df91cd4", + "sha256:13d7507252c5a5d7941a5fa1be27d335c378ef07983ea2bb24988bf600eadd5e", + "sha256:1671e5f368b0c136ed9fb62fef26c7e425b4ebb0bb669a1cb7ba453f5bba580b", + "sha256:1be99c1f07fca59510d657ef3eae584b5273fa4e203aff2383b3520744e19536", + "sha256:1f200cd2cf046a5a0b03babe84ebf8bbc12187d5d57f50bc03f24be89e7c1605", + "sha256:2a09d33a9264a6b29efca9dc633b53969aaedb250a9c8521d60f51280cef65ca", + "sha256:2bfab2f5d1d255dec82f4bd082a1c10e77df808e42210890f50a9c30bf91570e", + "sha256:33ae23a531795864fcdbbab91a40c824976e22642c05efca3bd8a0b00630d0e7", + "sha256:36f0fee56227b909c9d1392f17b23803616f1f04efbe020c176d9945cabc0be5", + "sha256:38fc772182ebff3e2ebba7886460476eb65842b601ca0b9221a6a5826136396e", + "sha256:3a19f6d5e1a373f2e4a5bdb9452c8ba212dd9f1e43df2fff042b896e28084e4a", + "sha256:3b34324deb3e09ad648039a0a86d945b83f23a44fe3da74a84e6ada71fe0b650", + "sha256:3cb57e6600ca77c0b1729cf8adc23bc0652633a37f18cfa934d9c7bc3de25519", + "sha256:3fb95166eaebad72f9deb1d0d781f652525f47e4693e553dad3954cf68ed6e9c", + "sha256:4304f03ed7f4ba000a8dcc941ad854bfa52e2f3b6112b8f099b6f431cf98e701", + "sha256:451077d2fc61a2a03f5dca54d84fbb01051ad781f48ea137eff35c775a4cb025", + "sha256:47dba566b4f475b0fb5f83129487c21b6a6a4edc41c0eec52524f969a68a3d45", + "sha256:48696b630069e29b8aa5ea8b034e4f651a2e112073938ec16bd536dadde1debf", + "sha256:4e2c1586b5b6588a47d02e2588170eefdc996b708f2659c44dbe169bd6fcacb5", + "sha256:50c41e30aa2e0130b80d1a58ac0f3ea7c02a854a70dbea1ff8d88e0ce524806f", + "sha256:5377e0e991e3e2be47fd1215414b20c2288b546e5a8c6d80b1a7cde9c72a89e1", + "sha256:592d8f72024dea0408739a92599e4f839b960e1e887b25adc76dc87271fdac76", + "sha256:59f44309ce78851c9621ee88e3f667ca3fbcc89dc0e8641336be3f12ba06bfd4", + "sha256:5ad690ea5bfd8913d1a6e5d5e9825ccf4ed342716e63c2b0d7f490d50235daef", + "sha256:5ccaa87b312219d02cf72a79f1eb2f3ce028882d6fd1b79336141005db83b84e", + "sha256:621b3152b5d0412381b792bacfe410ac1f09c2c4f28a44bd19d26fe7160cfc96", + "sha256:64e56d0d6a39780fee86955c758674538387b18f911ea904a4aae8f8e30fa26f", + "sha256:690ab72d338aa9bf8e5cd9aefb86e0d3c458d8b9de4df041fb7dc2ed4703144e", + "sha256:6c19a770a8d273371a37969003c143eaa629ab893c3db028af8b91d04c6f9a6d", + "sha256:720bcf27727193b0fe1883c2e036dc88e37047916e977f5c3daf6ee4316e9656", + "sha256:72d6d316dffc92eadb771f697f289ea7b60f689580931328905a267bd170f93b", + "sha256:7343cd0ef70edf8be7f4913cb9b55b992fb4e04055b47dcfecddcc2eb045a9d2", + "sha256:768a33bbe6ec5ba8f19979f938752f06d4e614cb554fd47abd7830f2007660e3", + "sha256:775364ac079e2ea7a2eedb5f9172c57b059d638ff79e2bf8d4257e5805713f32", + "sha256:77e0d4096a2ac60aebe43928b5382766df2d148577db8e8ff79b6a50879a6c06", + "sha256:8239e2ca24878715a19f061d065b5721e87da81d145e48b3418f771a469b5a24", + "sha256:846982e89b1861d6c9d7fcd6567aec3fa5a10ad313e7f2076045fcd339cfbd8e", + "sha256:84a3aed005de106fb1794372dace82eca50859d52ae26da4bb6c602480a41250", + "sha256:89ad62d116f45bb45873bb92fd69c14a720ba591cba488044731954a5565e194", + "sha256:8c21073cfe7129aaa070d94f575c1e2a880ae4aae1dcffd5352f174b96d27d16", + "sha256:8c58a8a9ad447bead6f91e5f50b23c0e4988538cdbd9bf2f68952b39f5900a84", + "sha256:916836845e4b1c1447bb61390ffb3cb5f2940fd9f5d6de4685539a81806c7764", + "sha256:952eb091689545d86d16e40f719ed7bb086dd810a07dcc9ea2ca0a81004810a3", + "sha256:9c38fece8156cbda31b42d49c4a187858056a35932b88233b6fb31eaca5cf67f", + "sha256:ad813967410ba6d24a52850df59b164ee17883f17b96a91b4b0ac6e9d7b5a118", + "sha256:ad8b4f7c754a627e91908fa1a1ccc90b489cd2810c0ba16acd26ea2ff5273db7", + "sha256:b274ed3106b8086f237b7dbb1529c28142ba10ae40b9d285be0ae6a44b2946d0", + "sha256:b3ddf350e74508102b33dc6b32984b6dd751359a7c57732bcd39f9d7cb37d71e", + "sha256:bd3239e5709fd4c3343db67245ede46aece610d7f7ef61afb174718122479282", + "sha256:e0ca4c8438dd6320f5850c9bbee3b3980455ee3bac602a9a0299caf9e799a0e8", + "sha256:e2b5d511ea012dce7bd6df12b279b7d7a5b01b019865717d03ae679f4b944fa5", + "sha256:e8a8545cbd58bd29494ffe81e3cb35f8a29332a8e495c42bec334145ce8cd65b", + "sha256:eb3c98cac93aac4b9f6e3ce2008325340b234cc9b0338ca6b513f31962a1e278", + "sha256:f672398385849ff79e7dd50c0a06efe110c8ba23d8890f9b45fbb922bc2f55f6", + "sha256:fc6b6b03aa44f504c8734e62ccc3e4dcda9f4b8213a85aa80742e4d1cc9d96ef", + "sha256:fcb9743140419410161acfe7ec205fb0a8a703acfccb85b586becb5a97c047c9", + "sha256:fd79e36c2968e9fc3e1b082f2ba7dc63ae88a161a3d8ceaa0746b906455f3617" + ], + "markers": "python_version >= '3.11'", + "version": "==4.67.0" + }, + "fqdn": { + "hashes": [ + "sha256:6c793c312ccfa29981e1135b3ef5e1277ebe639731a6b0f7350d90e96e63f0b4", + "sha256:e39bf62e2a9481aa2cb780554fdfd944dd8ee5f230eb831684d539cbafed8de2" + ], + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3' and python_version != '3.4' and python_version < '4'", + "version": "==1.6.0" + }, + "h11": { + "hashes": [ + "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", + "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" ], - "version": "==1.2.0" + "markers": "python_version >= '3.8'", + "version": "==0.16.0" }, - "fastjsonschema": { + "httpcore": { "hashes": [ - "sha256:01e366f25d9047816fe3d288cbfc3e10541daf0af2044763f3d0ade42476da18", - "sha256:21f918e8d9a1a4ba9c22e09574ba72267a6762d47822db9add95f6454e51cc1c" + "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", + "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" ], - "version": "==2.16.2" + "markers": "python_version >= '3.8'", + "version": "==1.0.9" }, - "fonttools": { + "httpx": { "hashes": [ - "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1", - "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb" + "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", + "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" ], - "markers": "python_version >= '3.7'", - "version": "==4.38.0" + "markers": "python_version >= '3.8'", + "version": "==0.28.1" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", + "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c" ], - "markers": "python_version >= '3.5'", - "version": "==3.4" + "markers": "python_version >= '3.9'", + "version": "==3.20" }, "imageio": { "hashes": [ - "sha256:5bce7f88eef7ee4e9aac798d3b218fea2e98cbbaa59a3e37b730a7aa5784eeac", - "sha256:6021d42debd2187e9c781e494a49a30eba002fbac1eef43f491bbc731e7a6d2b" + "sha256:36d23eb7423d2fb63f637098758edb3d2df3687f7125e0a5ce8596572022dcf9", + "sha256:6769f1f01c4dd46448307863a787c9a22fa4dbe11c0c88f525c6e410fdfd7983" ], - "markers": "python_version >= '3.7'", - "version": "==2.25.1" + "markers": "python_version >= '3.11'", + "version": "==2.38.1" }, - "importlib-metadata": { + "ipykernel": { "hashes": [ - "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", - "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" + "sha256:4330114d22b9b33575b2c7c68753fc8faabbbcfbc60b0cdff9f14dd4ce63742f", + "sha256:a6757f790ddc5a6006b813d82da1a9dbb65b3a00f2b6607df776418c6566ec1c" ], - "markers": "python_version < '3.10'", - "version": "==6.0.0" + "markers": "python_version >= '3.11'", + "version": "==7.4.0" }, - "importlib-resources": { + "ipython": { "hashes": [ - "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6", - "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484" + "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", + "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529" ], - "markers": "python_version < '3.9'", - "version": "==5.10.2" + "markers": "python_version >= '3.11'", + "version": "==9.17.1" }, - "ipykernel": { + "ipython-pygments-lexers": { "hashes": [ - "sha256:1893c5b847033cd7a58f6843b04a9349ffb1031bc6588401cadc9adb58da428e", - "sha256:5d0675d5f48bf6a95fd517d7b70bcb3b2c5631b2069949b5c2d6e1d7477fb5a0" + "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", + "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c" ], "markers": "python_version >= '3.8'", - "version": "==6.20.2" + "version": "==1.1.1" }, - "ipython": { + "isoduration": { "hashes": [ - "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e", - "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345" + "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", + "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042" ], - "markers": "python_version >= '3.8'", - "version": "==8.10.0" + "markers": "python_version >= '3.7'", + "version": "==20.11.0" }, - "ipython-genutils": { + "jedi": { "hashes": [ - "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", - "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + "sha256:0fb16d86c4a4c73c37ba518c77419975e30fcc620658a8d14fbb5720cdd34142", + "sha256:2f71208c3f9c1bca057c0e90d3f272aba44ace88fc4067d7587e9e069331b7e5" ], - "version": "==0.2.0" + "markers": "python_version >= '3.10'", + "version": "==0.20.1" }, - "ipywidgets": { + "jinja2": { "hashes": [ - "sha256:c0005a77a47d77889cafed892b58e33b4a2a96712154404c6548ec22272811ea", - "sha256:ebb195e743b16c3947fe8827190fb87b4d00979c0fbf685afe4d2c4927059fa1" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==8.0.4" + "version": "==3.1.6" }, - "jedi": { + "json5": { "hashes": [ - "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e", - "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612" + "sha256:2c8b22a893c35cd6a3c5ccbf1dd1c7d02c25dc1b5897fea658f9bf08c2e05f9a", + "sha256:8d0278ad34ebaa9c3af76d9519274811830224a2d1cd5af156dbd067f461b8a4" ], - "markers": "python_version >= '3.6'", - "version": "==0.18.2" + "markers": "python_full_version >= '3.8.0'", + "version": "==0.17.3" }, - "jinja2": { + "jsonpointer": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:136e2e3273fd82a1502274f56ec172f331b6a1703d86a1824d503af4d9a96142", + "sha256:e57015ae65af3821c6a3976069860789cf326103f38b22b2178d41d021adad25" ], - "markers": "python_version >= '3.7'", - "version": "==3.1.2" + "markers": "python_version >= '3.10'", + "version": "==3.4" }, "jsonschema": { + "extras": [ + "format-nongpl" + ], "hashes": [ - "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", - "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", + "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" ], - "markers": "python_version >= '3.7'", - "version": "==4.17.3" + "markers": "python_version >= '3.10'", + "version": "==4.26.0" }, - "jupyter": { + "jsonschema-specifications": { "hashes": [ - "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7", - "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78", - "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f" + "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", + "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" ], - "index": "pypi", - "version": "==1.0.0" + "markers": "python_version >= '3.9'", + "version": "==2025.9.1" }, - "jupyter-client": { + "jupyter-builder": { "hashes": [ - "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7", - "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392" + "sha256:01aba6794eb9b19e0e29ae21137ca60ba4135c70347d4b9f664a586822b8c809", + "sha256:c5ea5a7190c2a7b082494abade98eece1b2b5bd5dbd7d610606cbcd10a1d08b3" ], - "markers": "python_version >= '3.7'", - "version": "==7.4.9" + "markers": "python_version >= '3.10'", + "version": "==1.2.3" }, - "jupyter-console": { + "jupyter-client": { "hashes": [ - "sha256:172f5335e31d600df61613a97b7f0352f2c8250bbd1092ef2d658f77249f89fb", - "sha256:756df7f4f60c986e7bc0172e4493d3830a7e6e75c08750bbe59c0a5403ad6dee" + "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", + "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497" ], - "markers": "python_version >= '3.7'", - "version": "==6.4.4" + "markers": "python_version >= '3.10'", + "version": "==8.10.0" }, "jupyter-core": { "hashes": [ - "sha256:3815e80ec5272c0c19aad087a0d2775df2852cfca8f5a17069e99c9350cecff8", - "sha256:c2909b9bc7dca75560a6c5ae78c34fd305ede31cd864da3c0d0bb2ed89aa9337" + "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", + "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407" ], - "index": "pypi", - "version": "==4.11.2" + "markers": "python_version >= '3.10'", + "version": "==5.9.1" + }, + "jupyter-events": { + "hashes": [ + "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", + "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3" + ], + "markers": "python_version >= '3.9'", + "version": "==0.12.1" + }, + "jupyter-lsp": { + "hashes": [ + "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", + "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.1" }, "jupyter-server": { "hashes": [ - "sha256:4ee4f311bd944bcf8060a8b746059571c40f6b8ada1d1e6e51239d26ab23b15c", - "sha256:aa3398aeb5249d470ea53abcf81fca8a6876bb9dbdc652822e5bbbb0574a6e83" + "sha256:2a6467606af7dbae2e7e31640030025969e15db6a649eae334af90415dc71dca", + "sha256:a8960aa29263f6041283e97d4756b099fb49b767baab6371891ecb1bd40a63df" ], - "markers": "python_version >= '3.7'", - "version": "==1.23.4" + "markers": "python_version >= '3.10'", + "version": "==2.21.1" + }, + "jupyter-server-terminals": { + "hashes": [ + "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", + "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5" + ], + "markers": "python_version >= '3.8'", + "version": "==0.5.4" + }, + "jupyterlab": { + "hashes": [ + "sha256:15b13f991d3985129c797eb84d9949eeb8b6615e14b444868e642411f2c418b2", + "sha256:404f49b081819378524886c9db66dba57a5565981eff885830df1baba3a17df5" + ], + "markers": "python_version >= '3.10'", + "version": "==4.6.4" }, "jupyterlab-pygments": { "hashes": [ - "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f", - "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d" + "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", + "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780" ], - "markers": "python_version >= '3.7'", - "version": "==0.2.2" + "markers": "python_version >= '3.8'", + "version": "==0.3.0" }, - "jupyterlab-widgets": { + "jupyterlab-server": { "hashes": [ - "sha256:a04a42e50231b355b7087e16a818f541e53589f7647144ea0344c4bf16f300e5", - "sha256:eeaecdeaf6c03afc960ddae201ced88d5979b4ca9c3891bcb8f6631af705f5ef" + "sha256:0c3c2418d51021ce280916e63dfe4cba8386b4e2787be5c25433c98f560ddb31", + "sha256:4bd36c7c11d872e15cefa4951e12380a47e6901687165332dd50a007cb367ad9" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.5" + "markers": "python_version >= '3.10'", + "version": "==2.28.1" }, "kiwisolver": { "hashes": [ - "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b", - "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166", - "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c", - "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c", - "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0", - "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4", - "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9", - "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286", - "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767", - "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c", - "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6", - "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b", - "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004", - "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf", - "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494", - "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac", - "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626", - "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766", - "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514", - "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6", - "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f", - "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d", - "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191", - "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d", - "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51", - "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f", - "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8", - "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454", - "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb", - "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da", - "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8", - "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de", - "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a", - "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9", - "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008", - "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3", - "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32", - "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938", - "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1", - "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9", - "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d", - "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824", - "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b", - "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd", - "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2", - "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5", - "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69", - "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3", - "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae", - "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597", - "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e", - "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955", - "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca", - "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a", - "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea", - "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede", - "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4", - "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6", - "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686", - "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408", - "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871", - "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29", - "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750", - "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897", - "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0", - "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2", - "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09", - "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c" + "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", + "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", + "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", + "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", + "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", + "sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837", + "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", + "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", + "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", + "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", + "sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23", + "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", + "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", + "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", + "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", + "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", + "sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae", + "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", + "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5", + "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", + "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", + "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", + "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", + "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", + "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", + "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", + "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", + "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", + "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", + "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", + "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", + "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", + "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", + "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", + "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", + "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", + "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", + "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", + "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", + "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", + "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", + "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", + "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", + "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", + "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", + "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", + "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", + "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", + "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", + "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", + "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", + "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", + "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", + "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", + "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", + "sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e", + "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", + "sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc", + "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", + "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", + "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", + "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", + "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", + "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", + "sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0", + "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", + "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", + "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", + "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", + "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", + "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", + "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", + "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", + "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", + "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", + "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", + "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", + "sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26", + "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", + "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", + "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", + "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", + "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", + "sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b", + "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", + "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", + "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", + "sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874", + "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", + "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", + "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", + "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", + "sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92", + "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", + "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", + "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", + "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", + "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", + "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", + "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", + "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", + "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", + "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd", + "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", + "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", + "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", + "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", + "sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c", + "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", + "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", + "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", + "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", + "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", + "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", + "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", + "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", + "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", + "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", + "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", + "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", + "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", + "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", + "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", + "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", + "sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef", + "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", + "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", + "sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08", + "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", + "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", + "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", + "sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f", + "sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7", + "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", + "sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8", + "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", + "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", + "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", + "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", + "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", + "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404" + ], + "markers": "python_version >= '3.10'", + "version": "==1.5.1" + }, + "lark": { + "hashes": [ + "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", + "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12" ], - "markers": "python_version >= '3.7'", - "version": "==1.4.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.1" }, - "mahotas": { + "lazy-loader": { "hashes": [ - "sha256:0f14399255c39f0f6cf00819e9c3d787f94d87d99c8f1a8ed78079a3c73824d2", - "sha256:14dc57b5b2e01e3a3e832eb688a38cd245f8cdbdf382f29b6e07003d79d5f0e7", - "sha256:22920ea2f5285f8a99a357960fdeed540863d94a47dcfa79bcc3885a29eafab7", - "sha256:2df8091c037305398ae3e66990398b8f4502c49917536d28f562c88c1d1e53d6", - "sha256:315cf7a3e5dd94f9014923fa611b36d999ce7e855b62ffe103613eeeb8cdc319", - "sha256:3f0109f167fc9b599f77b4d30d57c45bcc34dea019ff79fe29eb43c866d87ce9", - "sha256:4ef9a73062012aac935a4398d91edad4ccddd25f98f61f201cefbe20b6b0aa8b", - "sha256:5a37ba53dda795f2534251df5887fbb37153b14a516f8a14a1c0c762b6513179", - "sha256:849054bdc095749104f6e13cf3ada97f98f29f0ae2af0e0c05de740b3b532fd0", - "sha256:a6d343b59c56fdc812ac2fa07e9e555ca1fb3fe9b555698e2f0bfae9fd9f2a75", - "sha256:d200a8ebe10bd0c07f82d202d1713a96b7596f20adc644955685aafcfa7ae9d3", - "sha256:d923b92f9a286626fe777464c8e6c684c24b2432cd6c6a97ddd5c8f6873ac006", - "sha256:ed07b14c843900072de039c5ae925c9afcce7cafb6e1a4ffd5e1889502de38f8" + "sha256:2f4b7824d6401958639008a0cae20c776b61dff619accd6890693e2de8260167", + "sha256:77253be3391b06124a0e16105bd663b6c54470af1a9ca8e1cf026f38d58ed056" ], - "index": "pypi", - "version": "==1.4.11" + "markers": "python_version >= '3.9'", + "version": "==0.6" + }, + "mahotas": { + "hashes": [ + "sha256:0315ae7665d1eacd10757b4c813d48f495ecd6f6f17c4f33822159c131a500d8", + "sha256:097ab80bcd8f4f8029187cd08d5308c11423388cba7b15e65f378251e3d4bd26", + "sha256:13810b2a944994ae646e6b57d2df26ec0543ce65807451bbc1477bf8d8054243", + "sha256:150fa6592bb30fbf0a5412223c8025f1eb10d86b00444f8782766685c2a388c2", + "sha256:2777b82c936a7ffa5056f094abc39ace46fe7566b68da35e2b3f0a0727094617", + "sha256:361be042deb8173615bd0ce352017e47769c43781fa7ec0fa6ccc990ea045e3d", + "sha256:3d2a93c3f020a1ebdb13f8da46eb7332ab28df5e6f53e9e8d863e835c192dcae", + "sha256:3d820f12497aba0bc962343b80b8d4620739e010f3ad0bbdd0390fccbdb1d9a0", + "sha256:433e731bdd281bac3225d9589c75e9f86c71427b1e9035b2c74925e31b1a3ebc", + "sha256:512f5c23a061ee37c05b65224cc9f99940b8413519c10ea148c7f785a31d0540", + "sha256:5bcd3c5247a6122d4c97839dc6b22d057e8649083f4ad45bda59020cffc89a60", + "sha256:697a3a51d8630ba2e9bf4834b94326ee0fa1b5eef9d82de9c6e5be8eb5c78946", + "sha256:70bf35ffebe018da8bd73df969aed1a678da0822cffa570e8f53718fb0978998", + "sha256:81d54e43c9480116479b2164ca4a92d75bdd94b080a1a5ea6c2413be09a4fe78", + "sha256:8c545f530cbafdd5173d9d2ceda6a31605d21fde914a70aa4fcbd2f547d484f3", + "sha256:9c40321637211b54389cee685d27bee89ba3f62655b757765319983bc3d33c6d", + "sha256:a034774cddf7aa6079eb0825ff66648dcfe1a530b303aa1fbc3ef8e24e9a4305", + "sha256:b7c8e43db6563fbbe6813366596b40d5ed2e4d7d32610cb1f7a1801dfa249fca", + "sha256:cf98d214901255f5bdad2aeeb0b03666db9357a6e9780cae981c5f0dabd5a102", + "sha256:d0c3a681eb1790fae6b89ecc2700b8c0b782a0eab03908a6d62d8d85b78d079b", + "sha256:e51a897d1dae68e4cc7cfa4ec8456e9e33603c3a71ca07307e793fd81e3be4b6" + ], + "markers": "python_version >= '3.10'", + "version": "==1.4.19" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98", + "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", + "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", + "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", + "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", + "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", + "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", + "sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a", + "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", + "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", + "sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97", + "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", + "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", + "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", + "sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2", + "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", + "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", + "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", + "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", + "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", + "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", + "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", + "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", + "sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc", + "sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b", + "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", + "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", + "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", + "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5", + "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", + "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", + "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", + "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", + "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", + "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", + "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", + "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", + "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", + "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", + "sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf", + "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", + "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", + "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", + "sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278", + "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d", + "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", + "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", + "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", + "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", + "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", + "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", + "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", + "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7", + "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", + "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", + "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e", + "sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295", + "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", + "sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7", + "sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0", + "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", + "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", + "sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378", + "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", + "sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac", + "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", + "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", + "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", + "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808", + "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", + "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb", + "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", + "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", + "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", + "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", + "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91", + "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", + "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", + "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e", + "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707", + "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21", + "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef", + "sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be", + "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", + "sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a", + "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", + "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", + "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", + "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", + "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692", + "sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3", + "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", + "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", + "sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36", + "sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9", + "sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768", + "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", + "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", + "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", + "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346", + "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", + "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", + "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", + "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", + "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", + "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", + "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", + "sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868", + "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", + "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", + "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", + "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", + "sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16", + "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", + "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", + "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", + "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", + "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", + "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169", + "sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa", + "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", + "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", + "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", + "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", + "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", + "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", + "sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a", + "sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9", + "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", + "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", + "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", + "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", + "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", + "sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889", + "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", + "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", + "sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2", + "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", + "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", + "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", + "sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8", + "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", + "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", + "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", + "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", + "sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd", + "sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624", + "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", + "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", + "sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8", + "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.4" }, "matplotlib": { "hashes": [ - "sha256:01b7f521a9a73c383825813af255f8c4485d1706e4f3e2ed5ae771e4403a40ab", - "sha256:11011c97d62c1db7bc20509572557842dbb8c2a2ddd3dd7f20501aa1cde3e54e", - "sha256:1183877d008c752d7d535396096c910f4663e4b74a18313adee1213328388e1e", - "sha256:12f999661589981e74d793ee2f41b924b3b87d65fd929f6153bf0f30675c59b1", - "sha256:1c235bf9be052347373f589e018988cad177abb3f997ab1a2e2210c41562cc0c", - "sha256:1f4d69707b1677560cd952544ee4962f68ff07952fb9069ff8c12b56353cb8c9", - "sha256:1fcc4cad498533d3c393a160975acc9b36ffa224d15a6b90ae579eacee5d8579", - "sha256:2787a16df07370dcba385fe20cdd0cc3cfaabd3c873ddabca78c10514c799721", - "sha256:29f17b7f2e068dc346687cbdf80b430580bab42346625821c2d3abf3a1ec5417", - "sha256:38d38cb1ea1d80ee0f6351b65c6f76cad6060bbbead015720ba001348ae90f0c", - "sha256:3f56a7252eee8f3438447f75f5e1148a1896a2756a92285fe5d73bed6deebff4", - "sha256:5223affa21050fb6118353c1380c15e23aedfb436bf3e162c26dc950617a7519", - "sha256:57ad1aee29043163374bfa8990e1a2a10ff72c9a1bfaa92e9c46f6ea59269121", - "sha256:59400cc9451094b7f08cc3f321972e6e1db4cd37a978d4e8a12824bf7fd2f03b", - "sha256:68d94a436f62b8a861bf3ace82067a71bafb724b4e4f9133521e4d8012420dd7", - "sha256:6adc441b5b2098a4b904bbf9d9e92fb816fef50c55aa2ea6a823fc89b94bb838", - "sha256:6d81b11ede69e3a751424b98dc869c96c10256b2206bfdf41f9c720eee86844c", - "sha256:73b93af33634ed919e72811c9703e1105185cd3fb46d76f30b7f4cfbbd063f89", - "sha256:77b384cee7ab8cf75ffccbfea351a09b97564fc62d149827a5e864bec81526e5", - "sha256:79e501eb847f4a489eb7065bb8d3187117f65a4c02d12ea3a19d6c5bef173bcc", - "sha256:809119d1cba3ece3c9742eb01827fe7a0e781ea3c5d89534655a75e07979344f", - "sha256:80c166a0e28512e26755f69040e6bf2f946a02ffdb7c00bf6158cca3d2b146e6", - "sha256:81b409b2790cf8d7c1ef35920f01676d2ae7afa8241844e7aa5484fdf493a9a0", - "sha256:994637e2995b0342699b396a320698b07cd148bbcf2dd2fa2daba73f34dd19f2", - "sha256:9ceebaf73f1a3444fa11014f38b9da37ff7ea328d6efa1652241fe3777bfdab9", - "sha256:9fb8fb19d03abf3c5dab89a8677e62c4023632f919a62b6dd1d6d2dbf42cd9f5", - "sha256:acc3b1a4bddbf56fe461e36fb9ef94c2cb607fc90d24ccc650040bfcc7610de4", - "sha256:bbddfeb1495484351fb5b30cf5bdf06b3de0bc4626a707d29e43dfd61af2a780", - "sha256:bbf269e1d24bc25247095d71c7a969813f7080e2a7c6fa28931a603f747ab012", - "sha256:bebcff4c3ed02c6399d47329f3554193abd824d3d53b5ca02cf583bcd94470e2", - "sha256:c3f08df2ac4636249b8bc7a85b8b82c983bef1441595936f62c2918370ca7e1d", - "sha256:ca94f0362f6b6f424b555b956971dcb94b12d0368a6c3e07dc7a40d32d6d873d", - "sha256:d00c248ab6b92bea3f8148714837937053a083ff03b4c5e30ed37e28fc0e7e56", - "sha256:d2cfaa7fd62294d945b8843ea24228a27c8e7c5b48fa634f3c168153b825a21b", - "sha256:d5f18430f5cfa5571ab8f4c72c89af52aa0618e864c60028f11a857d62200cba", - "sha256:debeab8e2ab07e5e3dac33e12456da79c7e104270d2b2d1df92b9e40347cca75", - "sha256:dfba7057609ca9567b9704626756f0142e97ec8c5ba2c70c6e7bd1c25ef99f06", - "sha256:e0a64d7cc336b52e90f59e6d638ae847b966f68582a7af041e063d568e814740", - "sha256:eb9421c403ffd387fbe729de6d9a03005bf42faba5e8432f4e51e703215b49fc", - "sha256:faff486b36530a836a6b4395850322e74211cd81fc17f28b4904e1bd53668e3e", - "sha256:ff2aa84e74f80891e6bcf292ebb1dd57714ffbe13177642d65fee25384a30894" - ], - "markers": "python_version >= '3.8'", - "version": "==3.6.3" + "sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb", + "sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575", + "sha256:07d9b9fa60cd4c393692f50d0bb03123242ddf61c99bb0e95e75feb354e7c1a8", + "sha256:116cdb0eb0eb5644fc98eb2975d4b4dd4ad35e5c8e6b851c22e6976f371f7ab5", + "sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539", + "sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863", + "sha256:1b9a7ad579856284135e401ecc918c5f8a017ee30539298862a109f51b971710", + "sha256:254d4ddb2fa8df3b4c689c0c306063aee10521df82cfb438185e499c75fe37c1", + "sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f", + "sha256:30ec15d7eefee71de16b7c689b42ba49715a4644650b76a6c7c70d79daf24e91", + "sha256:399fef672f7046ef7d6a57572b2a6f9845f3f5afcff04e7b2df7a363a9f42190", + "sha256:3aa4b8516fd26659e4363abbf317c703d9116496c5db2e9d0609a2866dd39dd2", + "sha256:3da3bc0cbf7245e7db72cc6d29d12c5abef72cb73059c73b945f14e3545f3eb2", + "sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9", + "sha256:57b9ea60a835937c2012861923cbb91f47db8565775d326d1c42fc926aa10351", + "sha256:5e1e923a3fc3326b99ec0a6ff1ab1338ac6c6cc62ad9d8a9c944197c7f8c6221", + "sha256:643ff850d8e0f5b8319337f87ed3cb59506afb3df3cc48de777d85871233be7b", + "sha256:75b6d88402770e181b5d06a67dda4129c31da7d05004d63a21c310ec78d1b83c", + "sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d", + "sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0", + "sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292", + "sha256:7ef7a53b66780e5d942923724f08577fbc5be1f7322da0f0f3f9dcaa45dd803d", + "sha256:854df8d7dfe9fdffcbaa6f39e44a6c24b409cb4d7561fbc09213b157d833f6a6", + "sha256:894a9cbbecbe30ae6787d464df2e8fc7a8d475cfc68f87c3029f7c11152899b3", + "sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71", + "sha256:930efb28f59fda124e39265d177bab302625297bb147d2910de725a2fc2aef54", + "sha256:a24d5fd36e4f0e742c3851dcd20810e56a95633a342e4bf6cb591c678e8fe61f", + "sha256:a6939df7567114b6bac7f4c5e06c84a67f197c1b2f2e4b234d4eecb3bfec9482", + "sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943", + "sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981", + "sha256:bbf1062991d826ed27e2144f3afa4461afbb8ff56e8f703043e191a8163b1ee9", + "sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7", + "sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b", + "sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc", + "sha256:c27e577ece613ea12a0790e00b4eb80d901c59a3e16cad474f31d8b1529690b9", + "sha256:c5c1c68ee401fc98271263410f0e5ce88285abacf7627132914e8adf3d70ff43", + "sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6", + "sha256:cc82dde2a0d3e3ad472edce04897ad7146b8d8bfd1df8a32992eebb81af18fdc", + "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", + "sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c", + "sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869", + "sha256:d480038c83691532ed52ff3147db51fa902fc78cb2d8349993a1cdb684435bff", + "sha256:df4f7784aca81a94f254c0a2767d592ee25f407e488f5fa7203e51093fb6ca27", + "sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba", + "sha256:e60cf3047a51edecdc4535a9196bbd6a732936b8e9f8184aabf4d16165884aaf", + "sha256:eac4b07d4e3743b172451e122ea964f72f152879d4f9adcf3f3d33e518f12ead", + "sha256:eb3712dc9b464793de0a4e42a7313d50293c751f94bddaf7332a1bc71bccdda9", + "sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6", + "sha256:ef31985c4dedb5f1424e1aec6849a47dd37689cb7fa3c20b1b82187f26806261", + "sha256:ef752769cd962f39ea0b6ffc82d1ea43a0012c5a6157c7a075212fa509cfcff2", + "sha256:f25446b2981717dca9786bac841cb3fd7efb568e3e3c755dd980481c5cb9228d", + "sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170", + "sha256:fea03cf56568cc1cba08b470be6a0559e71c3a5b688d54b7179bb35ba23d0821" + ], + "markers": "python_version >= '3.11'", + "version": "==3.11.2" }, "matplotlib-inline": { "hashes": [ - "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", - "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" + "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", + "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79" ], - "markers": "python_version >= '3.5'", - "version": "==0.1.6" + "markers": "python_version >= '3.9'", + "version": "==0.2.2" }, "mistune": { "hashes": [ - "sha256:0246113cb2492db875c6be56974a7c893333bf26cd92891c85f63151cee09d34", - "sha256:bad7f5d431886fcbaf5f758118ecff70d31f75231b34024a1341120340a65ce8" + "sha256:2eaf7952af928ca8d33b779762effc1da517b62c6ad85ec8ca957e3b07b9782a", + "sha256:d03cec4c8620f21517a996d82d14f3b59c30d79bda2cc6c56bd32b839f18ce6b" ], - "version": "==2.0.5" - }, - "nbclassic": { - "hashes": [ - "sha256:32c235e1f22f4048f3b877d354c198202898797cf9c2085856827598cead001b", - "sha256:8e8ffce7582bb7a4baf11fa86a3d88b184e8e7df78eed4ead69f15aa4fc0e323" - ], - "markers": "python_version >= '3.7'", - "version": "==0.5.1" + "markers": "python_version >= '3.10'", + "version": "==3.4.0" }, "nbclient": { "hashes": [ - "sha256:434c91385cf3e53084185334d675a0d33c615108b391e260915d1aa8e86661b8", - "sha256:a1d844efd6da9bc39d2209bf996dbd8e07bf0f36b796edfabaa8f8a9ab77c3aa" + "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", + "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895" ], - "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "markers": "python_full_version >= '3.10.0'", + "version": "==0.11.0" }, "nbconvert": { "hashes": [ - "sha256:495638c5e06005f4a5ce828d8a81d28e34f95c20f4384d5d7a22254b443836e7", - "sha256:a42c3ac137c64f70cbe4d763111bf358641ea53b37a01a5c202ed86374af5234" + "sha256:344a218e8595117ce37396ed025d994f146d076226ebac59255a2fa085ab24d8", + "sha256:6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2" ], - "markers": "python_version >= '3.7'", - "version": "==7.2.9" + "markers": "python_version >= '3.9'", + "version": "==7.17.2" }, "nbformat": { "hashes": [ - "sha256:22a98a6516ca216002b0a34591af5bcb8072ca6c63910baffc901cfa07fefbf0", - "sha256:4b021fca24d3a747bf4e626694033d792d594705829e5e35b14ee3369f9f6477" + "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", + "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec" ], - "markers": "python_version >= '3.7'", - "version": "==5.7.3" + "markers": "python_version >= '3.10'", + "version": "==5.11.1" }, - "nest-asyncio": { + "nest-asyncio2": { "hashes": [ - "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8", - "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290" + "sha256:86e77b52c3d1496b294af3c944b44213b9b82a12a5ee590c0acc4ca4692ef6c7", + "sha256:995b1409d926ec55e0876ea39a69bffdd8852cf1db3652c177e8c8ffdbcaf3cc" ], "markers": "python_version >= '3.5'", - "version": "==1.5.6" + "version": "==1.7.4" }, "networkx": { "hashes": [ - "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e", - "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412" + "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", + "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762" ], - "markers": "python_version >= '3.8'", - "version": "==3.0" - }, - "notebook": { - "hashes": [ - "sha256:c1897e5317e225fc78b45549a6ab4b668e4c996fd03a04e938fe5e7af2bfffd0", - "sha256:e04f9018ceb86e4fa841e92ea8fb214f8d23c1cedfde530cc96f92446924f0e4" - ], - "markers": "python_version >= '3.7'", - "version": "==6.5.2" + "markers": "python_version >= '3.11' and python_full_version != '3.14.1'", + "version": "==3.6.1" }, "notebook-shim": { "hashes": [ - "sha256:090e0baf9a5582ff59b607af523ca2db68ff216da0c69956b62cab2ef4fc9c3f", - "sha256:9c6c30f74c4fbea6fce55c1be58e7fd0409b1c681b075dcedceb005db5026949" + "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", + "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb" ], "markers": "python_version >= '3.7'", - "version": "==0.2.2" + "version": "==0.2.4" }, "numpy": { "hashes": [ - "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595", - "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f", - "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f", - "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5", - "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695", - "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9", - "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559", - "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982", - "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88", - "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226", - "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f", - "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1", - "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471", - "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383", - "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397", - "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c", - "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f", - "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd", - "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b", - "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9", - "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d", - "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c" + "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", + "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", + "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", + "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", + "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", + "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", + "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", + "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", + "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", + "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", + "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", + "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", + "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", + "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", + "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", + "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", + "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", + "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", + "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", + "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", + "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", + "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", + "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", + "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", + "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", + "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", + "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", + "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", + "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", + "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", + "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", + "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", + "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", + "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", + "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", + "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", + "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", + "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", + "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", + "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", + "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", + "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", + "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", + "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", + "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", + "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", + "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", + "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", + "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", + "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", + "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", + "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", + "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", + "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", + "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", + "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", + "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", + "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", + "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", + "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", + "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", + "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", + "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", + "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", + "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", + "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", + "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", + "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", + "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", + "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", + "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", + "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20" + ], + "markers": "python_version >= '3.11'", + "version": "==2.4.6" + }, + "overrides": { + "hashes": [ + "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", + "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49" ], - "index": "pypi", - "version": "==1.22.0" + "markers": "python_version >= '3.6'", + "version": "==7.7.0" }, "packaging": { "hashes": [ - "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", - "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", + "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" ], - "markers": "python_version >= '3.7'", - "version": "==23.0" + "markers": "python_version >= '3.9'", + "version": "==26.3" }, "pandocfilters": { "hashes": [ - "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38", - "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f" + "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", + "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.0" + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3'", + "version": "==1.5.1" }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", + "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" + "version": "==0.8.7" }, "pexpect": { "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" ], - "markers": "sys_platform != 'win32'", - "version": "==4.8.0" - }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" + "markers": "sys_platform != 'win32' and sys_platform != 'emscripten'", + "version": "==4.9.0" }, "pillow": { "hashes": [ - "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33", - "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b", - "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e", - "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35", - "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153", - "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9", - "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569", - "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57", - "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8", - "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1", - "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264", - "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157", - "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9", - "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133", - "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9", - "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab", - "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6", - "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5", - "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df", - "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503", - "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b", - "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa", - "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327", - "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493", - "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d", - "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4", - "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4", - "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35", - "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2", - "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c", - "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011", - "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a", - "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e", - "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f", - "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848", - "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57", - "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f", - "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c", - "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9", - "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5", - "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9", - "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d", - "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0", - "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1", - "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e", - "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815", - "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0", - "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b", - "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd", - "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c", - "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3", - "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab", - "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858", - "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5", - "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee", - "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343", - "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb", - "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47", - "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed", - "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837", - "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286", - "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28", - "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628", - "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df", - "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d", - "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d", - "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a", - "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6", - "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336", - "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132", - "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070", - "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe", - "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a", - "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd", - "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391", - "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a", - "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12" - ], - "markers": "python_version >= '3.7'", - "version": "==9.4.0" - }, - "pkgutil-resolve-name": { - "hashes": [ - "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174", - "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e" - ], - "markers": "python_version < '3.9'", - "version": "==1.3.10" + "sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27", + "sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782", + "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", + "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", + "sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502", + "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", + "sha256:10b5019883c98f1dd819ca48ffc6f33a91cee04948fe637c605f4345c7cafd6f", + "sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8", + "sha256:12fbb851cf323cc19aac3eea1df5ad17b57e91e9fe3cfd06100dba7bb8208c13", + "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", + "sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369", + "sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88", + "sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d", + "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", + "sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f", + "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", + "sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40", + "sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599", + "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", + "sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033", + "sha256:37290fbef0c73e2d51d594eb78fded7dc7170d1c3ad0a890269e6fd8c8c4e061", + "sha256:405be6058e206a86fedce2f715ecc2343f94072d7925251632dffa2c05f70dca", + "sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f", + "sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca", + "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", + "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", + "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", + "sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620", + "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", + "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", + "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", + "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", + "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", + "sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582", + "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", + "sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79", + "sha256:6a0920cb55df5a4d3f6862964908078c0bd59332bdd1bcd20d14e8c70ef27cc5", + "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", + "sha256:6f38f3f885a09cafc93fd667b1173a92f379e43785d635e7983c893f0da319fb", + "sha256:73ee25cf8cf8b75d5a5aeed7cb6588f5e5dbfbb0fde51707b630bbb23897645c", + "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", + "sha256:7c88c10dd432f3b39ccdafc9ed81d0c21b8fd634502574da76d1bfa4fe2c76d0", + "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", + "sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e", + "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", + "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", + "sha256:83b37abd87b590acd8f47ca8a61f70893322b11834d36e07c2298dacad4d14d2", + "sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463", + "sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d", + "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", + "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", + "sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63", + "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", + "sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42", + "sha256:a192360d512305fe92d3053e4f7627bbad1f7708f1f4ad38cccd9336acabe9ad", + "sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81", + "sha256:a625df9a2a5597fa5e5266e01aefc82604bb85d291e3547981e863560d75da6c", + "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", + "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", + "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", + "sha256:b84be41387fc00db28c13c02e30d42b02d8d8f6a4a741a1aee6012ca7f1f6039", + "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", + "sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51", + "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", + "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", + "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", + "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", + "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", + "sha256:d7710eb676e9bb68965cb045be0d333b4c0d7f9dfbaa7fbf121385b1588683d3", + "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", + "sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61", + "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", + "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", + "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", + "sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00", + "sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe", + "sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313", + "sha256:f391dc92a015c478745192fe4f2e120c2af2aba18d9fe0616341cbf537599806", + "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", + "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", + "sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52", + "sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b", + "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe" + ], + "markers": "python_version >= '3.11'", + "version": "==13.0.0" + }, + "platformdirs": { + "hashes": [ + "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", + "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e" + ], + "markers": "python_version >= '3.11'", + "version": "==4.13.5" }, "prometheus-client": { "hashes": [ - "sha256:0836af6eb2c8f4fed712b2f279f6c0a8bbab29f9f4aa15276b91c7cb0d1616ab", - "sha256:a03e35b359f14dd1630898543e2120addfdeacd1a6069c1367ae90fd93ad3f48" + "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", + "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6" ], - "markers": "python_version >= '3.6'", - "version": "==0.16.0" + "markers": "python_version >= '3.9'", + "version": "==0.26.0" }, "prompt-toolkit": { "hashes": [ - "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63", - "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305" + "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", + "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6" ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.36" + "markers": "python_version >= '3.10'", + "version": "==3.0.53" }, "psutil": { "hashes": [ - "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff", - "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1", - "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62", - "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549", - "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08", - "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7", - "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e", - "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe", - "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24", - "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad", - "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94", - "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8", - "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7", - "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==5.9.4" + "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", + "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", + "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", + "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", + "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", + "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", + "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", + "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", + "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", + "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", + "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", + "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", + "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", + "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", + "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", + "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", + "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", + "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", + "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", + "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", + "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8" + ], + "markers": "python_version >= '3.6'", + "version": "==7.2.2" }, "ptyprocess": { "hashes": [ "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" ], - "markers": "os_name != 'nt'", "version": "==0.7.0" }, "pure-eval": { "hashes": [ - "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", - "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" + "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1", + "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a" ], - "version": "==0.2.2" + "version": "==0.2.4" }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", + "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc" ], - "version": "==2.21" + "markers": "python_version >= '3.10'", + "version": "==3.11" + }, + "pydicom": { + "hashes": [ + "sha256:5942bfc2d72c6fa4b3b5b62c527f54b7f2355f21d6f5d296df6bb30188df6a4f", + "sha256:abf971a5440f84dbaf42c4b6758e30e62480902584f8b270b9a5d146e278a07b" + ], + "markers": "python_version >= '3.10'", + "version": "==3.0.2" }, "pygments": { "hashes": [ - "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", - "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" + "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", + "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" ], - "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "markers": "python_version >= '3.9'", + "version": "==2.21.0" }, "pyparsing": { "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pyrsistent": { - "hashes": [ - "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", - "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", - "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", - "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", - "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", - "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", - "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", - "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", - "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", - "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", - "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", - "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", - "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", - "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", - "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", - "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", - "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", - "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", - "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", - "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", - "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", - "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", - "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", - "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", - "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", - "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", - "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" + "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", + "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4" ], - "markers": "python_version >= '3.7'", - "version": "==0.19.3" + "markers": "python_version >= '3.9'", + "version": "==3.3.3" }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pywavelets": { - "hashes": [ - "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b", - "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4", - "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4", - "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6", - "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de", - "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c", - "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa", - "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93", - "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875", - "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd", - "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356", - "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1", - "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c", - "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2", - "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784", - "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4", - "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b", - "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc", - "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966", - "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e", - "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426", - "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c", - "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202", - "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc", - "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2'", + "version": "==2.9.0.post0" + }, + "python-json-logger": { + "hashes": [ + "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9", + "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa" + ], + "markers": "python_version >= '3.10'", + "version": "==4.2.0" + }, + "pyyaml": { + "hashes": [ + "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" ], "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "version": "==6.0.3" }, "pyzmq": { "hashes": [ - "sha256:00c94fd4c9dd3c95aace0c629a7fa713627a5c80c1819326b642adf6c4b8e2a2", - "sha256:01d53958c787cfea34091fcb8ef36003dbb7913b8e9f8f62a0715234ebc98b70", - "sha256:0282bba9aee6e0346aa27d6c69b5f7df72b5a964c91958fc9e0c62dcae5fdcdc", - "sha256:02f5cb60a7da1edd5591a15efa654ffe2303297a41e1b40c3c8942f8f11fc17c", - "sha256:0645b5a2d2a06fd8eb738018490c514907f7488bf9359c6ee9d92f62e844b76f", - "sha256:0a154ef810d44f9d28868be04641f837374a64e7449df98d9208e76c260c7ef1", - "sha256:0a90b2480a26aef7c13cff18703ba8d68e181facb40f78873df79e6d42c1facc", - "sha256:0e8d00228db627ddd1b418c7afd81820b38575f237128c9650365f2dd6ac3443", - "sha256:17e1cb97d573ea84d7cd97188b42ca6f611ab3ee600f6a75041294ede58e3d20", - "sha256:183e18742be3621acf8908903f689ec520aee3f08449bfd29f583010ca33022b", - "sha256:1f6116991568aac48b94d6d8aaed6157d407942ea385335a6ed313692777fb9d", - "sha256:20638121b0bdc80777ce0ec8c1f14f1ffec0697a1f88f0b564fa4a23078791c4", - "sha256:2754fa68da08a854f4816e05160137fa938a2347276471103d31e04bcee5365c", - "sha256:28bcb2e66224a7ac2843eb632e4109d6b161479e7a2baf24e37210461485b4f1", - "sha256:293a7c2128690f496057f1f1eb6074f8746058d13588389981089ec45d8fdc77", - "sha256:2a73af6504e0d2805e926abf136ebf536735a13c22f709be7113c2ec65b4bec3", - "sha256:2d05d904f03ddf1e0d83d97341354dfe52244a619b5a1440a5f47a5b3451e84e", - "sha256:2e7b87638ee30ab13230e37ce5331b3e730b1e0dda30120b9eeec3540ed292c8", - "sha256:3100dddcada66ec5940ed6391ebf9d003cc3ede3d320748b2737553019f58230", - "sha256:31e523d067ce44a04e876bed3ff9ea1ff8d1b6636d16e5fcace9d22f8c564369", - "sha256:3594c0ff604e685d7e907860b61d0e10e46c74a9ffca168f6e9e50ea934ee440", - "sha256:3670e8c5644768f214a3b598fe46378a4a6f096d5fb82a67dfd3440028460565", - "sha256:4046d03100aca266e70d54a35694cb35d6654cfbef633e848b3c4a8d64b9d187", - "sha256:4725412e27612f0d7d7c2f794d89807ad0227c2fc01dd6146b39ada49c748ef9", - "sha256:484c2c4ee02c1edc07039f42130bd16e804b1fe81c4f428e0042e03967f40c20", - "sha256:487305c2a011fdcf3db1f24e8814bb76d23bc4d2f46e145bc80316a59a9aa07d", - "sha256:4a1bc30f0c18444d51e9b0d0dd39e3a4e7c53ee74190bebef238cd58de577ea9", - "sha256:4c25c95416133942280faaf068d0fddfd642b927fb28aaf4ab201a738e597c1e", - "sha256:4cbb885f347eba7ab7681c450dee5b14aed9f153eec224ec0c3f299273d9241f", - "sha256:4d3d604fe0a67afd1aff906e54da557a5203368a99dcc50a70eef374f1d2abef", - "sha256:4e295f7928a31ae0f657e848c5045ba6d693fe8921205f408ca3804b1b236968", - "sha256:5049e75cc99db65754a3da5f079230fb8889230cf09462ec972d884d1704a3ed", - "sha256:5050f5c50b58a6e38ccaf9263a356f74ef1040f5ca4030225d1cb1a858c5b7b6", - "sha256:526f884a27e8bba62fe1f4e07c62be2cfe492b6d432a8fdc4210397f8cf15331", - "sha256:531866c491aee5a1e967c286cfa470dffac1e2a203b1afda52d62b58782651e9", - "sha256:5605621f2181f20b71f13f698944deb26a0a71af4aaf435b34dd90146092d530", - "sha256:58fc3ad5e1cfd2e6d24741fbb1e216b388115d31b0ca6670f894187f280b6ba6", - "sha256:60ecbfe7669d3808ffa8a7dd1487d6eb8a4015b07235e3b723d4b2a2d4de7203", - "sha256:610d2d112acd4e5501fac31010064a6c6efd716ceb968e443cae0059eb7b86de", - "sha256:6136bfb0e5a9cf8c60c6ac763eb21f82940a77e6758ea53516c8c7074f4ff948", - "sha256:62b9e80890c0d2408eb42d5d7e1fc62a5ce71be3288684788f74cf3e59ffd6e2", - "sha256:656281d496aaf9ca4fd4cea84e6d893e3361057c4707bd38618f7e811759103c", - "sha256:66509c48f7446b640eeae24b60c9c1461799a27b1b0754e438582e36b5af3315", - "sha256:6bf3842af37af43fa953e96074ebbb5315f6a297198f805d019d788a1021dbc8", - "sha256:731b208bc9412deeb553c9519dca47136b5a01ca66667cafd8733211941b17e4", - "sha256:75243e422e85a62f0ab7953dc315452a56b2c6a7e7d1a3c3109ac3cc57ed6b47", - "sha256:7877264aa851c19404b1bb9dbe6eed21ea0c13698be1eda3784aab3036d1c861", - "sha256:81f99fb1224d36eb91557afec8cdc2264e856f3464500b55749020ce4c848ef2", - "sha256:8539216173135e9e89f6b1cc392e74e6b935b91e8c76106cf50e7a02ab02efe5", - "sha256:85456f0d8f3268eecd63dede3b99d5bd8d3b306310c37d4c15141111d22baeaf", - "sha256:866eabf7c1315ef2e93e34230db7cbf672e0d7c626b37c11f7e870c8612c3dcc", - "sha256:926236ca003aec70574754f39703528947211a406f5c6c8b3e50eca04a9e87fc", - "sha256:930e6ad4f2eaac31a3d0c2130619d25db754b267487ebc186c6ad18af2a74018", - "sha256:94f0a7289d0f5c80807c37ebb404205e7deb737e8763eb176f4770839ee2a287", - "sha256:9a2d5e419bd39a1edb6cdd326d831f0120ddb9b1ff397e7d73541bf393294973", - "sha256:9ca6db34b26c4d3e9b0728841ec9aa39484eee272caa97972ec8c8e231b20c7e", - "sha256:9f72ea279b2941a5203e935a4588b9ba8a48aeb9a926d9dfa1986278bd362cb8", - "sha256:a0e7ef9ac807db50b4eb6f534c5dcc22f998f5dae920cc28873d2c1d080a4fc9", - "sha256:a1cd4a95f176cdc0ee0a82d49d5830f13ae6015d89decbf834c273bc33eeb3d3", - "sha256:a9c464cc508177c09a5a6122b67f978f20e2954a21362bf095a0da4647e3e908", - "sha256:ac97e7d647d5519bcef48dd8d3d331f72975afa5c4496c95f6e854686f45e2d9", - "sha256:af1fbfb7ad6ac0009ccee33c90a1d303431c7fb594335eb97760988727a37577", - "sha256:b055a1cddf8035966ad13aa51edae5dc8f1bba0b5d5e06f7a843d8b83dc9b66b", - "sha256:b6f75b4b8574f3a8a0d6b4b52606fc75b82cb4391471be48ab0b8677c82f9ed4", - "sha256:b90bb8dfbbd138558f1f284fecfe328f7653616ff9a972433a00711d9475d1a9", - "sha256:be05504af0619d1cffa500af1e0ede69fb683f301003851f5993b5247cc2c576", - "sha256:c21a5f4e54a807df5afdef52b6d24ec1580153a6bcf0607f70a6e1d9fa74c5c3", - "sha256:c48f257da280b3be6c94e05bd575eddb1373419dbb1a72c3ce64e88f29d1cd6d", - "sha256:cac602e02341eaaf4edfd3e29bd3fdef672e61d4e6dfe5c1d065172aee00acee", - "sha256:ccb3e1a863222afdbda42b7ca8ac8569959593d7abd44f5a709177d6fa27d266", - "sha256:e1081d7030a1229c8ff90120346fb7599b54f552e98fcea5170544e7c6725aab", - "sha256:e14df47c1265356715d3d66e90282a645ebc077b70b3806cf47efcb7d1d630cb", - "sha256:e4bba04ea779a3d7ef25a821bb63fd0939142c88e7813e5bd9c6265a20c523a2", - "sha256:e99629a976809fe102ef73e856cf4b2660acd82a412a51e80ba2215e523dfd0a", - "sha256:f330a1a2c7f89fd4b0aa4dcb7bf50243bf1c8da9a2f1efc31daf57a2046b31f2", - "sha256:f3f96d452e9580cb961ece2e5a788e64abaecb1232a80e61deffb28e105ff84a", - "sha256:fc7c1421c5b1c916acf3128bf3cc7ea7f5018b58c69a6866d70c14190e600ce9" + "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", + "sha256:010db74a1dd67c7cd8b8b30916355735db7d633a070510bb34e41ab679ab2c0e", + "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79", + "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", + "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7", + "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", + "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", + "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea", + "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", + "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", + "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", + "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", + "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080", + "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67", + "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", + "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", + "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb", + "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8", + "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", + "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0", + "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c", + "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e", + "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62", + "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3", + "sha256:507c0b33f95502723d325487e8e50c2cdd3b37444143f05423a3861327f69bf7", + "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", + "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", + "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", + "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7", + "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71", + "sha256:6eb63cc61ab93b01b9afc887a160255e2fbe703fdbacfe5feaef87214f51bd6c", + "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff", + "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638", + "sha256:76afba06ae698f2b8fe4fb34b32c760a650f168c2e622f370f2c528035b7f650", + "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00", + "sha256:7e2579c5de82ddf4544d723c1bc8b44c3b806d157acc9fb2a2d18e10ef28e202", + "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033", + "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", + "sha256:8a5c04ad2e368142aea52d1abdf6631cb2534864e3c16ab78268ab957060b2a6", + "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e", + "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b", + "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872", + "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645", + "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", + "sha256:95f52b877149b06bbdeec2e8ea6230aad14950bbfbcfa16e7eb88951f07d6b28", + "sha256:97d4c6622f129b514a4f5939af1b5f434c97f47085d9311b5f7f36e24b3bd447", + "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", + "sha256:9ab72ee77b313d0658447204c8201f9b315146e923b48c56ea7dbd005d464a91", + "sha256:a070a9cdad1f8f8a85ea153afcc4654f11b10895d14c0acabe10f1df0e0892ea", + "sha256:a0ee3c49be2aa15abd12cbbd14d4ea2892f872c688e1e487af39ec1972ed549d", + "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1", + "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", + "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", + "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759", + "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42", + "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2", + "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", + "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", + "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e", + "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", + "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c", + "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", + "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", + "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", + "sha256:c9322f9c87b0935870516c2876e1e29497fdc50439c785ece63e3fbbab06c821", + "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", + "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", + "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373", + "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", + "sha256:d61a0169ba05ab7ebc48dc793f092df12f789bf378dac8321ccd966fd93d94e8", + "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed", + "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", + "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", + "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43", + "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", + "sha256:dfcd024eade5870b25f890c4df0ba9421ed8167d8d3d82334237512c1158dada", + "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", + "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", + "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", + "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", + "sha256:f52f08101907609cc08db6a1f9f2a7a9afd54e9b2ca16178c9c38e99fb593cef", + "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802", + "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", + "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", + "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", + "sha256:ff60f0f7ccfda0e303ac43bec7096007b7cdf2c41b3739d1ec667febe67acab3" + ], + "markers": "python_version >= '3.9'", + "version": "==27.2.0" + }, + "referencing": { + "hashes": [ + "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", + "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" + ], + "markers": "python_version >= '3.10'", + "version": "==0.37.0" + }, + "requests": { + "hashes": [ + "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" + ], + "markers": "python_version >= '3.10'", + "version": "==2.34.2" + }, + "rfc3339-validator": { + "hashes": [ + "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", + "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa" + ], + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3' and python_version != '3.4'", + "version": "==0.1.4" + }, + "rfc3986-validator": { + "hashes": [ + "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", + "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055" + ], + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3' and python_version != '3.4'", + "version": "==0.1.1" + }, + "rfc3987-syntax": { + "hashes": [ + "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", + "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d" + ], + "markers": "python_version >= '3.9'", + "version": "==1.1.0" + }, + "rpds-py": { + "hashes": [ + "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", + "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68", + "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", + "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32", + "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", + "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834", + "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", + "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", + "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", + "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", + "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759", + "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff", + "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", + "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", + "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", + "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", + "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", + "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", + "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", + "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a", + "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a", + "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2", + "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", + "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", + "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044", + "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69", + "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", + "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3", + "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", + "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", + "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52", + "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", + "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", + "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", + "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", + "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c", + "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", + "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", + "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6", + "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", + "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14", + "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", + "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30", + "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31", + "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02", + "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af", + "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", + "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0", + "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888", + "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a", + "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", + "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", + "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", + "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f", + "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", + "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", + "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82", + "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d", + "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7", + "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448", + "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391", + "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb", + "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4", + "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e", + "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", + "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", + "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f", + "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", + "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", + "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", + "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", + "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237", + "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb", + "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", + "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", + "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e", + "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608", + "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07", + "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c", + "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", + "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", + "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", + "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", + "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", + "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", + "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", + "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f", + "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", + "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", + "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", + "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", + "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9", + "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab", + "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37", + "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", + "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc", + "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", + "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3", + "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", + "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", + "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", + "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", + "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f", + "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c", + "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", + "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", + "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", + "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1", + "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e", + "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", + "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa", + "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b", + "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", + "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", + "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", + "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", + "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", + "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", + "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", + "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", + "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da", + "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", + "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", + "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050", + "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", + "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df", + "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12", + "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567" + ], + "markers": "python_version >= '3.11'", + "version": "==2026.9.1" + }, + "scikit-image": { + "hashes": [ + "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", + "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", + "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", + "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", + "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", + "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", + "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", + "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", + "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", + "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", + "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", + "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", + "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", + "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", + "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", + "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", + "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", + "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", + "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", + "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", + "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", + "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", + "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", + "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", + "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", + "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", + "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", + "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", + "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", + "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", + "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", + "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", + "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", + "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", + "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", + "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", + "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", + "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", + "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", + "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", + "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", + "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", + "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", + "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", + "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", + "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", + "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", + "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", + "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62" + ], + "markers": "python_version >= '3.11'", + "version": "==0.26.0" + }, + "scipy": { + "hashes": [ + "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", + "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", + "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", + "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", + "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", + "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", + "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", + "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", + "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", + "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", + "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", + "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", + "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", + "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", + "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", + "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", + "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", + "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", + "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", + "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", + "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", + "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", + "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", + "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", + "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", + "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", + "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", + "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", + "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", + "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", + "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", + "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", + "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", + "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", + "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", + "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", + "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", + "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", + "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", + "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", + "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", + "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", + "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", + "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", + "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", + "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", + "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", + "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", + "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", + "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", + "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", + "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", + "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", + "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", + "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", + "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", + "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", + "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", + "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", + "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", + "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76" + ], + "markers": "python_version >= '3.11'", + "version": "==1.17.1" + }, + "send2trash": { + "hashes": [ + "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", + "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459" ], - "markers": "python_version >= '3.6'", - "version": "==25.0.0" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, - "qtconsole": { + "simpleitk": { "hashes": [ - "sha256:57748ea2fd26320a0b77adba20131cfbb13818c7c96d83fafcb110ff55f58b35", - "sha256:be13560c19bdb3b54ed9741a915aa701a68d424519e8341ac479a91209e694b2" + "sha256:0002b298efb31332f99587cf26ed9f42c2c3ec006a0570f492dfc9ea27303d73", + "sha256:22b825c609fe5ca114c8acc149a26a94a01bd62e8c9e685f362d5f880e237269", + "sha256:36658792fe2a62814cbfedb3b236ac722ca7e00953241726bc4a3a26cc7ef5a7", + "sha256:39cdecac58e50c976d1344328f0757b84d4600bdd4c969c63c0d8f0abfae3bf0", + "sha256:454d0fd8c9323e8246c8c7b8fd9c142b211080968359684acd0f0c48b7f55d49", + "sha256:58d010734206efc322da6c0feff0a66e9ea183fbaff7c0ef42c0cab9f4492657", + "sha256:5e731a91cd1376e5a4533395cf2b1ae9fb2a8fa1fbf7f396ab49d3223fc464fd", + "sha256:660b5436cf545c4fde76e10e71bdd373edc522c5f7035f2166dc134f0530cd31", + "sha256:698904c85cb9b8152a8b117465c9d047a5b4efb622ed2d064ce75f8c3052b8f9", + "sha256:8da14e678262e537372660ee89d604b752dd358fb7dfb279316fd2e6789f6478", + "sha256:99242c333ed17138134e9749f3a484518f41baebab0fc0f0fe7c657ab8090c07", + "sha256:a455eb6ae0ab4ed8d50d3a2762b5e3c7e17a8ee331dc40f468ce416ba4c0840d", + "sha256:af7ca101b23233745b813481ea91694ecea95c3e3b6eed8dca37be39a60f6894", + "sha256:afcda49474748548fa1b7dbb9f557c3b9feafeb0dd3c2e05d771cc935c86646e", + "sha256:b27be32b6a4993a5b6dfcde11280ab962e3df01445c89cd931b69679e18a0cdb", + "sha256:baf0d5731b8918efce8084e5e28e403aab116e1081b77822123bb55e0841e5e7", + "sha256:ce740fcd9a89d70f8183dc2ee752d342d75ff629626020c01857d86aed696f51", + "sha256:e4ba60dd948bd9db6adba7a654601e757b2a28013877afb108da1abc6654661e", + "sha256:f6d98d81f12281cb796a6427f295645de284bcc6b5e2e1958fffea88529ce835", + "sha256:fd3d21be01a3a5bfff76fe018d183b4adb64880f2ceed4a4505c42f1907fcbbb" + ], + "version": "==2.5.6" + }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '3.7'", - "version": "==5.4.0" + "markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2'", + "version": "==1.17.0" }, - "qtpy": { + "soupsieve": { "hashes": [ - "sha256:0603c9c83ccc035a4717a12908bf6bc6cb22509827ea2ec0e94c2da7c9ed57c5", - "sha256:8d6d544fc20facd27360ea189592e6135c614785f0dec0b4f083289de6beb408" + "sha256:7dcf6022eed0399eb9934a75e020148f7a2024c37b7dfcd3cf2c5505d69c364e", + "sha256:fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21" ], - "markers": "python_version >= '3.7'", - "version": "==2.3.0" + "markers": "python_full_version >= '3.11.5'", + "version": "==3.0.3" }, - "scikit-build": { + "stack-data": { "hashes": [ - "sha256:da40dfd69b2456fad1349a894b90180b43712152b8a85d2a00f4ae2ce8ac9a5c", - "sha256:dd236b60330f243e79a9795952c6efeb6e28fd0bd7a35fd92eb490456ae29356" + "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", + "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" ], - "index": "pypi", - "version": "==0.11.1" + "version": "==0.6.3" }, - "scikit-image": { + "terminado": { "hashes": [ - "sha256:113bcacdfc839854f527a166a71768708328208e7b66e491050d6a57fa6727c7", - "sha256:11eec2e65cd4cd6487fe1089aa3538dbe25525aec7a36f5a0f14145df0163ce7", - "sha256:178210582cc62a5b25c633966658f1f2598615f9c3f27f36cf45055d2a74b401", - "sha256:1fda9109a19dc9d7a4ac152d1fc226fed7282ad186a099f14c0aa9151f0c758e", - "sha256:6b65a103edbc34b22640daf3b084dc9e470c358d3298c10aa9e3b424dcc02db6", - "sha256:7bedd3881ca4fea657a894815bcd5e5bf80944c26274f6b6417bb770c3f4f8e6", - "sha256:86a834f9a4d30201c0803a48a25364fe8f93f9feb3c58f2c483d3ce0a3e5fe4a", - "sha256:87ca5168c6fc36b7a298a1db2d185a8298f549854342020f282f747a4e4ddce9", - "sha256:bd954c0588f0f7e81d9763dc95e06950e68247d540476e06cb77bcbcd8c2d8b3", - "sha256:c0876e562991b0babff989ff4d00f35067a2ddef82e5fdd895862555ffbaec25", - "sha256:c5c277704b12e702e34d1f7b7a04d5ee8418735f535d269c74c02c6c9f8abee2", - "sha256:e99fa7514320011b250a21ab855fdd61ddcc05d3c77ec9e8f13edcc15d3296b5", - "sha256:ee3db438b5b9f8716a91ab26a61377a8a63356b186706f5b979822cc7241006d" + "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", + "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e" ], - "index": "pypi", - "version": "==0.17.2" + "markers": "python_version >= '3.8'", + "version": "==0.18.1" }, - "scipy": { + "tifffile": { "hashes": [ - "sha256:168c45c0c32e23f613db7c9e4e780bc61982d71dcd406ead746c7c7c2f2004ce", - "sha256:213bc59191da2f479984ad4ec39406bf949a99aba70e9237b916ce7547b6ef42", - "sha256:25b241034215247481f53355e05f9e25462682b13bd9191359075682adcd9554", - "sha256:2c872de0c69ed20fb1a9b9cf6f77298b04a26f0b8720a5457be08be254366c6e", - "sha256:3397c129b479846d7eaa18f999369a24322d008fac0782e7828fa567358c36ce", - "sha256:368c0f69f93186309e1b4beb8e26d51dd6f5010b79264c0f1e9ca00cd92ea8c9", - "sha256:3d5db5d815370c28d938cf9b0809dade4acf7aba57eaf7ef733bfedc9b2474c4", - "sha256:4598cf03136067000855d6b44d7a1f4f46994164bcd450fb2c3d481afc25dd06", - "sha256:4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b", - "sha256:4f12d13ffbc16e988fa40809cbbd7a8b45bc05ff6ea0ba8e3e41f6f4db3a9e47", - "sha256:634568a3018bc16a83cda28d4f7aed0d803dd5618facb36e977e53b2df868443", - "sha256:65923bc3809524e46fb7eb4d6346552cbb6a1ffc41be748535aa502a2e3d3389", - "sha256:6b0ceb23560f46dd236a8ad4378fc40bad1783e997604ba845e131d6c680963e", - "sha256:8c8d6ca19c8497344b810b0b0344f8375af5f6bb9c98bd42e33f747417ab3f57", - "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62", - "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d", - "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437", - "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2", - "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54", - "sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474", - "sha256:e360cb2299028d0b0d0f65a5c5e51fc16a335f1603aa2357c25766c8dab56938", - "sha256:e98d49a5717369d8241d6cf33ecb0ca72deee392414118198a8e5b4c35c56340", - "sha256:ed572470af2438b526ea574ff8f05e7f39b44ac37f712105e57fc4d53a6fb660", - "sha256:f87b39f4d69cf7d7529d7b1098cb712033b17ea7714aed831b95628f483fd012", - "sha256:fa789583fc94a7689b45834453fec095245c7e69c58561dc159b5d5277057e4c" + "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", + "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170" ], - "index": "pypi", - "version": "==1.5.4" + "markers": "python_version >= '3.11'", + "version": "==2026.3.3" }, - "send2trash": { + "tinycss2": { "hashes": [ - "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d", - "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08" + "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", + "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957" ], - "version": "==1.8.0" + "markers": "python_version >= '3.10'", + "version": "==1.5.1" }, - "setuptools": { + "tornado": { "hashes": [ - "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c", - "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48" + "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", + "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", + "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", + "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", + "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", + "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", + "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", + "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", + "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", + "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694" ], - "markers": "python_version >= '3'", - "version": "==67.2.0" + "markers": "python_version >= '3.9'", + "version": "==6.5.10" }, - "simpleitk": { + "traitlets": { "hashes": [ - "sha256:09911709ae2d3e29115dce3e78fa437dd78836812306be0a252c2da120928435", - "sha256:13628c2b22679a8b36d209ebc1d0ace53fe4ab6c4e8654eff0bc96ee40f702c8", - "sha256:21bb06e640a8540d53b3ee00a8fcdcdf6e1a48aa4cf28172cc08bc153f012b76", - "sha256:46772fa3e6e167de0821b85e122debf90833d84daa23c5c66053cf28437f4573", - "sha256:4728c090a2e95b169ef8edab42f068fcb8657cac1d3d98df805ca468d9c45c38", - "sha256:5ab5ab167d27bb24e958f02df26b467603197b71e91b13b1635763cf64d2431f", - "sha256:6522c9e6a24301d0886f5e452724f165e0bd521548a9795b5bdc62031e950d1d", - "sha256:666b7b88bc53c07d012181fa5306134d2d837cc59bc99e5c6641954f81c6501a", - "sha256:6efe188b0f375f35734b272fe06863731d9ab08f9f0ff3e4bc342f941cf4ca08", - "sha256:73caad7fb842985abc0ccf3c2590fd4e66e2b66e846b2eb17b759ffe4cc0455c", - "sha256:8d795689bc613907196b1dc322e3b6247ec19c8e94cc12704a267d017551cb61", - "sha256:95070b5cd4293586aa788edf79593d1b9600d2c55ffe641920090f58dd69b995", - "sha256:a49b02bd405a3630be5388c0cafdcee951a8e8a81351fcca9e55b6b1b76f0ff9", - "sha256:cb969855ea1cd14839b10755a9b3cc1528cc912d4d5cc151c04f46fbcbd416c2", - "sha256:dc077928ab993170da5eed27f4737c9325a9fe705ff871945d0a6d6f15e24fab", - "sha256:e0be3a611b447fb923ebfaacc3ca1ce42d09aa31882086efdb792329e9ab123b", - "sha256:ed444d72a6a5f4870077a1ddf376974ac927afe0a2b7c8a34d359b074ac21c2d", - "sha256:f83fdde9eb91d0b01af73e64e22d65b26dbaf1edf6e6220a2ab21123229e0863", - "sha256:fa87ed3d73732a4668d8e216aac6e6d547ec92b566681a5d34ca5b7d86c569dc", - "sha256:fac923ee2c3c09dcee9b2a80c0d2bd886765a0727eba7a3befa3a36a8e17170f" + "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", + "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b" ], - "index": "pypi", - "version": "==2.0.2" + "markers": "python_version >= '3.10'", + "version": "==5.16.1" }, - "six": { + "typing-extensions": { + "hashes": [ + "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" + ], + "markers": "python_version >= '3.9'", + "version": "==4.16.0" + }, + "tzdata": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", + "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2'", + "version": "==2026.5" }, - "sniffio": { + "uri-template": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", + "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363" ], "markers": "python_version >= '3.7'", "version": "==1.3.0" }, - "soupsieve": { + "urllib3": { "hashes": [ - "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", - "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" + "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", + "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63" ], - "markers": "python_version >= '3.6'", - "version": "==2.3.2.post1" + "markers": "python_version >= '3.10'", + "version": "==2.8.0" }, - "stack-data": { + "wcwidth": { "hashes": [ - "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815", - "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8" - ], - "version": "==0.6.2" + "sha256:0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2", + "sha256:0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b", + "sha256:138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2", + "sha256:196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270", + "sha256:1bf361c8705576760623b4724ae564666d73b016f9a778bcfd1c7345378ef4ec", + "sha256:2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec", + "sha256:33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9", + "sha256:42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724", + "sha256:48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8", + "sha256:5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c", + "sha256:59dab4049cbd982b478bca098528df2c79a9160636a3a163ffebffcbd7d1b892", + "sha256:674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724", + "sha256:67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04", + "sha256:734aa9405b321d1042301aa19c943c4731ee9e3460e4f8feea3299c064c97a14", + "sha256:751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389", + "sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07", + "sha256:89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e", + "sha256:8b4e381590b9b7390e07e22b2c0c1bb96ce50e1d2243c866d9387600362d51ed", + "sha256:97b878d1e158da5ed9ac5aac53fa3a55e282103af6a09ec353865613d1a31a76", + "sha256:9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79", + "sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17", + "sha256:ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b", + "sha256:b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa", + "sha256:bb08ceb501d6aaf94066c3ee122dd825b152df40ff0bd0df4dc27126233b948e", + "sha256:c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7", + "sha256:e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d", + "sha256:eda88ffdc97c0fbf193d407114f2c7a54b379f67f6e52a7531ee3b9fe749eca7", + "sha256:ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4", + "sha256:f2f7b3bba5a5d5f31fc350fd36ce5b84b693c83b7eb95ee630b720da5a5ce06f" + ], + "markers": "python_version >= '3.9'", + "version": "==0.9.2" + }, + "webcolors": { + "hashes": [ + "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", + "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf" + ], + "markers": "python_version >= '3.10'", + "version": "==25.10.0" }, - "terminado": { + "webencodings": { "hashes": [ - "sha256:6ccbbcd3a4f8a25a5ec04991f39a0b8db52dfcd487ea0e578d977e6752380333", - "sha256:8650d44334eba354dd591129ca3124a6ba42c3d5b70df5051b6921d506fdaeae" + "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", + "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b" ], - "markers": "python_version >= '3.7'", - "version": "==0.17.1" + "markers": "python_version >= '3.10'", + "version": "==0.6.1" }, - "tifffile": { + "websocket-client": { "hashes": [ - "sha256:458df5ad9a5217f668edd636dc19fbc736062bf78aac823ab84cdbae9de8eaa6", - "sha256:6df45c1eab632eadb9384994c8ae8bc424cd3c26e9508481a345dbef1dcfd480" + "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", + "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce" ], - "markers": "python_version >= '3.8'", - "version": "==2023.2.3" - }, - "tinycss2": { + "markers": "python_version >= '3.10'", + "version": "==1.9.2" + } + }, + "develop": { + "build": { "hashes": [ - "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", - "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" + "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", + "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.1" + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==1.6.1" + }, + "iniconfig": { + "hashes": [ + "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", + "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.1" + }, + "librt": { + "hashes": [ + "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", + "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", + "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", + "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", + "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", + "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", + "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", + "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", + "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", + "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", + "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", + "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", + "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", + "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", + "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", + "sha256:20fe0bf9053885e21c62b3e091fb5e73e1c54d1daf2e70eb388d70763bcd4220", + "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", + "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", + "sha256:273d00be33792a15189331df10f1f1331621b043881e66c6c4377f7f776e1291", + "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", + "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", + "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", + "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", + "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", + "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", + "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", + "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", + "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", + "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", + "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", + "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", + "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", + "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", + "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", + "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", + "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", + "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", + "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", + "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", + "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", + "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", + "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", + "sha256:4b6183e2e2e0ee00aac2ec07c7f7d151c97e85666b304f574b71cff0f9fccc4e", + "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", + "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", + "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", + "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", + "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", + "sha256:5750a105b42a416f930edc59054927a406effb2550cd5bab92ad7a5842ed5d05", + "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", + "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", + "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", + "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", + "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", + "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", + "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", + "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", + "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", + "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", + "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", + "sha256:6a63610fa76524edfa605b5b259a603915c7a6e10e54f003e5503030506e81de", + "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", + "sha256:6c8893eae2fd13c5488d94056f3e6e5cf3142bfb1c4acaf136cb33d760c5964b", + "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", + "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", + "sha256:71b93b42784e25b975079573c642a8fedb049a7bb31d70a51721b1666b3b2ced", + "sha256:7393c9a48dcce4817dbd4b0d8ff6237efe9b0a0609f5b0adaef315f8541726b5", + "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", + "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", + "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", + "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", + "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", + "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", + "sha256:845a511b60ca43b9880dcc84a9784c891d6a2098c829130b320846c69c9c0c68", + "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", + "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", + "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", + "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", + "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", + "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", + "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", + "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", + "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", + "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", + "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", + "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", + "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", + "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", + "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", + "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", + "sha256:aa9357a1b4d4fc787bb718a59cb1112c28c8a976d6bfa268b71cc0a4ab8f3a94", + "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", + "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", + "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", + "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", + "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", + "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", + "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", + "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", + "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", + "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", + "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", + "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", + "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", + "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", + "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", + "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", + "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", + "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", + "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", + "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", + "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", + "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", + "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", + "sha256:d608f0bf3b8cbddd0067fe02cb8cab7d13e8eb9386b23a1843d4363044fd9e22", + "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", + "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", + "sha256:df183721229ae51eef90108c115b43e98cb169b6155d34480338e5fc6616df00", + "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", + "sha256:e1967e36ac4cae0c7e9615ad32e1a513cdacff79f9e8afb28bedc91caf48b4f3", + "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", + "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", + "sha256:e9ce0bc440e7fd09b5f51f372f0f6640658f854b8fb05260f819cc669c93c42d", + "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", + "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", + "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", + "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", + "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", + "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", + "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", + "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", + "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", + "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", + "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", + "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", + "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", + "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f" + ], + "markers": "python_version >= '3.9'", + "version": "==0.16.0" }, - "tornado": { + "mypy": { + "hashes": [ + "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", + "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", + "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", + "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", + "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", + "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", + "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", + "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", + "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", + "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", + "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", + "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", + "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", + "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", + "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", + "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", + "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", + "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", + "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", + "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", + "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", + "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", + "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", + "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", + "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", + "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", + "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", + "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", + "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", + "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", + "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", + "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", + "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", + "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", + "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", + "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", + "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", + "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", + "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", + "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", + "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", + "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", + "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", + "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==1.20.2" + }, + "mypy-extensions": { "hashes": [ - "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca", - "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72", - "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23", - "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8", - "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b", - "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9", - "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13", - "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75", - "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac", - "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e", - "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b" + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" ], - "markers": "python_version >= '3.7'", - "version": "==6.2" + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, - "traitlets": { + "packaging": { "hashes": [ - "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", - "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9" + "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", + "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" ], - "markers": "python_version >= '3.7'", - "version": "==5.9.0" + "markers": "python_version >= '3.9'", + "version": "==26.3" }, - "wcwidth": { + "pathspec": { "hashes": [ - "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", - "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" + "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", + "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" ], - "version": "==0.2.6" + "markers": "python_version >= '3.9'", + "version": "==1.1.1" }, - "webencodings": { + "pluggy": { "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", + "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8" ], - "version": "==0.5.1" + "markers": "python_version >= '3.10'", + "version": "==1.7.0" }, - "websocket-client": { + "pygments": { "hashes": [ - "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40", - "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e" + "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", + "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" ], - "markers": "python_version >= '3.7'", - "version": "==1.5.1" + "markers": "python_version >= '3.9'", + "version": "==2.21.0" }, - "wheel": { + "pyproject-hooks": { "hashes": [ - "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac", - "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8" + "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", + "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43" ], - "markers": "python_version >= '3.7'", - "version": "==0.38.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.3" }, - "widgetsnbextension": { + "pytest": { "hashes": [ - "sha256:003f716d930d385be3fd9de42dd9bf008e30053f73bddde235d14fbeaeff19af", - "sha256:eaaaf434fb9b08bd197b2a14ffe45ddb5ac3897593d43c69287091e5f3147bf7" + "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", + "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.1.1" + }, + "ruff": { + "hashes": [ + "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", + "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", + "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", + "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", + "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", + "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", + "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", + "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", + "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", + "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", + "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", + "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", + "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", + "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", + "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", + "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", + "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", + "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a" ], + "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==4.0.5" + "version": "==0.17.1" }, - "zipp": { + "typing-extensions": { "hashes": [ - "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", - "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" + "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" ], - "markers": "python_version >= '3.7'", - "version": "==3.13.0" + "markers": "python_version >= '3.9'", + "version": "==4.16.0" } - }, - "develop": {} + } } diff --git a/README.md b/README.md index 60f8878..395a4df 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,105 @@ -![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) +[![Tests](https://github.com/radxtools/collageradiomics/actions/workflows/tests.yml/badge.svg)](https://github.com/radxtools/collageradiomics/actions/workflows/tests.yml) +[![Documentation](https://readthedocs.org/projects/collageradiomics/badge/?version=latest)](https://collageradiomics.readthedocs.io/) # Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe) -# Table of Contents -- [Science](#science) - - [Overview](#overview) - - [References](#references) -- [Code](#code) -- [Installation & Setup](#installation--setup) - - [Pip Usage](#pip-usage) - - [Docker Notebooks](#docker-notebooks) - - [Docker Sandbox](#docker-sandbox) -- [Python Usage](#python-usage) +CoLlAGe produces thirteen voxel-wise Haralick statistics from local gradient +orientations in 2D or 3D images. The project includes a medical-image CLI, +notebooks, Docker targets, and a 3D Slicer extension. + +**The 0.4 development version changes numerical results.** Recompute cached +features from 0.3.x. See [numerical conventions](module/docs/source/numerics.rst). +The public import and thirteen-channel layout remain compatible. + +## Install from this checkout + +Use Python 3.10 or newer. Commands below run from the repository root: + +```shell +python3 -m venv env_collage +source env_collage/bin/activate +python -m pip install -e './module[cli]' +``` + +After publication, install with `python -m pip install 'collageradiomics[cli]'`. +The core needs NumPy, SciPy, scikit-image, and Mahotas. Example dependencies are +optional: `python -m pip install -r requirements.txt` installs them from this checkout. + +## Python usage + +```python +import numpy as np +from collageradiomics import Collage, HaralickFeature + +image = np.random.default_rng(42).random((32, 32)) +mask = np.zeros(image.shape, dtype=bool) +mask[8:24, 8:24] = True +collage = Collage(image, mask, svd_radius=2, num_unique_angles=8) +features = collage.execute() # (32, 32, 13) +entropy = collage.get_single_feature_output(HaralickFeature.Entropy) +print(entropy[mask].mean()) +``` + +Arrays use `(height,width,depth)` in 3D and return `(H,W,D,13,2)`, with theta at +index 0 and phi at index 1. Values outside the mask are NaN. SVD and Haralick +windows use edge replication at image boundaries. Three-dimensional gradients +include z; texture neighborhoods remain slice-wise. + +## Command line + +```shell +collageradiomics -i image.nii.gz -m mask.nii.gz -o features.csv --extendstats +collageradiomics -i cases.csv -o features.csv --verbose +collageradiomics --help +``` + +Batch CSVs need `ID,Image,Mask` columns. Relative paths are resolved against the +CSV directory. Successful cases become output rows; failures are recorded in +`errors_features.csv` and cause a nonzero exit code. Image and mask size, spacing, +origin, and direction must match. `--dimensions 2` accepts 2D images or single-slice +volumes; it never discards slices. Use `--label` to select one mask label. + +## Examples and Slicer + +```shell +python -m pip install -e './module[examples]' +jupyter lab jupyter/examples/example.ipynb +python module/test_script.py --output-dir output_data +python examples/isbi.py IMAGE MASK --output-dir output_data +``` + +The notebook uses deterministic synthetic data and inline figures. The DICOM +example preserves numeric arrays in `.npy` and writes derived DICOM previews. +The [Slicer extension](slicer/collageradiomics/README.md) accepts labelmaps and +segmentations, supports both angles, and provides cancellable progress. + +## Development and tests + +For containerized extraction or notebooks, see [docker/README.rst](docker/README.rst). +Both targets build the checkout and include the CLI; obsolete personal base images +and the pinned 0.2.6 Docker hierarchy have been retired. + +```shell +python -m pip install -r requirements-test.txt +python -m pip install --no-build-isolation -e './module[cli]' +python -m pytest +python -m mypy +python -m ruff check module/collageradiomics module/_collageradiomics tests cli +``` + +CI tests the minimum stack on Python 3.10, current dependencies on 3.11–3.13, +source/wheel rebuilding, and the extension in Slicer 5.12.4. Add +`-c constraints-test.txt` to install commands for the minimum stack, or use +`pipenv sync --dev` for the locked Python 3.11 environment. +Conda users can run `conda env create -f environment.yml` from the repository root; +see [the Conda guide](packaging/conda/README.rst) for local package builds. + +See [CONTRIBUTING.rst](CONTRIBUTING.rst) for style, tests, and documentation builds. +The [backlog record](BACKLOG.rst) maps all 28 issues to fixes and external follow-ups. +[Full documentation](https://collageradiomics.readthedocs.io/). # Science ## Overview -_[Back to **Table of Contents**](#table-of-contents)_ **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. @@ -39,7 +123,6 @@ Each of the 13 **CoLlAGe** correlate to one of the 13 Haralick texture features[ 13. _MaximalCorrelationCoefficient_ ## References -_[Back to **Table of Contents**](#table-of-contents)_ @@ -48,177 +131,3 @@ If you make use of this implementation, please cite the following paper: [1] Prasanna, P., Tiwari, P., & Madabhushi, A. (2016). "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports", 6:37241. [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). - -# Code -_[Back to **Table of Contents**](#table-of-contents)_ - -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. - -Documentation can be found at -http://collageradiomics.rtfd.io/ - -We depend on the following libraries: -- `matplotlib` -- `numpy` -- `scikit-learn` -- `scikit-build` -- `mahotas` -- `scipy` - -# Installation & Setup -_[Back to **Table of Contents**](#table-of-contents)_ - -## Pip Usage -```console -pip3 install --upgrade collageradiomics - -python3 -c 'import collageradiomics; print(collageradiomics.__name__)' -``` - -## Local usage -```shell -# get the code -git clone https://github.com/radxtools/collageradiomics -cd collageradiomics - -# set up virtual environment -python3 -m venv collageenv -source collageenv/bin/activate - -# install requirements -sudo apt -y install build-essential gcc gfortran python-dev libopenblas-dev liblapack-dev cython libjpeg-dev zlib1g-dev -pip3 install -r requirements.txt - -# run test script -cd modules -python3 test_script.py -``` - -## Docker Sandbox -_[Back to **Table of Contents**](#table-of-contents)_ - -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. - -```console -sudo docker pull radxtools/collageradiomics-pip:latest -sudo docker run -it -v $PWD:/root radxtools/collageradiomics-pip -``` -If your terminal prompt changes to `root@[random_string]:/#` then you are now working inside the standardized **Docker** sandbox container environment. - -Then you can try to import collage: -```shell -python3 -c 'import collageradiomics; print(collageradiomics.__name__)' -``` - -## Docker Notebooks - -### Prepare Jupyter Notebooks -To load the example jupyter notebooks, run the following commands (with or without `sudo` depending on your environment): -```console -sudo docker pull radxtools/collageradiomics-pip:latest -sudo docker run -it radxtools/collageradiomics-pip - -git clone https://github.com/radxtools/collageradiomics.git -sudo docker pull radxtools/collageradiomics-examples:latest -sudo docker run -it -p 8888:8888 -v $PWD:/root radxtools/collageradiomics-examples -``` - -### Exploring The Examples -_[Back to **Table of Contents**](#table-of-contents)_ - -1. Open up a web browser to http://localhost:8888 -![Jupyter Home](https://i.imgur.com/0XQ8OlT.png) -2. Navigate to the _Jupyter_ :arrow_right: _Examples_ directory. -![Jupyter Examples](https://i.imgur.com/NjdMlOr.png) -3. Click on one of the example `*.ipynb` files. -4. Run _Cell_ :arrow_right: _Run all_. -![Jupyter Run Cells](https://i.imgur.com/GaAaNAS.png) -![Jupyter Output](https://i.imgur.com/PapCcsg.png) -5. Feel free to add your own cells and run them to get familiar with the **CoLlAGe** code. -6. To stop the **Jupyter** notebook and exit the **Docker** image, press `Ctrl+C` twice: - -To run python code with collage: -```console -root@12b12d2bff59:/# python -Python 3.8.2 (default, Apr 27 2020, 15:53:34) -[GCC 9.3.0] on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> import collageradiomics ->>> collageradiomics.__name__ -'collageradiomics' -``` - -# Python Usage -_[Back to **Table of Contents**](#table-of-contents)_ -```python - -################################################## -# imports -import collageradiomics -import pydicom -import logging -from pydicom.pixel_data_handlers.util import apply_modality_lut, apply_voi_lut -from skimage.exposure import equalize_hist -import numpy as np -from sklearn.preprocessing import minmax_scale -from random import randint -################################################## - - -################################################## -# logging -level = logging.INFO -logging.basicConfig(level=level) -logger = logging.getLogger() -logger.setLevel(level) -logger.info('Hello, world.') -################################################## - - -################################################## -# loading data -local_dcm_file = 'test.dcm' -instance = pydicom.dcmread(local_dcm_file) -slice_instance_uid = instance.SOPInstanceUID -logger.debug(f'slice_instance_uid = {slice_instance_uid}') -################################################## - - -################################################## -# preprocessing -logger.info('Correcting image...') -np_array = instance.pixel_array -corrected = apply_modality_lut(np_array, instance) -corrected = apply_voi_lut(corrected, instance) -scaled_array = equalize_hist(corrected) -logger.debug(f'np.histogram(scaled_array) = {np.histogram(scaled_array)}') -logger.info('done.') -################################################## - - -################################################## -# rectangular selection -width = 50 -height = 50 -min_row = randint(30,300) -max_row = min_row + height -min_col = randint(30,300) -max_col = min_col + width - -original_shape = np_array.shape -logger.debug(f'original_shape = {original_shape}') -logger.info('Calculating collage features...') -mask_array = np.zeros(original_shape, dtype='int') -mask_array[min_row:max_row, min_col:max_col] = 1 -################################################## - - -################################################## -# run collage -textures = collageradiomics.Collage(scaled_array, mask_array).execute() - -logger.debug(f'textures.shape = {textures.shape}') -logger.debug(f'textures.dtype = {textures.dtype}') -logger.debug(f'np.histogram(textures.flatten()) = {np.histogram(textures.flatten(), range=(np.nanmin(textures), np.nanmax(textures)))}') -################################################## -``` diff --git a/cli/LICENSE b/cli/LICENSE new file mode 100644 index 0000000..8ddc387 --- /dev/null +++ b/cli/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2020 BrIC Laboratory + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/cli/README.rst b/cli/README.rst new file mode 100644 index 0000000..39da91b --- /dev/null +++ b/cli/README.rst @@ -0,0 +1,13 @@ +CoLlAGe CLI compatibility package +================================= + +This package installs ``collageradiomics[cli]>=0.4.0``. The command line interface +now ships with the core distribution. Both ``collageradiomics`` and the legacy +``collageradiomicscli.py`` commands remain available:: + + python -m pip install collageradiomicscli + collageradiomics --help + collageradiomics -i image.nii.gz -m mask.nii.gz -o features.csv + +See https://github.com/radxtools/collageradiomics for batch extraction, +installation from source, and numerical migration notes. diff --git a/cli/collageradiomicscli.py b/cli/collageradiomicscli.py index e64e92d..33073b0 100644 --- a/cli/collageradiomicscli.py +++ b/cli/collageradiomicscli.py @@ -1,246 +1,7 @@ -#!usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 +"""Compatibility entry point; install collageradiomics[cli] for dependencies.""" -import click -import os -import sys -import SimpleITK as sitk -import csv -from scipy import stats -import numpy as np +from _collageradiomics.cli import run -import collageradiomics - -@click.command() -@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.') -@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') -@click.option('-o', '--outputfile', required=True, help='Path to the output CSV file.') -@click.option('-v', '--verbose', default=True, help='Provides additional debug output.') -@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)) -@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.') -@click.option('-h', '--haralickwindow', default=-1, help='Number of pixels around each pixel used to calculate the haralick texture. DEFAULTS to svdradius * 2 - 1.') -@click.option('-b', '--binsize', default=64, help='Number of bins to use while calculating the grey level cooccurence matrix. DEFAULTS to 64.') -@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.') -@click.option('-e', '--extendstats', is_flag=True, help='Provides additional statistical metrics (mean and IQR) in the output.') - -def run(input, mask, outputfile, verbose, dimensions, svdradius, haralickwindow, binsize,label, extendstats): - """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.""" - - if input.endswith('.csv'): - header = ['ID', 'Image', 'Mask', 'svdradius', 'haralickwindow', 'binsize', 'label'] - features_list = [] - list_failed_cases = [['ID', 'Image', 'Mask', 'Error']] - if dimensions == 2: - suffix = '' - for feature in collageradiomics.HaralickFeature: - if extendstats: - 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]) - else: - features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Variance'+suffix]) - header.append(features_list) - output_list = [header] - else: - for suffix in ['Theta', 'Phi']: - for feature in collageradiomics.HaralickFeature: - if extendstats: - 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]) - else: - features_list.extend(['Collage'+feature.name+'Median'+suffix, 'Collage'+feature.name+'Skewness'+suffix, 'Collage'+feature.name+'Kurtosis'+suffix, 'Collage'+feature.name+'Variance'+suffix]) - header.extend(features_list) - output_list = [header] - - with open(input, newline='') as csvfile: - reader = csv.DictReader(csvfile) - for row in reader: - output_case = [] - try: - case_id = row['ID'] - image_filepath = row['Image'] - mask_filepath = row['Mask'] - image = sitk.ReadImage(image_filepath) - mask = sitk.ReadImage(mask_filepath) - - output_case.extend([case_id, image_filepath, mask_filepath, svdradius, haralickwindow, binsize, label]) - - # Check if user wants to select single label from the mask - if label != -1: - mask = sitk.BinaryThreshold(mask, lowerThreshold = label, upperThreshold = label, insideValue = 1, outsideValue = 0) - - image_array = sitk.GetArrayFromImage(image) - mask_array = sitk.GetArrayFromImage(mask) - - # Collage is expecting array with x,y,z but sitk.GetArrayFromImage as z,y,x, so x show be swapped by z - if dimensions != 2: - image_array = np.swapaxes(image_array,0,2) - mask_array = np.swapaxes(mask_array,0,2) - - # Remove any extra array dimensions if the user explicitly asks for 2D. - if dimensions == 2: - image_array = image_array[:,:,0] - mask_array = mask_array [:,:,0] - - collage = collageradiomics.Collage( - image_array, - mask_array, - svd_radius=svdradius, - verbose_logging=verbose, - num_unique_angles=binsize) - - collage.execute() - - for feature in collageradiomics.HaralickFeature: - feature_output = collage.get_single_feature_output(feature) - if image_array.ndim == 2: - feature_output = feature_output[~np.isnan(feature_output)] - - # NumPy supports median natively, we'll use that. - median = np.nanmedian(feature_output, axis=None) - - # Use SciPy for kurtosis, variance, and skewness. - feature_stats = stats.describe(feature_output, axis=None) - - if extendstats: - mean = feature_stats.mean #np.nanmean(feature_output, axis=None) - iqr = stats.iqr(feature_output) - - output_case.extend([median, iqr, feature_stats.skewness, feature_stats.kurtosis, feature_stats.mean, feature_stats.variance]) - else: - output_case.extend([median, feature_stats.skewness, feature_stats.kurtosis, feature_stats.variance]) - - else: - # Extract phi and theta angles. - feature_output_theta = feature_output[:,:,:,0] - feature_output_phi = feature_output[:,:,:,1] - - # Remove NaN for stat calculations. - feature_output_theta = feature_output_theta[~np.isnan(feature_output_theta)] - feature_output_phi = feature_output_phi[~np.isnan(feature_output_phi)] - - # NumPy supports median natively, we'll use that. - median_theta = np.nanmedian(feature_output_theta, axis=None) - median_phi = np.nanmedian(feature_output_phi, axis=None) - - # Use SciPy for kurtosis, variance, and skewness. - feature_stats_theta = stats.describe(feature_output_theta.flatten(), axis=None) - feature_stats_phi = stats.describe(feature_output_phi.flatten(), axis=None) - - if extendstats: - mean_theta = feature_stats_theta.mean - mean_phi = feature_stats_phi.mean - iqr_theta = stats.iqr(feature_output_theta) - iqr_phi = stats.iqr(feature_output_phi) - - 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]) - else: - 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]) - output_list.append(output_case) - except RuntimeError as err: - list_failed_cases.append([case_id, image_filepath, mask_filepath, err]) - except ValueError as err: - list_failed_cases.append([case_id, image_filepath, mask_filepath, err]) - - # Create collage radiomic features output csv file - with open(outputfile, 'w') as file: - writer = csv.writer(file) - writer.writerows(output_list) - - # Create errors output csv file - with open(os.path.join(os.path.dirname(outputfile), 'errors_' + os.path.basename(outputfile)), 'w') as file: - writer = csv.writer(file) - writer.writerows(list_failed_cases) - else: - image = sitk.ReadImage(input) - mask = sitk.ReadImage(mask) - - # Check if user wants to select single label from the mask - if label != -1: - mask = sitk.BinaryThreshold(mask, lowerThreshold = label, upperThreshold = label, insideValue = 1, outsideValue = 0) - - image_array = sitk.GetArrayFromImage(image) - mask_array = sitk.GetArrayFromImage(mask) - - # Collage is expecting array with x,y,z but sitk.GetArrayFromImage as z,y,x, so x show be swapped by z - if dimensions != 2: - image_array = np.swapaxes(image_array,0,2) - mask_array = np.swapaxes(mask_array,0,2) - - # Remove any extra array dimensions if the user explicitly asks for 2D. - if dimensions == 2: - image_array = image_array[:,:,0] - mask_array = mask_array [:,:,0] - - collage = collageradiomics.Collage( - image_array, - mask_array, - svd_radius=svdradius, - verbose_logging=verbose, - num_unique_angles=binsize) - - collage.execute() - - # Create a csv file at the passed in output file location. - with open(outputfile, 'w', newline='') as csv_output_file: - writer = csv.writer(csv_output_file) - - # Write the columns. - writer.writerow(['FeatureName', 'Value']) - for feature in collageradiomics.HaralickFeature: - feature_output = collage.get_single_feature_output(feature) - if image_array.ndim == 2: - feature_output = feature_output[~np.isnan(feature_output)] - - # NumPy supports median natively, we'll use that. - median = np.nanmedian(feature_output, axis=None) - - # Use SciPy for kurtosis, variance, and skewness. - feature_stats = stats.describe(feature_output, axis=None) - - # Write CSV row for current feature. - _write_csv_stats_row(writer, feature, median, feature_stats.skewness, feature_stats.kurtosis, feature_stats.variance) - else: - # Extract phi and theta angles. - feature_output_theta = feature_output[:,:,:,0] - feature_output_phi = feature_output[:,:,:,1] - - # Remove NaN for stat calculations. - feature_output_theta = feature_output_theta[~np.isnan(feature_output_theta)] - feature_output_phi = feature_output_phi[~np.isnan(feature_output_phi)] - - # NumPy supports median natively, we'll use that. - median_theta = np.nanmedian(feature_output_theta, axis=None) - median_phi = np.nanmedian(feature_output_phi, axis=None) - - # Use SciPy for kurtosis, variance, and skewness. - feature_stats_theta = stats.describe(feature_output_theta.flatten(), axis=None) - feature_stats_phi = stats.describe(feature_output_phi.flatten(), axis=None) - - if extendstats: - mean_phi = feature_stats_phi.mean - iqr_phi = stats.iqr(feature_output_phi.flatten()) - - mean_theta = feature_stats_theta.mean - iqr_theta = stats.iqr(feature_output_theta.flatten()) - - _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') - _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') - else: - # Write CSV rows for each angle. - _write_csv_stats_row(writer, feature, median_theta, feature_stats_theta.skewness, feature_stats_theta.kurtosis, feature_stats_theta.variance, 'Theta') - _write_csv_stats_row(writer, feature, median_phi, feature_stats_phi.skewness, feature_stats_phi.kurtosis, feature_stats_phi.variance, 'Phi') - -def _write_csv_stats_row(writer, feature, median, skewness, kurtosis, variance, suffix=''): - writer.writerow([f'Collage{feature.name}Median{suffix}', f'{median:.10f}']) - writer.writerow([f'Collage{feature.name}Skewness{suffix}', f'{skewness:.10f}']) - writer.writerow([f'Collage{feature.name}Kurtosis{suffix}', f'{kurtosis:.10f}']) - writer.writerow([f'Collage{feature.name}Variance{suffix}', f'{variance:.10f}']) - -def _write_csv_extented_stats_row(writer, feature, median, iqr, skewness, kurtosis, mean, variance, suffix=''): - writer.writerow([f'Collage{feature.name}Median{suffix}', f'{median:.10f}']) - writer.writerow([f'Collage{feature.name}IQR{suffix}', f'{iqr:.10f}']) - writer.writerow([f'Collage{feature.name}Skewness{suffix}', f'{skewness:.10f}']) - writer.writerow([f'Collage{feature.name}Kurtosis{suffix}', f'{kurtosis:.10f}']) - writer.writerow([f'Collage{feature.name}Mean{suffix}', f'{mean:.10f}']) - writer.writerow([f'Collage{feature.name}Variance{suffix}', f'{variance:.10f}']) - -if __name__ == '__main__': - run() +if __name__ == "__main__": + run() diff --git a/cli/pyproject.toml b/cli/pyproject.toml new file mode 100644 index 0000000..cf5bd9a --- /dev/null +++ b/cli/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=68", "wheel>=0.43"] +build-backend = "setuptools.build_meta" diff --git a/cli/setup.py b/cli/setup.py index 526bf30..7c52861 100644 --- a/cli/setup.py +++ b/cli/setup.py @@ -1,33 +1,17 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +"""Compatibility package; the CLI ships with the core distribution.""" + +from pathlib import Path from setuptools import setup -setup(name='collageradiomicscli', - version='1.0.0', - description='Get Collage features from an image and a binary mask', - url='https://github.com/radxtools/collageradiomics', - python_requires='>=3.6', - author='Toth Technology', - author_email='toth-tech@hillyer.me', - license='BSD-3-Clause', - zip_safe=False, - install_requires=[ - 'collageradiomics', - 'setuptools>=47', - 'SimpleITK==1.2.4', - 'click' - ], - scripts=['collageradiomicscli.py'], - classifiers=[ - 'Intended Audience :: Science/Research', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows :: Windows 10', - 'Operating System :: MacOS', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - ] - ) +setup( + name="collageradiomicscli", + version="1.1.0", + description="Compatibility package for the CoLlAGe command-line interface", + long_description=Path(__file__).with_name("README.rst").read_text(encoding="utf-8"), + long_description_content_type="text/x-rst", + url="https://github.com/radxtools/collageradiomics", + python_requires=">=3.10", + license="BSD-3-Clause", + install_requires=["collageradiomics[cli]>=0.4.0"], +) diff --git a/constraints-test.txt b/constraints-test.txt new file mode 100644 index 0000000..de86052 --- /dev/null +++ b/constraints-test.txt @@ -0,0 +1,5 @@ +# Minimum supported numerical stack, tested on Python 3.10. +mahotas==1.4.19 +numpy==1.26.4 +scipy==1.11.4 +scikit-image==0.22.0 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..a4264fd --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,39 @@ +ARG PYTHON_VERSION=3.12 +FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime + +ENV LANG=C.UTF-8 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + XDG_CACHE_HOME=/tmp/collage-cache \ + JUPYTER_CONFIG_DIR=/tmp/jupyter-config \ + JUPYTER_DATA_DIR=/tmp/jupyter-data \ + JUPYTER_RUNTIME_DIR=/tmp/jupyter-runtime \ + IPYTHONDIR=/tmp/ipython + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgomp1 \ + && rm -rf /var/lib/apt/lists/* +COPY module /src/module +RUN python -m pip install --no-cache-dir '/src/module[cli]' \ + && useradd --create-home --uid 10001 collage \ + && mkdir /work && chown collage:collage /work +WORKDIR /work +USER collage +CMD ["python"] + +FROM runtime AS notebook +USER root +RUN python -m pip install --no-cache-dir '/src/module[examples]' \ + && rm -rf /src +COPY jupyter/examples /opt/collage/examples +COPY sample_data /opt/collage/sample_data +USER collage +EXPOSE 8888 +CMD ["jupyter", "lab", "--ip=0.0.0.0", "--no-browser", "--ServerApp.root_dir=/work"] + +FROM runtime AS cli +USER root +RUN rm -rf /src +USER collage +ENTRYPOINT ["collageradiomics"] +CMD ["--help"] diff --git a/docker/README.rst b/docker/README.rst new file mode 100644 index 0000000..29f9ad8 --- /dev/null +++ b/docker/README.rst @@ -0,0 +1,30 @@ +Containers +========== + +Build from the repository root. These targets install the checkout, including +the CLI and SimpleITK; they do not install a stale package from PyPI:: + + docker build -f docker/Dockerfile --target cli -t collageradiomics-cli . + docker run --rm collageradiomics-cli --help + +Mount a data directory at /work and use the host user ID for output permissions:: + + docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" collageradiomics-cli \ + -i image.nii.gz -m mask.nii.gz -o features.csv + +For JupyterLab:: + + docker build -f docker/Dockerfile --target notebook -t collageradiomics-notebook . + docker run --rm --user "$(id -u):$(id -g)" -p 127.0.0.1:8888:8888 \ + -v "$PWD:/work" collageradiomics-notebook + +Use the token-bearing URL printed by Jupyter. The image preserves default +authentication. A copy of the runnable notebook is at /opt/collage/examples. +Both images use a non-root default user and temporary runtime/cache directories. + +The former dev_environment image hierarchy is retired. It depended on personal +base images and pinned collageradiomics 0.2.6. The single Dockerfile now supplies +both supported targets. The Containers workflow builds and smoke-tests both on +pull requests and relevant pushes. A manual run with publish=true publishes +ghcr.io/radxtools/collageradiomics-cli and collageradiomics-notebook with commit +and latest tags after the build checks pass. diff --git a/docker/dev_environment/collageradiomics_examples/Dockerfile b/docker/dev_environment/collageradiomics_examples/Dockerfile deleted file mode 100644 index 6f64c1d..0000000 --- a/docker/dev_environment/collageradiomics_examples/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM nathanhillyer/ml-base -ENV LANG C.UTF-8 -RUN PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ - $PIP_INSTALL \ - collageradiomics==0.2.6 \ - Pillow \ - && \ - ldconfig && \ - apt-get clean && \ - apt-get autoremove && \ - rm -rf /var/lib/apt/lists/* /tmp/* ~/* -CMD JUPYTER_RUNTIME_DIR=/tmp jupyter notebook --no-browser --ip=0.0.0.0 --allow-root --NotebookApp.token= --notebook-dir='/root' -EXPOSE 8888 diff --git a/docker/dev_environment/collageradiomics_pip/Dockerfile b/docker/dev_environment/collageradiomics_pip/Dockerfile deleted file mode 100644 index ea2c7f8..0000000 --- a/docker/dev_environment/collageradiomics_pip/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM nathanhillyer/ubuntu-base -ENV LANG C.UTF-8 -RUN PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ - $PIP_INSTALL \ - collageradiomics==0.2.6 \ - && \ - ldconfig && \ - apt-get clean && \ - apt-get autoremove && \ - rm -rf /var/lib/apt/lists/* /tmp/* ~/* \ No newline at end of file diff --git a/docker/dev_environment/ml_base/Dockerfile b/docker/dev_environment/ml_base/Dockerfile deleted file mode 100644 index 53f73e6..0000000 --- a/docker/dev_environment/ml_base/Dockerfile +++ /dev/null @@ -1,57 +0,0 @@ -FROM nathanhillyer/ubuntu-base -ENV LANG C.UTF-8 -RUN APT_INSTALL="apt-get install -y --no-install-recommends" && \ - PIP_INSTALL="python -m pip install --upgrade --no-cache-dir --retries 10 --timeout 60" && \ - GIT_CLONE="git clone --depth 10" && \ - DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ - software-properties-common \ - && \ - add-apt-repository ppa:deadsnakes/ppa && \ - apt-get update && \ - DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ - python3.8 \ - python3.8-dev \ - python3-distutils-extra \ - && \ - wget -O ~/get-pip.py \ - https://bootstrap.pypa.io/get-pip.py && \ - python3.8 ~/get-pip.py && \ - ln -s /usr/bin/python3.8 /usr/local/bin/python3 && \ - ln -s /usr/bin/python3.8 /usr/local/bin/python && \ - $PIP_INSTALL \ - setuptools \ - && \ - $PIP_INSTALL \ - numpy \ - scipy \ - pandas \ - cloudpickle \ - scikit-image>=0.14.2 \ - scikit-learn \ - matplotlib \ - Cython \ - tqdm \ - imageio \ - && \ - $PIP_INSTALL \ - pyradiomics \ - mahotas \ - eo-learn-features \ - && \ - $PIP_INSTALL -i https://test.pypi.org/simple/ \ - rad-i \ -# ================================================================== -# jupyter -# ------------------------------------------------------------------ - $PIP_INSTALL \ - jupyter \ - && \ -# ================================================================== -# config & cleanup -# ------------------------------------------------------------------ - ldconfig && \ - apt-get clean && \ - apt-get autoremove && \ - rm -rf /var/lib/apt/lists/* /tmp/* ~/* -CMD jupyter notebook --no-browser --ip=0.0.0.0 --allow-root --NotebookApp.token= --notebook-dir='/root' -EXPOSE 8888 \ No newline at end of file diff --git a/docker/dev_environment/ubuntu_base/Dockerfile b/docker/dev_environment/ubuntu_base/Dockerfile deleted file mode 100644 index d86188f..0000000 --- a/docker/dev_environment/ubuntu_base/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -FROM ubuntu:20.04 -ENV LANG C.UTF-8 -RUN APT_INSTALL="apt-get install -y --no-install-recommends" && \ - GIT_CLONE="git clone --depth 10" && \ - rm -rf /var/lib/apt/lists/* \ - /etc/apt/sources.list.d/cuda.list \ - /etc/apt/sources.list.d/nvidia-ml.list && \ - apt-get update && \ - DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ - software-properties-common \ - && \ - add-apt-repository "deb http://security.ubuntu.com/ubuntu xenial-security main" && \ - apt-get update && \ -# ================================================================== -# tools -# ------------------------------------------------------------------ - DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ - build-essential \ - apt-utils \ - ca-certificates \ - wget \ - git \ - vim \ - libssl-dev \ - curl \ - unzip \ - unrar \ - && \ - $GIT_CLONE https://github.com/Kitware/CMake ~/cmake && \ - cd ~/cmake && \ - ./bootstrap && \ - make -j"$(nproc)" install && \ - add-apt-repository ppa:deadsnakes/ppa && \ - apt-get update && \ - DEBIAN_FRONTEND=noninteractive $APT_INSTALL \ - python3.8 \ - python3.8-dev \ - python3-distutils-extra \ - && \ - wget -O ~/get-pip.py \ - https://bootstrap.pypa.io/get-pip.py && \ - python3.8 ~/get-pip.py && \ - ln -s /usr/bin/python3.8 /usr/local/bin/python3 && \ - ln -s /usr/bin/python3.8 /usr/local/bin/python && \ -# ================================================================== -# config & cleanup -# ------------------------------------------------------------------ - ldconfig && \ - apt-get clean && \ - apt-get autoremove && \ - rm -rf /var/lib/apt/lists/* /tmp/* ~/* - -EXPOSE 8888 \ No newline at end of file diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..657f7b3 --- /dev/null +++ b/environment.yml @@ -0,0 +1,22 @@ +name: collageradiomics +channels: + - conda-forge + - nodefaults +dependencies: + - python=3.12 + - pip + - setuptools>=68 + - wheel>=0.43 + - python-build>=1.2 + - numpy>=1.26,<3 + - scipy>=1.11,<2 + - scikit-image>=0.22,<1 + - mahotas>=1.4.19,<2 + - click>=8.1,<9 + - simpleitk>=2.3,<3 + - pydicom>=3,<4 + - pytest>=9.0.3,<10 + - mypy>=1.13,<2 + - ruff>=0.11,<1 + - pip: + - -e ./module[cli] diff --git a/examples/isbi.py b/examples/isbi.py index a039672..5de8d6a 100644 --- a/examples/isbi.py +++ b/examples/isbi.py @@ -1,90 +1,64 @@ -""" -The ISBI dataset can be found on https://theibsi.github.io/datasets -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. +"""Extract CoLlAGe from an aligned IBSI image/mask pair without hard-coded dataset paths.""" -Parameters: -Path of the input image. -Path of the input mask. -Haralick window sizes. -""" +import argparse from pathlib import Path -import nibabel as nib import numpy as np -from scipy.stats import kurtosis, skew - -from collageradiomics import Collage - -path_image = Path( - "/dataset/IBSI/ibsi_1_ct_radiomics_phantom/nifti/image/phantom.nii.gz" -) -path_mask = Path("/dataset/IBSI/ibsi_1_ct_radiomics_phantom/nifti/mask/mask.nii.gz") - -image = nib.load(path_image).get_fdata() -mask = nib.load(path_mask).get_fdata() - -image = np.swapaxes(image, 0, 2) -mask = np.swapaxes(mask, 0, 2) - -path_result = Path("./examples") - - -def compute_collage(image, mask, haralick_windows=[3, 5, 7, 9, 11]): - """This method computes Collage features - - Args: - image (path object): path of image - mask (path object): path of mask - haralick_windows (list, optional): Kernel window size. Defaults to [3, 5, 7, 9, 11]. - - Returns: - numpy array: mean, standard deviation, skewness, and kurtosis of COLLAGE features based on kernel size and orientation - """ - windows_length = len(haralick_windows) - - feats = np.zeros((13 * windows_length * 2, 4), dtype=np.double) - - for window_idx, haralick_window_size in enumerate(haralick_windows): - try: - collage = Collage( - image, - mask, - svd_radius=5, - verbose_logging=True, - num_unique_angles=64, - haralick_window_size=haralick_window_size, - ) - - collage_feats = collage.execute() - np.save( - rf"./{path_result}/COLLAGE_RAW_W{haralick_window_size}.npy", - collage_feats, - ) - print("saved") - - for orientation in range(2): - for collage_idx in range(13): - k = window_idx * collage_idx * orientation - feat = collage_feats[:, :, :, collage_idx, orientation].flatten() - feat = feat[~np.isnan(feat)] - - feats[k, 0] = feat.mean() - feats[k, 1] = feat.std() - feats[k, 2] = skew(feat) - feats[k, 3] = kurtosis(feat) - - except ValueError as err: - print(f"VALUE ERROR- {err}") - - except Exception as err: - print(f"EXCEPTION- {err}") - - return feats - - -feats = compute_collage(image, mask, haralick_windows=[3]) - -np.save( - rf"{path_result}/COLLAGE_STATS.npy", - feats, -) +from _collageradiomics.io import read_arrays +from _collageradiomics.statistics import summarize +from collageradiomics import Collage, HaralickFeature + + +def compute_collage( + image, + mask, + haralick_windows=(3, 5, 7, 9, 11), + output_dir=None, + svd_radius=5, + num_unique_angles=64, +): + """Return rows (window,feature,angle), with mean, population SD, skew, kurtosis.""" + angles = 2 if image.ndim == 3 else 1 + features = np.empty((len(haralick_windows) * len(HaralickFeature) * angles, 4)) + if output_dir is not None: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + for window_index, window in enumerate(haralick_windows): + output = Collage( + image, + mask, + svd_radius=svd_radius, + num_unique_angles=num_unique_angles, + haralick_window_size=window, + ).execute() + if output_dir is not None: + np.save(output_dir / f"COLLAGE_RAW_W{window}.npy", output) + for feature in HaralickFeature: + for angle in range(angles): + values = output[:, :, feature] if angles == 1 else output[:, :, :, feature, angle] + finite = values[np.isfinite(values)] + statistics = summarize(finite, extended=True) + index = (window_index * len(HaralickFeature) + int(feature)) * angles + angle + features[index] = [ + statistics["Mean"], + float(np.std(finite)), + statistics["Skewness"], + statistics["Kurtosis"], + ] + return features + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("image", type=Path) + parser.add_argument("mask", type=Path) + parser.add_argument("--output-dir", type=Path, default=Path("output_data")) + parser.add_argument("--windows", type=int, nargs="+", default=[3, 5, 7, 9, 11]) + args = parser.parse_args() + image, mask = read_arrays(args.image, args.mask) + values = compute_collage(image, mask, args.windows, output_dir=args.output_dir) + np.save(args.output_dir / "COLLAGE_STATS.npy", values) + + +if __name__ == "__main__": + main() diff --git a/jupyter/examples/example.ipynb b/jupyter/examples/example.ipynb index a24aed2..771c664 100644 --- a/jupyter/examples/example.ipynb +++ b/jupyter/examples/example.ipynb @@ -1,275 +1,101 @@ { "cells": [ { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "intro", "metadata": {}, - "outputs": [], - "source": [ - "import collageradiomics\n", - "import numpy as np\n", - "import SimpleITK as sitk\n", - "import matplotlib.pyplot as plt\n", - "from matplotlib.patches import Rectangle\n", - "from matplotlib.gridspec import GridSpec\n", - "from matplotlib.patches import Rectangle\n", - "from mpl_toolkits.axes_grid1 import make_axes_locatable" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], "source": [ - "# Load user data in this cell\n", - "\n", - "# Jupyter user: set this to define whether to test 3D calculations or not\n", - "use_3D = False\n", - "\n", - "# Read sample image (reads the grey image as an RGB array, so a 3D numpy array)\n", - "image_sitk = sitk.ReadImage('../../sample_data/BrainSliceTumor.png')\n", - "image_array = sitk.GetArrayFromImage(image_sitk)\n", - "mask_image = sitk.ReadImage('../../sample_data/BrainSliceTumorMask.png')\n", - "mask_array = sitk.GetArrayFromImage(mask_image)\n", + "# CoLlAGe: 2D and 3D examples\n", "\n", - "if use_3D:\n", - " # flip the first and last slice so there's some gradient in the Z dimension\n", - " image_array[:,:,0] = np.flip(image_array[:,:,0],0)\n", - " image_array[:,:,2] = np.flip(image_array[:,:,1],1)\n", - "else:\n", - " # extract a single slice\n", - " image_array = image_array[:,:,0]\n", - " mask_array = mask_array [:,:,0]" + "Install the checkout with `python -m pip install -e './module[examples]'` from the repository root.\n", + "These deterministic synthetic images need no external dataset. Figures render inside this notebook.\n", + "Version 0.4 corrects numerical results from earlier releases; see the numerical notes in the documentation.\n" ] }, { "cell_type": "code", - "execution_count": null, + "id": "imports", "metadata": {}, - "outputs": [], "source": [ - "# Helper functions for visualization\n", - "\n", - "def show_colored_image(figure, axis, image_data, colormap=plt.cm.jet):\n", - " \"\"\"Helper method to show a colored image in matplotlib.\n", - "\n", - "\n", - " :param figure: figure upon which to display\n", - " :type figure: matplotlib.figure.Figure\n", - " :param axis: axis upon which to display\n", - " :type axis: matplotlib.axes.Axes\n", - " :param image_data: image to display\n", - " :type image_data: numpy.ndarray\n", - " :param colormap: color map to convert for display. Defaults to plt.cm.jet.\n", - " :type colormap: matplotlib.colors.Colormap, optional\n", - " \"\"\"\n", - "\n", - " if image_data.ndim == 3:\n", - " image_data = image_data[:,:,0]\n", - " image = axis.imshow(image_data, cmap=colormap)\n", - " divider = make_axes_locatable(axis)\n", - " colorbar_axis = divider.append_axes(\"right\", size=\"5%\", pad=0.05)\n", - " figure.colorbar(image, cax=colorbar_axis)\n", - "\n", - "\n", - "def create_highlighted_rectangle(x, y, w, h):\n", - " \"\"\"Creates a matplotlib Rectangle object for a highlight effect\n", - "\n", - "\n", - " :param x: x location to start rectangle\n", - " :type x: int\n", - " :param y: y location to start rectangle\n", - " :type y: int\n", - " :param w: width of rectangle\n", - " :type w: int\n", - " :param h: height of rectangle\n", - " :type h: int\n", - "\n", - " :returns: Rectangle used to highlight within a plot\n", - " :rtype: matplotlib.patches.Rectangle\n", - " \"\"\"\n", - " return Rectangle((x, y), w, h, linewidth=3, edgecolor='cyan', facecolor='none')\n", - "\n", - "\n", - "def highlight_rectangle_on_image(image_data, min_x, min_y, w, h, colormap=plt.cm.gray):\n", - " \"\"\"Highlights a rectangle on an image at the passed in coordinate.\n", - "\n", - "\n", - " :param image_data: image to highlight\n", - " :type image_data: numpy.ndarray\n", - " :param min_x: x location to start highlight\n", - " :type min_x: int\n", - " :param min_y: y location to start highlight\n", - " :type min_y: int\n", - " :param w: width of highlight rectangle\n", - " :type w: int\n", - " :param h: height of highlight rectangle\n", - " :type h: int\n", - " :param colormap: color map to convert for display. Defaults to plt.cm.jet.\n", - " :type colormap: matplotlib.colors.Colormap, optional\n", - "\n", - " :returns: image array with highlighted rectangle\n", - " :rtype: numpy.ndarray\n", - " \"\"\"\n", - " figure, axes = plt.subplots(1, 2, figsize=(15, 15))\n", - "\n", - " # Highlight window within image.\n", - " show_colored_image(figure, axes[0], image_data, colormap)\n", - " axes[0].add_patch(create_highlighted_rectangle(min_x, min_y, w, h))\n", - "\n", - " # Crop window.\n", - " cropped_array = image_data[min_y:min_y + h, min_x:min_x + w]\n", - " axes[1].set_title(f'Cropped Region ({w}x{h})')\n", - " show_colored_image(figure, axes[1], cropped_array, colormap)\n", - "\n", - " plt.show()\n", - "\n", - " return cropped_array" - ] - }, - { - "cell_type": "code", + "%matplotlib inline\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from collageradiomics import Collage, HaralickFeature\n" + ], "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Show slice with mask\n", - "figure = plt.figure(figsize = (10, 10))\n", - "\n", - "extent = 0, image_array.shape[1], 0, image_array.shape[0]\n", - "\n", - "# show the image\n", - "plt.imshow(image_array[:,:,1] if use_3D else image_array, cmap = plt.cm.gray, extent=extent)\n", - "\n", - "# overlay the mask\n", - "plt.imshow(mask_array[:,:,0] if use_3D else mask_array, cmap = plt.cm.jet, alpha=0.3, extent=extent)\n", - "\n", - "plt.title('Input image')\n", - "\n", - "figure.axes[0].get_xaxis().set_visible(False)\n", - "figure.axes[0].get_yaxis().set_visible(False)\n", - "\n", - "print(image_array.shape)" - ] + "outputs": [] }, { "cell_type": "code", - "execution_count": null, + "id": "two-dimensional", "metadata": {}, - "outputs": [], "source": [ - "# Example of opti__init__.pyonal parameters\n", - "collage = collageradiomics.Collage(\n", - " image_array, \n", - " mask_array, \n", - " svd_radius=5, \n", - " verbose_logging=True,\n", - " num_unique_angles=64\n", - ")" - ] - }, - { - "cell_type": "code", + "rng = np.random.default_rng(42)\n", + "y, x = np.mgrid[:32, :32]\n", + "image = np.sin(x / 3) + np.cos(y / 5) + rng.normal(scale=0.1, size=x.shape)\n", + "mask = (x - 16)**2 + (y - 16)**2 <= 8**2\n", + "collage = Collage(image, mask, svd_radius=2, num_unique_angles=8)\n", + "features = collage.execute()\n", + "entropy = collage.get_single_feature_output(HaralickFeature.Entropy)\n", + "assert features.shape == (32, 32, 13)\n", + "assert np.isfinite(features[mask]).all()\n", + "assert np.isnan(features[~mask]).all()\n" + ], "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Run CoLlage Algorithm.Prepare\n", - "full_images = collage.execute()" - ] + "outputs": [] }, { "cell_type": "code", - "execution_count": null, + "id": "plot-two-dimensional", "metadata": {}, - "outputs": [], "source": [ - "# Display gradient\n", - "figure, axes = plt.subplots(1, 3, figsize=(15, 15))\n", - "show_colored_image(figure, axes[0], collage.dx)\n", - "axes[0].set_title(f'Gx size={collage.dx.shape}')\n", - "show_colored_image(figure, axes[1], collage.dy)\n", - "axes[1].set_title(f'Gy size={collage.dy.shape}')\n", - "show_colored_image(figure, axes[2], collage.dz)\n", - "axes[2].set_title(f'Gz size={collage.dz.shape}')" - ] - }, - { - "cell_type": "code", + "figure, axes = plt.subplots(1, 3, figsize=(12, 4))\n", + "for axis, values, title in zip(axes, [image, mask, entropy], ['Image', 'Mask', 'CoLlAGe entropy']):\n", + " axis.imshow(values, cmap='gray' if title == 'Image' else 'viridis')\n", + " axis.set_title(title)\n", + " axis.axis('off')\n", + "plt.show()\n", + "print('Mean masked entropy:', float(entropy[mask].mean()))\n" + ], "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "# Display dominant angles\n", - "figure, axes = plt.subplots(1, 2, figsize=(15, 15))\n", - "print(collage.dominant_angles.shape)\n", - "show_colored_image(figure, axes[0], collage.dominant_angles[:,:,:,0])\n", - "if use_3D:\n", - " show_colored_image(figure, axes[1], collage.dominant_angles[:,:,:,1])\n", - " axes[1].set_title('Secondary Angles: arctan(dz/(dx^2+dy^2))')\n", - "else:\n", - " axes[1].set_title('(Unused in 2D mode)')\n", - "axes[0].set_title('Dominant Angles: arctan(dy/dx)')\n" - ] + "outputs": [] }, { "cell_type": "code", - "execution_count": null, + "id": "three-dimensional", "metadata": {}, - "outputs": [], "source": [ - "# Display haralick\n", - "figure, axes = plt.subplots(3, 5, figsize=(15,15))\n", - "\n", - "for row in range(3):\n", - " for col in range(5):\n", - " feature = row*5+col\n", - " axis = axes[row][col]\n", - " axis.set_axis_off()\n", - " if feature>=13:\n", - " continue\n", - " collage_output = collage.get_single_feature_output(feature)\n", - " if use_3D:\n", - " collage_output = collage_output[:,:,:,0] # use dominant angle\n", - " show_colored_image(figure, axis, collage_output)\n", - " axis.set_title(f'Collage {feature+1}')" - ] + "volume = np.stack([image + z * 0.2 * np.cos(x / 4) for z in range(5)], axis=2)\n", + "volume_mask = np.repeat(mask[:, :, None], 5, axis=2)\n", + "collage_3d = Collage(volume, volume_mask, svd_radius=2, num_unique_angles=8)\n", + "features_3d = collage_3d.execute()\n", + "entropy_3d = collage_3d.get_single_feature_output(HaralickFeature.Entropy)\n", + "assert features_3d.shape == (32, 32, 5, 13, 2)\n", + "assert np.isfinite(features_3d[volume_mask]).all()\n", + "figure, axes = plt.subplots(1, 2, figsize=(8, 4))\n", + "for angle, title in enumerate(['Theta', 'Phi']):\n", + " axes[angle].imshow(entropy_3d[:, :, 2, angle], cmap='viridis')\n", + " axes[angle].set_title(title)\n", + " axes[angle].axis('off')\n", + "plt.show()\n" + ], + "execution_count": null, + "outputs": [] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "real-data", "metadata": {}, - "outputs": [], "source": [ - "# Extract a single collage feature by name and overlay it\n", - "which_feature = collageradiomics.HaralickFeature.Entropy\n", - "\n", - "alpha = 0.5 # transparency\n", - "\n", - "# extract the output\n", - "collage_output = collage.get_single_feature_output(which_feature)\n", - "print(collage_output.shape)\n", - "if use_3D:\n", - " collage_output = collage_output[:,:,1,1]\n", - "\n", - "# Show preview of larger version of image.\n", - "figure = plt.figure(figsize = (15, 15))\n", - "\n", - "# show the image\n", - "plt.imshow(image_array[:,:,1] if use_3D else image_array, cmap = plt.cm.gray, extent=extent)\n", - "\n", - "# overlay the collage output\n", - "plt.imshow(collage_output, cmap = plt.cm.jet, alpha=alpha, extent=extent)\n", + "For NIfTI, NRRD, or other medical images, use the command line:\n", "\n", - "figure.axes[0].get_xaxis().set_visible(False)\n", - "figure.axes[0].get_yaxis().set_visible(False)\n", + "```shell\n", + "collageradiomics -i image.nii.gz -m mask.nii.gz -o features.csv --extendstats\n", + "```\n", "\n", - "plt.title('Collage Overlay')" + "The CLI checks physical geometry and converts SimpleITK's `(z,y,x)` arrays to the core's\n", + "`(height,width,depth)` convention. A 3D result contains theta at index 0 and phi at index 1.\n", + "Save numeric arrays with `np.save` when retaining voxel-wise values and NaNs is important.\n" ] } ], @@ -280,18 +106,9 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" + "name": "python" } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/module/MANIFEST.in b/module/MANIFEST.in index ceeea23..2c749b6 100644 --- a/module/MANIFEST.in +++ b/module/MANIFEST.in @@ -1 +1,4 @@ include VERSION +include requirements.txt +include test.dcm +include test_script.py diff --git a/module/README.md b/module/README.md index 9474c7e..7ec6540 100644 --- a/module/README.md +++ b/module/README.md @@ -1 +1,21 @@ -Setup scripts and code for the collageradiomics pip module. \ No newline at end of file +# CoLlAGe radiomics + +Co-occurrence of Local Anisotropic Gradient Orientations captures local disorder +in image gradients. This Python package produces thirteen voxel-wise texture maps +for 2D and 3D images and includes optional medical-image CLI and notebook support. + +```shell +python -m pip install 'collageradiomics[cli]' +collageradiomics -i image.nii.gz -m mask.nii.gz -o features.csv +``` + +Requires Python 3.10 or newer. The numerical core uses NumPy, SciPy, scikit-image, +and Mahotas. Install `collageradiomics[examples]` for notebooks and DICOM examples. + +Version 0.4 corrects gradient orientation, feature mapping, and mask boundaries; +recompute cached results from earlier versions. The public import and +thirteen-feature output shape are preserved. + +[Documentation](https://collageradiomics.readthedocs.io/) +| [Source and issues](https://github.com/radxtools/collageradiomics) +| [Scientific reference](https://doi.org/10.1038/srep37241) diff --git a/module/VERSION b/module/VERSION index 6678432..1d0ba9e 100644 --- a/module/VERSION +++ b/module/VERSION @@ -1 +1 @@ -0.3.8 +0.4.0 diff --git a/module/_collageradiomics/__init__.py b/module/_collageradiomics/__init__.py new file mode 100644 index 0000000..36f00df --- /dev/null +++ b/module/_collageradiomics/__init__.py @@ -0,0 +1 @@ +"""Internal helpers shared by the CoLlAGe frontends.""" diff --git a/module/_collageradiomics/cli.py b/module/_collageradiomics/cli.py new file mode 100644 index 0000000..b914953 --- /dev/null +++ b/module/_collageradiomics/cli.py @@ -0,0 +1,174 @@ +"""Command-line extraction for one image or an ID,Image,Mask CSV.""" + +import csv +import logging +from pathlib import Path +from typing import Any + +import click +from collageradiomics import Collage, DifferenceVarianceInterpretation + +from .io import read_arrays +from .statistics import feature_statistics + + +def extract( + image: Path, + mask: Path, + dimensions: int | None, + label: int, + extended: bool, + options: dict[str, Any], +) -> dict[str, float]: + arrays = read_arrays(image, mask, dimensions, label) + return feature_statistics(Collage(*arrays, **options).execute(), extended) + + +def write_single(output: Path, values: dict[str, float]) -> None: + output.parent.mkdir(parents=True, exist_ok=True) + with output.open("w", newline="", encoding="utf-8") as stream: + writer = csv.writer(stream) + writer.writerow(["FeatureName", "Value"]) + writer.writerows((name, f"{value:.10f}") for name, value in values.items()) + + +def write_batch( + input_path: Path, + output: Path, + dimensions: int | None, + label: int, + extended: bool, + options: dict[str, Any], +) -> None: + errors_path = output.with_name("errors_" + output.name) + if errors_path.resolve() == input_path.resolve(): + raise ValueError("the errors report would overwrite the input CSV") + rows: list[dict[str, Any]] = [] + errors: list[dict[str, str]] = [] + fields = ["ID", "Image", "Mask", "svdradius", "haralickwindow", "binsize", "label"] + with input_path.open(newline="", encoding="utf-8-sig") as stream: + reader = csv.DictReader(stream) + if not {"ID", "Image", "Mask"} <= set(reader.fieldnames or []): + raise ValueError("input CSV must have ID, Image, and Mask columns") + for number, row in enumerate(reader, 2): + identity = {name: row.get(name) or "" for name in ("ID", "Image", "Mask")} + try: + if not all(identity.values()): + raise ValueError(f"CSV row {number} requires ID, Image, and Mask") + paths = [Path(identity[name]).expanduser() for name in ("Image", "Mask")] + image, mask = [p if p.is_absolute() else input_path.parent / p for p in paths] + values = extract(image, mask, dimensions, label, extended, options) + metadata = dict( + identity, + svdradius=options["svd_radius"], + haralickwindow=options["haralick_window_size"], + binsize=options["num_unique_angles"], + label=label, + ) + rows.append(dict(metadata, **values)) + fields.extend(key for key in values if key not in fields) + except (OSError, RuntimeError, TypeError, ValueError) as exc: + errors.append(dict(identity, Error=str(exc))) + output.parent.mkdir(parents=True, exist_ok=True) + with output.open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fields) + writer.writeheader() + writer.writerows(rows) + with errors_path.open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, ["ID", "Image", "Mask", "Error"]) + writer.writeheader() + writer.writerows(errors) + if errors: + raise ValueError(f"{len(errors)} case(s) failed; see {errors_path}") + + +@click.command() +@click.option( + "-i", + "--input", + "input_path", + required=True, + type=click.Path(exists=True, dir_okay=False, path_type=Path), + help="Image or CSV with ID, Image, Mask columns; CSV paths are relative to the CSV.", +) +@click.option( + "-m", + "--mask", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + help="Mask image; required for a single image. All nonzero labels are included by default.", +) +@click.option("-o", "--outputfile", required=True, type=click.Path(dir_okay=False, path_type=Path)) +@click.option("-v", "--verbose", is_flag=True, help="Show extraction progress.") +@click.option( + "-d", "--dimensions", type=click.IntRange(2, 3), help="Defaults to image dimensionality." +) +@click.option("-s", "--svdradius", default=5, show_default=True, type=click.IntRange(min=1)) +@click.option( + "-h", + "--haralickwindow", + default=-1, + type=int, + help="Odd window size >=3; -1 uses 2*svdradius+1.", +) +@click.option("-b", "--binsize", default=64, show_default=True, type=click.IntRange(min=2)) +@click.option( + "-l", "--label", default=-1, type=click.IntRange(min=-1), help="-1 includes all nonzero labels." +) +@click.option("-e", "--extendstats", is_flag=True, help="Also include mean and IQR.") +@click.option( + "--difference-variance", type=click.Choice(["x-minus-y", "probability"]), default="x-minus-y" +) +@click.option( + "--cooccurrence-angle", + type=float, + multiple=True, + help="GLCM angle in radians; repeat as needed.", +) +def run( + input_path: Path, + mask: Path | None, + outputfile: Path, + verbose: bool, + dimensions: int | None, + svdradius: int, + haralickwindow: int, + binsize: int, + label: int, + extendstats: bool, + difference_variance: str, + cooccurrence_angle: tuple[float, ...], +) -> None: + """Extract CoLlAGe feature summaries from aligned scalar images and masks.""" + logging.basicConfig( + level=logging.INFO if verbose else logging.WARNING, format="%(levelname)s: %(message)s" + ) + if outputfile.resolve() in {input_path.resolve(), mask.resolve() if mask else None}: + raise click.UsageError("output must differ from the input and mask files") + options = { + "svd_radius": svdradius, + "haralick_window_size": 2 * svdradius + 1 if haralickwindow == -1 else haralickwindow, + "num_unique_angles": binsize, + "cooccurence_angles": cooccurrence_angle or None, + "difference_variance_interpretation": ( + DifferenceVarianceInterpretation.XMinusYVariance + if difference_variance == "x-minus-y" + else DifferenceVarianceInterpretation.ProbabilityXMinusYVariance + ), + } + try: + if input_path.suffix.lower() == ".csv": + if mask is not None: + raise click.UsageError("batch input uses the Mask column; omit --mask") + write_batch(input_path, outputfile, dimensions, label, extendstats, options) + else: + if mask is None: + raise click.UsageError("--mask is required for image input") + write_single( + outputfile, extract(input_path, mask, dimensions, label, extendstats, options) + ) + except (OSError, RuntimeError, TypeError, ValueError) as exc: + raise click.ClickException(str(exc)) from exc + + +if __name__ == "__main__": + run() diff --git a/module/_collageradiomics/io.py b/module/_collageradiomics/io.py new file mode 100644 index 0000000..6fdbc41 --- /dev/null +++ b/module/_collageradiomics/io.py @@ -0,0 +1,44 @@ +"""Read scalar medical images using the same axis and geometry rules.""" + +from pathlib import Path +from typing import Any + +import numpy as np +from numpy.typing import NDArray + + +def read_arrays( + image_path: str | Path, + mask_path: str | Path, + dimensions: int | None = None, + label: int = -1, +) -> tuple[NDArray[Any], NDArray[Any]]: + import SimpleITK as sitk + + image, mask = sitk.ReadImage(str(image_path)), sitk.ReadImage(str(mask_path)) + if any(item.GetNumberOfComponentsPerPixel() != 1 for item in (image, mask)): + raise ValueError("image and mask must be scalar images") + if image.GetDimension() not in (2, 3): + raise ValueError("image must be 2D or 3D") + if image.GetDimension() != mask.GetDimension() or image.GetSize() != mask.GetSize(): + raise ValueError("image and mask must have the same dimensions and size") + for name in ("Spacing", "Origin", "Direction"): + left, right = getattr(image, f"Get{name}")(), getattr(mask, f"Get{name}")() + if not np.allclose(left, right, rtol=0, atol=1e-6): + raise ValueError( + f"image and mask {name.lower()} differ; align the mask before extraction" + ) + array, selection = sitk.GetArrayFromImage(image), sitk.GetArrayFromImage(mask) + if label != -1: + selection = selection == label + requested = image.GetDimension() if dimensions is None else dimensions + if requested == 2 and array.ndim == 3: + if array.shape[0] != 1: + raise ValueError("--dimensions 2 requires a 2D image or a single-slice volume") + array, selection = array[0], selection[0] + elif requested == 3 and array.ndim != 3: + raise ValueError("--dimensions 3 requires a 3D image") + if array.ndim == 3: + # SimpleITK is (z,y,x); the core expects (height,width,depth) = (y,x,z). + array, selection = np.moveaxis(array, 0, -1), np.moveaxis(selection, 0, -1) + return array, selection diff --git a/module/_collageradiomics/orientation.py b/module/_collageradiomics/orientation.py new file mode 100644 index 0000000..a9fb4de --- /dev/null +++ b/module/_collageradiomics/orientation.py @@ -0,0 +1,61 @@ +"""SVD of local gradient patches; all spatial arrays use (height,width,depth).""" + +import math +from collections.abc import Callable +from typing import Any + +import numpy as np +from numpy.lib.stride_tricks import sliding_window_view +from numpy.typing import NDArray +from scipy import linalg + +Array = NDArray[Any] + + +def dominant_angle( + x: int, + y: int, + z: int, + dx_windows: Array, + dy_windows: Array, + dz_windows: Array, + is_3d: bool | None = None, +) -> float | tuple[float, float]: + is_3d = dx_windows.shape[2] > 1 if is_3d is None else is_3d + patches = [dx_windows[y, x, z], dy_windows[y, x, z]] + if is_3d: + patches.append(dz_windows[y, x, z]) + gradients = np.column_stack([patch.ravel(order="F") for patch in patches]) + _, _, vh = linalg.svd(gradients, full_matrices=False, check_finite=False) + principal = vh[0].copy() + if principal[np.argmax(np.abs(principal))] < 0: + principal *= -1 + gx, gy = float(principal[0]), float(principal[1]) + theta = 0.0 if math.hypot(gx, gy) < np.finfo(float).eps else math.atan2(gy, gx) + if is_3d: + return theta, math.atan2(float(principal[2]), math.hypot(gx, gy)) + return theta + + +def dominant_angles( + dx: Array, + dy: Array, + dz: Array, + svd_radius: int, + is_3d: bool | None = None, + progress: Callable[[int, int], None] | None = None, +) -> NDArray[np.float32]: + is_3d = dx.shape[2] > 1 if is_3d is None else is_3d + window = (2 * svd_radius + 1, 2 * svd_radius + 1, 1) + # NumPy supplies this rolling view; no third-party source is copied or vendored. + windows = [sliding_window_view(field, window) for field in (dx, dy, dz)] + shape = windows[0].shape[:3] + output = np.zeros(shape + (2 if is_3d else 1,), dtype=np.float32) + total = math.prod(shape) + if progress: + progress(0, total) + for completed, (y, x, z) in enumerate(np.ndindex(shape), 1): + output[y, x, z] = dominant_angle(x, y, z, windows[0], windows[1], windows[2], is_3d) + if progress: + progress(completed, total) + return output diff --git a/module/_collageradiomics/progress.py b/module/_collageradiomics/progress.py new file mode 100644 index 0000000..9ed3c91 --- /dev/null +++ b/module/_collageradiomics/progress.py @@ -0,0 +1,31 @@ +"""Throttled logging and callbacks for the Python, CLI, and Slicer frontends.""" + +import logging +import time +from collections.abc import Callable + +ProgressCallback = Callable[[str, int, int], None] + + +class ProgressReporter: + def __init__(self, callback: ProgressCallback | None = None) -> None: + if callback is not None and not callable(callback): + raise TypeError("progress_callback must be callable") + self.callback = callback + self.stage = "" + self.last_percent = -10 + self.last_update = 0.0 + self.logger = logging.getLogger("collageradiomics") + + def __call__(self, stage: str, completed: int, total: int) -> None: + changed = stage != self.stage or completed == 0 + if changed: + self.stage, self.last_percent = stage, -10 + percent = int(100 * completed / total) + if changed or percent >= self.last_percent + 10 or completed == total: + self.logger.info("%s: %s/%s (%s%%)", stage, completed, total, percent) + self.last_percent = percent + now = time.monotonic() + if self.callback and (changed or completed == total or now - self.last_update >= 0.05): + self.callback(stage, completed, total) + self.last_update = now diff --git a/module/_collageradiomics/py.typed b/module/_collageradiomics/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/module/_collageradiomics/statistics.py b/module/_collageradiomics/statistics.py new file mode 100644 index 0000000..1f38b0e --- /dev/null +++ b/module/_collageradiomics/statistics.py @@ -0,0 +1,45 @@ +"""Consistent named summary statistics for feature maps.""" + +from typing import Any + +import numpy as np +from collageradiomics import HaralickFeature +from numpy.typing import NDArray + + +def summarize(values: NDArray[Any], extended: bool = False) -> dict[str, float]: + finite = np.asarray(values, dtype=float) + finite = finite[np.isfinite(finite)] + if not finite.size: + raise ValueError("feature contains no finite values") + centered = finite - np.mean(finite) + scale = float(np.max(np.abs(centered))) + # Normalize before calculating moments to avoid cancellation warnings and + # underflow for nearly constant maps. These are population (biased) moments. + skewness, kurtosis = 0.0, 0.0 + if scale > 0: + normalized = centered / scale + second = float(np.mean(normalized**2)) + skewness = float(np.mean(normalized**3)) / second**1.5 + kurtosis = float(np.mean(normalized**4)) / second**2 - 3 + result = {"Median": float(np.median(finite))} + if extended: + result["IQR"] = float(np.percentile(finite, 75) - np.percentile(finite, 25)) + result["Skewness"] = skewness + result["Kurtosis"] = kurtosis + if extended: + result["Mean"] = float(np.mean(finite)) + result["Variance"] = float(np.var(finite, ddof=1)) if finite.size > 1 else 0.0 + return result + + +def feature_statistics(output: NDArray[Any], extended: bool = False) -> dict[str, float]: + if output.ndim not in (3, 5): + raise ValueError("expected a 2D or 3D CoLlAGe feature map") + result: dict[str, float] = {} + for feature in HaralickFeature: + for angle, suffix in [(0, "")] if output.ndim == 3 else [(0, "Theta"), (1, "Phi")]: + values = output[:, :, feature] if output.ndim == 3 else output[:, :, :, feature, angle] + for statistic, value in summarize(values, extended).items(): + result[f"Collage{feature.name}{statistic}{suffix}"] = value + return result diff --git a/module/_collageradiomics/texture.py b/module/_collageradiomics/texture.py new file mode 100644 index 0000000..bdeb961 --- /dev/null +++ b/module/_collageradiomics/texture.py @@ -0,0 +1,42 @@ +"""Map co-occurrence statistics to CoLlAGe's thirteen public feature names.""" + +from typing import Any + +import mahotas +import numpy as np +from numpy.typing import NDArray +from scipy import linalg + + +def maximal_correlation(matrix: NDArray[Any]) -> float: + """Second singular value of P(i,j)/sqrt(Px(i)*Py(j)). + + This is the square root of the second eigenvalue of Haralick's Q matrix. + Empty marginals are removed; a constant variable has coefficient zero. + See https://haralick.org/journals/dependency_prl_1.pdf, section 3. + """ + probability = matrix.astype(float) / float(matrix.sum()) + rows, columns = probability.sum(axis=1), probability.sum(axis=0) + row_indices, column_indices = rows > 0, columns > 0 + if min(np.count_nonzero(row_indices), np.count_nonzero(column_indices)) < 2: + return 0.0 + normalized = probability[np.ix_(row_indices, column_indices)] / np.sqrt( + rows[row_indices, None] * columns[None, column_indices] + ) + singular_values = linalg.svdvals(normalized, check_finite=False) + return float(np.clip(singular_values[1], 0.0, 1.0)) + + +def named_haralick(matrix: NDArray[Any], x_minus_y_variance: bool) -> NDArray[np.float64]: + raw = mahotas.features.texture.haralick_features( + [matrix], + return_mean=True, + use_x_minus_y_variance=x_minus_y_variance, + ) + # Mahotas includes inverse difference moment at index 4. CoLlAGe's public + # list omits it and includes maximal correlation as the thirteenth feature. + output = np.empty(13, dtype=float) + output[:4] = raw[:4] + output[4:12] = raw[5:13] + output[12] = maximal_correlation(matrix) + return output diff --git a/module/_collageradiomics/validation.py b/module/_collageradiomics/validation.py new file mode 100644 index 0000000..26521e6 --- /dev/null +++ b/module/_collageradiomics/validation.py @@ -0,0 +1,47 @@ +"""Validate inputs before allocating or calculating feature maps.""" + +from collections.abc import Iterable +from numbers import Integral +from typing import Any + +import numpy as np +from numpy.typing import NDArray + + +def integer(value: Any, name: str, minimum: int = 1) -> int: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Integral): + raise TypeError(f"{name} must be an integer") + result = int(value) + if result < minimum: + raise ValueError(f"{name} must be at least {minimum}") + return result + + +def validate_arrays(image: NDArray[Any], mask: NDArray[Any]) -> None: + for name, array in [("image", image), ("mask", mask)]: + if not isinstance(array, np.ndarray): + raise TypeError(f"{name} must be a NumPy array") + if array.dtype.kind not in "biuf": + raise TypeError(f"{name} must contain real numeric values") + if not np.isfinite(array).all(): + raise ValueError(f"{name} must contain only finite values") + if image.ndim not in (2, 3): + raise ValueError("Expected a 2D or 3D image.") + if mask.shape != image.shape: + raise ValueError("Mask must be the same shape as image.") + if not all(image.shape): + raise ValueError("image dimensions must not be empty") + if not np.any(mask): + raise ValueError("mask must contain at least one nonzero voxel") + + +def angles(values: Iterable[float] | None) -> tuple[float, ...]: + if values is None: + return tuple(float(x * np.pi / 4) for x in range(8)) + try: + array = np.asarray(tuple(values), dtype=float) + except (TypeError, ValueError) as exc: + raise TypeError("cooccurence_angles must be an iterable of real angles") from exc + if array.ndim != 1 or not array.size or not np.isfinite(array).all(): + raise ValueError("cooccurence_angles must contain finite angles") + return tuple(float(value) for value in array) diff --git a/module/collageradiomics.py b/module/collageradiomics.py deleted file mode 100644 index 9d4a143..0000000 --- a/module/collageradiomics.py +++ /dev/null @@ -1,596 +0,0 @@ -import math -import logging -from itertools import product -import mahotas as mt -import numpy as np -from scipy import linalg -from skimage.feature import graycomatrix -from skimage.util.shape import view_as_windows -from enum import Enum, IntEnum - - -logger = logging.getLogger('collageradiomics') -logger.info('Logging set up.') - - -def _svd_dominant_angles(dx, dy, dz, svd_radius): - """Calculate a new numpy image containing the dominant angles for each voxel. - - :param dx: 3D numpy array of the pixel gradient in the x directions - :type dx: numpy.ndarray - - :returns: image array with the dominant angle calculated at each voxel - :rtype: numpy.ndarray - """ - - is_3D = dx.shape[2] > 1 - - # create rolling windows - svd_diameter = svd_radius * 2 + 1 - - # the first (commented out) version would actually take a patch of one slice above and below, - # but by convention, the third dimension is simply used for the gradient calculation; - # the actual collection of nearby gradient values to run through the SVD calculation is - # still done on a given 2D slice - #window_shape = (svd_diameter, svd_diameter) + ((3 if is_3D else 1),) - window_shape = (svd_diameter, svd_diameter, 1) - logger.info(f'Window patch shape for dominant angle calculation = {window_shape}') - dx_windows = view_as_windows(dx, window_shape) - dy_windows = view_as_windows(dy, window_shape) - dz_windows = view_as_windows(dz, window_shape) - - angles_shape = dx_windows.shape[0:3] - dominant_angles_array = np.zeros(angles_shape + (2 if is_3D else 1,), np.single) - - # loop through each voxel and use SVD to calculate the dominant angle for that rolling window - # centered on that x,y,z coordinate - center_x_range = range(angles_shape[1]) - center_y_range = range(angles_shape[0]) - center_z_range = range(angles_shape[2]) - for x, y, z in product(center_x_range, center_y_range, center_z_range): - dominant_angles_array[y, x, z, :] = _svd_dominant_angle(x, y, z, dx_windows, dy_windows, dz_windows) - - return dominant_angles_array - - -def _svd_dominant_angle(x, y, z, dx_windows, dy_windows, dz_windows): - """Calculates the dominate angle at the coordinate within the windows. - - :param x: x value of coordinate - :type x: int - :param y: y value of coordinate - :type y: int - :param z: z value of coordinate - :type z: int - :param dx_windows: dx windows of x, y shape to run svd upon (shape = rows, cols, slices, row_radius, col_radius, slice_radius) - :type dx_windows: numpy.ndarray - :param dy_windows: dy windows of x, y shape to run svd upon - :type dy_windows: numpy.ndarray - :param dz_windows: dy windows of x, y shape to run svd upon - :type dz_windows: numpy.ndarray - - :returns: dominant angle at x, y - :rtype: float - """ - - # extract the patch of pixel gradient values for this specific voxel - dx_patch = dx_windows[y, x, z] - dy_patch = dy_windows[y, x, z] - dz_patch = dz_windows[y, x, z] - - is_3D = dx_windows.shape[2] > 1 - - # flatten all N gradient values in this patch into an Nxd matrix to pass into svd - window_area = dx_patch.size - flattened_gradients = np.zeros((window_area, (3 if is_3D else 2))) - matrix_order = 'F' # fortran-style to be consistent with original matlab implementation - flattened_gradients[:, 0] = np.reshape(dx_patch, window_area, order=matrix_order) - flattened_gradients[:, 1] = np.reshape(dy_patch, window_area, order=matrix_order) - if is_3D: - flattened_gradients[:, 2] = np.reshape(dz_patch, window_area, order=matrix_order) - - # calculate svd - _, _, v = linalg.svd(flattened_gradients) - - # extract results from the first column (in matlab this would be the first row) - dominant_y = v[0,0] - dominant_x = v[1,0] - - # calculate the dominant angle for this voxel - dominant_angle = math.atan2(dominant_y, dominant_x) - - if is_3D: - # also include the secondary angle - dominant_z = v[2,0] - secondary_angle = math.atan2(dominant_z, math.sqrt(dominant_x ** 2 + dominant_y ** 2)) - return (dominant_angle, secondary_angle) - else: - return dominant_angle - -class HaralickFeature(IntEnum): - """Enumeration Helper For Haralick Features - - :param IntEnum: Enumeration Helper For Haralick Features - :type IntEnum: HaralickFeature - """ - AngularSecondMoment = 0 - Contrast = 1 - Correlation = 2 - SumOfSquareVariance = 3 - SumAverage = 4 - SumVariance = 5 - SumEntropy = 6 - Entropy = 7 - DifferenceVariance = 8 - DifferenceEntropy = 9 - InformationMeasureOfCorrelation1 = 10 - InformationMeasureOfCorrelation2 = 11 - MaximalCorrelationCoefficient = 12 - - -class DifferenceVarianceInterpretation(Enum): - """ Feature 10 has two interpretations, as the variance of |x-y| - or as the variance of P(|x-y|). - See: https://ieeexplore.ieee.org/document/4309314 - - :param Enum: Enumeration Helper For Haralick Features - :type Enum: DifferenceVarianceInterpretation - """ - XMinusYVariance = 0 - ProbabilityXMinusYVariance = 1 - - -class Collage: - """This is the main object in the Collage calculation system. Usage: create a Collage object and then call the :py:meth:`execute` function. - - :param image_array: image to run collage upon - :type image_array: numpy.ndarray - :param mask_array: mask that correlates with the image - :type mask_array: numpy.ndarray - :param svd_radius: radius of svd. Defaults to 5. - :type svd_radius: int, optional - :param verbose_logging: This parameter is now ignored. Please use the python logging module. - :type verbose_logging: bool, optional - :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x*numpy.pi/4 for x in range(8)] - :type cooccurence_angles: list, optional - :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. - :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional - :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. - :type haralick_window_size: int, optional - :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. - :type num_unique_angles: int, optional - """ - - - @property - def img_array(self): - """ - The original image. - - :getter: Returns the original image array. - :setter: Sets the original image array. - :type: np.ndarray - """ - return self._img_array - - @property - def mask_array(self): - """ - Array passed into Collage. - - :getter: Returns the original mask array. - :setter: Sets the original mask array. - :type: np.ndarray - """ - return self._mask_array - - @property - def is_3D(self): - """ - Whether we are using 3D collage calculations (True) or 2D (False) - """ - return self._is_3D - - @property - def svd_radius(self): - """ - SVD radius is used to calculate the pixel radius - for the dominant angle calculation. - - :getter: Returns the SVD radius. - :setter: Sets the SVD radius. - :type: int - """ - return self._svd_radius - - @property - def verbose_logging(self): - """ - This parameter is now ignored. Please use the python logging module. - - :getter: Returns True if on. - :setter: Turns verbose logging off or on. - :type: bool - """ - return self._verbose_logging - - @property - def cooccurence_angles(self): - """ - Iterable of angles that will be used in the cooccurence matrix. - - :getter: Returns the Iterable of cooccurence angles. - :setter: Sets the angles to be used in the cooccurence matrix. - :type: int - """ - return self._cooccurence_angles - - @property - def difference_variance_interpretation(self): - """ - Feature 10 has two interpretations, as the variance of |x-y| or as the variance of P(|x-y|).]. - Defaults to DifferenceVarianceInterpretation.XMinusYVariance. - - :getter: Returns requested variance interpretation. - :setter: Sets requested variance interpretation. - :type: DifferenceVarianceInterpretation - """ - return self._difference_variance_interpretation - - @property - def haralick_window_size(self): - """ - Number of pixels around each pixel to calculate a haralick texture. - - :getter: Returns requested number of pixels. - :setter: Sets requested number of pixels. - :type: int - """ - return self._haralick_window_size - - @property - def num_unique_angles(self): - """ - Number of bins to use for texture calculations. Defaults to 64. - - :getter: Returns requested number of unique angles to bin into. - :type: int - """ - return self._num_unique_angles - - @property - def collage_output(self): - """ - Array representing collage upon the mask within the full images. - If the input was 2D, the output will be height×width×13 where "13" is the number of haralick textures. - 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) - - The output will have numpy.nan values everywhere outside the masked region. - - :getter: Returns array the same shape as the original image with collage in the mask region. - :type: numpy.ndarray - """ - return self._collage_output - - @collage_output.setter - def collage_output(self, value): - self._collage_output = value - - - def get_single_feature_output(self, which_feature): - """ - Output a single collage output feature. - If this was a 3D calculation, the output will be of size height×width×depth×2 - where the "2" represents the collage calculation from the primary angle (0) or secondary angle (1). - - :param which_feature: Either an integer from 0 to 12 (inclusive) or a HaralickFeature enum value - :type which_feature HaralickFeature - """ - if self.is_3D: - return self.collage_output[:,:,:,which_feature,:] - else: - return self.collage_output[:,:,which_feature] - - - def __init__(self, - img_array, - mask_array, - svd_radius=5, - verbose_logging=False, - cooccurence_angles=[x * np.pi/4 for x in range(8)], - difference_variance_interpretation=DifferenceVarianceInterpretation.XMinusYVariance, - haralick_window_size=-1, - num_unique_angles=64, - ): - """Designated initializer for Collage - - :param image_array: image to run collage upon - :type image_array: numpy.ndarray - :param mask_array: mask that correlates with the image - :type mask_array: numpy.ndarray - :param svd_radius: radius of svd. Defaults to 5. - :type svd_radius: int, optional - :param verbose_logging: This parameter is now ignored. Please use the python logging module. - :type verbose_logging: bool, optional - :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x * np.pi/4 for x in range(8)] - :type cooccurence_angles: list, optional - :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. - :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional - :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. - :type haralick_window_size: int, optional - :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. - :type num_unique_angles: int, optional - """ - - logger.debug('Collage Module Initialized') - - # error checking - if haralick_window_size == -1: - self._haralick_window_size = svd_radius * 2 + 1 - else: - self._haralick_window_size = haralick_window_size - - if self._haralick_window_size < 1: - raise Exception('Haralick windows size must be at least 1 pixel.') - - if svd_radius < 1: - raise Exception('SVD radius must be at least 1 pixel') - - if num_unique_angles < 1: - raise Exception('num_unique_angles must contain at least 1 bin') - - if img_array.ndim < 2 or img_array.ndim > 3: - raise Exception('Expected a 2D or 3D image.') - - if mask_array.shape != img_array.shape: - raise Exception('Mask must be the same shape as image.') - - # Our minimum size for x & y is 50x50 - min_x_y_size = 50 - - if img_array.shape[0] < min_x_y_size or img_array.shape[1] < min_x_y_size: - 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.') - - self._is_3D = img_array.ndim == 3 - logger.debug(f'Running 3D Collage = {self.is_3D}') - - self._img_array = img_array - if not self.is_3D: - # in the case of a single 2D slice, give it a third dimension of unit length - self._img_array = self._img_array.reshape(self._img_array.shape + (1,)) - - min_3D_slices = 3 - 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): - raise Exception( - f'Image is too small for a window size of {self._haralick_window_size} pixels.') - - # threshold mask - uniqueValues = np.unique(mask_array) - numberOfValues = len(uniqueValues) - if numberOfValues > 2: - logger.info(f'Warning: Mask is not binary. Considering all {numberOfValues} nonzero values in the mask as a value of True.') - thresholded_mask_array = (mask_array != 0) - - # make correct shape - thresholded_mask_array = thresholded_mask_array.reshape(self.img_array.shape) - - # extract rectangular area of mask - non_zero_indices = np.argwhere(thresholded_mask_array) - min_mask_coordinates = non_zero_indices.min(0) - max_mask_coordinates = non_zero_indices.max(0)+1 - self.mask_min_x = min_mask_coordinates[1] - self.mask_min_y = min_mask_coordinates[0] - self.mask_min_z = min_mask_coordinates[2] - self.mask_max_x = max_mask_coordinates[1] - self.mask_max_y = max_mask_coordinates[0] - self.mask_max_z = max_mask_coordinates[2] - - cropped_mask_array = thresholded_mask_array[self.mask_min_y:self.mask_max_y, - self.mask_min_x:self.mask_max_x, - self.mask_min_z:self.mask_max_z] - - # store variables internally - self._mask_array = cropped_mask_array - - self._svd_radius = svd_radius - self._verbose_logging = verbose_logging - - self._cooccurence_angles = cooccurence_angles - self._difference_variance_interpretation = difference_variance_interpretation - - self._num_unique_angles = num_unique_angles - - - def _calculate_haralick_feature_values(self, img_array, center_x, center_y): - - """Gets the haralick texture feature values at the x, y, z coordinate. -, pos[1] - :param image_array: image to calculate texture - :type image_array: numpy.ndarray - :param center_x: x center of coordinate - :type center_x: int - :param center_y: y center of coordinate - :type center_y: int - :param window_size: size of window to pull for calculation - :type window_size: int - :param num_unique_angles: number of bins - :type num_unique_angles: int - :param haralick_feature: desired haralick feature - :type haralick_feature: HaralickFeature - - :returns: A 13x1 vector of haralick texture at the coordinate. - :rtype: numpy.ndarray - """ - # extract subpart of image (todo: pass in result from view_as_windows) - window_size = self.haralick_window_size - min_x = int(max(0, center_x - window_size / 2 - 1)) - min_y = int(max(0, center_y - window_size / 2 - 1)) - max_x = int(min(img_array.shape[1] - 1, center_x + window_size / 2 + 1)) - max_y = int(min(img_array.shape[0] - 1, center_y + window_size / 2 + 1)) - cropped_img_array = img_array[min_y:max_y, min_x:max_x] - - # co-occurence matrix of all 8 directions and sum them - cooccurence_matrix = graycomatrix(cropped_img_array, [1], self.cooccurence_angles, levels=self.num_unique_angles) - cooccurence_matrix = np.sum(cooccurence_matrix, axis=3) - cooccurence_matrix = cooccurence_matrix[:, :, 0] - - # extract haralick using mahotas library - return mt.features.texture.haralick_features([cooccurence_matrix], return_mean=True) - - - def _calculate_haralick_textures(self, dominant_angles): - """Gets haralick texture values - - :param dominant_angles_array: An image of the dominant angles at each voxel - :type dominant_angles_[:,:,:,feature_index]array: numpy.ndarray - :param desired_haralick_feature: which feature to calculate - :type desired_haralick_feature: Haralick Feature - :param num_unique_angles: number of bins - :type num_unique_angles: int - :param haralick_window_size: size of window around pixels to calculate haralick value - - :returns: An hxwxdx13 set of haralick texture. - :rtype: numpy.ndarray - """ - - # rescale from 0 to (num_unique_angles-1) - num_unique_angles = self.num_unique_angles - logger.debug(f'Rescaling dominant angles to {num_unique_angles} unique values.') - dominant_angles_max = dominant_angles.max() - dominant_angles_min = dominant_angles.min() - dominant_angles_binned = (dominant_angles - dominant_angles_min) / (dominant_angles_max - dominant_angles_min + np.finfo(float).eps) * (num_unique_angles - 1) - dominant_angles_binned = np.round(dominant_angles_binned).astype(int) - logger.debug(f'Rescaling dominant angles done.') - - # prepare output - shape = dominant_angles_binned.shape - haralick_image = np.empty(shape + (13,)) - haralick_image[:] = np.nan - - # the haralick is calculated for each slice separately - height, width, depth = shape - - logger.debug(f'dominant_angles_binned shape is {shape} mask shape is {self.mask_array.shape}') - - # In 3D, we extended the dominant angles by one slice in each direction, so now we need to trim those off. - for z in range(1, depth - 1) if self.is_3D else range(depth): - for y,x in product(range(height), range(width)): - if self.mask_array[y,x,z]: - haralick_image[y,x,z,:] = self._calculate_haralick_feature_values(dominant_angles_binned[:,:,z], x, y) - - return haralick_image - - - def execute(self): - """Begins haralick calculation. - - :returns: An image at original size that only has the masked section filled in with collage calculations. - :rtype: numpy.ndarray - """ - - svd_radius = self.svd_radius - - # mask location - mask_min_x = int(self.mask_min_x) - mask_min_y = int(self.mask_min_y) - mask_min_z = int(self.mask_min_z) - mask_max_x = int(self.mask_max_x) - mask_max_y = int(self.mask_max_y) - mask_max_z = int(self.mask_max_z) - - mask_width = mask_max_x - mask_min_x - mask_height = mask_max_y - mask_min_y - mask_depth = mask_max_z - mask_min_z - - img_array = self.img_array - - # extend the mask outwards a bit (up to the edge of the image) to handle the svd radius - cropped_min_x = max(mask_min_x - svd_radius, 0) - cropped_min_y = max(mask_min_y - svd_radius, 0) - cropped_min_z = max(mask_min_z - 1 , 0) # for 3D, we just extend 1 slice in both directions - cropped_max_x = min(mask_max_x + svd_radius, img_array.shape[1]) - cropped_max_y = min(mask_max_y + svd_radius, img_array.shape[0]) - cropped_max_z = min(mask_max_z + 1 , img_array.shape[2]) - - extended_below = mask_min_z > 0 - extended_above = mask_max_z < img_array.shape[2] - - cropped_image = img_array[cropped_min_y:cropped_max_y, - cropped_min_x:cropped_max_x, - cropped_min_z:cropped_max_z] - - logger.debug(f'Image shape = {img_array.shape}') - logger.debug(f'Mask size = {mask_height}x{mask_width}x{mask_depth}') - logger.debug(f'Image shape (cropped and padded) = {cropped_image.shape}') - - # ensure the image values range from 0-1 - if cropped_image.max() > 1: - logger.debug(f'Note: Dividing image values by {cropped_image.max()} to convert to 0-1 range') - cropped_image = cropped_image / cropped_image.max() - - # calculate x,y,z gradients - logger.debug('Calculating pixel gradients:') - dx = np.gradient(cropped_image, axis=1) - dy = np.gradient(cropped_image, axis=0) - dz = np.gradient(cropped_image, axis=2) if self.is_3D else np.zeros(dx.shape) - - if extended_below: - dx = dx[:,:,1:] - dy = dy[:,:,1:] - dz = dz[:,:,1:] - - if extended_above: - dx = dx[:,:,:-1] - dy = dy[:,:,:-1] - dz = dz[:,:,:-1] - - self.dx = dx - self.dy = dy - self.dz = dz - logger.debug('Calculating pixel gradients done.') - - # calculate dominant angles of each patch - logger.debug(f'Calculating dominant gradient angles using SVD for each image patch of size {svd_radius}x{svd_radius}') - dominant_angles = _svd_dominant_angles(dx, dy, dz, svd_radius) - self.dominant_angles = dominant_angles - angles_shape = dominant_angles.shape - logger.debug('Calculating dominant gradient angles done.') - logger.debug(f'Dominant angles shape = {angles_shape}') - - # calculate haralick features of the dominant angles - logger.debug('Calculating haralick features of angles:') - haralick_features = np.empty(angles_shape[0:3] + (13, 2 if self.is_3D else 1,)) - for angle_index in range(angles_shape[3]): - logger.info(f'Calculating features for angle {angle_index}:') - haralick_features[:,:,:,:,angle_index] = self._calculate_haralick_textures(dominant_angles[:,:,:,angle_index]) - logger.info(f'Calculating features for angle {angle_index} done.') - logger.debug('Calculating haralick features of angles done.') - - # prepare an output full of "NaN's" - collage_output = np.empty(img_array.shape + haralick_features.shape[3:5]) - collage_output[:] = np.nan - - # if a mask covers the whole image, we'll offset the edges as nans - if mask_height == img_array.shape[0] and mask_width == img_array.shape[1]: - y_offset = int((img_array.shape[0] - dominant_angles.shape[0]) / 2) - mask_min_y += y_offset - mask_max_y -= y_offset - x_offset = int((img_array.shape[1] - dominant_angles.shape[1]) / 2) - mask_min_x += x_offset - mask_max_x -= x_offset - - # insert the haralick output into the correct spot - collage_output[mask_min_y:mask_max_y, - mask_min_x:mask_max_x, - mask_min_z:mask_max_z, - :, :] = haralick_features - - # remove the singleton third dimension from the output - if not self.is_3D: - collage_output = np.squeeze(collage_output, 4) - collage_output = np.squeeze(collage_output, 2) - - # output - self.collage_output = collage_output - logger.debug(f'Output shape = {collage_output.shape}') - return collage_output diff --git a/module/collageradiomics/__init__.py b/module/collageradiomics/__init__.py new file mode 100644 index 0000000..717feb4 --- /dev/null +++ b/module/collageradiomics/__init__.py @@ -0,0 +1,409 @@ +"""CoLlAGe descriptors for finite, aligned 2D and 3D NumPy arrays.""" + +import logging +from collections.abc import Iterable +from enum import Enum, IntEnum +from typing import Any + +import numpy as np +from _collageradiomics.orientation import dominant_angle as _svd_dominant_angle # noqa: F401 +from _collageradiomics.orientation import dominant_angles as _svd_dominant_angles +from _collageradiomics.progress import ProgressCallback, ProgressReporter +from _collageradiomics.texture import named_haralick +from _collageradiomics.validation import angles, integer, validate_arrays +from numpy.typing import NDArray +from skimage.feature import graycomatrix + +Array = NDArray[Any] +FloatArray = NDArray[np.float64] +logger = logging.getLogger("collageradiomics") + + +class HaralickFeature(IntEnum): + """Enumeration Helper For Haralick Features + + :param IntEnum: Enumeration Helper For Haralick Features + :type IntEnum: HaralickFeature + """ + + AngularSecondMoment = 0 + Contrast = 1 + Correlation = 2 + SumOfSquareVariance = 3 + SumAverage = 4 + SumVariance = 5 + SumEntropy = 6 + Entropy = 7 + DifferenceVariance = 8 + DifferenceEntropy = 9 + InformationMeasureOfCorrelation1 = 10 + InformationMeasureOfCorrelation2 = 11 + MaximalCorrelationCoefficient = 12 + + +class DifferenceVarianceInterpretation(Enum): + """Difference variance has two interpretations, as the variance of ``|x-y|`` + or as the variance of ``P(|x-y|)``. + See: https://ieeexplore.ieee.org/document/4309314 + + :param Enum: Enumeration Helper For Haralick Features + :type Enum: DifferenceVarianceInterpretation + """ + + XMinusYVariance = 0 + ProbabilityXMinusYVariance = 1 + + +class Collage: + """This is the main object in the Collage calculation system. Usage: create a Collage object and then call the :py:meth:`execute` function. + + :param img_array: image to run collage upon + :type img_array: numpy.ndarray + :param mask_array: mask that correlates with the image + :type mask_array: numpy.ndarray + :param svd_radius: radius of svd. Defaults to 5. + :type svd_radius: int, optional + :param verbose_logging: This parameter is now ignored. Please use the python logging module. + :type verbose_logging: bool, optional + :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x*numpy.pi/4 for x in range(8)] + :type cooccurence_angles: list, optional + :param difference_variance_interpretation: Difference variance has two interpretations, as the variance of ``|x-y|`` or as the variance of ``P(|x-y|)``.].Defaults to DifferenceVarianceInterpretation.XMinusYVariance. + :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional + :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. + :type haralick_window_size: int, optional + :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. + :type num_unique_angles: int, optional + """ + + _collage_output: FloatArray | None + + @property + def img_array(self) -> Array: + """ + The original image. + + :getter: Returns the original image array. + :type: np.ndarray + """ + return self._img_array + + @property + def mask_array(self) -> NDArray[np.bool_]: + """ + Array passed into Collage. + + :getter: Returns the original mask array. + :type: np.ndarray + """ + return self._mask_array + + @property + def is_3D(self) -> bool: + """ + Whether we are using 3D collage calculations (True) or 2D (False) + """ + return self._is_3D + + @property + def svd_radius(self) -> int: + """ + SVD radius is used to calculate the pixel radius + for the dominant angle calculation. + + :getter: Returns the SVD radius. + :type: int + """ + return self._svd_radius + + @property + def verbose_logging(self) -> bool: + """ + This parameter is now ignored. Please use the python logging module. + + :getter: Returns True if on. + :type: bool + """ + return self._verbose_logging + + @property + def cooccurence_angles(self) -> tuple[float, ...]: + """ + Iterable of angles that will be used in the cooccurence matrix. + + :getter: Returns the Iterable of cooccurence angles. + :type: int + """ + return self._cooccurence_angles + + @property + def difference_variance_interpretation(self) -> DifferenceVarianceInterpretation: + """ + Difference variance has two interpretations, as the variance of ``|x-y|`` or as the variance of ``P(|x-y|)``.]. + Defaults to DifferenceVarianceInterpretation.XMinusYVariance. + + :getter: Returns requested variance interpretation. + :type: DifferenceVarianceInterpretation + """ + return self._difference_variance_interpretation + + @property + def haralick_window_size(self) -> int: + """ + Number of pixels around each pixel to calculate a haralick texture. + + :getter: Returns requested number of pixels. + :type: int + """ + return self._haralick_window_size + + @property + def num_unique_angles(self) -> int: + """ + Number of bins to use for texture calculations. Defaults to 64. + + :getter: Returns requested number of unique angles to bin into. + :type: int + """ + return self._num_unique_angles + + @property + def collage_output(self) -> FloatArray: + """ + Array representing collage upon the mask within the full images. + If the input was 2D, the output will be height×width×13 where "13" is the number of haralick textures. + 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) + + The output will have numpy.nan values everywhere outside the masked region. + + :getter: Returns array the same shape as the original image with collage in the mask region. + :type: numpy.ndarray + """ + if self._collage_output is None: + raise RuntimeError("execute() must be called before requesting features") + return self._collage_output + + @collage_output.setter + def collage_output(self, value: FloatArray) -> None: + self._collage_output = value + + def get_single_feature_output(self, which_feature: int | HaralickFeature) -> FloatArray: + """ + Output a single collage output feature. + If this was a 3D calculation, the output will be of size height×width×depth×2 + where the "2" represents the collage calculation from the primary angle (0) or secondary angle (1). + + :param which_feature: Either an integer from 0 to 12 (inclusive) or a HaralickFeature enum value + :type which_feature: HaralickFeature + """ + if self._collage_output is None: + raise RuntimeError("execute() must be called before requesting features") + which_feature = integer(which_feature, "which_feature", minimum=0) + if which_feature >= len(HaralickFeature): + raise ValueError("which_feature must be between 0 and 12") + if self.is_3D: + return self.collage_output[:, :, :, which_feature, :] + else: + return self.collage_output[:, :, which_feature] + + def __init__( + self, + img_array: Array, + mask_array: Array, + svd_radius: int = 5, + verbose_logging: bool = False, + cooccurence_angles: Iterable[float] | None = None, + difference_variance_interpretation: DifferenceVarianceInterpretation = DifferenceVarianceInterpretation.XMinusYVariance, + haralick_window_size: int = -1, + num_unique_angles: int = 64, + progress_callback: ProgressCallback | None = None, + ) -> None: + """Designated initializer for Collage + + :param img_array: image to run collage upon + :type img_array: numpy.ndarray + :param mask_array: mask that correlates with the image + :type mask_array: numpy.ndarray + :param svd_radius: radius of svd. Defaults to 5. + :type svd_radius: int, optional + :param verbose_logging: This parameter is now ignored. Please use the python logging module. + :type verbose_logging: bool, optional + :param cooccurence_angles: list of angles to use in the cooccurence matrix. Defaults to [x * np.pi/4 for x in range(8)] + :type cooccurence_angles: list, optional + :param difference_variance_interpretation: Difference variance has two interpretations, as the variance of ``|x-y|`` or as the variance of ``P(|x-y|)``.].Defaults to DifferenceVarianceInterpretation.XMinusYVariance. + :type difference_variance_interpretation: DifferenceVarianceInterpretation, optional + :param haralick_window_size: size of rolling window for texture calculations. Defaults to -1. + :type haralick_window_size: int, optional + :param num_unique_angles: number of bins to use for the texture calculation. Defaults to 64. + :type num_unique_angles: int, optional + """ + + self._collage_output = None + self._progress = ProgressReporter(progress_callback) + self._texture_stage = "haralick" + logger.debug("Collage Module Initialized") + + validate_arrays(img_array, mask_array) + svd_radius = integer(svd_radius, "SVD radius") + num_unique_angles = integer(num_unique_angles, "num_unique_angles", minimum=2) + cooccurence_angles = angles(cooccurence_angles) + if not isinstance(difference_variance_interpretation, DifferenceVarianceInterpretation): + raise TypeError( + "difference_variance_interpretation must be a DifferenceVarianceInterpretation" + ) + + if isinstance(haralick_window_size, (int, np.integer)) and haralick_window_size == -1: + self._haralick_window_size = svd_radius * 2 + 1 + else: + self._haralick_window_size = integer( + haralick_window_size, "haralick_window_size", minimum=3 + ) + if self._haralick_window_size % 2 == 0: + raise ValueError("haralick_window_size must be odd") + + self._is_3D = img_array.ndim == 3 + logger.debug(f"Running 3D Collage = {self.is_3D}") + + self._img_array = img_array + if not self.is_3D: + # in the case of a single 2D slice, give it a third dimension of unit length + self._img_array = self._img_array.reshape(self._img_array.shape + (1,)) + + # threshold mask + uniqueValues = np.unique(mask_array) + numberOfValues = len(uniqueValues) + if numberOfValues > 2: + logger.info( + f"Warning: Mask is not binary. Considering all {numberOfValues} nonzero values in the mask as a value of True." + ) + thresholded_mask_array = mask_array != 0 + + # make correct shape + thresholded_mask_array = thresholded_mask_array.reshape(self.img_array.shape) + + # extract rectangular area of mask + non_zero_indices = np.argwhere(thresholded_mask_array) + min_mask_coordinates = non_zero_indices.min(0) + max_mask_coordinates = non_zero_indices.max(0) + 1 + self.mask_min_x = min_mask_coordinates[1] + self.mask_min_y = min_mask_coordinates[0] + self.mask_min_z = min_mask_coordinates[2] + self.mask_max_x = max_mask_coordinates[1] + self.mask_max_y = max_mask_coordinates[0] + self.mask_max_z = max_mask_coordinates[2] + + cropped_mask_array = thresholded_mask_array[ + self.mask_min_y : self.mask_max_y, + self.mask_min_x : self.mask_max_x, + self.mask_min_z : self.mask_max_z, + ] + + # store variables internally + self._mask_array = cropped_mask_array + + self._svd_radius = svd_radius + self._verbose_logging = verbose_logging + + self._cooccurence_angles = cooccurence_angles + self._difference_variance_interpretation = difference_variance_interpretation + + self._num_unique_angles = num_unique_angles + + def _calculate_haralick_feature_values( + self, img_array: Array, center_x: int, center_y: int + ) -> FloatArray: + """Calculate the thirteen named features in a centered, odd-sized window.""" + half = self.haralick_window_size // 2 + patch = img_array[ + max(0, center_y - half) : min(img_array.shape[0], center_y + half + 1), + max(0, center_x - half) : min(img_array.shape[1], center_x + half + 1), + ] + matrix = graycomatrix(patch, [1], self.cooccurence_angles, levels=self.num_unique_angles) + matrix = np.sum(matrix[:, :, 0, :], axis=2) + return named_haralick( + matrix, + self.difference_variance_interpretation + == DifferenceVarianceInterpretation.XMinusYVariance, + ) + + def _calculate_haralick_textures(self, dominant_angles: Array) -> FloatArray: + """Calculate masked voxels using a halo of unmasked neighboring angles.""" + low, high = float(dominant_angles.min()), float(dominant_angles.max()) + if high - low <= np.finfo(float).eps: + binned = np.zeros(dominant_angles.shape, dtype=np.int32) + else: + scaled = (dominant_angles.astype(float) - low) / (high - low) + binned = np.rint(scaled * (self.num_unique_angles - 1)).astype(np.int32) + output = np.full(self.mask_array.shape + (13,), np.nan) + halo = self.haralick_window_size // 2 + coordinates = np.argwhere(self.mask_array) + total = len(coordinates) + self._progress(self._texture_stage, 0, total) + for completed, (y, x, z) in enumerate(coordinates, 1): + output[y, x, z] = self._calculate_haralick_feature_values( + binned[:, :, z], + int(x) + halo, + int(y) + halo, + ) + self._progress(self._texture_stage, completed, total) + return output + + def execute(self) -> FloatArray: + """Return feature maps at input size, with NaN only outside the mask. + + Image edges are extended by replication. SVD neighborhoods and Haralick + neighborhoods both include their complete halo, including for small ROIs. + In 3D, gradients use neighboring slices and textures remain slice-wise. + """ + self._collage_output = None + half = self.haralick_window_size // 2 + halo = self.svd_radius + half + image = self.img_array.astype(np.float64, copy=False) + y0, y1 = int(self.mask_min_y), int(self.mask_max_y) + x0, x1 = int(self.mask_min_x), int(self.mask_max_x) + z0, z1 = int(self.mask_min_z), int(self.mask_max_z) + height, width, depth = y1 - y0, x1 - x0, z1 - z0 + cy0, cy1 = max(0, y0 - halo), min(image.shape[0], y1 + halo) + cx0, cx1 = max(0, x0 - halo), min(image.shape[1], x1 + halo) + cz0, cz1 = max(0, z0 - 1), min(image.shape[2], z1 + 1) + patch = image[cy0:cy1, cx0:cx1, cz0:cz1] + patch = np.pad( + patch, + ( + (max(0, halo - y0), max(0, y1 + halo - image.shape[0])), + (max(0, halo - x0), max(0, x1 + halo - image.shape[1])), + (0, 0), + ), + mode="edge", + ) + magnitude = np.max(np.abs(patch)) + if magnitude > 1: + patch = patch / magnitude + z_slice = slice(z0 - cz0, z0 - cz0 + depth) + dx = np.gradient(patch, axis=1)[:, :, z_slice] + dy = np.gradient(patch, axis=0)[:, :, z_slice] + dz = (np.gradient(patch, axis=2) if patch.shape[2] > 1 else np.zeros_like(patch))[ + :, :, z_slice + ] + self.dx, self.dy, self.dz = dx, dy, dz + dominant = _svd_dominant_angles( + dx, + dy, + dz, + self.svd_radius, + is_3d=self.is_3D, + progress=lambda done, total: self._progress("orientation", done, total), + ) + self.dominant_angles = dominant[half : half + height, half : half + width] + num_angles = 2 if self.is_3D else 1 + features = np.empty((height, width, depth, 13, num_angles)) + for angle in range(num_angles): + self._texture_stage = "haralick-theta" if angle == 0 else "haralick-phi" + features[:, :, :, :, angle] = self._calculate_haralick_textures( + dominant[:, :, :, angle] + ) + output = np.full(image.shape + (13, num_angles), np.nan) + output[y0:y1, x0:x1, z0:z1] = features + if not self.is_3D: + output = output[:, :, 0, :, 0] + self.collage_output = output + return output diff --git a/module/collageradiomics/py.typed b/module/collageradiomics/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/module/docs/requirements.txt b/module/docs/requirements.txt index e0756f9..c11cc41 100644 --- a/module/docs/requirements.txt +++ b/module/docs/requirements.txt @@ -1,3 +1,2 @@ -numpydoc -matplotlib -collageradiomics +Sphinx>=8,<9 +sphinx-rtd-theme>=3,<4 diff --git a/module/docs/source/backlog.rst b/module/docs/source/backlog.rst new file mode 100644 index 0000000..923cfc5 --- /dev/null +++ b/module/docs/source/backlog.rst @@ -0,0 +1 @@ +.. include:: ../../../BACKLOG.rst diff --git a/module/docs/source/conf.py b/module/docs/source/conf.py index 9267a04..a320924 100644 --- a/module/docs/source/conf.py +++ b/module/docs/source/conf.py @@ -1,50 +1,18 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +"""Build documentation from the checked-out package and its single version file.""" -# -- Path setup -------------------------------------------------------------- +import sys +from pathlib import Path -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'collageradiomics' -copyright = '2020, BrIC Laboratory' -author = 'BrIC Laboratory' -release = '0.2' -extensions = [ - 'sphinx.ext.autodoc', - 'numpydoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.doctest', - 'sphinx.ext.autosummary', - 'sphinx.ext.graphviz', - 'sphinx.ext.ifconfig', - 'matplotlib.sphinxext.plot_directive', - 'sphinx.ext.imgmath', - 'sphinx_rtd_theme' -] -imgmath_image_format = 'svg' -templates_path = ['_templates'] -exclude_patterns = [] -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] -intersphinx_mapping = { - 'numpy': ('https://numpy.org/doc/stable/', None), - 'python': ('https://docs.python.org/dev', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), - 'matplotlib': ('https://matplotlib.org', None), - 'imageio': ('https://imageio.readthedocs.io/en/stable', None), - 'skimage': ('https://scikit-image.org/docs/stable', None), -} -master_doc = 'index' +module_dir = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(module_dir)) +project = "collageradiomics" +author = "BrIC Laboratory" +copyright = "2020, BrIC Laboratory" +release = (module_dir / "VERSION").read_text(encoding="utf-8").strip() +version = release +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.mathjax"] +html_theme = "sphinx_rtd_theme" +html_static_path = [] +master_doc = "index" +autodoc_typehints = "description" +autodoc_member_order = "bysource" diff --git a/module/docs/source/contributing.rst b/module/docs/source/contributing.rst new file mode 100644 index 0000000..b1cd2f3 --- /dev/null +++ b/module/docs/source/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../../CONTRIBUTING.rst diff --git a/module/docs/source/index.rst b/module/docs/source/index.rst index 737fef1..391edd4 100644 --- a/module/docs/source/index.rst +++ b/module/docs/source/index.rst @@ -1,40 +1,27 @@ -Welcome to collageradiomics's documentation! -============================================ +CoLlAGe radiomics +================= -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. +Co-occurrence of Local Anisotropic Gradient Orientations describes local disorder +in image gradients. This package provides thirteen voxel-wise Haralick statistics, +a medical-image CLI, reproducible examples, and a 3D Slicer extension. -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. +The scientific reference is Prasanna, Tiwari, and Madabhushi (2016), +`Scientific Reports 6:37241 `_. +The source and issue tracker are at +`radxtools/collageradiomics `_. -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. - -Helpful Links -============= -Instructions: `README `_ - -RadxTools Website: ``_ - -Original Paper: `Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor `_ +.. toctree:: + :maxdepth: 2 -Code Documentation -================== + usage + numerics + contributing + maintenance + releases + backlog -Notes ------ -The **attributes** below represent the public output intended to be available to consumers of this module. +Python API +---------- .. automodule:: collageradiomics - :members: - :undoc-members: - :inherited-members: - :show-inheritance: - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + :members: Collage, HaralickFeature, DifferenceVarianceInterpretation diff --git a/module/docs/source/maintenance.rst b/module/docs/source/maintenance.rst new file mode 100644 index 0000000..bf8c652 --- /dev/null +++ b/module/docs/source/maintenance.rst @@ -0,0 +1,34 @@ +Maintenance decisions +===================== + +Versioning +---------- + +Keep ``major.minor.patch`` versioning. Compatible fixes increment patch; before +1.0, numerical or API changes increment minor and require migration notes. Calendar +versions are accepted by Python packaging, but compatibility is more useful than +a date for this scientific library. This resolves the investigation in issue #63 +without changing version schemes. See the +`Python packaging versioning guide `_. + +``module/VERSION`` is the single version source for package builds and Sphinx. +Read the Docs installs the checkout rather than a stale PyPI copy. + +Shared code and dependencies +---------------------------- + +The internal package owns validation, SVD orientation, texture mapping, image I/O, +statistics, CLI, and progress. These helpers stay in this repository: there is no +second active consumer that justifies a separate common-code repository (#81). +The core depends on NumPy, SciPy, scikit-image, and Mahotas; GUI, I/O, and example +dependencies are optional. NumPy provides rolling views. Upstream co-occurrence +and texture implementations remain dependencies rather than vendored copies (#85). + +External ownership and catalogs +------------------------------- + +Repository code cannot grant PyPI or Read the Docs roles. A project owner must +verify organization ownership, add current maintainers, and configure the publisher. +Those checks remain the owner task in #78. Catalog and conda-forge submissions +also require acceptance by their maintainers; prepared contribution files and +submission links belong in the backlog record, not a claim of completed acceptance. diff --git a/module/docs/source/numerics.rst b/module/docs/source/numerics.rst new file mode 100644 index 0000000..dd186a1 --- /dev/null +++ b/module/docs/source/numerics.rst @@ -0,0 +1,42 @@ +Numerical conventions and migration +=================================== + +Version 0.4 changes numerical results. Recompute cached features when moving from +0.3.x, and record package versions and extraction parameters with experiments. +The Python import, feature names, and thirteen-channel layout remain compatible. + +Corrected calculations +---------------------- + +* The principal gradient comes from the first row of SciPy's returned ``Vh``. + Its largest-magnitude component is made positive to resolve SVD sign ambiguity. +* SVD and texture windows include their complete halo. Image boundaries use edge + replication. Small ROIs, masks touching one edge, full-image masks, and every + masked slice in a 3D image are calculated. +* 3D gradients include the z component; SVD neighborhoods and co-occurrence + calculations remain slice-wise, following the existing 3D convention. +* Haralick windows are centered and have exactly the specified odd size. +* Angles are linearly binned separately for theta and phi over the ROI plus its + texture halo; constant orientations use bin zero. +* CoLlAGe's documented feature list omits inverse difference moment and includes + maximal correlation. Mahotas' first thirteen outputs have a different layout; + the returned values are now explicitly mapped to the public feature names. +* The selected difference-variance interpretation is forwarded to Mahotas. + ``XMinusYVariance`` computes the variance of the absolute bin difference; + ``ProbabilityXMinusYVariance`` computes the variance of its probability vector. +* Maximal correlation is the second singular value of + ``P(i,j)/sqrt(Px(i)*Py(j))``, after removing empty marginals. It is zero when + either variable is constant. See Haralick's + `Dependence, section 3 `_. + +Regression tests contain analytical checkerboard, independent, and constant GLCMs, +known principal-gradient directions, volume-end masks, and single-voxel corners. +These are small numerical references and do not establish clinical validation. + +Summary conventions +------------------- + +The CLI reports median, population skewness, Fisher excess kurtosis, and sample +variance (ddof=1). ``--extendstats`` adds mean and IQR. For constant or single-voxel +features, skewness and kurtosis are zero; one voxel has variance zero. Moments +are scaled before evaluation to keep nearly constant maps finite. diff --git a/module/docs/source/releases.rst b/module/docs/source/releases.rst new file mode 100644 index 0000000..80bddd2 --- /dev/null +++ b/module/docs/source/releases.rst @@ -0,0 +1,51 @@ +Releasing +========= + +Version 0.4.0 changes numerical results. Read ``CHANGELOG.rst`` and the numerical +migration guide before updating an experiment. ``module/VERSION`` controls the +Python package and documentation version; update the Conda recipe's explicit +version at the same time. The compatibility CLI has its own version because it +is a small dependency shim around the core's ``[cli]`` extra. + +Preparation +----------- + +Run the documented tests, type/style checks, and documentation build. Require +green Python, native Slicer, example, container, Conda, and distribution workflows +on the final commit. Review the changelog, numerical differences, dependency lock, +and migration instructions. Merge the reviewed change before tagging it. + +``sh module/setup.sh`` builds an sdist and a wheel from that sdist; it does not +upload anything. The Release packages workflow also builds the compatibility CLI, +runs the tests and ``twine check --strict``, and retains both artifact sets. +It builds on pull requests and supports manual validation without publishing. + +Publisher setup (project owner) +-------------------------------- + +A PyPI owner must configure a trusted publisher for **each** project: +``collageradiomics`` and ``collageradiomicscli``. Use owner ``radxtools``, repository +``collageradiomics``, workflow ``python-publish.yml``, and environment ``pypi``. +Configure that GitHub environment's reviewers if the project requires a release +approval. See the official +`PyPI publisher setup `_. + +The workflow uses short-lived OIDC credentials and does not read the legacy +``PYPI_USERNAME``/``PYPI_PASSWORD`` secrets. Their presence does not prove they +are current. A project owner should retire them after trusted publishing works. +PyPI and Read the Docs ownership and maintainer roles remain tracked in #78; +GitHub repository access does not grant either service's owner role. + +Publication +----------- + +Create a draft GitHub release with tag ``v`` followed by ``module/VERSION`` and +attach the validated artifacts for review. Publishing a non-prerelease GitHub +release triggers the PyPI job; draft releases and workflow-dispatch validation +do not upload packages. The workflow rejects tags that disagree with the package +version. Do not reuse a published version; PyPI files are immutable. + +After publication, verify clean installs of ``collageradiomics[cli]`` and the +compatibility CLI, then update catalog submissions and the conda-forge recipe +to the released source URL and SHA256. External catalog/feedstock acceptance +is separate from producing tested contribution files in this repository. diff --git a/module/docs/source/usage.rst b/module/docs/source/usage.rst new file mode 100644 index 0000000..8b986c9 --- /dev/null +++ b/module/docs/source/usage.rst @@ -0,0 +1,84 @@ +Installation and extraction +=========================== + +Use Python 3.10 or newer in a virtual environment. Until 0.4 is published, install +this checkout to use the corrected results and new CLI:: + + python3 -m venv env_collage + source env_collage/bin/activate + python -m pip install -e './module[cli]' + +For a published release, use ``python -m pip install 'collageradiomics[cli]'``. +The numerical core requires NumPy, SciPy, scikit-image, and Mahotas. Image I/O and +Click are optional CLI dependencies; plotting, DICOM, and notebooks are examples +extras. ``pip install -r requirements.txt`` installs the checkout with all examples. + +Array API +--------- + +Images and masks must be finite NumPy arrays with matching shape. Every nonzero +mask label belongs to the ROI. Empty masks and invalid parameters raise a clear +``TypeError`` or ``ValueError``. Parameters use an integer SVD radius of at least +one, an odd Haralick window of at least three, and at least two angle bins. +The default window ``-1`` becomes ``2*svd_radius+1``. + +.. code-block:: python + + import numpy as np + from collageradiomics import Collage, HaralickFeature + + image = np.random.default_rng(42).random((32, 32)) + mask = np.zeros(image.shape, dtype=bool) + mask[8:24, 8:24] = True + collage = Collage(image, mask, svd_radius=2, num_unique_angles=8) + maps = collage.execute() # (32, 32, 13) + entropy = collage.get_single_feature_output(HaralickFeature.Entropy) + mean_entropy = entropy[mask].mean() + +A 3D array uses ``(height,width,depth)`` and returns ``(H,W,D,13,2)``. The final +axis contains theta (0) and phi (1). Pixels outside the mask are NaN; filter them +before calculating summaries. ``np.save`` preserves numeric feature maps and NaNs. + +The optional ``progress_callback(stage,completed,total)`` receives throttled +updates; raising an exception cancels extraction. Logging uses the +``collageradiomics`` logger. The legacy ``verbose_logging`` parameter is ignored. + +Medical-image CLI +----------------- + +Single images produce a FeatureName,Value CSV:: + + collageradiomics -i image.nii.gz -m mask.nii.gz -o features.csv --extendstats + +For batch extraction create a CSV with ``ID,Image,Mask`` columns:: + + collageradiomics -i cases.csv -o features.csv --verbose + +Paths in the CSV are relative to its directory. Each successful case produces a +row; failures appear in ``errors_features.csv`` and cause a nonzero exit code. +Mixed 2D/3D batches use a union of feature columns. Image and mask size, spacing, +origin, direction, and scalar dimensionality must match. Align mismatched masks +before extraction. ``--dimensions 2`` accepts 2D images or single-slice volumes; +it does not silently discard slices. ``--label`` selects a specific label. + +``--haralickwindow``, ``--svdradius``, ``--binsize``, ``--difference-variance`` and +repeatable ``--cooccurrence-angle`` options expose the numerical settings. +``collageradiomicscli.py`` remains a compatibility command. + +Examples and Slicer +------------------- + +Install ``./module[examples]`` and open ``jupyter/examples/example.ipynb`` using +``jupyter lab``. It uses deterministic synthetic data and inline Matplotlib +figures; it never opens an external image editor. + +``python module/test_script.py --output-dir output_data`` reads the bundled DICOM, +saves lossless ``collage_features.npy`` and thirteen derived DICOM previews. +Stored value zero is reserved for padding; rescale tags retain feature units. +``python examples/isbi.py IMAGE MASK --output-dir output_data`` accepts a local +IBSI pair and writes raw maps plus mean, population SD, skewness, and kurtosis. + +The extension in ``slicer/collageradiomics`` requires Slicer 5.10 or newer. It +accepts labelmaps and visible segmentations, supports both angles and cancellation, +and preserves image geometry and parent transforms. Its README contains native +test instructions. CI runs the extension in a verified Slicer 5.12.4 bundle. diff --git a/module/pyproject.toml b/module/pyproject.toml new file mode 100644 index 0000000..cf5bd9a --- /dev/null +++ b/module/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=68", "wheel>=0.43"] +build-backend = "setuptools.build_meta" diff --git a/module/requirements.txt b/module/requirements.txt index 014510d..ac34540 100644 --- a/module/requirements.txt +++ b/module/requirements.txt @@ -1,8 +1,4 @@ -mahotas==1.4.13 -numpy>=1.21.6 -Pillow==9.3.0 -scikit-build>=0.15.0 -scikit-image>=0.19.3 -scipy>=1.7.3 -pydicom>=2.3.0 -scikit-learn +mahotas>=1.4.19,<2 +numpy>=1.26,<3 +scikit-image>=0.22,<1 +scipy>=1.11,<2 diff --git a/module/setup.py b/module/setup.py index fdc67b6..6cb062d 100644 --- a/module/setup.py +++ b/module/setup.py @@ -1,13 +1,11 @@ -import setuptools - -with open('requirements.txt', 'r') as f: - requirements = f.read() +from pathlib import Path -with open('README.md', 'r') as fh: - long_description = fh.read() +import setuptools -with open('VERSION') as version_file: - version = version_file.read().strip() +module_dir = Path(__file__).resolve().parent +requirements = (module_dir / 'requirements.txt').read_text(encoding='utf-8').splitlines() +long_description = (module_dir / 'README.md').read_text(encoding='utf-8') +version = (module_dir / 'VERSION').read_text(encoding='utf-8').strip() setuptools.setup( name='collageradiomics', @@ -17,13 +15,18 @@ description='Python implementation of COLLAGE texture features from BRIC & INVENTLAB.', long_description=long_description, long_description_content_type='text/markdown', - url='https://github.com/ccipd/collageradiomics', + url='https://github.com/radxtools/collageradiomics', project_urls={ - 'Docker Examples': 'https://hub.docker.com/repository/docker/ccipd/collageradiomics-examples', - 'Docker Module': 'https://hub.docker.com/repository/docker/ccipd/collageradiomics-pip', - 'Github': 'https://github.com/ccipd/collageradiomics' + 'Docker Examples': 'https://github.com/radxtools/collageradiomics/tree/master/docker', + 'Documentation': 'https://collageradiomics.readthedocs.io/', + 'Github': 'https://github.com/radxtools/collageradiomics' }, - py_modules=['collageradiomics'], + packages=setuptools.find_packages(where=str(module_dir), include=['collageradiomics*', '_collageradiomics*']), + package_data={'collageradiomics': ['py.typed'], '_collageradiomics': ['py.typed']}, + entry_points={'console_scripts': [ + 'collageradiomics=_collageradiomics.cli:run', + 'collageradiomicscli.py=_collageradiomics.cli:run', + ]}, classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', @@ -37,6 +40,13 @@ 'Topic :: Scientific/Engineering :: Bio-Informatics', ], install_requires=requirements, - python_requires='>=3.6', + extras_require={ + 'cli': ['click>=8.1,<9', 'SimpleITK>=2.3,<3'], + 'examples': [ + 'matplotlib>=3.8,<4', 'pydicom>=3,<4', + 'jupyterlab>=4.3,<5', 'SimpleITK>=2.3,<3', + ], + }, + python_requires='>=3.10', keywords='radiomics cancerimaging medicalresearch computationalimaging', ) diff --git a/module/setup.sh b/module/setup.sh index e15b228..5fa8238 100644 --- a/module/setup.sh +++ b/module/setup.sh @@ -1,3 +1,4 @@ -#!/bin/bash -python setup.py sdist bdist_wheel -twine upload dist/* \ No newline at end of file +#!/bin/sh +set -eu +cd "$(dirname "$0")" +python -m build "$@" diff --git a/module/test_script.py b/module/test_script.py index 3abaa54..a782425 100644 --- a/module/test_script.py +++ b/module/test_script.py @@ -1,91 +1,74 @@ -import collageradiomics -import pydicom -import logging -from pydicom.pixel_data_handlers.util import apply_modality_lut, apply_voi_lut -from skimage.exposure import equalize_hist -import numpy as np -from sklearn.preprocessing import minmax_scale -from random import randint - -level = logging.INFO -logging.basicConfig(level=level) -logger = logging.getLogger() -logger.setLevel(level) -logger.info('Hello, world.') - -local_dcm_file = 'test.dcm' -instance = pydicom.dcmread(local_dcm_file) -slice_instance_uid = instance.SOPInstanceUID -logger.debug(f'slice_instance_uid = {slice_instance_uid}') +"""Create numeric feature maps and derived DICOM previews from the bundled sample.""" -logger.info('Correcting image...') -np_array = instance.pixel_array -corrected = apply_modality_lut(np_array, instance) -corrected = apply_voi_lut(corrected, instance) -logger.debug(f'{np.histogram(corrected) = }') -scaled_array = equalize_hist(corrected) -logger.debug(f'np.histogram(scaled_array) = {np.histogram(scaled_array)}') -logger.info('done.') +import argparse +import copy +from pathlib import Path -width = 50 -height = 50 -min_row = randint(50,200) -max_row = min_row + height -min_col = randint(50,200) -max_col = min_col + width - -original_shape = np_array.shape -logger.debug(f'original_shape = {original_shape}') +import numpy as np +import pydicom +from collageradiomics import Collage, HaralickFeature +from pydicom.pixels import apply_modality_lut, apply_voi_lut +from pydicom.uid import ExplicitVRLittleEndian, SecondaryCaptureImageStorage, generate_uid +from skimage.exposure import equalize_hist -logger.info('Calculating collage features...') -mask_array = np.zeros(original_shape, dtype='int') -mask_array[min_row:max_row, min_col:max_col] = 1 -textures = collageradiomics.Collage(scaled_array, mask_array).execute() -logger.info('Collage features calculated.') -logger.debug(f'textures.shape = {textures.shape}') -logger.debug(f'textures.dtype = {textures.dtype}') -logger.debug(f'np.histogram(textures) = {np.histogram(textures, range=(np.nanmin(textures), np.nanmax(textures)))}') +def run_example(input_path: Path, output_dir: Path) -> list[Path]: + instance = pydicom.dcmread(input_path) + corrected = apply_voi_lut(apply_modality_lut(instance.pixel_array, instance), instance) + image = equalize_hist(corrected) + mask = np.zeros(image.shape, dtype=bool) + cy, cx = np.asarray(image.shape) // 2 + mask[max(0, cy - 6) : cy + 6, max(0, cx - 6) : cx + 6] = True + output = Collage(image, mask, svd_radius=2, num_unique_angles=16).execute() + output_dir.mkdir(parents=True, exist_ok=True) + np.save(output_dir / "collage_features.npy", output) + files = [] + for feature in HaralickFeature: + values = output[:, :, feature] + finite = np.isfinite(values) + low, high = float(values[finite].min()), float(values[finite].max()) + # Reserve stored value zero for padding. Rescaling recovers feature units. + slope = (high - low) / 65534 if high > low else 1.0 + intercept = low - slope + pixels = np.zeros(values.shape, dtype=" None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", type=Path, default=Path(__file__).with_name("test.dcm")) + parser.add_argument("--output-dir", type=Path, default=Path("output_data")) + args = parser.parse_args() + for path in run_example(args.input, args.output_dir): + print(path) -min_output_value = 0 -max_output_value = 2**texture_bit_depth-1 -for texture_index in range(textures.shape[2]): - # https://stackoverflow.com/a/65964648 - texture_slice = textures[:,:,texture_index].copy() - - logger.info('Rescaling texture to full range of DICOM bit depth:') - flattened = texture_slice.flatten() - - logger.info('Replacing NaN values with sentinel value...') - flattened = np.nan_to_num(flattened, nan=np.nanmin(flattened)) - - scaled = minmax_scale(flattened, (min_output_value, max_output_value)) - texture_slice = scaled.reshape(texture_slice.shape) - #logger.debug(np.histogram(texture_slice, range=(np.nanmin(texture_slice), np.nanmax(texture_slice)))) - logger.info('Rescaling texture to full range of DICOM bit depth done.') - - logger.info(f'Casting to {texture_dtype} and storing in instance...') - output_pixel_data = texture_slice.astype(texture_dtype) - #logger.debug(np.histogram(output_pixel_data, range=(np.nanmin(output_pixel_data), np.nanmax(output_pixel_data)))) - #logger.debug(f'output_pixel_data.dtype = {output_pixel_data.dtype}') - instance.PixelData = output_pixel_data - logger.info('Storing in instance done.') - - feature_name = collageradiomics.HaralickFeature(texture_index).name - filename = f'collage_feature_{texture_index:02d}_{feature_name}.dcm' - instance.save_as(filename) - logger.info(f'Saved {filename = }') +if __name__ == "__main__": + main() diff --git a/packaging/catalogs/CollageRadiomics.json b/packaging/catalogs/CollageRadiomics.json new file mode 100644 index 0000000..2263bcc --- /dev/null +++ b/packaging/catalogs/CollageRadiomics.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/Slicer/Slicer/main/Schemas/slicer-extension-catalog-entry-schema-v1.0.1.json#", + "build_dependencies": [], + "build_subdirectory": ".", + "category": "Informatics", + "scm_revision": "master", + "scm_url": "https://github.com/radxtools/collageradiomics.git", + "tier": 1 +} diff --git a/packaging/catalogs/README.rst b/packaging/catalogs/README.rst new file mode 100644 index 0000000..e67094e --- /dev/null +++ b/packaging/catalogs/README.rst @@ -0,0 +1,29 @@ +Catalog contributions +===================== + +MRHub +----- + +``mrhub.json`` is an entry for ``ismrm/mrhub``'s ``_data/projects.json``. Prepend +it to the existing list without changing other projects. Copy the unchanged +``slicer/collageradiomics/collageradiomics.png`` to +``images_packages/collageradiomics.png``. The entry uses MRHub's existing Image +processing category and leaves the citation count for its automatic updater. +Follow the upstream README and submit both files together. Acceptance is tracked +in issue #73. + +3D Slicer +--------- + +``CollageRadiomics.json`` follows the current ExtensionsIndex schema and points +to the repository root. The root CMake entry point delegates to the extension +sources; ``build_subdirectory`` describes the build output, not a source subtree. +The extension can still be configured directly from ``slicer/collageradiomics``. + +The extension imports the core on demand. If it is absent or too old, the module +shows an Install button and retries its import after the user installs it. +Discovery at Slicer startup does not install packages. Native tests run in Slicer 5.12.4. +Submit the JSON file to ``Slicer/ExtensionsIndex`` after merging the source changes. +Keep the submission in draft until collageradiomics 0.4 is available from PyPI; +fresh Slicer installations need that dependency. Catalog acceptance is tracked +in issue #83. diff --git a/packaging/catalogs/mrhub.json b/packaging/catalogs/mrhub.json new file mode 100644 index 0000000..f1223bd --- /dev/null +++ b/packaging/catalogs/mrhub.json @@ -0,0 +1,31 @@ +{ + "name": "CoLlAGe Radiomics", + "category": "Image processing", + "shortDescription": "Voxel-wise radiomics from co-occurrences of local gradient orientations.", + "imageFile": "collageradiomics.png", + "mainURL": "https://github.com/radxtools/collageradiomics", + "repoURL": "https://github.com/radxtools/collageradiomics", + "principalDevelopers": "BrIC Laboratory; Rob Toth and Nathan Hillyer (Toth Technology); RadxTools contributors", + "longDescription": "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe) describes local disorder in image gradients. This open-source Python implementation produces thirteen voxel-wise Haralick statistics from 2D or 3D image regions. It includes a medical-image command-line interface, reproducible notebooks, Docker targets, and a 3D Slicer extension. The 0.4 series corrects numerical behavior from 0.3.x; experiments upgrading from that series should recompute cached features and record extraction parameters.", + "keywords": ["radiomics", "MRI", "texture", "gradient orientation", "CoLlAGe", "Python", "3D Slicer"], + "keyReferences": [ + { + "referenceText": "Prasanna, Tiwari, and Madabhushi (2016). Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports 6:37241.", + "referenceURL": "https://doi.org/10.1038/srep37241" + } + ], + "extraResources": [ + { + "resourceType": "Jupyter Notebook", + "URL": "https://github.com/radxtools/collageradiomics/blob/master/jupyter/examples/example.ipynb" + }, + { + "resourceType": "Documentation", + "URL": "https://github.com/radxtools/collageradiomics/blob/master/README.md" + } + ], + "dateAddedToMRHub": "2026-10-20", + "dateSoftwareLastUpdated": "2026-10-20", + "citationSearchString": "10.1038/srep37241", + "citationCount": "" +} diff --git a/packaging/conda/README.rst b/packaging/conda/README.rst new file mode 100644 index 0000000..fb3a8e0 --- /dev/null +++ b/packaging/conda/README.rst @@ -0,0 +1,34 @@ +Conda +===== + +From the repository root, create a development environment with native +dependencies from conda-forge:: + + conda env create -f environment.yml + conda activate collageradiomics + python -m pytest + +The pip section installs the checkout in editable mode. Run the environment +creation command from the repository root so that ``./module`` resolves correctly. +Use ``conda env update -f environment.yml --prune`` after dependency changes. +The native Conda package requires Python 3.11 or newer because conda-forge's +Mahotas 1.4.19 builds start at 3.11. Pip installations also support Python 3.10. + +Build and test a local Conda package:: + + conda install --override-channels -c conda-forge conda-build + conda build --override-channels -c conda-forge --python 3.12 packaging/conda + conda install --use-local --override-channels -c conda-forge collageradiomics + +The recipe builds the checkout, includes the CLI, and tests both 2D and 3D +extraction. CI independently tests the editable environment and built package. +Keep its explicit version aligned with ``module/VERSION`` when releasing. + +This local recipe is not a published conda-forge feedstock. The separate v1 recipe +in ``conda-forge/recipe.yaml`` prepares an upstream submission using an official +commit archive and verified SHA256. It builds with ``rattler-build``; keep the +submission in draft until the source release and maintainer confirmation are +ready. Replace the commit archive with the release archive and its SHA256 before +requesting acceptance. A maintainer must accept the feedstock invitation. The +old staged-recipes PR #14719 was closed without creating a feedstock. See issue +#25 for the current submission status. diff --git a/packaging/conda/conda-forge/recipe.yaml b/packaging/conda/conda-forge/recipe.yaml new file mode 100644 index 0000000..8146c82 --- /dev/null +++ b/packaging/conda/conda-forge/recipe.yaml @@ -0,0 +1,60 @@ +schema_version: 1 + +context: + version: "0.4.0" + python_min: "3.11" + +package: + name: collageradiomics + version: ${{ version }} + +# Draft submission: replace with the release archive and checksum after publication. +source: + url: https://github.com/radxtools/collageradiomics/archive/78f52da3659d866db676fd6782eaaddce784e359.tar.gz + sha256: 5e1ac1436a7b4fe823aa69499b8a1281e27f2d43330fd0a58098009c4350992f + +build: + number: 0 + noarch: python + script: python -m pip install ./module --no-deps --no-build-isolation -vv + +requirements: + host: + - python ${{ python_min }}.* + - pip + - setuptools >=68 + - wheel + run: + - python >=${{ python_min }} + - numpy >=1.26,<3 + - scipy >=1.11,<2 + - scikit-image >=0.22,<1 + - mahotas >=1.4.19,<2 + - click >=8.1,<9 + - simpleitk >=2.3,<3 + +tests: + - python: + imports: + - collageradiomics + - _collageradiomics + pip_check: true + python_version: ${{ python_min }}.* + - script: + - collageradiomics --help + - python packaging/conda/run_test.py + files: + source: + - packaging/conda/run_test.py + +about: + homepage: https://github.com/radxtools/collageradiomics + documentation: https://collageradiomics.readthedocs.io/ + repository: https://github.com/radxtools/collageradiomics + summary: Local gradient-orientation radiomics in Python and 3D Slicer + license: BSD-3-Clause + license_file: LICENSE + +extra: + recipe-maintainers: + - mohsenhariri diff --git a/packaging/conda/meta.yaml b/packaging/conda/meta.yaml new file mode 100644 index 0000000..7d59537 --- /dev/null +++ b/packaging/conda/meta.yaml @@ -0,0 +1,47 @@ +{% set version = "0.4.0" %} + +package: + name: collageradiomics + version: {{ version }} + +source: + path: ../.. + +build: + number: 0 + noarch: python + script: {{ PYTHON }} -m pip install ./module --no-deps --no-build-isolation -vv + +requirements: + host: + - python >=3.11 + - pip + - setuptools >=68 + - wheel + run: + - python >=3.11 + - numpy >=1.26,<3 + - scipy >=1.11,<2 + - scikit-image >=0.22,<1 + - mahotas >=1.4.19,<2 + - click >=8.1,<9 + - simpleitk >=2.3,<3 + +test: + imports: + - collageradiomics + - _collageradiomics + commands: + - collageradiomics --help + +about: + home: https://github.com/radxtools/collageradiomics + doc_url: https://collageradiomics.readthedocs.io/ + dev_url: https://github.com/radxtools/collageradiomics + summary: Local gradient-orientation radiomics in Python and 3D Slicer + license: BSD-3-Clause + license_file: LICENSE + +extra: + recipe-maintainers: + - mohsenhariri diff --git a/packaging/conda/run_test.py b/packaging/conda/run_test.py new file mode 100644 index 0000000..7177410 --- /dev/null +++ b/packaging/conda/run_test.py @@ -0,0 +1,12 @@ +import numpy as np +from collageradiomics import Collage + +for shape in ((8, 9), (8, 9, 3)): + image = np.random.default_rng(42).random(shape) + mask = np.zeros(shape, dtype=bool) + mask[0:2, 1:3] = True + output = Collage(image, mask, svd_radius=1, num_unique_angles=4).execute() + expected = shape + ((13,) if image.ndim == 2 else (13, 2)) + assert output.shape == expected + assert np.isfinite(output[mask]).all() + assert np.isnan(output[~mask]).all() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b325ae3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.mypy] +python_version = "3.10" +files = ["module/collageradiomics", "module/_collageradiomics"] +disallow_untyped_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_unused_ignores = true +ignore_missing_imports = true + +[tool.ruff] +target-version = "py310" +line-length = 100 + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..534701d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +minversion = 8.0 +testpaths = tests +addopts = -ra --strict-markers +xfail_strict = true diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..a8822bd --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,9 @@ +build>=1.2,<2 +pytest>=9.0.3,<10 +setuptools>=68 +wheel>=0.43 +pydicom>=3,<4 +click>=8.1,<9 +SimpleITK>=2.3,<3 +mypy>=1.13,<2 +ruff>=0.11,<1 diff --git a/requirements.txt b/requirements.txt index ca6fbe0..a9c218b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1 @@ -contourpy==1.0.7 -cycler==0.11.0 -distro==1.8.0 -fonttools==4.38.0 -imageio==2.25.1 -joblib==1.2.0 -kiwisolver==1.4.4 -mahotas==1.4.13 -matplotlib==3.6.3 -networkx==3.0 -nibabel==5.0.1 -numpy==1.24.1 -packaging==23.0 -Pillow==9.4.0 -pyparsing==3.0.9 -python-dateutil==2.8.2 -PyWavelets==1.4.1 -scikit-build==0.16.6 -scikit-image==0.19.3 -scikit-learn==1.2.1 -scipy==1.10.0 -six==1.16.0 -threadpoolctl==3.1.0 -tifffile==2023.2.3 +-e ./module[examples,cli] diff --git a/slicer/collageradiomics/CMakeLists.txt b/slicer/collageradiomics/CMakeLists.txt index 6ca1dbf..2e029b3 100644 --- a/slicer/collageradiomics/CMakeLists.txt +++ b/slicer/collageradiomics/CMakeLists.txt @@ -2,26 +2,5 @@ cmake_minimum_required(VERSION 3.13.4) project(CollageRadiomics) -#----------------------------------------------------------------------------- -# Extension meta-information -set(EXTENSION_HOMEPAGE "https://github.com/radxtools/collageradiomics/tree/master/slicer/collageradiomics") -set(EXTENSION_CATEGORY "Informatics") -set(EXTENSION_CONTRIBUTORS "Pallavi Tiwari (BriC), Satish Viswanath (BriC), Rob Toth (Toth Technology), Nathan Hillyer (Toth Technology)") -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.") -set(EXTENSION_ICONURL "https://github.com/radxtools/collageradiomics/raw/master/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/Icons/CollageRadiomicsSlicer.png") -set(EXTENSION_SCREENSHOTURLS "https://github.com/radxtools/collageradiomics/raw/master/slicer/collageradiomics/CollageRadiomicsSlicer/Screenshot.jpg") -set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies - -#----------------------------------------------------------------------------- -# Extension dependencies -find_package(Slicer REQUIRED) -include(${Slicer_USE_FILE}) - -#----------------------------------------------------------------------------- -# Extension modules -add_subdirectory(CollageRadiomicsSlicer) -## NEXT_MODULE - -#----------------------------------------------------------------------------- -include(${Slicer_EXTENSION_GENERATE_CONFIG}) -include(${Slicer_EXTENSION_CPACK}) \ No newline at end of file +# Compatibility entry point for direct builds of this subdirectory. +include("${CMAKE_CURRENT_LIST_DIR}/../../CMakeLists.txt") diff --git a/slicer/collageradiomics/CollageRadiomicsSlicer/CollageRadiomicsSlicer.py b/slicer/collageradiomics/CollageRadiomicsSlicer/CollageRadiomicsSlicer.py index 5afe215..c44e20f 100644 --- a/slicer/collageradiomics/CollageRadiomicsSlicer/CollageRadiomicsSlicer.py +++ b/slicer/collageradiomics/CollageRadiomicsSlicer/CollageRadiomicsSlicer.py @@ -1,38 +1,55 @@ -import os -import unittest +import datetime +import importlib +import importlib.util +import inspect +import json import logging -import vtk, qt, ctk, slicer +import sys +from pathlib import Path + +import ctk import numpy as np -import datetime -from slicer.ScriptedLoadableModule import * +import qt +import vtk +from slicer.ScriptedLoadableModule import ( + ScriptedLoadableModule, + ScriptedLoadableModuleLogic, + ScriptedLoadableModuleTest, + ScriptedLoadableModuleWidget, +) from slicer.util import VTKObservationMixin -try: - import collageradiomics -except: - slicer.util.pip_install('collageradiomics') - slicer.util.pip_install('mahotas') -from collageradiomics import HaralickFeature, DifferenceVarianceInterpretation, Collage -# -# CollageRadiomicsSlicer -# +import slicer + + +def load_core(): + """Import on demand; never install packages while Slicer discovers modules.""" + core = importlib.import_module("collageradiomics") + if "progress_callback" not in inspect.signature(core.Collage).parameters: + raise ImportError("The Slicer extension requires collageradiomics 0.4 or newer") + return core + + +class CollageCancelled(RuntimeError): + """The user cancelled extraction through the progress dialog.""" + class CollageRadiomicsSlicer(ScriptedLoadableModule): - """Uses ScriptedLoadableModule base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "CollageRadiomics" - self.parent.categories = ["Informatics"] - self.parent.dependencies = [] - self.parent.contributors = ["BriC Lab (Case Western University)"] - self.parent.helpText = """ + """Uses ScriptedLoadableModule base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "CollageRadiomics" + self.parent.categories = ["Informatics"] + self.parent.dependencies = [] + self.parent.contributors = ["BriC Lab (Case Western University)"] + self.parent.helpText = """ 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. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ If you make use of this implementation, please cite the following paper: [1] Prasanna, P., Tiwari, P., & Madabhushi, A. (2016). "Co-occurrence of Local Anisotropic Gradient Orientations (CoLlAGe): A new radiomics descriptor. Scientific Reports", 6:37241. @@ -40,284 +57,608 @@ def __init__(self, parent): [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. """ + # # CollageRadiomicsSlicerWidget # -class CollageRadiomicsSlicerWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent=None): - """ - Called when the user opens the module the first time and the widget is initialized. - """ - ScriptedLoadableModuleWidget.__init__(self, parent) - VTKObservationMixin.__init__(self) - self.logic = None - self._parameterNode = None - - def setup(self): - """ - Called when the user opens the module the first time and the widget is initialized. - """ - ScriptedLoadableModuleWidget.setup(self) - - uiWidget = slicer.util.loadUI(self.resourcePath('UI/CollageRadiomicsSlicer.ui')) - self.layout.addWidget(uiWidget) - self.ui = slicer.util.childWidgetVariables(uiWidget) - uiWidget.setMRMLScene(slicer.mrmlScene) - self.layout.addStretch(1) - - self.ui.CheckBox.setVisible(False) - advancedFormLayout = self.ui.featuresGridLayout - - self.individualFeatures = {} - - for i, feature in enumerate(HaralickFeature): - checkBox = ctk.ctkCheckBox() - checkBox.text = feature.name - self.individualFeatures[feature] = checkBox - checkBox.connect('clicked(bool)', self.onIndividualFeature) - row = i / 2 - column = i % 2 - advancedFormLayout.addWidget(checkBox, row, column) - - self.allFeatures = ctk.ctkCheckBox() - self.allFeatures.text = 'All' - self.allFeatures.setChecked(True) - self.allFeatures.connect('clicked(bool)', self.onAllFeature) - row = len(HaralickFeature) / 2 - column = len(HaralickFeature) % 2 - advancedFormLayout.addWidget(self.allFeatures, row, column) - - self.ui.inputMaskSelector.nodeTypes = ['vtkMRMLLabelMapVolumeNode', 'vtkMRMLSegmentationNode'] - self.ui.inputMaskSelector.selectNodeUponCreation = True - self.ui.inputMaskSelector.addEnabled = False - self.ui.inputMaskSelector.removeEnabled = False - self.ui.inputMaskSelector.noneEnabled = True - self.ui.inputMaskSelector.showHidden = False - self.ui.inputMaskSelector.showChildNodeTypes = False - self.ui.inputMaskSelector.setMRMLScene(slicer.mrmlScene) - self.ui.inputMaskSelector.setToolTip('Pick the regions for feature calculation - defined by a segmentation or labelmap volume node.') - - for interpretation in DifferenceVarianceInterpretation: - self.ui.differenceVarianceComboBox.addItem(interpretation.name) - - self.ui.phiCheckBox.connect('clicked(bool)', self.onPhi) - self.ui.thetaCheckBox.connect('clicked(bool)', self.onTheta) - - self.logic = CollageRadiomicsSlicerLogic() - self.ui.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", self.moduleName) - self.setParameterNode(self.logic.getParameterNode()) - - self.ui.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode) - self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) - - self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) - self.ui.inputMaskSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) - - self.updateGUIFromParameterNode() - - def onIndividualFeature(self): - self.allFeatures.setChecked(False) - - def onAllFeature(self): - for feature in self.individualFeatures.values(): - feature.setChecked(False) - - def onPhi(self): - if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: - self.ui.thetaCheckBox.setChecked(True) - - def onTheta(self): - if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: - self.ui.phiCheckBox.setChecked(True) - - def cleanup(self): - """ - Called when the application closes and the module widget is destroyed. - """ - self.removeObservers() - - def setParameterNode(self, inputParameterNode): - """ - Adds observers to the selected parameter node. Observation is needed because when the - parameter node is changed then the GUI must be updated immediately. - """ - - if inputParameterNode: - self.logic.setDefaultParameters(inputParameterNode) - - # Set parameter node in the parameter node selector widget - wasBlocked = self.ui.parameterNodeSelector.blockSignals(True) - self.ui.parameterNodeSelector.setCurrentNode(inputParameterNode) - self.ui.parameterNodeSelector.blockSignals(wasBlocked) - - if inputParameterNode == self._parameterNode: - return - - if self._parameterNode is not None: - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - if inputParameterNode is not None: - self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - self._parameterNode = inputParameterNode - - self.updateGUIFromParameterNode() - - def updateGUIFromParameterNode(self, caller=None, event=None): - """ - This method is called whenever parameter node is changed. - The module GUI is updated to show the current state of the parameter node. - """ - self.ui.basicCollapsibleButton.enabled = self._parameterNode is not None - self.ui.advancedCollapsibleButton.enabled = self._parameterNode is not None - if self._parameterNode is None: - return - - wasBlocked = self.ui.inputSelector.blockSignals(True) - self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume")) - self.ui.inputSelector.blockSignals(wasBlocked) - - wasBlocked = self.ui.inputMaskSelector.blockSignals(True) - self.ui.inputMaskSelector.setCurrentNode(self._parameterNode.GetNodeReference("MaskVolume")) - self.ui.inputMaskSelector.blockSignals(wasBlocked) - - if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("MaskVolume"): - self.ui.applyButton.toolTip = "Compute collage" - self.ui.applyButton.enabled = True - else: - self.ui.applyButton.toolTip = "Select input and mask volume nodes" - self.ui.applyButton.enabled = False - - def updateParameterNodeFromGUI(self, caller=None, event=None): - """ - This method is called when the user makes any change in the GUI. - The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). - """ - - if self._parameterNode is None: - return - self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) - self._parameterNode.SetNodeReferenceID("MaskVolume", self.ui.inputMaskSelector.currentNodeID) - - def onApplyButton(self): - """ - Run processing when user clicks "Apply" button. +class CollageRadiomicsSlicerWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - features = [] - for feature, checkbox in self.individualFeatures.items(): - if checkbox.checked or self.allFeatures.checked: - features.append(feature) - - svd_radius = self.ui.svdSlider.value - haralick_size = self.ui.windowSlider.value - grey_levels = self.ui.graylevelsSlider.value - dimensions = [] - if self.ui.phiCheckBox.checked: - dimensions.append(0) - if self.ui.thetaCheckBox.checked: - dimensions.append(1) - - try: - self.logic.run(self.ui.inputSelector.currentNode(), self.ui.inputMaskSelector.currentNode(), features=features, window_size=haralick_size, grey_levels=grey_levels, dimensions=dimensions) - except Exception as e: - slicer.util.errorDisplay("Failed to compute results: "+str(e)) - import traceback - traceback.print_exc() + def __init__(self, parent=None): + """ + Called when the user opens the module the first time and the widget is initialized. + """ + self.logic = None + self.core = None + self._parameterNode = None + self._updatingGUI = False + VTKObservationMixin.__init__(self) + ScriptedLoadableModuleWidget.__init__(self, parent) + + def resourcePath(self, filename): + return str(Path(__file__).parent / "Resources" / filename) + + def setup(self): + """ + Called when the user opens the module the first time and the widget is initialized. + """ + ScriptedLoadableModuleWidget.setup(self) + try: + self.core = load_core() + except ImportError: + self.dependenciesWidget = qt.QWidget() + layout = qt.QVBoxLayout(self.dependenciesWidget) + message = qt.QLabel( + "CoLlAGe 0.4 is required. Install it and its numerical dependencies " + "from PyPI into Slicer's Python environment. An internet connection " + "is required." + ) + message.wordWrap = True + layout.addWidget(message) + self.installButton = qt.QPushButton("Install CoLlAGe") + self.installButton.connect("clicked(bool)", self.onInstallCore) + layout.addWidget(self.installButton) + self.layout.addWidget(self.dependenciesWidget) + return + self.setupExtraction() + + def onInstallCore(self): + self.installButton.enabled = False + try: + with slicer.util.tryWithErrorDisplay("Unable to install CoLlAGe", waitCursor=True): + slicer.util.pip_install("collageradiomics>=0.4.0,<0.5") + importlib.invalidate_caches() + previous = sys.modules.get("collageradiomics") + if ( + previous is not None + and "progress_callback" not in inspect.signature(previous.Collage).parameters + ): + importlib.reload(previous) + self.core = load_core() + self.setupExtraction() + self.dependenciesWidget.hide() + finally: + self.installButton.enabled = True + + def setupExtraction(self): + + uiWidget = slicer.util.loadUI(self.resourcePath("UI/CollageRadiomicsSlicer.ui")) + self.layout.addWidget(uiWidget) + self.ui = slicer.util.childWidgetVariables(uiWidget) + uiWidget.setMRMLScene(slicer.mrmlScene) + self.layout.addStretch(1) + + self.ui.CheckBox.setVisible(False) + advancedFormLayout = self.ui.featuresGridLayout + + self.individualFeatures = {} + + for i, feature in enumerate(self.core.HaralickFeature): + checkBox = ctk.ctkCheckBox() + checkBox.text = feature.name + self.individualFeatures[feature] = checkBox + checkBox.connect("clicked(bool)", self.onIndividualFeature) + row = i // 2 + column = i % 2 + advancedFormLayout.addWidget(checkBox, row, column) + + self.allFeatures = ctk.ctkCheckBox() + self.allFeatures.text = "All" + self.allFeatures.setChecked(True) + self.allFeatures.connect("clicked(bool)", self.onAllFeature) + row = len(self.core.HaralickFeature) // 2 + column = len(self.core.HaralickFeature) % 2 + advancedFormLayout.addWidget(self.allFeatures, row, column) + + self.ui.inputMaskSelector.nodeTypes = [ + "vtkMRMLLabelMapVolumeNode", + "vtkMRMLSegmentationNode", + ] + self.ui.inputMaskSelector.selectNodeUponCreation = True + self.ui.inputMaskSelector.addEnabled = False + self.ui.inputMaskSelector.removeEnabled = False + self.ui.inputMaskSelector.noneEnabled = True + self.ui.inputMaskSelector.showHidden = False + self.ui.inputMaskSelector.showChildNodeTypes = False + self.ui.inputMaskSelector.setMRMLScene(slicer.mrmlScene) + self.ui.inputMaskSelector.setToolTip( + "Pick the regions for feature calculation - defined by a segmentation or labelmap volume node." + ) + + for interpretation in self.core.DifferenceVarianceInterpretation: + self.ui.differenceVarianceComboBox.addItem(interpretation.name) + + self.ui.phiCheckBox.connect("clicked(bool)", self.onPhi) + self.ui.thetaCheckBox.connect("clicked(bool)", self.onTheta) + + self.logic = CollageRadiomicsSlicerLogic() + self.ui.parameterNodeSelector.addAttribute( + "vtkMRMLScriptedModuleNode", "ModuleName", self.moduleName + ) + self.setParameterNode(self.logic.getParameterNode()) + + self.ui.parameterNodeSelector.connect( + "currentNodeChanged(vtkMRMLNode*)", self.setParameterNode + ) + self.ui.applyButton.connect("clicked(bool)", self.onApplyButton) + + self.ui.inputSelector.connect( + "currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI + ) + self.ui.inputMaskSelector.connect( + "currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI + ) + + for slider in (self.ui.svdSlider, self.ui.windowSlider, self.ui.graylevelsSlider): + slider.connect("valueChanged(double)", self.updateParameterNodeFromGUI) + self.ui.differenceVarianceComboBox.connect( + "currentIndexChanged(int)", self.updateParameterNodeFromGUI + ) + self.ui.thetaCheckBox.connect("clicked(bool)", self.updateParameterNodeFromGUI) + self.ui.phiCheckBox.connect("clicked(bool)", self.updateParameterNodeFromGUI) + self.updateGUIFromParameterNode() + + def onIndividualFeature(self): + self.allFeatures.setChecked(False) + self.updateParameterNodeFromGUI() + + def onAllFeature(self): + for feature in self.individualFeatures.values(): + feature.setChecked(False) + self.updateParameterNodeFromGUI() + + def onPhi(self): + if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: + self.ui.thetaCheckBox.setChecked(True) + + def onTheta(self): + if not self.ui.phiCheckBox.checked and not self.ui.thetaCheckBox.checked: + self.ui.phiCheckBox.setChecked(True) + + def cleanup(self): + """ + Called when the application closes and the module widget is destroyed. + """ + self.removeObservers() + + def setParameterNode(self, inputParameterNode): + """ + Adds observers to the selected parameter node. Observation is needed because when the + parameter node is changed then the GUI must be updated immediately. + """ + + if inputParameterNode: + self.logic.setDefaultParameters(inputParameterNode) + + # Set parameter node in the parameter node selector widget + wasBlocked = self.ui.parameterNodeSelector.blockSignals(True) + self.ui.parameterNodeSelector.setCurrentNode(inputParameterNode) + self.ui.parameterNodeSelector.blockSignals(wasBlocked) + + if inputParameterNode == self._parameterNode: + return + + if self._parameterNode is not None: + self.removeObserver( + self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode + ) + if inputParameterNode is not None: + self.addObserver( + inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode + ) + self._parameterNode = inputParameterNode + + self.updateGUIFromParameterNode() + + def updateGUIFromParameterNode(self, caller=None, event=None): + if self._updatingGUI: + return + self.ui.basicCollapsibleButton.enabled = self._parameterNode is not None + self.ui.advancedCollapsibleButton.enabled = self._parameterNode is not None + if self._parameterNode is None: + self.ui.applyButton.enabled = False + return + self._updatingGUI = True + try: + node = self._parameterNode + self.ui.inputSelector.setCurrentNode(node.GetNodeReference("InputVolume")) + self.ui.inputMaskSelector.setCurrentNode(node.GetNodeReference("MaskVolume")) + self.ui.svdSlider.value = int(node.GetParameter("SVDRadius")) + self.ui.windowSlider.value = int(node.GetParameter("HaralickWindow")) + self.ui.graylevelsSlider.value = int(node.GetParameter("Bins")) + self.ui.differenceVarianceComboBox.currentIndex = int(node.GetParameter("Variance")) + self.ui.thetaCheckBox.checked = node.GetParameter("Theta") == "true" + self.ui.phiCheckBox.checked = node.GetParameter("Phi") == "true" + selected = node.GetParameter("Features") + self.allFeatures.checked = selected == "all" + chosen = selected.split(",") + for feature, checkbox in self.individualFeatures.items(): + checkbox.checked = str(int(feature)) in chosen + has_features = self.allFeatures.checked or any( + c.checked for c in self.individualFeatures.values() + ) + self.ui.applyButton.enabled = bool( + node.GetNodeReference("InputVolume") + and node.GetNodeReference("MaskVolume") + and has_features + ) + self.ui.applyButton.toolTip = ( + "Compute CoLlAGe" + if self.ui.applyButton.enabled + else "Select an image, mask, and features" + ) + finally: + self._updatingGUI = False + + def updateParameterNodeFromGUI(self, caller=None, event=None): + if self._parameterNode is None or self._updatingGUI: + return + node = self._parameterNode + previous = node.StartModify() + try: + node.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) + node.SetNodeReferenceID("MaskVolume", self.ui.inputMaskSelector.currentNodeID) + node.SetParameter("SVDRadius", str(int(self.ui.svdSlider.value))) + node.SetParameter("HaralickWindow", str(int(self.ui.windowSlider.value))) + node.SetParameter("Bins", str(int(self.ui.graylevelsSlider.value))) + node.SetParameter("Variance", str(self.ui.differenceVarianceComboBox.currentIndex)) + node.SetParameter("Theta", "true" if self.ui.thetaCheckBox.checked else "false") + node.SetParameter("Phi", "true" if self.ui.phiCheckBox.checked else "false") + selected = ( + "all" + if self.allFeatures.checked + else ",".join( + str(int(f)) + for f, checkbox in self.individualFeatures.items() + if checkbox.checked + ) + ) + node.SetParameter("Features", selected) + finally: + node.EndModify(previous) + + def onApplyButton(self): + features = [ + feature + for feature, checkbox in self.individualFeatures.items() + if checkbox.checked or self.allFeatures.checked + ] + dimensions = [] + if self.ui.thetaCheckBox.checked: + dimensions.append(0) + if self.ui.phiCheckBox.checked: + dimensions.append(1) + progress = slicer.util.createProgressDialog(windowTitle="CoLlAGe", autoClose=False) + self.ui.applyButton.enabled = False + + def report(stage, completed, total): + progress.setLabelText(stage.replace("-", " ").capitalize()) + progress.setValue(int(100 * completed / total)) + slicer.app.processEvents() + if progress.wasCanceled: + raise CollageCancelled("Extraction cancelled") + + try: + self.logic.run( + self.ui.inputSelector.currentNode(), + self.ui.inputMaskSelector.currentNode(), + features=features, + dimensions=dimensions, + svd_radius=int(self.ui.svdSlider.value), + window_size=int(self.ui.windowSlider.value), + grey_levels=int(self.ui.graylevelsSlider.value), + difference_variance_interpretation=self.core.DifferenceVarianceInterpretation( + self.ui.differenceVarianceComboBox.currentIndex + ), + progress_callback=report, + ) + except CollageCancelled: + logging.info("CoLlAGe extraction cancelled") + except Exception as exc: + slicer.util.errorDisplay("Failed to compute results: " + str(exc)) + logging.exception("CoLlAGe extraction failed") + finally: + progress.close() + self.updateGUIFromParameterNode() -# -# CollageRadiomicsSlicerLogic -# class CollageRadiomicsSlicerLogic(ScriptedLoadableModuleLogic): - """This class should implement all the actual - computation done by your module. The interface - should be such that other python code can import - this class and make use of the functionality without - requiring an instance of the Widget. - Uses ScriptedLoadableModuleLogic base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setDefaultParameters(self, parameterNode): - """ - Initialize parameter node with default settings. - """ - # if not parameterNode.GetParameter("Threshold"): - # parameterNode.SetParameter("Threshold", "50.0") - # if not parameterNode.GetParameter("Invert"): - # parameterNode.SetParameter("Invert", "false") - - 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): - # This will convert the mask segmentation into a binary representation via LabelMapVolume - # LabelMapVolume supports conversion to numpy arrays and that's what we need - # to input to collage. - labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode') - slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode(mask, labelmapVolumeNode, inputVolume) - inputMaskArray = slicer.util.arrayFromVolume(labelmapVolumeNode) - # collage expects the image data to come first and arrayFromVolume returns it reversed order - # as such, we switch the first and last axes - inputMaskArray = np.swapaxes(inputMaskArray, 0, 2) - - # Input volume can be converted directly to a numpy array, unlike the mask. - inputArray = slicer.util.arrayFromVolume(inputVolume) - - # same as above, need to swap the axes - inputArray = np.swapaxes(inputArray, 0, 2) - - num_mask_values = np.count_nonzero(inputMaskArray) - max_num_mask_before_warning = 5000 - max_num_mask_before_error = 50000 - presentable_num_mask_values = num_mask_values / float(1000) - if num_mask_values > max_num_mask_before_warning: - warning = 'briefly' - if num_mask_values > max_num_mask_before_error: - warning = 'EXTENSIVELY' - 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?') - if response == False: - logging.info('User cancelled collage from large mask warning.') - return - - collage = Collage(inputArray, inputMaskArray, svd_radius=svd_radius, verbose_logging=True, haralick_window_size=window_size, num_unique_angles=int(grey_levels)) - results = collage.execute() - - iso_time_string = datetime.datetime.now().isoformat() - for dimension in dimensions: - for feature in features: - outputVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLScalarVolumeNode') - dimension_name = 'theta' - if dimension == 1: - dimension_name = 'phi' - outputVolumeNode.SetName(f'collage_{iso_time_string}_{feature.name.lower()}_{dimension_name}') - node_data = results[:,:,:,feature,dimension] - node_data = np.swapaxes(node_data, 0, 2) - outputVolumeNode.CopyOrientation(inputVolume) - slicer.util.updateVolumeFromArray(outputVolumeNode, node_data) - - - logging.info('Processing completed') - slicer.mrmlScene.RemoveNode(labelmapVolumeNode) - -# CollageRadiomicsSlicerTest -# + def setDefaultParameters(self, parameterNode): + defaults = { + "SVDRadius": "5", + "HaralickWindow": "-1", + "Bins": "64", + "Variance": "0", + "Theta": "true", + "Phi": "false", + "Features": "all", + } + for key, value in defaults.items(): + if not parameterNode.GetParameter(key): + parameterNode.SetParameter(key, value) + + @staticmethod + def validate_geometry(image, mask): + if image.GetTransformNodeID() != mask.GetTransformNodeID(): + raise ValueError( + "Image and mask must use the same parent transform; harden or align them first" + ) + matrices = [] + for node in (image, mask): + matrix = vtk.vtkMatrix4x4() + node.GetIJKToRASMatrix(matrix) + matrices.append(slicer.util.arrayFromVTKMatrix(matrix)) + if not np.allclose(matrices[0], matrices[1], rtol=0, atol=1e-6): + raise ValueError("Image and mask geometry differ; align the mask before extraction") + if image.GetImageData().GetDimensions() != mask.GetImageData().GetDimensions(): + raise ValueError("Image and mask dimensions differ") + + def run( + self, + inputVolume, + mask, + dimensions=(0,), + invert=False, + showResult=True, + svd_radius=5, + verbose_logging=False, + features=None, + window_size=-1, + grey_levels=64, + difference_variance_interpretation=None, + progress_callback=None, + ): + core = load_core() + if features is None: + features = (core.HaralickFeature.Contrast,) + if difference_variance_interpretation is None: + difference_variance_interpretation = ( + core.DifferenceVarianceInterpretation.XMinusYVariance + ) + if inputVolume is None or mask is None: + raise ValueError("An input image and mask are required") + if not inputVolume.IsA("vtkMRMLScalarVolumeNode") or inputVolume.GetImageData() is None: + raise TypeError("Input must be a scalar volume with image data") + dimensions = tuple(dict.fromkeys(dimensions)) + features = tuple(core.HaralickFeature(feature) for feature in features) + if not dimensions or any(d not in (0, 1) for d in dimensions) or not features: + raise ValueError("Select at least one valid angle and feature") + temporary = None + outputs = [] + try: + if mask.IsA("vtkMRMLSegmentationNode"): + temporary = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") + temporary.SetAndObserveTransformNodeID(inputVolume.GetTransformNodeID()) + success = slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode( + mask, temporary, inputVolume + ) + if not success: + raise ValueError( + "The visible segments could not be exported to the image geometry" + ) + maskVolume = temporary + elif mask.IsA("vtkMRMLLabelMapVolumeNode"): + maskVolume = mask + else: + raise TypeError("Mask must be a segmentation or labelmap volume") + if maskVolume.GetImageData() is None: + raise ValueError("The mask has no image data") + self.validate_geometry(inputVolume, maskVolume) + inputArray = np.moveaxis(slicer.util.arrayFromVolume(inputVolume), 0, -1).copy() + maskArray = np.moveaxis(slicer.util.arrayFromVolume(maskVolume), 0, -1).copy() + if invert: + maskArray = maskArray == 0 + collage = core.Collage( + inputArray, + maskArray, + svd_radius=svd_radius, + verbose_logging=verbose_logging, + haralick_window_size=window_size, + num_unique_angles=int(grey_levels), + difference_variance_interpretation=difference_variance_interpretation, + progress_callback=progress_callback, + ) + results = collage.execute() + stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") + parameters = json.dumps( + { + "svd_radius": svd_radius, + "haralick_window_size": collage.haralick_window_size, + "num_unique_angles": int(grey_levels), + "difference_variance_interpretation": difference_variance_interpretation.name, + } + ) + for angle in dimensions: + suffix = "theta" if angle == 0 else "phi" + for feature in features: + node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + outputs.append(node) + node.SetName(f"collage_{stamp}_{feature.name.lower()}_{suffix}") + node.CopyOrientation(inputVolume) + node.SetAndObserveTransformNodeID(inputVolume.GetTransformNodeID()) + node.SetAttribute("CollageRadiomics.Parameters", parameters) + slicer.util.updateVolumeFromArray( + node, np.moveaxis(results[:, :, :, feature, angle], -1, 0) + ) + node.CreateDefaultDisplayNodes() + if showResult: + slicer.util.setSliceViewerLayers(background=outputs[0]) + return outputs + except Exception: + for node in outputs: + slicer.mrmlScene.RemoveNode(node) + raise + finally: + if temporary is not None: + slicer.mrmlScene.RemoveNode(temporary) -class CollageRadiomicsSlicerTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - Uses ScriptedLoadableModuleTest base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. - """ - slicer.mrmlScene.Clear(0) - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() +class CollageRadiomicsSlicerTest(ScriptedLoadableModuleTest): + def setUp(self): + self.core = load_core() + slicer.mrmlScene.Clear(0) + self.logic = CollageRadiomicsSlicerLogic() + self.image = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", "testImage") + self.mask = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", "testMask") + self.image.SetSpacing(0.7, 1.2, 2.5) + self.image.SetOrigin(10, 20, 30) + self.mask.CopyOrientation(self.image) + image = np.random.default_rng(56).random((4, 7, 9)) + mask = np.zeros(image.shape, dtype=np.uint8) + mask[:, 2:5, 3:7] = 1 + slicer.util.updateVolumeFromArray(self.image, image) + slicer.util.updateVolumeFromArray(self.mask, mask) + + def runTest(self): + for name in ( + "test_labelmap_and_angles", + "test_segmentation", + "test_geometry_rejected", + "test_cancellation_cleanup", + "test_widget_parameters", + "test_startup_without_core", + "test_explicit_install", + "test_old_core_rejected", + ): + self.setUp() + getattr(self, name)() + + def test_labelmap_and_angles(self): + transform = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLinearTransformNode") + self.image.SetAndObserveTransformNodeID(transform.GetID()) + self.mask.SetAndObserveTransformNodeID(transform.GetID()) + outputs = self.logic.run( + self.image, + self.mask, + dimensions=(0, 1), + showResult=False, + svd_radius=1, + grey_levels=8, + features=(self.core.HaralickFeature.Contrast, self.core.HaralickFeature.Entropy), + ) + self.assertEqual(len(outputs), 4) + original = slicer.util.arrayFromVolume(self.mask) != 0 + for node in outputs: + values = slicer.util.arrayFromVolume(node) + self.assertEqual(values.shape, original.shape) + self.assertTrue(np.isfinite(values[original]).all()) + self.assertTrue(np.isnan(values[~original]).all()) + self.logic.validate_geometry(self.image, node) + self.assertEqual( + json.loads(node.GetAttribute("CollageRadiomics.Parameters"))["svd_radius"], 1 + ) + self.assertEqual(sum(node.GetName().endswith("_theta") for node in outputs), 2) + self.assertEqual(sum(node.GetName().endswith("_phi") for node in outputs), 2) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLLabelMapVolumeNode"), 1) + + def test_segmentation(self): + segmentation = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + segmentation.CreateDefaultDisplayNodes() + segmentation.SetReferenceImageGeometryParameterFromVolumeNode(self.image) + self.assertTrue( + slicer.modules.segmentations.logic().ImportLabelmapToSegmentationNode( + self.mask, segmentation + ) + ) + outputs = self.logic.run( + self.image, segmentation, showResult=False, svd_radius=1, grey_levels=8 + ) + self.assertEqual(len(outputs), 1) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLLabelMapVolumeNode"), 1) + + def test_geometry_rejected(self): + self.mask.SetOrigin(11, 20, 30) + with self.assertRaisesRegex(ValueError, "geometry"): + self.logic.run(self.image, self.mask, showResult=False) + + def test_cancellation_cleanup(self): + segmentation = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + segmentation.CreateDefaultDisplayNodes() + slicer.modules.segmentations.logic().ImportLabelmapToSegmentationNode( + self.mask, segmentation + ) + + def cancel(*args): + raise CollageCancelled("cancelled") + + with self.assertRaises(CollageCancelled): + self.logic.run( + self.image, + segmentation, + showResult=False, + svd_radius=1, + grey_levels=8, + progress_callback=cancel, + ) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLLabelMapVolumeNode"), 1) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScalarVolumeNode"), 2) + + def test_widget_parameters(self): + widget = CollageRadiomicsSlicerWidget() + try: + widget.ui.inputSelector.setCurrentNode(self.image) + widget.ui.inputMaskSelector.setCurrentNode(self.mask) + widget.ui.svdSlider.value = 1 + widget.ui.windowSlider.value = 3 + widget.ui.graylevelsSlider.value = 8 + widget.ui.differenceVarianceComboBox.currentIndex = 1 + widget.ui.thetaCheckBox.checked = True + widget.ui.phiCheckBox.checked = True + widget.updateParameterNodeFromGUI() + captured = {} + widget.logic.run = lambda *args, **kwargs: captured.update(kwargs) + widget.onApplyButton() + self.assertEqual(captured["svd_radius"], 1) + self.assertEqual(captured["window_size"], 3) + self.assertEqual(captured["grey_levels"], 8) + self.assertEqual(captured["dimensions"], [0, 1]) + self.assertEqual( + captured["difference_variance_interpretation"], + self.core.DifferenceVarianceInterpretation.ProbabilityXMinusYVariance, + ) + self.assertEqual(widget._parameterNode.GetParameter("SVDRadius"), "1") + finally: + widget.cleanup() + widget.parent.deleteLater() + + def test_startup_without_core(self): + from unittest.mock import patch + + with ( + patch.dict(sys.modules, {"collageradiomics": None}), + patch.object(slicer.util, "pip_install") as install, + ): + spec = importlib.util.spec_from_file_location("CollageWithoutCore", __file__) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + install.assert_not_called() + + def test_explicit_install(self): + from unittest.mock import patch + + with ( + patch(__name__ + ".load_core", side_effect=[ImportError("missing"), self.core]), + patch.object(slicer.util, "pip_install") as install, + ): + widget = CollageRadiomicsSlicerWidget() + try: + install.assert_not_called() + self.assertIsNone(widget.core) + widget.onInstallCore() + install.assert_called_once_with("collageradiomics>=0.4.0,<0.5") + self.assertIsNotNone(widget.ui) + finally: + widget.cleanup() + widget.parent.deleteLater() + + def test_old_core_rejected(self): + from types import SimpleNamespace + from unittest.mock import patch + + with patch.dict(sys.modules, {"collageradiomics": SimpleNamespace(Collage=lambda: None)}): + with self.assertRaisesRegex(ImportError, "0.4"): + load_core() diff --git a/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/UI/CollageRadiomicsSlicer.ui b/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/UI/CollageRadiomicsSlicer.ui index 9e927f7..3f98b4b 100644 --- a/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/UI/CollageRadiomicsSlicer.ui +++ b/slicer/collageradiomics/CollageRadiomicsSlicer/Resources/UI/CollageRadiomicsSlicer.ui @@ -96,7 +96,7 @@ - + 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. @@ -109,7 +109,7 @@ - + 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. @@ -171,7 +171,7 @@ 0 - 2.000000000000000 + 1.000000000000000 20.000000000000000 @@ -184,7 +184,7 @@ - Haralick Window Size (-1 is default): + Odd Haralick Window Size (-1 is automatic): @@ -231,7 +231,7 @@ 0 - 32.000000000000000 + 2.000000000000000 256.000000000000000 diff --git a/slicer/collageradiomics/README.md b/slicer/collageradiomics/README.md index 628b309..0aef574 100644 --- a/slicer/collageradiomics/README.md +++ b/slicer/collageradiomics/README.md @@ -2,6 +2,8 @@ # CollageRadiomics Slicer Extension +Distributed under the [BSD 3-Clause license](../../LICENSE). + **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. **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. @@ -20,7 +22,30 @@ Details on extraction of **CoLlAGe** features are included in [\[1\]](#reference # Slicer _[Back to **Table of Contents**](#table-of-contents)_ -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. +The extension requires Slicer 5.10 or newer and collageradiomics 0.4 or newer. +When the required core is absent, open the module and click **Install CoLlAGe** +to install its dependencies from PyPI. Module discovery at startup does not install +packages. For an unpublished checkout, use the PythonSlicer install command below. +It accepts a scalar input volume and either a labelmap or visible segmentation. +Labelmaps must match the image geometry and parent transform. Segmentations are +exported to the input geometry before extraction. Outputs preserve image geometry +and transforms, with separate volumes for every selected feature and angle. + +The SVD radius, Haralick window, bin count, variance interpretation, features, and +angle choices are stored in the scene parameter node. Use an odd window size of +at least 3, or -1 to derive it from the SVD radius. A progress dialog supports +cancellation; temporary labelmaps and incomplete results are cleaned up on failure. + +To test a checkout with a local Slicer installation: + +```shell +/path/to/Slicer/bin/PythonSlicer -m pip install ./module +/path/to/Slicer/Slicer --ignore-slicerrc --testing --no-main-window --no-splash --python-script tests/run_slicer.py +``` + +On a headless Linux machine, prefix the application command with `xvfb-run -a`. +CI downloads a checksum-pinned Slicer 5.12.4 and runs the native labelmap, +segmentation, geometry, cancellation, and widget tests. ## Overview _[Back to **Table of Contents**](#table-of-contents)_ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1922a20 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +import numpy as np +import pytest + + +@pytest.fixture +def image_and_mask_2d(): + image = np.random.default_rng(42).random((50, 52)) + mask = np.zeros(image.shape, dtype=np.uint8) + mask[20:26, 21:29] = 1 + mask[22:24, 24:26] = 0 + return image, mask + + +@pytest.fixture +def image_and_mask_3d(): + image = np.random.default_rng(43).random((50, 52, 7)) + mask = np.zeros(image.shape, dtype=np.uint8) + mask[20:26, 21:27, 2:5] = 1 + return image, mask diff --git a/tests/container_smoke.py b/tests/container_smoke.py new file mode 100644 index 0000000..b1caee6 --- /dev/null +++ b/tests/container_smoke.py @@ -0,0 +1,36 @@ +"""Exercise the installed CLI inside its built container.""" + +import csv +import subprocess +from pathlib import Path +from tempfile import TemporaryDirectory + +import numpy as np +import SimpleITK as sitk + +with TemporaryDirectory() as directory: + root = Path(directory) + image = np.random.default_rng(70).random((7, 9)) + mask = np.ones(image.shape, dtype=np.uint8) + for name, values in [("image", image), ("mask", mask)]: + sitk.WriteImage(sitk.GetImageFromArray(values), str(root / f"{name}.mha")) + subprocess.run( + [ + "collageradiomics", + "-i", + str(root / "image.mha"), + "-m", + str(root / "mask.mha"), + "-o", + str(root / "features.csv"), + "-s", + "1", + "-b", + "8", + ], + check=True, + ) + with (root / "features.csv").open() as stream: + rows = list(csv.DictReader(stream)) + assert len(rows) == 52 + assert np.isfinite([float(row["Value"]) for row in rows]).all() diff --git a/tests/run_slicer.py b/tests/run_slicer.py new file mode 100644 index 0000000..53543e5 --- /dev/null +++ b/tests/run_slicer.py @@ -0,0 +1,24 @@ +"""Run the extension integration suite with Slicer --python-script.""" + +import sys +import traceback +import unittest +from pathlib import Path + +import slicer + +repository = Path(__file__).resolve().parents[1] +sys.path[:0] = [ + str(repository / "module"), + str(repository / "slicer/collageradiomics/CollageRadiomicsSlicer"), +] + +try: + from CollageRadiomicsSlicer import CollageRadiomicsSlicerTest + + suite = unittest.defaultTestLoader.loadTestsFromTestCase(CollageRadiomicsSlicerTest) + result = unittest.TextTestRunner(verbosity=2).run(suite) + slicer.util.exit(0 if result.wasSuccessful() else 1) +except Exception: + traceback.print_exc() + slicer.util.exit(1) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..211b8f9 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,186 @@ +import csv + +import numpy as np +import pytest +import SimpleITK as sitk +from _collageradiomics.cli import run +from _collageradiomics.io import read_arrays +from _collageradiomics.statistics import summarize +from click.testing import CliRunner + + +@pytest.fixture +def cases(tmp_path): + result = {} + for name, shape in [("two", (7, 9)), ("three", (5, 7, 9)), ("single", (1, 7, 9))]: + array = np.random.default_rng(51).random(shape) + mask = np.ones(shape, dtype=np.uint8) + mask[..., :2] = 0 + paths = [] + for suffix, values in [("image", array), ("mask", mask)]: + path = tmp_path / f"{name}-{suffix}.mha" + sitk.WriteImage(sitk.GetImageFromArray(values), str(path)) + paths.append(path) + result[name] = paths + return result + + +def invoke(arguments): + return CliRunner().invoke(run, [str(arg) for arg in arguments]) + + +def test_sitk_axes_match_height_width_depth(cases): + image, mask = read_arrays(*cases["three"]) + assert image.shape == mask.shape == (7, 9, 5) + original = sitk.GetArrayFromImage(sitk.ReadImage(str(cases["three"][0]))) + assert image[2, 3, 4] == original[4, 2, 3] + + +@pytest.mark.parametrize("name", ["two", "three"]) +@pytest.mark.parametrize("extended", [False, True]) +@pytest.mark.parametrize("explicit_dimensions", [False, True]) +def test_single_and_batch_csv_match(cases, tmp_path, name, extended, explicit_dimensions): + image, mask = cases[name] + single, batch, inputs = (tmp_path / name for name in ["single.csv", "batch.csv", "input.csv"]) + options = ["-s", "1", "-b", "8", "-h", "3"] + if extended: + options.append("-e") + if explicit_dimensions: + options += ["-d", "2" if name == "two" else "3"] + first = invoke(["-i", image, "-m", mask, "-o", single, *options]) + assert first.exit_code == 0, first.output + with inputs.open("w", newline="") as stream: + writer = csv.writer(stream) + writer.writerow(["ID", "Image", "Mask"]) + writer.writerow(["case", image.name, mask.name]) + second = invoke(["-i", inputs, "-o", batch, *options]) + assert second.exit_code == 0, second.output + with single.open() as stream: + expected = {row["FeatureName"]: float(row["Value"]) for row in csv.DictReader(stream)} + with batch.open() as stream: + rows = list(csv.DictReader(stream)) + assert len(rows) == 1 + assert rows[0]["ID"] == "case" + assert rows[0]["haralickwindow"] == "3" + assert {key: float(rows[0][key]) for key in expected} == pytest.approx(expected, abs=1e-8) + assert len(expected) == 13 * (1 if name == "two" else 2) * (6 if extended else 4) + + +def test_explicit_2d_single_slice(cases, tmp_path): + result = invoke( + [ + "-i", + cases["single"][0], + "-m", + cases["single"][1], + "-o", + tmp_path / "out.csv", + "-d", + "2", + "-s", + "1", + "-b", + "8", + ] + ) + assert result.exit_code == 0, result.output + + +@pytest.mark.parametrize("property_name,values", [("Origin", (1.0, 0.0)), ("Spacing", (2.0, 1.0))]) +def test_misaligned_masks_rejected(cases, property_name, values): + image, path = cases["two"] + mask = sitk.ReadImage(str(path)) + getattr(mask, f"Set{property_name}")(values) + sitk.WriteImage(mask, str(path)) + with pytest.raises(ValueError, match=property_name.lower()): + read_arrays(image, path) + + +def test_mask_required(cases, tmp_path): + result = invoke(["-i", cases["two"][0], "-o", tmp_path / "out.csv"]) + assert result.exit_code == 2 + assert "--mask is required" in result.output + + +def test_multi_slice_volume_is_not_silently_truncated(cases): + with pytest.raises(ValueError, match="single-slice"): + read_arrays(*cases["three"], dimensions=2) + + +def test_batch_preserves_good_cases_and_reports_failures(cases, tmp_path): + inputs, output = tmp_path / "input.csv", tmp_path / "out.csv" + with inputs.open("w", newline="") as stream: + writer = csv.writer(stream) + writer.writerow(["ID", "Image", "Mask"]) + writer.writerow(["good", *cases["two"]]) + writer.writerow(["bad", "missing.mha", "missing-mask.mha"]) + result = invoke(["-i", inputs, "-o", output, "-s", "1", "-b", "8"]) + assert result.exit_code == 1 + with output.open() as stream: + assert [row["ID"] for row in csv.DictReader(stream)] == ["good"] + with (tmp_path / "errors_out.csv").open() as stream: + assert [row["ID"] for row in csv.DictReader(stream)] == ["bad"] + + +def test_window_option_changes_results(cases, tmp_path): + paths = [] + for size in [3, 5]: + output = tmp_path / f"window-{size}.csv" + result = invoke( + [ + "-i", + cases["two"][0], + "-m", + cases["two"][1], + "-o", + output, + "-s", + "1", + "-b", + "8", + "-h", + size, + ] + ) + assert result.exit_code == 0, result.output + paths.append(output.read_text()) + assert paths[0] != paths[1] + + +def test_label_selection(cases): + path = cases["two"][1] + mask = sitk.GetArrayFromImage(sitk.ReadImage(str(path))) + mask[3:5, 4:6] = 7 + sitk.WriteImage(sitk.GetImageFromArray(mask), str(path)) + _, selected = read_arrays(*cases["two"], label=7) + assert np.count_nonzero(selected) == 4 + + +def test_constant_and_single_voxel_statistics_are_finite(): + assert summarize(np.array([np.nan, 2.0]), True) == { + "Median": 2, + "IQR": 0, + "Skewness": 0, + "Kurtosis": 0, + "Mean": 2, + "Variance": 0, + } + + +def test_reference_summary_moments(): + result = summarize(np.array([0.0, 1.0, 2.0]), True) + assert result == pytest.approx( + { + "Median": 1, + "IQR": 1, + "Mean": 1, + "Variance": 1, + "Skewness": 0, + "Kurtosis": -1.5, + } + ) + + +def test_nearly_constant_statistics_remain_finite(): + result = summarize(np.array([1.0, 1.0 + 1e-15, 1.0 - 1e-15]), True) + assert np.isfinite(list(result.values())).all() diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..1ca77d5 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,80 @@ +import numpy as np +import pytest +from collageradiomics import Collage, HaralickFeature + + +def test_2d_output_preserves_shape_and_mask(image_and_mask_2d): + image, mask = image_and_mask_2d + image_before, mask_before = image.copy(), mask.copy() + collage = Collage(image, mask, svd_radius=1, num_unique_angles=8) + + output = collage.execute() + + assert output.shape == image.shape + (13,) + assert np.isfinite(output[mask != 0]).all() + assert np.isnan(output[mask == 0]).all() + assert collage.collage_output is output + np.testing.assert_array_equal(image, image_before) + np.testing.assert_array_equal(mask, mask_before) + + +def test_execute_is_repeatable(image_and_mask_2d): + collage = Collage(*image_and_mask_2d, svd_radius=1, num_unique_angles=8) + first = collage.execute().copy() + np.testing.assert_array_equal(collage.execute(), first) + + +@pytest.mark.parametrize("feature", [0, HaralickFeature.Contrast]) +def test_single_2d_feature_matches_output(image_and_mask_2d, feature): + collage = Collage(*image_and_mask_2d, svd_radius=1, num_unique_angles=8) + output = collage.execute() + np.testing.assert_array_equal(collage.get_single_feature_output(feature), output[:, :, feature]) + + +def test_all_nonzero_mask_labels_are_included(image_and_mask_2d): + image, mask = image_and_mask_2d + labels = mask.astype(np.int16) + labels[20:22, 21:23] = 7 + expected = Collage(image, mask, svd_radius=1, num_unique_angles=8).execute() + actual = Collage(image, labels, svd_radius=1, num_unique_angles=8).execute() + np.testing.assert_array_equal(actual, expected) + + +def test_3d_output_has_both_angles(image_and_mask_3d): + image, mask = image_and_mask_3d + collage = Collage(image, mask, svd_radius=1, num_unique_angles=8) + output = collage.execute() + + assert output.shape == image.shape + (13, 2) + assert np.isfinite(output[:, :, 3][mask[:, :, 3] != 0]).all() + assert np.isnan(output[mask == 0]).all() + np.testing.assert_array_equal( + collage.get_single_feature_output(HaralickFeature.Contrast), + output[:, :, :, HaralickFeature.Contrast, :], + ) + + +@pytest.mark.parametrize( + ("options", "message"), + [ + ({"svd_radius": 0}, "SVD radius"), + ({"svd_radius": -1, "haralick_window_size": 3}, "SVD radius"), + ({"haralick_window_size": 0}, "haralick_window_size"), + ({"num_unique_angles": 0}, "num_unique_angles"), + ], +) +def test_invalid_parameters_are_rejected(image_and_mask_2d, options, message): + with pytest.raises(ValueError, match=message): + Collage(*image_and_mask_2d, **options) + + +@pytest.mark.parametrize("shape", [(50,), (50, 52, 3, 2)]) +def test_invalid_image_dimensions_are_rejected(shape): + with pytest.raises(ValueError, match="Expected a 2D or 3D image"): + Collage(np.ones(shape), np.ones(shape)) + + +def test_mismatched_mask_shape_is_rejected(image_and_mask_2d): + image, _ = image_and_mask_2d + with pytest.raises(ValueError, match="Mask must be the same shape"): + Collage(image, np.ones((50, 51))) diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..0989cf3 --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,143 @@ +import os +import shutil +import subprocess +import sys +import tarfile +import zipfile +from email.parser import Parser +from pathlib import Path + +import pydicom +import pytest +from packaging.requirements import Requirement + +REPOSITORY = Path(__file__).resolve().parents[1] + + +def run_command(arguments, cwd, env=None): + result = subprocess.run( + [sys.executable, *arguments], + cwd=cwd, + env=env, + capture_output=True, + text=True, + timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr + return result + + +@pytest.fixture(scope="module") +def distributions(tmp_path_factory): + workspace = tmp_path_factory.mktemp("distribution") + source = workspace / "source" + shutil.copytree( + REPOSITORY / "module", + source, + ignore=shutil.ignore_patterns("build", "dist", "*.egg-info", "__pycache__", "docs"), + ) + output = workspace / "dist" + # The default build creates an sdist, then builds the wheel from that sdist. + # Use another working directory to catch accidental dependencies on the checkout. + run_command( + ["-m", "build", "--no-isolation", "--outdir", str(output), str(source)], + cwd=workspace, + ) + return next(output.glob("*.tar.gz")), next(output.glob("*.whl")) + + +def test_source_distribution_contains_build_inputs_and_sample(distributions): + source, _ = distributions + with tarfile.open(source) as archive: + names = { + Path(name).relative_to(Path(source.name.removesuffix(".tar.gz"))) + for name in archive.getnames() + } + assert { + Path(name) + for name in [ + "VERSION", + "requirements.txt", + "README.md", + "LICENSE", + "pyproject.toml", + "setup.py", + "test.dcm", + "test_script.py", + ] + } <= names + + +def test_packaged_sample_is_a_readable_dicom(distributions): + source, _ = distributions + with tarfile.open(source) as archive: + sample = next( + member for member in archive.getmembers() if member.name.endswith("/test.dcm") + ) + with archive.extractfile(sample) as stream: + dataset = pydicom.dcmread(stream) + assert dataset.pixel_array.shape == (dataset.Rows, dataset.Columns) + + +def test_wheel_metadata_matches_declared_version_and_requirements(distributions): + _, wheel = distributions + with zipfile.ZipFile(wheel) as archive: + metadata_name = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + metadata = Parser().parsestr(archive.read(metadata_name).decode("utf-8")) + + assert metadata["Name"] == "collageradiomics" + assert metadata["Version"] == (REPOSITORY / "module" / "VERSION").read_text().strip() + expected = { + str(Requirement(line)) + for line in (REPOSITORY / "module" / "requirements.txt").read_text().splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + runtime = { + str(Requirement(line)) + for line in metadata.get_all("Requires-Dist") + if Requirement(line).marker is None + } + assert runtime == expected + + +def test_built_wheel_installs_and_runs_outside_checkout(distributions, tmp_path): + _, wheel = distributions + destination = tmp_path / "installed" + run_command( + [ + "-m", + "pip", + "install", + "--no-deps", + "--no-index", + "--target", + str(destination), + str(wheel), + ], + cwd=tmp_path, + ) + env = os.environ.copy() + env["PYTHONPATH"] = str(destination) + run_command( + [ + "-c", + """ +from pathlib import Path +import numpy as np +import collageradiomics + +assert Path(collageradiomics.__file__).resolve().is_relative_to(Path('installed').resolve()) +image = np.ones((50, 50)) +mask = np.zeros(image.shape, dtype=np.uint8) +mask[20:26, 20:26] = 1 +output = collageradiomics.Collage(image, mask, svd_radius=1).execute() +assert output.shape == (50, 50, 13) +assert np.isfinite(output[mask != 0]).all() +assert np.isnan(output[mask == 0]).all() +""", + ], + cwd=tmp_path, + env=env, + ) diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..8aa1154 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,47 @@ +import importlib.util +from pathlib import Path + +import numpy as np +import pydicom + +REPOSITORY = Path(__file__).resolve().parents[1] + + +def load_example(path): + specification = importlib.util.spec_from_file_location("collage_example", REPOSITORY / path) + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + +def test_dicom_example_writes_valid_derived_instances(tmp_path): + example = load_example("module/test_script.py") + files = example.run_example(REPOSITORY / "module/test.dcm", tmp_path) + assert len(files) == 13 + identifiers = set() + for path in files: + dataset = pydicom.dcmread(path) + pixels = dataset.pixel_array + assert pixels.shape == (dataset.Rows, dataset.Columns) + assert dataset.PixelPaddingValue == 0 + assert dataset.SOPInstanceUID == dataset.file_meta.MediaStorageSOPInstanceUID + assert np.isfinite( + pixels.astype(float) * dataset.RescaleSlope + dataset.RescaleIntercept + ).all() + identifiers.add(dataset.SOPInstanceUID) + assert len(identifiers) == 13 + maps = np.load(tmp_path / "collage_features.npy") + assert maps.shape[-1] == 13 + assert np.isfinite(maps).any() and np.isnan(maps).any() + + +def test_isbi_example_covers_every_window_feature_and_angle(): + example = load_example("examples/isbi.py") + image = np.random.default_rng(65).random((7, 9, 3)) + mask = np.ones(image.shape, dtype=bool) + result = example.compute_collage( + image, mask, haralick_windows=[3, 5], svd_radius=1, num_unique_angles=8 + ) + assert result.shape == (2 * 13 * 2, 4) + assert np.isfinite(result).all() + assert np.any(result != 0, axis=1).all() diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 0000000..38c5375 --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,40 @@ +import numpy as np +import pytest +from collageradiomics import Collage + + +@pytest.mark.parametrize("shape", [(1, 1), (2, 3), (7, 9), (5, 6, 1), (5, 6, 2), (7, 9, 4)]) +def test_full_masks_and_small_images(shape): + image = np.ones(shape, dtype=np.uint16) + mask = np.ones(shape, dtype=bool) + output = Collage(image, mask, svd_radius=1, num_unique_angles=8).execute() + assert output.shape == shape + ((13,) if len(shape) == 2 else (13, 2)) + assert np.isfinite(output).all() + + +@pytest.mark.parametrize("corner", [(0, 0), (0, -1), (-1, 0), (-1, -1)]) +def test_single_voxel_corner_masks(corner): + image = np.random.default_rng(44).random((7, 9)) + mask = np.zeros_like(image, dtype=bool) + mask[corner] = True + output = Collage(image, mask, svd_radius=2, num_unique_angles=8).execute() + assert np.isfinite(output[mask]).all() + assert np.isnan(output[~mask]).all() + + +@pytest.mark.parametrize("slices", [slice(0, 1), slice(0, 2), slice(-2, None), slice(-1, None)]) +def test_3d_masks_at_volume_ends(slices): + image = np.random.default_rng(45).random((7, 9, 5)) + mask = np.zeros_like(image, dtype=bool) + mask[2:5, 3:6, slices] = True + output = Collage(image, mask, svd_radius=1, num_unique_angles=8).execute() + assert np.isfinite(output[mask]).all() + assert np.isnan(output[~mask]).all() + + +def test_strided_input_matches_contiguous_copy(): + image = np.random.default_rng(46).random((12, 14))[::2, ::2] + mask = np.ones(image.shape, dtype=bool) + first = Collage(image, mask, svd_radius=1, num_unique_angles=8).execute() + second = Collage(image.copy(), mask, svd_radius=1, num_unique_angles=8).execute() + np.testing.assert_array_equal(first, second) diff --git a/tests/test_numerics.py b/tests/test_numerics.py new file mode 100644 index 0000000..06c4937 --- /dev/null +++ b/tests/test_numerics.py @@ -0,0 +1,46 @@ +import math + +import numpy as np +import pytest +from _collageradiomics.texture import maximal_correlation, named_haralick +from collageradiomics import _svd_dominant_angles + + +def test_binary_checkerboard_reference_features(): + # P(0,1)=P(1,0)=1/2; the difference is always one and X determines Y. + matrix = np.array([[0, 1], [1, 0]], dtype=np.uint32) + expected = [0.5, 1, -1, 0.25, 1, 0, 0, 1, 0, 0, -1, math.sqrt(1 - math.exp(-2)), 1] + np.testing.assert_allclose(named_haralick(matrix, True), expected, atol=1e-12) + assert named_haralick(matrix, False)[8] == pytest.approx(0.25) + + +def test_independent_binary_reference_features(): + # A uniform 2x2 joint distribution has two bits of joint entropy and no dependence. + matrix = np.ones((2, 2), dtype=np.uint32) + expected = [0.25, 0.5, 0, 0.25, 1, 0.5, 1.5, 2, 0.25, 1, 0, 0, 0] + np.testing.assert_allclose(named_haralick(matrix, True), expected, atol=1e-12) + + +def test_constant_reference_features(): + expected = np.zeros(13) + expected[0] = expected[2] = 1 + np.testing.assert_allclose(named_haralick(np.ones((1, 1), dtype=np.uint32), True), expected) + + +def test_maximal_correlation_handles_empty_and_nonsymmetric_marginals(): + matrix = np.array([[1, 1, 0], [0, 0, 2], [0, 0, 0]], dtype=np.uint32) + assert maximal_correlation(matrix) == pytest.approx(1) + independent = np.outer([1, 2, 0], [2, 3, 4]).astype(np.uint32) + assert maximal_correlation(independent) == pytest.approx(0, abs=1e-12) + + +@pytest.mark.parametrize( + "gradient", [(0, 0, 0), (0, 0, 1), (1, 0, 0), (0, 1, 0), (1, 1, 0), (1, 2, 3)] +) +@pytest.mark.parametrize("sign", [1, -1]) +def test_principal_gradient_orientation(gradient, sign): + gx, gy, gz = gradient + fields = [np.full((5, 5, 3), sign * g, dtype=float) for g in gradient] + angles = _svd_dominant_angles(*fields, 1, is_3d=True) + np.testing.assert_allclose(angles[..., 0], math.atan2(gy, gx), atol=1e-6) + np.testing.assert_allclose(angles[..., 1], math.atan2(gz, math.hypot(gx, gy)), atol=1e-6) diff --git a/tests/test_progress.py b/tests/test_progress.py new file mode 100644 index 0000000..03708a3 --- /dev/null +++ b/tests/test_progress.py @@ -0,0 +1,39 @@ +import logging + +import pytest +from collageradiomics import Collage + + +def test_progress_reports_ordered_stages(image_and_mask_3d, caplog): + events = [] + with caplog.at_level(logging.INFO, logger="collageradiomics"): + Collage( + *image_and_mask_3d, + svd_radius=1, + num_unique_angles=8, + progress_callback=lambda *event: events.append(event), + ).execute() + stages = list(dict.fromkeys(stage for stage, _, _ in events)) + assert stages == ["orientation", "haralick-theta", "haralick-phi"] + for stage in stages: + updates = [(done, total) for name, done, total in events if name == stage] + assert updates[0][0] == 0 + assert updates[-1][0] == updates[-1][1] + assert [done for done, _ in updates] == sorted(done for done, _ in updates) + assert "100%" in caplog.text + + +def test_callback_can_cancel_without_retaining_stale_output(image_and_mask_2d): + cancelled = False + + def cancel(stage, done, total): + if cancelled: + raise RuntimeError("cancelled") + + collage = Collage(*image_and_mask_2d, svd_radius=1, progress_callback=cancel) + collage.execute() + cancelled = True + with pytest.raises(RuntimeError, match="cancelled"): + collage.execute() + with pytest.raises(RuntimeError, match="execute"): + collage.get_single_feature_output(0) diff --git a/tests/test_regressions.py b/tests/test_regressions.py new file mode 100644 index 0000000..23d1676 --- /dev/null +++ b/tests/test_regressions.py @@ -0,0 +1,70 @@ +import numpy as np +import pytest +from collageradiomics import Collage, DifferenceVarianceInterpretation, HaralickFeature + + +@pytest.mark.parametrize("edge", ["left", "right", "top", "bottom"]) +def test_masks_touching_image_edges(image_and_mask_2d, edge): + image, mask = image_and_mask_2d + mask[:] = 0 + regions = { + "left": (slice(20, 26), slice(0, 6)), + "right": (slice(20, 26), slice(-6, None)), + "top": (slice(0, 6), slice(21, 27)), + "bottom": (slice(-6, None), slice(21, 27)), + } + mask[regions[edge]] = 1 + output = Collage(image, mask, svd_radius=2, num_unique_angles=8).execute() + + assert output.shape == image.shape + (13,) + assert np.isfinite(output[mask != 0]).all() + assert np.isnan(output[mask == 0]).all() + + +@pytest.mark.parametrize("depth", [2, 3]) +def test_every_masked_3d_slice_is_calculated(image_and_mask_3d, depth): + image, mask = image_and_mask_3d + mask[:] = 0 + mask[20:26, 21:27, 2 : 2 + depth] = 1 + output = Collage(image, mask, svd_radius=1, num_unique_angles=8).execute() + assert np.isfinite(output[mask != 0]).all() + + +@pytest.fixture +def checkerboard_features(image_and_mask_2d): + # Horizontal pairs alternate 0->1 and 1->0 equally, so P(0,1)=P(1,0)=1/2. + checkerboard = (np.indices((8, 8)).sum(axis=0) % 2).astype(np.uint8) + + def calculate(**options): + collage = Collage( + *image_and_mask_2d, + svd_radius=1, + haralick_window_size=3, + num_unique_angles=2, + cooccurence_angles=[0], + **options, + ) + return collage._calculate_haralick_feature_values(checkerboard, 3, 3) + + return calculate + + +def test_checkerboard_has_known_energy_contrast_and_correlation(checkerboard_features): + features = checkerboard_features() + assert features[HaralickFeature.AngularSecondMoment] == pytest.approx(0.5) + assert features[HaralickFeature.Contrast] == pytest.approx(1.0) + assert features[HaralickFeature.Correlation] == pytest.approx(-1.0) + + +def test_checkerboard_joint_entropy_is_one_bit(checkerboard_features): + assert checkerboard_features()[HaralickFeature.Entropy] == pytest.approx(1.0) + + +def test_difference_variance_interpretations_produce_different_results(checkerboard_features): + x_minus_y = checkerboard_features( + difference_variance_interpretation=DifferenceVarianceInterpretation.XMinusYVariance, + ) + probability = checkerboard_features( + difference_variance_interpretation=DifferenceVarianceInterpretation.ProbabilityXMinusYVariance, + ) + assert not np.allclose(x_minus_y, probability) diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..4e735c4 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,65 @@ +import numpy as np +import pytest +from collageradiomics import Collage + + +@pytest.mark.parametrize("name", ["image", "mask"]) +def test_arrays_required(image_and_mask_2d, name): + image, mask = image_and_mask_2d + with pytest.raises(TypeError, match=f"{name} must be a NumPy array"): + Collage( + image.tolist() if name == "image" else image, mask.tolist() if name == "mask" else mask + ) + + +@pytest.mark.parametrize("bad_value", [np.nan, np.inf, -np.inf]) +@pytest.mark.parametrize("name", ["image", "mask"]) +def test_nonfinite_arrays_rejected(image_and_mask_2d, name, bad_value): + image, mask = (a.astype(float) for a in image_and_mask_2d) + (image if name == "image" else mask)[0, 0] = bad_value + with pytest.raises(ValueError, match=f"{name} must contain only finite"): + Collage(image, mask) + + +@pytest.mark.parametrize("dtype", [object, complex, str]) +def test_nonreal_image_rejected(image_and_mask_2d, dtype): + image, mask = image_and_mask_2d + with pytest.raises(TypeError, match="image must contain real numeric"): + Collage(image.astype(dtype), mask) + + +def test_empty_mask_has_clear_error(image_and_mask_2d): + image, mask = image_and_mask_2d + with pytest.raises(ValueError, match="mask must contain at least one nonzero"): + Collage(image, np.zeros_like(mask)) + + +@pytest.mark.parametrize("name", ["svd_radius", "haralick_window_size", "num_unique_angles"]) +@pytest.mark.parametrize("value", [True, 1.5, "3"]) +def test_integer_parameters_required(image_and_mask_2d, name, value): + with pytest.raises(TypeError, match="must be an integer"): + Collage(*image_and_mask_2d, **{name: value}) + + +def test_even_window_rejected(image_and_mask_2d): + with pytest.raises(ValueError, match="must be odd"): + Collage(*image_and_mask_2d, haralick_window_size=4) + + +@pytest.mark.parametrize("values", [[], [np.nan], [[0]], [np.inf]]) +def test_invalid_angles_rejected(image_and_mask_2d, values): + with pytest.raises(ValueError, match="cooccurence_angles"): + Collage(*image_and_mask_2d, cooccurence_angles=values) + + +def test_features_require_execute(image_and_mask_2d): + with pytest.raises(RuntimeError, match=r"execute\(\)"): + Collage(*image_and_mask_2d).get_single_feature_output(0) + + +@pytest.mark.parametrize("feature", [-1, 13]) +def test_feature_index_range(image_and_mask_2d, feature): + collage = Collage(*image_and_mask_2d, svd_radius=1, num_unique_angles=8) + collage.execute() + with pytest.raises(ValueError, match="which_feature"): + collage.get_single_feature_output(feature)