diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21b53808..0eb65c88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,25 +8,29 @@ on: workflow_dispatch: jobs: + lint: + name: "Black linting" + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.12" + + - name: Install the project + run: uv sync --extra dl + + - name: Run black check + run: uv run black --check . + + - name: Run ruff check + run: uv run ruff check src/valis tests examples + tests: - name: "Python ${{ matrix.python-version }}" + name: "Python 3.13" runs-on: "ubuntu-latest" - env: - USING_COVERAGE: '3.9, 3.10, 3.11, 3.12, 3.13' - - strategy: - fail-fast: false - matrix: - python-version: - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - os: - - "Ubuntu" - - "Windows" - - "macOS" steps: - uses: actions/checkout@v4 @@ -34,7 +38,7 @@ jobs: - name: Install the latest version of uv and set the python version uses: astral-sh/setup-uv@v5 with: - python-version: ${{ matrix.python-version }} + python-version: "3.13" - name: Install libvips run: | @@ -42,7 +46,7 @@ jobs: sudo apt-get install --no-install-recommends libvips - name: Install the project - run: uv sync + run: uv sync --extra dl - name: Run tests run: uv run pytest diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml new file mode 100644 index 00000000..41fc4bab --- /dev/null +++ b/.github/workflows/containers.yml @@ -0,0 +1,31 @@ +name: Container Builds +on: + push: + branches: + - 'main' +jobs: + build-docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push container + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + push: true + tags: | + jeffquinnmsk/valis:latest + jeffquinnmsk/valis:${{ github.sha }} + cache-from: type=registry,ref=jeffquinnmsk/valis:latest + cache-to: type=inline diff --git a/.github/workflows/doc_checks.yml b/.github/workflows/doc_checks.yml deleted file mode 100644 index 530e8278..00000000 --- a/.github/workflows/doc_checks.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Pull Request Docs Check - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - # Standard drop-in approach that should work for most people. - - uses: ammaraskar/sphinx-action@master - with: - docs-folder: "docs" diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index b544450a..00000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - release: - types: [published] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index bdd756d3..5ab9ec8a 100644 --- a/.gitignore +++ b/.gitignore @@ -238,3 +238,6 @@ tests/test_tiler.py tests/test_warp_assoc_imgs.py tests/test_warp_fxns.py + +tests/test_output/ +tests/example_datasets/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f50e0670 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: sort-simple-yaml + - id: check-added-large-files + - id: check-merge-conflict + - repo: https://github.com/psf/black + rev: 24.1.1 + hooks: + - id: black + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.11 + - repo: https://github.com/conorfalvey/check_pdb_hook + rev: 0.0.9 + hooks: + - id: check_pdb_hook diff --git a/Dockerfile b/Dockerfile index 99a9e99a..a5f27320 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,114 +1,28 @@ -FROM ubuntu:noble AS builder +FROM python:3.13-slim -ARG WKDIR=/usr/local/src -WORKDIR ${WKDIR} +USER root -ARG UV_VERSION=0.6.5 -ARG VIPS_VERSION=8.16.0 -ARG BF_VERSION=7.0.0 -ARG PYTORCH_VERSION=2.4.0 -ARG TORCHVISION_VERSION=0.20.1 -ARG OPENCV_VERSION=4.9.0.80 +RUN --mount=type=cache,target=/.cache/pip pip install --upgrade pip +RUN --mount=type=cache,target=/.cache/pip pip install ipython ipdb memray +RUN apt-get update && apt-get install --no-install-recommends -y \ + libvips-tools \ + libvips \ + libvips-dev \ + build-essential -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -# Get build dependencies -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y \ - build-essential \ - software-properties-common \ - ninja-build \ - python3-pip \ - bc \ - wget \ - ca-certificates \ - git-all \ - cmake \ - libjxr-dev \ - openjdk-11-jre - -# libvips dependencies for libvips build -RUN apt-get install -y python3-venv -RUN python3 -m venv ~/.local -RUN echo $PATH -ENV PATH="$PATH:~/.local/bin" -RUN echo $PATH -RUN ~/.local/bin/pip3 install meson - -RUN apt-get update -RUN apt-get install --no-install-recommends -y \ - libglib2.0-dev \ - glib-2.0-dev \ - libexpat1-dev \ - libexpat-dev \ - librsvg2-2 \ - librsvg2-common \ - librsvg2-dev \ - libpng-dev \ - libjpeg-turbo8-dev \ - libopenjp2-7-dev \ - libtiff-dev \ - libexif-dev \ - liblcms2-dev \ - libheif-dev \ - liborc-dev \ - libgirepository1.0-dev \ - libopenslide-dev \ - librsvg2-dev - -RUN update-ca-certificates -### Install libvips from source to get latest version -ENV LD_LIBRARY_PATH=/usr/local/lib -ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig +RUN mkdir -p /app +COPY src/ /app/src/ -# build the head of the stable 8.14 branch -ARG VIPS_URL=https://github.com/libvips/libvips/releases/download -RUN wget ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz --no-check-certificate \ - && tar xf vips-${VIPS_VERSION}.tar.xz \ - && cd vips-${VIPS_VERSION} \ - && ~/.local/bin/meson build --buildtype=release --libdir=lib \ - && cd build \ - && ninja \ - && ninja install +COPY pyproject.toml setup.py LICENSE.txt README.rst /app/ -RUN rm vips-${VIPS_VERSION}.tar.xz -RUN rm -r vips-${VIPS_VERSION} +RUN --mount=type=cache,target=/.cache/pip cd /app && pip install '.[dev,test]' -# Copy over necessary files -COPY valis valis -COPY pyproject.toml pyproject.toml -COPY README.rst README.rst -COPY LICENSE.txt LICENSE.txt -COPY CITATION.cff CITATION.cff - -# Install python packages using UV -ADD https://astral.sh/uv/install.sh /uv-installer.sh - -# Run the installer then remove it -RUN sh /uv-installer.sh && rm /uv-installer.sh - -# Ensure the installed binary is on the `PATH` -ENV PATH="/root/.local/bin/:$PATH" - -# RUN uv sync -RUN uv pip install . -# Set path to use .venv Python -ENV PATH="${WKDIR}/.venv/bin:$PATH" +ENV PYTHONUNBUFFERED=1 -# Install bioformats.jar in valis -RUN wget https://downloads.openmicroscopy.org/bio-formats/${BF_VERSION}/artifacts/bioformats_package.jar -P valis +WORKDIR /app # Download pytorch model weights COPY ./docker/docker_download_weights.py docker_download_weights.py RUN python3 docker_download_weights.py -# Clean up -RUN apt-get remove -y wget build-essential ninja-build && \ - apt-get autoremove -y && \ - apt-get autoclean && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - rm -rf /usr/local/lib/python* diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 00000000..8c955209 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,458 @@ +# VALIS Code Improvement Proposals + +This document captures code quality, usability, and architectural issues found in the codebase, along with concrete suggestions for addressing them. Issues are grouped by theme and roughly ordered by impact. + +--- + +## 1. God Class: `registration.py` + +`registration.py` is 6,278 lines and contains two deeply coupled classes (`Valis` and `Slide`) that each do far too much. `Valis` alone has 52 methods and ~80+ instance attributes. + +**Problems:** +- Impossible to understand the registration pipeline at a glance +- Tightly coupled I/O, preprocessing, rigid registration, non-rigid registration, cropping, and saving +- Cannot test any sub-step without running the whole pipeline +- Any change risks breaking unrelated behavior + +**Suggestion:** Split into focused components along the natural pipeline stages: + +``` +registration/ +├── pipeline.py # Valis orchestrator (thin, delegates to others) +├── slide.py # Slide metadata + transformation state +├── crop.py # All cropping logic extracted here +├── warp.py # Warp/save orchestration +└── state.py # Serialization / resume-from-disk logic +``` + +Each component can be tested and understood independently. The `Valis` class becomes a thin orchestrator that composes them. + +--- + +## 2. Missing Type Hints + +600+ functions have no type annotations, including the entire public API. This makes the package hard to use without reading source code. + +**Before:** +```python +def warp_img(self, img, transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, interp_method="bicubic", + bbox_xywh=None, out_shape_rc=None, ...): +``` + +**After:** +```python +def warp_img( + self, + img: np.ndarray, + transformation_src_shape_rc: tuple[int, int] | None = None, + transformation_dst_shape_rc: tuple[int, int] | None = None, + interp_method: str = "bicubic", + bbox_xywh: tuple[int, int, int, int] | None = None, + out_shape_rc: tuple[int, int] | None = None, +) -> np.ndarray: +``` + +At minimum, annotate all public API methods. Adding `py.typed` to the package and running `mypy` in CI would catch regressions. Start with `registration.py` since that's what users touch directly. + +--- + +## 3. 27-Parameter Constructor + +`Valis.__init__` accepts 27 parameters, many of which are interdependent. There's no validation that incompatible options weren't combined. + +**Suggestion:** Use a configuration dataclass to group related options: + +```python +@dataclass +class RegistrationConfig: + feature_detector: FeatureDetectorBase = field(default_factory=OrbFD) + feature_matcher: MatcherBase = field(default_factory=VggMatcher) + non_rigid_registrar_cls: type[NonRigidRegistrar] = OpticalFlowWarper + micro_rigid: bool = False + max_image_dim_px: int = 850 + max_processed_image_dim_px: int = 2000 + crop: Literal["overlap", "reference", "all"] = "overlap" + compose_non_rigid: bool = False + +registrar = Valis(src_dir, dst_dir, config=RegistrationConfig()) +``` + +This groups related settings, enables documentation per-config, and makes it easy to create pre-built configs for common cases (e.g., `RegistrationConfig.for_ihc()`, `RegistrationConfig.for_cycif()`). + +--- + +## 4. String Constants — Use Enums + +Crop mode and other string constants are used throughout the codebase with no validation. A typo silently does the wrong thing. + +**Before:** +```python +registrar.warp_and_save_slides(crop="overlap") # or "reference", or "all"? +``` + +**After:** +```python +from enum import StrEnum + +class CropMode(StrEnum): + OVERLAP = "overlap" + REFERENCE = "reference" + NONE = "all" + +registrar.warp_and_save_slides(crop=CropMode.OVERLAP) +``` + +`StrEnum` (Python 3.11+) or `str, Enum` base class maintains backward compatibility with code passing string literals. + +--- + +## 5. Inconsistent Array/Coordinate Conventions + +The codebase mixes three coordinate conventions without a clear boundary: + +- `rc` — (row, col), i.e. (height, width), numpy convention +- `wh` — (width, height), image dimension convention +- `xy` — (x, y), display/point convention + +The conversion `shape_rc[::-1]` appears 20+ times. Off-by-one errors between conventions are the most common source of subtle bugs. + +**Suggestion:** Define thin wrapper types or at minimum a module-level conversion function, and document which convention each function uses in its signature name or docstring. + +```python +# Explicit conversion utilities, not ad-hoc slicing +def rc_to_wh(shape_rc: tuple[int, int]) -> tuple[int, int]: + return shape_rc[1], shape_rc[0] + +def wh_to_rc(wh: tuple[int, int]) -> tuple[int, int]: + return wh[1], wh[0] +``` + +Longer term, consider `NamedTuple` or dataclass types (`ShapeRC`, `SizeWH`) to make convention violations a type error. + +--- + +## 6. Displacement Field State Management + +`Slide` uses a convoluted three-way state for displacement fields: in-memory numpy array, pyvips image, or lazy-loaded from disk — controlled by a `stored_dxdy` flag and private attributes `_bk_dxdy_f`, `_bk_dxdy_np`. The property getter conditionally loads from any of these sources. + +This pattern: +- Is easy to corrupt (can be in inconsistent state) +- Makes memory usage unpredictable +- Requires readers to understand all three paths + +**Suggestion:** Introduce a `DisplacementField` class that owns the backing storage decision: + +```python +class DisplacementField: + """Owns a displacement field, transparently backed by memory or disk.""" + def __init__(self, array: np.ndarray | None = None, path: Path | None = None): ... + + def as_numpy(self) -> np.ndarray: ... + def as_vips(self) -> pyvips.Image: ... + def save(self, path: Path) -> None: ... + def load(self, path: Path) -> None: ... +``` + +`Slide` then holds a single `DisplacementField` instance and delegates all storage decisions to it. + +--- + +## 7. Silent Exception Handling + +Throughout the code (especially `feature_detectors.py`, `feature_matcher.py`), broad `except Exception` blocks log a warning and continue. This hides failures completely. + +**Before:** +```python +try: + keypoints, descriptors = detector.detect_and_compute(img) +except Exception as e: + logger.warning(e) + keypoints, descriptors = [], None +``` + +**After:** +```python +try: + keypoints, descriptors = detector.detect_and_compute(img) +except DetectionError as e: + logger.warning("Feature detection failed for %s: %s", self.name, e) + raise # or return a typed failure result +``` + +At minimum, narrow the exception types. For features that can legitimately produce no matches, use a typed result rather than None-or-empty: + +```python +@dataclass +class DetectionResult: + keypoints: list + descriptors: np.ndarray | None + failed: bool = False + reason: str = "" +``` + +--- + +## 8. No Unit Tests + +There are exactly 4 test functions, all of which are integration tests requiring external datasets that are not in the repository. There are no unit tests, no mocks, no fixtures, and no edge case coverage. + +**Highest-value areas to add tests:** + +| Function/Class | What to test | +|---|---| +| `warp_tools` coordinate transforms | Roundtrip accuracy, edge cases | +| Crop logic (`get_crop_xywh`, etc.) | Off-by-one, overlap calculation | +| `slide_io` format detection | Common extensions, unknown formats | +| Feature matcher filtering | RANSAC with degenerate inputs | +| `Valis.register()` with 1 image | Should error cleanly | +| Array convention conversions | rc/wh/xy roundtrips | + +A small synthetic test image (black with known features) can drive most unit tests without requiring real WSI data. Add `pytest-cov` to CI to track coverage. + +--- + +## 9. Segfault on Import Order + +The README contains this warning: + +> "Python will segfault if this project is not imported first before any other pytorch-related import" + +This is a critical usability issue. It should be addressed at the package level rather than documented as a warning. Options: + +1. **Lazy-import torch** inside the functions/classes that need it, rather than at module level +2. **Use `importlib.import_module`** with explicit ordering if early binding is required +3. **Isolate SuperGlue/SuperPoint** into a subpackage with its own import guard and document that subpackage as optional +4. **Add a guard in `__init__.py`** that detects conflicting imports and raises a clear error with a fix message + +Leaving a segfault as a known issue makes the package unsuitable for use inside larger applications. + +--- + +## 10. Duplicate / Overlapping Crop Methods + +There are 5+ crop-related methods split across `Slide` and `Valis` with overlapping responsibilities: + +- `Slide.get_crop_xywh()` +- `Slide.get_overlap_crop_xywh()` +- `Slide.get_aligned_to_ref_slide_crop_xywh()` +- `Valis.get_crop_xywh()` +- `Valis.get_overlap_indices()` + +Each handles edge cases slightly differently. It's not obvious which to call for a given task. + +**Suggestion:** Consolidate into a single `CropCalculator` or make `Valis` the single entry point for crop logic and deprecate the `Slide`-level methods. Document in the class docstring which cropping scenario each method covers. + +--- + +## 11. Warp Method Proliferation + +There are 7+ warp methods with subtly different semantics: + +- `warp_img()` +- `warp_img_from_to()` +- `warp_xy()` +- `warp_points()` +- `warp_geojson()` +- `warp_annotations()` +- `warp_and_save_slides()` + +For new users, it's not clear which to use. For existing users, each has slightly different cropping and interpolation behavior. + +**Suggestion:** Document a decision tree in the class-level docstring: + +``` +Use warp_img() — to warp a numpy array +Use warp_img_from_to() — to warp between two specific slides (not just to reference) +Use warp_xy() — to transform point coordinates +Use warp_geojson() — to transform GeoJSON annotations +Use warp_and_save_slides() — to warp all registered slides and write to disk +``` + +Also consider a unified `warp(target)` method that dispatches based on type. + +--- + +## 12. Example Coverage + +There is exactly one example (`align_two_images.py`), which is a 378-line specialized CLI tool. It doesn't serve as an introduction to the library. + +**Missing examples:** + +- **Minimal working example** — register a directory of images, save as OME-TIFF, 20 lines +- **Multi-round CyCIF** — register across fluorescence rounds +- **Non-rigid registration** — when to use it, how to configure it +- **Resume from saved state** — how to re-use a registration without recomputing +- **Extract transformation matrices** — for users who want to apply transforms in another tool +- **Error handling** — what to do when `register()` returns failures + +--- + +## 13. No Validation on Construction + +`Valis.__init__` accepts `src_dir` and `dst_dir` but doesn't validate them until `register()` is called. If `src_dir` doesn't exist or contains no supported images, the error surfaces far later than it should. + +**Suggestion:** Validate at construction time and fail fast: + +```python +def __init__(self, src_dir: str | Path, dst_dir: str | Path, ...): + src_dir = Path(src_dir) + if not src_dir.exists(): + raise FileNotFoundError(f"src_dir does not exist: {src_dir}") + images = self._find_images(src_dir) + if len(images) == 0: + raise ValueError(f"No supported images found in {src_dir}") + if len(images) < 2: + raise ValueError(f"At least 2 images required for registration, found {len(images)}") +``` + +--- + +## 14. Package Size and Optional Dependencies + +The package requires torch 2.7+ (~2GB) as a hard dependency, even though torch is only needed for SuperGlue/SuperPoint-based feature detection. Users who want to use ORB or SIFT don't need torch at all. + +**Suggestion:** Make deep learning dependencies optional: + +```toml +[project.optional-dependencies] +dl = ["torch>=2.7.1", "torchvision", "kornia", "einops"] +full = ["valis[dl]", ...] +``` + +```python +# In feature_detectors.py +try: + import torch + _TORCH_AVAILABLE = True +except ImportError: + _TORCH_AVAILABLE = False + +class SuperPointFD(FeatureDetectorBase): + def __init__(self): + if not _TORCH_AVAILABLE: + raise ImportError("SuperPoint requires torch. Install with: pip install valis[dl]") +``` + +--- + +## Summary Table + +| # | Issue | Severity | Effort | Status | +|---|-------|----------|--------|--------| +| 1 | God class `registration.py` | Critical | High | **done** — split into package | +| 2 | No type hints | High | Medium | **partial** — public API annotated | +| 3 | 27-parameter constructor | High | Medium | **done** — `RegistrationConfig` dataclass | +| 4 | String constants, no enums | Medium | Low | **done** | +| 5 | Mixed coordinate conventions | High | Medium | **partial** — `rc_to_wh`/`wh_to_rc` added | +| 6 | Displacement field state | High | Medium | **done** — `DisplacementField` class added | +| 7 | Silent exception handling | Medium | Low | **done** — bare `except:` → `except Exception:` | +| 8 | No unit tests | High | High | **partial** — 26 unit tests | +| 9 | Import-order segfault | High | Medium | **done** | +| 10 | Duplicate crop methods | Medium | Low | **partial** — decision tree in Valis docstring | +| 11 | Warp method proliferation | Medium | Low | **done** — decision tree in Valis docstring | +| 12 | Example coverage | Medium | Low | **done** — 4 example scripts added | +| 13 | No construction validation | Medium | Low | **done** | +| 14 | Optional deep learning deps | Medium | Medium | **done** — torch/kornia moved to `[dl]` extras | + +## Completed work + +### Issue 4 — CropMode enum (`registration.py`) +Added `CropMode(StrEnum)` with members `OVERLAP`, `REFERENCE`, and `NONE`. The old +`CROP_OVERLAP`/`CROP_REF`/`CROP_NONE` module constants now point to enum members, so +all existing code is backward compatible. String literals (e.g. `"overlap"`) continue +to work wherever a `CropMode` is accepted. + +### Issue 13 — Construction validation (`registration.py`) +`Valis.__init__` now validates `src_dir` eagerly (when `img_list` is `None`) and raises: +- `FileNotFoundError` if the directory does not exist +- `NotADirectoryError` if the path is a file +- `ValueError` if no supported images are found +- `ValueError` if fewer than 2 images are found + +### Issue 9 — Import-order segfault guard (`__init__.py`) +`valis/__init__.py` inspects `sys.modules` on import. If any of `torch`, `torchvision`, +`kornia`, `einops`, or `timm` are already loaded, it raises a descriptive `ImportError` +with a clear fix message instead of silently segfaulting. + +### Issues 2 & 5 — Type hints and coordinate utilities +- Added `from __future__ import annotations` and `Optional`/`Union` imports to + `registration.py`. +- Annotated the primary public API: `load_registrar()`, `Slide.warp_img()`, + `Slide.warp_xy()`, `Slide.warp_geojson()`, `Valis.__init__()`, `Valis.register()`, + `Valis.warp_and_save_slides()`. +- Added `rc_to_wh(shape_rc)` and `wh_to_rc(wh)` conversion utilities to + `warp_tools.py` to replace ad-hoc `[::-1]` slicing. + +### Issue 8 — Unit tests (`tests/test_unit.py`) +Now 26 unit tests (no external datasets required) covering: +- `rc_to_wh` / `wh_to_rc` roundtrips and edge cases +- `CropMode` enum values and backward-compat constants +- `Valis` construction validation (bad path, empty dir, single image) +- `get_alignment_indices` edge cases +- `RegistrationConfig` defaults, presets, application to `Valis`, kwarg override +- `DisplacementField` empty state, array storage, disk path, pyvips rejection + +### Issues 7, 10, 11 — Exception handling and docstrings +- `feature_detectors.py`: bare `except:` narrowed to `except Exception:` +- Added a "Choosing a warp method" and "Choosing a crop mode" decision table + to the `Valis` class docstring. + +### Issue 3 — RegistrationConfig dataclass (`registration.py`) +A `RegistrationConfig` dataclass groups all registration-flow parameters with +typed, documented fields and factory defaults. Pass it via `Valis(..., config=cfg)`. +Explicit keyword arguments always win over config values (backward compatible). +Includes two convenience presets: +- `RegistrationConfig.for_ihc()` — larger window, rigid-only, reference crop +- `RegistrationConfig.for_cycif()` — non-rigid enabled, overlap crop + +### Issue 6 — DisplacementField class (`registration.py`) +`DisplacementField` encapsulates the three-way storage state (in-memory numpy, +pyvips-backed, or lazy disk load) that `Slide` previously managed with +`stored_dxdy`, `_bk_dxdy_np`, and `_bk_dxdy_f`. Public interface: +`as_numpy()`, `as_vips()`, `set_array()`, `set_path()`, `save()`, `load()`. + +### Issue 12 — Example scripts (`examples/`) +Four new runnable examples (no external datasets required to read): +- `basic_registration.py` — minimal 20-line pipeline +- `non_rigid_registration.py` — when and how to use non-rigid registration +- `resume_from_saved_state.py` — reload a pickled registrar; warp slides or points +- `extract_transforms.py` — dump transformation matrices to JSON + numpy files + +### Issue 14 — Optional deep-learning dependencies (`pyproject.toml`) +`torch`, `torchvision`, `kornia`, and `einops` moved from `[project.dependencies]` +to `[project.optional-dependencies].dl`. Users who only need OpenCV-based +detectors can install with `pip install valis-wsi` (~2 GB lighter). +`pip install 'valis-wsi[dl]'` restores the full feature set. + +Classes that require torch (`SuperPointFD`, `KorniaFD`, `DiskFD`, `DeDoDeFD`, +`SuperGlueMatcher`, `LightGlueMatcher`) now raise a descriptive `ImportError` +with install instructions when torch is not available. `DEFAULT_MATCHER` in +`registration.py` falls back to `VggFD`-based matching when torch is absent. + +--- + +### Issue 1 — God-class split (`registration/` package) +`registration.py` (6,649 lines, two deeply coupled classes) converted to a +proper Python package at `src/valis/registration/`: + +``` +registration/ +├── __init__.py Re-exports every public name — zero breaking changes +├── _constants.py Module-level constants, defaults, shared imports (~180 lines) +├── state.py CropMode, DisplacementField, RegistrationConfig, load_registrar +├── slide.py Slide class (~1,500 lines) +└── pipeline.py Valis class (~4,800 lines) +``` + +- `from valis.registration import Valis, Slide, load_registrar` continues to work. +- `slide.py` uses `TYPE_CHECKING` to reference `Valis` without a circular import. +- All 26 unit tests pass unchanged. + +A reasonable starting point that would have the most immediate usability impact without requiring a full rewrite: + +1. ~~Add type hints to all public API methods (issues 2, 5)~~ **done (partial)** +2. ~~Replace string constants with enums (issue 4)~~ **done** +3. ~~Add construction validation (issue 13)~~ **done** +4. ~~Fix or guard the import-order segfault (issue 9)~~ **done** +5. ~~Add unit tests for coordinate transforms and crop logic (issue 8)~~ **done (partial)** diff --git a/README.md b/README.md new file mode 100644 index 00000000..35ce133a --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +See documentation for original project at https://valis.readthedocs.io/en/latest/. + +See `examples` for example usage of this fork. + +## Changes in this fork + +- All bioformats/java related dependencies removed. These complicated the project and I don't care about them. This fork only accepts TIFF files (this includes .OME.TIF files) +- Handling of .ome.tif inputs and multiple channels simplified (made opinioned decisions so it "just works" for my use case) +- Fixed a bug causing program to crash in single cpu environments +- Organized into a better python package structure so this can be used as a dependency in other python projects +- Containerization + +## Running the smoketest + +A smoketest that downloads two small example images and runs a full registration is in `tests/test_align_two.py`. On first run it fetches ~12MB from the upstream repo and caches them locally; subsequent runs are offline. + +```bash +.venv/bin/python -m pytest tests/test_align_two.py -v +``` + +After it passes, visual artifacts are written to `tests/test_output/smoketest/`: + +- `overlaps/smoketest_original_overlap.png` — the two images before alignment +- `overlaps/smoketest_rigid_overlap.png` — after rigid registration +- `overlaps/smoketest_non_rigid_overlap.png` — after non-rigid registration +- `deformation_fields/` — warp meshes showing how much each image was corrected + +## Known Issues + +Python will segfault is this project (`valis`) is not imported first before any other pytorch-related import. +Don't ask me why! + +License +------- + +`MIT` © 2021-2025 Chandler Gatenbee diff --git a/README.rst b/README.rst deleted file mode 100644 index ecedc12d..00000000 --- a/README.rst +++ /dev/null @@ -1,83 +0,0 @@ - -|docs| |CI| |pypi| - -.. .. |Upload Python Package| image:: https://github.com/MathOnco/valis/actions/workflows/python-publish.yml/badge.svg - :target: https://github.com/MathOnco/valis/actions/workflows/python-publish.yml - -.. .. |build-status| image:: https://circleci.com/gh/readthedocs/readthedocs.org.svg?style=svg -.. :alt: build status -.. :target: https://circleci.com/gh/readthedocs/readthedocs.org - -.. |docs| image:: https://readthedocs.org/projects/valis/badge/?version=latest - :target: https://valis.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -.. |CI| image:: https://github.com/MathOnco/valis/workflows/CI/badge.svg?branch=main - :target: https://github.com/MathOnco/valis/actions?workflow=CI - :alt: CI Status - -.. .. |conda| image:: https://img.shields.io/conda/vn/conda-forge/valis_wsi - :alt: Conda (channel only) - -.. |pypi| image:: https://badge.fury.io/py/valis-wsi.svg - :target: https://badge.fury.io/py/valis-wsi - -.. image:: https://zenodo.org/badge/444523406.svg - :target: https://zenodo.org/badge/latestdoi/444523406 - - -.. .. |coverage| image:: https://codecov.io/gh/readthedocs/readthedocs.org/branch/master/graph/badge.svg -.. :alt: Test coverage -.. :scale: 100% -.. :target: https://codecov.io/gh/readthedocs/readthedocs.org - -| -| - -.. image:: https://github.com/MathOnco/valis/raw/main/docs/_images/banner.gif - -| -| - - -VALIS, which stands for Virtual Alignment of pathoLogy Image Series, is a fully automated pipeline to register whole slide images (WSI) using rigid and/or non-rigid transformtions. A full description of the method is described in the paper by `Gatenbee et al. 2023 `_. VALIS uses `Bio-Formats `_, `OpenSlide `__, `libvips `_, and `scikit-image `_ to read images and slides, and so is able to work with over 300 image formats. Registered images can be saved as `ome.tiff `_ slides that can be used in downstream analyses. ome.tiff format is opensource and widely supported, being readable in several different programming languages (Python, Java, Matlab, etc...) and software, such as `QuPath `_, `HALO by Idica Labs `_, etc... - -The registration pipeline is fully automated and goes as follows: - - .. image:: https://github.com/MathOnco/valis/raw/main/docs/_images/pipeline.png - - #. Images/slides are converted to numpy arrays. As WSI are often too large to fit into memory, these images are usually lower resolution images from different pyramid levels. - - #. Images are processed to single channel images. They are then normalized to make them look as similar as possible. Masks are then created to focus registration on the tissue. - - #. Image features are detected and then matched between all pairs of image. - - #. If the order of images is unknown, they will be optimally ordered based on their feature similarity. This increases the chances of successful registration because each image will be aligned to one that looks very similar. - - #. Images will be aligned *towards* (not to) a reference image. If the reference image is not specified, it will automatically be set to the image at the center of the stack. - - #. Rigid registration is performed serially, with each image being rigidly aligned towards the reference image. That is, if the reference image is the 5th in the stack, image 4 will be aligned to 5 (the reference), and then 3 will be aligned to the now registered version of 4, and so on. Only features found in both neighboring slides are used to align the image to the next one in the stack. VALIS uses feature detection to match and align images, but one can optionally perform a final step that maximizes the mutual information between each pair of images. This rigid registration can optionally be updated by matching features in higher resolution versions of the images (see :code:`micro_rigid_registrar.MicroRigidRegistrar`). - - #. The registered rigid masks are combined to create a non-rigid registration mask. The bounding box of this mask is then used to extract higher resolution versions of the tissue from each slide. These higher resolution images are then processed as above and used for non-rigid registration, which is performed either by: - - * aligning each image towards the reference image following the same sequence used during rigid registration. - * using groupwise registration that non-rigidly aligns the images to a common frame of reference. Currently this is only possible if `SimpleElastix `__ is installed. - - #. One can optionally perform a second non-rigid registration using an even higher resolution versions of each image. This is intended to better align micro-features not visible in the original images, and so is referred to as micro-registration. A mask can also be used to indicate where registration should take place. - - #. Error is estimated by calculating the distance between registered matched features in the full resolution images. - -The transformations found by VALIS can then be used to warp the full resolution slides. It is also possible to merge non-RGB registered slides to create a highly multiplexed image. These aligned and/or merged slides can then be saved as ome.tiff images. The transformations can also be use to warp point data, such as cell centroids, polygon vertices, etc... - -In addition to registering images, VALIS provides tools to read slides using Bio-Formats and OpenSlide, which can be read at multiple resolutions and converted to numpy arrays or pyvips.Image objects. One can also slice regions of interest from these slides and warp annotated images. VALIS also provides functions to convert slides to the ome.tiff format, preserving the original metadata. Please see examples and documentation for more details. - - -Full documentation with installation instructions and examples can be found at `ReadTheDocs `_. - - -License -------- - -`MIT`_ © 2021-2025 Chandler Gatenbee - -.. _MIT: LICENSE.txt diff --git a/docker/docker_download_weights.py b/docker/docker_download_weights.py index a83f6150..fc1a0a19 100644 --- a/docker/docker_download_weights.py +++ b/docker/docker_download_weights.py @@ -5,9 +5,7 @@ import torch import kornia -from valis import feature_detectors, feature_matcher#, non_rigid_registrars - - +from valis import feature_detectors, feature_matcher # , non_rigid_registrars print("Downloading DiskFD weights") disk_fd = feature_detectors.DiskFD() diff --git a/docs/conf.py b/docs/conf.py index bcb527f7..cdebc791 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,23 +15,20 @@ import re # sys.path.insert(0, os.path.abspath('../..')) -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) sys.setrecursionlimit(1500) # -- Project information ----------------------------------------------------- -project = 'valis' -copyright = '2022-2025, Chandler Gatenbee' -author = 'Chandler Gatenbee' +project = "valis" +copyright = "2022-2025, Chandler Gatenbee" +author = "Chandler Gatenbee" # Get full version, including alpha/beta/rc tags with open("../valis/__init__.py") as fp: Lines = fp.readlines() for line in Lines: - if re.search("__version__", line): - release = line.split("= " )[1] - - - + if re.search("__version__", line): + release = line.split("= ")[1] # -- General configuration --------------------------------------------------- @@ -39,28 +36,29 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.duration', - 'sphinx.ext.doctest', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - # 'rst2pdf.pdfbuilder', - 'sphinx.ext.githubpages' - # "myst_parser" - ] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + # 'rst2pdf.pdfbuilder', + "sphinx.ext.githubpages", + # "myst_parser" +] intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), } -intersphinx_disabled_domains = ['std'] +intersphinx_disabled_domains = ["std"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -69,19 +67,17 @@ from sphinx.builders.html import StandaloneHTMLBuilder + StandaloneHTMLBuilder.supported_image_types = [ - 'image/svg+xml', - 'image/gif', - 'image/png', - 'image/jpeg' + "image/svg+xml", + "image/gif", + "image/png", + "image/jpeg", ] from sphinx.builders.latex import LaTeXBuilder -LaTeXBuilder.supported_image_types = [ - 'image/png', - 'image/pdf' - 'image/jpeg' -] + +LaTeXBuilder.supported_image_types = ["image/png", "image/pdf" "image/jpeg"] # -- Options for HTML output ------------------------------------------------- @@ -90,25 +86,27 @@ # a list of builtin themes. # # html_theme = 'alabaster' -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_logo = "https://github.com/MathOnco/valis/raw/main/docs/_images/valis_logo_black_no_bg.png" +html_static_path = ["_static"] +html_logo = ( + "https://github.com/MathOnco/valis/raw/main/docs/_images/valis_logo_black_no_bg.png" +) html_theme_options = { # 'analytics_id': 'G-XXXXXXXXXX', # Provided by Google in your dashboard # 'analytics_anonymize_ip': False, - 'logo_only': True, - 'display_version': True, + "logo_only": True, + "display_version": True, # 'prev_next_buttons_location': 'bottom', # 'style_external_links': False, # 'vcs_pageview_mode': '', - 'style_nav_header_background': 'black', + "style_nav_header_background": "black", # Toc options # 'collapse_navigation': True, # 'sticky_navigation': True, - 'navigation_depth': 5 + "navigation_depth": 5, # 'includehidden': True, # 'titles_only': False } diff --git a/examples/EXAMPLES_README.md b/examples/EXAMPLES_README.md deleted file mode 100644 index 579c771f..00000000 --- a/examples/EXAMPLES_README.md +++ /dev/null @@ -1,46 +0,0 @@ -We have provided two test datasets, located in the `examples/example_datasets` folders. The images in each dataset are much smaller versions of the original WSI, as the originals are too large to transfer easily. However, VALIS can be used with much larger images. - -## IHC registration -The `register_ihc.py` example registers the slides in `examples/example_datasets/ihc`, with results being saved in `examples/expected_results/registration/ihc`. Inside this folder are several subfolders that show the results of the alignment: - -1. *data* contains 2 files: - * a summary spreadsheet of the alignment results, such - as the registration error between each pair of slides, their - dimensions, physical units, etc... - - * a pickled version of the registrar. This can be reloaded - (unpickled) and used later. For example, one could perform - the registration locally, but then use the pickled object - to warp and save the slides on an HPC. Or, one could perform - the registration and use the registrar later to warp - points in the slide. - -2. *overlaps* contains thumbnails showing the how the images - would look if stacked without being registered, how they - look after rigid registration, and how they would look - after non-rigid registration. - -3. *rigid_registration* shows thumbnails of how each image - looks after performing rigid registration. - -4. *non_rigid_registration* shows thumbnaials of how each - image looks after non-rigid registration. - -5. *deformation_fields* contains images showing what the - non-rigid deformation would do to a triangular mesh. - These can be used to get a better sense of how the - images were altered by non-rigid warping - -6. *processed* shows thumnails of the processed images. - This are thumbnails of the images that are actually - used to perform the registration. The pre-processing - and normalization methods should try to make these - images look as similar as possible. - -After the registration has completed, the script also saves the registered slides to `examples/expected_results/registered_slides/ihc`. On a 2018 MacBook pro with a 2.6 GHz Intel Core i7 processor and 32Gb RAM, registration took 1.16 minutes and saving all of the slides took 12.5 seconds. - -## CyCIF registration -The `register_and_merge_cycif.py` example registers the slides in `examples/example_datasets/cycif`, with results being saved in `examples/expected_results/registration/cycif`. After registration is complete, the slides are registered and merged to create a 7 channel immunofluorescence image, which is saved in `examples/expected_results/registered_slides/cycif`. On a 2018 MacBook pro with a 2.6 GHz Intel Core i7 processor and 32Gb RAM, registration took 1.33 minutes and saving all of the slides took 4.97 seconds. - -## Point warping -This example shows how warp a set of ROI coordinates and use them to slice the ROI from the registered images. \ No newline at end of file diff --git a/examples/acrobat_2023/Dockerfile b/examples/acrobat_2023/Dockerfile deleted file mode 100644 index 697b2e5b..00000000 --- a/examples/acrobat_2023/Dockerfile +++ /dev/null @@ -1,139 +0,0 @@ -FROM ubuntu:kinetic as builder -# Using kinetic to get Python 3.10, used by Poetry - -ARG WKDIR=/usr/local/src -WORKDIR ${WKDIR} - -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -# Get build dependencies -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y \ - build-essential \ - software-properties-common \ - ninja-build \ - python3-pip \ - bc \ - wget \ - ca-certificates \ - git-all \ - cmake - -# we need meson for libvips build -RUN pip3 install meson - -# libvips dependencies -RUN apt-get install --no-install-recommends -y \ - glib-2.0-dev \ - libexpat-dev \ - librsvg2-dev \ - libpng-dev \ - libjpeg-turbo8-dev \ - libtiff-dev \ - libexif-dev \ - liblcms2-dev \ - libheif-dev \ - liborc-dev \ - libgirepository1.0-dev \ - libopenslide-dev \ - libjxr-dev - -RUN update-ca-certificates -# Install libvips from source to get latest version -ENV LD_LIBRARY_PATH /usr/local/lib -ENV PKG_CONFIG_PATH /usr/local/lib/pkgconfig - -ARG VIPS_VERSION=8.14.1 -ARG VIPS_URL=https://github.com/libvips/libvips/releases/download - -# build the head of the stable 8.14 branch -RUN wget ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz --no-check-certificate \ - && tar xf vips-${VIPS_VERSION}.tar.xz \ - && cd vips-${VIPS_VERSION} \ - && meson build --buildtype=release --libdir=lib \ - && cd build \ - && ninja \ - && ninja install - -RUN rm vips-${VIPS_VERSION}.tar.xz -RUN rm -r vips-${VIPS_VERSION} - - - -# Install python packages using poetry -COPY . . - -RUN pip3 install --upgrade pip -RUN pip3 install poetry && poetry config virtualenvs.in-project true - - -RUN pip3 install poetry -RUN poetry remove aicspylibczi -RUN poetry install --only main - -# Set path to use .venv Python -ENV PATH="${WKDIR}/.venv/bin:$PATH" - -# Install python packages that can't be installed with poetry/pip (pep517) -RUN . .venv/bin/activate -ARG CZI_DIR=./aicspylibczi -RUN git config --global http.sslVerify false -RUN git clone https://github.com/AllenCellModeling/aicspylibczi.git ${CZI_DIR} --recurse-submodules -WORKDIR ${CZI_DIR} -RUN pip install -e .[dev] # for development (-e means editable so changes take effect when made) - -WORKDIR ${WKDIR} - - -# Install bioformats.jar in valis -ARG BF_VERSION=7.0.0 -RUN wget https://downloads.openmicroscopy.org/bio-formats/${BF_VERSION}/artifacts/bioformats_package.jar -P valis - -# Clean up -RUN apt-get remove -y wget build-essential ninja-build && \ - apt-get autoremove -y && \ - apt-get autoclean && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - rm -rf /usr/local/lib/python* - - -# Copy over only what is needed to run, but not build, the package. -FROM ubuntu:kinetic - -ARG WKDIR=/usr/local/src -WORKDIR ${WKDIR} - -COPY --from=builder /usr/local/lib /usr/local/lib -COPY --from=builder /etc/ssl/certs /etc/ssl/certs -COPY --from=builder /usr/local/src /usr/local/src - -ENV LD_LIBRARY_PATH /usr/local/lib -ENV PKG_CONFIG_PATH /usr/local/lib/pkgconfig -ENV PATH="${WKDIR}/.venv/bin:$PATH" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update \ - && apt-get install -y \ - glib-2.0-dev \ - libexpat-dev \ - librsvg2-dev \ - libpng-dev \ - libjpeg-turbo8-dev \ - libtiff-dev \ - libexif-dev \ - liblcms2-dev \ - libheif-dev \ - liborc-dev \ - libgirepository1.0-dev \ - libopenslide-dev \ - libjxr-dev - -# Install other non-Python dependencies -RUN apt-get install -y \ - openjdk-11-jre - -ENTRYPOINT [ "bash", "register" ] \ No newline at end of file diff --git a/examples/acrobat_2023/LICENSE.txt b/examples/acrobat_2023/LICENSE.txt deleted file mode 100644 index 6b9e1010..00000000 --- a/examples/acrobat_2023/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2022 Chandler Gatenbee - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/examples/acrobat_2023/README.rst b/examples/acrobat_2023/README.rst deleted file mode 100644 index 9cd46b06..00000000 --- a/examples/acrobat_2023/README.rst +++ /dev/null @@ -1,44 +0,0 @@ -VALIS for ACROBAT 2023 -====================== - -Chandler Gatenbee :sup:`1`, Alexander R.A. Anderson :sup:`1` - -:sup:`1` Department of Integrated Mathematical Oncology, H. Lee Moffitt Cancer Center & Research Institute, 12902 Magnolia Drive, SRB 4, Tampa, Florida, 336122 - -VALIS :sup:`1` was used to perform registration of the test images provided as part of the ACROBAT 2023 grand challenge, although with a few updates. In particular, a different preprocessing method was used, and a second higher-resolution rigid registration was performed prior to non-rigid registration. These updates will be included as options in the next release of VALIS. The code used for the grand challenge, which includes these updates, can found on GitHub. - -Preprocessing -************* - -.. image:: https://github.com/MathOnco/valis/raw/main/examples/acrobat_2023/_images/processing_example.png - -**Figure 1** Image pre-processing. a) Original moving image (top) and fixed image (bottom) b) Processed moving image (top) and processed fixed image (bottom). - -The goal of this preprocessing method is to “flatten” the image, such that lightly stained regions get enhanced while heavily stained or very dark regions (often artifacts) get lightened. To accomplish this, the image is first converted to the polar CAM16-UCS colorspace to create a JCH image :sup:`2`. The JCH colors are then then clustered into 100 groups using K-means clustering, and the centroids of each cluster then converted back to RGB. The RGB centroids are used to create a “color matrix” for the image, which is in turn used to deconvolve the image into 100 channels using color deconvolution :sup:`3`. The mean for each pixel is calculated using all channels, creating a single channel image, which further undergoes adaptive histogram equalization. The results of the preprocessing can be seen in Figure 1. - - - -High resolution rigid registration -********************************** - -.. image:: https://github.com/MathOnco/valis/raw/main/examples/acrobat_2023/_images/mico_rigid_reg.png - -**Figure 2** High resolution rigid registration. Left and right insets show which features were matched in the tiles extracted from the images aligned using preliminary rigid transforms (center). Fixed images are on the top, while the moving images are on the bottom. - -The images used for the initial rigid registration are much lower resolution than the original WSI. While this allows VALIS to attain a good initial alignment, using features from the higher resolution may improve the rigid registration, potentially reducing unwanted/excessive non-rigid deformations, both of which should result in more accurate alignments. Higher resolution feature detection should also improve VALIS’ error estimates, which were not very accurate. As such, we have introduced a second rigid registration step that is performed on higher resolution images warped using the initial rigid transforms (1/8th the size of the full resolution registered WSI) (see Figure 2). The moving and fixed images are then divided into tiles (512 x 512 pixels), each of which is processed with the preprocessing method described above. After processing, each pair of tiles is normalized to one another using the approach described in the VALIS manuscript. Next, Super Point and Super Glue :sup:`4` are used to detect and match features in the moving and fixed tiles. After all tiles have gone through this process, all matched keypoints are combined and filtered using RANSAC and Tukey’s outlier approach (i.e. the same filtering steps conducted during the initial rigid registration). The high-resolution rigid transformation matrix, M', can then be estimated using these matched features. If it is found that this higher resolution rigid registration produced more matches and that the average distances between registered matched features is smaller than before, M' is kept. Otherwise, the rigid transformation matrix found using the lower resolution images is retained. - -After the higher resolution rigid alignment is complete, registration proceeds as described in the VALIS manusript1, with the “micro-registration” being performed on images that are 0.25 of the full resolution WSI. After registration is complete, we estimate the error using only the rigid transform and using the rigid + non-rigid transform by calculating the distance between matched features using each approach. The provided landmarks are then warped using the transform (rigid or rigid + non-rigid) that had the lowest estimated error. - - -References -********** - -1. Gatenbee CD, et al. Virtual alignment of pathology image series for multi-gigapixel whole slide images. Nature Communications 14, 4502 (2023). - -2. Li C, et al. Comprehensive color solutions: CAM16, CAT16, and CAM16-UCS. Color Research & Application 42, 703-718 (2017). - -3. Ruifrok AC, Johnston DA. Quantification of histochemical staining by color deconvolution. Anal Quant Cytol Histol 23, 291-299 (2001). - -4. Sarlin P-E, DeTone D, Malisiewicz T, Rabinovich A. SuperGlue: Learning Feature Matching with Graph Neural Networks. In: CVPR) (2020). - - diff --git a/examples/acrobat_2023/_images/mico_rigid_reg.png b/examples/acrobat_2023/_images/mico_rigid_reg.png deleted file mode 100644 index 441cdb21..00000000 Binary files a/examples/acrobat_2023/_images/mico_rigid_reg.png and /dev/null differ diff --git a/examples/acrobat_2023/_images/processing_example.png b/examples/acrobat_2023/_images/processing_example.png deleted file mode 100644 index 52a347bd..00000000 Binary files a/examples/acrobat_2023/_images/processing_example.png and /dev/null differ diff --git a/examples/acrobat_2023/poetry.lock b/examples/acrobat_2023/poetry.lock deleted file mode 100644 index 99a85231..00000000 --- a/examples/acrobat_2023/poetry.lock +++ /dev/null @@ -1,2262 +0,0 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. - -[[package]] -name = "aicspylibczi" -version = "3.1.2" -description = "A python module and a python extension for Zeiss (CZI/ZISRAW) microscopy files." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "aicspylibczi-3.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39ab6d2fb7cfc4bc9bd65aa8a1fbd04d14c451619b5b56be55666d66ab93df17"}, - {file = "aicspylibczi-3.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:498e4936cc08de78916cbcd5cbf043b6cf42da6371b4449781419af738ad5b88"}, - {file = "aicspylibczi-3.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e53e3fb305c783a4ffe44fe2bf3f390bfe24b0f944f6d345171159b41d178f11"}, - {file = "aicspylibczi-3.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28df036ac02b66769273764231928e3134f61261942393205dda73479f272e8e"}, - {file = "aicspylibczi-3.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:9b72bb6c6dde1dfffa1d76c4976182a511a8dc95dd5a22b10fc5c0d6d2ce579f"}, - {file = "aicspylibczi-3.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f36343dcf7f5417144c37782f34a4b74fc60a212277c22639fe34f468fd0dcb7"}, - {file = "aicspylibczi-3.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c76fe294b734d379e0448fc0c0ab55cc6b979f41e2f9841ad98d064b15be7a1d"}, - {file = "aicspylibczi-3.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f763308dbfc120e0ae9ea7b4faa18a4fb699a6b14013c657e82c99c88a0fe37"}, - {file = "aicspylibczi-3.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6d494aa4f5eb42e9d9c95a590725758ec99591dcb6859c7a1952847f6b60fc"}, - {file = "aicspylibczi-3.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:434cbe451f57eabd5095857e6a2bdbd90ff8c8d19eeace74e4d018b38eb7f51f"}, - {file = "aicspylibczi-3.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:faf01cae5b440e494cc0c8f955736ba8485c9a69bedbf6146d86102016aea9fc"}, - {file = "aicspylibczi-3.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f75b52ae10a12ef827c49c9e15ddfff54c19800138a4347cfce9ceba2deac61"}, - {file = "aicspylibczi-3.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:070c9acaf1220a5f17c0d1f5acee0fdbf2e78463315afbbe702cbf061f626631"}, - {file = "aicspylibczi-3.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fc81e519f331409ab27eccf009b1d4f0797cb8064c368e2eb298ff67d4afa4c2"}, - {file = "aicspylibczi-3.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c29f896c81df8c974107397771ab2ecd198fc6f11b942653ca3f301a08aafcb2"}, - {file = "aicspylibczi-3.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:002313c4adb9278122c1fa6a0f285405236c69c550bcf6f655f32c5bdf523907"}, - {file = "aicspylibczi-3.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a26f02d56f06202dfaaead70add2d960f0f77c3641acedee4bee79dfbea5dac"}, - {file = "aicspylibczi-3.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:cdf57b1ab4b9219e488f44917f28d4b3ff5b974daedd4dc65c989145b1a7fe8f"}, - {file = "aicspylibczi-3.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0488df7f14c10c5723ba54bc7eafb1537cc3b98334f915b64738d62d49d8941"}, - {file = "aicspylibczi-3.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8784c24389d55bb158bd953e061c2c3166edafeccca752e3974bdd49cc9ed46a"}, - {file = "aicspylibczi-3.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172e451a8b7fd90393bb7d1fb0a15d916696a8e1736eefdb7866dfe21ebce422"}, - {file = "aicspylibczi-3.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e1da7a2e5947de4e56d85ff92772aac639c5a5ffdb6fb72c0bb1859665efb0a"}, - {file = "aicspylibczi-3.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:9cc7e43a2ba79a259afa17b4cf7854063fc6b54b403f7d6e5f494889a12472f1"}, - {file = "aicspylibczi-3.1.2.tar.gz", hash = "sha256:d903caaac5576922f8dd4b1170fae3421fb9d203128b4d8ecb11681173e7046c"}, -] - -[package.dependencies] -numpy = ">=1.14.1" - -[package.extras] -all = ["Sphinx", "altair", "breathe", "bump2version (>=1.0.1)", "cmake", "coverage (>=5.0a4)", "flake8", "flake8 (>=3.7.7)", "ipython (>=7.5.0)", "jupyterlab", "m2r2 (>=0.2.7)", "matplotlib", "numpy (>=1.14.1)", "pillow", "pytest", "pytest (>=4.3.0)", "pytest-cov", "pytest-cov (==2.6.1)", "pytest-raises", "pytest-raises (>=0.10)", "pytest-runner", "pytest-runner (>=4.4)", "pytest-xdist", "sphinx-rtd-theme (>=0.1.2)", "tox (>=3.5.2)", "twine (>=1.13.0)", "wheel (>=0.33.1)"] -dev = ["Sphinx", "breathe", "bump2version (>=1.0.1)", "cmake", "coverage (>=5.0a4)", "flake8 (>=3.7.7)", "ipython (>=7.5.0)", "m2r2 (>=0.2.7)", "pytest (>=4.3.0)", "pytest-cov (==2.6.1)", "pytest-raises (>=0.10)", "pytest-runner (>=4.4)", "sphinx-rtd-theme (>=0.1.2)", "tox (>=3.5.2)", "twine (>=1.13.0)", "wheel (>=0.33.1)"] -interactive = ["altair", "jupyterlab", "matplotlib", "pillow"] -setup = ["pytest-runner"] -test = ["flake8", "pytest", "pytest-cov", "pytest-raises", "pytest-xdist"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -description = "Screen-scraping library" -category = "main" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] - -[package.dependencies] -pycparser = "*" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "colour-science" -version = "0.4.2" -description = "Colour Science for Python" -category = "main" -optional = false -python-versions = ">=3.9,<3.12" -files = [ - {file = "colour_science-0.4.2-py3-none-any.whl", hash = "sha256:48246aa3d17a9a07ac335b4a04a00f8d5c624b2d15c5bdef820836ef13e2762d"}, - {file = "colour_science-0.4.2.tar.gz", hash = "sha256:29a8d4c67104dafeebcd7c942f31e0935e356527a265aa81c4164ab34dd69b66"}, -] - -[package.dependencies] -imageio = ">=2,<3" -numpy = ">=1.20,<2" -scipy = ">=1.7,<2" -typing-extensions = ">=4,<5" - -[package.extras] -development = ["biblib-simple", "black", "blackdoc", "coverage (!=6.3)", "coveralls", "flake8", "flynt", "invoke", "jupyter", "mypy", "pre-commit", "pydata-sphinx-theme", "pydocstyle", "pytest", "pytest-cov", "pytest-xdist", "pyupgrade", "restructuredtext-lint", "sphinx (>=4,<5)", "sphinxcontrib-bibtex", "toml", "twine"] -graphviz = ["pygraphviz (>=1,<2)"] -meshing = ["trimesh (>=3,<4)"] -optional = ["networkx (>=2.6,<3)", "pandas (>=1.3,<2)", "tqdm (>=4,<5)"] -plotting = ["matplotlib (>=3.4,!=3.5.0,!=3.5.1)"] -read-the-docs = ["matplotlib (>=3.4,!=3.5.0,!=3.5.1)", "networkx (>=2.6,<3)", "pydata-sphinx-theme", "pygraphviz (>=1,<2)", "sphinxcontrib-bibtex", "trimesh (>=3,<4)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "dnspython" -version = "2.4.2" -description = "DNS toolkit" -category = "main" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, - {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, -] - -[package.extras] -dnssec = ["cryptography (>=2.6,<42.0)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] -doq = ["aioquic (>=0.9.20)"] -idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.23)"] -wmi = ["wmi (>=1.5.1,<2.0.0)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "elementpath" -version = "4.1.5" -description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "elementpath-4.1.5-py3-none-any.whl", hash = "sha256:2ac1a2fb31eb22bbbf817f8cf6752f844513216263f0e3892c8e79782fe4bb55"}, - {file = "elementpath-4.1.5.tar.gz", hash = "sha256:c2d6dc524b29ef751ecfc416b0627668119d8812441c555d7471da41d4bacb8d"}, -] - -[package.extras] -dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", "memray", "mypy", "tox", "xmlschema (>=2.0.0)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "email-validator" -version = "2.0.0.post2" -description = "A robust email address syntax and deliverability validation library." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"}, - {file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "fastcluster" -version = "1.2.6" -description = "Fast hierarchical clustering routines for R and Python." -category = "main" -optional = false -python-versions = ">=3" -files = [ - {file = "fastcluster-1.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d0e8faef0437a25fd083df70fb86cc65ce3c9c9780d4ae377cbe6521e7746ce0"}, - {file = "fastcluster-1.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8be01f97bc2bf11a9188537864f8e520e1103cdc6007aa2c5d7979b1363b121"}, - {file = "fastcluster-1.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:855ab2b7e6fa9b05f19c4f3023dedfb1a35a88d831933d65d0d9e10a070a9e85"}, - {file = "fastcluster-1.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72503e727887a61a15f9aaa13178798d3994dfec58aa7a943e42dcfda07c0149"}, - {file = "fastcluster-1.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcb0973ca0e6978e3242046338c350cbed1493108929231fae9bd35ad05a6d6"}, - {file = "fastcluster-1.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9020899b67fe492d0ed87a3e993ec9962c5a0b51ea2df71d86b1766f065f1cef"}, - {file = "fastcluster-1.2.6-cp310-cp310-win32.whl", hash = "sha256:6cf156d4203708348522393c523c2e61c81f5a6a500e0411dcba2b064551ea2f"}, - {file = "fastcluster-1.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:1801c9daa9aa5bbbb0830efe8bd3034b4b7a417e4b8dd353683999be29797df2"}, - {file = "fastcluster-1.2.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce70c743490f6778b463524d1767a9ecccd31c8bd2dbb5739bb2174168c15d39"}, - {file = "fastcluster-1.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac1b84d4b28456a379a71451d13995eb3242143452ce9c861f8913360de842a3"}, - {file = "fastcluster-1.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b49f6033c45a28f93540847b495ed0f718b5c3f4fef446cf77e3726662e1d5"}, - {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1c776a4ec7594f47cd2e1e2da73a30134f1d402d7c93a81e3cb7c3d8e191173"}, - {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca61d16435bb7aea3901939d7d7d7e36aff9bb538123e649166a3014b280054"}, - {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04ea4a68e0675072ca761bad33322a0e998cb43693fd41165bc420d7db40429a"}, - {file = "fastcluster-1.2.6-cp311-cp311-win32.whl", hash = "sha256:773043d5db2790e1ff2a4e1eae0b6a60afb2a93ad2c74897a56c80bc800db04f"}, - {file = "fastcluster-1.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:841d128daa6597d13781793eb482b0b566bbd58d2a9d1e2cf1b58838773beb14"}, - {file = "fastcluster-1.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cf5acfe1156849279ebd44a8d1fbcbe8b8e21334f7538eda782ae31e7dade9e2"}, - {file = "fastcluster-1.2.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb27c13225f5f77f3c5986a27ca27277cce7db12844330cf535019cd38021257"}, - {file = "fastcluster-1.2.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fe543b6d45da27e84e5af6248722475b88943d8ef40d835cbabbb0ba5ee786b"}, - {file = "fastcluster-1.2.6-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c12224da0b1f2f9d2b3d715dc82ecb1a3a33b990606f97da075cc46bc6d9576f"}, - {file = "fastcluster-1.2.6-cp36-cp36m-win32.whl", hash = "sha256:86a1ad972e83ba48144884fa849f87626346308b650002157123aee67d3b16fe"}, - {file = "fastcluster-1.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:8d3c9eab8e69cb36dcdd64c8b3200e008aedf88e34d39e01ae6af98a9605ad18"}, - {file = "fastcluster-1.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c61be0bad81a21ee3e5bef91469fdd11968f33d41d142c656accba9b2992babe"}, - {file = "fastcluster-1.2.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06df1d97edca68b2ffa43d81c3b5f4e4147bc12ab241c6585fadcdeb0bfa23ca"}, - {file = "fastcluster-1.2.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab9337b0a6a9b07b6f86fc724972d1ad729c890e2f539c1b33271c2f1f00af8b"}, - {file = "fastcluster-1.2.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4093d5454bcbe48b30e32da5db43056a08889480a96e4555f28c1f7004fc5323"}, - {file = "fastcluster-1.2.6-cp37-cp37m-win32.whl", hash = "sha256:58958a0333e3dfbad198394e9b7dd6254de0a3e622019b057288405b2a4a6bba"}, - {file = "fastcluster-1.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:03f8efe6435a7b947fa4a420676942d0267dac0d323ec5ead50f1856cc7cf96f"}, - {file = "fastcluster-1.2.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a5ceb39379327316d34613f7c16c06d7a3816aa38f4437b5e8433aa1bf149e2c"}, - {file = "fastcluster-1.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0bb54283b4d5ec96f167c7fd31921f169226c1261637434fdae7a69ee3c69573"}, - {file = "fastcluster-1.2.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6e51db0067e65687a5c46f00a11843d0bb15ca759e8a1767eebac8c4f6e3f4df"}, - {file = "fastcluster-1.2.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11748a4e245c745030e9ddd8c2c37e378f8ad8bd8e869d954c84ff674495499f"}, - {file = "fastcluster-1.2.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7254f81dc71cd29ef6f2d9747cf97ff907b569c9ef9d9760352391be5b57118c"}, - {file = "fastcluster-1.2.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa4a4c01c5fbec3623e92bc33a9f712ca416ce93255c402f5c904ac4b890ac3c"}, - {file = "fastcluster-1.2.6-cp38-cp38-win32.whl", hash = "sha256:ffdb00782cd63bbf2c45bb048897531e868326dff5081ab9b752d294b0426c1d"}, - {file = "fastcluster-1.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:a952a84453123db0c2336b9a9c86162e99ad0b897bae8213107c055a64effd41"}, - {file = "fastcluster-1.2.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a085e7e13f1afc517358981b2b7ed774dc9abf95f2be0da9a495d9e6b58c4409"}, - {file = "fastcluster-1.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a7c7f51a6d2f5ab58b1d85e9d0af2af9600ec13bb43bc6aafc9085d2c4ccd93"}, - {file = "fastcluster-1.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8bac5cf64691060cf86b0752dd385ef1eccff6d24bdb8b60691cf8cbf0e4f9ef"}, - {file = "fastcluster-1.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060c1cb3c84942d8d3618385e2c25998ba690c46ec8c73d64477f808abfac3f2"}, - {file = "fastcluster-1.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03a228e018457842eb81de85be7af0b5fe8065d666dd093193e3bdcf1f13d2e"}, - {file = "fastcluster-1.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6f8da329c0032f2acaf4beaef958a2db0dae43d3f946f592dad5c29aa82c832"}, - {file = "fastcluster-1.2.6-cp39-cp39-win32.whl", hash = "sha256:eb3f98791427d5d5d02d023b66bcef61e48954edfadae6527ef72d70cf32ec86"}, - {file = "fastcluster-1.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:4b9cfd426966b8037bec2fc03a0d7a9c87313482c699b36ffa1432b49f84ed2e"}, - {file = "fastcluster-1.2.6.tar.gz", hash = "sha256:aab886efa7b6bba7ac124f4498153d053e5a08b822d2254926b7206cdf5a8aa6"}, -] - -[package.dependencies] -numpy = ">=1.9" - -[package.extras] -test = ["scipy (>=1.6.3)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "filelock" -version = "3.12.2" -description = "A platform independent file lock." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "fonttools" -version = "4.42.0" -description = "Tools to manipulate font files" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9c456d1f23deff64ffc8b5b098718e149279abdea4d8692dba69172fb6a0d597"}, - {file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:150122ed93127a26bc3670ebab7e2add1e0983d30927733aec327ebf4255b072"}, - {file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e82d776d2e93f88ca56567509d102266e7ab2fb707a0326f032fe657335238"}, - {file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c1165f9b2662645de9b19a8c8bdd636b36294ccc07e1b0163856b74f10bafc"}, - {file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d6dc3fa91414ff4daa195c05f946e6a575bd214821e26d17ca50f74b35b0fe4"}, - {file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fae4e801b774cc62cecf4a57b1eae4097903fced00c608d9e2bc8f84cd87b54a"}, - {file = "fonttools-4.42.0-cp310-cp310-win32.whl", hash = "sha256:b8600ae7dce6ec3ddfb201abb98c9d53abbf8064d7ac0c8a0d8925e722ccf2a0"}, - {file = "fonttools-4.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b68eab183fafac7cd7d464a7bfa0fcd4edf6c67837d14fb09c1c20516cf20b"}, - {file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0a1466713e54bdbf5521f2f73eebfe727a528905ff5ec63cda40961b4b1eea95"}, - {file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb2a69870bfe143ec20b039a1c8009e149dd7780dd89554cc8a11f79e5de86b"}, - {file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae881e484702efdb6cf756462622de81d4414c454edfd950b137e9a7352b3cb9"}, - {file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ec3246a088555629f9f0902f7412220c67340553ca91eb540cf247aacb1983"}, - {file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ece1886d12bb36c48c00b2031518877f41abae317e3a55620d38e307d799b7e"}, - {file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10dac980f2b975ef74532e2a94bb00e97a95b4595fb7f98db493c474d5f54d0e"}, - {file = "fonttools-4.42.0-cp311-cp311-win32.whl", hash = "sha256:83b98be5d291e08501bd4fc0c4e0f8e6e05b99f3924068b17c5c9972af6fff84"}, - {file = "fonttools-4.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:e35bed436726194c5e6e094fdfb423fb7afaa0211199f9d245e59e11118c576c"}, - {file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c36c904ce0322df01e590ba814d5d69e084e985d7e4c2869378671d79662a7d4"}, - {file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d54e600a2bcfa5cdaa860237765c01804a03b08404d6affcd92942fa7315ffba"}, - {file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01cfe02416b6d416c5c8d15e30315cbcd3e97d1b50d3b34b0ce59f742ef55258"}, - {file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f81ed9065b4bd3f4f3ce8e4873cd6a6b3f4e92b1eddefde35d332c6f414acc3"}, - {file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:685a4dd6cf31593b50d6d441feb7781a4a7ef61e19551463e14ed7c527b86f9f"}, - {file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:329341ba3d86a36e482610db56b30705384cb23bd595eac8cbb045f627778e9d"}, - {file = "fonttools-4.42.0-cp38-cp38-win32.whl", hash = "sha256:4655c480a1a4d706152ff54f20e20cf7609084016f1df3851cce67cef768f40a"}, - {file = "fonttools-4.42.0-cp38-cp38-win_amd64.whl", hash = "sha256:6bd7e4777bff1dcb7c4eff4786998422770f3bfbef8be401c5332895517ba3fa"}, - {file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9b55d2a3b360e0c7fc5bd8badf1503ca1c11dd3a1cd20f2c26787ffa145a9c7"}, - {file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0df8ef75ba5791e873c9eac2262196497525e3f07699a2576d3ab9ddf41cb619"}, - {file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd2363ea7728496827658682d049ffb2e98525e2247ca64554864a8cc945568"}, - {file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40673b2e927f7cd0819c6f04489dfbeb337b4a7b10fc633c89bf4f34ecb9620"}, - {file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c8bf88f9e3ce347c716921804ef3a8330cb128284eb6c0b6c4b3574f3c580023"}, - {file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:703101eb0490fae32baf385385d47787b73d9ea55253df43b487c89ec767e0d7"}, - {file = "fonttools-4.42.0-cp39-cp39-win32.whl", hash = "sha256:f0290ea7f9945174bd4dfd66e96149037441eb2008f3649094f056201d99e293"}, - {file = "fonttools-4.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae7df0ae9ee2f3f7676b0ff6f4ebe48ad0acaeeeaa0b6839d15dbf0709f2c5ef"}, - {file = "fonttools-4.42.0-py3-none-any.whl", hash = "sha256:dfe7fa7e607f7e8b58d0c32501a3a7cac148538300626d1b930082c90ae7f6bd"}, - {file = "fonttools-4.42.0.tar.gz", hash = "sha256:614b1283dca88effd20ee48160518e6de275ce9b5456a3134d5f235523fc5065"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "imageio" -version = "2.31.1" -description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "imageio-2.31.1-py3-none-any.whl", hash = "sha256:4106fb395ef7f8dc0262d6aa1bb03daba818445c381ca8b7d5dfc7a2089b04df"}, - {file = "imageio-2.31.1.tar.gz", hash = "sha256:f8436a02af02fd63f272dab50f7d623547a38f0e04a4a73e2b02ae1b8b180f27"}, -] - -[package.dependencies] -numpy = "*" -pillow = ">=8.3.2" - -[package.extras] -all-plugins = ["astropy", "av", "imageio-ffmpeg", "psutil", "tifffile"] -all-plugins-pypy = ["av", "imageio-ffmpeg", "psutil", "tifffile"] -build = ["wheel"] -dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] -docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] -ffmpeg = ["imageio-ffmpeg", "psutil"] -fits = ["astropy"] -full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpydoc", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] -gdal = ["gdal"] -itk = ["itk"] -linting = ["black", "flake8"] -pyav = ["av"] -test = ["fsspec[github]", "pytest", "pytest-cov"] -tifffile = ["tifffile"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "importlib-resources" -version = "6.0.1" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, - {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "interpolation" -version = "2.2.4" -description = "Interpolation in Python" -category = "main" -optional = false -python-versions = ">=3.8,<3.11" -files = [ - {file = "interpolation-2.2.4-py3-none-any.whl", hash = "sha256:a7803744b5ca1326e77f1d947b608303e291121cfcdbcd2e09344ad2a27015c3"}, - {file = "interpolation-2.2.4.tar.gz", hash = "sha256:637aca4c60e862b2d2367b091a179f48d372a6c4277f4eb51856dc58ec93a6dc"}, -] - -[package.dependencies] -numba = ">=0.47" -numpy = ">=1.22.2,<2.0.0" -packaging = ">=21.3,<22.0" -scipy = ">=1.4.1,<2.0.0" -tempita = ">=0.5.2,<0.6.0" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "jgo" -version = "1.0.5" -description = "Launch Java code from Python and the CLI, installation-free." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jgo-1.0.5-py3-none-any.whl", hash = "sha256:ad31c3f34a95e93b480d216c8347d1ba29029eeab55ca46c5361a3b9fbccba01"}, - {file = "jgo-1.0.5.tar.gz", hash = "sha256:48cfab8ec880692d93b22c42e0239f4792c009461c121c15549c456c6a6a4eb6"}, -] - -[package.dependencies] -psutil = "*" - -[package.extras] -dev = ["autopep8", "black", "build", "flake8", "isort", "pre-commit", "pyflakes", "pytest", "pytest-cov", "validate-pyproject[all]"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "joblib" -version = "1.3.2" -description = "Lightweight pipelining with Python functions" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, - {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "jpype1" -version = "1.4.1" -description = "A Python to Java bridge." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "JPype1-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:233a6a1a9c7f3633e7d74c14039f7ea35df81e138241f1acc8f94f65a8bd086e"}, - {file = "JPype1-1.4.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0099e77c6e99af13089b1c89cd99681b485fbf74daa492ee38e35d90d6349ffc"}, - {file = "JPype1-1.4.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:213f7154a7cc859dead7143cfee255bd3ce57938e0a5f2250b6768af0cef62c8"}, - {file = "JPype1-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:8d94013dfed3d1c7ee193e86393b2aea756a9910d222b7167ed493f9a0b1b3d5"}, - {file = "JPype1-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:34373696c3457f1d686639d928ef53b6a121203d9c0e651ed44dd3adf78bedb3"}, - {file = "JPype1-1.4.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622d07408e6d9a89e9eb70d4a9a675e51d5411657ac434ccec23cc94b828d9c"}, - {file = "JPype1-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d960ce12c3913242f9e4a11e55afa9c38e1a5410d0a38cc5a21086415c38a02"}, - {file = "JPype1-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5473a89d2cab327e38382fd69d1209517bad44158fb3ee9e699ebfeb5bc1cd51"}, - {file = "JPype1-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d85489a27c58b1b21feb8601406319b6a51d233ae9fa27de9b24c4dfea560b22"}, - {file = "JPype1-1.4.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9ce364d26ccbf7a21e35737f62663ce8d3aeb4e4b0f05d7e71f6126a6eb81059"}, - {file = "JPype1-1.4.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b9e29a0ea763c16d0fb05528785d4ed0fa56a421d600eca87e43398b569d2dea"}, - {file = "JPype1-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8c2fef2d0c298c8e69b2880ff866fa5db5f477ddd78ef310a357d697362c9f89"}, - {file = "JPype1-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6089cd28067d77e5b4a09e272525ecd70f838b92a7c08d91d4fa7d192ec3f3bb"}, - {file = "JPype1-1.4.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:24e601a31fb3b44decd5389ea87cbc39aaa0c61980c39b060561f9dc604f4cc5"}, - {file = "JPype1-1.4.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:000556d5839ffbe61c12f1fa41cbfd4e9a0abe103e0100febacc069d75defa8f"}, - {file = "JPype1-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7039db1a522af55cf89f6a4a72120f8b074abcde2535543da34616640ecbb3c1"}, - {file = "JPype1-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd8bd76bf8741fa20d44ded776e6a3ea7fe103bcab7156f53ba7a72c50b7dd2f"}, - {file = "JPype1-1.4.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d483a6c3e6cb19065e71d322c4742efcfafc44bf1a67ef8ef75f78626158c3d9"}, - {file = "JPype1-1.4.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ea4be3e9471bf62ccdeccc2417ad6b9a300effcc0e5f6af9534eafa14e3978"}, - {file = "JPype1-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:8692a7b14e807b224673010a648ab12415d3bc32323e351f03e6c64814ee319b"}, - {file = "JPype1-1.4.1.tar.gz", hash = "sha256:dc8ee854073474ad79ae168d90c2f6893854f58936cfa18f3587cadae0d3696d"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -docs = ["readthedocs-sphinx-ext", "sphinx", "sphinx-rtd-theme"] -tests = ["pytest"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "kiwisolver" -version = "1.4.4" -description = "A fast implementation of the Cassowary constraint solver" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "llvmlite" -version = "0.39.1" -description = "lightweight wrapper around basic LLVM functionality" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "llvmlite-0.39.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6717c7a6e93c9d2c3d07c07113ec80ae24af45cde536b34363d4bcd9188091d9"}, - {file = "llvmlite-0.39.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ddab526c5a2c4ccb8c9ec4821fcea7606933dc53f510e2a6eebb45a418d3488a"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3f331a323d0f0ada6b10d60182ef06c20a2f01be21699999d204c5750ffd0b4"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c00ff204afa721b0bb9835b5bf1ba7fba210eefcec5552a9e05a63219ba0dc"}, - {file = "llvmlite-0.39.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16f56eb1eec3cda3a5c526bc3f63594fc24e0c8d219375afeb336f289764c6c7"}, - {file = "llvmlite-0.39.1-cp310-cp310-win32.whl", hash = "sha256:d0bfd18c324549c0fec2c5dc610fd024689de6f27c6cc67e4e24a07541d6e49b"}, - {file = "llvmlite-0.39.1-cp310-cp310-win_amd64.whl", hash = "sha256:7ebf1eb9badc2a397d4f6a6c8717447c81ac011db00064a00408bc83c923c0e4"}, - {file = "llvmlite-0.39.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6546bed4e02a1c3d53a22a0bced254b3b6894693318b16c16c8e43e29d6befb6"}, - {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1578f5000fdce513712e99543c50e93758a954297575610f48cb1fd71b27c08a"}, - {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3803f11ad5f6f6c3d2b545a303d68d9fabb1d50e06a8d6418e6fcd2d0df00959"}, - {file = "llvmlite-0.39.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50aea09a2b933dab7c9df92361b1844ad3145bfb8dd2deb9cd8b8917d59306fb"}, - {file = "llvmlite-0.39.1-cp37-cp37m-win32.whl", hash = "sha256:b1a0bbdb274fb683f993198775b957d29a6f07b45d184c571ef2a721ce4388cf"}, - {file = "llvmlite-0.39.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e172c73fccf7d6db4bd6f7de963dedded900d1a5c6778733241d878ba613980e"}, - {file = "llvmlite-0.39.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e31f4b799d530255aaf0566e3da2df5bfc35d3cd9d6d5a3dcc251663656c27b1"}, - {file = "llvmlite-0.39.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:62c0ea22e0b9dffb020601bb65cb11dd967a095a488be73f07d8867f4e327ca5"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ffc84ade195abd4abcf0bd3b827b9140ae9ef90999429b9ea84d5df69c9058c"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0f158e4708dda6367d21cf15afc58de4ebce979c7a1aa2f6b977aae737e2a54"}, - {file = "llvmlite-0.39.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d36591cd5d02038912321d9ab8e4668e53ae2211da5523f454e992b5e13c36"}, - {file = "llvmlite-0.39.1-cp38-cp38-win32.whl", hash = "sha256:4c6ebace910410daf0bebda09c1859504fc2f33d122e9a971c4c349c89cca630"}, - {file = "llvmlite-0.39.1-cp38-cp38-win_amd64.whl", hash = "sha256:fb62fc7016b592435d3e3a8f680e3ea8897c3c9e62e6e6cc58011e7a4801439e"}, - {file = "llvmlite-0.39.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa9b26939ae553bf30a9f5c4c754db0fb2d2677327f2511e674aa2f5df941789"}, - {file = "llvmlite-0.39.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4f212c018db951da3e1dc25c2651abc688221934739721f2dad5ff1dd5f90e7"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39dc2160aed36e989610fc403487f11b8764b6650017ff367e45384dff88ffbf"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ec3d70b3e507515936e475d9811305f52d049281eaa6c8273448a61c9b5b7e2"}, - {file = "llvmlite-0.39.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60f8dd1e76f47b3dbdee4b38d9189f3e020d22a173c00f930b52131001d801f9"}, - {file = "llvmlite-0.39.1-cp39-cp39-win32.whl", hash = "sha256:03aee0ccd81735696474dc4f8b6be60774892a2929d6c05d093d17392c237f32"}, - {file = "llvmlite-0.39.1-cp39-cp39-win_amd64.whl", hash = "sha256:3fc14e757bc07a919221f0cbaacb512704ce5774d7fcada793f1996d6bc75f2a"}, - {file = "llvmlite-0.39.1.tar.gz", hash = "sha256:b43abd7c82e805261c425d50335be9a6c4f84264e34d6d6e475207300005d572"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "lxml" -version = "4.9.3" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -files = [ - {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, - {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, - {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, - {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, - {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, - {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, - {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, - {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, - {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, - {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, - {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, - {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, - {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, - {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, - {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, - {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, - {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, - {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, - {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, - {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, - {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, - {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, - {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, - {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, - {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.35)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "matplotlib" -version = "3.7.2" -description = "Python plotting package" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"}, - {file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"}, - {file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"}, - {file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"}, - {file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117"}, - {file = "matplotlib-3.7.2-cp38-cp38-win32.whl", hash = "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13"}, - {file = "matplotlib-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f"}, - {file = "matplotlib-3.7.2-cp39-cp39-win32.whl", hash = "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20"}, - {file = "matplotlib-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"}, - {file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1,<3.1" -python-dateutil = ">=2.7" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "networkx" -version = "3.1" -description = "Python package for creating and manipulating graphs and networks" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, - {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, -] - -[package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "numba" -version = "0.56.4" -description = "compiling Python code using LLVM" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "numba-0.56.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9f62672145f8669ec08762895fe85f4cf0ead08ce3164667f2b94b2f62ab23c3"}, - {file = "numba-0.56.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c602d015478b7958408d788ba00a50272649c5186ea8baa6cf71d4a1c761bba1"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85dbaed7a05ff96492b69a8900c5ba605551afb9b27774f7f10511095451137c"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f4cfc3a19d1e26448032049c79fc60331b104f694cf570a9e94f4e2c9d0932bb"}, - {file = "numba-0.56.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e08e203b163ace08bad500b0c16f6092b1eb34fd1fce4feaf31a67a3a5ecf3b"}, - {file = "numba-0.56.4-cp310-cp310-win32.whl", hash = "sha256:0611e6d3eebe4cb903f1a836ffdb2bda8d18482bcd0a0dcc56e79e2aa3fefef5"}, - {file = "numba-0.56.4-cp310-cp310-win_amd64.whl", hash = "sha256:fbfb45e7b297749029cb28694abf437a78695a100e7c2033983d69f0ba2698d4"}, - {file = "numba-0.56.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:3cb1a07a082a61df80a468f232e452d818f5ae254b40c26390054e4e868556e0"}, - {file = "numba-0.56.4-cp37-cp37m-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d69ad934e13c15684e7887100a8f5f0f61d7a8e57e0fd29d9993210089a5b531"}, - {file = "numba-0.56.4-cp37-cp37m-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:dbcc847bac2d225265d054993a7f910fda66e73d6662fe7156452cac0325b073"}, - {file = "numba-0.56.4-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8a95ca9cc77ea4571081f6594e08bd272b66060634b8324e99cd1843020364f9"}, - {file = "numba-0.56.4-cp37-cp37m-win32.whl", hash = "sha256:fcdf84ba3ed8124eb7234adfbb8792f311991cbf8aed1cad4b1b1a7ee08380c1"}, - {file = "numba-0.56.4-cp37-cp37m-win_amd64.whl", hash = "sha256:42f9e1be942b215df7e6cc9948cf9c15bb8170acc8286c063a9e57994ef82fd1"}, - {file = "numba-0.56.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:553da2ce74e8862e18a72a209ed3b6d2924403bdd0fb341fa891c6455545ba7c"}, - {file = "numba-0.56.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4373da9757049db7c90591e9ec55a2e97b2b36ba7ae3bf9c956a513374077470"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a993349b90569518739009d8f4b523dfedd7e0049e6838c0e17435c3e70dcc4"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:720886b852a2d62619ae3900fe71f1852c62db4f287d0c275a60219e1643fc04"}, - {file = "numba-0.56.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64d338b504c9394a4a34942df4627e1e6cb07396ee3b49fe7b8d6420aa5104f"}, - {file = "numba-0.56.4-cp38-cp38-win32.whl", hash = "sha256:03fe94cd31e96185cce2fae005334a8cc712fc2ba7756e52dff8c9400718173f"}, - {file = "numba-0.56.4-cp38-cp38-win_amd64.whl", hash = "sha256:91f021145a8081f881996818474ef737800bcc613ffb1e618a655725a0f9e246"}, - {file = "numba-0.56.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d0ae9270a7a5cc0ede63cd234b4ff1ce166c7a749b91dbbf45e0000c56d3eade"}, - {file = "numba-0.56.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c75e8a5f810ce80a0cfad6e74ee94f9fde9b40c81312949bf356b7304ef20740"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a12ef323c0f2101529d455cfde7f4135eaa147bad17afe10b48634f796d96abd"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:03634579d10a6129181129de293dd6b5eaabee86881369d24d63f8fe352dd6cb"}, - {file = "numba-0.56.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0240f9026b015e336069329839208ebd70ec34ae5bfbf402e4fcc8e06197528e"}, - {file = "numba-0.56.4-cp39-cp39-win32.whl", hash = "sha256:14dbbabf6ffcd96ee2ac827389afa59a70ffa9f089576500434c34abf9b054a4"}, - {file = "numba-0.56.4-cp39-cp39-win_amd64.whl", hash = "sha256:0da583c532cd72feefd8e551435747e0e0fbb3c0530357e6845fcc11e38d6aea"}, - {file = "numba-0.56.4.tar.gz", hash = "sha256:32d9fef412c81483d7efe0ceb6cf4d3310fde8b624a9cecca00f790573ac96ee"}, -] - -[package.dependencies] -llvmlite = ">=0.39.0dev0,<0.40" -numpy = ">=1.18,<1.24" -setuptools = "*" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "numpy" -version = "1.23.5" -description = "NumPy is the fundamental package for array computing with Python." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.23.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63"}, - {file = "numpy-1.23.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d"}, - {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43"}, - {file = "numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1"}, - {file = "numpy-1.23.5-cp310-cp310-win32.whl", hash = "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280"}, - {file = "numpy-1.23.5-cp310-cp310-win_amd64.whl", hash = "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6"}, - {file = "numpy-1.23.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96"}, - {file = "numpy-1.23.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa"}, - {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2"}, - {file = "numpy-1.23.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387"}, - {file = "numpy-1.23.5-cp311-cp311-win32.whl", hash = "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0"}, - {file = "numpy-1.23.5-cp311-cp311-win_amd64.whl", hash = "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d"}, - {file = "numpy-1.23.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a"}, - {file = "numpy-1.23.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9"}, - {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398"}, - {file = "numpy-1.23.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb"}, - {file = "numpy-1.23.5-cp38-cp38-win32.whl", hash = "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07"}, - {file = "numpy-1.23.5-cp38-cp38-win_amd64.whl", hash = "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e"}, - {file = "numpy-1.23.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f"}, - {file = "numpy-1.23.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de"}, - {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d"}, - {file = "numpy-1.23.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719"}, - {file = "numpy-1.23.5-cp39-cp39-win32.whl", hash = "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481"}, - {file = "numpy-1.23.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135"}, - {file = "numpy-1.23.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d"}, - {file = "numpy-1.23.5.tar.gz", hash = "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "ome-types" -version = "0.3.4" -description = "Python dataclasses for the OME data model" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ome_types-0.3.4-py3-none-any.whl", hash = "sha256:9335478a9fcea872b2bd5b479c5fb0070ca91500d7ca8e4077eb089125d2075f"}, - {file = "ome_types-0.3.4.tar.gz", hash = "sha256:971139bf5b85d6492937f2fee75c03e05bd20043ac01e78e3b00991cb41cfa7b"}, -] - -[package.dependencies] -lxml = ">=4.8.0" -pint = ">=0.15" -pydantic = {version = ">=1.0,<2.0", extras = ["email"]} -xmlschema = ">=2.0.0" - -[package.extras] -autogen = ["autoflake", "black", "isort (>=5.0)", "numpydoc"] -docs = ["autoflake", "black", "ipython", "isort (>=5.0)", "numpydoc", "pygments", "sphinx (==5.3.0)", "sphinx-rtd-theme (==1.1.1)"] -test = ["pytest", "pytest-benchmark", "pytest-cov", "tox"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "opencv-contrib-python-headless" -version = "4.6.0.66" -description = "Wrapper package for OpenCV python bindings." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "opencv-contrib-python-headless-4.6.0.66.tar.gz", hash = "sha256:6de67452f8fea8ac40e997dc158614ef25b8448d1e16977de242f0c4141fa2a0"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp36-abi3-macosx_10_15_x86_64.whl", hash = "sha256:bd0c8265c0dff6b2a05fc0470d9064be85c6835aa497958274a86f27f119cc81"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c00a0cb3724cdbd8d655ebb325d449fca52b3097c3a748e74c8a7157ff1ae5"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70f9833b16c236dc4c2939afe12aaf3a7d6fc5159c228904efdbf82498fa1c"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp36-abi3-win32.whl", hash = "sha256:fcd5ee0fbdc5434cf6b3907feff92b631114ad57a340fb4e9428db672f25c173"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp36-abi3-win_amd64.whl", hash = "sha256:1c223dc115f98d01792741f1ee607e50a56d8e290b48ebfaf7cab6c3945d450f"}, - {file = "opencv_contrib_python_headless-4.6.0.66-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:930362f7fffdf011fccfe967f15be442e1b6dad03eb8eff4a36f9f7ebc67cb37"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.21.2", markers = "python_version >= \"3.10\" or python_version >= \"3.6\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.14.5", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pandas" -version = "1.5.3" -description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, -] -python-dateutil = ">=2.8.1" -pytz = ">=2020.1" - -[package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pillow" -version = "9.5.0" -description = "Python Imaging Library (Fork)" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pint" -version = "0.22" -description = "Physical quantities module" -category = "main" -optional = false -python-versions = ">=3.9" -files = [ - {file = "Pint-0.22-py3-none-any.whl", hash = "sha256:6e2b3c5c2b4d9b516608bc860a417a39d66eb99c958f36540cf931d2c2e9f80f"}, - {file = "Pint-0.22.tar.gz", hash = "sha256:2d139f6abbcf3016cad7d3cec05707fe908ac4f99cf59aedfd6ee667b7a64433"}, -] - -[package.dependencies] -typing-extensions = "*" - -[package.extras] -babel = ["babel (<=2.8)"] -dask = ["dask"] -mip = ["mip (>=1.13)"] -numpy = ["numpy (>=1.19.5)"] -pandas = ["pint-pandas (>=0.3)"] -test = ["pytest", "pytest-cov", "pytest-mpl", "pytest-subtests"] -uncertainties = ["uncertainties (>=3.1.6)"] -xarray = ["xarray"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pkgconfig" -version = "1.5.5" -description = "Interface Python with pkg-config" -category = "main" -optional = false -python-versions = ">=3.3,<4.0" -files = [ - {file = "pkgconfig-1.5.5-py3-none-any.whl", hash = "sha256:d20023bbeb42ee6d428a0fac6e0904631f545985a10cdd71a20aa58bc47a4209"}, - {file = "pkgconfig-1.5.5.tar.gz", hash = "sha256:deb4163ef11f75b520d822d9505c1f462761b4309b1bb713d08689759ea8b899"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "psutil" -version = "5.9.5" -description = "Cross-platform lib for process and system monitoring in Python." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, - {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, - {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, - {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, - {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, - {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, - {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, - {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pydantic" -version = "1.10.12" -description = "Data validation and settings management using python type hints" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, -] - -[package.dependencies] -email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pytz" -version = "2023.3" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pyvips" -version = "2.2.1" -description = "binding for the libvips image processing library, API mode" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pyvips-2.2.1.tar.gz", hash = "sha256:b51dbb45b057a282925015d540c5597560993e2986df20a778646a6b37e7cbb5"}, -] - -[package.dependencies] -cffi = ">=1.0.0" -pkgconfig = "*" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["cffi (>=1.0.0)", "pyperf", "pytest", "pytest-flake8", "pytest-runner"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "pywavelets" -version = "1.4.1" -description = "PyWavelets, wavelet transform module" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"}, - {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "scikit-image" -version = "0.19.3" -description = "Image processing in Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "scikit-image-0.19.3.tar.gz", hash = "sha256:24b5367de1762da6ee126dd8f30cc4e7efda474e0d7d70685433f0e3aa2ec450"}, - {file = "scikit_image-0.19.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:3a01372ae4bca223873304b0bff79b9d92446ac6d6177f73d89b45561e2d09d8"}, - {file = "scikit_image-0.19.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fdf48d9b1f13af69e4e2c78e05067e322e9c8c97463c315cd0ecb47a94e259fc"}, - {file = "scikit_image-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6a8f98f2ac9bb73706461fd1dec875f6a5141759ed526850a5a49e90003d19"}, - {file = "scikit_image-0.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfbb073f23deb48e0e60c47f8741d8089121d89cc78629ea8c5b51096efc5be7"}, - {file = "scikit_image-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:cc24177de3fdceca5d04807ad9c87d665f0bf01032ed94a9055cd1ed2b3f33e9"}, - {file = "scikit_image-0.19.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:fd9dd3994bb6f9f7a35f228323f3c4dc44b3cf2ff15fd72d895216e9333550c6"}, - {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad5d8000207a264d1a55681a9276e6a739d3f05cf4429004ad00d61d1892235f"}, - {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84baa3179f3ae983c3a5d81c1e404bc92dcf7daeb41bfe9369badcda3fb22b92"}, - {file = "scikit_image-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9f8a1387afc6c70f2bed007c3854a2d7489f9f7713c242f16f32ee05934bc2"}, - {file = "scikit_image-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:9fb0923a3bfa99457c5e17888f27b3b8a83a3600b4fef317992e7b7234764732"}, - {file = "scikit_image-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:ce3d2207f253b8eb2c824e30d145a9f07a34a14212d57f3beca9f7e03c383cbe"}, - {file = "scikit_image-0.19.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:2a02d1bd0e2b53e36b952bd5fd6118d9ccc3ee51de35705d63d8eb1f2e86adef"}, - {file = "scikit_image-0.19.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:03779a7e1736fdf89d83c0ba67d44110496edd736a3bfce61a2b5177a1c8a099"}, - {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19a21a101a20c587a3b611a2cf6f86c35aae9f8d9563279b987e83ee1c9a9790"}, - {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f50b923f8099c1045fcde7418d86b206c87e333e43da980f41d8577b9605245"}, - {file = "scikit_image-0.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e207c6ce5ce121d7d9b9d2b61b9adca57d1abed112c902d8ffbfdc20fb42c12b"}, - {file = "scikit_image-0.19.3-cp38-cp38-win32.whl", hash = "sha256:a7c3985c68bfe05f7571167ee021d14f5b8d1a4a250c91f0b13be7fb07e6af34"}, - {file = "scikit_image-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:651de1c2ce1fbee834753b46b8e7d81cb12a5594898babba63ac82b30ddad49d"}, - {file = "scikit_image-0.19.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8d8917fcf85b987b1f287f823f3a1a7dac38b70aaca759bc0200f3bc292d5ced"}, - {file = "scikit_image-0.19.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b0a199157ce8487c77de4fde0edc0b42d6d42818881c11f459262351d678b2d"}, - {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33dfd463ee6cc509defa279b963829f2230c9e0639ccd3931045be055878eea6"}, - {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8714348ddd671f819457a797c97d4c672166f093def66d66c3254cbd1d43f83"}, - {file = "scikit_image-0.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3b1025356508d41f4fe48528e509d95f9e4015e90cf158cd58c56dc63e0ac5"}, - {file = "scikit_image-0.19.3-cp39-cp39-win32.whl", hash = "sha256:9439e5294de3f18d6e82ec8eee2c46590231cf9c690da80545e83a0733b7a69e"}, - {file = "scikit_image-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:32fb88cc36203b99c9672fb972c9ef98635deaa5fc889fe969f3e11c44f22919"}, -] - -[package.dependencies] -imageio = ">=2.4.1" -networkx = ">=2.2" -numpy = ">=1.17.0" -packaging = ">=20.0" -pillow = ">=6.1.0,<7.1.0 || >7.1.0,<7.1.1 || >7.1.1,<8.3.0 || >8.3.0" -PyWavelets = ">=1.1.1" -scipy = ">=1.4.1" -tifffile = ">=2019.7.26" - -[package.extras] -data = ["pooch (>=1.3.0)"] -docs = ["cloudpickle (>=0.2.1)", "dask[array] (>=0.15.0,!=2.17.0)", "ipywidgets", "kaleido", "matplotlib (>=3.3)", "myst-parser", "numpydoc (>=1.0)", "pandas (>=0.23.0)", "plotly (>=4.14.0)", "pooch (>=1.3.0)", "pytest-runner", "scikit-learn", "seaborn (>=0.7.1)", "sphinx (>=1.8)", "sphinx-copybutton", "sphinx-gallery (>=0.10.1)", "tifffile (>=2020.5.30)"] -optional = ["SimpleITK", "astropy (>=3.1.2)", "cloudpickle (>=0.2.1)", "dask[array] (>=1.0.0,!=2.17.0)", "matplotlib (>=3.0.3)", "pooch (>=1.3.0)", "pyamg", "qtpy"] -test = ["asv", "codecov", "flake8", "matplotlib (>=3.0.3)", "pooch (>=1.3.0)", "pytest (>=5.2.0)", "pytest-cov (>=2.7.0)", "pytest-faulthandler", "pytest-localserver"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "scikit-learn" -version = "1.3.0" -description = "A set of python modules for machine learning and data mining" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit-learn-1.3.0.tar.gz", hash = "sha256:8be549886f5eda46436b6e555b0e4873b4f10aa21c07df45c4bc1735afbccd7a"}, - {file = "scikit_learn-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981287869e576d42c682cf7ca96af0c6ac544ed9316328fd0d9292795c742cf5"}, - {file = "scikit_learn-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:436aaaae2c916ad16631142488e4c82f4296af2404f480e031d866863425d2a2"}, - {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e28d8fa47a0b30ae1bd7a079519dd852764e31708a7804da6cb6f8b36e3630"}, - {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80c08834a473d08a204d966982a62e11c976228d306a2648c575e3ead12111"}, - {file = "scikit_learn-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:552fd1b6ee22900cf1780d7386a554bb96949e9a359999177cf30211e6b20df6"}, - {file = "scikit_learn-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79970a6d759eb00a62266a31e2637d07d2d28446fca8079cf9afa7c07b0427f8"}, - {file = "scikit_learn-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:850a00b559e636b23901aabbe79b73dc604b4e4248ba9e2d6e72f95063765603"}, - {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee04835fb016e8062ee9fe9074aef9b82e430504e420bff51e3e5fffe72750ca"}, - {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d953531f5d9f00c90c34fa3b7d7cfb43ecff4c605dac9e4255a20b114a27369"}, - {file = "scikit_learn-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:151ac2bf65ccf363664a689b8beafc9e6aae36263db114b4ca06fbbbf827444a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a885a9edc9c0a341cab27ec4f8a6c58b35f3d449c9d2503a6fd23e06bbd4f6a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9877af9c6d1b15486e18a94101b742e9d0d2f343d35a634e337411ddb57783f3"}, - {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c470f53cea065ff3d588050955c492793bb50c19a92923490d18fcb637f6383a"}, - {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6e2d7389542eae01077a1ee0318c4fec20c66c957f45c7aac0c6eb0fe3c612"}, - {file = "scikit_learn-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a11936adbc379a6061ea32fa03338d4ca7248d86dd507c81e13af428a5bc1db"}, - {file = "scikit_learn-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:998d38fcec96584deee1e79cd127469b3ad6fefd1ea6c2dfc54e8db367eb396b"}, - {file = "scikit_learn-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ded35e810438a527e17623ac6deae3b360134345b7c598175ab7741720d7ffa7"}, - {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8102d5036e28d08ab47166b48c8d5e5810704daecf3a476a4282d562be9a28"}, - {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7617164951c422747e7c32be4afa15d75ad8044f42e7d70d3e2e0429a50e6718"}, - {file = "scikit_learn-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1d54fb9e6038284548072df22fd34777e434153f7ffac72c8596f2d6987110dd"}, -] - -[package.dependencies] -joblib = ">=1.1.1" -numpy = ">=1.17.3" -scipy = ">=1.5.0" -threadpoolctl = ">=2.0.0" - -[package.extras] -benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "scipy" -version = "1.11.1" -description = "Fundamental algorithms for scientific computing in Python" -category = "main" -optional = false -python-versions = "<3.13,>=3.9" -files = [ - {file = "scipy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aec8c62fbe52914f9cf28d846cf0401dd80ab80788bbab909434eb336ed07c04"}, - {file = "scipy-1.11.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3b9963798df1d8a52db41a6fc0e6fa65b1c60e85d73da27ae8bb754de4792481"}, - {file = "scipy-1.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e8eb42db36526b130dfbc417609498a6192381abc1975b91e3eb238e0b41c1a"}, - {file = "scipy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:366a6a937110d80dca4f63b3f5b00cc89d36f678b2d124a01067b154e692bab1"}, - {file = "scipy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:08d957ca82d3535b3b9ba6c8ff355d78fe975271874e2af267cb5add5bd78625"}, - {file = "scipy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:e866514bc2d660608447b6ba95c8900d591f2865c07cca0aa4f7ff3c4ca70f30"}, - {file = "scipy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba94eeef3c9caa4cea7b402a35bb02a5714ee1ee77eb98aca1eed4543beb0f4c"}, - {file = "scipy-1.11.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:512fdc18c65f76dadaca139348e525646d440220d8d05f6d21965b8d4466bccd"}, - {file = "scipy-1.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cce154372f0ebe88556ed06d7b196e9c2e0c13080ecb58d0f35062dc7cc28b47"}, - {file = "scipy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4bb943010203465ac81efa392e4645265077b4d9e99b66cf3ed33ae12254173"}, - {file = "scipy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:249cfa465c379c9bb2c20123001e151ff5e29b351cbb7f9c91587260602c58d0"}, - {file = "scipy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:ffb28e3fa31b9c376d0fb1f74c1f13911c8c154a760312fbee87a21eb21efe31"}, - {file = "scipy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:39154437654260a52871dfde852adf1b93b1d1bc5dc0ffa70068f16ec0be2624"}, - {file = "scipy-1.11.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:b588311875c58d1acd4ef17c983b9f1ab5391755a47c3d70b6bd503a45bfaf71"}, - {file = "scipy-1.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d51565560565a0307ed06fa0ec4c6f21ff094947d4844d6068ed04400c72d0c3"}, - {file = "scipy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b41a0f322b4eb51b078cb3441e950ad661ede490c3aca66edef66f4b37ab1877"}, - {file = "scipy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:396fae3f8c12ad14c5f3eb40499fd06a6fef8393a6baa352a652ecd51e74e029"}, - {file = "scipy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:be8c962a821957fdde8c4044efdab7a140c13294997a407eaee777acf63cbf0c"}, - {file = "scipy-1.11.1.tar.gz", hash = "sha256:fb5b492fa035334fd249f0973cc79ecad8b09c604b42a127a677b45a9a3d4289"}, -] - -[package.dependencies] -numpy = ">=1.21.6,<1.28.0" - -[package.extras] -dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "scyjava" -version = "1.9.1" -description = "Supercharged Java access from Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "scyjava-1.9.1-py3-none-any.whl", hash = "sha256:92de163eab66a14eebadfd8c00a0484da51689301ea9c7145a1533a3f29ecf4d"}, - {file = "scyjava-1.9.1.tar.gz", hash = "sha256:b305327386e51d7f5af984c42793fcc5f89e0a82d2e6ecb45f5fe9467fc0cbe3"}, -] - -[package.dependencies] -jgo = "*" -jpype1 = ">=1.3.0" - -[package.extras] -dev = ["autopep8", "black", "build", "flake8", "flake8-pyproject", "flake8-typing-imports", "isort", "jep", "numpy", "pandas", "pytest", "pytest-cov", "toml", "validate-pyproject[all]"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "setuptools" -version = "68.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-68.1.0-py3-none-any.whl", hash = "sha256:e13e1b0bc760e9b0127eda042845999b2f913e12437046e663b833aa96d89715"}, - {file = "setuptools-68.1.0.tar.gz", hash = "sha256:d59c97e7b774979a5ccb96388efc9eb65518004537e85d52e81eaee89ab6dd91"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "shapely" -version = "2.0.1" -description = "Manipulation and analysis of geometric objects" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b06d031bc64149e340448fea25eee01360a58936c89985cf584134171e05863f"}, - {file = "shapely-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a6ac34c16f4d5d3c174c76c9d7614ec8fe735f8f82b6cc97a46b54f386a86bf"}, - {file = "shapely-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:865bc3d7cc0ea63189d11a0b1120d1307ed7a64720a8bfa5be2fde5fc6d0d33f"}, - {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45b4833235b90bc87ee26c6537438fa77559d994d2d3be5190dd2e54d31b2820"}, - {file = "shapely-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce88ec79df55430e37178a191ad8df45cae90b0f6972d46d867bf6ebbb58cc4d"}, - {file = "shapely-2.0.1-cp310-cp310-win32.whl", hash = "sha256:01224899ff692a62929ef1a3f5fe389043e262698a708ab7569f43a99a48ae82"}, - {file = "shapely-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:da71de5bf552d83dcc21b78cc0020e86f8d0feea43e202110973987ffa781c21"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:502e0a607f1dcc6dee0125aeee886379be5242c854500ea5fd2e7ac076b9ce6d"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7d3bbeefd8a6a1a1017265d2d36f8ff2d79d0162d8c141aa0d37a87063525656"}, - {file = "shapely-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f470a130d6ddb05b810fc1776d918659407f8d025b7f56d2742a596b6dffa6c7"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4641325e065fd3e07d55677849c9ddfd0cf3ee98f96475126942e746d55b17c8"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90cfa4144ff189a3c3de62e2f3669283c98fb760cfa2e82ff70df40f11cadb39"}, - {file = "shapely-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70a18fc7d6418e5aea76ac55dce33f98e75bd413c6eb39cfed6a1ba36469d7d4"}, - {file = "shapely-2.0.1-cp311-cp311-win32.whl", hash = "sha256:09d6c7763b1bee0d0a2b84bb32a4c25c6359ad1ac582a62d8b211e89de986154"}, - {file = "shapely-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d8f55f355be7821dade839df785a49dc9f16d1af363134d07eb11e9207e0b189"}, - {file = "shapely-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:83a8ec0ee0192b6e3feee9f6a499d1377e9c295af74d7f81ecba5a42a6b195b7"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a529218e72a3dbdc83676198e610485fdfa31178f4be5b519a8ae12ea688db14"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91575d97fd67391b85686573d758896ed2fc7476321c9d2e2b0c398b628b961c"}, - {file = "shapely-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8b0d834b11be97d5ab2b4dceada20ae8e07bcccbc0f55d71df6729965f406ad"}, - {file = "shapely-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:b4f0711cc83734c6fad94fc8d4ec30f3d52c1787b17d9dca261dc841d4731c64"}, - {file = "shapely-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:05c51a29336e604c084fb43ae5dbbfa2c0ef9bd6fedeae0a0d02c7b57a56ba46"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b519cf3726ddb6c67f6a951d1bb1d29691111eaa67ea19ddca4d454fbe35949c"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193a398d81c97a62fc3634a1a33798a58fd1dcf4aead254d080b273efbb7e3ff"}, - {file = "shapely-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e55698e0ed95a70fe9ff9a23c763acfe0bf335b02df12142f74e4543095e9a9b"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32a748703e7bf6e92dfa3d2936b2fbfe76f8ce5f756e24f49ef72d17d26ad02"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a34a23d6266ca162499e4a22b79159dc0052f4973d16f16f990baa4d29e58b6"}, - {file = "shapely-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173d24e85e51510e658fb108513d5bc11e3fd2820db6b1bd0522266ddd11f51"}, - {file = "shapely-2.0.1-cp38-cp38-win32.whl", hash = "sha256:3cb256ae0c01b17f7bc68ee2ffdd45aebf42af8992484ea55c29a6151abe4386"}, - {file = "shapely-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c7eed1fb3008a8a4a56425334b7eb82651a51f9e9a9c2f72844a2fb394f38a6c"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac1dfc397475d1de485e76de0c3c91cc9d79bd39012a84bb0f5e8a199fc17bef"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33403b8896e1d98aaa3a52110d828b18985d740cc9f34f198922018b1e0f8afe"}, - {file = "shapely-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2569a4b91caeef54dd5ae9091ae6f63526d8ca0b376b5bb9fd1a3195d047d7d4"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a70a614791ff65f5e283feed747e1cc3d9e6c6ba91556e640636bbb0a1e32a71"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43755d2c46b75a7b74ac6226d2cc9fa2a76c3263c5ae70c195c6fb4e7b08e79"}, - {file = "shapely-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad81f292fffbd568ae71828e6c387da7eb5384a79db9b4fde14dd9fdeffca9a"}, - {file = "shapely-2.0.1-cp39-cp39-win32.whl", hash = "sha256:b50c401b64883e61556a90b89948297f1714dbac29243d17ed9284a47e6dd731"}, - {file = "shapely-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bca57b683e3d94d0919e2f31e4d70fdfbb7059650ef1b431d9f4e045690edcd5"}, - {file = "shapely-2.0.1.tar.gz", hash = "sha256:66a6b1a3e72ece97fc85536a281476f9b7794de2e646ca8a4517e2e3c1446893"}, -] - -[package.dependencies] -numpy = ">=1.14" - -[package.extras] -docs = ["matplotlib", "numpydoc (>=1.1.0,<1.2.0)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] -test = ["pytest", "pytest-cov"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "simpleitk" -version = "2.2.1" -description = "SimpleITK is a simplified interface to the Insight Toolkit (ITK) for image registration and segmentation" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "SimpleITK-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f386a83c20e70741a3e3f096a60ef6272d07e76ab0117dbea428db81581335cc"}, - {file = "SimpleITK-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:00c1776185c339b50b4081d082d19124f479ded4f7153ffc2f9354761269b459"}, - {file = "SimpleITK-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5419582bc7a7a7f4b5d2540666c215800e388e8b99d1ee1c7c714cc009a7242a"}, - {file = "SimpleITK-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f963a715eee2aed95cc9a0b7066231b0944f85e99f65cc0e216a15a291c4320"}, - {file = "SimpleITK-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:740a8f1628e9f8f974acaf2ab49bd153fc4ea15db94ca4765e6c18edb2c86681"}, - {file = "SimpleITK-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:beb2081b912fae321be4c060ca2ff105edddf83bf6d5d3bd833f1801d23b773d"}, - {file = "SimpleITK-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:498fedec0d12671933c4861b0674a2435df18a840abaef2e0810d08e3f9686cd"}, - {file = "SimpleITK-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea705139755f1ef234239caa72bec400b8babf3c5b83d2164d68afaa18b61a2b"}, - {file = "SimpleITK-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a19d1fadeb1c7ad6454e3a0b5dc375bcca8d2d84b25d9ca5bd9897cc26d7e22"}, - {file = "SimpleITK-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5da94685c6c082e567ac699786065b8b834c7c1fc92fbd7b2822e9f8e251b066"}, - {file = "SimpleITK-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d2615c8f87e2cfa215638ad6ab719215f8dc2981537da9528046d6c8bfc9d05d"}, - {file = "SimpleITK-2.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97f85a0a51e5aaf1cc46318a039513081ebc74ccfb9030d03d2cfbca97844317"}, - {file = "SimpleITK-2.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0f725a44974c697fec57137bda70a4801b8b3ffd0aa2c180b67452cd72093b"}, - {file = "SimpleITK-2.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4878f5c032d638d7833901d8d3f79d4c6ed90d041d1fcc1b074dd6e4b7c9abc2"}, - {file = "SimpleITK-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:565723d39b836d91fcc1e78eb4e494c9dd22e2392df44548c64617656769f095"}, - {file = "SimpleITK-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a2eafedbf99874019e00dfd2b5e94e15a7cf4c17f12d194678fc10c277f4d4c"}, - {file = "SimpleITK-2.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b599f115cc6fcaf4222c75de47a1d9bb45a9e8aea554c532d5f1b2a61ff2695"}, - {file = "SimpleITK-2.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e035e488880b10139680d6a796fab77735d6a868f34977f5ea9145b5bd88938"}, - {file = "SimpleITK-2.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:56863d90ea3bddae86a83120a98b803007240a22c8de299946208327089c7581"}, - {file = "SimpleITK-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e032bb2e54b96ce426187f07ce5dd9733077b18005399f6fbbcd4765b386299f"}, - {file = "SimpleITK-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ffec97db30f923788f4e19b5a6e708d68afcce8f89cb54d7c976bd099b9a580"}, - {file = "SimpleITK-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6f58cdf90dbd0b84be68e0b46545c92f28c650d86e9f24acb91ad1c27416357"}, - {file = "SimpleITK-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b90dfd94cba0b068bacddaf2550d96df2683df830b9ee71cd9440e64c701196"}, - {file = "SimpleITK-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:7e20bbc467a8fff15978fee34a97c851ec3d0caed2d01929f600a13a0ff2f955"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "soupsieve" -version = "2.4.1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "sympy" -version = "1.12" -description = "Computer algebra system (CAS) in Python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, - {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, -] - -[package.dependencies] -mpmath = ">=0.19" - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "tempita" -version = "0.5.2" -description = "A very small text templating language" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "Tempita-0.5.2-py3-none-any.whl", hash = "sha256:f4554840cb59c6b4a5df4fad27eea4e3cb47ca7089bfeefb5890ff1bb8af2117"}, - {file = "Tempita-0.5.2.tar.gz", hash = "sha256:cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "threadpoolctl" -version = "3.2.0" -description = "threadpoolctl" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "threadpoolctl-3.2.0-py3-none-any.whl", hash = "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032"}, - {file = "threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "tifffile" -version = "2023.8.12" -description = "Read and write TIFF files" -category = "main" -optional = false -python-versions = ">=3.9" -files = [ - {file = "tifffile-2023.8.12-py3-none-any.whl", hash = "sha256:d1ef06461a947a6800ba6121b330b54a57fb9cbf7e5bc0adab8307081297d66b"}, - {file = "tifffile-2023.8.12.tar.gz", hash = "sha256:824956b6d974b9d346aae59932bea862a2ad18fcc2b1a820b6941b7f6ddb2bca"}, -] - -[package.dependencies] -numpy = "*" - -[package.extras] -all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib", "zarr"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "torch" -version = "2.0.1+cpu" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -category = "main" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "torch-2.0.1+cpu-cp310-cp310-linux_x86_64.whl", hash = "sha256:fec257249ba014c68629a1994b0c6e7356e20e1afc77a87b9941a40e5095285d"}, - {file = "torch-2.0.1+cpu-cp310-cp310-win_amd64.whl", hash = "sha256:ca88b499973c4c027e32c4960bf20911d7e984bd0c55cda181dc643559f3d93f"}, - {file = "torch-2.0.1+cpu-cp311-cp311-linux_x86_64.whl", hash = "sha256:274d4acf486ef50ce1066ffe9d500beabb32bde69db93e3b71d0892dd148956c"}, - {file = "torch-2.0.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:e2603310bdff4b099c4c41ae132192fc0d6b00932ae2621d52d87218291864be"}, - {file = "torch-2.0.1+cpu-cp38-cp38-linux_x86_64.whl", hash = "sha256:8046f49deae5a3d219b9f6059a1f478ae321f232e660249355a8bf6dcaa810c1"}, - {file = "torch-2.0.1+cpu-cp38-cp38-win_amd64.whl", hash = "sha256:2ac4382ff090035f9045b18afe5763e2865dd35f2d661c02e51f658d95c8065a"}, - {file = "torch-2.0.1+cpu-cp39-cp39-linux_x86_64.whl", hash = "sha256:73482a223d577407c45685fde9d2a74ba42f0d8d9f6e1e95c08071dc55c47d7b"}, - {file = "torch-2.0.1+cpu-cp39-cp39-win_amd64.whl", hash = "sha256:f263f8e908288427ae81441fef540377f61e339a27632b1bbe33cf78292fdaea"}, -] - -[package.dependencies] -filelock = "*" -jinja2 = "*" -networkx = "*" -sympy = "*" -typing-extensions = "*" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] - -[package.source] -type = "legacy" -url = "https://download.pytorch.org/whl/cpu" -reference = "torchcpu201" - -[[package]] -name = "tqdm" -version = "4.66.1" -description = "Fast, Extensible Progress Meter" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "weightedstats" -version = "0.4.1" -description = "Mean, weighted mean, median, weighted median" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "weightedstats-0.4.1-py2-none-any.whl", hash = "sha256:5633991d01864dca581816da3070eed95fb3671020937a8dbad7afab4a38ef0c"}, - {file = "weightedstats-0.4.1-py3-none-any.whl", hash = "sha256:6ead0c27df10b0598d7e3a1c2bc201b925f5ac47099df0dafccce91932a5d155"}, - {file = "weightedstats-0.4.1.tar.gz", hash = "sha256:beb488a3f46aa06dbc8491578ec7e408847ca682edc7ec90846f6df9e36cab50"}, -] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "xmlschema" -version = "2.4.0" -description = "An XML Schema validator and decoder" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "xmlschema-2.4.0-py3-none-any.whl", hash = "sha256:dc87be0caaa61f42649899189aab2fd8e0d567f2cf548433ba7b79278d231a4a"}, - {file = "xmlschema-2.4.0.tar.gz", hash = "sha256:d74cd0c10866ac609e1ef94a5a69b018ad16e39077bc6393408b40c6babee793"}, -] - -[package.dependencies] -elementpath = ">=4.1.5,<5.0.0" - -[package.extras] -codegen = ["elementpath (>=4.1.5,<5.0.0)", "jinja2"] -dev = ["Sphinx", "coverage", "elementpath (>=4.1.5,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] -docs = ["Sphinx", "elementpath (>=4.1.5,<5.0.0)", "jinja2", "sphinx-rtd-theme"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[[package]] -name = "zipp" -version = "3.16.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://pypi.org/simple" -reference = "pypi-public" - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9,<3.11" -content-hash = "a49f202e41a05c289f4e27113094ab17af4a4c7e54a7e63fa42973e043410486" diff --git a/examples/acrobat_2023/pyproject.toml b/examples/acrobat_2023/pyproject.toml deleted file mode 100644 index 8523ee59..00000000 --- a/examples/acrobat_2023/pyproject.toml +++ /dev/null @@ -1,50 +0,0 @@ -[tool.poetry] -name = "valis-wsi" -version = "1.0.0rc16" -description = "" -authors = ["Chandler Gatenbee "] -license = "MIT" -readme = "README.rst" -packages = [{include = "valis"}] - -[tool.poetry.dependencies] -python = ">=3.9,<3.11" -beautifulsoup4 = "^4.11.1" -scyjava = "^1.8.1" -colorama = "^0.4.6" -colour-science = "^0.4.2" -fastcluster = "^1.2.6" -interpolation = "^2.2.4" -joblib = "^1.2.0" -jpype1 = "^1.4.1" -matplotlib = "^3.6.3" -numba = "^0.56.4" -numpy = "<1.24" -ome-types = "^0.3.2" -opencv-contrib-python-headless = "4.6.0.66" -pandas = "^1.5.2" -pillow = "^9.4.0" -pyvips = "^2.2.1" -scikit-image = "^0.19.3" -scikit-learn = "^1.2.0" -scipy = "^1.10.0" -shapely = "^2.0.0" -simpleitk = "^2.2.1" -tqdm = "^4.64.1" -weightedstats = "^0.4.1" -aicspylibczi = "^3.1.2" -torch = {version = "2.0.1", source = "torchcpu201"} - -[build-system] -requires = ["poetry-core", "setuptools>=57.0.0", "Cython>=0.29.27"] -build-backend = "poetry.core.masonry.api" - -[[tool.poetry.source]] -name = "pypi-public" -url = "https://pypi.org/simple/" -default = true - -[[tool.poetry.source]] -name = "torchcpu201" -url = "https://download.pytorch.org/whl/cpu" - diff --git a/examples/acrobat_2023/valis/__init__.py b/examples/acrobat_2023/valis/__init__.py deleted file mode 100644 index d9c3e712..00000000 --- a/examples/acrobat_2023/valis/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -__version__ = "1.0.0rc16" - -from . import affine_optimizer -from . import feature_detectors -from . import feature_matcher -from . import non_rigid_registrars -from . import preprocessing -from . import registration -from . import serial_non_rigid -from . import serial_rigid -from . import slide_io -from . import slide_tools -from . import valtils -from . import viz -from . import warp_tools -from . import micro_rigid_registrar - -__all__ = ["affine_optimizer", - "feature_detectors", - "feature_matcher", - "non_rigid_registrars", - "preprocessing", - "registration", - "serial_non_rigid", - "serial_rigid", - "slide_io", - "slide_tools", - "valtils", - "viz", - "warp_tools", - "micro_rigid_registrar" - ] \ No newline at end of file diff --git a/examples/acrobat_2023/valis/affine_optimizer.py b/examples/acrobat_2023/valis/affine_optimizer.py deleted file mode 100644 index bede3e79..00000000 --- a/examples/acrobat_2023/valis/affine_optimizer.py +++ /dev/null @@ -1,1118 +0,0 @@ -"""Optimize rigid alignment - -Contains functions related to optimization, as well as the AffineOptimizer -class that performs the optimzation. This class can be subclassed to implement -custom optimization methods. - -There are several subclasses, but AffineOptimizerMattesMI is the -the fastest and most accurate, and so is default affine optimizer in VALIS. -It's not recommended that the other subclasses be used, but they are kept -to provide examples on how to subclass AffineOptimizer. -""" - -from scipy import ndimage, optimize -import numba as nba -import numpy as np -from skimage import transform, util -import cv2 -import os -import SimpleITK as sitk -from scipy import interpolate -import pathlib -from . warp_tools import get_affine_transformation_params, \ - get_corners_of_image, warp_xy - -# Cost functions # -EPS = np.finfo("float").eps - - -def mse(arr1, arr2, mask=None): - """Compute the mean squared error between two arrays.""" - - if mask is None: - return np.mean((arr1 - arr2)**2) - else: - return np.mean((arr1[mask != 0] - arr2[mask != 0]) ** 2) - - -def displacement(moving_image, target_image, mask=None): - """Minimize average displacement between moving_image and target_image - """ - - opt_flow = cv2.optflow.createOptFlow_DeepFlow() - flow = opt_flow.calc(util.img_as_ubyte(target_image), - util.img_as_ubyte(moving_image), None) - if mask is not None: - dx = flow[..., 0][mask != 0] - dy = flow[..., 1][mask != 0] - else: - dx = flow[..., 0].reshape(-1) - dy = flow[..., 1].reshape(-1) - - mean_displacement = np.mean(np.sqrt(dx**2 + dy**2)) - return mean_displacement - - -def cost_mse(param, reference_image, target_image, mask=None): - transformation = make_transform(param) - transformed = transform.warp(target_image, transformation, order=3) - return mse(reference_image, transformed, mask) - - -def downsample2x(image): - """Down sample image. - """ - - offsets = [((s + 1) % 2) / 2 for s in image.shape] - slices = [slice(offset, end, 2) - for offset, end in zip(offsets, image.shape)] - coords = np.mgrid[slices] - return ndimage.map_coordinates(image, coords, order=1) - - -def gaussian_pyramid(image, levels=6): - """Make a Gaussian image pyramid. - - Parameters - ---------- - image : array of float - The input image. - max_layer : int, optional - The number of levels in the pyramid. - - Returns - ------- - pyramid : iterator of array of float - An iterator of Gaussian pyramid levels, starting with the top - (lowest resolution) level. - """ - pyramid = [image] - - for level in range(levels - 1): - image = downsample2x(image) - pyramid.append(image) - - return pyramid - - -def make_transform(param): - if len(param) == 3: - r, tc, tr = param - s = None - else: - r, tc, tr, s = param - - return transform.SimilarityTransform(rotation=r, - translation=(tc, tr), - scale=s) - - -@nba.njit() -def bin_image(img, p): - x_min = np.min(img) - x_max_ = np.max(img) - x_range = x_max_ - x_min + EPS - binned_img = np.zeros_like(img) - _bins = p * (1 - EPS) # Keeps right bin closed - for i in range(img.shape[0]): - for j in range(img.shape[1]): - binned_img[i, j] = int(_bins * ((img[i, j] - x_min) / (x_range))) - - return binned_img - - -@nba.njit() -def solve_abc(verts): - """ - Find coefficients A,B,C that will allow estimation of intesnity of point - inside triangle with vertices v0, v1, v2. Each vertex is in the format of - [x,y,z] were z=intensity of pixel at point x,y - - Parameters - ---------- - verts : 3x3 array - Each row has coordinates x,y and z, where z in the image intensiy at - point xy (i.e. image[y, r]) - - Returns - ------- - abc : [A,B,C] - Coefficients to estimate intensity in triangle, as well as the - intersection of isointensity lines - - """ - a = np.array([[verts[0, 0], verts[0, 1], 1], - [verts[1, 0], verts[1, 1], 1], - [verts[2, 0], verts[2, 1], 1]]) - b = verts[:, 2] - - try: - abc = np.linalg.inv(a) @ b - except np.linalg.LinAlgError: - sln = np.linalg.lstsq(a, b) - abc = sln[0] - - return abc - - -@nba.njit() -def area(x1, y1, x2, y2, x3, y3): - # From https://www.geeksforgeeks.org/check-whether-a-given-point-lies-inside-a-triangle-or-not/ - a = np.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0) - return a - - -@nba.njit() -def isInside(x1, y1, x2, y2, x3, y3, x, y): - # Calculate area of triangle ABC - A = area(x1, y1, x2, y2, x3, y3) - # A = np.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2.0) - # Calculate area of triangle PBC - A1 = area(x, y, x2, y2, x3, y3) - - # Calculate area of triangle PAC - A2 = area(x1, y1, x, y, x3, y3) - - # Calculate area of triangle PAB - A3 = area(x1, y1, x2, y2, x, y) - - # print(A, A1, A2, A3) - # print(A == (A1 + A2 + A3)) - - # Check if sum of A1, A2 and A3 - # is same as A - if (A == (A1 + A2 + A3)): - return 1 - else: - return 0 - - -@nba.njit() -def get_intersection(alpha1, alpha2, abc1, abc2): - """ - - Parameters - ---------- - alpha1 : float - Intensity of point in image 1 - - alpha2 : float - Intensity of point in image 2 - - abc1: [A,B,C] - Coefficients to interpolate value for triangle in image1 - - abc2: [A,B,C] - Coefficients to interpolate value for corresponding triangle in image2 - - """ - # Find interestion of isointensity lines ### - intensities = np.array([alpha1 - abc1[2], alpha2 - abc2[2]]) - coef = np.array([[abc1[0], abc1[1]], - [abc2[0], abc2[1]] - ]) - try: - xy = np.linalg.inv(coef) @ intensities - except np.linalg.LinAlgError: - sln = np.linalg.lstsq(coef, intensities) - xy = sln[0] - return xy - - -@nba.njit() -def get_verts(img, x, y, pos=0): - """ - Get veritices of triangle and intenisty at each vertex - """ - if pos == 0: - # Lower left - verts = np.array([[x, y, img[y, x]], # BL - [x + 1, y, img[y, x + 1]], # BR - [x, y + 1, img[y + 1, x]] # TL - ]) - if pos == 1: - # Upper right - verts = np.array([[x, y+1, img[y+1, x]], # BL - [x + 1, y, img[y, x + 1]], # BR - [x+1, y + 1, img[y + 1, x + 1]] # TL - ]) - - return verts - - -@nba.njit() -def hist2d(x, y, n_bins): - """ - Build 2D histogram by determining the bin each x and y value falls in - https://stats.stackexchange.com/questions/236205/programmatically-calculate-which-bin-a-value-will-fall-into-for-a-histogram - """ - - x_min = np.min(x) - x_max_ = np.max(x) - x_range = x_max_ - x_min + EPS - - y_min = np.min(y) - y_max = np.max(y) - y_range = y_max - y_min + EPS - - _bins = n_bins * (1 - EPS) # Keeps right bin closed - x_margins = np.zeros(n_bins) - y_margins = np.zeros(n_bins) - results = np.zeros((n_bins, n_bins)) - for i in range(len(x)): - x_bin = int(_bins*((x[i]-x_min)/(x_range))) - y_bin = int(_bins*((y[i] - y_min) / (y_range))) - - x_margins[x_bin] += 1 - y_margins[y_bin] += 1 - results[x_bin, y_bin] += 1 - - return results, x_margins, y_margins - - -@nba.njit() -def update_joint_H(binned_moving, binned_fixed, H, M, sample_pts, pos=0, - precalcd_abc=None): - - q = H.shape[0] - for i, sxy in enumerate(sample_pts): - # Get vertices and intensities in each image. - # Note that indices are as rc, but vertices need to be xy - img1_v = get_verts(binned_moving, sxy[0], sxy[1], pos) - abc1 = solve_abc(img1_v) - - if precalcd_abc is None: - img2_v = get_verts(binned_fixed, sxy[0], sxy[1], pos) - abc2 = solve_abc(img2_v) - else: - # ABC for fixed image's trianges are precomputed - abc2 = precalcd_abc[i] - - x_lims = np.array([np.min(img1_v[:, 0]), np.max(img1_v[:, 0])]) - y_lims = np.array([np.min(img1_v[:, 1]), np.max(img1_v[:, 1])]) - for alpha1 in range(0, q): - for alpha2 in range(0, q): - xy = get_intersection(alpha1, alpha2, abc1, abc2) - if xy[0] <= x_lims[0] or xy[0] >= x_lims[1] or \ - xy[1] <= y_lims[0] or xy[1] >= y_lims[1]: - continue - - # Determine if intersection inside triangle ### - vote = isInside(img1_v[0, 0], img1_v[0, 1], - img1_v[1, 0], img1_v[1, 1], - img1_v[2, 0], img1_v[2, 1], - xy[0], xy[1]) - - H[alpha1, alpha2] += vote - - return H - - -@nba.jit() -def get_neighborhood(im, i, j, r): - """ - Get values in a neighborhood - """ - - return im[i - r:i + r + 1, j - r:j + r + 1].flatten() - - -@nba.jit() -def build_P(A, B, r, mask): - hood_size = (2 * r + 1) ** 2 - d = 2 * hood_size - N = (A.shape[0] - 2*r)*(A.shape[1] - 2*r) - P = np.zeros((d, N)) - - idx = 0 - for i in range(r, A.shape[0]): - # Skip borders - if i < r or i > A.shape[0] - r - 1: - continue - for j in range(r, A.shape[1]): - pmask = get_neighborhood(mask, i, j, r) - if j < r or j > A.shape[1] - r - 1 or np.min(pmask) == 0: - continue - - pa = get_neighborhood(A, i, j, r) - pb = get_neighborhood(B, i, j, r) - - P[:hood_size, idx] = pa - P[hood_size:, idx] = pb - - idx += 1 - - return P[:, :idx] - - -@nba.njit() -def entropy(x): - """ - Caclulate Shannon's entropy for array x - - Parameters - ---------- - x : array - Array from which to calculate entropy - - Returns - ------- - h : float - Shannon's entropy - """ - # x += EPS ## Avoid -Inf if there is log(0) - px = x/np.sum(x) - px = px[px > 0] - h = -np.sum(px * np.log(px)) - return h - - -@nba.njit() -def entropy_from_c(cov_mat, d): - e = np.log(((2*np.pi*np.e) ** (d/2)) * - (np.linalg.det(cov_mat) ** 0.5) + EPS) - return e - - -@nba.njit() -def region_mi(A, B, mask, r=4): - P = build_P(A, B, r, mask) # d x N matrix: N points with d dimensions - - # Center points so each dimensions is around 0 - C = np.cov(P, rowvar=True, bias=True) - hood_size = (2 * r + 1) ** 2 - d = hood_size*2 - HA = entropy_from_c(C[0:hood_size, 0:hood_size], d) - HB = entropy_from_c(C[hood_size:, hood_size:], d) - HC = entropy_from_c(C, d) - - RMI = HA + HB - HC - if RMI < 0: - RMI = 0 - - return RMI - - -@nba.njit() -def normalized_mutual_information(A, B, mask, n_bins=256): - """ - Build 2D histogram by determining the bin each x and y value falls in - https://stats.stackexchange.com/questions/236205/programmatically-calculate-which-bin-a-value-will-fall-into-for-a-histogram - """ - - x_min = np.min(A) - x_max_ = np.max(A) - x_range = x_max_ - x_min + EPS - - y_min = np.min(B) - y_max = np.max(B) - y_range = y_max - y_min + EPS - - _bins = n_bins * (1 - EPS) # Keeps right bin closed - x_margins = np.zeros(n_bins) - y_margins = np.zeros(n_bins) - results = np.zeros((n_bins, n_bins)) - - for i in range(A.shape[0]): - for j in range(A.shape[1]): - if mask[i, j] == 0: - continue - - x = A[i, j] - y = B[i, j] - - x_bin = int(_bins * ((x - x_min) / x_range)) - y_bin = int(_bins * ((y - y_min) / y_range)) - - x_margins[x_bin] += 1 - y_margins[y_bin] += 1 - results[x_bin, y_bin] += 1 - - n = np.sum(results) - results /= n - x_margins /= n - y_margins /= n - - H_A = entropy(x_margins) - H_B = entropy(y_margins) - H_AB = entropy(results.flatten()) - MI = (H_A + H_B) / H_AB - if MI < 0: - MI = 0 - - return MI - - -def sample_img(img, spacing=10): - sr, sc = np.meshgrid(np.arange(0, img.shape[0], spacing), np.arange(0, img.shape[1], spacing)) - sample_r = sr.reshape(-1) + np.random.uniform(0, spacing/2, sr.size) - sample_c = sc.reshape(-1) + np.random.uniform(0, spacing/2, sc.size) - interp = interpolate.RectBivariateSpline(np.arange(0, img.shape[0]), np.arange(0, img.shape[1]), img) - z = np.array([interp(sample_r[i], sample_c[i])[0][0] for i in range(len(sample_c))]) - return z[(0 <= z) & (z <= img.max())] - - -def MI(fixed, moving, nb, spacing): - fixed_sampled = sample_img(fixed, spacing) - moving_sampled = sample_img(moving, spacing) - results, x_margins, y_margins = hist2d(moving_sampled, fixed_sampled, nb) - - n = np.sum(results) - results /= n - x_margins /= n - y_margins /= n - - H_A = entropy(x_margins) - H_B = entropy(y_margins) - H_AB = entropy(results.flatten()) - MI = (H_A + H_B) / H_AB - if MI < 0: - MI = 0 - - return MI - - -class AffineOptimizer(object): - """Class that optimizes ridid registration - - Attributes - ---------- - nlevels : int - Number of levels in the Gaussian pyramid - - nbins : int - Number of bins to have in histograms used to estimate mutual information - - optimization : str - Optimization method. Can be any method from scipy.optimize - "FuzzyPSO" for Fuzzy Self-Tuning PSO in the fst-pso package (https://pypi.org/project/fst-pso/) - "gp_minimize", "forest_minimize", "gbrt_minimize" from scikit-opt - - transformation : str - Type of transformation, "EuclideanTransform" or "SimilarityTransform" - - current_level : int - Current level of the Guassian pyramid that is being registered - - accepts_xy : bool - Bool declaring whether or not the optimizer will use corresponding points to optimize the registration - - Methods - ------- - setup(moving, fixed, mask, initial_M=None) - Gets images ready for alignment - - cost_fxn(fixed_image, transformed, mask) - Calculates metric that is to be minimized - - align(moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None) - Align images by minimizing cost_fxn - - - Notes - ----- - All AffineOptimizer subclasses need to have the method align(moving, fixed, mask, initial_M, moving_xy, fixed_xy) - that returns the aligned image, optimal_M, cost_list - - AffineOptimizer subclasses must also have a cost_fxn(fixed_image, transformed, mask) method that - returns the registration metric value - - If one wants to use the same optimization methods, but a different cost function, then the subclass only needs - to have a new cost_fxn method. See AffineOptimizerDisplacement for an example implementing a new cost function - - Major overhauls are possible too. See AffineOptimizerMattesMI for an example on using SimpleITK's - optimization methods inside of an AffineOptimizer subclass - - If the optimizer uses corressponding points, then the class attribute - accepts_xy needs to be set to True. The default is False. - - """ - accepts_xy = False - - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transformation="EuclideanTransform"): - """AffineOptimizer registers moving and fixed images by minimizing a cost function - - Parameters - ---------- - nlevels : int - Number of levels in the Gaussian pyramid - - nbins : int - Number of bins to have in histograms used to estimate mutual information - - optimization : str - Optimization method. Can be any method from scipy.optimize - - transformation : str - Type of transformation, "EuclideanTransform" or "SimilarityTransform" - """ - - self.nlevels = nlevels - self.nbins = nbins - self.optimization = optimization - self.transformation = transformation - self.current_level = nlevels - 1 - self.accepts_xy = AffineOptimizer.accepts_xy - - def setup(self, moving, fixed, mask, initial_M=None): - """Get images ready for alignment - - Parameters - ---------- - - moving : ndarray - Image to warp to align with fixed - - fixed : ndarray - Image moving is warped to align to - - mask : ndarray - 2D array having non-zero pixel values, where values of 0 are ignnored during registration - - initial_M : (3x3) array - Initial transformation matrix - - """ - self.moving = moving - self.fixed = fixed - - if mask is None: - self.mask = np.zeros(fixed.shape[0:2], dtype=np.uint8) - self.mask[fixed != 0] = 1 - else: - self.mask = mask - - self.pyramid_fixed = list(gaussian_pyramid(fixed, levels=self.nlevels)) - self.pyramid_moving = list(gaussian_pyramid(moving, levels=self.nlevels)) - self.pyramid_mask = list(gaussian_pyramid(self.mask, levels=self.nlevels)) - if self.transformation == "EuclideanTransform": - self.p = np.zeros(3) - else: - self.p = np.zeros(4) - self.p[3] = 1 - - if initial_M is not None: - (tx, ty), rotation, (scale_x, scale_y), shear = \ - get_affine_transformation_params(initial_M) - - self.p[0] = rotation - self.p[1] = tx - self.p[2] = ty - if transform == "SimilarityTransform": - self.p[3] = scale_x - - def cost_fxn(self, fixed_image, transformed, mask): - return -normalized_mutual_information(fixed_image, transformed, mask, n_bins=self.nbins) - - def calc_cost(self, p): - """Static cost function passed into scipy.optimize - """ - transformation = make_transform(p) - transformed = transform.warp(self.pyramid_moving[self.current_level], transformation.params, order=3) - if np.all(transformed == 0): - return np.inf - - return self.cost_fxn(self.pyramid_fixed[self.current_level], transformed, self.pyramid_mask[self.current_level]) - - def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None): - """Align images by minimizing self.cost_fxn. Aligns each level of the Gaussian pyramid, and uses previous transform - as the initial guess in the next round of optimization. Also uses other "good" estimates to define the - parameter boundaries. - - Parameters - ---------- - moving : ndarray - Image to warp to align with fixed - - fixed : ndarray - Image moving is warped to align with - - mask : ndarray - 2D array having non-zero pixel values, where values of 0 are ignnored during registration - - initial_M : (3x3) array - Initial transformation matrix - - moving_xy : ndarray, optional - (N, 2) array containing points in the moving image that correspond to those in the fixed image - - fixed_xy : ndarray, optional - (N, 2) array containing points in the fixed image that correspond to those in the moving image - - Returns - ------- - aligned : (N,M) array - Moving image warped to align with the fixed image - M : (3,3) array - Optimal transformation matrix - - cost_list : list - list containing the minimized cost for each level in the pyramid - - """ - - self.setup(moving, fixed, mask, initial_M) - method = self.optimization - levels = range(self.nlevels-1, -1, -1) # Iterate from top to bottom of pyramid - cost_list = [None] * self.nlevels - other_params = None - for n in levels: - self.current_level = n - self.p[1:3] *= 2 - - if other_params is None: - max_tc = self.pyramid_moving[self.current_level].shape[1] - max_tr = self.pyramid_moving[self.current_level].shape[0] - param_bounds = [[0, np.deg2rad(360)], - [-max_tc, max_tc], - [-max_tr, max_tr]] - - if self.transformation == "SimilarityTransform": - param_bounds.append([self.p[3] * 0.5, self.p[3] * 2]) - # Update bounds based on best fits in previous level - else: - param_mins = np.min(other_params, axis=0) - param_maxes = np.max(other_params, axis=0) - param_bounds = [[param_mins[0], param_maxes[0]], - [2*param_mins[1], 2*param_maxes[1]], - [2*param_mins[2], 2*param_maxes[2]]] - - if self.transformation == "SimilarityTransform": - param_bounds.append([param_mins[3], param_maxes[3]]) - - # Optimize # - if method.upper() == 'BH': - res = optimize.basinhopping(self.calc_cost, self.p) - new_p = res.x - cst = res.fun - if n <= self.nlevels//2: # avoid basin-hopping in lower levels - method = 'Powell' - - elif method == 'Nelder-Mead': - res = optimize.minimize(self.calc_cost, self.p, method=method, bounds=param_bounds) - new_p = res.x - cst = np.float(res.fun) - - else: - # Default is Powell, which doesn't accept bounds - res = optimize.minimize(self.calc_cost, self.p, method=method, options={"return_all": True}) - new_p = res.x - cst = np.float(res.fun) - if hasattr(res, "allvecs"): - other_params = np.vstack(res.allvecs) - - if n <= self.nlevels // 2: # avoid basin-hopping in lower levels - method = 'Powell' - - # Update # - self.p = new_p - - cost_list[self.current_level] = cst - tf = make_transform(self.p) - optimal_M = tf.params - w = transform.warp(self.pyramid_moving[n], optimal_M, order=3) - if np.all(w == 0): - print(Warning("Image warped out of bounds. Registration failed")) - return False, np.ones_like(optimal_M), cost_list - - tf = make_transform(self.p) - M = tf.params - aligned = transform.warp(self.moving, M, order=3) - return aligned, M, cost_list - - -class AffineOptimizerMattesMI(AffineOptimizer): - """ Optimize rigid registration using Simple ITK - - AffineOptimizerMattesMI is an AffineOptimizer subclass that uses simple ITK's AdvancedMattesMutualInformation. - If moving_xy and fixed_xy are also provided, then Mattes mutual information will be maximized, while the distance - between moving_xy and fixed_xy will be minimized (the CorrespondingPointsEuclideanDistanceMetric in Simple ITK). - - Attributes - ---------- - nlevels : int - Number of levels in the Gaussian pyramid - - nbins : int - Number of bins to have in histograms used to estimate mutual information - - transformation : str - Type of transformation, "EuclideanTransform" or "SimilarityTransform" - - Reg : sitk.ElastixImageFilter - sitk.ElastixImageFilter object that will perform the optimization - - fixed_kp_fname : str - Name of file where to fixed_xy will be temporarily be written. Eventually deleted - - moving_kp_fname : str - Name of file where to moving_xy will be temporarily be written. Eventually deleted - - - Methods - ------- - setup(moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None) - Create parameter map and initialize Reg - - calc_cost(p) - Inherited but not used, returns None - - write_elastix_kp(kp, fname) - Temporarily write fixed_xy and moving_xy to file - - align(moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None) - Align images by minimizing cost_fxn - - """ - - accepts_xy = True - - def __init__(self, nlevels=4.0, nbins=32, - optimization="AdaptiveStochasticGradientDescent", transform="EuclideanTransform"): - super().__init__(nlevels, nbins, optimization, transform) - - self.Reg = None - self.accepts_xy = AffineOptimizerMattesMI.accepts_xy - self.fixed_kp_fname = os.path.join(pathlib.Path(__file__).parent, ".fixedPointSet.pts") - self.moving_kp_fname = os.path.join(pathlib.Path(__file__).parent, ".movingPointSet.pts") - - def cost_fxn(self, fixed_image, transformed, mask): - return None - - def write_elastix_kp(self, kp, fname): - """ - Temporarily write fixed_xy and moving_xy to file - - Parameters - ---------- - kp: ndarray - (N, 2) numpy array of points (xy) - - fname: str - Name of file in which to save the points - """ - - argfile = open(fname, 'w') - npts = kp.shape[0] - argfile.writelines(f"index\n{npts}\n") - for i in range(npts): - xy = kp[i] - argfile.writelines(f"{xy[0]} {xy[1]}\n") - - def setup(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None): - """ - Create parameter map and initialize Reg - - Parameters - ---------- - - moving : ndarray - Image to warp to align with fixed - - fixed : ndarray - Image moving is warped to align to - - mask : ndarray - 2D array having non-zero pixel values, where values of 0 are ignnored during registration - - initial_M : (3x3) array - Initial transformation matrix - - moving_xy : ndarray, optional - (N, 2) array containing points in the moving image that correspond to those in the fixed image - - fixed_xy : ndarray, optional - (N, 2) array containing points in the fixed image that correspond to those in the moving image - """ - - if initial_M is None: - initial_M = np.eye(3) - - self.moving = moving - self.fixed = fixed - - self.Reg = sitk.ElastixImageFilter() - rigid_map = sitk.GetDefaultParameterMap('affine') - - rigid_map['NumberOfResolutions'] = [str(int(self.nlevels))] - if self.transformation == "EuclideanTransform": - rigid_map["Transform"] = ["EulerTransform"] - else: - rigid_map["Transform"] = ["SimilarityTransform"] - - rigid_map["Registration"] = ["MultiMetricMultiResolutionRegistration"] - if moving_xy is not None and fixed_xy is not None: - self.write_elastix_kp(fixed_xy, self.fixed_kp_fname) - self.write_elastix_kp(moving_xy, self.moving_kp_fname) - current_metrics = rigid_map["Metric"] - current_metrics = list(current_metrics) - current_metrics.append("CorrespondingPointsEuclideanDistanceMetric") - rigid_map["Metric"] = current_metrics - self.Reg.SetFixedPointSetFileName(self.fixed_kp_fname) - self.Reg.SetMovingPointSetFileName(self.moving_kp_fname) - - rigid_map["Optimizer"] = [self.optimization] - rigid_map["NumberOfHistogramBins"] = [str(self.nbins)] - self.Reg.SetParameterMap(rigid_map) - - if mask is not None: - self.Reg.SetFixedMask(sitk.GetImageFromArray(mask)) - - sitk_moving = sitk.GetImageFromArray(moving) - sitk_fixed = sitk.GetImageFromArray(fixed) - self.Reg.SetMovingImage(sitk_moving) # image to warp - self.Reg.SetFixedImage(sitk_fixed) # image to align with - - def calc_cost(self, p): - return None - - def align(self, moving, fixed, mask, initial_M=None, - moving_xy=None, fixed_xy=None): - """ - Optimize rigid registration - - Parameters - ---------- - moving : ndarray - Image to warp to align with fixed - - fixed : ndarray - Image moving is warped to align with - - mask : ndarray - 2D array having non-zero pixel values, where values of 0 are ignnored during registration - - initial_M : (3x3) array - Initial transformation matrix - - moving_xy : ndarray, optional - (N, 2) array containing points in the moving image that correspond to those in the fixed image - - fixed_xy : ndarray, optional - (N, 2) array containing points in the fixed image that correspond to those in the moving image - - - Returns - ------- - aligned : (N,M) array - Moving image warped to align with the fixed image - - M : (3,3) array - Optimal transformation matrix - - cost_list : None - None is returned because costs are not recorded - - """ - - self.setup(moving, fixed, mask, initial_M, moving_xy, fixed_xy) - self.Reg.Execute() - - # See section 2.6 in manual. This is the inverse transform. - # Rotation is in radians - tform_params = self.Reg.GetTransformParameterMap()[0]["TransformParameters"] - if self.transformation == "EuclideanTransform": - rotation, tx, ty = [eval(v) for v in tform_params] - scale = 1.0 - else: - scale, rotation, tx, ty = [eval(v) for v in tform_params] - - M = transform.SimilarityTransform(scale=scale, rotation=rotation, - translation=(tx, ty)).params - - aligned = transform.warp(self.moving, M, order=3) - - # Clean up # - if moving_xy is not None and fixed_xy is not None: - if os.path.exists(self.fixed_kp_fname): - os.remove(self.fixed_kp_fname) - - if os.path.exists(self.moving_kp_fname): - os.remove(self.moving_kp_fname) - - tform_files = [f for f in os.listdir(".") if - f.startswith("TransformParameters.") and - f.endswith(".txt")] - - if len(tform_files) > 0: - for f in tform_files: - os.remove(f) - - return aligned, M, None - - -class AffineOptimizerRMI(AffineOptimizer): - def __init__(self, r=6, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): - super().__init__(nlevels, nbins, optimization, transform) - self.r = r - - def cost_fxn(self, fixed_image, transformed, mask): - r_ratio = self.r/np.min(self.pyramid_fixed[0].shape) - level_rad = int(r_ratio*np.min(fixed_image.shape)) - if level_rad == 0: - level_rad = 1 - - return -region_mi(fixed_image, transformed, mask, r=level_rad) - - -class AffineOptimizerDisplacement(AffineOptimizer): - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): - super().__init__(nlevels, nbins, optimization, transform) - - def cost_fxn(self, fixed_image, transformed, mask): - - return displacement(fixed_image, transformed, mask) - - -class AffineOptimizerKNN(AffineOptimizer): - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): - super().__init__(nlevels, nbins, optimization, transform) - self.HA_list = [None]*nlevels - - def shannon_entropy(self, X, k=1): - """ - Adapted from https://pybilt.readthedocs.io/en/latest/_modules/pybilt/common/knn_entropy.html - to use sklearn's KNN, which is much faster - """ - - from sklearn import neighbors - from scipy.special import gamma, psi - # Get distance to kth nearest neighbor - knn = neighbors.NearestNeighbors(n_neighbors=k) - knn.fit(X.reshape(-1, 1)) - r_k, idx = knn.kneighbors() - lr_k = np.log(r_k[r_k > 0]) - d = 1 - if len(X.shape) == 2: - d = X.shape[1] - # volume of unit ball in d^n - v_unit_ball = np.pi ** (0.5 * d) / gamma(0.5 * d + 1.0) - n = len(X) - H = psi(n) - psi(k) + np.log(v_unit_ball) + (np.float(d) / np.float(n)) * (lr_k.sum()) - - return H - - def mutual_information(self, A, B): - - if self.HA_list[self.current_level] is None: - # Only need to caluclate once per level, becuase the fixed - # image doesn't change - - self.HA_list[self.current_level] = self.shannon_entropy(A) - - HA = self.HA_list[self.current_level] - HB = self.shannon_entropy(B) - - joint = np.hstack([A, B]) - - Hjoint = self.shannon_entropy(joint, k=2) - - MI = HA + HB - Hjoint - if MI < 0: - MI = 0 - return MI - - def cost_fxn(self, fixed_image, transformed, mask): - if mask is not None: - fixed_flat = fixed_image[mask != 0] - transformed_flat = transformed[mask != 0] - else: - fixed_flat = fixed_image.reshape(-1) - transformed_flat = transformed.reshape(-1) - - return -self.mutual_information(fixed_flat, transformed_flat) - - -class AffineOptimizerOffGrid(AffineOptimizer): - def __init__(self, nlevels, nbins=256, optimization="Powell", transform="euclidean", spacing=5): - super().__init__(nlevels, nbins, optimization, transform) - self.spacing = spacing - - def setup(self, moving, fixed, mask, initial_M=None): - AffineOptimizer.setup(self, moving, fixed, mask, initial_M) - - self.moving_interps = [self.get_interp(img) - for img in self.pyramid_moving] - self.fixed_interps = [self.get_interp(img) - for img in self.pyramid_fixed] - - self.z_range = (min(np.min(self.moving[self.nlevels - 1]), - np.min(self.fixed[self.nlevels - 1])), - max(np.max(self.moving[self.nlevels - 1]), - np.max(self.fixed[self.nlevels - 1]))) - - self.grid_spacings = [self.get_scpaing_for_levels(self.pyramid_fixed[i], self.spacing) for i in range(self.nlevels)] - self.grid_flat = [self.get_regular_grid_flat(i) - for i in range(self.nlevels)] - - def get_scpaing_for_levels(self, img_shape, max_level_spacing): - max_shape = self.pyramid_fixed[self.nlevels - 1].shape - shape_ratio = np.mean([img_shape[0]/max_shape[0], - img_shape[0]/max_shape[0]]) - - level_spacing = int(max_level_spacing*shape_ratio) - if level_spacing == 0: - level_spacing = 1 - - return level_spacing - - def get_regular_grid_flat(self, level): - sr, sc = np.meshgrid(np.arange(0, self.pyramid_fixed[level].shape[0], - self.grid_spacings[level]), - np.arange(0, self.pyramid_fixed[level].shape[1], - self.grid_spacings[level])) - - sr = sr.reshape(-1) - sc = sc.reshape(-1) - filtered_sr = sr[self.pyramid_mask[level][sr, sc] > 0] - filtered_sc = sc[self.pyramid_mask[level][sr, sc] > 0] - return (filtered_sr, filtered_sc) - - def get_interp(self, img): - return interpolate.RectBivariateSpline(np.arange(0, img.shape[0], dtype=np.float), np.arange(0, img.shape[1], dtype=np.float), img) - - def interp_point(self, zr, zc, interp, z_range): - z = np.array([interp(zr[i], zc[i])[0][0] for i in range(zr.size)]) - z[z < z_range[0]] = z_range[0] - z[z > z_range[1]] = z_range[1] - return z - - def calc_cost(self, p): - - transformation = make_transform(p) - corners_rc = get_corners_of_image(self.pyramid_fixed[self.current_level].shape) - warped_corners = warp_xy(corners_rc, transformation.params) - if np.any(warped_corners < 0) or \ - np.any(warped_corners[:, 0] > self.pyramid_fixed[self.current_level].shape[0]) or \ - np.any(warped_corners[:, 1] > self.pyramid_fixed[self.current_level].shape[1]): - return np.inf - - sr, sc = self.grid_flat[self.current_level] - sample_r = sr + np.random.uniform(0, self.grid_spacings[self.current_level] / 2, sr.size) - sample_c = sc + np.random.uniform(0, self.grid_spacings[self.current_level] / 2, sc.size) - # Only sample points in mask - warped_xy = warp_xy(np.dstack([sample_c, sample_r])[0], transformation.params) - fixed_intensities = self.interp_point(warped_xy[:, 1], warped_xy[:, 0], self.fixed_interps[self.current_level], self.z_range) - moving_intensities = self.interp_point(sample_r, sample_c, self.moving_interps[self.current_level], self.z_range) - - return self.cost_fxn(fixed_intensities, moving_intensities, self.pyramid_mask[self.current_level]) - - def cost_fxn(self, fixed_intensities, transformed_intensities, mask): - """ - """ - results, _, _ = np.histogram2d(fixed_intensities, transformed_intensities, bins=self.nbins) - n = np.sum(results) - - results /= n - x_margins = np.sum(results, axis=0) - y_margins = np.sum(results, axis=1) - - H_A = entropy(x_margins) - H_B = entropy(y_margins) - H_AB = entropy(results.flatten()) - - MI = (H_A + H_B) / H_AB - if MI < 0: - MI = 0 - - return -MI diff --git a/examples/acrobat_2023/valis/feature_detectors.py b/examples/acrobat_2023/valis/feature_detectors.py deleted file mode 100644 index 45ab9c2f..00000000 --- a/examples/acrobat_2023/valis/feature_detectors.py +++ /dev/null @@ -1,503 +0,0 @@ -"""Functions and classes to detect and describe image features - -Bundles OpenCV feature detectors and descriptors into the FeatureDD class - -Also makes it easier to mix and match feature detectors and descriptors -from different pacakges (e.g. skimage and OpenCV). See CensureVggFD for -an example - -""" - -import cv2 -from skimage import feature, exposure -import numpy as np -import torch - -from . import valtils -from .superglue_models import superpoint - - -DEFAULT_FEATURE_DETECTOR = cv2.BRISK_create() -"""The default OpenCV feature detector""" - -MAX_FEATURES = 20000 -"""Maximum number of image features that will be recorded. If the number -of features exceeds this value, the MAX_FEATURES features with the -highest response will be returned.""" - - -def filter_features(kp, desc, n_keep=MAX_FEATURES): - """Get keypoints with highest response - - Parameters - ---------- - kp : list - List of cv2.KeyPoint detected by an OpenCV feature detector. - - desc : ndarray - 2D numpy array of keypoint descriptors, where each row is a keypoint - and each column a feature. - - n_keep : int - Maximum number of features that are retained. - - Returns - ------- - Keypoints and and corresponding descriptors that the the n_keep highest - responses. - - """ - - response = np.array([x.response for x in kp]) - keep_idx = np.argsort(response)[::-1][0:n_keep] - return [kp[i] for i in keep_idx], desc[keep_idx, :] - - -class FeatureDD(object): - """Abstract class for feature detection and description. - - User can create other feature detectors as subclasses, but each must - return keypoint positions in xy coordinates along with the descriptors - for each keypoint. - - Note that in some cases, such as KAZE, kp_detector can also detect - features. However, in other cases, there may need to be a separate feature - detector (like BRISK or ORB) and feature descriptor (like VGG). - - Attributes - ---------- - kp_detector : object - Keypoint detetor, by default from OpenCV - - kp_descriptor : object - Keypoint descriptor, by default from OpenCV - - kp_detector_name : str - Name of keypoint detector - - kp_descriptor : str - Name of keypoint descriptor - - Methods - ------- - detectAndCompute(image, mask=None) - Detects and describes keypoints in image - - """ - - def __init__(self, kp_detector=None, kp_descriptor=None): - """ - Parameters - ---------- - kp_detector : object - Keypoint detetor, by default from OpenCV - - kp_descriptor : object - Keypoint descriptor, by default from OpenCV - - """ - - self.kp_detector = kp_detector - self.kp_descriptor = kp_descriptor - - if kp_descriptor is not None and kp_detector is not None: - # User provides both a detector and descriptor # - self.kp_descriptor_name = kp_descriptor.__class__.__name__ - self.kp_detector_name = kp_detector.__class__.__name__ - - if kp_descriptor is None and kp_detector is not None: - # Will be using kp_descriptor for detectAndCompute # - kp_descriptor = kp_detector - kp_detector = None - - if kp_descriptor is not None and kp_detector is None: - # User provides a descriptor, which must also be able to detect # - self.kp_descriptor_name = kp_descriptor.__class__.__name__ - self.kp_detector_name = self.kp_descriptor_name - - try: - _img = np.zeros((10, 10), dtype=np.uint8) - kp_descriptor.detectAndCompute(_img, mask=None) - - except: - msg = f"{self.kp_descriptor_name} unable to both detect and compute features. Setting to {DEFAULT_FEATURE_DETECTOR.__class__.__name__}" - valtils.print_warning(msg) - - self.kp_detector = DEFAULT_FEATURE_DETECTOR - - def detect_and_compute(self, image, mask=None): - """Detect the features in the image - - Detect the features in the image using the defined kp_detector, then - describe the features using the kp_descriptor. The user can override - this method so they don't have to use OpenCV's Keypoint class. - - Parameters - ---------- - image : ndarray - Image in which the features will be detected. Should be a 2D uint8 - image if using OpenCV - - mask : ndarray, optional - Binary image with same shape as image, where foreground > 0, - and background = 0. If provided, feature detection will only be - performed on the foreground. - - Returns - ------- - kp : ndarry - (N, 2) array positions of keypoints in xy corrdinates for N - keypoints - - desc : ndarry - (N, M) array containing M features for each of the N keypoints - - """ - - image = exposure.rescale_intensity(image, out_range=(0, 255)).astype(np.uint8) - if self.kp_detector is not None: - detected_kp = self.kp_detector.detect(image) - kp, desc = self.kp_descriptor.compute(image, detected_kp) - # type(desc) - - else: - kp, desc = self.kp_descriptor.detectAndCompute(image, mask=mask) - - if desc.shape[0] > MAX_FEATURES: - - kp, desc = filter_features(kp, desc) - - kp_pos_xy = np.array([k.pt for k in kp]) - - return kp_pos_xy, desc - -# Thin wrappers around OpenCV detectors and descriptors # - - -class OrbFD(FeatureDD): - """Uses ORB for feature detection and description""" - def __init__(self, kp_descriptor=cv2.ORB_create(MAX_FEATURES)): - super().__init__(kp_descriptor=kp_descriptor) - - -class BriskFD(FeatureDD): - """Uses BRISK for feature detection and description""" - def __init__(self, kp_descriptor=cv2.BRISK_create()): - super().__init__(kp_descriptor=kp_descriptor) - - -class KazeFD(FeatureDD): - """Uses KAZE for feature detection and description""" - def __init__(self, kp_descriptor=cv2.KAZE_create(extended=False)): - super().__init__(kp_descriptor=kp_descriptor) - - -class AkazeFD(FeatureDD): - """Uses AKAZE for feature detection and description""" - def __init__(self, kp_descriptor=cv2.AKAZE_create()): - super().__init__(kp_descriptor=kp_descriptor) - - -class DaisyFD(FeatureDD): - """Uses BRISK for feature detection and DAISY for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.DAISY_create()): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - -class LatchFD(FeatureDD): - """Uses BRISK for feature detection and LATCH for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.LATCH_create(rotationInvariance=True)): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - -class BoostFD(FeatureDD): - """Uses BRISK for feature detection and Boost for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.BoostDesc_create()): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - -class VggFD(FeatureDD): - """Uses BRISK for feature detection and VGG for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=5.0)): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - -class OrbVggFD(FeatureDD): - """Uses ORB for feature detection and VGG for feature description""" - def __init__(self, kp_detector=cv2.ORB_create(nfeatures=MAX_FEATURES, fastThreshold=0), kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=0.75)): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - -# Example of a custom detector that uses the Censure feature detector -# from scikit-image along with the KAZE descriptor (OpenCV) -class FeatureDetector(object): - """Abstract class that detects features in an image - - Features should be returned in a list of OpenCV cv2.KeyPoint objects. - Useful if wanting to use a non-OpenCV feature detector - - Attributes - ---------- - detector : object - Object that can detect image features. - - Methods - ------- - detect(image) - - Interface - --------- - Required methods are: detect - - """ - def __init__(self): - self.detector = None - - def detect(self, image): - """ - Use detector to detect features, and return keypoints as XY - - Returns - --------- - kp : KeyPoints - List of OpenCV KeyPoint objects - - """ - pass - - -# Example of how to create a feature detector using OpenCV + skimage # -class SkCensureDetector(FeatureDetector): - """A CENSURE feature detector from scikit image - - This scikit-image feature detecotr can be used with an - OpenCV feature descriptor - - """ - def __init__(self, **kwargs): - super().__init__() - self.detector = feature.CENSURE(**kwargs) - - def detect(self, image): - """ - Detect keypoints in image using CENSURE. - See https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.CENSURE - - Uses keypoint info to create KeyPoint objects for OpenCV - - Paramters - --------- - image : ndarray - image from keypoints will be detected - - - Returns - --------- - kp : KeyPoints - List of OpenCV KeyPoint objects - - """ - self.detector.detect(image) - - # Skimage returns keypoints as row, col, but need to be returned as xy - kp_xy = self.detector.keypoints[:, ::-1].astype(float) - # Now create a list of OpenCV KeyPoint objects with these coordinates - kp = cv2.KeyPoint_convert(kp_xy.tolist()) - - return kp - - -class CensureVggFD(FeatureDD): - def __init__(self, kp_detector=SkCensureDetector(mode="Octagon", - max_scale=8, non_max_threshold=0.02), - kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=6.25)): - - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - self.kp_descriptor_name = self.__class__.__name__ - self.kp_detector_name = self.__class__.__name__ - - -# Example of a custom detector and descriptor using scikit-image # -class SkDaisy(FeatureDD): - def __init__(self, dasiy_arg_dict=None): - """ - Create FeatureDD that uses scikit-image's dense DASIY - https://scikit-image.org/docs/dev/auto_examples/features_detection/plot_daisy.html#sphx-glr-auto-examples-features-detection-plot-daisy-py - - """ - self.dasiy_arg_dict = {"step": 4, - "radius": 15, - "rings": 3, - "histograms": 8, - "orientations": 8, - "normalization": "l1", - "sigmas": None, - "ring_radii": None, - "visualize": False - } - - if dasiy_arg_dict is not None: - self.dasiy_arg_dict.update(dasiy_arg_dict) - - self.kp_descriptor_name = self.__class__.__name__ - self.kp_detector_name = self.__class__.__name__ - - def detect_and_compute(self, image, mask=None): - descs = feature.daisy(image, **self.dasiy_arg_dict) - - # Keypoints in a regular grid, and each point has a feature array # - # Below determines grid and then gets features - rows = np.arange(0, descs.shape[0]) - cols = np.arange(0, descs.shape[1]) - all_rows, all_cols = np.meshgrid(rows, cols) - - all_rows = all_rows.reshape(-1) - all_cols = all_cols.reshape(-1) - n_samples = len(all_rows) - - flat_desc = [descs[all_rows[i]][all_cols[i]] for i in range(n_samples)] - desc2d = np.vstack(flat_desc) - - step = self.dasiy_arg_dict["step"] - radius = self.dasiy_arg_dict["radius"] - feature_x = all_cols * step + radius - feature_y = all_rows * step + radius - kp_xy = np.dstack([feature_x, feature_y])[0] - - return kp_xy, desc2d - - -class SuperPointFD(FeatureDD): - - """SuperPoint `FeatureDD` - - Use SuperPoint to detect and describe features (`detect_and_compute`) - Adapted from https://github.com/magicleap/SuperGluePretrainedNetwork/blob/master/match_pairs.py - - References - ----------- - Paul-Edouard Sarlin, Daniel DeTone, Tomasz Malisiewicz, and Andrew - Rabinovich. SuperGlue: Learning Feature Matching with Graph Neural - Networks. In CVPR, 2020. https://arxiv.org/abs/1911.11763 - - """ - - def __init__(self, keypoint_threshold=0.005, nms_radius=4, force_cpu=False, kp_descriptor=None, kp_detector=None): - - """ - Parameters - ---------- - - keypoint_threshold : float - SuperPoint keypoint detector confidence threshold - - nms_radius : int - SuperPoint Non Maximum Suppression (NMS) radius (must be positive) - - force_cpu : bool - Force pytorch to run in CPU mode - - kp_descriptor : optional, OpenCV feature desrciptor - - References - ---------- - - - """ - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor) - - self.keypoint_threshold = keypoint_threshold - self.nms_radius = nms_radius - self.device = 'cuda' if torch.cuda.is_available() and not force_cpu else "cpu" - - if kp_detector is None: - self.kp_detector_name = "SuperPoint" - self.kp_detector = None - else: - self.kp_detector_name = kp_detector.__class__.__name__ - - if kp_descriptor is None: - self.kp_descriptor_name = "SuperPoint" - self.kp_descriptor = None - else: - self.kp_descriptor_name = kp_descriptor.__class__.__name__ - - self.config = { - 'superpoint': { - 'nms_radius': self.nms_radius, - 'keypoint_threshold': self.keypoint_threshold, - 'max_keypoints': MAX_FEATURES - }} - - def frame2tensor(self, img): - tensor = torch.from_numpy(img/255.).float()[None, None].to(self.device) - - return tensor - - def detect(self, img): - if self.kp_detector is None: - kp_pos_xy, _ = self.detect_and_compute_sg(img) - else: - kp = self.kp_detector.detect(img) - kp_pos_xy = np.array([k.pt for k in kp]) - - return kp_pos_xy - - def compute(self, img, kp_pos_xy): - - if self.kp_descriptor is None: - sp = superpoint.SuperPoint(self.config["superpoint"]) - - x = sp.relu(sp.conv1a(self.frame2tensor(img))) - x = sp.relu(sp.conv1b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv2a(x)) - x = sp.relu(sp.conv2b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv3a(x)) - x = sp.relu(sp.conv3b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv4a(x)) - x = sp.relu(sp.conv4b(x)) - - cDa = sp.relu(sp.convDa(x)) - descriptors = sp.convDb(cDa) - descriptors = torch.nn.functional.normalize(descriptors, p=2, dim=1) - - descriptors = [superpoint.sample_descriptors(k[None], d[None], 8)[0] - for k, d in zip([torch.from_numpy(kp_pos_xy.astype(np.float32))], descriptors)] - - descriptors = descriptors[0].detach().numpy().T - else: - kp = cv2.KeyPoint_convert(kp_pos_xy.tolist()) - kp, descriptors = self.kp_descriptor.compute(img, kp) - if descriptors.shape[0] > MAX_FEATURES: - kp, descriptors = filter_features(kp, descriptors) - - kp_pos_xy = np.array([k.pt for k in kp]) - - return descriptors - - def detect_and_compute_sg(self, img): - inp = self.frame2tensor(img) - superpoint_obj = superpoint.SuperPoint(self.config.get('superpoint', {})) - pred = superpoint_obj({'image': inp}) - pred = {**pred, **{k+'0': v for k, v in pred.items()}} - kp_pos_xy = pred['keypoints'][0].detach().numpy() - desc = pred['descriptors'][0].detach().numpy().T - - return kp_pos_xy, desc - - def detect_and_compute(self, img): - if self.kp_detector is None and self.kp_descriptor is None: - kp_pos_xy, desc = self.detect_and_compute_sg(img) - - else: - kp_pos_xy = self.detect(img) - desc = self.compute(img, kp_pos_xy) - - return kp_pos_xy, desc diff --git a/examples/acrobat_2023/valis/feature_matcher.py b/examples/acrobat_2023/valis/feature_matcher.py deleted file mode 100644 index f22ad469..00000000 --- a/examples/acrobat_2023/valis/feature_matcher.py +++ /dev/null @@ -1,1407 +0,0 @@ -"""Functions and classes to match and filter image features -""" - -import numpy as np -import cv2 -import numba as nba -import torch -from copy import deepcopy -from sklearn import metrics -from sklearn.metrics.pairwise import pairwise_kernels -from skimage import transform -from scipy.spatial import distance -from . import warp_tools, valtils, feature_detectors -from .superglue_models import matching, superglue, superpoint - -AMBIGUOUS_METRICS = set(metrics.pairwise._VALID_METRICS).intersection( - metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS.keys()) -"""set: - Metrics found in both the valid metrics ang kernel methods in - sklearn.metrics.pairwise. Issue is that metrics are distances, - while kernels are similarities. Metrics in this set are assumed - to be distaces, unless the metric_type parameter in match_descriptors - is set to "similarity". """ - - -EPS = np.finfo(float).eps -"""float: epsilon error to avoid division by 0""" - -GMS_NAME = "GMS" -"""str: If filter_method parameter in match_desc_and_kp is set to this, - Grid-based Motion Statistics will be used to remove poor matches""" - -RANSAC_NAME = "RANSAC" -"""str: If filter_method parameter in match_desc_and_kp is set to this, -RANSAC will be used to remove poor matches -""" - -SUPERGLUE_FILTER_NAME = "superglue" -"""str: If filter_method parameter in match_desc_and_kp is set to this, -only SuperGlue will be used to remove poor matches -""" - -DEFAULT_MATCH_FILTER = RANSAC_NAME -"""str: The defulat filter_method value, either RANSAC_NAME or GMS_NAME""" - -DEFAULT_RANSAC = 7 -"""int: Default RANSAC threshold""" - - -@nba.njit() -def convert_distance_to_similarity(d, n_features=64): - """ - Convert distance to similarity - Based on https://scikit-learn.org/stable/modules/metrics.html - - Parameters - ---------- - d : float - Value to convert - - n_features: int - Number of features used to calcuate distance. - Only needed when calc == 0 - Returns - ------- - y : float - Similarity - """ - return np.exp(-d * (1 / n_features)) - - -@nba.njit() -def convert_similarity_to_distance(s, n_features=64): - """Convert similarity to distance - - Based on https://scikit-learn.org/stable/modules/metrics.html - - Parameters - ---------- - s : float - Similarity to convert - - n_features: int - Number of features used to calcuate similarity. - Only needed when calc == 0 - - Returns - ------- - y : float - Distance - - """ - - return -np.log(s + EPS) / (1 / n_features) - - -def filter_matches_ransac(kp1_xy, kp2_xy, ransac_val=DEFAULT_RANSAC): - f"""Remove poor matches using RANSAC - - Parameters - ---------- - kp1_xy : ndarray - (N, 2) array containing image 1s keypoint positions, in xy coordinates. - - kp2_xy : ndarray - (N, 2) array containing image 2s keypoint positions, in xy coordinates. - - ransac_val: int - RANSAC threshold, passed to cv2.findHomography as the - ransacReprojThreshold parameter. Default value is {DEFAULT_RANSAC} - - Returns - ------- - filtered_src_points : (N, 2) array - Inlier keypoints from kp1_xy - - filtered_dst_points : (N, 2) array - Inlier keypoints from kp1_xy - - good_idx : (1, N) array - Indices of inliers - - """ - - if kp1_xy.shape[0] >= 4: - _, mask = cv2.findHomography(kp1_xy, kp2_xy, cv2.RANSAC, ransac_val) - good_idx = np.where(mask.reshape(-1) == 1)[0] - filtered_src_points = kp1_xy[good_idx, :] - filtered_dst_points = kp2_xy[good_idx, :] - else: - msg = f"Need at least 4 keypoints for RANSAC filtering, but only have {kp1_xy.shape[0]}" - valtils.print_warning(msg) - filtered_src_points = kp1_xy.copy() - filtered_dst_points = kp2_xy.copy() - good_idx = np.arange(0, kp1_xy.shape[0]) - - return filtered_src_points, filtered_dst_points, good_idx - - -def filter_matches_gms(kp1_xy, kp2_xy, feature_d, img1_shape, img2_shape, - scaling, thresholdFactor=6.0): - """Filter matches using GMS (Grid-based Motion Statistics) [1] - - This filtering method does best when there are a large number of features, - so the ORB detector is recommended - - Note that this function assumes the keypoints and distances have been - sorted such that each keypoint in kp1_xy has the same index as the - matching keypoint in kp2_xy andd corresponding feautre distance in - feature_d. For example, kp1_xy[0] should have the corresponding keypoint - at kp2_xy[0] and the corresponding feature distance at feature_d[0]. - - - Parameters - ---------- - kp1_xy : ndarray - (N, 2) array with image 1s keypoint positions, in xy coordinates, for - each of the N matched descriptors in desc1 - - kp2_xy : narray - (N, 2) array with image 2s keypoint positions, in xy coordinates, for - each of the N matched descriptors in desc2 - - feature_d: ndarray - Feature distances between corresponding keypoints - - img1_shape: tuple - Shape of image 1 (row, col) - - img2_shape: tuple - Shape of image 2 (row, col) - - scaling: bool - Whether or not image scaling should be considered - - thresholdFactor: float - The higher, the fewer matches - - Returns - ------- - filtered_src_points : (N, 2) array - Inlier keypoints from kp1_xy - - filtered_dst_points : (N, 2) array - Inlier keypoints from kp1_xy - - good_idx : (1, N) array - Indices of inliers - - References - ---------- - .. [1] JiaWang Bian, Wen-Yan Lin, Yasuyuki Matsushita, Sai-Kit Yeung, - Tan Dat Nguyen, and Ming-Ming Cheng. Gms: Grid-based motion statistics for - fast, ultra-robust feature correspondence. In IEEE Conference on Computer - Vision and Pattern Recognition, 2017 - - """ - - kp1 = cv2.KeyPoint_convert(kp1_xy.tolist()) - kp2 = cv2.KeyPoint_convert(kp2_xy.tolist()) - matches = [cv2.DMatch(_queryIdx=i, _trainIdx=i, _imgIdx=0, _distance=feature_d[i]) for i in range(len(kp1_xy))] - gms_matches = cv2.xfeatures2d.matchGMS(img1_shape, img2_shape, kp1, kp2, matches, withRotation=True, - withScale=scaling, thresholdFactor=thresholdFactor) - good_idx = np.array([d.queryIdx for d in gms_matches]) - - if len(good_idx) == 0: - filtered_src_points = [] - filtered_dst_points = [] - else: - filtered_src_points = kp1_xy[good_idx, :] - filtered_dst_points = kp2_xy[good_idx, :] - - return np.array(filtered_src_points), np.array(filtered_dst_points), np.array(good_idx) - - -def filter_matches_tukey(src_xy, dst_xy, tform=transform.SimilarityTransform()): - """Detect and remove outliers using Tukey's method - Adapted from https://towardsdatascience.com/detecting-and-treating-outliers-in-python-part-1-4ece5098b755 - - Parameters - ---------- - src_xy : ndarray - (N, 2) array containing image 1s keypoint positions, in xy coordinates. - - dst_xy : ndarray - (N, 2) array containing image 2s keypoint positions, in xy coordinates. - - Returns - ------- - filtered_src_points : (N, 2) array - Inlier keypoints from kp1_xy - - filtered_dst_points : (N, 2) array - Inlier keypoints from kp1_xy - - good_idx : (1, N) array - Indices of inliers - - """ - - # tform = transform.SimilarityTransform() - tform.estimate(src=dst_xy, dst=src_xy) - M = tform.params - - warped_xy = warp_tools.warp_xy(src_xy, M) - d = warp_tools.calc_d(warped_xy, dst_xy) - - q1 = np.quantile(d, 0.25) - q3 = np.quantile(d, 0.75) - iqr = q3-q1 - inner_fence = 1.5*iqr - outer_fence = 3*iqr - - # inner fence lower and upper end - inner_fence_le = q1-inner_fence - inner_fence_ue = q3+inner_fence - - # outer fence lower and upper end - outer_fence_le = q1-outer_fence - outer_fence_ue = q3+outer_fence - - outliers_prob = [] - outliers_poss = [] - inliers_prob = [] - inliers_poss = [] - for index, v in enumerate(d): - if v <= outer_fence_le or v >= outer_fence_ue: - outliers_prob.append(index) - else: - inliers_prob.append(index) - for index, v in enumerate(d): - if v <= inner_fence_le or v >= inner_fence_ue: - outliers_poss.append(index) - else: - inliers_poss.append(index) - - src_xy_inlier = src_xy[inliers_prob, :] - dst_xy_inlier = dst_xy[inliers_prob, :] - - return src_xy_inlier, dst_xy_inlier, inliers_prob - - -def filter_matches(kp1_xy, kp2_xy, method=DEFAULT_MATCH_FILTER, - filtering_kwargs=None): - """Use RANSAC or GMS to remove poor matches - - Parameters - ---------- - kp1_xy : ndarray - (N, 2) array containing image 1s keypoint positions, in xy coordinates. - - kp2_xy : ndarray - (N, 2) array containing image 2s keypoint positions, in xy coordinates. - - method: str - `method` = "GMS" will use filter_matches_gms() to remove poor matches. - This uses the Grid-based Motion Statistics. - `method` = "RANSAC" will use RANSAC to remove poor matches - - filtering_kwargs: dict - Extra arguments passed to filtering function - - If `method` == "GMS", these need to include: img1_shape, img2_shape, - scaling, thresholdFactor. See filter_matches_gms for details - - If `method` == "RANSAC", this can be None, since the ransac value is - a class attribute - - Returns - ------- - filtered_src_points : ndarray - (M, 2) ndarray of inlier keypoints from kp1_xy - - filtered_dst_points : (N, 2) array - (M, 2) ndarray of inlier keypoints from kp2_xy - - good_idx : ndarray - (M, 1) array containing ndices of inliers - - """ - - all_matching_args = filtering_kwargs.copy() - all_matching_args.update({"kp1_xy": kp1_xy, "kp2_xy": kp2_xy}) - if method.upper() == GMS_NAME: - filter_fxn = filter_matches_gms - else: - filter_fxn = filter_matches_ransac - - filtered_src_points, filtered_dst_points, good_idx = filter_fxn(**all_matching_args) - - # Do additional filtering to remove other outliers that may have been missed by RANSAC - filtered_src_points, filtered_dst_points, good_idx = filter_matches_tukey(filtered_src_points, filtered_dst_points) - - return filtered_src_points, filtered_dst_points, good_idx - - -def match_descriptors(descriptors1, descriptors2, metric=None, - metric_type=None, p=2, max_distance=np.inf, - cross_check=True, max_ratio=1.0, metric_kwargs=None): - """Brute-force matching of descriptors - - For each descriptor in the first set this matcher finds the closest - descriptor in the second set (and vice-versa in the case of enabled - cross-checking). - - - Parameters - ---------- - descriptors1 : ndarray - (M, P) array of descriptors of size P about M keypoints in image 1. - - descriptors2 : ndarray - (N, P) array of descriptors of size P about N keypoints in image 2. - - metric : string or callable - Distance metrics used in spatial.distance.cdist() or sklearn.metrics.pairwise() - Alterntively, can also use similarity metrics in sklearn.metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS. - By default the L2-norm is used for all descriptors of dtype float or - double and the Hamming distance is used for binary descriptors automatically. - - p : int, optional - The p-norm to apply for ``metric='minkowski'``. - - max_distance : float, optional - Maximum allowed distance between descriptors of two keypoints - in separate images to be regarded as a match. - - cross_check : bool, optional - If True, the matched keypoints are returned after cross checking i.e. a - matched pair (keypoint1, keypoint2) is returned if keypoint2 is the - best match for keypoint1 in second image and keypoint1 is the best - match for keypoint2 in first image. - - max_ratio : float, optional - Maximum ratio of distances between first and second closest descriptor - in the second set of descriptors. This threshold is useful to filter - ambiguous matches between the two descriptor sets. The choice of this - value depends on the statistics of the chosen descriptor, e.g., - for SIFT descriptors a value of 0.8 is usually chosen, see - D.G. Lowe, "Distinctive Image Features from Scale-Invariant Keypoints", - International Journal of Computer Vision, 2004. - - metric_kwargs : dict - Optionl keyword arguments to be passed into pairwise_distances() or pairwise_kernels() - from the sklearn.metrics.pairwise module - - Returns - ------- - matches : (Q, 2) array - Indices of corresponding matches in first and second set of - descriptors, where ``matches[:, 0]`` denote the indices in the first - and ``matches[:, 1]`` the indices in the second set of descriptors. - - distances : (Q, 1) array - Distance values between each pair of matched descriptor - - metric_name : str or function - Name metric used to calculate distances or similarity - - NOTE - ---- - Modified from scikit-image to use scikit-learn's distance and kernal methods. - """ - - if descriptors1.shape[1] != descriptors2.shape[1]: - raise ValueError("Descriptor length must equal.") - - if metric is None: - if np.issubdtype(descriptors1.dtype, np.bool_): - metric = 'hamming' - else: - metric = 'euclidean' - - if metric_kwargs is None: - metric_kwargs = {} - - if metric == 'minkowski': - metric_kwargs['p'] = p - - if metric in AMBIGUOUS_METRICS: - print("metric", metric, "could be a distance in pairwise_distances() or similarity in pairwise_kernels().", - "Please set metric_type. Otherwise, metric is assumed to be a distance") - if callable(metric) or metric in metrics.pairwise._VALID_METRICS: - - distances = metrics.pairwise_distances(descriptors1, descriptors2, metric=metric, **metric_kwargs) - if callable(metric) and metric_type is None: - print(Warning("Metric passed as a function or class, but the metric type not provided", - "Assuming the metric function returns a distance. If a similarity is actually returned", - "set metric_type = 'similiarity'. If metric is a distance, set metric_type = 'distance'" - "to avoid this message")) - - metric_type = "distance" - if metric_type == "similarity": - distances = convert_similarity_to_distance(distances, n_features=descriptors1.shape[1]) - if metric in metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS: - similarities = pairwise_kernels(descriptors1, descriptors2, metric=metric, **metric_kwargs) - distances = convert_similarity_to_distance(similarities, n_features=descriptors1.shape[1]) - - if callable(metric): - metric_name = metric.__name__ - else: - metric_name = metric - - indices1 = np.arange(descriptors1.shape[0]) - indices2 = np.argmin(distances, axis=1) - - if cross_check: - matches1 = np.argmin(distances, axis=0) - mask = indices1 == matches1[indices2] - indices1 = indices1[mask] - indices2 = indices2[mask] - - if max_distance < np.inf: - mask = distances[indices1, indices2] < max_distance - indices1 = indices1[mask] - indices2 = indices2[mask] - - if max_ratio < 1.0: - best_distances = distances[indices1, indices2] - distances[indices1, indices2] = np.inf - second_best_indices2 = np.argmin(distances[indices1], axis=1) - second_best_distances = distances[indices1, second_best_indices2] - second_best_distances[second_best_distances == 0] \ - = np.finfo(np.double).eps - ratio = best_distances / second_best_distances - mask = ratio < max_ratio - indices1 = indices1[mask] - indices2 = indices2[mask] - - return np.column_stack((indices1, indices2)), best_distances[indices1, indices2], metric, metric_type - else: - - return np.column_stack((indices1, indices2)), distances[indices1, indices2], metric_name, metric_type - - -def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, - metric_type=None, metric_kwargs=None, max_ratio=1.0, - filter_method=DEFAULT_MATCH_FILTER, - filtering_kwargs=None): - """Match the descriptors of image 1 with those of image 2 and remove outliers. - - Metric can be a string to use a distance in scipy.distnce.cdist(), - or a custom distance function - - Parameters - ---------- - desc1 : ndarray - (N, P) array of image 1's descriptions for N keypoints, - which each keypoint having P features - - kp1_xy : ndarray - (N, 2) array containing image 1's keypoint positions (xy) - - desc2 : ndarray - (M, P) array of image 2's descriptions for M keypoints, - which each keypoint having P features - - kp2_xy : (M, 2) array - (M, 2) array containing image 2's keypoint positions (xy) - - metric: string, or callable - Metric to calculate distance between each pair of features - in desc1 and desc2. Can be a string to use as distance in - spatial.distance.cdist, or a custom distance function - - metric_kwargs : dict - Optionl keyword arguments to be passed into pairwise_distances() - or pairwise_kernels() from the sklearn.metrics.pairwise module - - max_ratio : float, optional - Maximum ratio of distances between first and second closest descriptor - in the second set of descriptors. This threshold is useful to filter - ambiguous matches between the two descriptor sets. The choice of this - value depends on the statistics of the chosen descriptor, e.g., - for SIFT descriptors a value of 0.8 is usually chosen, see - D.G. Lowe, "Distinctive Image Features from Scale-Invariant Keypoints", - International Journal of Computer Vision, 2004. - - filter_method: str - "GMS" will use uses the Grid-based Motion Statistics - "RANSAC" will use RANSAC - - filtering_kwargs: dict - Dictionary containing extra arguments for the filtering method. - kp1_xy, kp2_xy, feature_d are calculated here, and don't need to - be in filtering_kwargs. If filter_method == "GMS", then the - required arguments are: img1_shape, img2_shape, scaling, - thresholdFactor. See filter_matches_gms for details. - - If filter_method == "RANSAC", then the required - arguments are: ransac_val. See filter_matches_ransac for details. - - Returns - ------- - - match_info12 : MatchInfo - Contains information regarding the matches between image 1 and - image 2. These results haven't undergone filtering, so - contain many poor matches. - - filtered_match_info12 : MatchInfo - Contains information regarding the matches between image 1 and - image 2. These results have undergone filtering, and so - contain good matches - - match_info21 : MatchInfo - Contains information regarding the matches between image 2 and - image 1. These results haven't undergone filtering, so contain - many poor matches. - - filtered_match_info21 : MatchInfo - Contains information regarding the matches between image 2 and - image 1. These results have undergone filtering, and so contain - good matches - - """ - - if metric_kwargs is None: - metric_kwargs = {} - - if filter_method.upper() == GMS_NAME: - # GMS is supposed to perform best with a large number of features # - cross_check = False - else: - cross_check = True - - matches, match_distances, metric_name, metric_type = \ - match_descriptors(desc1, desc2, metric=metric, - metric_type=metric_type, - metric_kwargs=metric_kwargs, - max_ratio=max_ratio, - cross_check=cross_check) - - desc1_match_idx = matches[:, 0] - matched_kp1_xy = kp1_xy[desc1_match_idx, :] - matched_desc1 = desc1[desc1_match_idx, :] - - desc2_match_idx = matches[:, 1] - matched_kp2_xy = kp2_xy[desc2_match_idx, :] - matched_desc2 = desc2[desc2_match_idx, :] - - mean_unfiltered_distance = np.mean(match_distances) - mean_unfiltered_similarity = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - match_info12 = MatchInfo(matched_kp1_xy=matched_kp1_xy, matched_desc1=matched_desc1, - matches12=desc1_match_idx, matched_kp2_xy=matched_kp2_xy, - matched_desc2=matched_desc2, matches21=desc2_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=metric_name, - metric_type=metric_type) - - match_info21 = MatchInfo(matched_kp1_xy=matched_kp2_xy, matched_desc1=matched_desc2, - matches12=desc2_match_idx, matched_kp2_xy=matched_kp1_xy, - matched_desc2=matched_desc1, matches21=desc1_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=metric_name, - metric_type=metric_type) - - # Filter matches # - all_filtering_kwargs = {"kp1_xy": matched_kp1_xy, "kp2_xy": matched_kp2_xy} - if filtering_kwargs is None: - if filter_method != RANSAC_NAME: - print(Warning(f"filtering_kwargs not provided for {filter_method} match filtering. Will use RANSAC instead")) - filter_method = RANSAC_NAME - all_filtering_kwargs.update({"ransac_val": DEFAULT_RANSAC}) - else: - all_filtering_kwargs.update({"ransac_val": DEFAULT_RANSAC}) - else: - all_filtering_kwargs.update(filtering_kwargs) - if filter_method == GMS_NAME: - # At this point, filtering_kwargs needs to include: - # img1_shape, img2_shape, scaling, and thresholdFactor. - # Already added kp1_xy, kp2_xy. Now adding feature_d to - # the argument dictionary - - all_filtering_kwargs.update({"feature_d": match_distances}) - - filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = \ - filter_matches(matched_kp1_xy, matched_kp2_xy, filter_method, all_filtering_kwargs) - - if len(good_matches_idx) > 0: - filterd_match_distances = match_distances[good_matches_idx] - filterd_matched_desc1 = matched_desc1[good_matches_idx, :] - filterd_matched_desc2 = matched_desc2[good_matches_idx, :] - - good_matches12 = desc1_match_idx[good_matches_idx] - good_matches21 = desc2_match_idx[good_matches_idx] - - mean_filtered_distance = np.mean(filterd_match_distances) - mean_filtered_similarity = \ - np.mean(convert_distance_to_similarity(filterd_match_distances, - n_features=desc1.shape[1])) - else: - filterd_match_distances = [] - filterd_matched_desc1 = [] - filterd_matched_desc2 = [] - - good_matches12 = [] - good_matches21 = [] - - mean_filtered_distance = np.inf - mean_filtered_similarity = 0 - - # Record filtered matches - filtered_match_info12 = MatchInfo(matched_kp1_xy=filtered_matched_kp1_xy, matched_desc1=filterd_matched_desc1, - matches12=good_matches12, matched_kp2_xy=filtered_matched_kp2_xy, - matched_desc2=filterd_matched_desc2, matches21=good_matches21, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=metric_name, - metric_type=metric_type) - - filtered_match_info21 = MatchInfo(matched_kp1_xy=filtered_matched_kp2_xy, matched_desc1=filterd_matched_desc2, - matches12=good_matches21, matched_kp2_xy=filtered_matched_kp1_xy, - matched_desc2=filterd_matched_desc1, matches21=good_matches12, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=metric_name, - metric_type=metric_type) - - return match_info12, filtered_match_info12, match_info21, filtered_match_info21 - - -class MatchInfo(object): - """Class that stores information related to matches. One per pair of images - - All attributes are all set as parameters during initialization - """ - - def __init__(self, - matched_kp1_xy, matched_desc1, matches12, - matched_kp2_xy, matched_desc2, matches21, - match_distances, distance, similarity, - metric_name, metric_type, - img1_name=None, img2_name=None): - - """Stores information about matches and features - - Parameters - ---------- - matched_kp1_xy : ndarray - (Q, 2) array of image 1 keypoint xy coordinates after filtering - - matched_desc1 : ndarray - (Q, P) array of matched descriptors for image 1, each of which has P features - - matches12 : ndarray - (1, Q) array of indices of featiures in image 1 that matched those in image 2 - - matched_kp2_xy : ndarray - (Q, 2) array containing Q matched image 2 keypoint xy coordinates after filtering - - matched_desc2 : ndarray - (Q, P) containing Q matched descriptors for image 2, each of which has P features - - matches21 : ndarray - (1, Q) containing indices of featiures in image 2 that matched those in image 1 - - match_distances : ndarray - Distances between each of the Q pairs of matched descriptors - - n_matches : int - Number of good matches (i.e. the number of inlier keypoints) - - distance : float - Mean distance of features - - similarity : float - Mean similarity of features - - metric_name : str - Name of metric - - metric_type : str - "distsnce" or "similarity" - - img1_name : str - Name of the image that kp1 and desc1 belong to - - img2_name : str - Name of the image that kp2 and desc2 belong to - - """ - - self.matched_kp1_xy = matched_kp1_xy - self.matched_desc1 = matched_desc1 - self.matches12 = matches12 - self.matched_kp2_xy = matched_kp2_xy - self.matched_desc2 = matched_desc2 - self.matches21 = matches21 - self.match_distances = match_distances - self.n_matches = len(match_distances) - self.distance = distance - self.similarity = similarity - self.metric_name = metric_name - self.metric_type = metric_type - self.img1_name = img1_name - self.img2_name = img2_name - - def set_names(self, img1_name, img2_name): - self.img1_name = img1_name - self.img2_name = img2_name - - -class Matcher(object): - """Class that matchs the descriptors of image 1 with those of image 2 - - Outliers removed using RANSAC or GMS - - Attributes - ---------- - metric: str, or callable - Metric to calculate distance between each pair of features in - desc1 and desc2. Can be a string to use as distance in - spatial.distance.cdist, or a custom distance function - - metric_name: str - Name metric used. Will be the same as metric if metric is string. - If metric is function, this will be the name of the function. - - metric_type: str, or callable - String describing what the custom metric function returns, e.g. - 'similarity' or 'distance'. If None, and metric is a function it - is assumed to be a distance, but there will be a warning that this - variable should be provided to either define that it is a - similarity, or to avoid the warning by having - metric_type='distance' In the case of similarity, the number of - features will be used to convert distances - - ransac : int - The residual threshold to determine if a match is an inlier. - Only used if filter_method == {RANSAC_NAME}. Default is "RANSAC" - - gms_threshold : int - Used when filter_method is "GMS". - The higher, the fewer matches. - - scaling: bool - Whether or not image scaling should be considered when - filter_method is "GMS" - - metric_kwargs : dict - Keyword arguments passed into the metric when calling - spatial.distance.cdist - - match_filter_method: str - "GMS" will use filter_matches_gms() to remove poor matches. - This uses the Grid-based Motion Statistics (GMS) or RANSAC. - - """ - - def __init__(self, metric=None, metric_type=None, metric_kwargs=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - gms_threshold=15, scaling=False): - """ - Parameters - ---------- - - metric: str, or callable - Metric to calculate distance between each pair of features in - desc1 and desc2. Can be a string to use as distance in - spatial.distance.cdist, or a custom distance function - - metric_type: str, or callable - String describing what the custom metric function returns, e.g. - 'similarity' or 'distance'. If None, and metric is a function it - is assumed to be a distance, but there will be a warning that this - variable should be provided to either define that it is a - similarity, or to avoid the warning by having - metric_type='distance' In the case of similarity, the number of - features will be used to convert distances - - metric_kwargs : dict - Keyword arguments passed into the metric when calling - spatial.distance.cdist - - filter_method: str - "GMS" will use filter_matches_gms() to remove poor matches. - This uses the Grid-based Motion Statistics (GMS) or RANSAC. - - ransac_val : int - The residual threshold to determine if a match is an inlier. - Only used if filter_method is "RANSAC". - - gms_threshold : int - Used when filter_method is "GMS". - The higher, the fewer matches. - - scaling: bool - Whether or not image scaling should be considered when - filter_method is "GMS". - - """ - - self.metric = metric - if metric is not None: - if isinstance(metric, str): - self.metric_name = metric - elif callable(metric): - self.metric_name = metric.__name__ - else: - self.metric_name = None - - self.metric_type = metric_type - self.ransac = ransac_thresh - self.gms_threshold = gms_threshold - self.scaling = scaling - self.metric_kwargs = metric_kwargs - self.match_filter_method = match_filter_method - - def match_images(self, desc1, kp1_xy, desc2, kp2_xy, - additional_filtering_kwargs=None, *args, **kwargs): - """Match the descriptors of image 1 with those of image 2, - Outliers removed using match_filter_method. Metric can be a string - to use a distance in scipy.distnce.cdist(), or a custom distance - function. Sets atttributes for Matcher object - - Parameters - ---------- - desc1 : (N, P) array - Image 1s 2D array containinng N keypoints, each of which - has P features - - kp1_xy : (N, 2) array - Image 1s keypoint positions, in xy coordinates, for each of the - N descriptors in desc1 - - desc2 : (M, P) array - Image 2s 2D array containinng M keypoints, each of which has - P features - - kp2_xy : (M, 2) array - Image 1s keypoint positions, in xy coordinates, for each of - the M descriptors in desc2 - - additional_filtering_kwargs: dict, optional - Extra arguments passed to filtering function - If self.match_filter_method == "GMS", these need to - include: img1_shape, img2_shape. See filter_matches_gms for details - If If self.match_filter_method == "RANSAC", this can be None, - since the ransac value is class attribute - - Returns - ------- - match_info12 : MatchInfo - Contains information regarding the matches between image 1 - and image 2. These results haven't undergone filtering, - so contain many poor matches. - - filtered_match_info12 : MatchInfo - Contains information regarding the matches between image 1 - and image 2. These results have undergone - filtering, and so contain good matches - - match_info21 : MatchInfo - Contains information regarding the matches between image 2 - and image 1. These results haven't undergone filtering, so - contain many poor matches. - - filtered_match_info21 : MatchInfo - Contains information regarding the matches between image 2 - and image 1. - - """ - - if self.match_filter_method == GMS_NAME: - if additional_filtering_kwargs is not None: - # At this point arguments need to include: img1_shape, img2_shape # - filtering_kwargs = additional_filtering_kwargs.copy() - filtering_kwargs.update({"scaling": self.scaling, - "thresholdFactor": self.gms_threshold}) - else: - print(Warning(f"Selected {self.match_filter_method},\ - but did not provide argument\ - additional_filtering_kwargs.\ - Defaulting to RANSAC")) - - self.match_filter_method = RANSAC_NAME - filtering_kwargs = {"ransac_val": self.ransac} - - elif self.match_filter_method == RANSAC_NAME: - filtering_kwargs = {"ransac_val": self.ransac} - - else: - print(Warning(f"Dont know {self.match_filter_method}.\ - Defaulting to RANSAC")) - - self.match_filter_method = RANSAC_NAME - filtering_kwargs = {"ransac_val": self.ransac} - - match_info12, filtered_match_info12, match_info21, filtered_match_info21 = \ - match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, - metric=self.metric, metric_type=self.metric_type, - metric_kwargs=self.metric_kwargs, - filter_method=self.match_filter_method, - filtering_kwargs=filtering_kwargs) - - if self.metric_name is None: - self.metric_name = match_info12.metric_name - - return match_info12, filtered_match_info12, match_info21, filtered_match_info21 - - -class SuperPointAndGlue(Matcher): - def __init__(self, weights="indoor", keypoint_threshold=0.005, nms_radius=4, - sinkhorn_iterations=100, match_threshold=0.2, force_cpu=False, - metric=None, metric_type=None, metric_kwargs=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - gms_threshold=15, scaling=False): - - """ - SuperPoint and SuperGlue - - Use SuperPoint SuperPoint + SuperGlue to match images (`match_images`) - - Adapted from https://github.com/magicleap/SuperGluePretrainedNetwork/blob/master/match_pairs.py - - Parameters - ---------- - weights : str - SuperGlue weights. Options= ["indoor", "outdoor"] - - keypoint_threshold : float - SuperPoint keypoint detector confidence threshold - - nms_radius : int - SuperPoint Non Maximum Suppression (NMS) radius (must be positive) - - sinkhorn_iterations : int - Number of Sinkhorn iterations performed by SuperGlue - - match_threshold : float - SuperGlue match threshold - - force_cpu : bool - Force pytorch to run in CPU mode - - scaling: bool - Whether or not image scaling should be considered when - filter_method is "GMS". - - References - ---------- - - - """ - super().__init__(metric=metric, metric_type=metric_type, metric_kwargs=metric_kwargs, - match_filter_method=match_filter_method, ransac_thresh=ransac_thresh, - gms_threshold=gms_threshold, scaling=scaling) - - self.weights = weights - self.keypoint_threshold = keypoint_threshold - self.nms_radius = nms_radius - self.sinkhorn_iterations = sinkhorn_iterations - self.match_threshold = match_threshold - self.kp_descriptor_name = "SuperPoint" - self.kp_detector_name = "SuperPoint" - self.matcher = "SuperGlue" - self.metric_name = "SuperGlue" - self.metric_type = "distance" - self.device = 'cuda' if torch.cuda.is_available() and not force_cpu else "cpu" - - self.config = { - 'superpoint': { - 'nms_radius': self.nms_radius, - 'keypoint_threshold': self.keypoint_threshold, - 'max_keypoints': feature_detectors.MAX_FEATURES - }, - 'superglue': { - 'weights': self.weights, - 'sinkhorn_iterations': self.sinkhorn_iterations, - 'match_threshold': self.match_threshold, - } - } - - def frame2tensor(self, img): - tensor = torch.from_numpy(img/255.).float()[None, None].to(self.device) - - return tensor - - def filter_indv_matches(self, sg_pred, img_id): - img_id_str = str(img_id - 1) - kpts = sg_pred[f'keypoints{img_id_str}'] - desc = sg_pred[f'descriptors{img_id_str}'] - sg_matches = sg_pred[f'matches{img_id_str}'] - - valid = np.where(sg_matches > -1)[0] - sg_filtered_kp = kpts[valid, :] - sg_filtered_desc = desc[:, valid].T - - return sg_filtered_kp, sg_filtered_desc, valid - - def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None, kp2_xy=None, additional_filtering_kwargs=None): - if img1 is not None and img2 is not None: - return self._match_images(img1, img2, additional_filtering_kwargs=additional_filtering_kwargs) - else: - return self._match_kp(desc1=desc1, kp1_xy=kp1_xy, desc2=desc2, kp2_xy=kp2_xy, additional_filtering_kwargs=additional_filtering_kwargs) - - def _match_kp(self, desc1, kp1_xy, desc2, kp2_xy, additional_filtering_kwargs=None): - return super().match_images(desc1=desc1, kp1_xy=kp1_xy, desc2=desc2, kp2_xy=kp2_xy, additional_filtering_kwargs=additional_filtering_kwargs) - - - def _match_images(self, img1, img2, matcher_obj=None, additional_filtering_kwargs=None, *args, **kwargs): - """Detect, compute, and match images using SuperPoint and SuperGlue - - Returns - ------- - - match_info12 : MatchInfo - Contains information regarding the matches between image 1 - and image 2. These results haven't undergone filtering, - so contain many poor matches. - - filtered_match_info12 : MatchInfo - Contains information regarding the matches between image 1 - and image 2. These results have undergone - filtering, and so contain good matches - - match_info21 : MatchInfo - Contains information regarding the matches between image 2 - and image 1. These results haven't undergone filtering, so - contain many poor matches. - - filtered_match_info21 : MatchInfo - Contains information regarding the matches between image 2 - and image 1. - """ - - inp1 = self.frame2tensor(img1) - inp2 = self.frame2tensor(img2) - - with valtils.HiddenPrints(): - sg_matching = matching.Matching(self.config).eval().to(self.device) - - sg_pred = sg_matching({'image0': inp1, 'image1': inp2}) - sg_pred = {k: v[0].detach().numpy() for k, v in sg_pred.items()} - - matches, conf = sg_pred['matches0'], sg_pred['matching_scores0'] - - # Keep the matching keypoints and descriptors - valid = matches > -1 - matches12 = np.where(valid)[0] - matches21 = matches[valid] - - kp1_pos_xy = sg_pred['keypoints0'][matches12, :] - desc1 = sg_pred['descriptors0'].T[matches12, :] - - kp2_pos_xy = sg_pred['keypoints1'][matches21, :] - desc2 = sg_pred['descriptors1'].T[matches21, :] - - if matcher_obj is None: - matcher_obj = Matcher() - - n_sg_matches = len(matches12) - if matcher_obj.metric is None: - metric = 'euclidean' - else: - metric = matcher_obj.metric - - if n_sg_matches < 4 or self.match_filter_method.lower() == SUPERGLUE_FILTER_NAME: - - if n_sg_matches == 0: - match_d = np.inf - match_s = 0 - match_distances = np.array([]) - else: - match_distances = metrics.pairwise_distances(desc1, desc2, metric=metric) - match_d = np.mean(match_distances) - match_s = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - - match_info12 = MatchInfo(matched_kp1_xy=kp1_pos_xy, - matched_desc1=desc1, - matches12=matches12, - matched_kp2_xy=kp2_pos_xy, - matched_desc2=desc2, - matches21=matches21, - match_distances=match_distances, - distance=match_d, - similarity=match_s, - metric_name="SuperGlue", - metric_type="distance") - - match_info21 = MatchInfo(matched_kp1_xy=kp2_pos_xy, - matched_desc1=desc2, - matches12=matches21, - matched_kp2_xy=kp1_pos_xy, - matched_desc2=desc1, - matches21=matches12, - match_distances=match_distances, - distance=match_d, - similarity=match_s, - metric_name="SuperGlue", - metric_type="distance") - - unfiltered_match_info12 = deepcopy(match_info12) - filtered_match_info12 = deepcopy(match_info12) - unfiltered_match_info21 = deepcopy(match_info21) - filtered_match_info21 = deepcopy(match_info21) - else: - unfiltered_match_info12, filtered_match_info12, \ - unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(desc1, kp1_pos_xy, - desc2, kp2_pos_xy, - additional_filtering_kwargs) - - return unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 - - -class SuperGlueMatcher(Matcher): - def __init__(self, weights="indoor", keypoint_threshold=0.005, nms_radius=4, - sinkhorn_iterations=100, match_threshold=0.2, force_cpu=False, - metric=None, metric_type=None, metric_kwargs=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - gms_threshold=15, scaling=False): - - """ - SuperPoint and SuperGlue - - Use SuperPoint SuperPoint + SuperGlue to match images (`match_images`) - - Adapted from https://github.com/magicleap/SuperGluePretrainedNetwork/blob/master/match_pairs.py - - Parameters - ---------- - weights : str - SuperGlue weights. Options= ["indoor", "outdoor"] - - keypoint_threshold : float - SuperPoint keypoint detector confidence threshold - - nms_radius : int - SuperPoint Non Maximum Suppression (NMS) radius (must be positive) - - sinkhorn_iterations : int - Number of Sinkhorn iterations performed by SuperGlue - - match_threshold : float - SuperGlue match threshold - - force_cpu : bool - Force pytorch to run in CPU mode - - scaling: bool - Whether or not image scaling should be considered when - filter_method is "GMS". - - References - ---------- - - - """ - super().__init__(metric=metric, metric_type=metric_type, metric_kwargs=metric_kwargs, - match_filter_method=match_filter_method, ransac_thresh=ransac_thresh, - gms_threshold=gms_threshold, scaling=scaling) - - self.weights = weights - self.keypoint_threshold = keypoint_threshold - self.nms_radius = nms_radius - self.sinkhorn_iterations = sinkhorn_iterations - self.match_threshold = match_threshold - self.kp_descriptor_name = "SuperPoint" - self.kp_detector_name = "SuperPoint" - self.matcher = "SuperGlue" - self.metric_name = "SuperGlue" - self.metric_type = "distance" - self.device = 'cuda' if torch.cuda.is_available() and not force_cpu else "cpu" - - self.config = { - 'superpoint': { - 'nms_radius': self.nms_radius, - 'keypoint_threshold': self.keypoint_threshold, - 'max_keypoints': feature_detectors.MAX_FEATURES - }, - 'superglue': { - 'weights': self.weights, - 'sinkhorn_iterations': self.sinkhorn_iterations, - 'match_threshold': self.match_threshold, - } - } - - def frame2tensor(self, img): - tensor = torch.from_numpy(img/255.).float()[None, None].to(self.device) - - return tensor - - def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None, kp2_xy=None, additional_filtering_kwargs=None): - if img1 is not None and img2 is not None: - return self._match_images(img1=img1, desc1=desc1, kp1_xy=kp1_xy, img2=img2, desc2=desc2, kp2_xy=kp2_xy, additional_filtering_kwargs=additional_filtering_kwargs) - else: - return self._match_kp(desc1=desc1, kp1_xy=kp1_xy, desc2=desc2, kp2_xy=kp2_xy, additional_filtering_kwargs=additional_filtering_kwargs) - - def _match_kp(self, desc1, kp1_xy, desc2, kp2_xy, additional_filtering_kwargs=None): - return super().match_images(desc1=desc1, kp1_xy=kp1_xy, desc2=desc2, kp2_xy=kp2_xy, additional_filtering_kwargs=additional_filtering_kwargs) - - - def calc_scores(self, tensor_img, kp_xy): - sp = superpoint.SuperPoint(self.config["superpoint"]) - - x = sp.relu(sp.conv1a(tensor_img)) - x = sp.relu(sp.conv1b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv2a(x)) - x = sp.relu(sp.conv2b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv3a(x)) - x = sp.relu(sp.conv3b(x)) - x = sp.pool(x) - x = sp.relu(sp.conv4a(x)) - x = sp.relu(sp.conv4b(x)) - - cPa = sp.relu(sp.convPa(x)) - scores = sp.convPb(cPa) - scores = torch.nn.functional.softmax(scores, 1)[:, :-1] - b, _, h, w = scores.shape - scores = scores.permute(0, 2, 3, 1).reshape(b, h, w, 8, 8) - scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h*8, w*8) - scores = superpoint.simple_nms(scores, sp.config['nms_radius']) - kp = [torch.from_numpy(kp_xy[:, ::-1].astype(int))] - scores = [s[tuple(k.t())] for s, k in zip(scores, kp)] - scores = scores[0].unsqueeze(dim=0) - return scores - - # def prep_data(self, img, kp_xy, desc, n_features=256): - # """ - # sp_kp = pred["keypoints"] # Tensor with shape [1, n_kp, 2], float32 - # sp_desc = pred["descriptors"] # Tensor with shape [1, n_features, n_kp], float32 - # sp_scores = pred["scores"] # Tensor with shape [1, n_kp], float32 - # """ - - # inp = self.frame2tensor(img) - # scores = self.calc_scores(inp, kp_xy) - # kp_xy_inp = torch.from_numpy(kp_xy[None, :].astype(np.float32)) - - # if desc.shape[1] < n_features: - # # Expects that there are 256 features - # padding = np.zeros((desc.shape[0], n_features-desc.shape[1])) - # padded_desc = np.hstack([desc, padding]) - # elif desc.shape[1] > n_features: - # padded_desc = desc[:, :n_features] - # else: - # padded_desc = desc - - # desc_inp = torch.from_numpy(padded_desc.T[None].astype(np.float32)) - # desc_inp = torch.nn.functional.normalize(desc_inp, p=2, dim=1) - - # n_kp = kp_xy.shape[0] - - # assert scores.dtype == kp_xy_inp.dtype == desc_inp.dtype == torch.float32 - # assert scores.shape[1] == kp_xy_inp.shape[1] == desc_inp.shape[2] == n_kp - - # return inp, kp_xy_inp, desc_inp, scores - - - - def prep_data(self, img, kp_xy): - """ - sp_kp = pred["keypoints"] # Tensor with shape [1, n_kp, 2], float32 - sp_desc = pred["descriptors"] # Tensor with shape [1, n_features, n_kp], float32 - sp_scores = pred["scores"] # Tensor with shape [1, n_kp], float32 - """ - - inp = self.frame2tensor(img) - scores = self.calc_scores(inp, kp_xy) - kp_xy_inp = torch.from_numpy(kp_xy[None, :].astype(np.float32)) - - sp_fd = feature_detectors.SuperPointFD() - sp_dec = sp_fd.compute(img, kp_xy) - desc_inp = torch.from_numpy(sp_dec.T[None, :].astype(np.float32)) - - n_kp = kp_xy.shape[0] - - assert scores.dtype == kp_xy_inp.dtype == desc_inp.dtype == torch.float32 - assert scores.shape[1] == kp_xy_inp.shape[1] == desc_inp.shape[2] == n_kp - - return inp, kp_xy_inp, desc_inp, scores - - - def _match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None, kp2_xy=None, additional_filtering_kwargs=None): - - inp1, kp1_xy_inp, desc1_inp, scores1 = self.prep_data(img=img1, kp_xy=kp1_xy) - inp2, kp2_xy_inp, desc2_inp, scores2 = self.prep_data(img2, kp2_xy) - - data = {"image0":inp1, - "descriptors0": desc1_inp, - "keypoints0": kp1_xy_inp, - "scores0": scores1, - "image1": inp2, - "descriptors1": desc2_inp, - "keypoints1": kp2_xy_inp, - "scores1": scores2 - } - - sg = superglue.SuperGlue(self.config["superglue"]) - - sg_pred = sg(data) - - sg_pred = {k: v[0].detach().numpy() for k, v in sg_pred.items()} - sg_pred.update(data) - - # Keep the matching keypoints and descriptors - matches, conf = sg_pred['matches0'], sg_pred['matching_scores0'] - valid = matches > -1 - matches12 = np.where(valid)[0] - matches21 = matches[valid] - - kp1_pos_xy = kp1_xy[matches12, :] - kp2_pos_xy = kp2_xy[matches21, :] - - sg_desc1 = desc1[matches12, :] - sg_desc2 = desc2[matches21, :] - - matcher_obj = Matcher() - - n_sg_matches = len(matches12) - if matcher_obj.metric is None: - metric = 'euclidean' - else: - metric = matcher_obj.metric - - if n_sg_matches < 4 or self.match_filter_method.lower() == SUPERGLUE_FILTER_NAME: - if n_sg_matches == 0: - match_d = np.inf - match_s = 0 - match_distances = np.array([]) - else: - match_distances = metrics.pairwise_distances(desc1, desc2, metric=metric) - match_d = np.mean(match_distances) - match_s = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - - match_info12 = MatchInfo(matched_kp1_xy=kp1_pos_xy, - matched_desc1=sg_desc1, - matches12=matches12, - matched_kp2_xy=kp2_pos_xy, - matched_desc2=sg_desc2, - matches21=matches21, - match_distances=match_distances, - distance=match_d, - similarity=match_s, - metric_name="SuperGlue", - metric_type="distance") - - match_info21 = MatchInfo(matched_kp1_xy=kp2_pos_xy, - matched_desc1=sg_desc2, - matches12=matches21, - matched_kp2_xy=kp1_pos_xy, - matched_desc2=sg_desc1, - matches21=matches12, - match_distances=match_distances, - distance=match_d, - similarity=match_s, - metric_name="SuperGlue", - metric_type="distance") - - unfiltered_match_info12 = deepcopy(match_info12) - filtered_match_info12 = deepcopy(match_info12) - unfiltered_match_info21 = deepcopy(match_info21) - filtered_match_info21 = deepcopy(match_info21) - else: - unfiltered_match_info12, filtered_match_info12, \ - unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(sg_desc1, kp1_pos_xy, - sg_desc2, kp2_pos_xy, - additional_filtering_kwargs) - - - return unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 - diff --git a/examples/acrobat_2023/valis/micro_rigid_registrar.py b/examples/acrobat_2023/valis/micro_rigid_registrar.py deleted file mode 100644 index 6743f972..00000000 --- a/examples/acrobat_2023/valis/micro_rigid_registrar.py +++ /dev/null @@ -1,393 +0,0 @@ -import numpy as np -from skimage import color as skcolor, draw, exposure, transform, io -from tqdm import tqdm - -from . import feature_matcher -from . import feature_detectors -from . import preprocessing -from . import viz -from . import warp_tools - - -ROI_MASK = "mask" -ROI_MATCHES = "matches" - -DEFAULT_ROI = ROI_MASK -DEFAULT_FD = feature_detectors.SuperPointFD -DEFAULT_MATCHER = feature_matcher.SuperPointAndGlue -DEFAULT_PROCESSOR = preprocessing.StainFlattener -DEFAULT_PROCESSOR_KWARGS = {"adaptive_eq":False, "with_mask":False} - - - -class MicroRigidRegistrar(object): - """Refine rigid registration using higher resolution images - - Rigid transforms found during lower resolution images are applied to the - WSI and then downsampled. The higher resolution registered images are then - divided into tiles, which are processed and normalized. Next, features are - detected and matched for each tile, the results of which are combined into - a common keypoint list. These higher resolution keypoints are then used to - estimate a new rigid transform. Replaces thumbnails in the - rigid registration folder. - - Attributes - ---------- - val_obj : Valis - The "parent" object that registers all of the slides. - - feature_detector_cls : FeatureDD, optional - Uninstantiated FeatureDD object that detects and computes - image features. Default is SuperPointFD. The - available feature_detectors are found in the `feature_detectors` - module. If a desired feature detector is not available, - one can be created by subclassing `feature_detectors.FeatureDD`. - - matcher : Matcher - Matcher object that will be used to match image features - - scale : float - Degree of downsampling to use for the reigistration, based on the - registered WSI shape (i.e. Slide.aligned_slide_shape_rc) - - tile_wh : int - Width and height of tiles extracted from registered WSI - - roi : string - Determines how the region of interest is defined. `roi="mask"` will - use the bounding box of non-rigid registration mask to define the search area. - `roi=matches` will use the bounding box of the previously matched features to - define the search area. - - iter_order : list of tuples - Determines the order in which images are aligned. Goes from reference image to - the edges of the stack. - - """ - - def __init__(self, val_obj, feature_detector_cls=DEFAULT_FD, - matcher=DEFAULT_MATCHER, img_processor_cls=DEFAULT_PROCESSOR, - img_processor_kwargs=DEFAULT_PROCESSOR_KWARGS, - scale=0.5**3, tile_wh=2**9, roi=DEFAULT_ROI): - """ - - Parameters - ---------- - val_obj : Valis - The "parent" object that registers all of the slides. - - feature_detector_cls : FeatureDD, optional - Uninstantiated FeatureDD object that detects and computes - image features. Default is SuperPointFD. The - available feature_detectors are found in the `feature_detectors` - module. If a desired feature detector is not available, - one can be created by subclassing `feature_detectors.FeatureDD`. - - matcher : Matcher - Matcher object that will be used to match image features - - scale : float - Degree of downsampling to use for the reigistration, based on the - registered WSI shape (i.e. Slide.aligned_slide_shape_rc) - - tile_wh : int - Width and height of tiles extracted from registered WSI - - roi : string - Determines how the region of interest is defined. `roi="mask"` will - use the bounding box of non-rigid registration mask to define the search area. - `roi=matches` will use the bo - - """ - - self.val_obj = val_obj - self.feature_detector_cls = feature_detector_cls - self.matcher = matcher - self.img_processor_cls = img_processor_cls - self.img_processor_kwargs = img_processor_kwargs - self.scale = scale - self.tile_wh = tile_wh - self.roi = roi - self.iter_order = warp_tools.get_alignment_indices(val_obj.size, val_obj.reference_img_idx) - - def create_mask(self, moving_slide, fixed_slide): - """Create mask used to define bounding box of search area - - """ - - pair_slide_list = [moving_slide, fixed_slide] - if self.val_obj.create_masks: - - temp_mask = self.val_obj._create_mask_from_processed(slide_list=pair_slide_list) - else: - temp_mask = self.val_obj._create_non_rigid_reg_mask_from_bbox(slide_list=pair_slide_list) - - fixed_bbox = np.full(fixed_slide.processed_img_shape_rc, 255, dtype=np.uint8) - fixed_mask = fixed_slide.warp_img(fixed_bbox, non_rigid=False, crop=False, interp_method="nearest") - - mask = preprocessing.combine_masks(temp_mask, fixed_mask, op="and") - - return mask - - def register(self): - # Get slides in correct order - slide_idx, slide_names = list(zip(*[[slide_obj.stack_idx, slide_obj.name] for slide_obj in self.val_obj.slide_dict.values()])) - slide_order = np.argsort(slide_idx) # sorts ascending - slide_list = [self.val_obj.slide_dict[slide_names[i]] for i in slide_order] - - for moving_idx, fixed_idx in self.iter_order: - moving_slide = slide_list[moving_idx] - fixed_slide = slide_list[fixed_idx] - - assert moving_slide.fixed_slide == fixed_slide - - mask = self.create_mask(moving_slide, fixed_slide) - - self.align_slides(moving_slide, fixed_slide, mask=mask) - - def align_slides(self, moving_slide, fixed_slide, mask=None): - moving_img = moving_slide.warp_slide(level=0, non_rigid=False, crop=False) - moving_img = warp_tools.rescale_img(moving_img, self.scale) - - moving_shape_rc = warp_tools.get_shape(moving_img)[0:2] - moving_sxy = (moving_shape_rc/moving_slide.reg_img_shape_rc)[::-1] - - fixed_img = fixed_slide.warp_slide(0, non_rigid=False, crop=False) - fixed_img = warp_tools.rescale_img(fixed_img, self.scale) - - fixed_shape_rc = warp_tools.get_shape(fixed_img)[0:2] - fixed_sxy = (fixed_shape_rc/fixed_slide.reg_img_shape_rc)[::-1] - - # Perform Rigid registration where masks overlap - aligned_slide_shape_rc = warp_tools.get_shape(moving_img)[0:2] - - if self.roi == ROI_MASK: - small_reg_bbox = warp_tools.mask2xy(mask) - elif self.roi == ROI_MATCHES: - reg_moving_xy = warp_tools.warp_xy(moving_slide.xy_matched_to_prev, moving_slide.M) - reg_fixed_xy = warp_tools.warp_xy(moving_slide.xy_in_prev, fixed_slide.M) - small_reg_bbox = np.vstack([reg_moving_xy, reg_fixed_xy]) - - reg_s = (aligned_slide_shape_rc/np.array(mask.shape))[::-1] - reg_bbox = warp_tools.xy2bbox(small_reg_bbox*reg_s) - slide_mask = warp_tools.resize_img(warp_tools.numpy2vips(mask), warp_tools.get_shape(fixed_img)[0:2], interp_method="nearest") - - ## Draw mask on image - # small_reg_bbox_xywh = warp_tools.xy2bbox(small_reg_bbox) - # mask_draw_img = warp_tools.resize_img(fixed_slide.image, fixed_slide.processed_img.shape[0:2]) - # mask_draw_img = fixed_slide.warp_img(mask_draw_img, crop=False, non_rigid=False) - # bbox_draw_rc = draw.rectangle_perimeter(start=small_reg_bbox_xywh[0:2][::-1], extent=small_reg_bbox_xywh[2:][::-1], shape=mask_draw_img.shape[0:2]) - # mask_draw_img[bbox_draw_rc[0], bbox_draw_rc[1]] = [0, 255, 0] - # io.imsave(os.path.join(self.val_obj.dst_dir, f"{self.val_obj.name}_{moving_slide.name}_to_{fixed_slide.name}_micro_rigid_bbox.png"), mask_draw_img) - ### - - - # Collect high rez matches - high_rez_moving_match_xy_list = [] - high_rez_moving_match_desc_list = [] - - high_rez_fixed_match_xy_list = [] - high_rez_fixed_match_desc_list = [] - - bbox_tiles = self.get_tiles(reg_bbox, self.tile_wh) - matcher = self.matcher() - fd = self.feature_detector_cls() - for bbox_id, bbox_xy in enumerate(tqdm(bbox_tiles)): - region_xywh = warp_tools.xy2bbox(bbox_xy) - region_mask = slide_mask.extract_area(*region_xywh) - if region_mask.max() == 0: - continue - - moving_region, moving_processed, moving_bbox_xywh = self.process_roi(img=moving_img, - slide_obj=moving_slide, - xy=bbox_xy, - processor_cls=self.img_processor_cls, - processor_kwargs=self.img_processor_kwargs, - apply_mask=False, - scale=1.0 - ) - - fixed_region, fixed_processed, fixed_bbox_xywh = self.process_roi(img=fixed_img, - slide_obj=fixed_slide, - xy=bbox_xy, - processor_cls=self.img_processor_cls, - processor_kwargs=self.img_processor_kwargs, - apply_mask=False, - scale=1.0 - ) - - moving_normed, fixed_normed = self.norm_imgs(img_list=[moving_processed, fixed_processed]) - - try: - if hasattr(matcher, "kp_detector_name"): - # Matcher ( e.g. SuperPointAndGlue) can both detect and describe keypoints - _, filtered_match_info12, _, _ = matcher.match_images(img1=moving_normed, img2=fixed_normed) - - else: - - moving_kp, moving_desc = fd.detect_and_compute(moving_normed) - fixed_kp, fixed_desc = fd.detect_and_compute(fixed_normed) - - _, filtered_match_info12, _, _ = matcher.match_images(img1=moving_normed, desc1=moving_desc, kp1_xy=moving_kp, - img2=fixed_normed, desc2=fixed_desc, kp2_xy=fixed_kp) - - filtered_matched_moving_xy = filtered_match_info12.matched_kp1_xy - filtered_matched_fixed_xy = filtered_match_info12.matched_kp2_xy - matched_moving_desc = filtered_match_info12.matched_desc1 - matched_fixed_desc = filtered_match_info12.matched_desc2 - - if filtered_matched_moving_xy.shape[0] < 3: - continue - - filtered_matched_moving_xy, filtered_matched_fixed_xy, tukey_idx = feature_matcher.filter_matches_tukey(filtered_matched_moving_xy, filtered_matched_fixed_xy, tform=transform.EuclideanTransform()) - matched_moving_desc = matched_moving_desc[tukey_idx, :] - matched_fixed_desc = matched_fixed_desc[tukey_idx, :] - if filtered_matched_moving_xy.shape[0] < 3: - continue - - except Exception as e: - print(e) - continue - - matched_moving_xy = filtered_matched_moving_xy.copy() - matched_fixed_xy = filtered_matched_fixed_xy.copy() - - # Add ROI offset to matched points - matched_moving_xy += moving_bbox_xywh[0:2] - matched_fixed_xy += fixed_bbox_xywh[0:2] - - high_rez_moving_match_xy_list.append(matched_moving_xy) - high_rez_moving_match_desc_list.append(matched_moving_desc) - - high_rez_fixed_match_xy_list.append(matched_fixed_xy) - high_rez_fixed_match_desc_list.append(matched_fixed_desc) - - # Draw matches in tile # - # print("saving matches image") - # match_img = viz.draw_matches(src_img=moving_region, kp1_xy=filtered_matched_moving_xy, - # dst_img=fixed_region, kp2_xy=filtered_matched_fixed_xy, - # rad=3, alignment='horizontal') - - # io.imsave(os.path.join(self.val_obj.dst_dir, f"{self.val_obj.name}_{bbox_id}_{moving_slide.name}_to_{fixed_slide.name}.png"), match_img) - - high_rez_moving_match_xy = np.vstack(high_rez_moving_match_xy_list) - high_rez_fixed_match_xy = np.vstack(high_rez_fixed_match_xy_list) - - temp_high_rez_moving_matched_kp_xy, temp_high_rez_fixed_matched_kp_xy, ransac_idx = feature_matcher.filter_matches_ransac(high_rez_moving_match_xy, high_rez_fixed_match_xy, 20) - high_rez_moving_matched_kp_xy, high_rez_fixed_matched_kp_xy, tukey_idx = feature_matcher.filter_matches_tukey(temp_high_rez_moving_matched_kp_xy, temp_high_rez_fixed_matched_kp_xy, tform=transform.EuclideanTransform()) - - scaled_moving_kp = high_rez_moving_matched_kp_xy*(1/moving_sxy) - scaled_fixed_kp = high_rez_fixed_matched_kp_xy*(1/fixed_sxy) - - if self.val_obj.create_masks: - moving_kp_in_og = warp_tools.warp_xy(scaled_moving_kp, M=np.linalg.inv(moving_slide.M)) - moving_features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=moving_kp_in_og, mask=moving_slide.rigid_reg_mask) - - fixed_kp_in_og = warp_tools.warp_xy(scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M)) - fixed_features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=fixed_kp_in_og, mask=fixed_slide.rigid_reg_mask) - - if len(moving_features_in_mask_idx) > 0 and len(fixed_features_in_mask_idx) > 0: - matches_in_masks = np.intersect1d(moving_features_in_mask_idx, fixed_features_in_mask_idx) - n_removed = scaled_moving_kp.shape[0] - len(matches_in_masks) - print(f"Removed {n_removed} features outside of the micro rigid mask for {moving_slide.name}. Went from {scaled_moving_kp.shape[0]} to {len(matches_in_masks)}") - if len(matches_in_masks) > 0: - scaled_moving_kp = scaled_moving_kp[matches_in_masks, :] - scaled_fixed_kp = scaled_fixed_kp[matches_in_masks, :] - - high_rez_moving_matched_kp_xy = high_rez_moving_matched_kp_xy[matches_in_masks, :] - high_rez_fixed_matched_kp_xy = high_rez_fixed_matched_kp_xy[matches_in_masks, :] - - # Estimate M using position in larger image - transformer = transform.SimilarityTransform() - transformer.estimate(high_rez_fixed_matched_kp_xy, high_rez_moving_matched_kp_xy) - M = transformer.params - - # Scale for use on original processed image - slide_corners_xy = warp_tools.get_corners_of_image(moving_shape_rc)[::-1] - warped_slide_corners = warp_tools.warp_xy(slide_corners_xy, M=M, - transformation_src_shape_rc=moving_shape_rc, - transformation_dst_shape_rc=fixed_shape_rc, - src_shape_rc=moving_slide.reg_img_shape_rc, - dst_shape_rc=fixed_slide.reg_img_shape_rc) - - M_tform = transform.ProjectiveTransform() - M_tform.estimate(warped_slide_corners, slide_corners_xy) - scaled_M = M_tform.params - - new_M = moving_slide.M @ scaled_M - - matched_moving_in_og = warp_tools.warp_xy(scaled_moving_kp, M=np.linalg.inv(moving_slide.M)) - matched_fixed_in_og = warp_tools.warp_xy(scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M)) - - og_d = np.mean(warp_tools.calc_d(warp_tools.warp_xy(moving_slide.xy_matched_to_prev, M=moving_slide.M), warp_tools.warp_xy(moving_slide.xy_in_prev, fixed_slide.M))) - new_d = np.mean(warp_tools.calc_d(warp_tools.warp_xy(matched_moving_in_og, M=new_M), warp_tools.warp_xy(matched_fixed_in_og, fixed_slide.M))) - - n_old_matches = moving_slide.xy_matched_to_prev.shape[0] - n_new_matches = matched_moving_in_og.shape[0] - # print(f"N Old matches= {n_old_matches}, N new= {n_new_matches}. Old D= {og_d}, new D={new_d}") - if (n_old_matches <= n_new_matches) and (new_d < og_d): - # print("micro rigid registration improved alignments") - - moving_slide.M = new_M - moving_slide.xy_matched_to_prev = matched_moving_in_og - moving_slide.xy_in_prev = matched_fixed_in_og - - moving_slide.xy_matched_to_prev_in_bbox = matched_moving_in_og - moving_slide.xy_in_prev_in_bbox = matched_fixed_in_og - - def get_tiles(self, bbox_xywh, wh): - - x_step = np.min([wh, np.floor(bbox_xywh[2]).astype(int)]) - y_step = np.min([wh, np.floor(bbox_xywh[3]).astype(int)]) - - x_pos = np.arange(bbox_xywh[0], bbox_xywh[0]+bbox_xywh[2], x_step) - max_x, max_y = np.round(bbox_xywh[0:2] + bbox_xywh[2:]).astype(int) - if x_pos[-1] < max_x - 1: - x_pos = np.array([*x_pos, max_x]) - - y_pos = np.arange(bbox_xywh[1], bbox_xywh[1]+bbox_xywh[3], y_step) - if y_pos[-1] < max_y - 1: - y_pos = np.array([*y_pos, max_y]) - - tile_bbox_list = [np.array([[x_pos[i], y_pos[j]], [x_pos[i+1], y_pos[j+1]]]) for j in range(len(y_pos) - 1) for i in range(len(x_pos) - 1)] - - return tile_bbox_list - - def norm_imgs(self, img_list): - target_processing_stats = preprocessing.get_channel_stats(np.hstack([img.reshape(-1) for img in img_list])) - - normed_list = [None] * len(img_list) - for i, img in enumerate(img_list): - try: - processed = preprocessing.norm_img_stats(img, target_processing_stats) - except ValueError: - processed = img - - normed_list[i] = exposure.rescale_intensity(processed, out_range=(0, 255)).astype(np.uint8) - - return normed_list - - def process_roi(self, img, slide_obj, xy, processor_cls, processor_kwargs, apply_mask=True, scale=0.5): - is_array = isinstance(img, np.ndarray) - if is_array: - vips_img = warp_tools.numpy2vips(img) - else: - vips_img = img - - bbox = warp_tools.xy2bbox(xy) - bbox_wh = np.ceil(bbox[2:]).astype(int) - region = vips_img.extract_area(*bbox[0:2], *bbox_wh) - - if scale != 1.0: - region = warp_tools.rescale_img(region, scale) - - region_np = warp_tools.vips2numpy(region) - - processor = processor_cls(region_np, src_f=slide_obj.src_f, level=0, series=slide_obj.series) - processed_img = processor.process_image(**processor_kwargs) - - if apply_mask: - mask = processor.create_mask() - processed_img[mask == 0] = 0 - - return region_np, processed_img, bbox diff --git a/examples/acrobat_2023/valis/non_rigid_registrars.py b/examples/acrobat_2023/valis/non_rigid_registrars.py deleted file mode 100644 index 7740416b..00000000 --- a/examples/acrobat_2023/valis/non_rigid_registrars.py +++ /dev/null @@ -1,1482 +0,0 @@ -"""Perform non-rigid registration -""" - -import os -import pathlib -import cv2 -import numpy as np -import SimpleITK as sitk -from skimage import transform, color, filters, exposure, util -from skimage import color as skcolor -import pyvips -import multiprocessing -from joblib import Parallel, delayed, parallel_backend - -from tqdm import tqdm - -from . import viz -from . import warp_tools -from . import preprocessing -from . import valtils - -NR_CLS_KEY = "non_rigid_registrar_cls" -NR_PROCESSING_KW_KEY = "processing_kwargs" -NR_PROCESSING_CLASS_KEY = "processing_cls" -NR_STATS_KEY = "target_stats" -NR_TILE_WH_KEY = "tile_wh" -NR_PARAMS_KEY = "params" -# Abstract Classes # -class NonRigidRegistrar(object): - """Abstract class for non-rigid registration using displacement fields - - Warps moving_img to align with fixed_img using backwards transformations - VALIS offers 3 implementations: dense optical flow (OpenCV), - SimpleElastix, and groupwise SimpleElastix. - Displacement fields can come from other packages, indluding - SimpleITK, PIRT, DIPY, etc... Those other methods can be used by - subclassing the NonRigidRegistrar classes in VALIS. - - Attributes - ---------- - moving_img : ndarray - Image with shape (N,M) thata is warp to align with `fixed_img`. - - fixed_img : ndarray - Image with shape (N,M) that `moving_img` is warped to align with. - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - shape : tuple - Number of rows and columns in each image. Will be (N,M). - - grid_spacing : int - Number of pixels between deformation grid points. - - warped_image : ndarray - Registered copy of `moving_img`. - - deformation_field_img : ndarray - Image showing deformation applied to a regular grid. - - backward_dx : ndarray - (N,M) array defining the displacements in the x-dimension. - - backward_dy : ndarray - (N,M) array defining the displacements in the y-dimension. - - method : str - Name of registration method. - - Note - ----- - All NonRigidRegistrar subclasses need to have a calc() method, - which must at least take the following arguments: - moving_img, fixed_img, mask. calc() should return the displacement field - as a (2, N, M) numpy array, with the first element being an array of - displacements in the x-dimension, and the second element being an array of - displacements in the y-dimension. - - Note that the NonRigidRegistrarXY subclass should be used if - corresponding points in moving and fixed images can be used - to aid the registration. - - """ - - def __init__(self, params=None): - """ - Parameters - ---------- - params : dictionary - Keyword: value dictionary of parameters to be used in reigstration. - Will get used in the calc() method. - - In the case where simple ITK will be used, params should be - a SimpleITK.ParameterMap. Note that numeric values needd to be - converted to strings. - - """ - - self.params = params - self.moving_img = None - self.fixed_img = None - self.mask = None - self.shape = None - self.grid_spacing = None - self.method = None - self.warped_image = None - self.deformation_field_img = None - self.backward_dx = None - self.backward_dy = None - - if isinstance(params, dict): - if len(params) > 0: - self._params_provided = True - else: - # Empty kwargs dictionary - self._params_provided = False - else: - self._params_provided = False - - def apply_mask(self, mask): - - masked_moving = warp_tools.apply_mask(self.moving_img, mask) - masked_fixed = warp_tools.apply_mask(self.fixed_img, mask) - - return masked_moving, masked_fixed - - def calc(self, moving_img, fixed_img, mask, *args, **kwargs): - """Cacluate displacement fields - - Can record subclass specific atrributes here too - - Parameters - ---------- - moving_img : ndarray - Image to warp to align with `fixed_img`. Has shape (N, M). - - fixed_img : ndarray - Image `moving_img` is warped to align with. Has shape (N, M). - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - Returns - ------- - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions. dx = bk_dxdy[0], and dy=bk_dxdy[1]. - - """ - - bk_dxdy = None - - return bk_dxdy - - def create_mask(self): - temp_mask = np.zeros(self.shape, dtype=np.uint8) - img_list = [self.moving_img, self.fixed_img] - for img in img_list: - temp_mask[img > 0] = 255 - - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) - - return mask - - def register(self, moving_img, fixed_img, mask=None, **kwargs): - """ - Register images, warping moving_img to align with fixed_img - - Uses backwards transforms to register images (i.e. aligning - fixed to moving), so the inverse transform needs to be used - to warp points from moving_img. This is automatically done in - warp_tools.warp_xy - - Parameters - ---------- - moving_img : ndarray - Image to warp to align with `fixed_img`. - - fixed_img : ndarray - Image `moving_img` is warped to align with. - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - **kwargs : dict, optional - Additional keyword arguments passed to NonRigidRegistrar.calc - - Returns - ------- - warped_img : ndarray - Moving image registered to align with fixed image. - - warped_grid : ndarray - Image showing deformation applied to a regular grid. - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions. - - """ - - moving_shape = warp_tools.get_shape(moving_img)[0:2] - fixed_shape = warp_tools.get_shape(fixed_img)[0:2] - - assert np.all(moving_shape == fixed_shape), \ - print("Images have different shapes") - - self.shape = moving_shape - self.moving_img = moving_img - self.fixed_img = fixed_img - - if mask is None: - mask = np.full(self.shape, 255, dtype=np.uint8) - - self.mask = mask - - if self.mask is not None: - # Only do registration inside mask # - _, masked_fixed = self.apply_mask(self.mask) - masked_moving = self.moving_img.copy() - - mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(self.mask)) - min_c, min_r = mask_bbox[0:2] - max_c, max_r = mask_bbox[0:2] + mask_bbox[2:] - mask = self.mask[min_r:max_r, min_c:max_c] - masked_moving = masked_moving[min_r:max_r, min_c:max_c] - masked_fixed = masked_fixed[min_r:max_r, min_c:max_c] - - else: - masked_moving = self.moving_img.copy() - masked_fixed = self.fixed_img.copy() - - bk_dxdy = self.calc(moving_img=masked_moving, - fixed_img=masked_fixed, - mask=mask, **kwargs) - - if mask is not None: - bk_dx = np.zeros(self.shape) - bk_dx[min_r:max_r, min_c:max_c] = bk_dxdy[0] - bk_dx[self.mask == 0] = 0 - - bk_dy = np.zeros(self.shape) - bk_dy[min_r:max_r, min_c:max_c] = bk_dxdy[1] - bk_dy[self.mask == 0] = 0 - - bk_dxdy = np.array([bk_dx, bk_dy]) - - warped_img, warp_grid = self.get_warped_img_and_grid(bk_dxdy) - - self.backward_dx = bk_dxdy[..., 0] - self.backward_dy = bk_dxdy[..., 1] - self.deformation_field_img = warp_grid - self.warped_image = warped_img - - return warped_img, warp_grid, bk_dxdy - - def get_grid_image(self, grid_spacing=None, thickness=1, grid_spacing_ratio=0.025): - """Create an image of a regular grid. - - Usually used to visualize non-rigid deformations. - - Parameters - ---------- - grid_spacing : int, optional - Number of pixels between grid points. - - thickness : int, optional - Thickness of lines in image. - - """ - - if grid_spacing is None: - if self.grid_spacing is not None: - grid_spacing = int(self.grid_spacing) - else: - grid_spacing = np.max(np.array(self.shape)*grid_spacing_ratio).astype(int) - - grid_r, grid_c = viz.get_grid(self.shape[:2], - grid_spacing=grid_spacing, - thickness=thickness) - - grid_img = np.zeros(self.shape[:2]) - grid_img[grid_r, grid_c] = 255 - - return grid_img - - def get_warped_img_and_grid(self, bk_dxdy): - """Apply deformation to moving image and regular grid - - Parameters - ---------- - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions. - - Returns - ------- - warped_img : ndarray - Warped copy of moving image. - - warp_grid : ndarray - Image showing deformation applied to regular grid. - - """ - - warp_map = warp_tools.get_warp_map(dxdy=bk_dxdy) - warped_img =transform.warp(self.moving_img, warp_map, preserve_range=True) - self.warped_image = warped_img - grid_img = self.get_grid_image(grid_spacing=16) - warp_grid = transform.warp(grid_img, warp_map, preserve_range=True) - - return warped_img, warp_grid - - -class NonRigidRegistrarXY(NonRigidRegistrar): - """Abstract class for non-rigid registration using displacement fields - - Subclass of NonRigidRegistrar that can (optionally) use corresponding - points (xy coordinates) to aid in the registration - - Attributes - ---------- - moving_img : ndarray - Image with shape (N,M) thata is warp to align with `fixed_img`. - - fixed_img : ndarray - Image with shape (N,M) that `moving_img` is warped to align with. - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - shape : tuple - Number of rows and columns in each image. Will be (N,M). - - grid_spacing : int - Number of pixels between deformation grid points/ - - warped_image : ndarray - Registered copy of `moving_img`. - - deformation_field_img : ndarray - Image showing deformation applied to a regular grid. - - backward_dx : ndarray - (N,M) array defining the displacements in the x-dimension. - - backward_dy : ndarray - (N,M) array defining the displacements in the y-dimension. - - method : str - Name of registration method. - - moving_xy : ndarray, optional - (N, 2) array containing points in `moving_img` that correspond - to those in the fixed image. - - fixed_xy : ndarray, optional - (N, 2) array containing points in `fixed_img` that correspond - to those in the moving image. - - Note - ---- - All NonRigidRegistrarXY subclasses need to have a calc() method, - which needs to at least take the following arguments: - moving_img, fixed_img, mask, moving_xy, fixed_xy. - calc() should return the warped moving image, warped regular grid, - and the displacement field as an (2, N, M) numpy array. - - Note that NonRigidRegistrar should be used if corresponding points in - moving and fixed images can not be used to aid the registration. - - """ - - def __init__(self, params=None): - super().__init__(params=params) - """ - Parameters - ---------- - params : dictionary - Keyword: value dictionary of parameters to be used in reigstration. - Will get used in the calc() method. - - In the case where simple ITK will be used, params should be - a SimpleITK.ParameterMap. Note that numeric values needd to be - converted to strings. - - moving_xy : ndarray, optional - (N, 2) array containing points in the moving image that correspond - to those in the fixed image. - - fixed_xy : ndarray, optional - (N, 2) array containing points in the fixed image that correspond - to those in the moving image. - - """ - - self.moving_xy = None - self.fixed_xy = None - - def register(self, moving_img, fixed_img, mask=None, moving_xy=None, - fixed_xy=None, **kwargs): - """Register images, warping moving_img to align with fixed_img - - Uses backwards transforms to register images (i.e. aligning - fixed to moving), so the inverse transform needs to be used - to warp points from moving_img. This is automatically done in - warp_tools.warp_xy - - Parameters - ---------- - moving_img : ndarray - Image to warp to align with `fixed_img`. - - fixed_img : ndarray - Image `moving_img` is warped to align with. - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - moving_xy : ndarray, optional - (N, 2) array containing points in the `moving_img` that correspond - to those in `fixed_img`. - - fixed_xy : ndarray, optional - (N, 2) array containing points in the `fixed_img` that correspond - to those in the `moving_img`. - - Returns - ------- - warped_img : ndarray - `moving_img` registered to align with `fixed_img`. - - warped_grid : ndarray - Image showing deformation applied to a regular grid. - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the - x and y directions. - - """ - - if moving_xy is not None: - moving_xy, fixed_xy = self.filter_xy(moving_xy, fixed_xy, - moving_img.shape, - mask) - - self.moving_xy = moving_xy - self.fixed_xy = fixed_xy - warped_img, warp_grid, bk_dxdy = \ - NonRigidRegistrar.register(self, moving_img=moving_img, - fixed_img=fixed_img, - mask=mask, - moving_xy=moving_xy, - fixed_xy=fixed_xy, - **kwargs) - - return warped_img, warp_grid, bk_dxdy - - def filter_xy(self, moving_xy, fixed_xy, img_shape_rc, mask=None): - """Remove points outside image and/or mask - - """ - - if mask is None: - mask = np.full(img_shape_rc, 255, dtype=np.uint8) - - moving_inside_idx = warp_tools.get_inside_mask_idx(moving_xy, mask) - fixed_inside_idx = warp_tools.get_inside_mask_idx(fixed_xy, mask) - inside_idx = np.intersect1d(moving_inside_idx, fixed_inside_idx) - - return moving_xy[inside_idx, :], fixed_xy[inside_idx, :] - - -class NonRigidRegistrarGroupwise(NonRigidRegistrar): - """Performs groupwise non-rigid registration - - This subclass can register a collection (>= 2) of images, - and so is not limited to pairs of images. - - Attributes - ---------- - img_list : list of ndarray - List of images, each with shape (N,M) that are to be co-registered - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - shape : tuple of int - Number of rows and columns in each image. Will be (N,M). - - warped_image : ndarray - Registered copy of `moving_img`. - - deformation_field_img : ndarray - Image showing deformation applied to a regular grid. - - backward_dx : ndarray - (N,M) array defining the displacements in the x-dimension. - - backward_dy : ndarray - (N,M) array defining the displacements in the y-dimension. - - grid_spacing : int - Number of pixels between deformation grid points - - method : str - Name of registration method. - - size : int - Number of images that are being registered as a group - - """ - def __init__(self, params=None): - super().__init__(params=params) - self.img_list = None - self.size = 0 - - def apply_mask(self, mask): - """ - Apply mask to all images in img_list - """ - for img in self.img_list: - img[mask == 0] = 0 - - def create_mask(self): - temp_mask = np.zeros(self.shape, dtype=np.uint8) - for img in self.img_list: - temp_mask[img > 0] = 255 - - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) - return mask - - def register(self, img_list, mask=None): - """Register images in img_list - - Uses backwards transforms to register images (i.e. aligning - fixed to moving), so the inverse transform needs to be used - to warp points from moving_img. This is automatically done in - warp_tools.warp_xy - - Parameters - ---------- - img_list : list of ndarray - List of I images, each with shape (N,M) that are to - be co-registered. - - Returns - ------- - warped_img : list of ndarray - List of moving images registered to align with the fixed image. - - warped_grid : list of ndarray - Image showing deformation applied to a regular grid. - - bk_dxdy : list of ndarray - List numpy array of pixel displacements in the x and y directions - for each image. Has shape (I, N, M, 2). - - """ - - self.shape = img_list[0].shape - for img in img_list: - assert img.shape == self.shape, print("Images have differernt shapes") - - self.img_list = img_list - self.size = len(img_list) - if mask is None: - mask = np.full(self.img_list[0].shape[0:2], 255, dtype=np.uiint8) - - self.mask = mask - if self.mask is not None: - mask = mask.astype(np.uint8) - self.mask = mask.copy() - - self.apply_mask(self.mask) - mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(mask)) - min_c, min_r = mask_bbox[0:2] - max_c, max_r = mask_bbox[0:2] + mask_bbox[2:] - mask = self.mask[min_r:max_r, min_c:max_c] - temp_img_list = [None] * self.size - for i, img in enumerate(self.img_list): - - temp_img_list[i] = img[min_r:max_r, min_c:max_c] - else: - temp_img_list = self.img_list - - backward_deformations = self.calc(temp_img_list, mask=mask) - if self.mask is not None: - temp_backward_deformations = [None] * self.size - for i in range(self.size): - bk_dx = np.zeros(self.shape) - bk_dx[min_r:max_r, min_c:max_c] = backward_deformations[i][0] - bk_dx[self.mask == 0] = 0 - - bk_dy = np.zeros(self.shape) - bk_dy[min_r:max_r, min_c:max_c] = backward_deformations[i][1] - bk_dy[self.mask == 0] = 0 - - temp_backward_deformations[i] = np.array([bk_dx, bk_dy]) - - backward_deformations = np.array(temp_backward_deformations) - - self.backward_dx = backward_deformations[:, 0] - self.backward_dy = backward_deformations[:, 1] - - n_imgs = len(self.img_list) - warp_maps = [warp_tools.get_warp_map(dxdy=[self.backward_dx[i], - self.backward_dy[i]]) - for i in range(n_imgs)] - - warped_imgs = [transform.warp(img_list[i], warp_maps[i], preserve_range=True) - for i in range(n_imgs)] - - grid_img = self.get_grid_image(grid_spacing=16) - warped_grids = [transform.warp(grid_img, warp_maps[i], preserve_range=True) - for i in range(n_imgs)] - - self.warped_image = warped_imgs - self.deformation_field_img = warped_grids - - return warped_imgs, warped_grids, backward_deformations - - -# Class members that perform non-rigid registrations # -class SimpleElastixWarper(NonRigidRegistrarXY): - """Uses SimpleElastix to register images - - May optionally using corresponding points - - """ - def __init__(self, params=None, ammi_weight=0.33, - bending_penalty_weight=0.33, kp_weight=0.33): - """ - Parameters - ---------- - ammi_weight : float - Weight given to the AdvancedMattesMutualInformation metric. - - bending_penalty_weight : float - Weight given to the TransformBendingEnergyPenalty metric. - - kp_weight : float - Weight given to the CorrespondingPointsEuclideanDistanceMetric - metric. Only used if moving_xy and fixed_xy are provided as - arguments to the `register()` method. - - """ - super().__init__(params=params) - - self.ammi_weight = ammi_weight - self.bending_penalty_weight = bending_penalty_weight - self.kp_weight = kp_weight - - - @staticmethod - def get_default_params(img_shape, grid_spacing_ratio=0.025): - """ - Get default parameters for registration with sitk.ElastixImageFilter - - See https://simpleelastix.readthedocs.io/Introduction.html - for advice on parameter selection - """ - p = sitk.GetDefaultParameterMap("bspline") - p["Metric"] = ['AdvancedMattesMutualInformation', 'TransformBendingEnergyPenalty'] - p["MaximumNumberOfIterations"] = ['1500'] # Can try up to 2000 - p['FixedImagePyramid'] = ["FixedRecursiveImagePyramid"] - p['MovingImagePyramid'] = ["MovingRecursiveImagePyramid"] - p['Interpolator'] = ["BSplineInterpolator"] - p["ImageSampler"] = ["RandomCoordinate"] - p["MetricSamplingStrategy"] = ["None"] # Use all points - p["UseRandomSampleRegion"] = ["true"] - p["ErodeMask"] = ["true"] - p["NumberOfHistogramBins"] = ["32"] - p["NumberOfSpatialSamples"] = ["3000"] - p["NewSamplesEveryIteration"] = ["true"] - p["SampleRegionSize"] = [str(min([img_shape[1]//3, img_shape[0]//3]))] - p["Optimizer"] = ["AdaptiveStochasticGradientDescent"] - p["ASGDParameterEstimationMethod"] = ["DisplacementDistribution"] - p["HowToCombineTransforms"] = ["Compose"] - grid_spacing_x = img_shape[1]*grid_spacing_ratio - grid_spacing_y = img_shape[0]*grid_spacing_ratio - grid_spacing = str(int(np.mean([grid_spacing_x, grid_spacing_y]))) - p["FinalGridSpacingInPhysicalUnits"] = [grid_spacing] - p["WriteResultImage"] = ["false"] - - return p - - @staticmethod - def elastix_invert_transform(registed_elastix_obj, sitk_fixed): - """Invert transformation as described in elastix manual. - - See section 6.1.6: DisplacementMagnitudePenalty: inverting transformations - - Parameters - ---------- - registed_elastix_obj: sitk.ElastixImageFilter - sitk.ElastixImageFilter object that has completed - image registration. - - sitk_fixed : SimpleITK.Image - SimpleITK.Image created from the fixed image. - - Returns - ------- - inverted_deformationField : ndarray - (N,M,2) numpy array of pixel displacements in the - x and y directions. - - NOTE - ---- - sitk.IterativeInverseDisplacementField seems to do a better job, - and is what is used in warp_tools.get_inverse_field. However, this - method is maintained in case one would like to use it. - - """ - - inverse_transformationFilter = sitk.TransformixImageFilter() - transf_parameter_map = registed_elastix_obj.GetTransformParameterMap() - transf_parameter_map[0]["Metric"] = ["DisplacementMagnitudePenalty"] - transf_parameter_map[0]["HowToCombineTransforms"] = ["Compose"] - inverse_transformationFilter.SetMovingImage(sitk_fixed) - inverse_transformationFilter.SetTransformParameterMap(transf_parameter_map) - inverse_transformationFilter.ComputeDeformationFieldOn() - inverse_transformationFilter.Execute() - inverted_deformationField = sitk.GetArrayFromImage(inverse_transformationFilter.GetDeformationField()) - - return inverted_deformationField - - def write_elastix_kp(self, kp, fname): - """Temporarily write fixed_xy and moving_xy to file - - Parameters - ---------- - kp: ndarray - (N, 2) numpy array of points (xy). - - fname: str - Name of file in which to save the points. - - """ - - argfile = open(fname, 'w') - npts = kp.shape[0] - argfile.writelines(f"index\n{npts}\n") - for i in range(npts): - xy = kp[i] - argfile.writelines(f"{xy[0]} {xy[1]}\n") - - def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, - params=None, mask=None): - - """Run SimpleElastix to register images. - - Can using corresponding points to aid in registration by providing - moving_xy and fixed_xy. - - Parameters - ---------- - moving_img : ndarray - Image to warp to align with `fixed_img`. - - fixed_img : ndarray - Image `moving_img` is warped to align with. - - moving_xy : ndarray, optional - (N, 2) array containing points in the moving image that correspond - to those in the fixed image. - - fixed_xy : ndarray, optional - (N, 2) array containing points in the fixed image that correspond - to those in the moving image. - - mask : ndarray, optional - 2D array with shape (N,M) where non-zero pixel values are - foreground, and 0 is background, which is ignnored during - registration. If None, then all non-zero pixels in images - will be used to create the mask. - - """ - - elastix_image_filter_obj = sitk.ElastixImageFilter() - - if moving_xy is not None and fixed_xy is not None: - - rand_id = np.random.randint(0, 10000) - fixed_kp_fname = os.path.join(pathlib.Path(__file__).parent, - f".{rand_id}_fixedPointSet.pts") - moving_kp_fname = os.path.join(pathlib.Path(__file__).parent, - f".{rand_id}_.movingPointSet.pts") - - self.write_elastix_kp(fixed_xy, fixed_kp_fname) - self.write_elastix_kp(moving_xy, moving_kp_fname) - - kp_dist_met = "CorrespondingPointsEuclideanDistanceMetric" - current_metrics = list(params["Metric"]) - if not self._params_provided or kp_dist_met not in current_metrics: - current_metrics.append(kp_dist_met) - params["Metric"] = current_metrics - weights = np.array([self.ammi_weight, - self.bending_penalty_weight, - self.kp_weight]) - - elastix_image_filter_obj.SetParameterMap(params) - elastix_image_filter_obj.SetFixedPointSetFileName(fixed_kp_fname) - elastix_image_filter_obj.SetMovingPointSetFileName(moving_kp_fname) - else: - weights = np.array([self.ammi_weight, self.bending_penalty_weight]) - - # Set metric weights # - weights /= weights.sum() - n_metrics = len(params["Metric"]) - n_res = eval(params["NumberOfResolutions"][0]) - for r in range(n_metrics): - params[f'Metric{r}Weight'] = [str(weights[r])]*n_res - - elastix_image_filter_obj.SetParameterMap(params) - - # Perform registration # - sitk_moving = sitk.GetImageFromArray(moving_img) - sitk_fixed = sitk.GetImageFromArray(fixed_img) - elastix_image_filter_obj.SetMovingImage(sitk_moving) - elastix_image_filter_obj.SetFixedImage(sitk_fixed) - - if mask is not None: - sitk_mask = sitk.Cast(sitk.GetImageFromArray(mask.astype(np.uint8)), - sitk.sitkUInt8) - - elastix_image_filter_obj.SetFixedMask(sitk_mask) - - elastix_image_filter_obj.Execute() - - # Get deformation field # - transformixImageFilter = sitk.TransformixImageFilter() - transformixImageFilter.SetTransformParameterMap(elastix_image_filter_obj.GetTransformParameterMap()) - transformixImageFilter.ComputeDeformationFieldOn() - transformixImageFilter.Execute() - deformationField = sitk.GetArrayFromImage(transformixImageFilter.GetDeformationField()) - - # Warp image # - resultImage = elastix_image_filter_obj.GetResultImage() - resultImage = sitk.GetArrayFromImage(resultImage) - - # Get deformation grid # - grid_spacing = int(eval(params["FinalGridSpacingInPhysicalUnits"][0])) - grid_img = self.get_grid_image(grid_spacing=grid_spacing) - transformixImageFilter.SetMovingImage(sitk.GetImageFromArray(grid_img)) - transformixImageFilter.Execute() - warped_grid = sitk.GetArrayFromImage(transformixImageFilter.GetResultImage()) - - if moving_xy is not None and fixed_xy is not None: - if os.path.exists(fixed_kp_fname): - os.remove(fixed_kp_fname) - - if os.path.exists(moving_kp_fname): - os.remove(moving_kp_fname) - - tform_files = [f for f in os.listdir(".") - if f.startswith("TransformParameters.") - and f.endswith(".txt")] - - if len(tform_files) > 0: - for f in tform_files: - os.remove(f) - - return resultImage, warped_grid, deformationField, elastix_image_filter_obj, transformixImageFilter - - def calc(self, moving_img, fixed_img, mask=None, - moving_xy=None, fixed_xy=None, *args, **kwargs): - """Perform non-rigid registration using SimpleElastix. - - Can include corresponding points to help in registration by providing - `moving_xy` and `fixed_xy`. - - """ - - assert moving_img.shape == fixed_img.shape,\ - print("Images have different shapes") - - if not self._params_provided: - self.params = self.get_default_params(self.moving_img.shape) - - warped_img, \ - warped_grid, \ - backward_deformation, \ - backward_elastix_image_filter_obj, \ - backward_transformixImageFilter = \ - self.run_elastix(moving_img, fixed_img, - moving_xy=moving_xy, fixed_xy=fixed_xy, - params=self.params, mask=mask) - - # Record other params # - self.grid_spacing = int(eval(self.params["FinalGridSpacingInPhysicalUnits"][0])) - self.elastix_params = self.params.asdict() - self.params = None # Can't pickle SimpleITK.ParameterMap - self.method = backward_elastix_image_filter_obj.__class__.__name__ - dxdy = np.array([backward_deformation[..., 0], backward_deformation[..., 1]]) - - return dxdy - - -class OpticalFlowWarper(NonRigidRegistrar): - """Use dense optical flow to register images. - - Dense optical flow fields may not be diffeomorphic, and so - this class provides options to smooth displacement fields. - """ - def __init__(self, params=None, optical_flow_obj=None, - n_grid_pts=50, sigma_ratio=0.005, - paint_size=5000, fold_penalty=1e-6, - smoothing_method=None): - """ - Parameters - ---------- - params : dictionary - Keyword: value dictionary of parameters to be used in reigstration. - Will get used in the calc() method. - - optical_flow_obj : object - Object that will perform dense optical flow. - - n_grid_pts : int - Number of gridpoints used to detect folds. Also the number - of gridpoints to use when regularizing he mesh when - `method` = "regularize". - - paint_size : int - Used to determine how much to resize the image to have - efficient inpainting. Larger values = longer processing time. - Only used if `smoothing_method` = "inpaint". - - fold_penalty : float - How much to penalize folding/stretching. Larger values will make - the deformation field more uniform, which may or may not be - desired, as too much can remove all displacements. - Only used if `smoothing_method` = "regularize" - - sigma_ratio : float - Determines the amount of Gaussian smoothing, as - sigma = max(shape) *sigma_ratio. Larger values do more - smoothing. Only used if `smoothing_method` is "gauss". - - smoothing : str - If "gauss", then a Gaussian blur will be applied to the - deformation fields, using sigma defined by sigma_ratio. - - If "inpaint", folded regions will be detected and removed. - Folded regions will then be removed using inpainting. - - If "regularize", folded regions will be detected and - regularized using the method fescribed in - "Foldover-free maps in 50 lines of code" Garanzha et al. 2021. - - If "None" then no smoothing will be applied. - - """ - - super().__init__(params) - - self.smoothing_method = smoothing_method - self.sigma_ratio = sigma_ratio - self.paint_size = paint_size - self.fold_penalty = fold_penalty - self.n_grid_pts = n_grid_pts - if optical_flow_obj is None: - optical_flow_obj = cv2.optflow.createOptFlow_DeepFlow - - self.method = optical_flow_obj.__name__ - self.optical_flow_obj = optical_flow_obj() - - def calc(self, moving_img, fixed_img, *args, **kwargs): - if self.method in ['createOptFlow_DenseRLOF', 'createOptFlow_SimpleFlow']: - if moving_img.ndim == 2: - moving_img = color.gray2rgb(moving_img) - - if fixed_img.ndim == 2: - fixed_img = color.gray2rgb(fixed_img) - - backward_flow = self.optical_flow_obj.calc(fixed_img, moving_img, - np.zeros(moving_img.shape[0:2])) - - backward_flow = np.array([backward_flow[..., 0], backward_flow[..., 1]]) - if self.smoothing_method == "gauss": - sigma = self.sigma_ratio*np.max(backward_flow[0].shape) - smooth_dx = filters.gaussian(backward_flow[0], sigma=sigma) - smooth_dy = filters.gaussian(backward_flow[1], sigma=sigma) - backward_flow = np.array([smooth_dx, smooth_dy]) - - elif self.smoothing_method == "inpaint": - backward_flow = warp_tools.remove_folds_in_dxdy(backward_flow, - n_grid_pts=self.n_grid_pts, - paint_size=self.paint_size, - method=self.smoothing_method) - elif self.smoothing_method == "regularize": - backward_flow = warp_tools.untangle(backward_flow, - n_grid_pts=self.n_grid_pts, - penalty=self.fold_penalty, - mask=self.mask) - - self.optical_flow_obj = None # Can't pickle OpenCV objects - - return np.array(backward_flow) - - -class SimpleElastixGroupwiseWarper(NonRigidRegistrarGroupwise): - """ - Performs groupwise non-rigid registration using SimpleElastix. - - SimpleElastixGroupwiseWarper can register a collection (>= 2) of images, - and so is not limited to pairs of images. - - Attributes - ---------- - img_list : list - List of images, each with shape (N,M) that are to be co-registered. - - mask : ndarray - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - shape : tuple of int - Number of rows and columns in each image. - Will have shaape (N,M). - - warped_image : ndarray - Registered copy of `moving_img`. - - deformation_field_img : ndarray - Image showing deformation applied to a regular grid. - - backward_dx : ndarray - (N,M) array defining the displacements in the x-dimension. - - backward_dy : ndarray - (N,M) array defining the displacements in the y-dimension. - - grid_spacing : int - Number of pixels between deformation grid points. - - method : str - Name of registration method. - - """ - - def __init__(self, params=None): - super().__init__(params=params) - - @staticmethod - def get_default_params(img_shape, grid_spacing_ratio=0.025): - """ - See https://simpleelastix.readthedocs.io/Introduction.html for advice on parameter selection - """ - p = sitk.GetDefaultParameterMap("groupwise") - p["Metric"] = ['AdvancedMattesMutualInformation'] - p["MaximumNumberOfIterations"] = ['1500'] # Can try up to 2000 - p['FixedImagePyramid'] = ["FixedRecursiveImagePyramid"] - p['MovingImagePyramid'] = ["MovingRecursiveImagePyramid"] - p["ImageSampler"] = ["RandomCoordinate"] - p["MetricSamplingStrategy"] = ["None"] # Use all points - p["UseRandomSampleRegion"] = ["true"] - p["ErodeMask"] = ["true"] - p["NumberOfSpatialSamples"] = ["3000"] - p["NewSamplesEveryIteration"] = ["true"] - p["Optimizer"] = ["AdaptiveStochasticGradientDescent"] - p["ASGDParameterEstimationMethod"] = ["DisplacementDistribution"] - p["HowToCombineTransforms"] = ["Compose"] - grid_spacing_x = img_shape[1]*grid_spacing_ratio - grid_spacing_y = img_shape[0]*grid_spacing_ratio - grid_spacing = str(int(np.mean([grid_spacing_x, grid_spacing_y]))) - p["FinalGridSpacingInPhysicalUnits"] = [grid_spacing] - p["WriteResultImage"] = ["false"] - - return p - - def calc(self, img_list, mask=None, *args, **kwargs): - if self.params is None: - self.params = SimpleElastixGroupwiseWarper.get_default_params(self.img_list[0].shape[:2]) - - vectorOfImages = sitk.VectorOfImage() - for img in img_list: - vectorOfImages.push_back(sitk.GetImageFromArray(img)) - - image = sitk.JoinSeries(vectorOfImages) - elastix_image_filter_obj = sitk.ElastixImageFilter() - elastix_image_filter_obj.SetFixedImage(image) - elastix_image_filter_obj.SetMovingImage(image) - elastix_image_filter_obj.SetParameterMap(self.params) - - if mask is not None: - vectorOfMasks = sitk.VectorOfImage() - for i in range(len(img_list)): - vectorOfMasks.push_back(sitk.GetImageFromArray(mask)) - mask3d = sitk.JoinSeries(vectorOfMasks) - elastix_image_filter_obj.SetFixedMask(mask3d) - - elastix_image_filter_obj.Execute() - - # Get warped images # - resultImage = elastix_image_filter_obj.GetResultImage() - resultImage = sitk.GetArrayFromImage(resultImage) - - # Get deformation fields # - transformixImageFilter = sitk.TransformixImageFilter() - transformixImageFilter.SetTransformParameterMap(elastix_image_filter_obj.GetTransformParameterMap()) - transformixImageFilter.SetMovingImage(image) - transformixImageFilter.ComputeDeformationFieldOn() - transformixImageFilter.Execute() - deformationField = sitk.GetArrayFromImage(transformixImageFilter.GetDeformationField())[..., 0:2] - - # Get deformation grid # - grid_spacing = int(eval(self.params["FinalGridSpacingInPhysicalUnits"][0])) - self.elastix_params = self.params.asdict() - self.params = None # Can't pickle SimpleITK.ParameterMap - grid_img = self.get_grid_image(grid_spacing=grid_spacing) - self.method = elastix_image_filter_obj.__class__.__name__ - - vectorOfGrids = sitk.VectorOfImage() - for i in range(len(img_list)): - vectorOfGrids.push_back(sitk.GetImageFromArray(grid_img)) - grid3d = sitk.JoinSeries(vectorOfGrids) - - transformixImageFilter.SetMovingImage(grid3d) - transformixImageFilter.Execute() - warped_grid = sitk.GetArrayFromImage(transformixImageFilter.GetResultImage()) - - tform_files = [f for f in os.listdir(".") - if f.startswith("TransformParameters.") - and f.endswith(".txt")] - - if len(tform_files) > 0: - for f in tform_files: - os.remove(f) - - deformationField = np.array([[deformationField[i][..., 0], - deformationField[i][..., 1]] - for i in range(len(deformationField))]) - return deformationField - - -class NonRigidTileRegistrar(object): - """Tile-wise non-rigid regisration - - Slices moving and fixed images into tiles and then registers each tile. - Probably best for very large images. - - Attributes - ---------- - moving_img : pyvips.Image - Image with shape (N,M) thata is warp to align with `fixed_img`. - - fixed_img : pyvips.Image - Image with shape (N,M) that `moving_img` is warped to align with. - - mask : pyvips.Image - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - shape : tuple - Number of rows and columns in each image. Will be (N,M). - - bk_dxdy_tiles : list - List of bk_dxdy for each tile - - bk_dxdy : pyvips.Image - Backwards isplacement field after stitching `bk_dxdy_tiles` together - - fwd_dxdy_tiles : list - List of forward dxdy for each tile - - fwd_dxdy : pyvips.Image - Displacement field after stitching `fwd_dxdy_tiles` together - - pbar : tqdm - Progress bar to track registration time - """ - - def __init__(self, params=None, tile_wh=512, tile_buffer=100): - """ - Parameters - ---------- - params : dictionary - Keyword: value dictionary of parameters to be used in reigstration. - Will get used when initializing the `non_rigid_registrar_cls` - - In the case where simple ITK will be used, params should be - a SimpleITK.ParameterMap. Note that numeric values needd to be - converted to strings. - - tile_wh : int - Width and height of tiles that will be used for registration - - tile_buffer : int - The amount of overlap between each tile. - - """ - self.tile_wh = tile_wh - self.tile_buffer = tile_buffer - self.params = params - self.moving_img = None - self.fixed_img = None - self.mask = None - self.shape = None - self.warped_image = None - - self.bk_dxdy_tiles = None - self.bk_dxdy = None - - self.fwd_dxdy_tiles = None - self.fwd_dxdy = None - - self.pbar = None - - - def norm_img(self, img, stats, mask=None): - normed_img = exposure.rescale_intensity(img, out_range=(0, 255)).astype(np.uint8) - normed_img = preprocessing.norm_img_stats(normed_img, stats, mask) - normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype(np.uint8) - - return normed_img - - def norm_tiles(self, moving_img, fixed_img, tile_mask): - try: - # Try norming using these tile stats - if tile_mask is not None: - pos_px = np.where(tile_mask != 0) - tile_v = np.hstack([fixed_img[pos_px], moving_img[pos_px]]) - else: - tile_v = np.hstack([fixed_img.reshape(-1), moving_img.reshape(-1)]) - - target_processing_stats = preprocessing.get_channel_stats(tile_v) - - fixed_normed = self.norm_img(fixed_img, target_processing_stats, tile_mask) - moving_normed = self.norm_img(moving_img, target_processing_stats, tile_mask) - - except ValueError: - # Norm using full image's stats - if self.target_stats is not None: - try: - fixed_normed = self.norm_img(fixed_img, self.target_stats, tile_mask) - moving_normed = self.norm_img(moving_img, self.target_stats, tile_mask) - except ValueError: - fixed_normed = fixed_img - moving_normed = moving_img - else: - fixed_normed = fixed_img - moving_normed = moving_img - - return moving_normed, fixed_normed - - def process_tile(self, img): - """Process tiles - """ - processor = self.processing_cls(image=img, src_f=None, level=None, series=None) - try: - processed_img = processor.process_image(**self.processing_kwargs) - except TypeError: - # processor.process_image doesn't take kwargs - processed_img = processor.process_image() - - return processed_img - - def reg_tile(self, tile_idx, lock): - - with lock: - # Use lock when accessing images - tile_bbox_xywh = self.expanded_bboxes[tile_idx] - moving_tile = self.moving_img.extract_area(*tile_bbox_xywh) - fixed_tile = self.fixed_img.extract_area(*tile_bbox_xywh) - - np_fixed = warp_tools.vips2numpy(fixed_tile) - np_moving = warp_tools.vips2numpy(moving_tile) - - if self.mask is not None: - tile_mask = self.mask.extract_area(*tile_bbox_xywh) - np_mask = warp_tools.vips2numpy(tile_mask) - else: - np_mask = None - - if moving_tile.interpretation == "srgb": - # Limit registration to be inside image - # Warped areas outside image have the same pixel values, usually 0 - edge_mask = 255*((np_moving.min(axis=2) != np_moving.max(axis=2)) & (np_fixed.min(axis=2) != np_fixed.max(axis=2))).astype(np.uint8) - - if np_mask is not None: - np_mask = 255*((edge_mask > 0) & (np_mask > 0)).astype(np.uint8) - else: - np_mask = edge_mask - - - # Check if either of the tiles are empty - is_empty = fixed_tile.max() == fixed_tile.min() or moving_tile.max() == moving_tile.min() - if np_mask is not None: - is_empty = is_empty or np_mask.max() == 0 - - if is_empty: - # Nothing to register - empty_dxdy = pyvips.Image.black(moving_tile.width, moving_tile.height, bands=2).cast("float") - self.bk_dxdy_tiles[tile_idx] = empty_dxdy - self.fwd_dxdy_tiles[tile_idx] = empty_dxdy - self.pbar.update(1) - - return None - - if self.processing_cls is not None: - # Process tiles # - fixed_processed = self.process_tile(np_fixed) - moving_processed = self.process_tile(np_moving) - - else: - - if np_fixed.ndim > 2: - fixed_g = np.abs(1 - skcolor.rgb2gray(np_fixed)) - fixed_processed = util.img_as_ubyte(fixed_g) - else: - fixed_processed = np_fixed - - if np_moving.ndim > 2: - moving_g = np.abs(1 - skcolor.rgb2gray(np_moving)) - moving_processed = util.img_as_ubyte(moving_g) - else: - moving_processed = np_moving - - moving_normed, fixed_normed = self.norm_tiles(moving_processed, fixed_processed, np_mask) - - tile_non_rigid_reg_obj = self.non_rigid_registrar_cls() - - _, _, bk_dxdy = tile_non_rigid_reg_obj.register(moving_normed, fixed_normed) - fwd_dxdy = warp_tools.get_inverse_field(bk_dxdy) - - vips_tile_bk_dxdy = warp_tools.numpy2vips(np.dstack(bk_dxdy).astype(np.float32)) - vips_tile_fwd_dxdy = warp_tools.numpy2vips(np.dstack(fwd_dxdy).astype(np.float32)) - - self.bk_dxdy_tiles[tile_idx] = vips_tile_bk_dxdy - self.fwd_dxdy_tiles[tile_idx] = vips_tile_fwd_dxdy - - self.pbar.update(1) - - def calc(self, *args, **kwargs): - """Cacluate displacement fields - Each tile is registered and then stitched together - """ - - print("======== Registering tiles\n") - lock = multiprocessing.Lock() - n_cpu = valtils.get_ncpus_available() - 1 - self.pbar = tqdm(total=self.n_tiles) - with parallel_backend("threading", n_jobs=n_cpu): - Parallel()(delayed(self.reg_tile)(i, lock) for i in range(self.n_tiles)) - - bk_dxdy = warp_tools.stitch_tiles(self.bk_dxdy_tiles, self.expanded_bboxes, self.n_rows, self.n_cols, self.tile_buffer) - fwd_dxdy = warp_tools.stitch_tiles(self.fwd_dxdy_tiles, self.expanded_bboxes, self.n_rows, self.n_cols, self.tile_buffer) - - return bk_dxdy, fwd_dxdy - - def register(self, moving_img, fixed_img, mask=None, non_rigid_registrar_cls=OpticalFlowWarper, - processing_cls=None, processing_kwargs=None, target_stats=None, **kwargs): - """ - Register images, warping moving_img to align with fixed_img - - Uses backwards transforms to register images (i.e. aligning - fixed to moving), so the inverse transform needs to be used - to warp points from moving_img. This is automatically done in - warp_tools.warp_xy - - Parameters - ---------- - moving_img : ndarray, pyvips.Image - Image to warp to align with `fixed_img`. - - fixed_img : ndarray, pyvips.Image - Image `moving_img` is warped to align with. - - mask : ndarray, pyvips.Image - 2D array with shape (N,M) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - non_rigid_registrar_cls : NonRigidRegistrar, optional - Uninstantiated NonRigidRegistrar class that will be used - to calculate the deformation fields between images. - - processing_cls : preprocessing.ImageProcesser, optional - preprocessing.ImageProcesser used to process the images - - processing_kwargs : dict - Dictionary of keyward arguments to be passed to `processing_cls` - - target_stats : ndarray - Target stats used to normalize each tile after being processed. - - **kwargs : dict, optional - Additional keyword arguments passed to NonRigidRegistrar.calc - - Returns - ------- - warped_img : pyvips.Image - Moving image registered to align with fixed image. - - fwd_dxdy : - (2, N, M) pyvips.Image with pixel displacements in - the x and y directions. Found by registering `moving_img` - to `fixed_img`. Used for point warping - - bk_dxdy : pyvips.Image - (2, N, M) pyvips.Image with pixel displacements in - the x and y directions. Found by registering `fixed_img` to - `moving_img`. Used for image warping - - """ - - self.is_array = False - if not isinstance(moving_img, pyvips.Image): - self.is_array = True - - if self.is_array: - shape_rc = np.array(moving_img.shape) - else: - shape_rc = np.array([moving_img.height, moving_img.width]) - - self.shape = shape_rc - - self.non_rigid_registrar_cls = non_rigid_registrar_cls - self.processing_cls = processing_cls - self.target_stats = target_stats - self.processing_kwargs = processing_kwargs - - if self.is_array: - moving_img = warp_tools.numpy2vips(moving_img) - - if not isinstance(fixed_img, pyvips.Image): - fixed_img = warp_tools.numpy2vips(fixed_img) - - if mask is not None: - if not isinstance(mask, pyvips.Image): - mask = warp_tools.numpy2vips(mask) - - self.moving_img = moving_img - self.fixed_img = fixed_img - self.mask = mask - - temp_tile_bboxes = warp_tools.get_grid_bboxes(self.shape, self.tile_wh, self.tile_wh, inclusive=True) - self.expanded_bboxes = np.array([warp_tools.expand_bbox(bbox_xywh, self.tile_buffer, self.shape) for bbox_xywh in temp_tile_bboxes]) - - self.n_tiles = len(temp_tile_bboxes) - self.bk_dxdy_tiles = [None] * self.n_tiles - self.fwd_dxdy_tiles = [None] * self.n_tiles - self.n_cols = len(np.unique(temp_tile_bboxes[:, 0])) - self.n_rows = len(np.unique(temp_tile_bboxes[:, 1])) - - bk_dxdy, fwd_dxdy = self.calc() - - warped_img = warp_tools.warp_img(moving_img, bk_dxdy=bk_dxdy) - if self.is_array: - bk_dxdy = warp_tools.vips2numpy(bk_dxdy) - bk_dxdy = [bk_dxdy[..., 0], bk_dxdy[..., 1]] - - warped_img = warp_tools.vips2numpy(warped_img) - - self.bk_dxdy = bk_dxdy - self.fwd_dxdy = fwd_dxdy - - return warped_img, fwd_dxdy, bk_dxdy - - diff --git a/examples/acrobat_2023/valis/preprocessing.py b/examples/acrobat_2023/valis/preprocessing.py deleted file mode 100644 index e734d5b0..00000000 --- a/examples/acrobat_2023/valis/preprocessing.py +++ /dev/null @@ -1,1043 +0,0 @@ -""" -Collection of pre-processing methods for aligning images -""" -from scipy.interpolate import Akima1DInterpolator -from skimage import exposure, filters, measure, morphology, restoration -from sklearn.cluster import estimate_bandwidth, MiniBatchKMeans, MeanShift -import numpy as np -import cv2 -from skimage import color as skcolor -import pyvips -import colour -from scipy import ndimage - -from . import slide_io -from . import warp_tools - -# DEFAULT_COLOR_STD_C = 0.01 # jzazbz -DEFAULT_COLOR_STD_C = 0.2 # cam16-ucs - - -class ImageProcesser(object): - """Process images for registration - - `ImageProcesser` sub-classes processes images to single channel - images which are then used in image registration. - - Each `ImageProcesser` is initialized with an image, the path to the - image, the pyramid level, and the series number. These values will - be set during the registration process. - - `ImageProcesser` must also have a `process_image` method, which is - called during registration. As `ImageProcesser` has the image and - and its relevant information (filename, level, series) as attributes, - it should be able to access and modify the image as needed. However, - one can also pass extra args and kwargs to `process_image`. As such, - `process_image` will also need to accept args and kwargs. - - Attributes - ---------- - image : ndarray - Image to be processed - - src_f : str - Path to slide/image. - - level : int - Pyramid level to be read. - - series : int - The series to be read. - - """ - - def __init__(self, image, src_f, level, series): - """ - Parameters - ---------- - image : ndarray - Image to be processed - - src_f : str - Path to slide/image. - - level : int - Pyramid level to be read. - - series : int - The series to be read. - - """ - - self.image = image - self.src_f = src_f - self.level = level - self.series = series - - def create_mask(self): - return np.full(self.image.shape[0:2], 255, dtype=np.uint8) - - def process_image(self, *args, **kwargs): - """Pre-process image for registration - - Pre-process image for registration. Processed image should - be a single channel uint8 image. - - Returns - ------- - processed_img : ndarray - Single channel processed copy of `image` - - """ - - -class ChannelGetter(ImageProcesser): - """Select channel from image - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_multichannel(self.image) - - return tissue_mask - - def process_image(self, channel="dapi", adaptive_eq=True, *args, **kwaargs): - reader_cls = slide_io.get_slide_reader(self.src_f, series=self.series) - reader = reader_cls(self.src_f) - if self.image is None: - chnl = reader.get_channel(channel=channel, level=self.level, series=self.series).astype(float) - else: - if self.image.ndim == 2: - # the image is already the channel - chnl = self.image - else: - chnl_idx = reader.get_channel_index(channel) - chnl = self.image[..., chnl_idx] - chnl = exposure.rescale_intensity(chnl, in_range="image", out_range=(0.0, 1.0)) - - if adaptive_eq: - chnl = exposure.equalize_adapthist(chnl) - - chnl = exposure.rescale_intensity(chnl, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return chnl - - -class ColorfulStandardizer(ImageProcesser): - """Standardize the colorfulness of the image - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_rgb(self.image) - - return tissue_mask - - def process_image(self, c=DEFAULT_COLOR_STD_C, invert=True, adaptive_eq=False, *args, **kwargs): - std_rgb = standardize_colorfulness(self.image, c) - std_g = skcolor.rgb2gray(std_rgb) - - if invert: - std_g = 255 - std_g - - if adaptive_eq: - std_g = exposure.equalize_adapthist(std_g/255) - - processed_img = exposure.rescale_intensity(std_g, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return processed_img - - -class Luminosity(ImageProcesser): - """Get luminosity of an RGB image - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_rgb(self.image) - - return tissue_mask - - def process_image(self, *args, **kwaargs): - lum = get_luminosity(self.image) - inv_lum = 255 - lum - processed_img = exposure.rescale_intensity(inv_lum, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return processed_img - - -class BgColorDistance(ImageProcesser): - """Calculate distance between each pixel and the background color - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_rgb(self.image) - - return tissue_mask - - def process_image(self, brightness_q=0.99, *args, **kwargs): - - processed_img, _ = calc_background_color_dist(self.image, brightness_q=brightness_q) - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 1)) - processed_img = exposure.equalize_adapthist(processed_img) - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return processed_img - -class StainFlattener(ImageProcesser): - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - - def create_mask(self): - - processed = self.process_image(adaptive_eq=True) - - # Want to ignore black background - to_thresh_mask = 255*(np.all(self.image > 25, axis=2)).astype(np.uint8) - - low_t, high_t = filters.threshold_multiotsu(processed[to_thresh_mask > 0]) - tissue_mask = 255*filters.apply_hysteresis_threshold(processed, low_t, high_t).astype(np.uint8) - - kernel_size=3 - tissue_mask = mask2contours(tissue_mask, kernel_size) - - return tissue_mask - - def process_image_with_mask(self, n_stains=100, q=95): - fg_mask, _ = create_tissue_mask_from_rgb(self.image) - mean_bg_rgb = np.mean(self.image[fg_mask == 0], axis=0) - - # Get stain vectors - fg_rgb = self.image[fg_mask > 0] - fg_to_cluster = rgb2jab(fg_rgb) - - if n_stains > 0: - clusterer = MiniBatchKMeans(n_clusters=n_stains, - reassignment_ratio=0, - n_init=3) - else: - bandwidth = estimate_bandwidth(fg_to_cluster, quantile=0.2, n_samples=500) - clusterer = MeanShift(bandwidth=bandwidth, bin_seeding=True) - # clusterer = MiniBatchKMeans(n_init="auto", reassignment_ratio=0) - - clusterer.fit(fg_to_cluster) - - stain_rgb = jab2rgb(clusterer.cluster_centers_) - stain_rgb = np.clip(stain_rgb, 0, 1) - - stain_rgb = np.vstack([255*stain_rgb, mean_bg_rgb]) - D = stainmat2decon(stain_rgb) - deconvolved = deconvolve_img(self.image, D) - - eps = np.finfo("float").eps - d_flat = deconvolved.reshape(-1, deconvolved.shape[2]) - dmax = np.percentile(d_flat, q, axis=0) - for i in range(deconvolved.shape[2]): - c_dmax = dmax[i] + eps - deconvolved[..., i] = np.clip(deconvolved[..., i], 0, c_dmax) - deconvolved[..., i] /= c_dmax - - summary_img = deconvolved.mean(axis=2) - - return summary_img - - def process_image_all(self, n_stains=100, q=95): - img_to_cluster = rgb2jch(self.image) - if n_stains > 0: - clusterer = MiniBatchKMeans(n_clusters=n_stains, - reassignment_ratio=0, - n_init=3) - else: - bandwidth = estimate_bandwidth(img_to_cluster, quantile=0.2, n_samples=500) - clusterer = MeanShift(bandwidth=bandwidth, bin_seeding=True) - # clusterer = MiniBatchKMeans(n_init="auto", reassignment_ratio=0) - - - clusterer.fit(img_to_cluster.reshape(-1, img_to_cluster.shape[2])) - centers = np.clip(clusterer.cluster_centers_, -1, 1) - stain_rgb = jab2rgb(centers) - - stain_rgb = 255*stain_rgb - stain_rgb = np.clip(stain_rgb, 0, 255) - stain_rgb = np.unique(stain_rgb, axis=0) - D = stainmat2decon(stain_rgb) - deconvolved = deconvolve_img(self.image, D) - - d_flat = deconvolved.reshape(-1, deconvolved.shape[2]) - dmax = np.percentile(d_flat, q, axis=0) + np.finfo("float").eps - for i in range(deconvolved.shape[2]): - - deconvolved[..., i] = np.clip(deconvolved[..., i], 0, dmax[i]) - deconvolved[..., i] /= dmax[i] - - summary_img = deconvolved.mean(axis=2) - - return summary_img - - def process_image(self, n_stains=100, q=95, with_mask=True, adaptive_eq=True): - if with_mask: - processed_img = self.process_image_with_mask(n_stains=n_stains, q=q) - else: - processed_img = self.process_image_all(n_stains=n_stains, q=q) - - if adaptive_eq: - processed_img = exposure.equalize_adapthist(processed_img) - - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return processed_img - - -class Gray(ImageProcesser): - """Get luminosity of an RGB image - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_rgb(self.image) - - return tissue_mask - - def process_image(self, *args, **kwaargs): - g = skcolor.rgb2gray(self.image) - processed_img = exposure.rescale_intensity(g, in_range="image", out_range=(0, 255)).astype(np.uint8) - - return processed_img - - -class HEDeconvolution(ImageProcesser): - """Normalize staining appearence of hematoxylin and eosin (H&E) stained image - and get the H or E deconvolution image. - - Reference - --------- - A method for normalizing histology slides for quantitative analysis. M. Macenko et al., ISBI 2009. - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - def create_mask(self): - _, tissue_mask = create_tissue_mask_from_rgb(self.image) - - return tissue_mask - - - def process_image(self, stain="hem", Io=240, alpha=1, beta=0.15, *args, **kwargs): - """ - Reference - --------- - A method for normalizing histology slides for quantitative analysis. M. Macenko et al., ISBI 2009. - - Note - ---- - Adaptation of the code from https://github.com/schaugf/HEnorm_python. - - """ - - normalized_stains_conc = normalize_he(self.image, Io=Io, alpha=alpha, beta=beta) - processed_img = deconvolution_he(self.image, Io=Io, normalized_concentrations=normalized_stains_conc, stain=stain) - - return processed_img - - -def denoise_img(img, mask=None, weight=None): - if mask is None: - sigma = restoration.estimate_sigma(img) - sigma_scale = 40 - else: - sigma = restoration.estimate_sigma(img[mask != 0]) - sigma_scale = 400 - - if weight is None: - weight=sigma/sigma_scale - - denoised_img = restoration.denoise_tv_chambolle(img, weight=weight) - denoised_img = exposure.rescale_intensity(denoised_img, out_range="uint8") - - return denoised_img - - -def standardize_colorfulness(img, c=DEFAULT_COLOR_STD_C, h=0): - """Give image constant colorfulness and hue - - Image is converted to cylindrical CAM-16UCS assigned a constant - hue and colorfulness, and then coverted back to RGB. - - Parameters - ---------- - img : ndarray - Image to be processed - c : int - Colorfulness - h : int - Hue, in radians (-pi to pi) - - Returns - ------- - rgb2 : ndarray - `img` with constant hue and colorfulness - - """ - # Convert to CAM16 # - eps = np.finfo("float").eps - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255 + eps, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(img + eps, 'sRGB', 'CAM16UCS') - - lum = cam[..., 0] - cc = np.full_like(lum, c) - hc = np.full_like(lum, h) - new_a, new_b = cc * np.cos(hc), cc * np.sin(hc) - new_cam = np.dstack([lum, new_a+eps, new_b+eps]) - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb2 = colour.convert(new_cam, 'CAM16UCS', 'sRGB') - rgb2 -= eps - - rgb2 = (np.clip(rgb2, 0, 1)*255).astype(np.uint8) - - return rgb2 - - -def get_luminosity(img, **kwargs): - """Get luminosity of an RGB image - Converts and RGB image to the CAM16-UCS colorspace, extracts the - luminosity, and then scales it between 0-255 - - Parameters - --------- - img : ndarray - RGB image - - Returns - ------- - lum : ndarray - CAM16-UCS luminosity - - """ - - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(img, 'sRGB', 'CAM16UCS') - - lum = exposure.rescale_intensity(cam[..., 0], in_range=(0, 1), out_range=(0, 255)) - - return lum - - -def calc_background_color_dist(img, brightness_q=0.99, mask=None): - """Create mask that only covers tissue - - #. Find background pixel (most luminescent) - #. Convert image to CAM16-UCS - #. Calculate distance between each pixel and background pixel - #. Threshold on distance (i.e. higher distance = different color) - - Returns - ------- - cam_d : float - Distance from background color - cam : float - CAM16UCS image - - """ - eps = np.finfo("float").eps - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255 + eps, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(img + eps, 'sRGB', 'CAM16UCS') - - if mask is None: - brightest_thresh = np.quantile(cam[..., 0], brightness_q) - else: - brightest_thresh = np.quantile(cam[..., 0][mask > 0], brightness_q) - - brightest_idx = np.where(cam[..., 0] >= brightest_thresh) - brightest_pixels = cam[brightest_idx] - bright_cam = brightest_pixels.mean(axis=0) - cam_d = np.sqrt(np.sum((cam - bright_cam)**2, axis=2)) - - return cam_d, cam - - -def normalize_he(img: np.array, Io: int = 240, alpha: int = 1, beta: int = 0.15): - """ Normalize staining appearence of H&E stained images. - - Parameters - ---------- - img : ndarray - 2D RGB image to be transformed, np.array. - Io : int, optional - The transmitted light intensity. The default value is ``240``. - alpha : int, optional - This value is used to get the alpha(th) and (100-alpha)(th) percentile - as robust approximations of the intensity histogram min and max values. - The default value, found empirically, is ``1``. - beta : float, optional - Threshold value used to remove the pixels with a low OD for stability reasons. - The default value, found empirically, is ``0.15``. - - Returns - ------- - normalized_stains_conc : ndarray - The normalized stains vector, np.array<2, im_height*im_width>. - - """ - - max_conc_ref = np.array([1.9705, 1.0308]) - - # reshape image - img = img.reshape((-1, 3)) - - # calculate optical density - opt_density = -np.log((img.astype(np.float)+1)/Io) - - # remove transparent pixels - opt_density_hat = opt_density[~np.any(opt_density v_max[0]: - h_e_vector = np.array((v_min[:, 0], v_max[:, 0])).T - else: - h_e_vector = np.array((v_max[:, 0], v_min[:, 0])).T - - # rows correspond to channels (RGB), columns to OD values - y = np.reshape(opt_density, (-1, 3)).T - - # determine concentrations of the individual stains - stains_conc = np.linalg.lstsq(h_e_vector, y, rcond=None)[0] - - # normalize stains concentrations - max_conc = np.array([np.percentile(stains_conc[0, :], 99), np.percentile(stains_conc[1, :],99)]) - tmp = np.divide(max_conc, max_conc_ref) - normalized_stains_conc = np.divide(stains_conc, tmp[:, np.newaxis]) - - return normalized_stains_conc - - -def deconvolution_he(img: np.array, normalized_concentrations: np.array, stain: str = "hem", Io: int = 240): - """ Unmix the hematoxylin or eosin channel based on their respective normalized concentrations. - - Parameters - ---------- - img : ndarray - 2D RGB image to be transformed, np.array. - stain : str - Either ``hem`` for the hematoxylin stain or ``eos`` for the eosin one. - Io : int, optional - The transmitted light intensity. The default value is ``240``. - - Returns - ------- - out : ndarray - 2D image with a single channel corresponding to the desired stain, np.array. - - """ - # define height and width of image - h, w, _ = img.shape - - # unmix hematoxylin or eosin - if stain == "hem": - out = np.multiply(Io, normalized_concentrations[0,:]) - elif stain == "eos": - out = np.multiply(Io, normalized_concentrations[1,:]) - else: - raise ValueError(f"Stain ``{stain}`` is unknown.") - - np.clip(out, a_min=0, a_max=255, out=out) - out = np.reshape(out, (h, w)).astype(np.float32) - - return out - - -def rgb2jab(rgb, cspace='CAM16UCS'): - eps = np.finfo("float").eps - if np.issubdtype(rgb.dtype, np.integer) and rgb.max() > 1: - rgb01 = rgb/255.0 - else: - rgb01 = rgb - - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - jab = colour.convert(rgb01+eps, 'sRGB', cspace) - - return jab - - -def jab2rgb(jab, cspace='CAM16UCS'): - eps = np.finfo("float").eps - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(jab+eps, cspace, 'sRGB') - - return rgb - - -def rgb2jch(rgb, cspace='CAM16UCS', h_rotation=0): - jab = rgb2jab(rgb, cspace) - jch = colour.models.Jab_to_JCh(jab) - jch[..., 2] += h_rotation - - above_360 = np.where(jch[..., 2] > 360) - if len(above_360[0]) > 0: - jch[..., 2][above_360] = jch[..., 2][above_360] - 360 - - return jch - - -def rgb255_to_rgb1(rgb_img): - if np.issubdtype(rgb_img.dtype, np.integer) or rgb_img.max() > 1: - rgb01 = rgb_img/255.0 - else: - rgb01 = rgb_img - - return rgb01 - - -def rgb2od(rgb_img): - eps = np.finfo("float").eps - rgb01 = rgb255_to_rgb1(rgb_img) - - od = -np.log10(rgb01 + eps) - od[od < 0] = 0 - - return od - - -def stainmat2decon(stain_mat_srgb255): - - od_mat = rgb2od(stain_mat_srgb255) - - M = od_mat / np.linalg.norm(od_mat, axis=1, keepdims=True) - M[np.isnan(M)] = 0 - D = np.linalg.pinv(M) - - return D - - -def deconvolve_img(rgb_img, D): - od_img = rgb2od(rgb_img) - deconvolved_img = np.dot(od_img, D) - deconvolved_img[deconvolved_img < 0] = 0 - - return deconvolved_img - - -def combine_masks_by_hysteresis(mask_list): - """ - Combine masks. Keeps areas where they overlap _and_ touch - """ - m0 = mask_list[0] - if isinstance(m0, pyvips.Image): - mshape = np.array([m0.height, m0.width]) - else: - mshape = m0.shape[0:2] - - to_hyst_mask = np.zeros(mshape) - for m in mask_list: - - if(isinstance(m, pyvips.Image)): - np_mask = warp_tools.vips2numpy(m) - else: - np_mask = m.copy() - - to_hyst_mask[ np_mask > 0] += 1 - - hyst_mask = 255*filters.apply_hysteresis_threshold(to_hyst_mask, 0.5, len(mask_list) - 0.5).astype(np.uint8) - - return hyst_mask - - -def combine_masks(mask1, mask2, op="or"): - if not isinstance(mask1, pyvips.Image): - vmask1 = warp_tools.numpy2vips(mask1) - else: - vmask1 = mask1 - - if not isinstance(mask2, pyvips.Image): - vmask2 = warp_tools.numpy2vips(mask2) - else: - vmask2 = mask2 - - vips_combo_mask = vmask1.bandjoin(vmask2) - if op == "or": - combo_mask = vips_combo_mask.bandor() - else: - combo_mask = vips_combo_mask.bandand() - - if not isinstance(mask1, pyvips.Image): - combo_mask = warp_tools.vips2numpy(combo_mask) - - return combo_mask - -def remove_small_obj_and_lines_by_dist(mask): - """ - Will remove smaller objects and thin lines that - do not interesct with larger objects - """ - - dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 5) - dst_t = filters.threshold_li(dist_transform[mask > 0]) - temp_sure_fg = 255*(dist_transform >= dst_t).astype(np.uint8) - sure_mask = combine_masks_by_hysteresis([mask, temp_sure_fg]) - - return sure_mask - - -def create_edges_mask(labeled_img): - """ - Create two masks, one with objects not touching image borders, - and a second with objects that do touch the border - - """ - unique_v = np.unique(labeled_img) - unique_v = unique_v[unique_v != 0] - if len(unique_v) == 1: - labeled_img = measure.label(labeled_img) - - img_regions = measure.regionprops(labeled_img) - inner_mask = np.zeros(labeled_img.shape, dtype=np.uint8) - edges_mask = np.zeros(labeled_img.shape, dtype=np.uint8) - for regn in img_regions: - on_border_idx = np.where((regn.coords[:, 0] == 0) | - (regn.coords[:, 0] == labeled_img.shape[0]-1) | - (regn.coords[:, 1] == 0) | - (regn.coords[:, 1] == labeled_img.shape[1]-1) - )[0] - if len(on_border_idx) == 0: - inner_mask[regn.coords[:, 0], regn.coords[:, 1]] = 255 - else: - edges_mask[regn.coords[:, 0], regn.coords[:, 1]] = 255 - - return inner_mask, edges_mask - - -def create_tissue_mask_from_rgb(img, brightness_q=0.99, kernel_size=3, gray_thresh=0.075, light_gray_thresh=0.875, dark_gray_thresh=0.7): - """Create mask that only covers tissue - - Also remove dark regions on the edge of the slide, which could be artifacts - - Parameters - ---------- - grey_thresh : float - Colorfulness values (from JCH) below this are considered "grey", and thus possibly dirt, hair, coverslip edges, etc... - - light_gray_thresh : float - Upper limit for light gray - - dark_gray_thresh : float - Upper limit for dark gray - - Returns - ------- - tissue_mask : ndarray - Mask covering tissue - - concave_tissue_mask : ndarray - Similar to `tissue_mask`, but each region is replaced by a concave hull. - Covers more area - - """ - # Ignore artifacts that could throw off thresholding. These are often greyish in color - - jch = rgb2jch(img) - light_greys = 255*((jch[..., 1] < gray_thresh) & (jch[..., 0] < light_gray_thresh)).astype(np.uint8) - dark_greys = 255*((jch[..., 1] < gray_thresh) & (jch[..., 0] < dark_gray_thresh)).astype(np.uint8) - grey_mask = combine_masks_by_hysteresis([light_greys, dark_greys]) - - color_mask = 255 - grey_mask - - cam_d, cam = calc_background_color_dist(img, brightness_q=brightness_q, mask=color_mask) - - # Reduce intensity of thick horizontal and vertial lines, usually artifacts like edges, streaks, folds, etc... - vert_knl = np.ones((1, 5)) - no_v_lines = morphology.opening(cam_d, vert_knl) - - horiz_knl = np.ones((5, 1)) - no_h_lines = morphology.opening(cam_d, horiz_knl) - cam_d_no_lines = np.dstack([no_v_lines, no_h_lines]).min(axis=2) - - # Foreground is where color is different than backaground color - cam_d_t, _ = filters.threshold_multiotsu(cam_d_no_lines[grey_mask == 0]) - tissue_mask = np.zeros(cam_d_no_lines.shape, dtype=np.uint8) - tissue_mask[cam_d_no_lines >= cam_d_t] = 255 - - concave_tissue_mask = mask2contours(tissue_mask, kernel_size) - - return tissue_mask, concave_tissue_mask - - -def create_tissue_mask_from_multichannel(img, kernel_size=3): - """ - Get foreground of multichannel imaage - """ - - tissue_mask = np.zeros(img.shape[:2], dtype=np.uint8) - if img.ndim > 2: - for i in range(img.shape[2]): - chnl_t = np.quantile(img[..., i], 0.01) - tissue_mask[img[..., i] > chnl_t] = 255 - - else: - t = np.quantile(img, 0.01) - tissue_mask[img > t] = 255 - tissue_mask = 255*ndimage.binary_fill_holes(tissue_mask).astype(np.uint8) - concave_tissue_mask = mask2contours(tissue_mask, kernel_size=kernel_size) - - return tissue_mask, concave_tissue_mask - - -def create_tissue_mask(img, is_rgb=True): - """ - Returns - ------- - tissue_mask : ndarray - Mask covering tissue - - concave_tissue_mask : ndarray - Similar to `tissue_mask`, but each region is replaced by a concave hull - - """ - if is_rgb: - tissue_mask, concave_tissue_mask = create_tissue_mask_from_rgb(img) - else: - tissue_mask, concave_tissue_mask = create_tissue_mask_from_multichannel(img) - - return tissue_mask, concave_tissue_mask - - -def mask2covexhull(mask): - labeled_mask = measure.label(mask) - mask_regions = measure.regionprops(labeled_mask) - concave_mask = np.zeros_like(mask) - for region in mask_regions: - r0, c0, r1, c1 = region.bbox - concave_mask[r0:r1, c0:c1] += region.convex_image.astype(np.uint8) - - concave_mask[concave_mask != 0] = 255 - concave_mask = 255*ndimage.binary_fill_holes(concave_mask).astype(np.uint8) - - return concave_mask - - -def mask2bbox_mask(mask, merge_bbox=True): - """ - Replace objects in mask with bounding boxes. If `combine_bbox` - is True, then bounding boxes will merged if they are touching, - and the bounding box will be drawn around those overlapping boxes. - """ - - n_regions = -1 - n_prev_regions = 0 - max_iter = 10000 - i = 0 - updated_mask = mask.copy() - while n_regions != n_prev_regions: - n_prev_regions = n_regions - labeled_mask = measure.label(updated_mask) - bbox_mask = np.zeros_like(updated_mask) - regions = measure.regionprops(labeled_mask) - for r in regions: - r0, c0, r1, c1 = r.bbox - bbox_mask[r0:r1, c0:c1] = 255 - - n_regions = len(regions) - updated_mask = bbox_mask - - if not merge_bbox: - break - - i += 1 - if i > max_iter: - break - - return updated_mask - - -def mask2contours(mask, kernel_size=3): - kernel = morphology.disk(kernel_size) - mask_dilated = cv2.dilate(mask, kernel) - contours, _ = cv2.findContours(mask_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - contour_mask = np.zeros_like(mask_dilated) - for cnt in contours: - cv2.drawContours(contour_mask, [cnt], 0, 255, -1) - - return contour_mask - - -def match_histograms(src_image, ref_histogram, bins=256): - """ - Source: https://automaticaddison.com/how-to-do-histogram-matching-using-opencv/ - - - This method matches the source image histogram to the - reference signal - :param image src_image: The original source image - :param image ref_image: The reference image - :return: image_after_matching - :rtype: image (array) - """ - def calculate_cdf(histogram): - """ - This method calculates the cumulative distribution function - :param array histogram: The values of the histogram - :return: normalized_cdf: The normalized cumulative distribution function - :rtype: array - """ - # Get the cumulative sum of the elements - cdf = histogram.cumsum() - - # Normalize the cdf - normalized_cdf = cdf / float(cdf.max()) - - return normalized_cdf - - def calculate_lookup(src_cdf, ref_cdf): - """ - This method creates the lookup table - :param array src_cdf: The cdf for the source image - :param array ref_cdf: The cdf for the reference image - :return: lookup_table: The lookup table - :rtype: array - """ - lookup_table = np.zeros(256) - lookup_val = 0 - for src_pixel_val in range(len(src_cdf)): - lookup_val - for ref_pixel_val in range(len(ref_cdf)): - if ref_cdf[ref_pixel_val] >= src_cdf[src_pixel_val]: - lookup_val = ref_pixel_val - break - lookup_table[src_pixel_val] = lookup_val - return lookup_table - - # Split the images into the different color channels - src_hist, _ = np.histogram(src_image.flatten(), bins) - - # Compute the normalized cdf for the source and reference image - src_cdf = calculate_cdf(src_hist) - ref_cdf = calculate_cdf(ref_histogram) - - # Make a separate lookup table for each color - lookup_table = calculate_lookup(src_cdf, ref_cdf) - - # Use the lookup function to transform the colors of the original - # source image - src_after_transform = cv2.LUT(src_image, lookup_table) - image_after_matching = cv2.convertScaleAbs(src_after_transform) - - return image_after_matching - - -def get_channel_stats(img): - img_stats = [None] * 5 - img_stats[0] = np.percentile(img, 1) - img_stats[1] = np.percentile(img, 5) - img_stats[2] = np.mean(img) - img_stats[3] = np.percentile(img, 95) - img_stats[4] = np.percentile(img, 99) - - return np.array(img_stats) - - -def norm_img_stats(img, target_stats, mask=None): - """Normalize an image - - Image will be normalized to have same stats as `target_stats` - - Based on method in - "A nonlinear mapping approach to stain normalization in digital histopathology - images using image-specific color deconvolution.", Khan et al. 2014 - - Assumes that `img` values range between 0-255 - - """ - - if mask is None: - src_stats_flat = get_channel_stats(img) - else: - - if isinstance(mask, pyvips.Image): - np_mask = warp_tools.vips2numpy(mask) - else: - np_mask = mask - - src_stats_flat = get_channel_stats(img[np_mask > 0]) - - # Avoid duplicates and keep in ascending order - lower_knots = np.array([0]) - upper_knots = np.array([300, 350, 400, 450]) - src_stats_flat = np.hstack([lower_knots, src_stats_flat, upper_knots]).astype(float) - target_stats_flat = np.hstack([lower_knots, target_stats, upper_knots]).astype(float) - - # Add epsilon to avoid duplicate values - eps = 10*np.finfo(float).resolution - eps_array = np.arange(len(src_stats_flat)) * eps - src_stats_flat = src_stats_flat + eps_array - target_stats_flat = target_stats_flat + eps_array - - # Make sure src stats are in ascending order - src_order = np.argsort(src_stats_flat) - src_stats_flat = src_stats_flat[src_order] - target_stats_flat = target_stats_flat[src_order] - - cs = Akima1DInterpolator(src_stats_flat, target_stats_flat) - - if mask is None: - normed_img = cs(img.reshape(-1)).reshape(img.shape) - else: - normed_img = img.copy() - fg_px = np.where(np_mask > 0) - normed_img[fg_px] = cs(img[fg_px]) - - if img.dtype == np.uint8: - normed_img = np.clip(normed_img, 0, 255) - - return normed_img - diff --git a/examples/acrobat_2023/valis/registration.py b/examples/acrobat_2023/valis/registration.py deleted file mode 100644 index 0da8c863..00000000 --- a/examples/acrobat_2023/valis/registration.py +++ /dev/null @@ -1,4627 +0,0 @@ -""" -Classes and functions to register a collection of images -""" - -import traceback -import re -import os -import numpy as np -import pathlib -from skimage import transform, exposure, filters -from time import time -import tqdm -import pandas as pd -import pickle -import colour -import pyvips -from scipy import ndimage -import shapely -from copy import deepcopy -from pprint import pformat -import json - -from . import feature_matcher -from . import serial_rigid -from . import feature_detectors -from . import non_rigid_registrars -from . import valtils -from . import preprocessing -from . import slide_tools -from . import slide_io -from . import viz -from . import warp_tools -from . import serial_non_rigid - -pyvips.cache_set_max(0) - -# Destination directories # -CONVERTED_IMG_DIR = "images" -PROCESSED_IMG_DIR = "processed" -RIGID_REG_IMG_DIR = "rigid_registration" -NON_RIGID_REG_IMG_DIR = "non_rigid_registration" -DEFORMATION_FIELD_IMG_DIR = "deformation_fields" -OVERLAP_IMG_DIR = "overlaps" -REG_RESULTS_DATA_DIR = "data" -MICRO_REG_DIR = "micro_registration" -DISPLACEMENT_DIRS = os.path.join(REG_RESULTS_DATA_DIR, "displacements") -MASK_DIR = "masks" - -# Default image processing # -DEFAULT_BRIGHTFIELD_CLASS = preprocessing.ColorfulStandardizer -DEFAULT_BRIGHTFIELD_PROCESSING_ARGS = {'c': preprocessing.DEFAULT_COLOR_STD_C, "h": 0} -DEFAULT_FLOURESCENCE_CLASS = preprocessing.ChannelGetter -DEFAULT_FLOURESCENCE_PROCESSING_ARGS = {"channel": "dapi", "adaptive_eq": True} -DEFAULT_NORM_METHOD = "img_stats" - -# Default rigid registration parameters # -DEFAULT_FD = feature_detectors.VggFD -DEFAULT_TRANSFORM_CLASS = transform.SimilarityTransform -DEFAULT_MATCH_FILTER = feature_matcher.Matcher(match_filter_method=feature_matcher.RANSAC_NAME) -DEFAULT_SIMILARITY_METRIC = "n_matches" -DEFAULT_AFFINE_OPTIMIZER_CLASS = None -DEFAULT_MAX_PROCESSED_IMG_SIZE = 850 -DEFAULT_MAX_IMG_DIM = 850 -DEFAULT_THUMBNAIL_SIZE = 500 -DEFAULT_MAX_NON_RIGID_REG_SIZE = 3000 - -# Tiled non-rigid registration arguments -TILER_THRESH_GB = 10 -DEFAULT_NR_TILE_WH = 512 - -# Rigid registration kwarg keys # -AFFINE_OPTIMIZER_KEY = "affine_optimizer" -TRANSFORMER_KEY = "transformer" -SIM_METRIC_KEY = "similarity_metric" -FD_KEY = "feature_detector" -MATCHER_KEY = "matcher" -NAME_KEY = "name" -IMAGES_ORDERD_KEY = "imgs_ordered" -REF_IMG_KEY = "reference_img_f" -QT_EMMITER_KEY = "qt_emitter" -TFORM_SRC_SHAPE_KEY = "transformation_src_shape_rc" -TFORM_DST_SHAPE_KEY = "transformation_dst_shape_rc" -TFORM_MAT_KEY = "M" -CHECK_REFLECT_KEY = "check_for_reflections" - -# Rigid registration kwarg keys # -NON_RIGID_REG_CLASS_KEY = "non_rigid_reg_class" -NON_RIGID_REG_PARAMS_KEY = "non_rigid_reg_params" -NON_RIGID_USE_XY_KEY = "moving_to_fixed_xy" -NON_RIGID_COMPOSE_KEY = "compose_transforms" - -# Default non-rigid registration parameters # -DEFAULT_NON_RIGID_CLASS = non_rigid_registrars.OpticalFlowWarper -DEFAULT_NON_RIGID_KWARGS = {} - -# Cropping options -CROP_OVERLAP = "overlap" -CROP_REF = "reference" -CROP_NONE = "all" - - -def init_jvm(jar=None, mem_gb=10): - """Initialize JVM for BioFormats - """ - slide_io.init_jvm(jar=None, mem_gb=10) - - -def kill_jvm(): - """Kill JVM for BioFormats - """ - slide_io.kill_jvm() - - -def load_registrar(src_f): - """Load a Valis object - - Parameters - ---------- - src_f : string - Path to pickled Valis object - - Returns - ------- - registrar : Valis - - Valis object used for registration - - """ - registrar = pickle.load(open(src_f, 'rb')) - - data_dir = registrar.data_dir - read_data_dir = os.path.split(src_f)[0] - - # If registrar has moved, will need to update paths to results - # and displacement fields - if data_dir != read_data_dir: - new_dst_dir = os.path.split(read_data_dir)[0] - registrar.dst_dir = new_dst_dir - registrar.set_dst_paths() - - for slide_obj in registrar.slide_dict.values(): - slide_obj.update_results_img_paths() - - return registrar - - -class Slide(object): - """Stores registration info and warps slides/points - - `Slide` is a class that stores registration parameters - and other metadata about a slide. Once registration has been - completed, `Slide` is also able warp the slide and/or points - using the same registration parameters. Warped slides can be saved - as ome.tiff images with valid ome-xml. - - Attributes - ---------- - src_f : str - Path to slide. - - image: ndarray - Image to registered. Taken from a level in the image pyramid. - However, image may be resized to fit within the `max_image_dim_px` - argument specified when creating a `Valis` object. - - val_obj : Valis - The "parent" object that registers all of the slide. - - reader : SlideReader - Object that can read slides and collect metadata. - - original_xml : str - Xml string created by bio-formats - - img_type : str - Whether the image is "brightfield" or "fluorescence" - - is_rgb : bool - Whether or not the slide is RGB. - - slide_shape_rc : tuple of int - Dimensions of the largest resolution in the slide, in the form - of (row, col). - - series : int - Slide series to be read - - slide_dimensions_wh : ndarray - Dimensions of all images in the pyramid (width, height). - - resolution : float - Physical size of each pixel. - - units : str - Physical unit of each pixel. - - name : str - Name of the image. Usually `img_f` but with the extension removed. - - processed_img : ndarray - Image used to perform registration - - rigid_reg_mask : ndarray - Mask of convex hulls covering tissue in unregistered image. - Could be used to mask `processed_img` before rigid registration - - non_rigid_reg_mask : ndarray - Created by combining rigidly warped `rigid_reg_mask` in all - other slides. - - stack_idx : int - Position of image in sorted Z-stack - - processed_img_f : str - Path to thumbnail of the processed `image`. - - rigid_reg_img_f : str - Path to thumbnail of rigidly aligned `image`. - - non_rigid_reg_img_f : str - Path to thumbnail of non-rigidly aligned `image`. - - processed_img_shape_rc : tuple of int - Shape (row, col) of the processed image used to find the - transformation parameters. Maximum dimension will be less or - equal to the `max_processed_image_dim_px` specified when - creating a `Valis` object. As such, this may be smaller than - the image's shape. - - aligned_slide_shape_rc : tuple of int - Shape (row, col) of aligned slide, based on the dimensions in the 0th - level of they pyramid. In - - reg_img_shape_rc : tuple of int - Shape (row, col) of the registered image - - M : ndarray - Rigid transformation matrix that aligns `image` to the previous - image in the stack. Found using the processed copy of `image`. - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions. dx = bk_dxdy[0], and dy=bk_dxdy[1]. Used - to warp images. Found using the rigidly aligned version of the - processed image. - - fwd_dxdy : ndarray - Inverse of `bk_dxdy`. Used to warp points. - - _bk_dxdy_f : str - Path to file containing bk_dxdy, if saved - - _fwd_dxdy_f : str - Path to file containing fwd_dxdy, if saved - - _bk_dxdy_np : ndarray - `bk_dxdy` as a numpy array. Only not None if `bk_dxdy` becomes - associated with a file - - _fwd_dxdy_np : ndarray - `fwd_dxdy` as a numpy array. Only not None if `fwd_dxdy` becomes - associated with a file - - stored_dxdy : bool - Whether or not the non-rigid displacements are saved in a file - Should only occur if image is very large. - - fixed_slide : Slide - Slide object to which this one was aligned. - - xy_matched_to_prev : ndarray - Coordinates (x, y) of features in `image` that had matches in the - previous image. Will have shape (N, 2) - - xy_in_prev : ndarray - Coordinates (x, y) of features in the previous that had matches - to those in `image`. Will have shape (N, 2) - - xy_matched_to_prev_in_bbox : ndarray - Subset of `xy_matched_to_prev` that were within `overlap_mask_bbox_xywh`. - Will either have shape (N, 2) or (M, 2), with M < N. - - xy_in_prev_in_bbox : ndarray - Subset of `xy_in_prev` that were within `overlap_mask_bbox_xywh`. - Will either have shape (N, 2) or (M, 2), with M < N. - - crop : str - Crop method - - bg_px_pos_rc : tuple - Position of pixel that has the background color - - bg_color : list, optional - Color of background pixels - - is_empty : bool - True if the image is empty (i.e. contains only 1 value) - - """ - - def __init__(self, src_f, image, val_obj, reader, name=None): - """ - Parameters - ---------- - src_f : str - Path to slide. - - image: ndarray - Image to registered. Taken from a level in the image pyramid. - However, image may be resized to fit within the `max_image_dim_px` - argument specified when creating a `Valis` object. - - val_obj : Valis - The "parent" object that registers all of the slide. - - reader : SlideReader - Object that can read slides and collect metadata. - - name : str, optional - Name of slide. If None, it will be `src_f` with the extension removed - - """ - - self.src_f = src_f - self.image = image - self.val_obj = val_obj - self.reader = reader - - # Metadata # - self.is_rgb = reader.metadata.is_rgb - self.img_type = reader.guess_image_type() - self.slide_shape_rc = reader.metadata.slide_dimensions[0][::-1] - self.series = reader.series - self.slide_dimensions_wh = reader.metadata.slide_dimensions - self.resolution = np.mean(reader.metadata.pixel_physical_size_xyu[0:2]) - self.units = reader.metadata.pixel_physical_size_xyu[2] - self.original_xml = reader.metadata.original_xml - - if name is None: - name = valtils.get_name(src_f) - - self.name = name - - # To be filled in during registration # - self.processed_img = None - self.rigid_reg_mask = None - self.non_rigid_reg_mask = None - self.stack_idx = None - - self.aligned_slide_shape_rc = None - self.processed_img_shape_rc = None - self.reg_img_shape_rc = None - self.M = None - self.bk_dxdy = None - self.fwd_dxdy = None - - self.stored_dxdy = False - self._bk_dxdy_f = None - self._fwd_dxdy_f = None - self._bk_dxdy_np = None - self._fwd_dxdy_np = None - self.processed_img_f = None - self.rigid_reg_img_f = None - self.non_rigid_reg_img_f = None - - self.fixed_slide = None - self.xy_matched_to_prev = None - self.xy_in_prev = None - self.xy_matched_to_prev_in_bbox = None - self.xy_in_prev_in_bbox = None - - self.crop = None - self.bg_px_pos_rc = (0, 0) - self.bg_color = None - - self.is_empty = self.check_if_empty(image) - - def check_if_empty(self, img): - """Check if the image is empty - - Return - ------ - is_empty : bool - Whether or not the image is empty - - """ - - is_empty = img.min() == img.max() - - return is_empty - - def slide2image(self, level, series=None, xywh=None): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - img = self.reader.slide2image(level=level, series=series, xywh=xywh) - - return img - - def slide2vips(self, level, series=None, xywh=None): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - vips_img = self.reader.slide2vips(level=level, series=series, xywh=xywh) - - return vips_img - - def get_aligned_to_ref_slide_crop_xywh(self, ref_img_shape_rc, ref_M, scaled_ref_img_shape_rc=None): - """Get bounding box used to crop slide to fit in reference image - - Parameters - ---------- - ref_img_shape_rc : tuple of int - shape of reference image used to find registration parameters, i.e. processed image) - - ref_M : ndarray - Transformation matrix for the reference image - - scaled_ref_img_shape_rc : tuple of int, optional - shape of scaled image with shape `img_shape_rc`, i.e. slide corresponding - to the image used to find the registration parameters. - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - mask : ndarray - Mask covering reference image - - """ - - mask , _ = self.val_obj.get_crop_mask(CROP_REF) - - if scaled_ref_img_shape_rc is not None: - sxy = np.array([*scaled_ref_img_shape_rc[::-1]]) / np.array([*ref_img_shape_rc[::-1]]) - else: - scaled_ref_img_shape_rc = ref_img_shape_rc - sxy = np.ones(2) - - reg_txy = -ref_M[0:2, 2] - slide_xywh = (*reg_txy*sxy, *scaled_ref_img_shape_rc[::-1]) - - return slide_xywh, mask - - def get_overlap_crop_xywh(self, warped_img_shape_rc, scaled_warped_img_shape_rc=None): - """Get bounding box used to crop slide to where all slides overlap - - Parameters - ---------- - warped_img_shape_rc : tuple of int - shape of registered image - - warped_scaled_img_shape_rc : tuple of int, optional - shape of scaled registered image (i.e. registered slied) - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - """ - mask , mask_bbox_xywh = self.val_obj.get_crop_mask(CROP_OVERLAP) - - if scaled_warped_img_shape_rc is not None: - sxy = np.array([*scaled_warped_img_shape_rc[::-1]]) / np.array([*warped_img_shape_rc[::-1]]) - else: - sxy = np.ones(2) - - to_slide_transformer = transform.SimilarityTransform(scale=sxy) - overlap_bbox = warp_tools.bbox2xy(mask_bbox_xywh) - scaled_overlap_bbox = to_slide_transformer(overlap_bbox) - scaled_overlap_xywh = warp_tools.xy2bbox(scaled_overlap_bbox) - - scaled_overlap_xywh[2:] = np.ceil(scaled_overlap_xywh[2:]) - scaled_overlap_xywh = tuple(scaled_overlap_xywh.astype(int)) - - return scaled_overlap_xywh, mask - - def get_crop_xywh(self, crop, out_shape_rc=None): - """Get bounding box used to crop aligned slide - - Parameters - ---------- - - out_shape_rc : tuple of int, optional - If crop is "reference", this should be the shape of scaled reference image, such - as the unwarped slide that corresponds to the unwarped processed reference image. - - If crop is "overlap", this should be the shape of the registered slides. - - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - mask : ndarray - Mask, before crop - """ - - ref_slide = self.val_obj.get_ref_slide() - if crop == CROP_REF: - transformation_shape_rc = np.array(ref_slide.processed_img_shape_rc) - crop_xywh, mask = self.get_aligned_to_ref_slide_crop_xywh(ref_img_shape_rc=transformation_shape_rc, - ref_M=ref_slide.M, - scaled_ref_img_shape_rc=out_shape_rc) - elif crop == CROP_OVERLAP: - transformation_shape_rc = np.array(ref_slide.reg_img_shape_rc) - crop_xywh, mask = self.get_overlap_crop_xywh(warped_img_shape_rc=transformation_shape_rc, - scaled_warped_img_shape_rc=out_shape_rc) - - return crop_xywh, mask - - def get_crop_method(self, crop): - """Get string or logic defining how to crop the image - """ - if crop is True: - crop_method = self.crop - else: - crop_method = crop - - do_crop = crop_method in [CROP_REF, CROP_OVERLAP] - - if do_crop: - return crop_method - else: - return False - - def get_bg_color_px_pos(self): - """Get position of pixel that has color used for background - """ - if self.img_type == slide_tools.IHC_NAME: - # RGB. Get brightest pixel - eps = np.finfo("float").eps - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < self.image.max() <= 255 and np.issubdtype(self.image.dtype, np.integer): - cam = colour.convert(self.image/255 + eps, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(self.image + eps, 'sRGB', 'CAM16UCS') - - lum = cam[..., 0] - bg_px = np.unravel_index(np.argmax(lum, axis=None), lum.shape) - else: - # IF. Get darkest pixel - sum_img = self.image.sum(axis=2) - bg_px = np.unravel_index(np.argmin(sum_img, axis=None), sum_img.shape) - - self.bg_px_pos_rc = bg_px - self.bg_color = list(self.image[bg_px]) - - def update_results_img_paths(self): - n_digits = len(str(self.val_obj.size)) - stack_id = str.zfill(str(self.stack_idx), n_digits) - - self.processed_img_f = os.path.join(self.val_obj.processed_dir, self.name + ".png") - self.rigid_reg_img_f = os.path.join(self.val_obj.reg_dst_dir, f"{stack_id}_f{self.name}.png") - self.non_rigid_reg_img_f = os.path.join(self.val_obj.non_rigid_dst_dir, f"{stack_id}_f{self.name}.png") - if self.stored_dxdy: - bk_dxdy_f, fwd_dxdy_f = self.get_displacement_f() - self._bk_dxdy_f = bk_dxdy_f - self._fwd_dxdy_f = fwd_dxdy_f - - def get_displacement_f(self): - bk_dxdy_f = os.path.join(self.val_obj.displacements_dir, f"{self.name}_bk_dxdy.tiff") - fwd_dxdy_f = os.path.join(self.val_obj.displacements_dir, f"{self.name}_fwd_dxdy.tiff") - - return bk_dxdy_f, fwd_dxdy_f - - def get_bk_dxdy(self): - if self.stored_dxdy: - bk_dxdy_f, _ = self.get_displacement_f() - cropped_bk_dxdy = pyvips.Image.new_from_file(bk_dxdy_f) - full_bk_dxdy = self.val_obj.pad_displacement(cropped_bk_dxdy, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - - return full_bk_dxdy - else: - return self._bk_dxdy_np - - def set_bk_dxdy(self, bk_dxdy): - """ - Only set if an array - """ - if not isinstance(bk_dxdy, pyvips.Image): - self._bk_dxdy_np = bk_dxdy - else: - print(f"Cannot set bk_dxdy when data is type {type(bk_dxdy)}") - - bk_dxdy = property(fget=get_bk_dxdy, - fset=set_bk_dxdy, - doc="Get and set backwards displacements") - - def get_fwd_dxdy(self): - if self.stored_dxdy: - _, fwd_dxdy_f = self.get_displacement_f() - cropped_fwd_dxdy = pyvips.Image.new_from_file(fwd_dxdy_f) - full_fwd_dxdy = self.val_obj.pad_displacement(cropped_fwd_dxdy, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - - return full_fwd_dxdy - - else: - return self._fwd_dxdy_np - - def set_fwd_dxdy(self, fwd_dxdy): - if not isinstance(fwd_dxdy, pyvips.Image): - self._fwd_dxdy_np = fwd_dxdy - else: - print(f"Cannot set fwd_dxdy when data is type {type(fwd_dxdy)}") - - fwd_dxdy = property(fget=get_fwd_dxdy, - fset=set_fwd_dxdy, - doc="Get forward displacements") - - def warp_img(self, img=None, non_rigid=True, crop=True, interp_method="bicubic"): - """Warp an image using the registration parameters - - img : ndarray, optional - The image to be warped. If None, then Slide.image - will be warped. - - non_rigid : bool - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - Returns - ------- - warped_img : ndarray - Warped copy of `img` - - """ - - if img is None: - img = self.image - - if non_rigid: - dxdy = self.bk_dxdy - else: - dxdy = None - - if isinstance(img, pyvips.Image): - img_shape_rc = (img.width, img.height) - img_dim = img.bands - else: - img_shape_rc = img.shape[0:2] - img_dim = img.ndim - - if not np.all(img_shape_rc == self.processed_img_shape_rc): - msg = ("scaling transformation for image with different shape. " - "However, without knowing all of other image's shapes, " - "the scaling may not be the same for all images, and so " - "may not overlap." - ) - valtils.print_warning(msg) - same_shape = False - img_scale_rc = np.array(img_shape_rc)/(np.array(self.processed_img_shape_rc)) - out_shape_rc = self.val_obj.get_aligned_slide_shape(img_scale_rc) - - - else: - same_shape = True - out_shape_rc = self.reg_img_shape_rc - - if isinstance(crop, bool) or isinstance(crop, str): - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - if not same_shape: - scaled_shape_rc = np.array(ref_slide.processed_img_shape_rc)*img_scale_rc - else: - scaled_shape_rc = ref_slide.processed_img_shape_rc - elif crop_method == CROP_OVERLAP: - scaled_shape_rc = out_shape_rc - - bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_shape_rc) - else: - bbox_xywh = None - - elif isinstance(crop[0], (int, float)) and len(crop) == 4: - bbox_xywh = crop - else: - bbox_xywh = None - - if img_dim == self.image.ndim: - bg_color = self.bg_color - else: - bg_color = None - - warped_img = \ - warp_tools.warp_img(img, M=self.M, - bk_dxdy=dxdy, - out_shape_rc=out_shape_rc, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - bbox_xywh=bbox_xywh, - bg_color=bg_color, - interp_method=interp_method) - - return warped_img - - def warp_img_from_to(self, img, to_slide_obj, - dst_slide_level=0, non_rigid=True, interp_method="bicubic", bg_color=None): - - """Warp an image from this slide onto another unwarped slide - - Note that if `img` is a labeled image then it is recommended to set `interp_method` to "nearest" - - Parameters - ---------- - img : ndarray, pyvips.Image - Image to warp. Should be a scaled version of the same one used for registration - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image that `img` will be warped on to - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - """ - - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(dst_slide_level) - else: - - to_slide_src_shape_rc = np.array(dst_slide_level) - - dst_scale_rc = (to_slide_src_shape_rc/np.array(to_slide_obj.processed_img_shape_rc)) - aligned_slide_shape = np.round(dst_scale_rc*np.array(to_slide_obj.reg_img_shape_rc)).astype(int) - - if non_rigid: - from_bk_dxdy = self.bk_dxdy - to_fwd_dxdy = to_slide_obj.fwd_dxdy - - else: - from_bk_dxdy = None - to_fwd_dxdy = None - - warped_img = \ - warp_tools.warp_img_from_to(img, - from_M=self.M, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_bk_dxdy=from_bk_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_fwd_dxdy=to_fwd_dxdy, - bg_color=bg_color, - interp_method=interp_method - ) - - return warped_img - - @valtils.deprecated_args(crop_to_overlap="crop") - def warp_slide(self, level, non_rigid=True, crop=True, - src_f=None, interp_method="bicubic"): - """Warp a slide using registration parameters - - Parameters - ---------- - level : int - Pyramid level to be warped - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - src_f : str, optional - Path of slide to be warped. If None (the default), Slide.src_f - will be used. Otherwise, the file to which `src_f` points to should - be an alternative copy of the slide, such as one that has undergone - processing (e.g. stain segmentation), has a mask applied, etc... - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - """ - if src_f is None: - src_f = self.src_f - - if non_rigid: - bk_dxdy = self.bk_dxdy - else: - bk_dxdy = None - - if level != 0: - if not np.issubdtype(type(level), np.integer): - msg = "Need slide level to be an integer indicating pyramid level" - valtils.print_warning(msg) - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if isinstance(crop, bool) or isinstance(crop, str): - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[level][::-1] - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - slide_bbox_xywh, _ = self.get_crop_xywh(crop=crop_method, - out_shape_rc=scaled_aligned_shape_rc) - if crop_method == CROP_REF: - assert np.all(slide_bbox_xywh[2:]==scaled_aligned_shape_rc[::-1]) - else: - slide_bbox_xywh = None - - elif isinstance(crop[0], (int, float)) and len(crop) == 4: - slide_bbox_xywh = crop - else: - slide_bbox_xywh = None - - if src_f == self.src_f: - bg_color = self.bg_color - else: - bg_color = None - - warped_slide = slide_tools.warp_slide(src_f, M=self.M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - aligned_slide_shape_rc=aligned_slide_shape, - dxdy=bk_dxdy, level=level, series=self.series, - interp_method=interp_method, - bbox_xywh=slide_bbox_xywh, - bg_color=bg_color) - return warped_slide - - @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") - def warp_and_save_slide(self, dst_f, level=0, non_rigid=True, - crop=True, src_f=None, - channel_names=None, - colormap=None, - interp_method="bicubic", - tile_wh=None, compression="lzw"): - - """Warp and save a slide - - Slides will be saved in the ome.tiff format. - - Parameters - ---------- - dst_f : str - Path to were the warped slide will be saved. - - level : int - Pyramid level to be warped - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initializing the `Valis object`. - - channel_names : list, optional - List of channel names. If None, then Slide.reader - will attempt to find the channel names associated with `src_f`. - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added - - src_f : str, optional - Path of slide to be warped. If None (the default), Slide.src_f - will be used. Otherwise, the file to which `src_f` points to should - be an alternative copy of the slide, such as one that has undergone - processing (e.g. stain segmentation), has a mask applied, etc... - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - tile_wh : int, optional - Tile width and height used to save image - - compression : str - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See pyips for more details. - - """ - - warped_slide = self.warp_slide(level=level, non_rigid=non_rigid, - crop=crop, - interp_method=interp_method, - src_f=src_f) - - # Get ome-xml # - # slide_meta = self.reader.metadata - slide_reader_cls = slide_io.get_slide_reader(src_f) - slide_reader = slide_reader_cls(src_f) - slide_meta = slide_reader.metadata - if slide_meta.pixel_physical_size_xyu[2] == slide_io.PIXEL_UNIT: - px_phys_size = None - else: - px_phys_size = self.reader.scale_physical_size(level) - - if channel_names is None: - if src_f is None: - channel_names = slide_meta.channel_names - else: - reader_cls = slide_io.get_slide_reader(src_f) - reader = reader_cls(src_f) - channel_names = reader.metadata.channel_names - - bf_dtype = slide_io.vips2bf_dtype(warped_slide.format) - out_xyczt = slide_io.get_shape_xyzct((warped_slide.width, warped_slide.height), warped_slide.bands) - ome_xml_obj = slide_io.update_xml_for_new_img(current_ome_xml_str=slide_meta.original_xml, - new_xyzct=out_xyczt, - bf_dtype=bf_dtype, - is_rgb=self.is_rgb, - series=self.series, - pixel_physical_size_xyu=px_phys_size, - channel_names=channel_names, - colormap=colormap - ) - - ome_xml = ome_xml_obj.to_xml() - if tile_wh is None: - tile_wh = slide_meta.optimal_tile_wh - if level != 0: - down_sampling = np.mean(slide_meta.slide_dimensions[level]/slide_meta.slide_dimensions[0]) - tile_wh = int(np.round(tile_wh*down_sampling)) - tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 - if tile_wh < 16: - tile_wh = 16 - if np.any(np.array(out_xyczt[0:2]) < tile_wh): - tile_wh = min(out_xyczt[0:2]) - - slide_io.save_ome_tiff(warped_slide, dst_f=dst_f, ome_xml=ome_xml, - tile_wh=tile_wh, compression=compression) - - def warp_xy(self, xy, M=None, slide_level=0, pt_level=0, - non_rigid=True, crop=True): - """Warp points using registration parameters - - Warps `xy` to their location in the registered slide/image - - Parameters - ---------- - xy : ndarray - (N, 2) array of points to be warped. Must be x,y coordinates - - slide_level: int, tuple, optional - Pyramid level of the slide. Used to scale transformation matrices. - Can also be the shape of the warped image (row, col) into which - the points should be warped. Default is 0. - - pt_level: int, tuple, optional - Pyramid level from which the points origingated. For example, if - `xy` are from the centroids of cell segmentation performed on the - full resolution image, this should be 0. Alternatively, the value can - be a tuple of the image's shape (row, col) from which the points came. - For example, if `xy` are bounding box coordinates from an analysis on - a lower resolution image, then pt_level is that lower resolution - image's shape (row, col). Default is 0. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True. - - crop: bool, str - Apply crop to warped points by shifting points to the mask's origin. - Note that this can result in negative coordinates, but might be useful - if wanting to draw the coordinates on the registered slide, such as - annotation coordinates. - - If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - """ - if M is None: - M = self.M - - if np.issubdtype(type(pt_level), np.integer): - pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] - else: - pt_dim_rc = np.array(pt_level) - - if np.issubdtype(type(slide_level), np.integer): - if slide_level != 0: - if np.issubdtype(type(slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(slide_level) - else: - aligned_slide_shape = np.array(slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - else: - aligned_slide_shape = np.array(slide_level) - - if non_rigid: - fwd_dxdy = self.fwd_dxdy - else: - fwd_dxdy = None - - warped_xy = warp_tools.warp_xy(xy, M=M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - src_shape_rc=pt_dim_rc, - dst_shape_rc=aligned_slide_shape, - fwd_dxdy=fwd_dxdy) - - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - if isinstance(slide_level, int): - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[slide_level][::-1] - else: - if len(slide_level) == 2: - scaled_aligned_shape_rc = slide_level - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) - warped_xy -= crop_bbox_xywh[0:2] - - return warped_xy - - def warp_xy_from_to(self, xy, to_slide_obj, src_slide_level=0, src_pt_level=0, - dst_slide_level=0, non_rigid=True): - - """Warp points from this slide to another unwarped slide - - Takes a set of points found in this unwarped slide, and warps them to - their position in the unwarped "to" slide. - - Parameters - ---------- - xy : ndarray - (N, 2) array of points to be warped. Must be x,y coordinates - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - src_pt_level: int, tuple, optional - Pyramid level of the slide/image in which `xy` originated. - For example, if `xy` are from the centroids of cell segmentation - performed on the unwarped full resolution image, this should be 0. - Alternatively, the value can be a tuple of the image's shape (row, col) - from which the points came. For example, if `xy` are bounding - box coordinates from an analysis on a lower resolution image, - then pt_level is that lower resolution image's shape (row, col). - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image in to `xy` will be warped. - Similar to `src_pt_level`, if `dst_slide_level` is an int then - the points will be warped to that pyramid level. If `dst_slide_level` - is the "to" image's shape (row, col), then the points will be warped - to their location in an image with that same shape. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - """ - - if np.issubdtype(type(src_pt_level), np.integer): - src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] - else: - src_pt_dim_rc = np.array(src_pt_level) - - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - else: - to_slide_src_shape_rc = np.array(dst_slide_level) - - if src_slide_level != 0: - if np.issubdtype(type(src_slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(src_slide_level) - else: - aligned_slide_shape = np.array(src_slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if non_rigid: - src_fwd_dxdy = self.fwd_dxdy - dst_bk_dxdy = to_slide_obj.bk_dxdy - - else: - src_fwd_dxdy = None - dst_bk_dxdy = None - - xy_in_unwarped_to_img = \ - warp_tools.warp_xy_from_to(xy=xy, - from_M=self.M, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_src_shape_rc=src_pt_dim_rc, - from_fwd_dxdy=src_fwd_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_dst_shape_rc=aligned_slide_shape, - to_bk_dxdy=dst_bk_dxdy - ) - - return xy_in_unwarped_to_img - - def warp_geojson(self, geojson_f, M=None, slide_level=0, pt_level=0, - non_rigid=True, crop=True): - """Warp geometry using registration parameters - - Warps geometries to their location in the registered slide/image - - Parameters - ---------- - geojson_f : str - Path to geojson file containing the annotation geometries. Assumes - coordinates are in pixels. - - slide_level: int, tuple, optional - Pyramid level of the slide. Used to scale transformation matrices. - Can also be the shape of the warped image (row, col) into which - the points should be warped. Default is 0. - - pt_level: int, tuple, optional - Pyramid level from which the points origingated. For example, if - `xy` are from the centroids of cell segmentation performed on the - full resolution image, this should be 0. Alternatively, the value can - be a tuple of the image's shape (row, col) from which the points came. - For example, if `xy` are bounding box coordinates from an analysis on - a lower resolution image, then pt_level is that lower resolution - image's shape (row, col). Default is 0. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True. - - crop: bool, str - Apply crop to warped points by shifting points to the mask's origin. - Note that this can result in negative coordinates, but might be useful - if wanting to draw the coordinates on the registered slide, such as - annotation coordinates. - - If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - """ - if M is None: - M = self.M - - if np.issubdtype(type(pt_level), np.integer): - pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] - else: - pt_dim_rc = np.array(pt_level) - - if np.issubdtype(type(slide_level), np.integer): - if slide_level != 0: - if np.issubdtype(type(slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(slide_level) - else: - aligned_slide_shape = np.array(slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - else: - aligned_slide_shape = np.array(slide_level) - - if non_rigid: - fwd_dxdy = self.fwd_dxdy - else: - fwd_dxdy = None - - with open(geojson_f) as f: - annotation_geojson = json.load(f) - - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - if isinstance(slide_level, int): - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[slide_level][::-1] - else: - if len(slide_level) == 2: - scaled_aligned_shape_rc = slide_level - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) - shift_xy = crop_bbox_xywh[0:2] - else: - shift_xy = None - - warped_features = [None]*len(annotation_geojson["features"]) - for i, ft in tqdm.tqdm(enumerate(annotation_geojson["features"])): - geom = shapely.geometry.shape(ft["geometry"]) - warped_geom = warp_tools.warp_shapely_geom(geom, M=M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - src_shape_rc=pt_dim_rc, - dst_shape_rc=aligned_slide_shape, - fwd_dxdy=fwd_dxdy, - shift_xy=shift_xy) - warped_ft = deepcopy(ft) - warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) - warped_features[i] = warped_ft - - warped_geojson = {"type":annotation_geojson["type"], "features":warped_features} - - return warped_geojson - - def warp_geojson_from_to(self, geojson_f, to_slide_obj, src_slide_level=0, src_pt_level=0, - dst_slide_level=0, non_rigid=True): - """Warp geoms in geojson file from annotation slide to another unwarped slide - - Takes a set of geometries found in this annotation slide, and warps them to - their position in the unwarped "to" slide. - - Parameters - ---------- - geojson_f : str - Path to geojson file containing the annotation geometries. Assumes - coordinates are in pixels. - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - src_pt_level: int, tuple, optional - Pyramid level of the slide/image in which `xy` originated. - For example, if `xy` are from the centroids of cell segmentation - performed on the unwarped full resolution image, this should be 0. - Alternatively, the value can be a tuple of the image's shape (row, col) - from which the points came. For example, if `xy` are bounding - box coordinates from an analysis on a lower resolution image, - then pt_level is that lower resolution image's shape (row, col). - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image in to `xy` will be warped. - Similar to `src_pt_level`, if `dst_slide_level` is an int then - the points will be warped to that pyramid level. If `dst_slide_level` - is the "to" image's shape (row, col), then the points will be warped - to their location in an image with that same shape. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - Returns - ------- - warped_geojson : dict - Dictionry of warped geojson geometries - - """ - - if np.issubdtype(type(src_pt_level), np.integer): - src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] - else: - src_pt_dim_rc = np.array(src_pt_level) - - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - else: - to_slide_src_shape_rc = np.array(dst_slide_level) - - if src_slide_level != 0: - if np.issubdtype(type(src_slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(src_slide_level) - else: - aligned_slide_shape = np.array(src_slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if non_rigid: - src_fwd_dxdy = self.fwd_dxdy - dst_bk_dxdy = to_slide_obj.bk_dxdy - - else: - src_fwd_dxdy = None - dst_bk_dxdy = None - - with open(geojson_f) as f: - annotation_geojson = json.load(f) - - warped_features = [None]*len(annotation_geojson["features"]) - for i, ft in tqdm.tqdm(enumerate(annotation_geojson["features"])): - geom = shapely.geometry.shape(ft["geometry"]) - warped_geom = warp_tools.warp_shapely_geom_from_to(geom=geom, - from_M=self.M, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_src_shape_rc=src_pt_dim_rc, - from_fwd_dxdy=src_fwd_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_dst_shape_rc=aligned_slide_shape, - to_bk_dxdy=dst_bk_dxdy - ) - - warped_ft = deepcopy(ft) - warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) - warped_features[i] = warped_ft - - warped_geojson = {"type":annotation_geojson["type"], "features":warped_features} - - return warped_geojson - - -class Valis(object): - """Reads, registers, and saves a series of slides/images - - Implements the registration pipeline described in - "VALIS: Virtual Alignment of pathoLogy Image Series" by Gatenbee et al. - This pipeline will read images and whole slide images (WSI) using pyvips, - bioformats, or openslide, and so should work with a wide variety of formats. - VALIS can perform both rigid and non-rigid registration. The registered slides - can be saved as ome.tiff slides that can be used in downstream analyses. The - ome.tiff format is opensource and widely supported, being readable in several - different programming languages (Python, Java, Matlab, etc...) and software, - such as QuPath or HALO. - - The pipeline is fully automated and goes as follows: - - 1. Images/slides are converted to numpy arrays. As WSI are often - too large to fit into memory, these images are usually lower resolution - images from different pyramid levels. - - 2. Images are processed to single channel images. They are then - normalized to make them look as similar as possible. - - 3. Image features are detected and then matched between all pairs of image. - - 4. If the order of images is unknown, they will be optimally ordered - based on their feature similarity - - 5. Rigid registration is performed serially, with each image being - rigidly aligned to the previous image in the stack. - - 6. Non-rigid registration is then performed either by 1) aliging each image - towards the center of the stack, composing the deformation fields - along the way, or 2) using groupwise registration that non-rigidly aligns - the images to a common frame of reference. - - 7. Error is measured by calculating the distance between registered - matched features. - - The transformations found by VALIS can then be used to warp the full - resolution slides. It is also possible to merge non-RGB registered slides - to create a highly multiplexed image. These aligned and/or merged slides - can then be saved as ome.tiff images using pyvips. - - In addition to warping images and slides, VALIS can also warp point data, - such as cell centoids or ROI coordinates. - - Attributes - ---------- - name : str - Descriptive name of registrar, such as the sample's name. - - src_dir: str - Path to directory containing the slides that will be registered. - - dst_dir : str - Path to where the results should be saved. - - original_img_list : list of ndarray - List of images converted from the slides in `src_dir` - - name_dict : dictionary - Key=full path to image, value = name used to look up `Slide` in `Valis.slide_dict` - - slide_dims_dict_wh : - Dictionary of slide dimensions. Only needed if dimensions not - available in the slide/image's metadata. - - resolution_xyu: tuple - Physical size per pixel and the unit. - - image_type : str - Type of image, i.e. "brightfield" or "fluorescence" - - series : int - Slide series to that was read. - - size : int - Number of images to align - - aligned_img_shape_rc : tuple of int - Shape (row, col) of aligned images - - aligned_slide_shape_rc : tuple of int - Shape (row, col) of the aligned slides - - slide_dict : dict of Slide - Dictionary of Slide objects, each of which contains information - about a slide, and methods to warp it. - - brightfield_procsseing_fxn_str: str - Name of function used to process brightfield images. - - if_procsseing_fxn_str : str - Name of function used to process fluorescence images. - - max_image_dim_px : int - Maximum width or height of images that will be saved. - This limit is mostly to keep memory in check. - - max_processed_image_dim_px : int - Maximum width or height of processed images. An important - parameter, as it determines the size of of the image in which - features will be detected and displacement fields computed. - - reference_img_f : str - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be the reference. - - reference_img_idx : int - Index of slide that corresponds to `reference_img_f`, after - the `img_obj_list` has been sorted during rigid registration. - - align_to_reference : bool - Whether or not images should be aligne to a reference image - specified by `reference_img_f`. Will be set to True if - `reference_img_f` is provided. - - crop: str, optional - How to crop the registered images. - - rigid_registrar : SerialRigidRegistrar - SerialRigidRegistrar object that performs the rigid registration. - - rigid_reg_kwargs : dict - Dictionary of keyward arguments passed to - `serial_rigid.register_images`. - - feature_descriptor_str : str - Name of feature descriptor. - - feature_detector_str : str - Name of feature detector. - - transform_str : str - Name of rigid transform - - similarity_metric : str - Name of similarity metric used to order slides. - - match_filter_method : str - Name of method used to filter out poor feature matches. - - non_rigid_registrar : SerialNonRigidRegistrar - SerialNonRigidRegistrar object that performs serial - non-rigid registration. - - non_rigid_reg_kwargs : dict - Dictionary of keyward arguments passed to - `serial_non_rigid.register_images`. - - non_rigid_registrar_cls : NonRigidRegistrar - Uninstantiated NonRigidRegistrar class that will be used - by `non_rigid_registrar` to calculate the deformation fields - between images. - - non_rigid_reg_class_str : str - Name of the of class `non_rigid_registrar_cls` belongs to. - - thumbnail_size : int - Maximum width or height of thumbnails that show results - - original_overlap_img : ndarray - Image showing how original images overlap before registration. - Created by merging coloring the inverted greyscale copies of each - image, and then merging those images. - - rigid_overlap_img : ndarray - Image showing how images overlap after rigid registration. - - non_rigid_overlap_img : ndarray - Image showing how images overlap after rigid + non-rigid registration. - - has_rounds : bool - Whether or not the contents of `src_dir` contain subdirectories that - have single images spread across multiple files. An example would be - .ndpis images. - - norm_method : str - Name of method used to normalize the processed images - - target_processing_stats : ndarray - Array of processed images' stats used to normalize all images - - summary_df : pd.Dataframe - Pandas dataframe containing information about the results, such - as the error, shape of aligned slides, time to completion, etc... - - start_time : float - The time at which registation was initiated. - - end_rigid_time : float - The time at which rigid registation was completed. - - end_non_rigid_time : float - The time at which non-rigid registation was completed. - - qt_emitter : PySide2.QtCore.Signal - Used to emit signals that update the GUI's progress bars - - _non_rigid_bbox : list - Bounding box of area in which non-rigid registration was conducted - - _full_displacement_shape_rc : tuple - Shape of full displacement field. Would be larger than `_non_rigid_bbox` - if non-rigid registration only performed in a masked region - - _dup_names_dict : dictionary - Dictionary describing which images would have been assigned duplicate - names. Key= duplicated name, value=list of paths to images which - would have been assigned the same name - - _empty_slides : dictionary - Dictionary of `Slide` objects that have empty images. Ignored during - registration but added back at the end - - - Examples - -------- - - Basic example using default parameters - - >>> from valis import registration, data - >>> slide_src_dir = data.dcis_src_dir - >>> results_dst_dir = "./slide_registration_example" - >>> registered_slide_dst_dir = "./slide_registration_example/registered_slides" - - Perform registration - - >>> rigid_registrar, non_rigid_registrar, error_df = registrar.register() - - View results in "./slide_registration_example". - If they look good, warp and save the slides as ome.tiff - - >>> registrar.warp_and_save_slides(registered_slide_dst_dir) - - This example shows how to register CyCIF images and then merge - to create a high dimensional ome.tiff slide - - >>> registrar = registration.Valis(slide_src_dir, results_dst_dir) - >>> rigid_registrar, non_rigid_registrar, error_df = registrar.register() - - Create function to get marker names from each slides' filename - - >>> def cnames_from_filename(src_f): - ... f = valtils.get_name(src_f) - ... return ["DAPI"] + f.split(" ")[1:4] - ... - >>> channel_name_dict = {f:cnames_from_filename(f) for f in registrar.original_img_list} - >>> merged_img, channel_names, ome_xml = registrar.warp_and_merge_slides(merged_slide_dst_f, channel_name_dict=channel_name_dict) - - View ome.tiff, located at merged_slide_dst_f - - """ - @valtils.deprecated_args(max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px", img_type="image_type") - def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, - feature_detector_cls=DEFAULT_FD, - transformer_cls=DEFAULT_TRANSFORM_CLASS, - affine_optimizer_cls=DEFAULT_AFFINE_OPTIMIZER_CLASS, - similarity_metric=DEFAULT_SIMILARITY_METRIC, - matcher=DEFAULT_MATCH_FILTER, - imgs_ordered=False, - non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, - non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, - compose_non_rigid=False, - img_list=None, - reference_img_f=None, - align_to_reference=False, - do_rigid=True, - crop=None, - create_masks=True, - check_for_reflections=False, - resolution_xyu=None, slide_dims_dict_wh=None, - max_image_dim_px=DEFAULT_MAX_IMG_DIM, - max_processed_image_dim_px=DEFAULT_MAX_PROCESSED_IMG_SIZE, - max_non_rigid_registration_dim_px=DEFAULT_MAX_PROCESSED_IMG_SIZE, - thumbnail_size=DEFAULT_THUMBNAIL_SIZE, - norm_method=DEFAULT_NORM_METHOD, - micro_rigid_registrar_cls=None, - micro_rigid_registrar_params={}, - qt_emitter=None): - - """ - src_dir: str - Path to directory containing the slides that will be registered. - - dst_dir : str - Path to where the results should be saved. - - name : str, optional - Descriptive name of registrar, such as the sample's name - - series : int, optional - Slide series to that was read. If None, series will be set to 0. - - image_type : str, optional - The type of image, either "brightfield", "fluorescence", - or "multi". If None, VALIS will guess `image_type` - of each image, based on the number of channels and datatype. - Will assume that RGB = "brightfield", - otherwise `image_type` will be set to "fluorescence". - - feature_detector_cls : FeatureDD, optional - Uninstantiated FeatureDD object that detects and computes - image features. Default is VggFD. The - available feature_detectors are found in the `feature_detectors` - module. If a desired feature detector is not available, - one can be created by subclassing `feature_detectors.FeatureDD`. - - transformer_cls : scikit-image Transform class, optional - Uninstantiated scikit-image transformer used to find - transformation matrix that will warp each image to the target - image. Default is SimilarityTransform - - affine_optimizer_cls : AffineOptimzer class, optional - Uninstantiated AffineOptimzer that will minimize a - cost function to find the optimal affine transformations. - If a desired affine optimization is not available, - one can be created by subclassing `affine_optimizer.AffineOptimizer`. - - similarity_metric : str, optional - Metric used to calculate similarity between images, which is in - turn used to build the distance matrix used to sort the images. - Can be "n_matches", or a string to used as - distance in spatial.distance.cdist. "n_matches" - is the number of matching features between image pairs. - - match_filter_method: str, optional - "GMS" will use filter_matches_gms() to remove poor matches. - This uses the Grid-based Motion Statistics (GMS) or RANSAC. - - imgs_ordered : bool, optional - Boolean defining whether or not the order of images in img_dir - are already in the correct order. If True, then each filename should - begin with the number that indicates its position in the z-stack. If - False, then the images will be sorted by ordering a feature distance - matix. Default is False. - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be the reference. - - align_to_reference : bool, optional - If `False`, images will be non-rigidly aligned serially towards the - reference image. If `True`, images will be non-rigidly aligned - directly to the reference image. If `reference_img_f` is None, - then the reference image will be the one in the middle of the stack. - - non_rigid_registrar_cls : NonRigidRegistrar, optional - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images. See - the `non_rigid_registrars` module for a desciption of available - methods. If a desired non-rigid registration method is not available, - one can be implemented by subclassing.NonRigidRegistrar. - If None, then only rigid registration will be performed - - non_rigid_reg_params: dictionary, optional - Dictionary containing key, value pairs to be used to initialize - `non_rigid_registrar_cls`. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. See the NonRigidRegistrar classes in - `non_rigid_registrars` for the available non-rigid registration - methods and arguments. - - compose_non_rigid : bool, optional - Whether or not to compose non-rigid transformations. If `True`, - then an image is non-rigidly warped before aligning to the - adjacent non-rigidly aligned image. This allows the transformations - to accumulate, which may bring distant features together but could - also result in un-wanted deformations, particularly around the edges. - If `False`, the image not warped before being aaligned to the adjacent - non-rigidly aligned image. This can reduce unwanted deformations, but - may not bring distant features together. - - img_list : list, dictionary, optional - List of images to be registered. However, it can also be a dictionary, - in which case the key: value pairs are full_path_to_image: name_of_image, - where name_of_image is the key that can be used to access the image from - Valis.slide_dict. - - do_rigid: bool, dictionary, optional - Whether or not to perform rigid registration. If `False`, rigid - registration will be skipped. - - If `do_rigid` is a dictionary, it should contain inverse transformation - matrices to rigidly align images to the specificed by `reference_img_f`. - M will be estimated for images that are not in the dictionary. - Each key is the filename of the image associated with the transformation matrix, - and value is a dictionary containing the following values: - `M` : (required) a 3x3 inverse transformation matrix as a numpy array. - Found by determining how to align fixed to moving. - If `M` was found by determining how to align moving to fixed, - then `M` will need to be inverted first. - `transformation_src_shape_rc` : (optional) shape (row, col) of image used to find the rigid transformation. - If not provided, then it is assumed to be the shape of the level 0 slide - `transformation_dst_shape_rc` : (optional) shape of registered image. - If not provided, this is assumed to be the shape of the level 0 reference slide. - - crop: str, optional - How to crop the registered images. "overlap" will crop to include - only areas where all images overlapped. "reference" crops to the - area that overlaps with a reference image, defined by - `reference_img_f`. This option can be used even if `reference_img_f` - is `None` because the reference image will be set as the one at the center - of the stack. - - If both `crop` and `reference_img_f` are `None`, `crop` - will be set to "overlap". If `crop` is None, but `reference_img_f` - is defined, then `crop` will be set to "reference". - - create_masks : bool, optional - Whether or not to create and apply masks for registration. - Can help focus alignment on the tissue, but can sometimes - mask too much if there is a lot of variation in the image. - - check_for_reflections : bool, optional - Determine if alignments are improved by relfecting/mirroring/flipping - images. Optional because it requires re-detecting features in each version - of the images and then re-matching features, and so can be time consuming and - not always necessary. - - resolution_xyu: tuple, optional - Physical size per pixel and the unit. If None (the default), these - values will be determined for each slide using the slides' metadata. - If provided, this physical pixel sizes will be used for all of the slides. - This option is available in case one cannot easily access to the original - slides, but does have the information on pixel's physical units. - - slide_dims_dict_wh : dict, optional - Key= slide/image file name, - value= dimensions = [(width, height), (width, height), ...] for each level. - If None (the default), the slide dimensions will be pulled from the - slides' metadata. If provided, those values will be overwritten. This - option is available in case one cannot easily access to the original - slides, but does have the information on the slide dimensions. - - max_image_dim_px : int, optional - Maximum width or height of images that will be saved. - This limit is mostly to keep memory in check. - - max_processed_image_dim_px : int, optional - Maximum width or height of processed images. An important - parameter, as it determines the size of of the image in which - features will be detected and displacement fields computed. - - max_non_rigid_registration_dim_px : int, optional - Maximum width or height of images used for non-rigid registration. - Larger values may yeild more accurate results, at the expense of - speed and memory. There is also a practical limit, as the specified - size may be too large to fit in memory. - - mask_dict : dictionary - Dictionary where key = overlap type (all, overlap, or reference), and - value = (mask, mask_bbox_xywh) - - thumbnail_size : int, optional - Maximum width or height of thumbnails that show results - - norm_method : str - Name of method used to normalize the processed images. Options - are None when normalization is not desired, "histo_match" for - histogram matching and "img_stats" for normalizing by image statistics. - See preprocessing.match_histograms and preprocessing.norm_khan - for details. - - micro_rigid_registrar_cls : MicroRigidRegistrar, optional - Class used to perform higher resolution rigid registration. If `None`, - this step is skipped. - - micro_rigid_registrar_params : dictionary - Dictionary of keyword arguments used intialize the `MicroRigidRegistrar` - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - - if name is None: - name = os.path.split(src_dir)[1] - self.name = name.replace(" ", "_") - - # Set paths # - self.src_dir = src_dir - self.dst_dir = os.path.join(dst_dir, self.name) - self.name_dict = None - if img_list is not None: - if isinstance(img_list, dict): - # Key=original file name, value=name - self.original_img_list = list(img_list.keys()) - self.name_dict = img_list - elif isinstance(img_list, list): - self.original_img_list = img_list - else: - self.get_imgs_in_dir() - - if self.name_dict is None: - self.name_dict = self.get_img_names(self.original_img_list) - - self.check_for_duplicated_names(self.original_img_list) - - self.set_dst_paths() - - # Some information may already be provided # - self.slide_dims_dict_wh = slide_dims_dict_wh - self.resolution_xyu = resolution_xyu - self.image_type = image_type - - # Results fields # - self.series = series - self.size = 0 - self.aligned_img_shape_rc = None - self.aligned_slide_shape_rc = None - self.slide_dict = {} - - # Fields related to image pre-processing # - self.brightfield_procsseing_fxn_str = None - self.if_procsseing_fxn_str = None - - if max_image_dim_px < max_processed_image_dim_px: - msg = f"max_image_dim_px is {max_image_dim_px} but needs to be less or equal to {max_processed_image_dim_px}. Setting max_image_dim_px to {max_processed_image_dim_px}" - valtils.print_warning(msg) - max_image_dim_px = max_processed_image_dim_px - - self.max_image_dim_px = max_image_dim_px - self.max_processed_image_dim_px = max_processed_image_dim_px - self.max_non_rigid_registration_dim_px = max_non_rigid_registration_dim_px - - # Setup rigid registration # - self.reference_img_idx = None - self.reference_img_f = reference_img_f - self.align_to_reference = align_to_reference - - self.do_rigid = do_rigid - self.rigid_registrar = None - self.micro_rigid_registrar_cls = micro_rigid_registrar_cls - self.micro_rigid_registrar_params = micro_rigid_registrar_params - - self._set_rigid_reg_kwargs(name=name, - feature_detector=feature_detector_cls, - similarity_metric=similarity_metric, - matcher=matcher, - transformer=transformer_cls, - affine_optimizer=affine_optimizer_cls, - imgs_ordered=imgs_ordered, - reference_img_f=reference_img_f, - check_for_reflections=check_for_reflections, - qt_emitter=qt_emitter) - - - # Setup non-rigid registration # - self.non_rigid_registrar = None - self.non_rigid_registrar_cls = non_rigid_registrar_cls - - if crop is None: - if reference_img_f is None: - self.crop = CROP_OVERLAP - else: - self.crop = CROP_REF - else: - self.crop = crop - - self.compose_non_rigid = compose_non_rigid - if non_rigid_registrar_cls is not None: - self._set_non_rigid_reg_kwargs(name=name, - non_rigid_reg_class=non_rigid_registrar_cls, - non_rigid_reg_params=non_rigid_reg_params, - reference_img_f=reference_img_f, - compose_non_rigid=compose_non_rigid, - qt_emitter=qt_emitter) - - # Info realted to saving images to view results # - self.mask_dict = None - self.create_masks = create_masks - - self.thumbnail_size = thumbnail_size - self.original_overlap_img = None - self.rigid_overlap_img = None - self.non_rigid_overlap_img = None - self.micro_reg_overlap_img = None - - self.has_rounds = False - self.norm_method = norm_method - self.summary_df = None - self.start_time = None - self.end_rigid_time = None - self.end_non_rigid_time = None - - self._empty_slides = {} - - def _set_rigid_reg_kwargs(self, name, feature_detector, similarity_metric, - matcher, transformer, affine_optimizer, - imgs_ordered, reference_img_f, check_for_reflections, qt_emitter): - - """Set rigid registration kwargs - Keyword arguments will be passed to `serial_rigid.register_images` - - """ - - # if isinstance(match_filter_method, str): - # matcher = feature_matcher.Matcher(match_filter_method=match_filter_method) - # else: - # matcher = match_filter_method - - if affine_optimizer is not None: - afo = affine_optimizer(transform=transformer.__name__) - else: - afo = affine_optimizer - - self.rigid_reg_kwargs = {NAME_KEY: name, - FD_KEY: feature_detector(), - SIM_METRIC_KEY: similarity_metric, - TRANSFORMER_KEY: transformer(), - MATCHER_KEY: matcher, - AFFINE_OPTIMIZER_KEY: afo, - REF_IMG_KEY: reference_img_f, - IMAGES_ORDERD_KEY: imgs_ordered, - CHECK_REFLECT_KEY: check_for_reflections, - QT_EMMITER_KEY: qt_emitter - } - - # Save methods as strings since some objects cannot be pickled # - self.feature_descriptor_str = self.rigid_reg_kwargs[FD_KEY].kp_descriptor_name - self.feature_detector_str = self.rigid_reg_kwargs[FD_KEY].kp_detector_name - self.transform_str = self.rigid_reg_kwargs[TRANSFORMER_KEY].__class__.__name__ - self.similarity_metric = self.rigid_reg_kwargs[SIM_METRIC_KEY] - self.match_filter_method = matcher.__class__.__name__ - self.imgs_ordered = imgs_ordered - - def _set_non_rigid_reg_kwargs(self, name, non_rigid_reg_class, non_rigid_reg_params, - reference_img_f, compose_non_rigid, qt_emitter): - """Set non-rigid registration kwargs - Keyword arguments will be passed to `serial_non_rigid.register_images` - - """ - - self.non_rigid_reg_kwargs = {NAME_KEY: name, - NON_RIGID_REG_CLASS_KEY: non_rigid_reg_class, - NON_RIGID_REG_PARAMS_KEY: non_rigid_reg_params, - REF_IMG_KEY: reference_img_f, - QT_EMMITER_KEY: qt_emitter, - NON_RIGID_COMPOSE_KEY: compose_non_rigid - } - - self.non_rigid_reg_class_str = self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY].__name__ - - def _add_empty_slides(self): - - # Fill in missing attributes - for slide_name, slide_obj in self._empty_slides.items(): - - slide_obj.processed_img_shape_rc = slide_obj.image.shape[0:2] - slide_obj.aligned_slide_shape_rc = self.aligned_slide_shape_rc - slide_obj.reg_img_shape_rc = self.aligned_img_shape_rc - - slide_obj.processed_img = np.zeros(slide_obj.processed_img_shape_rc) - slide_obj.rigid_reg_mask = np.full(slide_obj.processed_img_shape_rc, 255) - slide_obj.non_rigid_reg_mask = np.full(slide_obj.reg_img_shape_rc, 255) - - slide_obj.M = np.eye(3) - - slide_obj.stack_idx = self.size - self.size += 1 - self.slide_dict[slide_name] = slide_obj - - def get_imgs_in_dir(self): - """Get all images in Valis.src_dir - - """ - full_path_list = [os.path.join(self.src_dir, f) for f in os.listdir(self.src_dir)] - self.original_img_list = [] - img_names = [] - for f in full_path_list: - if os.path.isfile(f): - if slide_tools.get_img_type(f) is not None: - self.original_img_list.append(f) - img_names.append(valtils.get_name(f)) - - for f in full_path_list: - if os.path.isdir(f): - dir_name = os.path.split(f)[1] - is_round, master_slide = slide_tools.determine_if_staining_round(f) - if is_round: - self.original_img_list.append(master_slide) - - else: - # Some formats, like .mrxs have the main file but - # data in a subdirectory with the same name - matching_f = [ff for ff in full_path_list if re.search(dir_name, ff) is not None and os.path.split(ff)[1] != dir_name] - if len(matching_f) == 1: - if not matching_f[0] in self.original_img_list: - # Make sure that file not already in list - self.original_img_list.extend(matching_f) - img_names.append(dir_name) - - elif len(matching_f) > 1: - msg = f"found {len(matching_f)} matches for {dir_name}: {', '.join(matching_f)}" - valtils.print_warning(msg) - elif len(matching_f) == 0: - msg = f"Can't find slide file associated with {dir_name}" - valtils.print_warning(msg) - - def set_dst_paths(self): - """Set paths to where the results will be saved. - - """ - - self.img_dir = os.path.join(self.dst_dir, CONVERTED_IMG_DIR) - self.processed_dir = os.path.join(self.dst_dir, PROCESSED_IMG_DIR) - self.reg_dst_dir = os.path.join(self.dst_dir, RIGID_REG_IMG_DIR) - self.non_rigid_dst_dir = os.path.join(self.dst_dir, NON_RIGID_REG_IMG_DIR) - self.deformation_field_dir = os.path.join(self.dst_dir, DEFORMATION_FIELD_IMG_DIR) - self.overlap_dir = os.path.join(self.dst_dir, OVERLAP_IMG_DIR) - self.data_dir = os.path.join(self.dst_dir, REG_RESULTS_DATA_DIR) - self.displacements_dir = os.path.join(self.dst_dir, DISPLACEMENT_DIRS) - self.micro_reg_dir = os.path.join(self.dst_dir, MICRO_REG_DIR) - self.mask_dir = os.path.join(self.dst_dir, MASK_DIR) - - def get_slide(self, src_f): - """Get Slide - - Get the Slide associated with `src_f`. - Slide store registration parameters and other metadata about - the slide associated with `src_f`. Slide can also: - - * Convert the slide to a numpy array (Slide.slide2image) - * Convert the slide to a pyvips.Image (Slide.slide2vips) - * Warp the slide (Slide.warp_slide) - * Save the warped slide as an ome.tiff (Slide.warp_and_save_slide) - * Warp an image of the slide (Slide.warp_img) - * Warp points (Slide.warp_xy) - * Warp points in one slide to their position in another unwarped slide (Slide.warp_xy_from_to) - * Access slide ome-xml (Slide.original_xml) - - See Slide for more details. - - Parameters - ---------- - src_f : str - Path to the slide, or name assigned to slide (see Valis.name_dict) - - Returns - ------- - slide_obj : Slide - Slide associated with src_f - - """ - - default_name = valtils.get_name(src_f) - - if src_f in self.name_dict.keys(): - # src_f is full path to image - assigned_name = self.name_dict[src_f] - elif src_f in self.name_dict.values(): - # src_f is name of image - assigned_name = src_f - else: - # src_f isn't in name_dict - assigned_name = None - - if default_name in self.slide_dict: - # src_f is the image name or file name - slide_obj = self.slide_dict[default_name] - - elif assigned_name in self.slide_dict: - # src_f is full path and name was looked up - slide_obj = self.slide_dict[assigned_name] - - elif src_f in self.slide_dict: - # src_f is the name of the slide - slide_obj = self.slide_dict[src_f] - - elif default_name in self._dup_names_dict: - # default name has multiple matches - n_matching = len(self._dup_names_dict[default_name]) - possible_names_dict = {f: self.name_dict[f] for f in self._dup_names_dict[default_name]} - - msg = (f"\n{src_f} matches {n_matching} images in this dataset:\n" - f"{pformat(self._dup_names_dict[default_name])}" - f"\n\nPlease see `Valis.name_dict` to find correct name in " - f"the dictionary. Either key (filenmae) or value (assigned name) will work:\n" - f"{pformat(possible_names_dict)}") - - valtils.print_warning(msg) - slide_obj = None - - return slide_obj - - def get_ref_slide(self): - ref_slide = self.get_slide(self.reference_img_f) - - return ref_slide - - def get_img_names(self, img_list): - """ - Check that each image will have a unique name, which is based on the file name. - Images that would otherwise have the same name are assigned extra ids, starting at 0. - For example, if there were three images named "HE.tiff", they would be - named "HE_0", "HE_1", and "HE_2". - - Parameters - ---------- - - img_list : list - List of image names - - Returns - ------- - name_dict : dict - Dictionary, where key= full path to image, value = image name used as - key in Valis.slide_dict - - """ - - img_df = pd.DataFrame({"img_f": img_list, - "name": [valtils.get_name(f) for f in img_list]}) - - names_dict = {f: valtils.get_name(f) for f in img_list} - count_df = img_df["name"].value_counts() - dup_idx = np.where(count_df.values > 1)[0] - if len(dup_idx) > 0: - for i in dup_idx: - dup_name = count_df.index[i] - dup_paths = img_df["img_f"][img_df["name"] == dup_name] - z = len(str(len(dup_paths))) - - msg = f"Detected {len(dup_paths)} images that would be named {dup_name}" - valtils.print_warning(msg) - - for j, p in enumerate(dup_paths): - new_name = f"{names_dict[p]}_{str(j).zfill(z)}" - msg = f"Renmaing {p} to {new_name} in Valis.slide_dict)" - valtils.print_warning(msg) - names_dict[p] = new_name - - return names_dict - - def check_for_duplicated_names(self, img_list): - """ - Create dictionary that tracks which files - might be assigned the same name, which - can happen if the filenames (minus the rest of the path) are the same - """ - default_names_dict = {} - for f in img_list: - default_name = valtils.get_name(f) - if default_name not in default_names_dict: - default_names_dict[default_name] = [f] - else: - default_names_dict[default_name].append(f) - - self._dup_names_dict = {k: v for k, v in default_names_dict.items() if len(v) > 1} - - def convert_imgs(self, series=None, reader_cls=None): - """Convert slides to images and create dictionary of Slides. - - series : int, optional - Slide series to be read. If None, the series with largest image will be read - - reader_cls : SlideReader, optional - Uninstantiated SlideReader class that will convert - the slide to an image, and also collect metadata. - - """ - - img_types = [] - self.size = 0 - for f in tqdm.tqdm(self.original_img_list): - if reader_cls is None: - try: - slide_reader_cls = slide_io.get_slide_reader(f, series=series) - except Exception as e: - msg = f"Attempting to get reader for {f} created the following error:\n{e}" - valtils.print_warning(msg) - else: - slide_reader_cls = reader_cls - - try: - reader = slide_reader_cls(f, series=series) - except Exception as e: - msg = f"Attempting to read {f} created the following error:\n{e}" - valtils.print_warning(msg) - - slide_dims = reader.metadata.slide_dimensions - levels_in_range = np.where(slide_dims.max(axis=1) < self.max_image_dim_px)[0] - if len(levels_in_range) > 0: - level = levels_in_range[0] - else: - level = len(slide_dims) - 1 - - vips_img = reader.slide2vips(level=level) - - scaling = np.min(self.max_image_dim_px/np.array([vips_img.width, vips_img.height])) - if scaling < 1: - vips_img = warp_tools.rescale_img(vips_img, scaling) - - img = warp_tools.vips2numpy(vips_img) - - slide_name = self.name_dict[f] - slide_obj = Slide(f, img, self, reader, name=slide_name) - slide_obj.crop = self.crop - - # Will overwrite data if provided. Can occur if reading images, not the actual slides # - if self.slide_dims_dict_wh is not None: - matching_slide = [k for k in self.slide_dims_dict_wh.keys() - if valtils.get_name(k) == slide_obj.name][0] - - slide_dims = self.slide_dims_dict_wh[matching_slide] - if slide_dims.ndim == 1: - slide_dims = np.array([[slide_dims]]) - slide_obj.slide_shape_rc = slide_dims[0][::-1] - - if self.resolution_xyu is not None: - slide_obj.resolution = np.mean(self.resolution_xyu[0:2]) - slide_obj.units = self.resolution_xyu[2] - - if slide_obj.is_empty: - msg = f"{slide_obj.name} appears to be empty and will be skipped during registration" - valtils.print_warning(msg) - self._empty_slides[slide_obj.name] = slide_obj - continue - - img_types.append(slide_obj.img_type) - self.slide_dict[slide_obj.name] = slide_obj - self.size += 1 - - if self.image_type is None: - unique_img_types = list(set(img_types)) - if len(unique_img_types) > 1: - self.image_type = slide_tools.MULTI_MODAL_NAME - else: - self.image_type = unique_img_types[0] - - self.check_img_max_dims() - - def check_img_max_dims(self): - """Ensure that all images have similar sizes. - - `max_image_dim_px` will be set to the maximum dimension of the - smallest image if that value is less than max_image_dim_px - - """ - - og_img_sizes_wh = np.array([slide_obj.image.shape[0:2][::-1] for slide_obj in self.slide_dict.values()]) - img_max_dims = og_img_sizes_wh.max(axis=1) - min_max_wh = img_max_dims.min() - scaling_for_og_imgs = min_max_wh/img_max_dims - - if np.any(scaling_for_og_imgs < 1): - msg = f"Smallest image is less than max_image_dim_px. parameter max_image_dim_px is being set to {min_max_wh}" - valtils.print_warning(msg) - self.max_image_dim_px = min_max_wh - for slide_obj in self.slide_dict.values(): - # Rescale images - scaling = self.max_image_dim_px/max(slide_obj.image.shape[0:2]) - assert scaling <= self.max_image_dim_px - if scaling < 1: - slide_obj.image = warp_tools.rescale_img(slide_obj.image, scaling) - - if self.max_processed_image_dim_px > self.max_image_dim_px: - msg = f"parameter max_processed_image_dim_px also being updated to {self.max_image_dim_px}" - valtils.print_warning(msg) - self.max_processed_image_dim_px = self.max_image_dim_px - - def create_original_composite_img(self, rigid_registrar): - """Create imaage showing how images overlap before registration - """ - - min_r = np.inf - max_r = 0 - min_c = np.inf - max_c = 0 - composite_img_list = [None] * self.size - for i, img_obj in enumerate(rigid_registrar.img_obj_list): - img = img_obj.image - padded_img = transform.warp(img, img_obj.T, preserve_range=True, - output_shape=img_obj.padded_shape_rc) - - composite_img_list[i] = padded_img - - img_corners_rc = warp_tools.get_corners_of_image(img.shape[0:2]) - warped_corners_xy = warp_tools.warp_xy(img_corners_rc[:, ::-1], img_obj.T) - min_r = min(warped_corners_xy[:, 1].min(), min_r) - max_r = max(warped_corners_xy[:, 1].max(), max_r) - min_c = min(warped_corners_xy[:, 0].min(), min_c) - max_c = max(warped_corners_xy[:, 0].max(), max_c) - - composite_img = np.dstack(composite_img_list) - cmap = viz.jzazbz_cmap() - channel_colors = viz.get_n_colors(cmap, composite_img.shape[2]) - overlap_img = viz.color_multichannel(composite_img, channel_colors, - rescale_channels=True, - normalize_by="channel", - cspace="CAM16UCS") - - min_r = int(min_r) - max_r = int(np.ceil(max_r)) - min_c = int(min_c) - max_c = int(np.ceil(max_c)) - overlap_img = overlap_img[min_r:max_r, min_c:max_c] - overlap_img = (255*overlap_img).astype(np.uint8) - - return overlap_img - - def measure_original_mmi(self, img1, img2): - """Measure Mattes mutation inormation between 2 unregistered images. - """ - - dst_rc = np.max([img1.shape, img2.shape], axis=1) - padded_img_list = [None] * self.size - for i, img in enumerate([img1, img2]): - T = warp_tools.get_padding_matrix(img.shape, dst_rc) - padded_img = transform.warp(img, T, preserve_range=True, output_shape=dst_rc) - padded_img_list[i] = padded_img - - og_mmi = warp_tools.mattes_mi(padded_img_list[0], padded_img_list[1]) - - return og_mmi - - def create_img_processor_dict(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None): - """Create dictionary to get processors for each image - - Create dictionary to get processors for each image. If an image is not in `processing_dict`, - this function will try to guess the modality and then assign a default processor. - - Parameters - ---------- - brightfield_processing_cls : ImageProcesser - ImageProcesser to pre-process brightfield images to make them look as similar as possible. - Should return a single channel uint8 image. - - brightfield_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `brightfield_processing_cls` - - if_processing_cls : ImageProcesser - ImageProcesser to pre-process immunofluorescent images to make them look as similar as possible. - Should return a single channel uint8 image. - - if_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `if_processing_cls` - - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then this function will assign a processor to each image. - - Returns - ------- - named_processing_dict : Dict - Each key is the name of the slide, and the value is a list, where - the 1st element is the processor, and the second element a dictionary - of keyword arguments passed to the processor - - """ - - if processor_dict is None: - named_processing_dict = {} - else: - named_processing_dict = {self.get_slide(f).name: processor_dict[f] for f in processor_dict.keys()} - - for i, slide_obj in enumerate(self.slide_dict.values()): - - if slide_obj.name in named_processing_dict: - slide_p = named_processing_dict[slide_obj.name] - if isinstance(slide_p, list): - if len(slide_p) == 2: - slide_p, slide_kwargs = slide_p - elif len(slide_p) == 1: - # Provided processor, but no kwargs - slide_kwargs = {} - else: - # Provided processor, but no kwargs - slide_kwargs = {} - - named_processing_dict[slide_obj.name] = [slide_p, slide_kwargs] - - else: - # Processor not provided, so assign one based on inferred modality - is_ihc = slide_obj.img_type == slide_tools.IHC_NAME - if is_ihc: - processing_cls = brightfield_processing_cls - processing_kwargs = brightfield_processing_kwargs - - else: - processing_cls = if_processing_cls - processing_kwargs = if_processing_kwargs - - named_processing_dict[slide_obj.name] = [processing_cls, processing_kwargs] - - return named_processing_dict - - def process_imgs(self, processor_dict): - """Process images to make them look as similar as possible - - Images will also be normalized after images are processed - - Parameters - ---------- - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then a default processor will be used for each image based on - the inferred modality. - - """ - - pathlib.Path(self.processed_dir).mkdir(exist_ok=True, parents=True) - if self.norm_method is not None: - if self.norm_method == "histo_match": - ref_histogram = np.zeros(256, dtype=np.int) - else: - all_v = [None]*self.size - - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values())): - - levels_in_range = np.where(slide_obj.slide_dimensions_wh.max(axis=1) < self.max_processed_image_dim_px)[0] - if len(levels_in_range) > 0: - level = levels_in_range[0] - else: - level = len(slide_obj.slide_dimensions_wh) - 1 - - processing_cls, processing_kwargs = processor_dict[slide_obj.name] - processor = processing_cls(image=slide_obj.image, src_f=slide_obj.src_f, level=level, series=slide_obj.series) - - try: - processed_img = processor.process_image(**processing_kwargs) - except TypeError: - # processor.process_image doesn't take kwargs - processed_img = processor.process_image() - - processed_img = exposure.rescale_intensity(processed_img, out_range=(0, 255)).astype(np.uint8) - scaling = np.min(self.max_processed_image_dim_px/np.array(processed_img.shape[0:2])) - if scaling < 1: - processed_img = warp_tools.rescale_img(processed_img, scaling) - - if self.create_masks: - # Get masks # - pathlib.Path(self.mask_dir).mkdir(exist_ok=True, parents=True) - - # Slice region from slide and process too - mask = processor.create_mask() - if not np.all(mask.shape == processed_img.shape[0:2]): - mask = warp_tools.resize_img(mask, processed_img.shape[0:2], interp_method="nearest") - - slide_obj.rigid_reg_mask = mask - # print("not applying rigid mask (line ~2536)") - # processed_img[mask == 0] = 0 - - # Save image with mask drawn on top of it - thumbnail_mask = self.create_thumbnail(mask) - if slide_obj.img_type == slide_tools.IHC_NAME: - thumbnail_img = self.create_thumbnail(slide_obj.image) - else: - thumbnail_img = self.create_thumbnail(processed_img) - - thumbnail_mask_outline = viz.draw_outline(thumbnail_img, thumbnail_mask) - outline_f_out = os.path.join(self.mask_dir, f'{slide_obj.name}.png') - warp_tools.save_img(outline_f_out, thumbnail_mask_outline) - - else: - mask = np.full(processed_img.shape, 255, dtype=np.uint8) - - slide_obj.rigid_reg_mask = mask - slide_obj.processed_img = processed_img - - processed_f_out = os.path.join(self.processed_dir, slide_obj.name + ".png") - slide_obj.processed_img_f = processed_f_out - slide_obj.processed_img_shape_rc = np.array(processed_img.shape[0:2]) - warp_tools.save_img(processed_f_out, processed_img) - - img_for_stats = processed_img.reshape(-1) - - if self.norm_method is not None: - if self.norm_method == "histo_match": - img_hist, _ = np.histogram(img_for_stats, bins=256) - ref_histogram += img_hist - else: - all_v[i] = img_for_stats.reshape(-1) - - if self.norm_method is not None: - if self.norm_method == "histo_match": - target_stats = ref_histogram - else: - all_v = np.hstack(all_v) - target_stats = all_v - - self.normalize_images(target_stats) - - def denoise_images(self): - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values())): - if slide_obj.rigid_reg_mask is None: - is_ihc = slide_obj.img_type == slide_tools.IHC_NAME - _, tissue_mask = preprocessing.create_tissue_mask(slide_obj.image, is_ihc) - mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(tissue_mask)) - c0, r0 = mask_bbox[:2] - c1, r1 = mask_bbox[:2] + mask_bbox[2:] - denoise_mask = np.zeros_like(tissue_mask) - denoise_mask[r0:r1, c0:c1] = 255 - else: - denoise_mask = slide_obj.rigid_reg_mask - - denoised = preprocessing.denoise_img(slide_obj.processed_img, mask=denoise_mask) - warp_tools.save_img(slide_obj.processed_img_f, denoised) - - def normalize_images(self, target): - """Normalize intensity values in images - - Parameters - ---------- - target : ndarray - Target statistics used to normalize images - - """ - print("\n==== Normalizing images\n") - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values())): - vips_img = pyvips.Image.new_from_file(slide_obj.processed_img_f) - img = warp_tools.vips2numpy(vips_img) - if self.norm_method == "histo_match": - self.target_processing_stats = target - normed_img = preprocessing.match_histograms(img, self.target_processing_stats) - elif self.norm_method == "img_stats": - self.target_processing_stats = preprocessing.get_channel_stats(target) - normed_img = preprocessing.norm_img_stats(img, self.target_processing_stats) - - normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype(np.uint8) - slide_obj.processed_img = normed_img - - slide_obj.processed_img_shape_rc = np.array(normed_img.shape[0:2]) - warp_tools.save_img(slide_obj.processed_img_f, normed_img) - - def create_thumbnail(self, img, rescale_color=False): - """Create thumbnail image to view results - """ - - is_vips = isinstance(img, pyvips.Image) - - img_shape = warp_tools.get_shape(img) - scaling = np.min(self.thumbnail_size/np.array(img_shape[:2])) - if scaling < 1: - thumbnail = warp_tools.rescale_img(img, scaling) - else: - thumbnail = img - - if rescale_color is True: - if is_vips: - # Convert to numpy to rescale - thumbnail = warp_tools.vips2numpy(img) - thumbnail = exposure.rescale_intensity(thumbnail, out_range=(0, 255)).astype(np.uint8) - - if is_vips: - # Convert back to pyvips - thumbnail = warp_tools.numpy2vips(thumbnail) - - return thumbnail - - def draw_overlap_img(self, img_list): - """Create image showing the overlap of registered images - """ - - composite_img = np.dstack(img_list) - cmap = viz.jzazbz_cmap() - channel_colors = viz.get_n_colors(cmap, composite_img.shape[2]) - overlap_img = viz.color_multichannel(composite_img, channel_colors, - rescale_channels=True, - normalize_by="channel", - cspace="CAM16UCS") - - overlap_img = exposure.equalize_adapthist(overlap_img) - overlap_img = exposure.rescale_intensity(overlap_img, out_range=(0, 255)).astype(np.uint8) - - return overlap_img - - def get_ref_img_mask(self, rigid_registrar): - """Create mask that covers reference image - - Returns - ------- - mask : ndarray - Mask that covers reference image in registered images - mask_bbox_xywh : tuple of int - XYWH of mask in reference image - - """ - - ref_name = self.name_dict[self.reference_img_f] - ref_slide = rigid_registrar.img_obj_dict[ref_name] - ref_shape_wh = ref_slide.image.shape[0:2][::-1] - - uw_mask = np.full(ref_shape_wh[::-1], 255, dtype=np.uint8) - mask = warp_tools.warp_img(uw_mask, ref_slide.M, - out_shape_rc=ref_slide.registered_shape_rc) - - reg_txy = -ref_slide.M[0:2, 2] - mask_bbox_xywh = np.array([*reg_txy, *ref_shape_wh]) - - return mask, mask_bbox_xywh - - def get_all_overlap_mask(self, rigid_registrar): - """Create mask that covers all tissue - - - Returns - ------- - mask : ndarray - Mask that covers reference image in registered images - mask_bbox_xywh : tuple of int - XYWH of mask in reference image - - """ - - ref_name = self.name_dict[self.reference_img_f] - ref_slide = rigid_registrar.img_obj_dict[ref_name] - combo_mask = np.zeros(ref_slide.registered_shape_rc, dtype=int) - for img_obj in rigid_registrar.img_obj_list: - - img_mask = self.slide_dict[img_obj.name].rigid_reg_mask - warped_img_mask = warp_tools.warp_img(img_mask, - M=img_obj.M, - out_shape_rc=img_obj.registered_shape_rc, - interp_method="nearest") - - combo_mask[warped_img_mask > 0] += 1 - - temp_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, self.size-0.5).astype(np.uint8) - mask = 255*ndimage.binary_fill_holes(temp_mask).astype(np.uint8) - mask = preprocessing.mask2contours(mask) - - mask_bbox_xywh = warp_tools.xy2bbox(warp_tools.mask2xy(mask)) - - return mask, mask_bbox_xywh - - - - def get_null_overlap_mask(self, rigid_registrar): - """Create mask that covers all of the image. - Not really a mask - - - Returns - ------- - mask : ndarray - Mask that covers reference image in registered images - mask_bbox_xywh : tuple of int - XYWH of mask in reference image - - """ - reg_shape = rigid_registrar.img_obj_list[0].registered_shape_rc - mask = np.full(reg_shape, 255, dtype=np.uint8) - mask_bbox_xywh = np.array([0, 0, reg_shape[1], reg_shape[0]]) - - return mask, mask_bbox_xywh - - def create_crop_masks(self, rigid_registrar): - """Create masks based on rigid registration - - """ - mask_dict = {} - mask_dict[CROP_REF] = self.get_ref_img_mask(rigid_registrar) - mask_dict[CROP_OVERLAP] = self.get_all_overlap_mask(rigid_registrar) - mask_dict[CROP_NONE] = self.get_null_overlap_mask(rigid_registrar) - self.mask_dict = mask_dict - - def get_crop_mask(self, overlap_type): - """Get overlap mask and bounding box - - Returns - ------- - mask : ndarray - Mask - - mask_xywh : tuple - XYWH for bounding box around mask - - """ - if overlap_type is None: - overlap_type = CROP_NONE - - return self.mask_dict[overlap_type] - - def rigid_register_partial(self, tform_dict=None): - """Perform rigid registration using provided parameters - - Still sorts images by similarity for use with non-rigid registration. - - tform_dict : dictionary - Dictionary with rigid registration parameters. Each key is the image's file name, and - the values are another dictionary with transformation parameters: - M: 3x3 inverse transformation matrix. Found by determining how to align fixed to moving. - If M was found by determining how to align moving to fixed, then it will need to be inverted - - transformation_src_shape_rc: shape (row, col) of image used to find the rigid transformation. If - not provided, then it is assumed to be the shape of the level 0 slide - transformation_dst_shape_rc: shape of registered image. If not presesnt, but a reference was provided - and `transformation_src_shape_rc` was not provided, this is assumed to be the shape of the reference slide - - If None, then all rigid M will be the identity matrix - """ - - - # Still need to sort images # - rigid_registrar = serial_rigid.SerialRigidRegistrar(self.processed_dir, - imgs_ordered=self.imgs_ordered, - reference_img_f=self.reference_img_f, - name=self.name, - align_to_reference=self.align_to_reference) - - feature_detector = self.rigid_reg_kwargs[FD_KEY] - matcher = self.rigid_reg_kwargs[MATCHER_KEY] - similarity_metric = self.rigid_reg_kwargs[SIM_METRIC_KEY] - transformer = self.rigid_reg_kwargs[TRANSFORMER_KEY] - - print("\n======== Detecting features\n") - rigid_registrar.generate_img_obj_list(feature_detector) - - if self.create_masks: - # Remove feature points outside of mask - for img_obj in rigid_registrar.img_obj_dict.values(): - slide_obj = self.get_slide(img_obj.name) - features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=img_obj.kp_pos_xy, mask=slide_obj.rigid_reg_mask) - n_removed = img_obj.kp_pos_xy.shape[0] - len(features_in_mask_idx) - print(f"Removed {n_removed} features outside of the mask") - if len(features_in_mask_idx) > 0: - img_obj.kp_pos_xy = img_obj.kp_pos_xy[features_in_mask_idx, :] - img_obj.desc = img_obj.desc[features_in_mask_idx, :] - - - print("\n======== Matching images\n") - if rigid_registrar.aleady_sorted: - rigid_registrar.match_sorted_imgs(matcher, keep_unfiltered=False) - - for i, img_obj in enumerate(rigid_registrar.img_obj_list): - img_obj.stack_idx = i - - else: - rigid_registrar.match_imgs(matcher, keep_unfiltered=False) - - print("\n======== Sorting images\n") - rigid_registrar.build_metric_matrix(metric=similarity_metric) - rigid_registrar.sort() - - rigid_registrar.distance_metric_name = matcher.metric_name - rigid_registrar.distance_metric_type = matcher.metric_type - rigid_registrar.get_iter_order() - if rigid_registrar.size > 2: - rigid_registrar.update_match_dicts_with_neighbor_filter(transformer, matcher) - - if self.reference_img_f is not None: - ref_name = self.name_dict[self.reference_img_f] - else: - ref_name = valtils.get_name(rigid_registrar.reference_img_f) - if self.do_rigid is not False: - msg = " ".join([f"Best to specify `{REF_IMG_KEY}` when manually providing `{TFORM_MAT_KEY}`.", - f"Setting this image to be {ref_name}"]) - - valtils.print_warning(msg) - - # Get output shapes # - if tform_dict is None: - named_tform_dict = {o.name: {"M":np.eye(3)} for o in rigid_registrar.img_obj_list} - else: - named_tform_dict = {valtils.get_name(k):v for k, v in tform_dict.items()} - - # Get output shapes # - rigid_ref_obj = rigid_registrar.img_obj_dict[ref_name] - ref_slide_obj = self.get_ref_slide() - if ref_name in named_tform_dict.keys(): - ref_tforms = named_tform_dict[ref_name] - if TFORM_SRC_SHAPE_KEY in ref_tforms: - ref_tform_src_shape_rc = ref_tforms[TFORM_SRC_SHAPE_KEY] - else: - ref_tform_src_shape_rc = ref_slide_obj.slide_dimensions_wh[0][::-1] - - if TFORM_DST_SHAPE_KEY in ref_tforms: - temp_out_shape_rc = ref_tforms[TFORM_DST_SHAPE_KEY] - else: - # Assume M was found by aligning to level 0 reference - temp_out_shape_rc = ref_slide_obj.slide_dimensions_wh[0][::-1] - - ref_to_reg_sxy = (np.array(rigid_ref_obj.image.shape)/np.array(ref_tform_src_shape_rc))[::-1] - out_rc = np.round(temp_out_shape_rc*ref_to_reg_sxy).astype(int) - - else: - out_rc = rigid_ref_obj.image.shape - - scaled_M_dict = {} - for img_name, img_tforms in named_tform_dict.items(): - matching_rigid_obj = rigid_registrar.img_obj_dict[img_name] - matching_slide_obj = self.slide_dict[img_name] - - if TFORM_SRC_SHAPE_KEY in img_tforms: - og_src_shape_rc = img_tforms[TFORM_SRC_SHAPE_KEY] - else: - og_src_shape_rc = matching_slide_obj.slide_dimensions_wh[0][::-1] - - temp_M = img_tforms[TFORM_MAT_KEY] - if temp_M.shape[0] == 2: - temp_M = np.vstack([temp_M, [0, 0, 1]]) - - if TFORM_DST_SHAPE_KEY in img_tforms: - og_dst_shape_rc = img_tforms[TFORM_DST_SHAPE_KEY] - else: - og_dst_shape_rc = ref_slide_obj.slide_dimensions_wh[0][::-1] - - img_corners_xy = warp_tools.get_corners_of_image(matching_rigid_obj.image.shape)[::-1] - warped_corners = warp_tools.warp_xy(img_corners_xy, M=temp_M, - transformation_src_shape_rc=og_src_shape_rc, - transformation_dst_shape_rc=og_dst_shape_rc, - src_shape_rc=matching_rigid_obj.image.shape, - dst_shape_rc=out_rc) - M_tform = transform.ProjectiveTransform() - M_tform.estimate(warped_corners, img_corners_xy) - for_reg_M = M_tform.params - scaled_M_dict[matching_rigid_obj.name] = for_reg_M - matching_rigid_obj.M = for_reg_M - - # Find M if not provided - for moving_idx, fixed_idx in tqdm.tqdm(rigid_registrar.iter_order): - img_obj = rigid_registrar.img_obj_list[moving_idx] - if img_obj.name in scaled_M_dict: - continue - - prev_img_obj = rigid_registrar.img_obj_list[fixed_idx] - img_obj.fixed_obj = prev_img_obj - - print(f"finding M for {img_obj.name}, which is being aligned to {prev_img_obj.name}") - - if fixed_idx == rigid_registrar.reference_img_idx: - prev_M = np.eye(3) - - to_prev_match_info = img_obj.match_dict[prev_img_obj] - src_xy = to_prev_match_info.matched_kp1_xy - dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - - transformer.estimate(dst_xy, src_xy) - img_obj.M = transformer.params - - prev_M = img_obj.M - - # Add registered image - for img_obj in rigid_registrar.img_obj_list: - img_obj.M_inv = np.linalg.inv(img_obj.M) - - img_obj.registered_img = warp_tools.warp_img(img=img_obj.image, - M=img_obj.M, - out_shape_rc=out_rc) - - img_obj.registered_shape_rc = img_obj.registered_img.shape[0:2] - - return rigid_registrar - - def rigid_register(self): - """Rigidly register slides - - Also saves thumbnails of rigidly registered images. - - Returns - ------- - rigid_registrar : SerialRigidRegistrar - SerialRigidRegistrar object that performed the rigid registration. - - """ - denoise = True - if denoise: - self.denoise_images() - - if self.do_rigid is True: - rigid_registrar = serial_rigid.register_images(self.processed_dir, - align_to_reference=self.align_to_reference, - valis_obj=self, - **self.rigid_reg_kwargs) - else: - if isinstance(self.do_rigid, dict): - # User provided transforms - rigid_tforms = self.do_rigid - elif self.do_rigid is False: - # Skip rigid registration - rigid_tforms = None - - rigid_registrar = self.rigid_register_partial(tform_dict=rigid_tforms) - - self.end_rigid_time = time() - self.rigid_registrar = rigid_registrar - - if rigid_registrar is False: - msg = "Rigid registration failed" - valtils.print_warning(msg) - - return False - - # Draw and save overlap image # - self.aligned_img_shape_rc = rigid_registrar.img_obj_list[0].registered_shape_rc - self.reference_img_idx = rigid_registrar.reference_img_idx - - ref_slide = self.slide_dict[valtils.get_name(rigid_registrar.reference_img_f)] - self.reference_img_f = ref_slide.src_f - - self.create_crop_masks(rigid_registrar) - overlap_mask, overlap_mask_bbox_xywh = self.get_crop_mask(self.crop) - - overlap_mask_bbox_xywh = overlap_mask_bbox_xywh.astype(int) - - # Create original overlap image # - self.original_overlap_img = self.create_original_composite_img(rigid_registrar) - - pathlib.Path(self.overlap_dir).mkdir(exist_ok=True, parents=True) - original_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_original_overlap.png") - warp_tools.save_img(original_overlap_img_fout, self.original_overlap_img, thumbnail_size=self.thumbnail_size) - - pathlib.Path(self.reg_dst_dir).mkdir(exist_ok= True, parents= True) - # Update attributes in slide_obj # - n_digits = len(str(rigid_registrar.size)) - for slide_reg_obj in rigid_registrar.img_obj_list: - slide_obj = self.slide_dict[slide_reg_obj.name] - slide_obj.M = slide_reg_obj.M - slide_obj.stack_idx = slide_reg_obj.stack_idx - slide_obj.reg_img_shape_rc = slide_reg_obj.registered_img.shape - slide_obj.rigid_reg_img_f = os.path.join(self.reg_dst_dir, - str.zfill(str(slide_obj.stack_idx), n_digits) + "_" + slide_obj.name + ".png") - if slide_obj.image.ndim > 2: - # Won't know if single channel image is processed RGB (bight bg) or IF channel (dark bg) - slide_obj.get_bg_color_px_pos() - - if slide_reg_obj.stack_idx == self.reference_img_idx: - continue - - fixed_slide = self.slide_dict[slide_reg_obj.fixed_obj.name] - slide_obj.fixed_slide = fixed_slide - - match_dict = slide_reg_obj.match_dict[slide_reg_obj.fixed_obj] - slide_obj.xy_matched_to_prev = match_dict.matched_kp1_xy - slide_obj.xy_in_prev = match_dict.matched_kp2_xy - - # Get points in overlap box # - prev_kp_warped_for_bbox_test = warp_tools.warp_xy(slide_obj.xy_in_prev, M=slide_obj.M) - _, prev_kp_in_bbox_idx = \ - warp_tools.get_pts_in_bbox(prev_kp_warped_for_bbox_test, overlap_mask_bbox_xywh) - - current_kp_warped_for_bbox_test = \ - warp_tools.warp_xy(slide_obj.xy_matched_to_prev, M=slide_obj.M) - - _, current_kp_in_bbox_idx = \ - warp_tools.get_pts_in_bbox(current_kp_warped_for_bbox_test, overlap_mask_bbox_xywh) - - matched_kp_in_bbox = np.intersect1d(prev_kp_in_bbox_idx, current_kp_in_bbox_idx) - slide_obj.xy_matched_to_prev_in_bbox = slide_obj.xy_matched_to_prev[matched_kp_in_bbox] - slide_obj.xy_in_prev_in_bbox = slide_obj.xy_in_prev[matched_kp_in_bbox] - - if denoise: - # Processed image may have been denoised for rigid registration. Replace with unblurred image - for img_obj in rigid_registrar.img_obj_list: - matching_slide = self.slide_dict[img_obj.name] - reg_img = matching_slide.warp_img(matching_slide.processed_img, non_rigid=False, crop=False) - img_obj.registered_img = reg_img - img_obj.image = matching_slide.processed_img - - rigid_img_list = [img_obj.registered_img for img_obj in rigid_registrar.img_obj_list] - self.rigid_overlap_img = self.draw_overlap_img(rigid_img_list) - self.rigid_overlap_img = warp_tools.crop_img(self.rigid_overlap_img, overlap_mask_bbox_xywh) - - rigid_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_rigid_overlap.png") - warp_tools.save_img(rigid_overlap_img_fout, self.rigid_overlap_img, thumbnail_size=self.thumbnail_size) - - # Overwrite black and white processed images # - for slide_name, slide_obj in self.slide_dict.items(): - slide_reg_obj = rigid_registrar.img_obj_dict[slide_name] - if not slide_obj.is_rgb: - img_to_warp = slide_reg_obj.image - else: - img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - warped_img = slide_obj.warp_img(img_to_warp, non_rigid=False, crop=self.crop) - warp_tools.save_img(slide_obj.rigid_reg_img_f, warped_img.astype(np.uint8), thumbnail_size=self.thumbnail_size) - - # Replace processed image with a thumbnail # - warp_tools.save_img(slide_obj.processed_img_f, slide_reg_obj.image, thumbnail_size=self.thumbnail_size) - - return rigid_registrar - - def micro_rigid_register(self): - - micro_rigid_registar = self.micro_rigid_registrar_cls(val_obj=self, **self.micro_rigid_registrar_params) - micro_rigid_registar.register() - - rigid_img_list = [slide_obj.warp_img(slide_obj.processed_img, non_rigid=False) for slide_obj in self.slide_dict.values()] - self.micro_rigid_overlap_img = self.draw_overlap_img(rigid_img_list) - - micro_rigid_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_micro_rigid_overlap.png") - warp_tools.save_img(micro_rigid_overlap_img_fout, self.micro_rigid_overlap_img, thumbnail_size=self.thumbnail_size) - - # Overwrite rigid registration results # - for slide_name, slide_obj in self.slide_dict.items(): - if not slide_obj.is_rgb: - img_to_warp = slide_obj.processed_img - else: - img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - warped_img = slide_obj.warp_img(img_to_warp, non_rigid=False, crop=self.crop) - warp_tools.save_img(slide_obj.rigid_reg_img_f, warped_img.astype(np.uint8), thumbnail_size=self.thumbnail_size) - - # Draw matches - # slide_idx, slide_names = list(zip(*[[slide_obj.stack_idx, slide_obj.name] for slide_obj in self.slide_dict.values()])) - # slide_order = np.argsort(slide_idx) # sorts ascending - # slide_list = [self.slide_dict[slide_names[i]] for i in slide_order] - # for moving_idx, fixed_idx in self.iter_order: - # moving_slide = slide_list[moving_idx] - # fixed_slide = slide_list[fixed_idx] - - # moving_draw_img = warp_tools.resize_img(moving_slide.image, moving_slide.processed_img.shape[0:2]) - # fixed_draw_img = warp_tools.resize_img(fixed_slide.image, fixed_slide.processed_img.shape[0:2]) - - # all_matches_img = viz.draw_matches(src_img=moving_draw_img, kp1_xy=moving_slide.xy_matched_to_prev, - # dst_img=fixed_draw_img, kp2_xy=moving_slide.xy_in_prev, - # rad=3, alignment='horizontal') - # matches_f_out = os.path.join(self.dst_dir, f"{self.val_obj.name}_{moving_slide.name}_to_{fixed_slide.name}_micro_rigid_matches.png") - # warp_tools.save_img(matches_f_out, all_matches_img) - - - def create_non_rigid_reg_mask(self): - """ - Get mask for non-rigid registration - """ - - if self.create_masks: - non_rigid_mask = self._create_mask_from_processed() - else: - non_rigid_mask = self._create_non_rigid_reg_mask_from_bbox() - - for slide_obj in self.slide_dict.values(): - slide_obj.non_rigid_reg_mask = non_rigid_mask - - # Save thumbnail of mask - ref_slide = self.get_ref_slide() - if ref_slide.img_type == slide_tools.IHC_NAME: - ref_img = warp_tools.resize_img(ref_slide.image, ref_slide.processed_img_shape_rc) - warped_ref_img = ref_slide.warp_img(ref_img, non_rigid=False, crop=CROP_REF) - else: - warped_ref_img = ref_slide.warp_img(ref_slide.processed_img, non_rigid=False, crop=CROP_REF) - - pathlib.Path(self.mask_dir).mkdir(exist_ok=True, parents=True) - thumbnail_img = self.create_thumbnail(warped_ref_img) - - draw_mask = warp_tools.resize_img(non_rigid_mask, ref_slide.reg_img_shape_rc, interp_method="nearest") - _, overlap_mask_bbox_xywh = self.get_crop_mask(CROP_REF) - draw_mask = warp_tools.crop_img(draw_mask, overlap_mask_bbox_xywh.astype(int)) - thumbnail_mask = self.create_thumbnail(draw_mask) - - thumbnail_mask_outline = viz.draw_outline(thumbnail_img, thumbnail_mask) - outline_f_out = os.path.join(self.mask_dir, f'{self.name}_non_rigid_mask.png') - warp_tools.save_img(outline_f_out, thumbnail_mask_outline) - - def _create_non_rigid_reg_mask_from_bbox(self, slide_list=None): - """Mask will be bounding box of image overlaps - - """ - ref_slide = self.get_ref_slide() - combo_mask = np.zeros(ref_slide.reg_img_shape_rc, dtype=int) - - if slide_list is None: - slide_list = list(self.slide_dict.values()) - - for slide_obj in slide_list: - img_bbox = np.full(slide_obj.processed_img_shape_rc, 255, dtype=np.uint8) - rigid_mask = slide_obj.warp_img(img_bbox, non_rigid=False, crop=False, interp_method="nearest") - combo_mask[rigid_mask > 0] += 1 - - overlap_mask = (combo_mask == self.size).astype(np.uint8) - overlap_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(overlap_mask)) - c0, r0 = overlap_bbox[:2] - c1, r1 = overlap_bbox[:2] + overlap_bbox[2:] - - non_rigid_mask = np.zeros_like(overlap_mask) - non_rigid_mask[r0:r1, c0:c1] = 255 - - return non_rigid_mask - - def _create_mask_from_processed(self, slide_list=None): - - combo_mask = np.zeros(self.aligned_img_shape_rc, dtype=int) - - if slide_list is None: - slide_list = list(self.slide_dict.values()) - - for i, slide_obj in enumerate(self.slide_dict.values()): - rigid_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=False, crop=False, interp_method="nearest") - combo_mask[rigid_mask > 0] += 1 - - temp_non_rigid_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, self.size-0.5).astype(np.uint8) - overlap_mask = 255*ndimage.binary_fill_holes(temp_non_rigid_mask).astype(np.uint8) - - to_combine_list = [None] * self.size - for i, slide_obj in enumerate(slide_list): - for_summary = exposure.rescale_intensity(slide_obj.warp_img(slide_obj.processed_img, non_rigid=False, crop=False), out_range=(0,1)) - to_combine_list[i] = for_summary - - combo_img = np.dstack(to_combine_list) - summary_img = np.median(combo_img, axis=2) - summary_img[overlap_mask == 0] = 0 - - low_t, high_t = filters.threshold_multiotsu(summary_img[overlap_mask > 0]) - fg = 255*filters.apply_hysteresis_threshold(summary_img, low_t, high_t).astype(np.uint8) - fg_bbox_mask = np.zeros_like(overlap_mask) - fg_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(fg)) - c0, r0 = fg_bbox[0:2] - c1, r1 = fg_bbox[0:2] + fg_bbox[2:] - fg_bbox_mask[r0:r1, c0:c1] = 255 - - return fg_bbox_mask - - def _create_non_rigid_reg_mask_from_rigid_masks(self, slide_list=None): - """ - Get mask that will cover all tissue. Use hysteresis thresholding to ignore - masked regions found in only 1 image. - - """ - - if slide_list is None: - slide_list = list(self.slide_dict.values()) - - combo_mask = np.zeros(self.aligned_img_shape_rc, dtype=int) - for i, slide_obj in enumerate(slide_list): - rigid_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=False, crop=False, interp_method="nearest") - combo_mask[rigid_mask > 0] += 1 - - temp_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, self.size-0.5).astype(np.uint8) - - # Draw convex hull around each region - final_mask = 255*ndimage.binary_fill_holes(temp_mask).astype(np.uint8) - final_mask = preprocessing.mask2contours(final_mask) - - return final_mask - - def pad_displacement(self, dxdy, out_shape_rc, bbox_xywh): - - is_array = not isinstance(dxdy, pyvips.Image) - if is_array: - vips_dxdy = warp_tools.numpy2vips(np.dstack(dxdy)) - else: - vips_dxdy = dxdy - - full_dxdy = pyvips.Image.black(out_shape_rc[1], out_shape_rc[0], bands=2).cast("float") - full_dxdy = full_dxdy.insert(vips_dxdy, *bbox_xywh[0:2]) - - if is_array: - full_dxdy = warp_tools.vips2numpy(full_dxdy) - full_dxdy = np.array([full_dxdy[..., 0], full_dxdy[..., 1]]) - - return full_dxdy - - def get_nr_tiling_params(self, non_rigid_registrar_cls, - processor_dict, - img_specific_args, - tile_wh): - """Get extra parameters need for tiled non-rigid registration - - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then a default processor will be used for each image based on - the inferred modality. - """ - if img_specific_args is None: - img_specific_args = {} - - for slide_obj in self.slide_dict.values(): - - processing_cls, processing_kwargs = processor_dict[slide_obj.name] - # Add registration parameters - tiled_non_rigid_reg_params = {} - tiled_non_rigid_reg_params[non_rigid_registrars.NR_CLS_KEY] = non_rigid_registrar_cls - if self.norm_method is not None: - tiled_non_rigid_reg_params[non_rigid_registrars.NR_STATS_KEY] = self.target_processing_stats - tiled_non_rigid_reg_params[non_rigid_registrars.NR_TILE_WH_KEY] = tile_wh - - tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_CLASS_KEY] = processing_cls - tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_KW_KEY] = processing_kwargs - - img_specific_args[slide_obj.name] = tiled_non_rigid_reg_params - - non_rigid_registrar_cls = non_rigid_registrars.NonRigidTileRegistrar - - return non_rigid_registrar_cls, img_specific_args - - def prep_images_for_large_non_rigid_registration(self, max_img_dim, - processor_dict, - updating_non_rigid=False, - mask=None): - - """Scale and process images for non-rigid registration using larger images - - Parameters - ---------- - max_img_dim : int, optional - Maximum size of image to be used for non-rigid registration. If None, the whole image - will be used for non-rigid registration - - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then a default processor will be used for each image based on - the inferred modality. - - updating_non_rigid : bool, optional - If `True`, the slide's current non-rigid registration will be applied - The new displacements found using these larger images can therefore be used - to update existing dxdy. If `False`, only the rigid transform will be applied, - so this will be the first non-rigid transformation. - - mask : ndarray, optional - Binary image indicating where to perform the non-rigid registration. Should be - based off an already registered image. - - Returns - ------- - img_dict : dictionary - Dictionary that can be passed to a non-rigid registrar - - max_img_dim : int - Maximum size of image to do non-rigid registration on. May be different - if the requested size was too big - - scaled_non_rigid_mask : ndarray - Scaled mask to use for non-rigid registration - - full_out_shape : ndarray of int - Shape (row, col) of the warped images, without cropping - - mask_bbox_xywh : list - Bounding box of `mask`. If `mask` is None, then so will `mask_bbox_xywh` - - """ - - warp_full_img = max_img_dim is None - if not warp_full_img: - all_max_dims = [np.any(np.max(slide_obj.slide_dimensions_wh, axis=1) >= max_img_dim) for slide_obj in self.slide_dict.values()] - if not np.all(all_max_dims): - img_maxes = [np.max(slide_obj.slide_dimensions_wh, axis=1)[0] for slide_obj in self.slide_dict.values()] - smallest_img_max = np.min(img_maxes) - msg = (f"Requested size of images for non-rigid registration was {max_img_dim}. " - f"However, not all images are this large. Setting `max_non_rigid_registration_dim_px` to " - f"{smallest_img_max}, which is the largest dimension of the smallest image") - valtils.print_warning(msg) - max_img_dim = smallest_img_max - - ref_slide = self.get_ref_slide() - - max_s = np.min(ref_slide.slide_dimensions_wh[0]/np.array(ref_slide.processed_img_shape_rc[::-1])) - if mask is None: - if warp_full_img: - s = max_s - else: - s = np.min(max_img_dim/np.array(ref_slide.processed_img_shape_rc)) - else: - # Determine how big image would have to be to get mask with maxmimum dimension = max_img_dim - if isinstance(mask, pyvips.Image): - mask_shape_rc = np.array((mask.height, mask.width)) - else: - mask_shape_rc = np.array(mask.shape[0:2]) - - to_reg_mask_sxy = (mask_shape_rc/np.array(ref_slide.reg_img_shape_rc))[::-1] - if not np.all(to_reg_mask_sxy == 1): - # Resize just in case it's huge. Only need bounding box - reg_size_mask = warp_tools.resize_img(mask, ref_slide.reg_img_shape_rc, interp_method="nearest") - else: - reg_size_mask = mask - reg_size_mask_xy = warp_tools.mask2xy(reg_size_mask) - to_reg_mask_bbox_xywh = list(warp_tools.xy2bbox(reg_size_mask_xy)) - to_reg_mask_wh = np.round(to_reg_mask_bbox_xywh[2:]).astype(int) - if warp_full_img: - s = max_s - else: - s = np.min(max_img_dim/np.array(to_reg_mask_wh)) - - if s < max_s: - full_out_shape = self.get_aligned_slide_shape(s) - else: - full_out_shape = self.get_aligned_slide_shape(0) - - if mask is None: - out_shape = full_out_shape - mask_bbox_xywh = None - else: - # If masking, the area will be smaller. Get bounding box - mask_sxy = (full_out_shape/mask_shape_rc)[::-1] - mask_bbox_xywh = list(warp_tools.xy2bbox(mask_sxy*reg_size_mask_xy)) - mask_bbox_xywh[2:] = np.round(mask_bbox_xywh[2:]).astype(int) - out_shape = mask_bbox_xywh[2:][::-1] - - if not isinstance(mask, pyvips.Image): - vips_micro_reg_mask = warp_tools.numpy2vips(mask) - else: - vips_micro_reg_mask = mask - vips_micro_reg_mask = warp_tools.resize_img(vips_micro_reg_mask, full_out_shape, interp_method="nearest") - vips_micro_reg_mask = warp_tools.crop_img(img=vips_micro_reg_mask, xywh=mask_bbox_xywh) - - if ref_slide.reader.metadata.bf_datatype is not None: - np_dtype = slide_tools.BF_FORMAT_NUMPY_DTYPE[ref_slide.reader.metadata.bf_datatype] - else: - # Assuming images not read by bio-formats are RGB read using from openslide or png, jpeg, etc... - np_dtype = "uint8" - - displacement_gb = self.size*warp_tools.calc_memory_size_gb(full_out_shape, 2, "float32") - processed_img_gb = self.size*warp_tools.calc_memory_size_gb(out_shape, 1, "uint8") - img_gb = self.size*warp_tools.calc_memory_size_gb(out_shape, ref_slide.reader.metadata.n_channels, np_dtype) - - # Size of full displacement fields, all larger processed images, and an image that will be processed - estimated_gb = img_gb + displacement_gb + processed_img_gb - use_tiler = False - if estimated_gb > TILER_THRESH_GB: - # Avoid having huge displacement fields saved in registrar. - use_tiler = True - - scaled_warped_img_list = [None] * self.size - scaled_mask_list = [None] * self.size - img_names_list = [None] * self.size - img_f_list = [None] * self.size - - print("\n======== Preparing images for non-rigid registration\n") - for slide_obj in tqdm.tqdm(self.slide_dict.values()): - # Get image to warp. Likely a larger image scaled down to specified shape # - src_img_shape_rc, src_M = warp_tools.get_src_img_shape_and_M(transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - dst_shape_rc=full_out_shape, - M=slide_obj.M) - - if max_img_dim is not None: - closest_img_levels = np.where(np.max(slide_obj.slide_dimensions_wh, axis=1) < np.max(src_img_shape_rc))[0] - if len(closest_img_levels) > 0: - closest_img_level = closest_img_levels[0] - 1 - else: - closest_img_level = len(slide_obj.slide_dimensions_wh) - 1 - else: - closest_img_level = 0 - - vips_level_img = slide_obj.slide2vips(closest_img_level) - img_to_warp = warp_tools.resize_img(vips_level_img, src_img_shape_rc) - - if updating_non_rigid: - dxdy = slide_obj.bk_dxdy - else: - dxdy = None - - # Get mask covering tissue - temp_slide_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=dxdy is not None, crop=False, interp_method="nearest") - temp_slide_mask = warp_tools.numpy2vips(temp_slide_mask) - slide_mask = warp_tools.resize_img(temp_slide_mask, full_out_shape, interp_method="nearest") - if mask_bbox_xywh is not None: - slide_mask = warp_tools.crop_img(slide_mask, mask_bbox_xywh) - - # Get mask that covers image - temp_processing_mask = pyvips.Image.black(img_to_warp.width, img_to_warp.height).invert() - processing_mask = warp_tools.warp_img(img=temp_processing_mask, M=slide_obj.M, - bk_dxdy=dxdy, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - out_shape_rc=full_out_shape, - bbox_xywh=mask_bbox_xywh, - interp_method="nearest") - - if not use_tiler: - # Process image using same method for rigid registration # - unprocessed_warped_img = warp_tools.warp_img(img=img_to_warp, M=slide_obj.M, - bk_dxdy=dxdy, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - out_shape_rc=full_out_shape, - bbox_xywh=mask_bbox_xywh, - bg_color=slide_obj.bg_color) - - unprocessed_warped_img = warp_tools.vips2numpy(unprocessed_warped_img) - - processing_cls, processing_kwargs = processor_dict[slide_obj.name] - processor = processing_cls(image=unprocessed_warped_img, src_f=slide_obj.src_f, level=closest_img_level, series=slide_obj.series) - - try: - processed_img = processor.process_image(**processing_kwargs) - except TypeError: - # processor.process_image doesn't take kwargs - processed_img = processor.process_image() - processed_img = exposure.rescale_intensity(processed_img, out_range=(0, 255)).astype(np.uint8) - - np_mask = warp_tools.vips2numpy(slide_mask) - - # print("not applying non-rigid mask (~3477)") - processed_img[np_mask == 0] = 0 - - # Normalize images using stats collected for rigid registration # - if self.norm_method is not None: - processed_img = preprocessing.norm_img_stats(img=processed_img, target_stats=self.target_processing_stats, mask=np_mask) - - warped_img = exposure.rescale_intensity(processed_img, out_range=(0, 255)).astype(np.uint8) - - else: - if not warp_full_img: - warped_img = warp_tools.warp_img(img=img_to_warp, M=slide_obj.M, - bk_dxdy=dxdy, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - out_shape_rc=full_out_shape, - bbox_xywh=mask_bbox_xywh) - else: - warped_img = slide_obj.warp_slide(0, non_rigid=updating_non_rigid, crop=mask_bbox_xywh) - - # Get mask # - if mask is not None: - slide_mask = (vips_micro_reg_mask==0).ifthenelse(0, slide_mask) - - # Update lists - img_f_list[slide_obj.stack_idx] = slide_obj.src_f - img_names_list[slide_obj.stack_idx] = slide_obj.name - scaled_warped_img_list[slide_obj.stack_idx] = warped_img - scaled_mask_list[slide_obj.stack_idx] = processing_mask - - - img_dict = {serial_non_rigid.IMG_LIST_KEY: scaled_warped_img_list, - serial_non_rigid.IMG_F_LIST_KEY: img_f_list, - serial_non_rigid.MASK_LIST_KEY: scaled_mask_list, - serial_non_rigid.IMG_NAME_KEY: img_names_list - } - - if ref_slide.non_rigid_reg_mask is not None: - vips_nr_mask = warp_tools.numpy2vips(ref_slide.non_rigid_reg_mask) - scaled_non_rigid_mask = warp_tools.resize_img(vips_nr_mask, full_out_shape, interp_method="nearest") - if mask is not None: - scaled_non_rigid_mask = scaled_non_rigid_mask.extract_area(*mask_bbox_xywh) - scaled_non_rigid_mask = (vips_micro_reg_mask == 0).ifthenelse(0, scaled_non_rigid_mask) - if not use_tiler: - scaled_non_rigid_mask = warp_tools.vips2numpy(scaled_non_rigid_mask) - else: - scaled_non_rigid_mask = None - - if mask is not None: - final_max_img_dim = np.max(mask_bbox_xywh[2:]) - else: - final_max_img_dim = max_img_dim - - return img_dict, final_max_img_dim, scaled_non_rigid_mask, full_out_shape, mask_bbox_xywh, use_tiler - - def non_rigid_register(self, rigid_registrar, processor_dict): - - """Non-rigidly register slides - - Non-rigidly register slides after performing rigid registration. - Also saves thumbnails of non-rigidly registered images and deformation - fields. - - Parameters - ---------- - rigid_registrar : SerialRigidRegistrar - SerialRigidRegistrar object that performed the rigid registration. - - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then a default processor will be used for each image based on - the inferred modality. - Returns - ------- - non_rigid_registrar : SerialNonRigidRegistrar - SerialNonRigidRegistrar object that performed serial - non-rigid registration. - - """ - - ref_slide = self.get_ref_slide() - - self.create_non_rigid_reg_mask() - non_rigid_reg_mask = ref_slide.non_rigid_reg_mask - cropped_mask_shape_rc = warp_tools.xy2bbox(warp_tools.mask2xy(non_rigid_reg_mask))[2:][::-1] - - nr_on_scaled_img = self.max_processed_image_dim_px != self.max_non_rigid_registration_dim_px or \ - (non_rigid_reg_mask is not None and np.any(cropped_mask_shape_rc != ref_slide.reg_img_shape_rc)) - - using_tiler = False - img_specific_args = {} - if nr_on_scaled_img: - - # Use higher resolution and/or roi for non-rigid - nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler = \ - self.prep_images_for_large_non_rigid_registration(max_img_dim=self.max_non_rigid_registration_dim_px, - processor_dict=processor_dict, - mask=non_rigid_reg_mask) - - self._non_rigid_bbox = mask_bbox_xywh - self.max_non_rigid_registration_dim_px = max_img_dim - - if using_tiler: - non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params(self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY], - processor_dict=processor_dict, - img_specific_args=None, - tile_wh=DEFAULT_NR_TILE_WH) - - # Update args to use tiled non-rigid registrar - self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY] = non_rigid_registrar_cls - - else: - nr_reg_src = rigid_registrar - full_out_shape_rc = ref_slide.reg_img_shape_rc - - self._full_displacement_shape_rc = full_out_shape_rc - non_rigid_registrar = serial_non_rigid.register_images(src=nr_reg_src, - align_to_reference=self.align_to_reference, - img_params = img_specific_args, - **self.non_rigid_reg_kwargs) - self.end_non_rigid_time = time() - - for d in [self.non_rigid_dst_dir, self.deformation_field_dir]: - pathlib.Path(d).mkdir(exist_ok=True, parents=True) - self.non_rigid_registrar = non_rigid_registrar - - - # Clean up displacements and expand if mask was used - for nr_name, nr_obj in non_rigid_registrar.non_rigid_obj_dict.items(): - if nr_on_scaled_img: - # If a mask was used, the displacement fields will be smaller - # So need to insert them in the full image - bk_dxdy = self.pad_displacement(nr_obj.bk_dxdy, full_out_shape_rc, mask_bbox_xywh) - fwd_dxdy = self.pad_displacement(nr_obj.fwd_dxdy, full_out_shape_rc, mask_bbox_xywh) - else: - bk_dxdy = nr_obj.bk_dxdy - fwd_dxdy = nr_obj.fwd_dxdy - - nr_obj.bk_dxdy = bk_dxdy - nr_obj.fwd_dxdy = fwd_dxdy - - # Draw overlap image # - overlap_mask, overlap_mask_bbox_xywh = self.get_crop_mask(self.crop) - overlap_mask_bbox_xywh = overlap_mask_bbox_xywh.astype(int) - - if not nr_on_scaled_img: - non_rigid_img_list = [nr_img_obj.registered_img for nr_img_obj in non_rigid_registrar.non_rigid_obj_list] - else: - non_rigid_img_list = [warp_tools.warp_img(img=o.image, - M=o.M, - bk_dxdy= non_rigid_registrar.non_rigid_obj_dict[o.name].bk_dxdy, - out_shape_rc=o.registered_img.shape[0:2], - transformation_src_shape_rc=o.image.shape[0:2], - transformation_dst_shape_rc=o.registered_img.shape[0:2]) - for o in rigid_registrar.img_obj_list] - - self.non_rigid_overlap_img = self.draw_overlap_img(non_rigid_img_list) - self.non_rigid_overlap_img = warp_tools.crop_img(self.non_rigid_overlap_img, overlap_mask_bbox_xywh) - - overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_non_rigid_overlap.png") - warp_tools.save_img(overlap_img_fout, self.non_rigid_overlap_img, thumbnail_size=self.thumbnail_size) - - n_digits = len(str(self.size)) - for slide_name, slide_obj in self.slide_dict.items(): - img_save_id = str.zfill(str(slide_obj.stack_idx), n_digits) - slide_nr_reg_obj = non_rigid_registrar.non_rigid_obj_dict[slide_name] - - if not using_tiler: - slide_obj.bk_dxdy = slide_nr_reg_obj.bk_dxdy - slide_obj.fwd_dxdy = slide_nr_reg_obj.fwd_dxdy - else: - # save displacements as images - pathlib.Path(self.displacements_dir).mkdir(exist_ok=True, parents=True) - slide_obj.stored_dxdy = True - bk_dxdy_f, fwd_dxdy_f = slide_obj.get_displacement_f() - slide_obj._bk_dxdy_f = bk_dxdy_f - slide_obj._fwd_dxdy_f = fwd_dxdy_f - # Save space by only writing the necessary areas. Most displacements may be 0 - cropped_bk_dxdy = slide_nr_reg_obj.bk_dxdy.extract_area(*mask_bbox_xywh) - cropped_fwd_dxdy = slide_nr_reg_obj.fwd_dxdy.extract_area(*mask_bbox_xywh) - - cropped_bk_dxdy.cast("float").tiffsave(slide_obj._bk_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - cropped_fwd_dxdy.cast("float").tiffsave(slide_obj._fwd_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - - slide_obj.nr_rigid_reg_img_f = os.path.join(self.non_rigid_dst_dir, img_save_id + "_" + slide_obj.name + ".png") - - if not slide_obj.is_rgb: - img_to_warp = rigid_registrar.img_obj_dict[slide_name].image - else: - img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - warped_img = slide_obj.warp_img(img_to_warp, non_rigid=True, crop=self.crop) - warp_tools.save_img(slide_obj.nr_rigid_reg_img_f, warped_img, thumbnail_size=self.thumbnail_size) - - # Draw displacements on image actually used in non-rigid. Might be higher resolution - if not isinstance(slide_nr_reg_obj.bk_dxdy, pyvips.Image): - draw_dxdy = np.dstack(slide_nr_reg_obj.bk_dxdy) - else: - #pyvips - draw_dxdy = slide_nr_reg_obj.bk_dxdy - - if nr_on_scaled_img: - draw_dxdy = warp_tools.crop_img(draw_dxdy, self._non_rigid_bbox) - - dxdy_shape = warp_tools.get_shape(draw_dxdy) - thumbnail_scaling = np.min(self.thumbnail_size/np.array(dxdy_shape[0:2])) - thumbnail_bk_dxdy = self.create_thumbnail(draw_dxdy) - thumbnail_bk_dxdy *= float(thumbnail_scaling) - - if isinstance(thumbnail_bk_dxdy, pyvips.Image): - thumbnail_bk_dxdy = warp_tools.vips2numpy(thumbnail_bk_dxdy) - - draw_img = warp_tools.resize_img(slide_nr_reg_obj.registered_img, thumbnail_bk_dxdy[..., 0].shape) - if isinstance(draw_img, pyvips.Image): - draw_img = warp_tools.vips2numpy(draw_img) - - draw_img = exposure.rescale_intensity(draw_img, out_range=(0, 255)) - - if draw_img.ndim == 2: - draw_img = np.dstack([draw_img] * 3) - - thumbanil_deform_grid = viz.color_displacement_tri_grid(bk_dx=thumbnail_bk_dxdy[..., 0], - bk_dy=thumbnail_bk_dxdy[..., 1], - img=draw_img, - n_grid_pts=25) - - deform_img_f = os.path.join(self.deformation_field_dir, img_save_id + "_" + slide_obj.name + ".png") - warp_tools.save_img(deform_img_f, thumbanil_deform_grid, thumbnail_size=self.thumbnail_size) - - return non_rigid_registrar - - def measure_error(self): - """Measure registration error - - Error is measured as the distance between matched features - after registration. - - Returns - ------- - summary_df : Dataframe - `summary_df` contains various information about the registration. - - The "from" column is the name of the image, while the "to" column - name of the image it was aligned to. "from" is analagous to "moving" - or "current", while "to" is analgous to "fixed" or "previous". - - Columns begining with "original" refer to error measurements of the - unregistered images. Those beginning with "rigid" or "non_rigid" refer - to measurements related to rigid or non-rigid registration, respectively. - - Columns beginning with "mean" are averages of error measurements. In - the case of errors based on feature distances (i.e. those ending in "D"), - the mean is weighted by the number of feature matches between "from" and "to". - - Columns endining in "D" indicate the median distance between matched - features in "from" and "to". - - Columns ending in "rTRE" indicate the target registration error between - "from" and "to". - - Columns ending in "mattesMI" contain measurements of the Mattes mutual - information between "from" and "to". - - "processed_img_shape" indicates the shape (row, column) of the processed - image actually used to conduct the registration - - "shape" is the shape of the slide at full resolution - - "aligned_shape" is the shape of the registered full resolution slide - - "physical_units" are the names of the pixels physcial unit, e.g. u'\u00B5m' - - "resolution" is the physical unit per pixel - - "name" is the name assigned to the Valis instance - - "rigid_time_minutes" is the total number of minutes it took - to convert the images and then rigidly align them. - - "non_rigid_time_minutes" is the total number of minutes it took - to convert the images, and then perform rigid -> non-rigid registration. - - """ - - path_list = [None] * (self.size) - all_og_d = [None] * (self.size) - all_og_tre = [None] * (self.size) - - all_rigid_d = [None] * (self.size) - all_rigid_tre = [None] * (self.size) - - all_nr_d = [None] * (self.size) - all_nr_tre = [None] * (self.size) - - all_n = [None] * (self.size) - from_list = [None] * (self.size) - to_list = [None] * (self.size) - shape_list = [None] * (self.size) - processed_img_shape_list = [None] * (self.size) - unit_list = [None] * (self.size) - resolution_list = [None] * (self.size) - - slide_obj_list = list(self.slide_dict.values()) - outshape = slide_obj_list[0].aligned_slide_shape_rc - - ref_slide = self.get_ref_slide() - ref_diagonal = np.sqrt(np.sum(np.power(ref_slide.processed_img_shape_rc, 2))) - - measure_idx = [] - for slide_obj in tqdm.tqdm(self.slide_dict.values()): - i = slide_obj.stack_idx - slide_name = slide_obj.name - - shape_list[i] = tuple(slide_obj.slide_shape_rc) - processed_img_shape_list[i] = tuple(slide_obj.processed_img_shape_rc) - unit_list[i] = slide_obj.units - resolution_list[i] = slide_obj.resolution - from_list[i] = slide_name - path_list[i] = slide_obj.src_f - - if slide_obj.name == ref_slide.name or slide_obj.is_empty: - continue - - measure_idx.append(i) - prev_slide_obj = slide_obj.fixed_slide - to_list[i] = prev_slide_obj.name - - img_T = warp_tools.get_padding_matrix(slide_obj.processed_img_shape_rc, - slide_obj.reg_img_shape_rc) - - prev_T = warp_tools.get_padding_matrix(prev_slide_obj.processed_img_shape_rc, - prev_slide_obj.reg_img_shape_rc) - - - prev_kp_in_slide = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_T, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=False) - - current_kp_in_slide = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=img_T, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=False) - - og_d = warp_tools.calc_d(prev_kp_in_slide, current_kp_in_slide) - - og_rtre = og_d/ref_diagonal - median_og_tre = np.median(og_rtre) - og_d *= slide_obj.resolution - median_d_og = np.median(og_d) - - all_og_d[i] = median_d_og - all_og_tre[i] = median_og_tre - - - prev_warped_rigid = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_slide_obj.M, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=False) - - current_warped_rigid = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=slide_obj.M, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=False) - - - rigid_d = warp_tools.calc_d(prev_warped_rigid, current_warped_rigid) - rtre = rigid_d/ref_diagonal - median_rigid_tre = np.median(rtre) - rigid_d *= slide_obj.resolution - median_d_rigid = np.median(rigid_d) - - all_rigid_d[i] = median_d_rigid - all_n[i] = len(rigid_d) - all_rigid_tre[i] = median_rigid_tre - - if slide_obj.bk_dxdy is not None: - - - prev_warped_nr = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_slide_obj.M, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=True) - - current_warped_nr = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=slide_obj.M, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=True) - - nr_d = warp_tools.calc_d(prev_warped_nr, current_warped_nr) - nrtre = nr_d/ref_diagonal - mean_nr_tre = np.median(nrtre) - - nr_d *= slide_obj.resolution - median_d_nr = np.median(nr_d) - all_nr_d[i] = median_d_nr - all_nr_tre[i] = mean_nr_tre - - weights = np.array(all_n)[measure_idx] - mean_og_d = np.average(np.array(all_og_d)[measure_idx], weights=weights) - median_og_tre = np.average(np.array(all_og_tre)[measure_idx], weights=weights) - - mean_rigid_d = np.average(np.array(all_rigid_d)[measure_idx], weights=weights) - median_rigid_tre = np.average(np.array(all_rigid_tre)[measure_idx], weights=weights) - - rigid_min = (self.end_rigid_time - self.start_time)/60 - - self.summary_df = pd.DataFrame({ - "filename": path_list, - "from":from_list, - "to": to_list, - "original_D": all_og_d, - "original_rTRE": all_og_tre, - "rigid_D": all_rigid_d, - "rigid_rTRE": all_rigid_tre, - "non_rigid_D": all_nr_d, - "non_rigid_rTRE": all_nr_tre, - "processed_img_shape": processed_img_shape_list, - "shape": shape_list, - "aligned_shape": [tuple(outshape)]*self.size, - "mean_original_D": [mean_og_d]*self.size, - "mean_rigid_D": [mean_rigid_d]*self.size, - "physical_units":unit_list, - "resolution":resolution_list, - "name": [self.name]*self.size, - "rigid_time_minutes" : [rigid_min]*self.size - }) - - if any([d for d in all_nr_d if d is not None]): - mean_nr_d = np.average(np.array(all_nr_d)[measure_idx], weights=weights) - mean_nr_tre = np.average(np.array(all_nr_tre)[measure_idx], weights=weights) - non_rigid_min = (self.end_non_rigid_time - self.start_time)/60 - - self.summary_df["mean_non_rigid_D"] = [mean_nr_d]*self.size - self.summary_df["non_rigid_time_minutes"] = [non_rigid_min]*self.size - - return self.summary_df - - def register(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None, - reader_cls=None): - - """Register a collection of images - - This function will convert the slides to images, pre-process and normalize them, and - then conduct rigid registration. Non-rigid registration will then be performed if the - `non_rigid_registrar_cls` argument used to initialize the Valis object was not None. - - In addition to the objects returned, the desination directory (i.e. `dst_dir`) - will contain thumbnails so that one can visualize the results: converted image - thumbnails will be in "images/"; processed images in "processed/"; - rigidly aligned images in "rigid_registration/"; non-rigidly aligned images in "non_rigid_registration/"; - non-rigid deformation field images (i.e. warped grids colored by the direction and magntidue) - of the deformation) will be in ""deformation_fields/". The size of these thumbnails - is determined by the `thumbnail_size` argument used to initialze this object. - - One can get a sense of how well the registration worked by looking - in the "overlaps/", which shows how the images overlap before - registration, after rigid registration, and after non-rigid registration. Each image - is created by coloring an inverted greyscale version of the processed images, and then - blending those images. - - The "data/" directory will contain a pickled copy of this registrar, which can be - later be opened (unpickled) and used to warp slides and/or point data. - - "data/" will also contain the `summary_df` saved as a csv file. - - - Parameters - ---------- - brightfield_processing_cls : preprocessing.ImageProcesser - preprocessing.ImageProcesser used to pre-process brightfield images to make - them look as similar as possible. - - brightfield_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `brightfield_processing_cls` - - if_processing_cls : preprocessing.ImageProcesser - preprocessing.ImageProcesser used to pre-process immunofluorescent images - to make them look as similar as possible. - - if_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `if_processing_cls` - - processor_dict : dict - Each key should be the filename of the image, and the value either a subclassed - preprocessing.ImageProcessor, or a list, where the 1st element is the processor, - and the second element a dictionary of keyword arguments passed to the processor. - If `None`, then a default processor will be used for each image based on - the inferred modality. - - reader_cls : SlideReader, optional - Uninstantiated SlideReader class that will convert - the slide to an image, and also collect metadata. If None (the default), - the appropriate SlideReader will be found by `slide_io.get_slide_reader`. - This option is provided in case the slides cannot be opened by a current - SlideReader class. In this case, the user should create a subclass of - SlideReader. See slide_io.SlideReader for details. - - Returns - ------- - rigid_registrar : SerialRigidRegistrar - SerialRigidRegistrar object that performed the rigid registration. - This object can be pickled if so desired - - non_rigid_registrar : SerialNonRigidRegistrar - SerialNonRigidRegistrar object that performed serial - non-rigid registration. This object can be pickled if so desired. - - summary_df : Dataframe - `summary_df` contains various information about the registration. - - The "from" column is the name of the image, while the "to" column - name of the image it was aligned to. "from" is analagous to "moving" - or "current", while "to" is analgous to "fixed" or "previous". - - Columns begining with "original" refer to error measurements of the - unregistered images. Those beginning with "rigid" or "non_rigid" refer - to measurements related to rigid or non-rigid registration, respectively. - - Columns beginning with "mean" are averages of error measurements. In - the case of errors based on feature distances (i.e. those ending in "D"), - the mean is weighted by the number of feature matches between "from" and "to". - - Columns endining in "D" indicate the median distance between matched - features in "from" and "to". - - Columns ending in "TRE" indicate the target registration error between - "from" and "to". - - Columns ending in "mattesMI" contain measurements of the Mattes mutual - information between "from" and "to". - - "processed_img_shape" indicates the shape (row, column) of the processed - image actually used to conduct the registration - - "shape" is the shape of the slide at full resolution - - "aligned_shape" is the shape of the registered full resolution slide - - "physical_units" are the names of the pixels physcial unit, e.g. u'\u00B5m' - - "resolution" is the physical unit per pixel - - "name" is the name assigned to the Valis instance - - "rigid_time_minutes" is the total number of minutes it took - to convert the images and then rigidly align them. - - "non_rigid_time_minutes" is the total number of minutes it took - to convert the images, and then perform rigid -> non-rigid registration. - - """ - - self.start_time = time() - try: - print("\n==== Converting images\n") - self.convert_imgs(series=self.series, reader_cls=reader_cls) - - print("\n==== Processing images\n") - slide_processors = self.create_img_processor_dict(brightfield_processing_cls=brightfield_processing_cls, - brightfield_processing_kwargs=brightfield_processing_kwargs, - if_processing_cls=if_processing_cls, - if_processing_kwargs=if_processing_kwargs, - processor_dict=processor_dict) - - self.brightfield_procsseing_fxn_str = brightfield_processing_cls.__name__ - self.if_processing_fxn_str = if_processing_cls.__name__ - self.process_imgs(processor_dict=slide_processors) - - print("\n==== Rigid registration\n") - rigid_registrar = self.rigid_register() - aligned_slide_shape_rc = self.get_aligned_slide_shape(0) - self.aligned_slide_shape_rc = aligned_slide_shape_rc - for slide_obj in self.slide_dict.values(): - slide_obj.aligned_slide_shape_rc = aligned_slide_shape_rc - - if self.micro_rigid_registrar_cls is not None: - print("\n==== Micro-rigid registration\n") - self.micro_rigid_register() - - if rigid_registrar is False: - return None, None, None - - if self.non_rigid_registrar_cls is not None: - print("\n==== Non-rigid registration\n") - non_rigid_registrar = self.non_rigid_register(rigid_registrar, slide_processors) - - else: - non_rigid_registrar = None - - print("\n==== Measuring error\n") - # aligned_slide_shape_rc = self.get_aligned_slide_shape(0) - # self.aligned_slide_shape_rc = aligned_slide_shape_rc - # for slide_obj in self.slide_dict.values(): - # slide_obj.aligned_slide_shape_rc = aligned_slide_shape_rc - - self._add_empty_slides() - - error_df = self.measure_error() - self.cleanup() - - pathlib.Path(self.data_dir).mkdir(exist_ok=True, parents=True) - f_out = os.path.join(self.data_dir, self.name + "_registrar.pickle") - self.reg_f = f_out - pickle.dump(self, open(f_out, 'wb')) - - data_f_out = os.path.join(self.data_dir, self.name + "_summary.csv") - error_df.to_csv(data_f_out, index=False) - except Exception as e: - valtils.print_warning(e) - print(traceback.format_exc()) - kill_jvm() - return None, None, None - - - return rigid_registrar, non_rigid_registrar, error_df - - def cleanup(self): - """Remove objects that can't be pickled - """ - self.rigid_reg_kwargs["feature_detector"] = None - self.rigid_reg_kwargs["affine_optimizer"] = None - self.non_rigid_registrar_cls = None - self.rigid_registrar = None - self.micro_rigid_registrar_cls = None - self.non_rigid_registrar = None - - - @valtils.deprecated_args(max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px") - def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None, - max_non_rigid_registration_dim_px=DEFAULT_MAX_NON_RIGID_REG_SIZE, - non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, - non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, - reference_img_f=None, align_to_reference=False, mask=None, tile_wh=DEFAULT_NR_TILE_WH): - """Improve alingment of microfeatures by performing second non-rigid registration on larger images - - Caclculates additional non-rigid deformations using a larger image - - Parameters - ---------- - brightfield_processing_cls : preprocessing.ImageProcesser - preprocessing.ImageProcesser used to pre-process brightfield images to make - them look as similar as possible. - - brightfield_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `brightfield_processing_cls` - - if_processing_cls : preprocessing.ImageProcesser - preprocessing.ImageProcesser used to pre-process immunofluorescent images - to make them look as similar as possible. - - if_processing_kwargs : dict - Dictionary of keyward arguments to be passed to `if_processing_cls` - - max_non_rigid_registration_dim_px : int, optional - Maximum width or height of images used for non-rigid registration. - If None, then the full sized image will be used. However, this - may take quite some time to complete. - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be the reference, and - images will be aligned towards it. If provided, images will be - aligned to this reference. - - align_to_reference : bool, optional - If `False`, images will be non-rigidly aligned serially towards the - reference image. If `True`, images will be non-rigidly aligned - directly to the reference image. If `reference_img_f` is None, - then the reference image will be the one in the middle of the stack. - - non_rigid_registrar_cls : NonRigidRegistrar, optional - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images. See - the `non_rigid_registrars` module for a desciption of available - methods. If a desired non-rigid registration method is not available, - one can be implemented by subclassing.NonRigidRegistrar. - - non_rigid_reg_params: dictionary, optional - Dictionary containing key, value pairs to be used to initialize - `non_rigid_registrar_cls`. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. See the NonRigidRegistrar classes in - `non_rigid_registrars` for the available non-rigid registration - methods and arguments. - - """ - - - # Remove empty slides - for empty_slide_name, empty_slide in self._empty_slides.items(): - del self.slide_dict[empty_slide_name] - self.size -= 1 - - ref_slide = self.get_ref_slide() - if mask is None: - if ref_slide.non_rigid_reg_mask is not None: - mask = ref_slide.non_rigid_reg_mask.copy() - - slide_processors = self.create_img_processor_dict(brightfield_processing_cls=brightfield_processing_cls, - brightfield_processing_kwargs=brightfield_processing_kwargs, - if_processing_cls=if_processing_cls, - if_processing_kwargs=if_processing_kwargs, - processor_dict=processor_dict) - - nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler = \ - self.prep_images_for_large_non_rigid_registration(max_img_dim=max_non_rigid_registration_dim_px, - processor_dict=slide_processors, - updating_non_rigid=True, - mask=mask) - - img_specific_args = None - write_dxdy = isinstance(ref_slide.bk_dxdy, pyvips.Image) - - if using_tiler: - # Have determined that these images will be too big - msg = (f"Registration would more than {TILER_THRESH_GB} GB if all images opened in memory. " - f"Will use NonRigidTileRegistrar to register cooresponding tiles to reduce memory consumption, " - f"but this method is experimental") - - valtils.print_warning(msg) - - write_dxdy = True - non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params(non_rigid_registrar_cls, - processor_dict=slide_processors, - img_specific_args=img_specific_args, - tile_wh=tile_wh) - - print("\n==== Performing microregistration\n") - non_rigid_registrar = serial_non_rigid.register_images(src=nr_reg_src, - non_rigid_reg_class=non_rigid_registrar_cls, - non_rigid_reg_params=non_rigid_reg_params, - reference_img_f=reference_img_f, - mask=non_rigid_reg_mask, - align_to_reference=align_to_reference, - name=self.name, - img_params=img_specific_args - ) - - pathlib.Path(self.micro_reg_dir).mkdir(exist_ok=True, parents=True) - out_shape = full_out_shape_rc - n_digits = len(str(self.size)) - micro_reg_imgs = [None] * self.size - - # Update displacements - for slide_obj in self.slide_dict.values(): - - if slide_obj == ref_slide: - continue - - nr_obj = non_rigid_registrar.non_rigid_obj_dict[slide_obj.name] - # Will be combining original and new dxdy as pyvips Images - if not isinstance(slide_obj.bk_dxdy[0], pyvips.Image): - vips_current_bk_dxdy = warp_tools.numpy2vips(np.dstack(slide_obj.bk_dxdy)).cast("float") - vips_current_fwd_dxdy = warp_tools.numpy2vips(np.dstack(slide_obj.fwd_dxdy)).cast("float") - else: - vips_current_bk_dxdy = slide_obj.bk_dxdy - vips_current_fwd_dxdy = slide_obj.fwd_dxdy - - if not isinstance(nr_obj.bk_dxdy, pyvips.Image): - vips_new_bk_dxdy = warp_tools.numpy2vips(np.dstack(nr_obj.bk_dxdy)).cast("float") - vips_new_fwd_dxdy = warp_tools.numpy2vips(np.dstack(nr_obj.fwd_dxdy)).cast("float") - else: - vips_new_bk_dxdy = nr_obj.bk_dxdy - vips_new_fwd_dxdy = nr_obj.fwd_dxdy - - if np.any(non_rigid_registrar.shape != full_out_shape_rc): - # Micro-registration performed on sub-region. Need to put in full image - vips_new_bk_dxdy = self.pad_displacement(vips_new_bk_dxdy, full_out_shape_rc, mask_bbox_xywh) - vips_new_fwd_dxdy = self.pad_displacement(vips_new_fwd_dxdy, full_out_shape_rc, mask_bbox_xywh) - - # Scale original dxdy to match scaled shape of new dxdy - slide_sxy = (np.array(out_shape)/np.array([vips_current_bk_dxdy.height, vips_current_bk_dxdy.width]))[::-1] - if not np.all(slide_sxy == 1): - scaled_bk_dx = float(slide_sxy[0])*vips_current_bk_dxdy[0] - scaled_bk_dy = float(slide_sxy[1])*vips_current_bk_dxdy[1] - vips_current_bk_dxdy = scaled_bk_dx.bandjoin(scaled_bk_dy) - vips_current_bk_dxdy = warp_tools.resize_img(vips_current_bk_dxdy, out_shape) - - scaled_fwd_dx = float(slide_sxy[0])*vips_current_fwd_dxdy[0] - scaled_fwd_dy = float(slide_sxy[1])*vips_current_fwd_dxdy[1] - vips_current_fwd_dxdy = scaled_fwd_dx.bandjoin(scaled_fwd_dy) - vips_current_fwd_dxdy = warp_tools.resize_img(vips_current_fwd_dxdy, out_shape) - - vips_updated_bk_dxdy = vips_current_bk_dxdy + vips_new_bk_dxdy - vips_updated_fwd_dxdy = vips_current_fwd_dxdy + vips_new_fwd_dxdy - - if not write_dxdy: - # Will save numpy dxdy as Slide attributes - np_updated_bk_dxdy = warp_tools.vips2numpy(vips_updated_bk_dxdy) - np_updated_fwd_dxdy = warp_tools.vips2numpy(vips_updated_fwd_dxdy) - - slide_obj.bk_dxdy = np.array([np_updated_bk_dxdy[..., 0], np_updated_bk_dxdy[..., 1]]) - slide_obj.fwd_dxdy = np.array([np_updated_fwd_dxdy[..., 0], np_updated_fwd_dxdy[..., 1]]) - else: - pathlib.Path(self.displacements_dir).mkdir(exist_ok=True, parents=True) - slide_obj.stored_dxdy = True - - bk_dxdy_f, fwd_dxdy_f = slide_obj.get_displacement_f() - slide_obj._bk_dxdy_f = bk_dxdy_f - slide_obj._fwd_dxdy_f = fwd_dxdy_f - - # Save space by only writing the necessary areas. Most displacements may be 0 - cropped_bk_dxdy = vips_updated_bk_dxdy.extract_area(*mask_bbox_xywh) - cropped_fwd_dxdy = vips_updated_fwd_dxdy.extract_area(*mask_bbox_xywh) - - if not os.path.exists(slide_obj._bk_dxdy_f): - cropped_bk_dxdy.cast("float").tiffsave(slide_obj._bk_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - - else: - # Don't seem to be able to overwrite directly because also accessing it? - disp_dir, temp_bk_f = os.path.split(slide_obj._bk_dxdy_f) - full_temp_dx_f = os.path.join(disp_dir, f".temp_{temp_bk_f}") - cropped_bk_dxdy.cast("float").tiffsave(full_temp_dx_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - os.remove(slide_obj._bk_dxdy_f) - os.rename(full_temp_dx_f, slide_obj._bk_dxdy_f) - - if not os.path.exists(slide_obj._fwd_dxdy_f): - cropped_fwd_dxdy.cast("float").tiffsave(slide_obj._fwd_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - else: - disp_dir, temp_fwd_f = os.path.split(slide_obj._fwd_dxdy_f) - full_temp_fwd_f = os.path.join(disp_dir, f".temp_{temp_fwd_f}") - cropped_fwd_dxdy.cast("float").tiffsave(full_temp_fwd_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - os.remove(slide_obj._fwd_dxdy_f) - os.rename(full_temp_fwd_f, slide_obj._fwd_dxdy_f) - - # Update dxdy padding attributes here, in the event that previous displacements were also saved as files - # Updating these attributes earlier will cause errors - self._non_rigid_bbox = mask_bbox_xywh - self._full_displacement_shape_rc = full_out_shape_rc - for slide_obj in self.slide_dict.values(): - if not slide_obj.is_rgb: - img_to_warp = slide_obj.processed_img - else: - img_to_warp = slide_obj.image - - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - micro_reg_img = slide_obj.warp_img(img_to_warp, non_rigid=True, crop=self.crop) - - img_save_id = str.zfill(str(slide_obj.stack_idx), n_digits) - micro_fout = os.path.join(self.micro_reg_dir, f"{img_save_id}_{slide_obj.name}.png") - micro_thumb = self.create_thumbnail(micro_reg_img) - warp_tools.save_img(micro_fout, micro_thumb) - - processed_micro_reg_img = slide_obj.warp_img(slide_obj.processed_img) - micro_reg_imgs[slide_obj.stack_idx] = processed_micro_reg_img - - - # Add empty slides back and save results - for empty_slide_name, empty_slide in self._empty_slides.items(): - self.slide_dict[empty_slide_name] = empty_slide - self.size += 1 - - pickle.dump(self, open(self.reg_f, 'wb')) - - micro_overlap = self.draw_overlap_img(micro_reg_imgs) - self.micro_reg_overlap_img = micro_overlap - overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_micro_reg.png") - warp_tools.save_img(overlap_img_fout, micro_overlap, thumbnail_size=self.thumbnail_size) - - - print("\n==== Measuring error\n") - error_df = self.measure_error() - data_f_out = os.path.join(self.data_dir, self.name + "_summary.csv") - error_df.to_csv(data_f_out, index=False) - - return non_rigid_registrar, error_df - - def get_aligned_slide_shape(self, level): - """Get size of aligned images - - Parameters - ---------- - level : int, float - If `level` is an integer, then it is assumed that `level` is referring to - the pyramid level that will be warped. - - If `level` is a float, it is assumed `level` is how much to rescale the - registered image's size. - - """ - - ref_slide = self.get_ref_slide() - - if np.issubdtype(type(level), np.integer): - n_levels = len(ref_slide.slide_dimensions_wh) - if level >= n_levels: - msg = (f"requested to scale transformation for pyramid level {level}, ", - f"but the image only has {n_levels} (starting from 0). ", - f"Will use level {level-1}, which is the smallest level") - valtils.print_warning(msg) - level = level - 1 - - slide_shape_rc = ref_slide.slide_dimensions_wh[level][::-1] - s_rc = (slide_shape_rc/np.array(ref_slide.processed_img_shape_rc)) - else: - s_rc = level - - aligned_out_shape_rc = np.ceil(np.array(ref_slide.reg_img_shape_rc)*s_rc).astype(int) - - return aligned_out_shape_rc - - - @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") - def warp_and_save_slides(self, dst_dir, level=0, non_rigid=True, - crop=True, - colormap=None, - interp_method="bicubic", - tile_wh=None, compression="lzw"): - - f"""Warp and save all slides - - Each slide will be saved as an ome.tiff. The extension of each file will - be changed to ome.tiff if it is not already. - - Parameters - ---------- - dst_dir : str - Path to were the warped slides will be saved. - - level : int, optional - Pyramid level to be warped. Default is 0, which means the highest - resolution image will be warped and saved. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - colormap : list - List of RGB colors (0-255) to use for channel colors - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - tile_wh : int, optional - Tile width and height used to save image - - compression : str, optional - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See pyips for more details. - - """ - pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - - for slide_obj in tqdm.tqdm(self.slide_dict.values()): - slide_cmap = None - if colormap is not None: - chnl_names = slide_obj.reader.metadata.channel_names - if chnl_names is not None: - if len(colormap) >= len(chnl_names): - slide_cmap = {chnl_names[i]:tuple(colormap[i]) for i in range(len(chnl_names))} - - else: - msg = f'{slide_obj.name} has {len(chnl_names)} but colormap only has {len(colormap)} colors' - valtils.print_warning(msg) - - dst_f = os.path.join(dst_dir, slide_obj.name + ".ome.tiff") - slide_obj.warp_and_save_slide(dst_f=dst_f, level=level, - non_rigid=non_rigid, - crop=crop, - interp_method=interp_method, - colormap=slide_cmap, - tile_wh=tile_wh, compression=compression) - - @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") - def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, - crop=True, channel_name_dict=None, - src_f_list=None, colormap=None, - drop_duplicates=True, tile_wh=None, - interp_method="bicubic", compression="lzw"): - - """Warp and merge registered slides - - Parameters - ---------- - dst_f : str, optional - Path to were the warped slide will be saved. If None, then the slides will be merged - but not saved. - - level : int, optional - Pyramid level to be warped. Default is 0, which means the highest - resolution image will be warped and saved. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - channel_name_dict : dict of lists, optional. - key = slide file name, value = list of channel names for that slide. If None, - the the channel names found in each slide will be used. - - src_f_list : list of str, optionaal - List of paths to slide to be warped. If None (the default), Valis.original_img_list - will be used. Otherwise, the paths to which `src_f_list` points to should - be an alternative copy of the slides, such as ones that have undergone - processing (e.g. stain segmentation), had a mask applied, etc... - - colormap : list - List of RGB colors (0-255) to use for channel colors - - drop_duplicates : bool, optional - Whether or not to drop duplicate channels that might be found in multiple slides. - For example, if DAPI is in multiple slides, then the only the DAPI channel in the - first slide will be kept. - - tile_wh : int, optional - Tile width and height used to save image - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - compression : str - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See pyips for more details. - - Returns - ------- - merged_slide : pyvips.Image - Image with all channels merged. If `drop_duplicates` is True, then this - will only contain unique channels. - - all_channel_names : list of str - Name of each channel in the image - - ome_xml : str - OME-XML string containing the slide's metadata - - """ - - if channel_name_dict is not None: - channel_name_dict_by_name = {valtils.get_name(k):channel_name_dict[k] for k in channel_name_dict} - - if src_f_list is None: - src_f_list = self.original_img_list - - all_channel_names = [] - merged_slide = None - - for f in src_f_list: - slide_name = valtils.get_name(os.path.split(f)[1]) - slide_obj = self.slide_dict[slide_name] - - warped_slide = slide_obj.warp_slide(level, non_rigid=non_rigid, - crop=crop, - interp_method=interp_method) - - keep_idx = list(range(warped_slide.bands)) - if channel_name_dict is not None: - slide_channel_names = channel_name_dict_by_name[slide_obj.name] - - if drop_duplicates: - keep_idx = [idx for idx in range(len(slide_channel_names)) if - slide_channel_names[idx] not in all_channel_names] - - else: - slide_channel_names = slide_obj.reader.metadata.channel_names - slide_channel_names = [c + " (" + slide_name + ")" for c in slide_channel_names] - - if drop_duplicates and warped_slide.bands != len(keep_idx): - keep_channels = [warped_slide[c] for c in keep_idx] - slide_channel_names = [slide_channel_names[idx] for idx in keep_idx] - if len(keep_channels) == 1: - warped_slide = keep_channels[0] - else: - warped_slide = keep_channels[0].bandjoin(keep_channels[1:]) - print(f"merging {', '.join(slide_channel_names)}") - - if merged_slide is None: - merged_slide = warped_slide - else: - merged_slide = merged_slide.bandjoin(warped_slide) - - all_channel_names.extend(slide_channel_names) - - - if colormap is not None: - if len(colormap) >= len(all_channel_names): - cmap_dict = {all_channel_names[i]:tuple(colormap[i]) for i in range(len(all_channel_names))} - - else: - msg = f'Merged image has {len(all_channel_names)} but colormap only has {len(colormap)} colors' - valtils.print_warning(msg) - - else: - cmap_dict = None - - slide_obj = self.get_ref_slide() - px_phys_size = slide_obj.reader.scale_physical_size(level) - bf_dtype = slide_io.vips2bf_dtype(merged_slide.format) - out_xyczt = slide_io.get_shape_xyzct((merged_slide.width, merged_slide.height), merged_slide.bands) - - ome_xml_obj = slide_io.create_ome_xml(out_xyczt, bf_dtype, is_rgb=False, - pixel_physical_size_xyu=px_phys_size, - channel_names=all_channel_names, - colormap=cmap_dict) - ome_xml = ome_xml_obj.to_xml() - - if dst_f is not None: - dst_dir = os.path.split(dst_f)[0] - pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - if tile_wh is None: - tile_wh = slide_obj.reader.metadata.optimal_tile_wh - if level != 0: - down_sampling = np.mean(slide_obj.slide_dimensions_wh[level]/slide_obj.slide_dimensions_wh[0]) - tile_wh = int(np.round(tile_wh*down_sampling)) - tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 - if tile_wh < 16: - tile_wh = 16 - if np.any(np.array(out_xyczt[0:2]) < tile_wh): - tile_wh = min(out_xyczt[0:2]) - - slide_io.save_ome_tiff(merged_slide, dst_f=dst_f, - ome_xml=ome_xml,tile_wh=tile_wh, - compression=compression) - - return merged_slide, all_channel_names, ome_xml - - - diff --git a/examples/acrobat_2023/valis/serial_non_rigid.py b/examples/acrobat_2023/valis/serial_non_rigid.py deleted file mode 100644 index fb9c68b0..00000000 --- a/examples/acrobat_2023/valis/serial_non_rigid.py +++ /dev/null @@ -1,1176 +0,0 @@ -"""Classes and functions to perform serial non-rigid registration of a set of images - -""" - -import numpy as np -from skimage import io -from tqdm import tqdm -import os -import imghdr -from time import time -import pathlib -import pandas as pd -import pickle -import cv2 -import pyvips -import inspect - -from . import warp_tools -from . import non_rigid_registrars -from . import valtils -from . import serial_rigid -from . import viz -from . import preprocessing - -IMG_LIST_KEY = "img_list" -IMG_F_LIST_KEY = "img_f_list" -IMG_NAME_KEY = "name_list" -MASK_LIST_KEY = "mask_list" - -def get_matching_xy_from_rigid_registrar(rigid_registrar, ref_img_name=None): - """Get matching keypoints to use in serial non-rigid registration - - Parameters - ---------- - rigid_registrar : SerialRigidRegistrar - SerialRigidRegistrar that has aligned a series of images - - ref_img_name : str, optional - Name of image that will be treated as the center of the stack. - If None, the middle image will be used as the center - - Returns - ------- - from_to_kp_dict : dict of list - Key = image name, value = list of matched and aligned keypoints between - each registered moving image and the registered fixed image. - Each element in the list contains 2 arrays: - - #. Rigid registered xy in moving/current/from image - #. Rigid registered xy in fixed/next/to image - - """ - - img_f_list = [img_obj.full_img_f for img_obj in rigid_registrar.img_obj_list] - ref_img_idx = warp_tools.get_ref_img_idx(img_f_list, ref_img_name) - n_imgs = len(img_f_list) - - from_to_indices = warp_tools.get_alignment_indices(n_imgs, ref_img_idx) - from_to_kp_dict = {} - for idx in from_to_indices: - - moving_obj = rigid_registrar.img_obj_list[idx[0]] - fixed_obj = rigid_registrar.img_obj_list[idx[1]] - - current_match_dict = moving_obj.match_dict[fixed_obj] - moving_kp = current_match_dict.matched_kp1_xy - fixed_kp = current_match_dict.matched_kp2_xy - - assert moving_kp.shape[0] == fixed_kp.shape[0] - - registered_moving = warp_tools.warp_xy(moving_kp, M=moving_obj.M) - registered_fixed = warp_tools.warp_xy(fixed_kp, M=fixed_obj.M) - - from_to_kp_dict[moving_obj.name] = [registered_moving, registered_fixed] - - return from_to_kp_dict - - -def get_imgs_from_dir(src_dir): - """Get images from source directory. - - Parameters - ---------- - src_dir : str - Location of images to be registered. - - Returns - ------- - img_list : list of ndarray - List of images to be registered - - img_f_list : list of str - List of image file names - - img_names : list of str - List of names for each image. Created by removing the extension - - mask_list : list of ndarray - List of masks used for registration - """ - - img_f_list = [f for f in os.listdir(src_dir) if - imghdr.what(os.path.join(src_dir, f)) is not None] - - valtils.sort_nicely(img_f_list) - - img_list = [io.imread(os.path.join(src_dir, f)) for f in img_f_list] - - img_names = [valtils.get_name(f) for f in img_f_list] - - mask_list = [None] * len(img_f_list) - - return img_list, img_f_list, img_names, mask_list - - -def get_imgs_rigid_reg(serial_rigid_reg): - """Get images from SerialRigidRegistrar - - Parameters - ---------- - serial_rigid_reg : SerialRigidRegistrar - SerialRigidRegistrar that has rigidly aligned images - - Returns - ------- - img_list : list of ndarray - List of images to be registered - - img_f_list : list of str - List of image file names - - img_names : list of str - List of names for each image. Created by removing the extension - - mask_list : list of ndarray - List of masks used for registration - - """ - img_list = [None] * serial_rigid_reg.size - img_names = [None] * serial_rigid_reg.size - img_f_list = [None] * serial_rigid_reg.size - mask_list = [None] * serial_rigid_reg.size - - for i, img_obj in enumerate(serial_rigid_reg.img_obj_list): - img_list[i] = img_obj.registered_img - img_names[i] = img_obj.name - img_f_list[i] = img_obj.full_img_f - - # Moving mask - temp_mask = np.full_like(img_obj.image, 255) - img_mask = warp_tools.warp_img(temp_mask, M=img_obj.M, - out_shape_rc=img_obj.registered_img.shape, - interp_method="nearest") - mask_list[i] = img_mask - - return img_list, img_f_list, img_names, mask_list - - -def get_imgs_from_dict(img_dict): - """Get images from source directory. - - Parameters - ---------- - img_dict : dictionary - Dictionary containing the following key : value pairs - - "img_list" : list of images to register - "img_f_list" : list of filenames of each image - "name_list" : list of image names. If not provided, will come from file names - "mask_list" list of masks for each image - - All of the above are optional, except `img_list`. - - Returns - ------- - img_list : list of ndarray - List of images to be registered - - img_f_list : list of str - List of image file names - - img_names : list of str - List of names for each image. Created by removing the extension - - mask_list : list of ndarray - List of masks used for registration - - """ - img_list = img_dict[IMG_LIST_KEY] - - names_provided = IMG_NAME_KEY in img_dict.keys() - files_provided = IMG_F_LIST_KEY in img_dict.keys() - masks_provided = MASK_LIST_KEY in img_dict.keys() - - n_imgs = len(img_list) - if files_provided: - img_f_list = img_dict[IMG_F_LIST_KEY] - else: - img_f_list = [None] * n_imgs - - if names_provided: - img_names = img_dict[IMG_NAME_KEY] - else: - if files_provided: - img_names = [valtils.get_name(f) for f in img_f_list] - else: - img_names = [None] * n_imgs - - if masks_provided: - mask_list = img_dict[MASK_LIST_KEY] - else: - mask_list = [None] * n_imgs - - return img_list, img_f_list, img_names, mask_list - - -class NonRigidZImage(object): - """ Class that store info about an image, including both - rigid and non-rigid registration parameters - - Attributes - ---------- - - image : ndarray - Original, unwarped image with shape (P, Q) - - name : str - Name of image. - - stack_idx : int - Position of image in the stack - - moving_xy : ndarray, optional - (V, 2) array containing points in the moving image that correspond - to those in the fixed image. If these are provided, non_rigid_reg_class - should be a subclass of non_rigid_registrars.NonRigidRegistrarXY - - fixed_xy : ndarray, optional - (V, 2) array containing points in the fixed image that correspond - to those in the moving image - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions from the reference image. - dx = bk_dxdy[0], and dy=bk_dxdy[1]. - Used to warp images - - fwd_dxdy : ndarray - Inversion of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - Used to warp points - - warped_grid : ndarray - Image showing deformation applied to a regular grid. - - """ - - def __init__(self, reg_obj, image, name, stack_idx, moving_xy=None, fixed_xy=None, mask=None): - """ - Parameters - ---------- - - image : ndarray - Original, unwarped image with shape (P, Q) - - name : str - Name of image. - - stack_idx : int - Position of image in the stack - - moving_xy : ndarray, optional - (V, 2) array containing points in the moving image that correspond - to those in the fixed image. If these are provided, non_rigid_reg_class - should be a subclass of non_rigid_registrars.NonRigidRegistrarXY - - fixed_xy : ndarray, optional - (V, 2) array containing points in the fixed image that correspond - to those in the moving image - - mask : ndarray, optional - Mask covering area to be registered. - - """ - self.reg_obj = reg_obj - self.image = image - self.name = name - self.stack_idx = stack_idx - self.moving_xy = moving_xy - self.fixed_xy = fixed_xy - self.registered_img = None - self.warped_grid = None - self.bk_dxdy = None - self.fwd_dxdy = None - - self.is_vips = isinstance(image, pyvips.Image) - self.shape = self.get_shape(image) - - mask_shape = self.get_shape(mask) - if self.is_vips and not self.check_if_vips(mask): - mask = warp_tools.numpy2vips(mask) - - if np.all(mask_shape == self.shape): - mask = warp_tools.resize_img(mask, self.shape) - - self.mask = mask - - def get_shape(self, img): - if isinstance(img, pyvips.Image): - shape = np.array([img.height, img.width]) - else: - shape = img.shape[0:2] - - return shape - - def check_if_vips(self, img): - return isinstance(img, pyvips.Image) - - def mask_img(self, img, mask): - - if isinstance(img, pyvips.Image): - if isinstance(mask, np.ndarray): - vips_mask = warp_tools.numpy2vips(mask) - else: - vips_mask = mask - masked_img = (vips_mask == 0).ifthenelse(0, img) - else: - masked_img = img.copy() - masked_img[mask == 0] = 0 - - return masked_img - - def mask_dxdy(self, dxdy, mask): - if isinstance(dxdy, pyvips.Image): - masked_dxdy = self.mask_img(dxdy, mask) - else: - masked_dxdy = [self.mask_img(dxdy[0], mask), self.mask_img(dxdy[1], mask)] - - return masked_dxdy - - def split_params(self, params, non_rigid_reg_class): - if params is not None: - init_arg_list = inspect.getfullargspec(non_rigid_reg_class.__init__).args - reg_arg_list = inspect.getfullargspec(non_rigid_reg_class.register).args - - init_kwargs = {k:v for k, v in params.items() if k in init_arg_list} - reg_kwargs = {k:v for k, v in params.items() if k in reg_arg_list} - - else: - init_kwargs = {} - reg_kwargs = {} - - return init_kwargs, reg_kwargs - - def calc_deformation(self, registered_fixed_image, non_rigid_reg_class, - bk_dxdy=None, params=None, mask=None): - """ - Finds the non-rigid deformation fields that align this ("moving") image - to the "fixed" image - - Parameters - ---------- - registered_fixed_image : ndarray - Adjacent, aligned image in the stack that this image is being - aligned to. Has shape (P, Q) - - non_rigid_reg_class : NonRigidRegistrar - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images - - bk_dxdy : ndarray, optional - (2, P, Q) numpy array of pixel displacements in - the x and y directions. dx = dxdy[0], and dy=dxdy[1]. - Used to warp the registered_img before finding deformation fields. - - params : dictionary, optional - Keyword: value dictionary of parameters to be used in reigstration. - Passed to the non_rigid_reg_class' init() method. - - In the case where simple ITK will be used, params should be - a SimpleITK.ParameterMap. Note that numeric values needd to be - converted to strings. - - mask : ndarray, optional - 2D array with shape (P,Q) where non-zero pixel values are foreground, - and 0 is background, which is ignnored during registration. If None, - then all non-zero pixels in images will be used to create the mask. - - """ - - if self.reg_obj.from_rigid_reg: - rigid_img_obj = self.reg_obj.src.img_obj_dict[self.name] - M = rigid_img_obj.M - unwarped_shape = rigid_img_obj.image.shape[0:2] - og_reg_shape_rc = rigid_img_obj.registered_shape_rc - - if mask is not None: - if isinstance(mask, pyvips.Image): - reg_mask = warp_tools.vips2numpy(mask) - else: - reg_mask = mask.copy() - else: - reg_mask = None - - if bk_dxdy is not None: - if isinstance(bk_dxdy, list): - bk_dxdy = np.array(bk_dxdy) - - if reg_mask is not None: - for_reg_dxdy = self.mask_dxdy(bk_dxdy, reg_mask) - else: - for_reg_dxdy = bk_dxdy - - if self.reg_obj.from_rigid_reg: - for_reg_dxdy = warp_tools.remove_invasive_displacements(for_reg_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) - - moving_img = warp_tools.warp_img(self.image, bk_dxdy=for_reg_dxdy) - if reg_mask is not None: - # Update mask too - reg_mask = warp_tools.warp_img(reg_mask, bk_dxdy=for_reg_dxdy) - - else: - moving_img = self.image.copy() - for_reg_dxdy = None - if self.is_vips: - bk_dxdy = pyvips.Image.black(self.shape[1], self.shape[0], bands=2) - else: - bk_dxdy = np.array([np.zeros(self.shape[0:2]), np.zeros(self.shape[0:2])]) - - init_kwargs, reg_kwargs = self.split_params(params, non_rigid_reg_class) - - non_rigid_reg = non_rigid_reg_class(params=init_kwargs) - - if self.moving_xy is not None and self.fixed_xy is not None and \ - issubclass(non_rigid_reg_class, non_rigid_registrars.NonRigidRegistrarXY): - if for_reg_dxdy is not None: - # Update positions # - fwd_dxdy = warp_tools.get_inverse_field(for_reg_dxdy) - fixed_xy = warp_tools.warp_xy(self.fixed_xy, M=None, fwd_dxdy=fwd_dxdy) - moving_xy = warp_tools.warp_xy(self.moving_xy, M=None, fwd_dxdy=fwd_dxdy) - else: - fixed_xy = self.fixed_xy - moving_xy = self.moving_xy - else: - fixed_xy = None - moving_xy = None - - xy_args = {"moving_xy": moving_xy, "fixed_xy": fixed_xy} - reg_kwargs.update(xy_args) - - warped_moving, moving_grid_img, moving_bk_dxdy = \ - non_rigid_reg.register(moving_img=moving_img, - fixed_img=registered_fixed_image, - mask=reg_mask, - **reg_kwargs) - - if self.reg_obj.from_rigid_reg: - moving_bk_dxdy = warp_tools.remove_invasive_displacements(moving_bk_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) - - if not self.check_if_vips(moving_bk_dxdy): - if reg_mask is not None: - # Only add new transformations - moving_bk_dxdy = self.mask_dxdy(moving_bk_dxdy, reg_mask) - bk_dxdy_from_ref = np.array([bk_dxdy[0] + moving_bk_dxdy[0], - bk_dxdy[1] + moving_bk_dxdy[1]]) - else: - if reg_mask is not None: - moving_bk_dxdy = self.mask_dxdy(moving_bk_dxdy, reg_mask) - bk_dxdy_from_ref = bk_dxdy + moving_bk_dxdy - - img_bk_dxdy = bk_dxdy_from_ref.copy() - if reg_mask is not None: - img_bk_dxdy = self.mask_dxdy(img_bk_dxdy, reg_mask) - - if self.reg_obj.from_rigid_reg: - img_bk_dxdy = warp_tools.remove_invasive_displacements(img_bk_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) - self.bk_dxdy = img_bk_dxdy - if hasattr(non_rigid_reg, "fwd_dxdy"): - # Already calculated - self.fwd_dxdy = non_rigid_reg.fwd_dxdy - else: - self.fwd_dxdy = warp_tools.get_inverse_field(self.bk_dxdy) - - if not self.is_vips: - # If dxdy is a pyvips.Image, it's likely the displacement is too large to draw - self.warped_grid = viz.color_displacement_grid(*self.bk_dxdy) - - self.registered_img = warp_tools.warp_img(self.image, - bk_dxdy=self.bk_dxdy, - out_shape_rc=self.shape) - - return bk_dxdy_from_ref - - -class SerialNonRigidRegistrar(object): - """Class that performs serial non-rigid registration, based on results SerialRigidRegistrar - - A SerialNonRigidRegistrar finds the deformation fields that will non-rigidly align - a series of images, using the rigid registration parameters found by a - SerialRigidRegistrar object. There are two types of non-rigid registration - methods: - - #. Images are aligned towards a reference image, which may or may not - be at the center of the stack. In this case, the image directly "above" the - reference image is aligned to the reference image, after which the image 2 steps - above the reference image is aligned to the 1st (now aligned) image above - the reference image, and so on. The process is similar when aligning images - "below" the reference image. - - #. All images are aligned simultaneously, and so a reference image is not - # required. An example is the SimpleElastix groupwise registration. - - Similar to SerialRigidRegistrar, SerialNonRigidRegistrar creates a list - and dictionary of NonRigidZImage objects each of which contains information - related to the non-rigid registration, including the original rigid - transformation matrices, and the calculated deformation fields. - - Attributes - ---------- - name : str, optional - Optional name of this SerialNonRigidRegistrar - - from_rigid_reg : bool - Whether or not the images are from a SerialRigidRegistrar - - ref_image_name : str - Name of mage that is being treated as the "center" of the stack. - For example, this may be associated with an H+E image that is - the 2nd image in a stack of 7 images. - - size : int - Number of images to align - - shape : tuple of int - Shape of each image to register. Must be the same for all images - - non_rigid_obj_dict : dict - Dictionary, where each key is the name of a NonRigidZImage, and - the value is the assocatiated NonRigidZImage - - non_rigid_reg_params: dictionary - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - - mask : ndarray - Mask used in non-rigid alignments, with shape (P, Q). - - mask_bbox_xywh : ndarray - Bounding box of `mask` (top left x, top left y, width, height) - - summary : Dataframe - Pandas dataframe containing the median distance between matched - features before and after registration. - - """ - - def __init__(self, src, reference_img_f=None, moving_to_fixed_xy=None, - mask=None, name=None, align_to_reference=False, compose_transforms=True): - """ - Parameters - ---------- - src : SerialRigidRegistrar, str, dict - - A SerialRigidRegistrar object that was used to optimally - align a series of images. - - If a string, it should indicating where the images - to be aligned are located. If src is a string, the images should be - named such that they are read in the correct order, i.e. each - starting with a number. - - If a dictionary, it should contain the following key, value pairs: - - "img_list" : list of images to register - "img_f_list" : list of filenames of each image - "name_list" : list of image names. If not provided, will come from file names - "mask_list" list of masks for each image - - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be returned. - - moving_to_fixed_xy : dict of list, or bool - If `moving_to_fixed_xy` is a dict of list, then - Key = image name, value = list of matched keypoints between - each moving image and the fixed image. - Each element in the list contains 2 arrays: - - #. Rigid registered xy in moving/current/from image - #. Rigid registered xy in fixed/next/to image - - To deterime which pairs of images will be aligned, use - `get_alignment_indices`. Can use `get_imgs_from_dir` - to see the order inwhich the images will be read, which will correspond - to the indices retuned by `get_alignment_indices`. - - If `src` is a SerialRigidRegistrar and `moving_to_fixed_xy` is - True, then the matching features in the SerialRigidRegistrar will - be used. If False, then matching features will not be used. - - mask : ndarray, bool, optional - Mask used for all non-rigid alignments. - - If an ndarray, it must have the same size as the other images. - - If True, then the `overlap_mask` in the SerialRigidRegistrar - will be used. - - If False or None, no mask will be used. - - name : optional - Optional name for this SerialNonRigidRegistrar - - align_to_reference : bool, optional - Whether or not images should be aligned to a reference image - specified by `reference_img_f`. - - img_params : dict, optional - Dictionary of parameters to be used for each particular image. - Useful if images to be registered haven't been processed. - Will be passed to `non_rigid_reg_class` init and register functions. - key = file name, value= dictionary of keyword arguments and values - - """ - - self.src = src - if isinstance(src, serial_rigid.SerialRigidRegistrar): - self.from_rigid_reg = True - elif isinstance(src, str): - self.from_rigid_reg = False - elif isinstance(src, dict): - self.from_rigid_reg = False - else: - valtils.print_warning(f"src must be either a SerialRigidRegistrar, string, or dictionary") - return None - - self.name = name - self.size = 0 - self.shape = None - self.non_rigid_obj_dict = {} - self.non_rigid_obj_list = None - self.non_rigid_reg_params = None - self.summary = None - self.mask = mask - - self.reference_img_f = None - self.ref_img_name = None - self.ref_img_idx = None - self.compose_transforms = compose_transforms - - self.align_to_reference = align_to_reference - self.generate_non_rigid_obj_list(reference_img_f, moving_to_fixed_xy) - - if self.align_to_reference is False and reference_img_f is not None: - og_ref_name = valtils.get_name(reference_img_f) - msg = (f"The reference was specified as {og_ref_name} ", - f"but `align_to_reference` is `False`, and so images will be aligned serially. ", - f"If you would like all images to be directly aligned to {og_ref_name}, " - f"then set `align_to_reference` to `True`") - valtils.print_warning(msg) - - - def get_shape(self, img): - if isinstance(img, pyvips.Image): - shape = np.array([img.height, img.width]) - else: - shape = img.shape[0:2] - - return shape - - def create_mask(self): - temp_mask = np.zeros(self.shape, dtype=np.uint8) - for nr_img_obj in self.non_rigid_obj_list: - temp_mask[nr_img_obj.image > 0] = 255 - - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) - return mask - - def set_mask(self, mask): - """Set mask and get its bounding box - """ - - if mask is not None: - if isinstance(mask, bool) and self.from_rigid_reg: - mask = self.src.overlap_mask - mask = np.clip(mask.astype(int)*255, 0, 255).astype(np.uint8) - - else: - mask = self.create_mask() - - mask_bbox_xywh = warp_tools.xy2bbox(warp_tools.mask2xy(mask)) - self.mask = mask - self.mask_bbox_xywh = mask_bbox_xywh - - def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=None): - """Create non_rigid_obj_list - - """ - - if self.from_rigid_reg: - img_list, img_f_list, img_names, mask_list = \ - get_imgs_rigid_reg(self.src) - else: - if isinstance(self.src, str): - img_list, img_f_list, img_names, mask_list = \ - get_imgs_from_dir(self.src) - # overwrite `src` because all info now in NonRigidZImages - self.src = "dictionary" - - elif isinstance(self.src, dict): - img_list, img_f_list, img_names, mask_list = \ - get_imgs_from_dict(self.src) - - self.size = len(img_list) - self.shape = self.get_shape(img_list[0]) - - if reference_img_f is not None: - reference_name = valtils.get_name(reference_img_f) - else: - reference_name = None - - ref_img_idx = warp_tools.get_ref_img_idx(img_f_list, reference_name) - - if reference_img_f is None: - reference_img_f = img_f_list[ref_img_idx] - - self.reference_img_f = reference_img_f - self.ref_img_idx = ref_img_idx - self.ref_img_name = reference_name - - if self.from_rigid_reg and isinstance(moving_to_fixed_xy, bool): - if moving_to_fixed_xy: - moving_to_fixed_xy = \ - get_matching_xy_from_rigid_registrar(self.src, reference_name) - else: - moving_to_fixed_xy = None - - self.non_rigid_obj_list = [None] * self.size - for i, img in enumerate(img_list): - img_shape = self.get_shape(img) - - assert np.all(img_shape == self.shape), \ - valtils.print_warning("Images must all have the shape") - - img_name = img_names[i] - mask = mask_list[i] - - moving_xy = None - fixed_xy = None - if moving_to_fixed_xy is not None and img_name != reference_img_f: - if isinstance(moving_to_fixed_xy, dict): - xy_coords = moving_to_fixed_xy[img_name] - moving_xy = xy_coords[0] - fixed_xy = xy_coords[1] - else: - msg = "moving_to_fixed_xy is not a dictionary. Will be ignored" - valtils.print_warning(msg) - - nr_obj = NonRigidZImage(self, img, img_name, stack_idx=i, - moving_xy=moving_xy, - fixed_xy=fixed_xy, - mask=mask) - - if i == ref_img_idx: - # Set reference image attributes # - zero_displacement = np.zeros(self.shape) - if not nr_obj.is_vips: - nr_obj.bk_dxdy = [zero_displacement, zero_displacement] - nr_obj.fwd_dxdy = [zero_displacement, zero_displacement] - nr_obj.warped_grid = viz.color_displacement_grid(*nr_obj.bk_dxdy) - else: - nr_obj.bk_dxdy = pyvips.Image.black(nr_obj.shape[1], nr_obj.shape[0], bands=2) - nr_obj.fwd_dxdy = pyvips.Image.black(nr_obj.shape[1], nr_obj.shape[0], bands=2) - - nr_obj.registered_img = img.copy() - - self.non_rigid_obj_list[i] = nr_obj - - def update_img_params(self, non_rigid_reg_params=None, img_params=None, name=None): - - if img_params is not None and name is not None: - if len(img_params) == 0: - indv_img_params = None - else: - indv_img_params = img_params[name] - else: - indv_img_params = img_params - - if non_rigid_reg_params is not None and indv_img_params is not None: - - updated_params = indv_img_params.copy() - updated_params[non_rigid_registrars.NR_PARAMS_KEY] = non_rigid_reg_params - - elif non_rigid_reg_params is not None and indv_img_params is None: - updated_params = non_rigid_reg_params - - elif non_rigid_reg_params is None and indv_img_params is not None: - updated_params = indv_img_params - - else: - updated_params = None - - return updated_params - - - def register_serial(self, non_rigid_reg_class, non_rigid_reg_params=None, img_params=None): - """Non-rigidly align images in serial - Parameters - ---------- - non_rigid_reg_class : NonRigidRegistrar - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images - - non_rigid_reg_params: dictionary, optional - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - - """ - current_dxdy = None - self.non_rigid_reg_params = non_rigid_reg_params - iter_order = warp_tools.get_alignment_indices(self.size, self.ref_img_idx) - for moving_idx, fixed_idx in tqdm(iter_order): - moving_obj = self.non_rigid_obj_list[moving_idx] - fixed_obj = self.non_rigid_obj_list[fixed_idx] - - if self.compose_transforms: - if fixed_obj.stack_idx == self.ref_img_idx: - current_dxdy = None - else: - current_dxdy = updated_dxdy - - if moving_obj.mask is not None: - if self.mask is not None: - reg_mask = preprocessing.combine_masks(self.mask, moving_obj.mask, op="and") - else: - reg_mask = moving_obj.mask - - elif self.mask is not None: - reg_mask = self.mask - - else: - reg_mask is None - - nr_reg_params = self.update_img_params(non_rigid_reg_params, img_params, moving_obj.name) - updated_dxdy = moving_obj.calc_deformation(registered_fixed_image=fixed_obj.registered_img, - non_rigid_reg_class=non_rigid_reg_class, - bk_dxdy=current_dxdy, - params=nr_reg_params, - mask=reg_mask - ) - - - def register_to_ref(self, non_rigid_reg_class, non_rigid_reg_params=None, img_params=None): - """Non-rigidly align images to a reference image - Parameters - ---------- - non_rigid_reg_class : NonRigidRegistrar - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images - - non_rigid_reg_params: dictionary, optional - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - - """ - self.non_rigid_reg_params = non_rigid_reg_params - ref_nr_obj = self.non_rigid_obj_list[self.ref_img_idx] - ref_img = ref_nr_obj.image - for moving_idx in tqdm(range(self.size)): - moving_obj = self.non_rigid_obj_list[moving_idx] - if moving_obj.stack_idx == self.ref_img_idx: - continue - - overlap_mask = None - - nr_reg_params = self.update_img_params(non_rigid_reg_params, img_params, moving_obj.name) - - moving_obj.calc_deformation(ref_img, - non_rigid_reg_class, - params=nr_reg_params, - mask=overlap_mask) - - def register_groupwise(self, non_rigid_reg_class, non_rigid_reg_params=None): - """Non-rigidly align images as a group - - Parameters - ---------- - non_rigid_reg_class : NonRigidRegistrarGroupwise - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images - - non_rigid_reg_params: dictionary, optional - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - - """ - - img_list = [nr_img_obj.image for nr_img_obj in self.non_rigid_obj_list] - non_rigid_reg = non_rigid_reg_class(params=non_rigid_reg_params) - - print("\n======== Registering images (non-rigid)\n") - warped_imgs, warped_grids, backward_deformations = non_rigid_reg.register(img_list, self.mask) - for i, nr_img_obj in enumerate(self.non_rigid_obj_list): - nr_img_obj.registered_img = warped_imgs[i] - nr_img_obj.bk_dxdy = backward_deformations[i] - nr_img_obj.warped_grid = viz.color_displacement_grid(*nr_img_obj.bk_dxdy) - nr_img_obj.fwd_dxdy = warp_tools.get_inverse_field(nr_img_obj.bk_dxdy) - - def register(self, non_rigid_reg_class, non_rigid_reg_params, img_params=None): - """Non-rigidly align images, either as a group or serially - - Images will be registered serially if `non_rigid_reg_class` is a - subclass of NonRigidRegistrarGroupwise, then groupwise registration - will be conductedd. If `non_rigid_reg_class` is a subclass of - NonRigidRegistrar then images will be aligned serially. - - Parameters - ---------- - non_rigid_reg_class : NonRigidRegistrar, NonRigidRegistrarGroupwise - Uninstantiated NonRigidRegistrar or NonRigidRegistrarGroupwise class - that will be used to calculate the deformation fields between images - - non_rigid_reg_params: dictionary, optional - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - img_params : dict, optional - Dictionary of parameters to be used for each particular image. - Useful if images to be registered haven't been processed. - Will be passed to `non_rigid_reg_class` init and register functions. - key = file name, value= dictionary of keyword arguments and values - - """ - - if img_params is not None: - named_img_params = {valtils.get_name(k):v for k, v in img_params.items()} - else: - named_img_params = None - - if issubclass(non_rigid_reg_class, non_rigid_registrars.NonRigidRegistrarGroupwise): - self.register_groupwise(non_rigid_reg_class, non_rigid_reg_params) - elif self.align_to_reference: - self.register_to_ref(non_rigid_reg_class, non_rigid_reg_params, img_params=named_img_params) - else: - self.register_serial(non_rigid_reg_class, non_rigid_reg_params, img_params=named_img_params) - - self.non_rigid_obj_dict = {img_obj.name: img_obj for img_obj - in self.non_rigid_obj_list} - - def summarize(self): - """Summarize alignment error - - Returns - ------- - summary_df: Dataframe - Pandas dataframe containin the registration error of the - alignment between each image and the previous one in the stack. - - """ - - src_img_names = [None] * self.size - dst_img_names = [None] * self.size - shape_list = [None] * self.size - - og_med_d_list = [None] * self.size - og_tre_list = [None] * self.size - med_d_list = [None] * self.size - tre_list = [None] * self.size - - src_img_names[self.ref_img_idx] = self.ref_img_name - shape_list[self.ref_img_idx] = self.non_rigid_obj_list[self.ref_img_idx].image.shape - - iter_order = warp_tools.get_alignment_indices(self.size, self.ref_img_idx) - print("\n======== Summarizing registration\n") - for moving_idx, fixed_idx in tqdm(iter_order): - moving_obj = self.non_rigid_obj_list[moving_idx] - fixed_obj = self.non_rigid_obj_list[fixed_idx] - src_img_names[moving_idx] = moving_obj.name - dst_img_names[moving_idx] = fixed_obj.name - shape_list[moving_idx] = moving_obj.image.shape - - og_tre_list[moving_idx], og_med_d_list[moving_idx] = \ - warp_tools.measure_error(moving_obj.moving_xy, - moving_obj.fixed_xy, - moving_obj.image.shape) - - warped_moving_xy = warp_tools.warp_xy(moving_obj.moving_xy, - M=None, - fwd_dxdy=moving_obj.fwd_dxdy) - - warped_fixed_xy = warp_tools.warp_xy(moving_obj.fixed_xy, - M=None, - fwd_dxdy=moving_obj.fwd_dxdy) - - tre_list[moving_idx], med_d_list[moving_idx] = \ - warp_tools.measure_error(warped_moving_xy, - warped_fixed_xy, - moving_obj.image.shape) - - summary_df = pd.DataFrame({ - "from": src_img_names, - "to": dst_img_names, - "original_D": og_med_d_list, - "D": med_d_list, - "original_TRE": og_tre_list, - "TRE": tre_list, - "shape": shape_list, - }) - to_summarize_idx = [i for i in range(self.size) if i != self.ref_img_idx] - summary_df["series_d"] = warp_tools.calc_total_error(np.array(med_d_list)[to_summarize_idx]) - summary_df["series_tre"] = warp_tools.calc_total_error(np.array(tre_list)[to_summarize_idx]) - summary_df["name"] = self.name - - self.summary_df = summary_df - - return summary_df - - -def register_images(src, non_rigid_reg_class=non_rigid_registrars.OpticalFlowWarper, - non_rigid_reg_params=None, dst_dir=None, - reference_img_f=None, moving_to_fixed_xy=None, - mask=None, name=None, align_to_reference=False, - img_params=None, compose_transforms=True, qt_emitter=None): - """ - Parameters - ---------- - src : SerialRigidRegistrar, str - Either a SerialRigidRegistrar object that was used to optimally - align a series of images, or a string indicating where the images - to be aligned are located. If src is a string, the images should be - named such that they are read in the correct order, i.e. each - starting with a number. - - non_rigid_reg_class : NonRigidRegistrar - Uninstantiated NonRigidRegistrar class that will be used to - calculate the deformation fields between images. - By default this is an OpticalFlowWarper that uses the OpenCV - implementation of DeepFlow. - - non_rigid_reg_params: dictionary, optional - Dictionary containing parameters {name: value} to be used to initialize - the NonRigidRegistrar. - In the case where simple ITK is used by the, params should be - a SimpleITK.ParameterMap. Note that numeric values nedd to be - converted to strings. - - dst_dir : str, optional - Top directory where aliged images should be save. SerialNonRigidRegistrar will - be in this folder, and aligned images in the "registered_images" - sub-directory. If None, the images will not be written to file - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be returned. - - moving_to_fixed_xy : dict of list, or bool - If `moving_to_fixed_xy` is a dict of list, then - Key = image name, value = list of matched keypoints between - each moving image and the fixed image. - Each element in the list contains 2 arrays: - - #. Rigid registered xy in moving/current/from image - #. Rigid registered xy in fixed/next/to image - - To deterime which pairs of images will be aligned, use - `warp_tools.get_alignment_indices`. Can use `get_imgs_from_dir` - to see the order inwhich the images will be read, which will correspond - to the indices retuned by `warp_tools.get_alignment_indices`. - - If `src` is a SerialRigidRegistrar and `moving_to_fixed_xy` is - True, then the matching features in the SerialRigidRegistrar will - be used. If False, then matching features will not be used. - - mask : ndarray, bool, optional - Mask used in non-rigid alignments. - - If an ndarray, it must have the same size as the other images. - - If True, then the `overlap_mask` in the SerialRigidRegistrar - will be used. - - If False or None, no mask will be used. - - name : optional - Optional name for this SerialNonRigidRegistrar - - align_to_reference : bool, optional - Whether or not images should be aligne to a reference image - specified by `reference_img_f`. Will be set to True if - `reference_img_f` is provided. - - img_params : dict, optional - Dictionary of parameters to be used for each particular image. - Useful if images to be registered haven't been processed. - Will be passed to `non_rigid_reg_class` init and register functions. - key = file name, value= dictionary of keyword arguments and values - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - Returns - ------- - nr_reg : SerialNonRigidRegistrar - SerialNonRigidRegistrar that has registeredt the images in `src` - """ - - tic = time() - nr_reg = SerialNonRigidRegistrar(src=src, reference_img_f=reference_img_f, - moving_to_fixed_xy=moving_to_fixed_xy, - mask=mask, name=name, - align_to_reference=align_to_reference, - compose_transforms=compose_transforms) - - nr_reg.register(non_rigid_reg_class, non_rigid_reg_params, img_params=img_params) - - if dst_dir is not None: - registered_img_dir = os.path.join(dst_dir, "non_rigid_registered_images") - registered_data_dir = os.path.join(dst_dir, "data") - registered_grids_dir = os.path.join(dst_dir, "deformation_grids") - for d in [registered_img_dir, registered_data_dir, registered_grids_dir]: - pathlib.Path(d).mkdir(exist_ok=True, parents=True) - - print("\n======== Saving results\n") - if moving_to_fixed_xy is not None: - summary_df = nr_reg.summarize() - summary_file = os.path.join(registered_data_dir, name + "_results.csv") - summary_df.to_csv(summary_file, index=False) - - pickle_file = os.path.join(registered_data_dir, name + "_non_rigid_registrar.pickle") - pickle.dump(nr_reg, open(pickle_file, 'wb')) - - for img_obj in nr_reg.non_rigid_obj_list: - f_out = f"{img_obj.name}.png" - - io.imsave(os.path.join(registered_img_dir, f_out), - img_obj.registered_img.astype(np.uint8)) - - colord_tri_grid = viz.color_displacement_tri_grid(img_obj.bk_dxdy[0], - img_obj.bk_dxdy[1]) - - io.imsave(os.path.join(registered_grids_dir, f_out), colord_tri_grid) - - toc = time() - elapsed = toc - tic - time_string, time_units = valtils.get_elapsed_time_string(elapsed) - print(f"\n======== Non-rigid registration complete in {time_string} {time_units}\n") - - return nr_reg diff --git a/examples/acrobat_2023/valis/serial_rigid.py b/examples/acrobat_2023/valis/serial_rigid.py deleted file mode 100644 index 1dae2b00..00000000 --- a/examples/acrobat_2023/valis/serial_rigid.py +++ /dev/null @@ -1,1622 +0,0 @@ -"""Classes and functions to perform serial rigid registration of a set of images - -""" -import numpy as np -import os -import pickle -from fastcluster import linkage -from scipy.spatial.distance import squareform -from scipy.cluster.hierarchy import optimal_leaf_ordering, leaves_list -from skimage import transform, io -from skimage.transform import EuclideanTransform -import pandas as pd -import warnings -import imghdr -from tqdm import tqdm -import pathlib -import multiprocessing -from joblib import Parallel, delayed, parallel_backend -from time import time -import inspect -from . import valtils -from . import warp_tools -from .feature_detectors import VggFD -from .feature_matcher import Matcher, convert_distance_to_similarity, GMS_NAME - -def get_image_files(img_dir, imgs_ordered=False): - """Get images filenames in img_dir - - If imgs_ordered is True, then this ensures the returned list is sorted - properly. Otherwise, the list is sorted lexicographicly. - - Parameters - ---------- - img_dir : str - Path to directory containing the images. - - imgs_ordered: bool, optinal - Whether or not the order of images already known. If True, the file - names should start with ascending numbers, with the first image file - having the smallest number, and the last image file having the largest - number. If False (the default), the order of images will be determined - by ordering a distance matrix. - - Returns - ------- - If `imgs_ordered` is True, then this ensures the returned list is sorted - properly. Otherwise, the list is sorted lexicographicly. - - """ - - img_list = [f for f in os.listdir(img_dir) if - imghdr.what(os.path.join(img_dir, f)) is not None] - - if imgs_ordered: - valtils.sort_nicely(img_list) - else: - img_list.sort() - - return img_list - - -def get_max_image_dimensions(img_list): - """Find the maximum width and height of all images - - Parameters - ---------- - img_list : list - List of images - - Returns - ------- - max_wh : tuple - Maximum width and height of all images - - """ - - shapes = [img.shape[0:2] for img in img_list] - all_w, all_h = list(zip(*shapes)) - max_wh = (max(all_w), max(all_h)) - - return max_wh - - -def order_Dmat(D): - """ Cluster distance matrix and sort - - Leaf sorting is accomplished using optimal leaf ordering (Bar-Joseph 2001) - - Parmaters - --------- - D: ndarray - (N, N) Symmetric distance matrix for N samples - - Returns - ------- - sorted_D :ndarray - (N, N) array Distance matrix sorted using optimal leaf ordering - - ordered_leaves : ndarray - (1, N) array containing the leaves of dendrogram found during - hierarchical clustering - - optimal_Z : ndarray - ordered linkage matrix - - """ - - D = D.copy() - sq_D = squareform(D) - Z = linkage(sq_D, 'single', preserve_input=True) - - optimal_Z = optimal_leaf_ordering(Z, sq_D) - ordered_leaves = leaves_list(optimal_Z) - - sorted_D = D[ordered_leaves, :] - sorted_D = sorted_D[:, ordered_leaves] - - return sorted_D, ordered_leaves, optimal_Z - - -class ZImage(object): - """Class store info about an image, including the rigid registration parameters - - Attributes - ---------- - image : ndarray - Greyscale image that will be used for feature detection. This images - should be greyscale and may need to have undergone preprocessing to - make them look as similar as possible. - - full_img_f : str - full path to the image - - img_id : int - ID of the image, based on its ordering `processed_src_dir` - - name : str - Name of the image. Usually `img_f` but with the extension removed. - - desc : ndarray - (N, M) array of N desciptors for each keypoint, each of which has - M features - - kp_pos_xy : ndarray - (N, 2) array of position for each keypoint - - match_dict : dict - Dictionary of image matches. Key= img_obj this ZImage is being - compared to, value= MatchInfo containing information about the - comparison, such as the position of matches, features for each match, - number of matches, etc... The MatchInfo objects in this dictionary - contain only the info for matches that were considered "good". - - unfiltered_match_dict : dict - Dictionary of image matches. Key= img_obj this ZImage is being - compared to, value= MatchInfo containing inoformation about the - comparison, such as the position of matches, features for each match, - number of matches, etc... The MatchInfo objects in this dictionary - contain info for all matches that were cross-checked. - - stack_idx : int - Position of image in sorted Z-stack - - fixed_obj : ZImage - ZImage to which this ZImage was aligned, i.e. this is the "moving" - image, and `fixed_obj` is the "fixed" image. This is set during - the `align_to_prev` method of the SerialRigidRegistrar. The - `fixed_obj` will either be immediately above or immediately - below this ZImage in the image stack. - - reflection_M : ndarray - Transformation to reflect the image in the x and/or y axis, before padding. - Will be the first transformation performed - - T : ndarray - Transformation matrix that translates the image such that it is in a - padded image that has the same shape as all other images - - to_prev_A : ndarray - Transformation matrix that warps image to align with the previous image - - optimal_M : ndarray - Transformation matrix found by minimizing a cost function. - Used as final optional step to refine alignment - - crop_T : ndarray - Transformation matrix used to crop image after registration - - M : ndarray - Final transformation matrix that aligns image in the Z-stack. - - M_inv : ndarray - Inverse of final transformation matrix that aligns image in - the Z-stack. - - registered_img : ndarray - image after being warped - - padded_shape_rc : tuple - Shape of padded image. All other images will have this shape - - registered_shape_rc = tuple: - Shape of aligned image. All other aligned images will have this shape - - """ - - def __init__(self, image, img_f, img_id, name): - """Class that stores information about an image - - Parameters - ---------- - image : ndarray - Greyscale image that will be used for feature detection. This - images should be single channel uint8 images, and may need to - have undergone preprocessing and/or normalization to make them - look as similar as possible. - - img_f : str - full path to `image` - - img_id : int - ID of the image, based on its ordering in the image source directory - - name : str - Name of the image. Usually img_f but with the extension removed. - - """ - - self.image = image - self.full_img_f = img_f - self.id = img_id - self.name = name - - self.desc = None - self.kp_pos_xy = None - self.match_dict = {} - self.unfiltered_match_dict = {} - self.stack_idx = None - self.fixed_obj = None - - self.padded_shape_rc = None - self.reflection_M = np.identity(3) - self.T = np.identity(3) - self.to_prev_A = np.identity(3) - self.optimal_M = np.identity(3) - self.crop_T = np.identity(3) - self.M = np.identity(3) - self.M_inv = np.identity(3) - self.registered_img = None - self.padded_shape_rc = None - self.registered_shape_rc = None - - def reduce(self, prev_img_obj, next_img_obj): - """Reduce amount of info stored, which can take up a lot of space. - - No longer need all descriptors. Only keep match info for neighgbors - - Parameters - ---------- - prev_img_obj : Zimage - Zimage below this Zimage - - next_img_obj : Zimage - Zimage above this Zimage - - """ - - self.desc = None - for img_obj in self.match_dict.keys(): - if prev_img_obj is not None and next_img_obj is not None: - if prev_img_obj != img_obj and img_obj != next_img_obj: - # In middle of stack - self.match_dict[img_obj] = None - - elif prev_img_obj is None and img_obj != next_img_obj: - # First image doesn't have a previous neighbor - self.match_dict[img_obj] = None - - elif prev_img_obj != img_obj and next_img_obj is None: - # Last image doesn't have a next neighbor - self.match_dict[img_obj] = None - - -class SerialRigidRegistrar(object): - """Class that performs serial rigid registration - - Registration is conducted by first detecting features in all images. - Features are then matched between images, which are then used to construct - a distance matrix, D. D is then sorted such that the most similar images - are adjcent to one another. The rigid transformation matrics are then found to - align each image with the previous image. Optionally, optimization can be - performed to improve the alignments, although the "optimized" matrix will be - discarded if it increases the distances between matched features. - - SerialRigidRegistrar creates a list and dictionary of ZImage objects, - each of which contains information related to feature matching and - the rigid registration matrices. - - Attributes - ---------- - img_dir : str - Path to directory containing the images that will be registered. - The images in this folder should be single channel uint8 images. - For the best registration results, they have undergone some sort - of pre-processing and normalization. The preprocessing module - contains methods for this, but the user may want/need to use other - methods. - - aleady_sorted: bool, optional - Whether or not the order of images already known. If True, the file - names should start with ascending numbers, with the first image file - having the smallest number, and the last image file having the largest - number. If False (the default), the order of images will be determined - by ordering a distance matrix. - - name : str - Descriptive name of registrar, such as the sample's name - - img_file_list : list - List of full paths to single channel uint8 images - - size : int - Number of images to align - - distance_metric_name : str - Name of distance metric used to determine the dis/similarity between - each pair of images - - distance_metric_type : str - Name of the type of metric used to determine the dis/similarity - between each pair of images. Despite the name, it could be "similarity" - if the Matcher object compares image feautres using a similarity - metric. In that case, similarities are converted to distances. - - img_obj_list : list - List of ZImage objects. Initially unordered, but - eventually be sorted - - img_obj_dict : dict - Dictionary of ZImage objects. Created to conveniently - access ZIimages. Key = ZImage.name, value= ZImage - - optimal_Z :ndarray - Ordered linkage matrix for `distance_mat` - - unsorted_distance_mat : ndarray - Distance matrix with shape (N, N), where each element is the - disimilariy betweewn each pair of the N images. The order of - rows and columns reflects the order in which the images were read. - This matrix is used to order the images the Z-stack. - - distance_mat : ndarray - `unsorted_distance_mat` reorderd such that the most similar images - are adjacent to one another - - unsorted_similarity_mat : ndarray - Similar to `unsorted_distance_mat`, except the elements are - image similarity - - similarity_mat : ndarray - Similar to `distance_mat`, except the elements are image similarity - - features : str - Name of feature detector and descriptor used - - transform_type : str - Name of scikit-image transformer class that was used - - reference_img_f : str - Filename of image that will be treated as the center of the stack. - - reference_img_idx : int - Index of ZImage that corresponds to `reference_img_f`, after - the `img_obj_list` has been sorted. - - align_to_reference : bool, optional - Whether or not images should be aligne to a reference image - specified by `reference_img_f`. Will be set to True if - `reference_img_f` is provided. - - iter_order : list of tuples - Each element of `iter_order` contains a tuple of stack - indices. The first value is the index of the moving/current/from - image, while the second value is the index of the moving/next/to - image. - - summary_df : Dataframe - Pandas dataframe containin the registration error of the - alignment between each image and the previous one in the stack. - - """ - - def __init__(self, img_dir, imgs_ordered=False, reference_img_f=None, - name=None, align_to_reference=False): - """Class that performs serial rigid registration - - Parameters - ---------- - img_dir : str - Path to directory containing the images that will be registered. - The images in this folder should be single channel uint8 images. - For the best registration results, they have undergone some sort - of pre-processing and normalization. The preprocessing module - contains methods for this, but the user may want/need to use other - methods. - - imgs_ordered : bool - Whether or not the order of images already known. If True, the file - names should start with ascending numbers, with the first image - file having the smallest number, and the last image file having - the largest number. If False (the default), the order of images - will be determined by sorting a distance matrix. - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be the reference. - - name : str, optional - Descriptive name of registrar, such as the sample's name - - align_to_reference : bool, optional - Whether or not images should be aligne to a reference image - specified by `reference_img_f`. Will be set to True if - `reference_img_f` is provided. - - """ - self.img_dir = img_dir - self.aleady_sorted = imgs_ordered - self.name = name - self.img_file_list = get_image_files(img_dir, imgs_ordered=imgs_ordered) - self.size = len(self.img_file_list) - self.distance_metric_name = None - self.distance_metric_type = None - self.img_obj_list = None - self.img_obj_dict = {} - self.optimal_z = None - self.unsorted_distance_mat = None - self.distance_mat = None - self.unsorted_similarity_mat = None - self.similarity_mat = None - self.features = None - self.transform_type = None - - self.reference_img_f = reference_img_f - self.reference_img_idx = 0 - self.align_to_reference = align_to_reference - self.iter_order = None - - self.summary = None - - if self.align_to_reference is False and reference_img_f is not None: - og_ref_name = valtils.get_name(reference_img_f) - msg = (f"The reference was specified as {og_ref_name} ", - f"but `align_to_reference` is `False`, and so images will be aligned serially. ", - f"If you would like all images to be directly aligned to {og_ref_name}, " - f"then set `align_to_reference` to `True`") - valtils.print_warning(msg) - - - def generate_img_obj_list(self, feature_detector, qt_emitter=None): - """Create a list of ZImage objects - - Create a list of ZImage objects, each of which represents an image. - This function also determines the maximum size of the images so that - there is no cropping during warping. Finally, the features of each - image are detected using the feature_detector - - Parameters - ---------- - feature_detector : FeatureDD - FeatureDD object that detects and computes image features. - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - - # NOTE tried parallelizing with joblib, but it's actually slower # - sorted_img_list = [io.imread(os.path.join(self.img_dir, f), True) - for f in self.img_file_list] - - out_w, out_h = get_max_image_dimensions(sorted_img_list) - - # Get dimensions if images were rotated 45 degrees or 90 degrees - max_new_w = out_w*np.cos(45) + out_h*np.sin(45) - max_new_h = out_w*np.sin(45) + out_h*np.cos(45) - - max_dist = np.ceil(np.max([out_w, out_h, max_new_h, max_new_w])).astype(int) - out_shape = (max_dist, max_dist) - img_obj_list = [None] * self.size - - for i in tqdm(range(self.size)): - img_f = self.img_file_list[i] - img = sorted_img_list[i] - - img_name = valtils.get_name(img_f) - img_obj = ZImage(img, os.path.join(self.img_dir, img_f), i, name=img_name) - img_obj.padded_shape_rc = out_shape - img_obj.T = warp_tools.get_padding_matrix(img.shape, img_obj.padded_shape_rc) - img_obj.kp_pos_xy, img_obj.desc = feature_detector.detect_and_compute(img) - img_obj_list[i] = img_obj - self.img_obj_dict[img_name] = img_obj - if qt_emitter is not None: - qt_emitter.emit(1) - - self.img_obj_list = img_obj_list - self.features = feature_detector.__class__.__name__ - - def match_sorted_imgs(self, matcher_obj, keep_unfiltered=False, qt_emitter=None): - """Conduct feature matching between images that have already been sorted. - - Results will be stored in each ZImage's match_dict - - Parameters - ---------- - matcher_obj : Matcher - Object to match features between images. - - keep_unfiltered : bool - Whether or not matcher_obj should store unfiltered matches - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - - def match_adj_img_obj(i): - if i == 0: - return None - img_obj_1 = self.img_obj_list[i] - img_obj_2 = self.img_obj_list[i-1] - - if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":img_obj_1.image.shape[0:2], "img2_shape": img_obj_2.image.shape[0:2]} - else: - filter_kwargs = None - - unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=img_obj_1.image, desc1=img_obj_1.desc, kp1_xy=img_obj_1.kp_pos_xy, - img2=img_obj_2.image, desc2=img_obj_2.desc, kp2_xy=img_obj_2.kp_pos_xy, - additional_filtering_kwargs=filter_kwargs) - - # unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - # matcher_obj.match_images(img_obj_1.desc, img_obj_1.kp_pos_xy, - # img_obj_2.desc, img_obj_2.kp_pos_xy, - # filter_kwargs) - if len(filtered_match_info12.matched_kp1_xy) == 0: - warnings.warn(f"{len(filtered_match_info12.matched_kp1_xy)} between {img_obj_1.name} and {img_obj_2.name}") - - # Update match dictionaries - if keep_unfiltered: - unfiltered_match_info12.set_names(img_obj_1.name, img_obj_2.name) - img_obj_1.unfiltered_match_dict[img_obj_2] = unfiltered_match_info12 - - unfiltered_match_info21.set_names(img_obj_2.name, img_obj_1.name) - img_obj_2.unfiltered_match_dict[img_obj_1] = unfiltered_match_info21 - - filtered_match_info12.set_names(img_obj_1.name, img_obj_2.name) - img_obj_1.match_dict[img_obj_2] = filtered_match_info12 - - filtered_match_info21.set_names(img_obj_2.name, img_obj_1.name) - img_obj_2.match_dict[img_obj_1] = filtered_match_info21 - - if qt_emitter is not None: - qt_emitter.emit(1) - - n_cpu = valtils.get_ncpus_available() - 1 - with parallel_backend("threading", n_jobs=n_cpu): - Parallel()(delayed(match_adj_img_obj)(i) for i in range(self.size)) - - def match_imgs(self, matcher_obj, keep_unfiltered=False, qt_emitter=None): - """Conduct feature matching between all pairs of images. - - Results will be stored in each ZImage's match_dict - - Parameters - ---------- - matcher_obj : Matcher - Object to match features between images. - - keep_unfiltered : bool - Whether or not matcher_obj should store unfiltered matches - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - - n_comparisions = int((self.size*(self.size-1))/2) - pbar = tqdm(total=n_comparisions) - - def match_img_obj(i): - - img_obj_1 = self.img_obj_list[i] - for j in np.arange(i+1, self.size): - img_obj_2 = self.img_obj_list[j] - if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":img_obj_1.image.shape[0:2], "img2_shape": img_obj_2.image.shape[0:2]} - else: - filter_kwargs = None - - unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=img_obj_1.image, desc1=img_obj_1.desc, kp1_xy=img_obj_1.kp_pos_xy, - img2=img_obj_2.image, desc2=img_obj_2.desc, kp2_xy=img_obj_2.kp_pos_xy, - additional_filtering_kwargs=filter_kwargs) - - # unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - # matcher_obj.match_images(img_obj_1.desc, img_obj_1.kp_pos_xy, - # img_obj_2.desc, img_obj_2.kp_pos_xy, - # filter_kwargs) - - if len(filtered_match_info12.matched_kp1_xy) == 0: - warnings.warn(f"{len(filtered_match_info12.matched_kp1_xy)} between {img_obj_1.name} and {img_obj_2.name}") - # Update match dictionaries # - if keep_unfiltered: - unfiltered_match_info12.set_names(img_obj_1.name, img_obj_2.name) - img_obj_1.unfiltered_match_dict[img_obj_2] = unfiltered_match_info12 - - unfiltered_match_info21.set_names(img_obj_2.name, img_obj_1.name) - img_obj_2.unfiltered_match_dict[img_obj_1] = unfiltered_match_info21 - - filtered_match_info12.set_names(img_obj_1.name, img_obj_2.name) - img_obj_1.match_dict[img_obj_2] = filtered_match_info12 - - filtered_match_info21.set_names(img_obj_2.name, img_obj_1.name) - img_obj_2.match_dict[img_obj_1] = filtered_match_info21 - - pbar.update(1) - if qt_emitter is not None: - qt_emitter.emit(1) - - n_cpu = valtils.get_ncpus_available() - 1 - with parallel_backend("threading", n_jobs=n_cpu): - Parallel()(delayed(match_img_obj)(i) for i in range(self.size)) - - def get_neighbor_matches_idx(self, img_obj, prev_img_obj, next_img_obj): - """Get indices of features found in both neighbors - - Returns - ------- - nf_prev_idx - - nf_next_idx - - - """ - - xy_to_prev = img_obj.match_dict[prev_img_obj].matched_kp1_xy - xy_to_next = img_obj.match_dict[next_img_obj].matched_kp1_xy - - xy_to_prev_idx = warp_tools.index2d_to_1d(xy_to_prev[:, 1], xy_to_prev[:, 0], img_obj.image.shape[1]) - xy_to_next_idx = warp_tools.index2d_to_1d(xy_to_next[:, 1], xy_to_next[:, 0], img_obj.image.shape[1]) - - shared_pts, nf_prev_idx, nf_next_idx = np.intersect1d(xy_to_prev_idx, xy_to_next_idx, return_indices=True) - - # trying to remove diff features if they are different... (possible due to some very rare rounding errors?) - diff = np.where(xy_to_prev[nf_prev_idx, :] != xy_to_next[nf_next_idx, :]) - if diff[0].any(): - diff = list(np.unique(diff[0])) - nf_prev_idx = np.delete(nf_prev_idx, diff) - nf_next_idx = np.delete(nf_next_idx, diff) - - return nf_prev_idx, nf_next_idx - - - def get_common_desc(self, current_img_obj, neighbor_obj, nf_kp_idx): - """Get descriptors that correspond to filtered neighbor points - Parameters - ---------- - nf_kp_idx : ndarray - Indicies of already matched keypoints that were found after - neighbonr filtering - """ - - neighbor_match_info12 = current_img_obj.match_dict[neighbor_obj] - nf_kp = neighbor_match_info12.matched_kp1_xy[nf_kp_idx] - nf_desc = neighbor_match_info12.matched_desc1[nf_kp_idx] - - return nf_desc, nf_kp - - def neighbor_match_filtering(self, img_obj, prev_img_obj, next_img_obj, - tform, matcher_obj): - """Remove poor matches by keeping only the matches found in neighbors - - Parameters - ---------- - img_obj : ZImage - current ZImage - - prev_img_obj : ZImage - ZImage to below `img_obj` - - next_img_obj : ZImage - ZImage to above `img_obj` - - tform : skimage.transform object - The scikit-image transform object that estimates the - parameter matrix - - matcher_obj : Matcher - Object to match features between images. - - Returns - ------- - - improved: bool - Whether or not neighbor filtering improved the alignment - - updated_prev_match_info12 : MatchInfo - If improved is True, then `updated_prev_match_info12` includes only - features, descriptors that were found in both neighbors. Otherwise, - all of the original features will be maintained - - updated_next_match_info12 : MatchInfo - If improved is True, then `updated_next_match_info12` includes only - features, descriptors that were found in both neighbors. Otherwise, - all of the original features will be maintained - - """ - - def measure_d(src_xy, dst_xy, tform, M=None): - """Measure distance between warped corresponding points - """ - if M is None: - tform.estimate(src=dst_xy, dst=src_xy) - M = tform.params - warped_xy = warp_tools.warp_xy(src_xy, M) - d = np.median(warp_tools.calc_d(warped_xy, dst_xy)) - - return d, M - - - nf_prev_idx, nf_next_idx = self.get_neighbor_matches_idx(img_obj, prev_img_obj, next_img_obj) - to_prev_match_info12 = img_obj.match_dict[prev_img_obj] - to_next_match_info12 = img_obj.match_dict[next_img_obj] - - improved = False - if len(nf_prev_idx) >= 3: - # Need at least 3 points for an affine transform - - common_kp = to_prev_match_info12.matched_kp1_xy[nf_prev_idx] - _common_kp = to_next_match_info12.matched_kp1_xy[nf_next_idx] - assert np.all(common_kp == _common_kp) - - common_prev_kp = to_prev_match_info12.matched_kp2_xy[nf_prev_idx] - common_next_kp = to_next_match_info12.matched_kp2_xy[nf_next_idx] - - common_matches_d, common_matches_M = measure_d(common_kp, - common_prev_kp, - tform) - - original_d, _ = measure_d(to_prev_match_info12.matched_kp1_xy, - to_prev_match_info12.matched_kp2_xy, - tform) - - original_with_neighbor_filter_d, _ = measure_d(to_prev_match_info12.matched_kp1_xy, - to_prev_match_info12.matched_kp2_xy, - tform, M=common_matches_M) - - if common_matches_d < original_d and original_with_neighbor_filter_d <= original_d: - # neighbor filtering improved alignment - improved = True - - filtered_desc, filtered_kp = self.get_common_desc(img_obj, prev_img_obj, nf_prev_idx) - _filtered_desc, _filtered_kp = self.get_common_desc(img_obj, next_img_obj, nf_next_idx) - - filtered_prev_desc, filtered_prev_kp = self.get_common_desc(prev_img_obj, img_obj, nf_prev_idx) - assert np.all(common_prev_kp == filtered_prev_kp) - - filtered_next_desc, filtered_next_kp = self.get_common_desc(next_img_obj, img_obj, nf_next_idx) - assert np.all(common_next_kp == filtered_next_kp) - - updated_prev_match_info12, _, updated_prev_match_info21, _ = \ - matcher_obj.match_images(desc1=filtered_desc, - kp1_xy=filtered_kp, - desc2=filtered_prev_desc, - kp2_xy=filtered_prev_kp) - - updated_next_match_info12, _, updated_next_match_info21, _ = \ - matcher_obj.match_images(desc1=_filtered_desc, - kp1_xy=_filtered_kp, - desc2=filtered_next_desc, - kp2_xy=filtered_next_kp) - - if improved: - return improved, updated_prev_match_info12, updated_next_match_info12 - else: - return improved, to_prev_match_info12, to_next_match_info12 - - def update_match_dicts_with_neighbor_filter(self, tform, matcher_obj): - """Remove poor matches by keeping only the matches found in neighbors - - Parameters - ---------- - tform : skimage.transform object - The scikit-image transform object that estimates the - parameter matrix - - matcher_obj : Matcher - Object to match features between images. - - """ - new_matches = {} - for i, img_obj in enumerate(self.img_obj_list): - if i == 0 or i == self.size - 1: - continue - - prev_idx = i - 1 - prev_img_obj = self.img_obj_list[prev_idx] - - next_idx = i + 1 - next_img_obj = self.img_obj_list[next_idx] - improved, updated_prev_match_info12, updated_next_match_info12 = \ - self.neighbor_match_filtering(img_obj, prev_img_obj, - next_img_obj, tform, matcher_obj) - - if improved: - new_matches[img_obj.name] = [updated_prev_match_info12, updated_next_match_info12] - - # Update matches - for i, img_obj in enumerate(self.img_obj_list): - if not img_obj.name in new_matches: - continue - prev_idx = i - 1 - prev_img_obj = self.img_obj_list[prev_idx] - - next_idx = i + 1 - next_img_obj = self.img_obj_list[next_idx] - - img_obj_new_matches = new_matches[img_obj.name] - img_obj.match_dict[prev_img_obj] = img_obj_new_matches[0] - img_obj.match_dict[next_img_obj] = img_obj_new_matches[1] - - - def build_metric_matrix(self, metric="n_matches"): - """Create metric matrix based image similarity/distance - - Parameters - ---------- - metric: str - Name of metrric to use. If 'distance' that the distances and - similiarities calculated during feature matching will be used. - If 'n_matches', then the number of matches will be used for - similariy, and 1/n_matches for distance. - - """ - - distance_mat = np.zeros((self.size, self.size)) - similarity_mat = np.zeros_like(distance_mat) - - for i, obj1 in enumerate(self.img_obj_list): - for j in np.arange(i, self.size): - obj2 = self.img_obj_list[j] - if i == j: - continue - - if metric == "n_matches": - s = obj1.match_dict[obj2].n_matches - else: - s = obj1.match_dict[obj2].similarity - d = obj1.match_dict[obj2].distance - distance_mat[i, j] = d - distance_mat[j, i] = d - - similarity_mat[i, j] = s - similarity_mat[j, i] = s - - min_s = similarity_mat.min() - max_s = similarity_mat.max() - min_d = distance_mat.min() - max_d = distance_mat.max() - - # Make sure that image has highest similarity with itself - similarity_mat[np.diag_indices_from(similarity_mat)] += max_s*0.01 - - # Scale metrics between 0 and 1 - similarity_mat = (similarity_mat - min_s) / (max_s - min_s) - similarity_mat[np.diag_indices_from(similarity_mat)] = 1 - if metric == "n_matches": - distance_mat = 1 - similarity_mat - else: - distance_mat = (distance_mat - min_d) / (max_d - min_d) - - distance_mat[np.diag_indices_from(distance_mat)] = 0 - self.unsorted_similarity_mat = similarity_mat - self.unsorted_distance_mat = distance_mat - - def sort(self): - """Order images such that most similar images are adjacent - - Order the images in the stack by optimally ordering the leaves of - dendrogram created by clustering a matrix of image feature distances. - """ - - sorted_D, sorted_idx, optimal_Z = order_Dmat(self.unsorted_distance_mat) - self.optimal_z = optimal_Z - self.distance_mat = sorted_D - self.similarity_mat = self.unsorted_similarity_mat[sorted_idx, :] - self.similarity_mat = self.similarity_mat[:, sorted_idx] - self.img_file_list = [self.img_file_list[i] for i in sorted_idx] - self.img_file_list = [self.img_file_list[i] for i in sorted_idx] - self.img_obj_list = [self.img_obj_list[i] for i in sorted_idx] - for z, img_obj in enumerate(self.img_obj_list): - img_obj.stack_idx = z - - def get_iter_order(self): - """Get order in which to align images - - Will treat the reference image as the center of the stack - - """ - if self.reference_img_f is not None: - ref_img_name = valtils.get_name(self.reference_img_f) - else: - ref_img_name = None - - # obj_names = [img_obj.name for img_obj in self.img_obj_list] - obj_f = [img_obj.name for img_obj in self.img_obj_list] - ref_img_idx = warp_tools.get_ref_img_idx(obj_f, ref_img_name) - self.reference_img_idx = ref_img_idx - self.reference_img_f = self.img_obj_list[ref_img_idx].full_img_f - self.iter_order = warp_tools.get_alignment_indices(self.size, ref_img_idx) - for moving_idx, fixed_idx in self.iter_order: - img_obj = self.img_obj_list[moving_idx] - prev_img_obj = self.img_obj_list[fixed_idx] - img_obj.fixed_obj = prev_img_obj - - def align_to_prev_check_reflections(self, transformer, feature_detector, matcher_obj, keep_unfiltered=False, qt_emitter=None): - """Use key points to align current image to previous image in the stack, but checking if reflection improves alignment - - Parameters - --------- - transformer : skimage.transform object - The scikit-image transform object that estimates the - parameter matrix - - feature_detector : FeatureDD - FeatureDD object that detects and computes image features. - - matcher_obj : Matcher - Object to match features between images. - - keep_unfiltered : bool - Whether or not matcher_obj should store unfiltered matches - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - - ref_img_obj = self.img_obj_list[self.reference_img_idx] - for moving_idx, fixed_idx in tqdm(self.iter_order): - img_obj = self.img_obj_list[moving_idx] - prev_img_obj = self.img_obj_list[fixed_idx] - - if fixed_idx == self.reference_img_idx: - prev_M = ref_img_obj.T.copy() - - if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":img_obj.image.shape[0:2], "img2_shape": prev_img_obj.image.shape[0:2]} - else: - filter_kwargs = None - - # Estimate curent error without reflections. Don't need to re-detect and match features - to_prev_match_info = img_obj.match_dict[prev_img_obj] - transformer.estimate(to_prev_match_info.matched_kp2_xy, to_prev_match_info.matched_kp1_xy) - unreflected_warped_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, transformer.params) - _, unreflected_d = warp_tools.measure_error(to_prev_match_info.matched_kp2_xy, unreflected_warped_src_xy, prev_img_obj.image.shape) - - reflected_d_vals = [unreflected_d] - reflection_M = [np.eye(3)] - transforms = [transformer.params] - reflected_matches12 = [to_prev_match_info] - reflected_matches21 = [prev_img_obj.match_dict[img_obj]] - - if keep_unfiltered and prev_img_obj in img_obj.unfiltered_match_dict: - unfiltered_reflected_matches12 = [img_obj.unfiltered_match_dict[prev_img_obj]] - unfiltered_reflected_matches21 = [prev_img_obj.unfiltered_match_dict[img_obj]] - - # Estimate error with reflections - dst_xy = warp_tools.warp_xy(prev_img_obj.kp_pos_xy, prev_M) - for rx in [False, True]: - for ry in [False, True]: - if not rx and not ry: - continue - - rM = warp_tools.get_reflection_M(rx, ry, img_obj.image.shape) - reflected_img = warp_tools.warp_img(img_obj.image, rM @ img_obj.T, out_shape_rc=img_obj.padded_shape_rc) - - reflected_src_xy, reflected_desc = feature_detector.detect_and_compute(reflected_img) - - unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=reflected_img, desc1=reflected_desc, kp1_xy=reflected_src_xy, - img2=prev_img_obj.image, desc2=prev_img_obj.desc, kp2_xy=dst_xy, - additional_filtering_kwargs=filter_kwargs) - - # unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - # matcher_obj.match_images(reflected_desc, reflected_src_xy, - # prev_img_obj.desc, dst_xy, - # filter_kwargs) - - # Record info # - _ = transformer.estimate(filtered_match_info12.matched_kp2_xy, filtered_match_info12.matched_kp1_xy) - _, reflected_d = warp_tools.measure_error(filtered_match_info12.matched_kp2_xy, filtered_match_info12.matched_kp1_xy, prev_img_obj.padded_shape_rc) - reflected_d_vals.append(reflected_d) - reflection_M.append(rM) - transforms.append(transformer.params) - - # Move matched features to position in original images - img_inv_M = np.linalg.inv(rM @ img_obj.T) - prev_img_inv_M = np.linalg.inv(prev_M) - - filtered_match_info12.matched_kp1_xy = warp_tools.warp_xy(filtered_match_info12.matched_kp1_xy, img_inv_M) - filtered_match_info12.matched_kp2_xy = warp_tools.warp_xy(filtered_match_info12.matched_kp2_xy, prev_img_inv_M) - - filtered_match_info21.matched_kp1_xy = warp_tools.warp_xy(filtered_match_info21.matched_kp1_xy, prev_img_inv_M) - filtered_match_info21.matched_kp2_xy = warp_tools.warp_xy(filtered_match_info21.matched_kp2_xy, img_inv_M) - - reflected_matches12.append(filtered_match_info12) - reflected_matches21.append(filtered_match_info21) - - if keep_unfiltered: - unfiltered_match_info12.matched_kp1_xy = warp_tools.warp_xy(unfiltered_match_info12.matched_kp1_xy, img_inv_M) - unfiltered_match_info12.matched_kp2_xy = warp_tools.warp_xy(unfiltered_match_info12.matched_kp2_xy, prev_img_inv_M) - - unfiltered_match_info21.matched_kp1_xy = warp_tools.warp_xy(unfiltered_match_info21.matched_kp1_xy, prev_img_inv_M) - unfiltered_match_info21.matched_kp2_xy = warp_tools.warp_xy(unfiltered_match_info21.matched_kp2_xy, img_inv_M) - - unfiltered_reflected_matches12.append(unfiltered_match_info12) - unfiltered_reflected_matches21.append(unfiltered_match_info21) - - best_idx = np.argmin(reflected_d_vals) - best_reflect_M = reflection_M[best_idx] - best_M = transforms[best_idx] - img_obj.to_prev_A = best_M - img_obj.reflection_M = best_reflect_M - prev_M = img_obj.reflection_M @ img_obj.T @ img_obj.to_prev_A - - ref_x, ref_y = best_reflect_M[[0, 1], [0, 1]] < 0 - if ref_x or ref_y: - msg = f'detected relfections between {img_obj.name} and {prev_img_obj.name} along the' - if ref_x and ref_y: - msg = f'{msg} x and y axes' - elif ref_x: - msg = f'{msg} x axis' - elif ref_y: - msg = f'{msg} y axis' - - valtils.print_warning(msg) - - # Update matches - img_obj.match_dict[prev_img_obj] = reflected_matches12[best_idx] - prev_img_obj.match_dict[img_obj] = reflected_matches21[best_idx] - - if keep_unfiltered: - img_obj.unfiltered_match_dict[prev_img_obj] = unfiltered_reflected_matches12[best_idx] - prev_img_obj.unfiltered_match_dict[img_obj] = unfiltered_reflected_matches21[best_idx] - - if qt_emitter is not None: - qt_emitter.emit(1) - - - def align_to_prev(self, transformer, qt_emitter=None): - """Use key points to align current image to previous image in the stack - - Parameters - --------- - transformer : skimage.transform object - The scikit-image transform object that estimates the - parameter matrix - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - ref_img_obj = self.img_obj_list[self.reference_img_idx] - - if qt_emitter is not None: - qt_emitter.emit(1) - - for moving_idx, fixed_idx in tqdm(self.iter_order): - img_obj = self.img_obj_list[moving_idx] - prev_img_obj = self.img_obj_list[fixed_idx] - img_obj.fixed_obj = prev_img_obj - - if fixed_idx == self.reference_img_idx: - prev_M = ref_img_obj.T.copy() - - to_prev_match_info = img_obj.match_dict[prev_img_obj] - src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, img_obj.T) - dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - - transformer.estimate(dst_xy, src_xy) - img_obj.to_prev_A = transformer.params - - prev_M = img_obj.T @ img_obj.to_prev_A - - if qt_emitter is not None: - qt_emitter.emit(1) - - def optimize(self, affine_optimizer, qt_emitter=None): - """Refine alignment by minimizing a metric - - Transformation will only be allowed if it both decreases the - cost and median distance between keypoints. - - Parameters - ----------- - affine_optimizer : AffineOptimzer - Object that will minimize a cost function to find the optimal - affine transformations - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - """ - ref_img_obj = self.img_obj_list[self.reference_img_idx] - ref_warped = warp_tools.warp_img(ref_img_obj.image, M=ref_img_obj.T, - out_shape_rc=ref_img_obj.padded_shape_rc) - if qt_emitter is not None: - qt_emitter.emit(1) - - for moving_idx, fixed_idx in tqdm(self.iter_order): - img_obj = self.img_obj_list[moving_idx] - prev_img_obj = self.img_obj_list[fixed_idx] - - if prev_img_obj == ref_img_obj: - prev_img = ref_warped - prev_M = ref_img_obj.T - - M = img_obj.reflection_M @ img_obj.T @ img_obj.to_prev_A - warped_img = warp_tools.warp_img(img_obj.image, - M=M, - out_shape_rc=img_obj.padded_shape_rc) - - to_prev_match_info = img_obj.match_dict[prev_img_obj] - before_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, M) - before_dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - before_tre, before_med_d = warp_tools.measure_error(before_src_xy, - before_dst_xy, - warped_img.shape) - - # Get mask - img_mask = np.ones(img_obj.image.shape[0:2], dtype=np.uint8) - warped_img_mask = warp_tools.warp_img(img_mask, - M=M, - out_shape_rc=img_obj.padded_shape_rc) - - prev_img_mask = np.ones(prev_img_obj.image.shape[0:2], dtype=np.uint8) - warped_prev_img_mask = warp_tools.warp_img(prev_img_mask, - M=prev_M, - out_shape_rc=prev_img_obj.padded_shape_rc) - - mask = np.zeros(warped_img_mask.shape, dtype=np.uint8) - mask[(warped_img_mask != 0) & (warped_prev_img_mask != 0)] = 255 - - # Optimize area inside mask - if affine_optimizer.accepts_xy: - moving_xy = before_src_xy - fixed_xy = before_dst_xy - else: - moving_xy = None - fixed_xy = None - - with valtils.HiddenPrints(): - _, optimal_M, _ = affine_optimizer.align(moving=warped_img, fixed=prev_img, - mask=mask, initial_M=None, - moving_xy=moving_xy, - fixed_xy=fixed_xy) - - # Keep optimal M if it actually improved alignment - initial_cst = affine_optimizer.cost_fxn(warped_img, prev_img, mask) - - after_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, M @ optimal_M) - after_dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - - optimal_reg_img = warp_tools.warp_img(warped_img, - M=optimal_M, - out_shape_rc=img_obj.padded_shape_rc) - - after_cst = affine_optimizer.cost_fxn(optimal_reg_img, prev_img, mask) - - after_tre, after_med_d = warp_tools.measure_error(after_src_xy, - after_dst_xy, - warped_img.shape) - - if after_cst is not None and initial_cst is not None: - lower_cost = after_cst <= initial_cst - else: - lower_cost = True - - lower_d = after_med_d <= before_med_d - if lower_cost and lower_d: - prev_img = optimal_reg_img - img_obj.optimal_M = optimal_M - else: - msg = (f"Somehow optimization made things worse. " - f"Cost was {initial_cst} but is now {after_cst}" - f"KP medD was {before_med_d}, but is now {after_med_d}.") - valtils.print_warning(msg) - prev_img = warped_img - - prev_M = M @ img_obj.optimal_M - - if qt_emitter is not None: - qt_emitter.emit(1) - - def calc_warped_img_size(self): - """Determine the shape of the registered images - """ - min_x = np.inf - max_x = 0 - min_y = np.inf - max_y = 0 - for i in range(self.size): - img_obj = self.img_obj_list[i] - M = img_obj.reflection_M @ img_obj.T @ img_obj.to_prev_A @ img_obj.optimal_M - img_corners_rc = warp_tools.get_corners_of_image(img_obj.image.shape) - warped_corners_xy = warp_tools.warp_xy(img_corners_rc[:, ::-1], M) - - min_x = np.min([np.min(warped_corners_xy[:, 0]), min_x]) - max_x = np.max([np.max(warped_corners_xy[:, 0]), max_x]) - min_y = np.min([np.min(warped_corners_xy[:, 1]), min_y]) - max_y = np.max([np.max(warped_corners_xy[:, 1]), max_y]) - - w = int(np.ceil(max_x - min_x)) - h = int(np.ceil(max_y - min_y)) - - return np.array([h, w]) - - def finalize(self): - """Combine transformation matrices and get final shape of registered images - """ - - min_x = np.inf - max_x = 0 - min_y = np.inf - max_y = 0 - M_list = [None] * self.size - for i in tqdm(range(self.size)): - - img_obj = self.img_obj_list[i] - - M = img_obj.reflection_M @ img_obj.T @ img_obj.to_prev_A @ img_obj.optimal_M - M_list[i] = M - - img_corners_rc = warp_tools.get_corners_of_image(img_obj.image.shape) - warped_corners_xy = warp_tools.warp_xy(img_corners_rc[:, ::-1], M) - - min_x = np.min([np.min(warped_corners_xy[:, 0]), min_x]) - max_x = np.max([np.max(warped_corners_xy[:, 0]), max_x]) - min_y = np.min([np.min(warped_corners_xy[:, 1]), min_y]) - max_y = np.max([np.max(warped_corners_xy[:, 1]), max_y]) - - w = int(np.ceil(max_x - min_x)) - h = int(np.ceil(max_y - min_y)) - crop_T = np.identity(3) - crop_T[0, 2] = min_x - crop_T[1, 2] = min_y - - for i, img_obj in enumerate(self.img_obj_list): - img_obj.crop_T = crop_T - img_obj.M = M_list[i] @ crop_T - img_obj.M_inv = np.linalg.inv(img_obj.M) - img_obj.registered_img = warp_tools.warp_img(img=img_obj.image, - M=img_obj.M, - out_shape_rc=(h, w)) - - img_obj.registered_shape_rc = img_obj.registered_img.shape[0:2] - - - def wiggle_to_ref(self, transformer): - """Compose rigid transforms to wiggle image to reference - - #. For each slide, get M that aligns it's rigidly warp points - to it's fixed image's rigidly warped points. These will be `rolling_M` - #. Then, for each slide, compose their `M` with each neighbor's `rolling M` - until it gets to the reference slide - - """ - ref_obj = self.img_obj_list[self.reference_img_idx] - # Find inverse transforms that will align rigid image to rigid neighbor - rolling_M_list = [None] * self.size - for img_obj in self.img_obj_list: - if img_obj == ref_obj: - continue - - matches = img_obj.match_dict[img_obj.fixed_obj] - - rigid_reg_moving_xy = warp_tools.warp_xy(matches.matched_kp1_xy, M=img_obj.M) - rigid_reg_fixed_xy = warp_tools.warp_xy(matches.matched_kp2_xy, M=img_obj.fixed_obj.M) - - transformer.estimate(src=rigid_reg_fixed_xy, dst=rigid_reg_moving_xy) - - rolling_M = transformer.params - rolling_M_list[img_obj.stack_idx] = rolling_M - - # Compose rolling transforms - wiggle_M_list = [None] * self.size - for img_obj in self.img_obj_list: - if img_obj == ref_obj: - continue - - neighbor_slide = img_obj.fixed_obj - wiggle_M = np.eye(3) - while neighbor_slide != ref_obj: - neighbor_rolling_M = rolling_M_list[neighbor_slide.stack_idx] - wiggle_M = wiggle_M @ neighbor_rolling_M - neighbor_slide = neighbor_slide.fixed_obj - - wiggle_M_list[img_obj.stack_idx] = wiggle_M - - # Update M - for img_obj in self.img_obj_list: - if img_obj == ref_obj: - continue - updated_M = img_obj.M @ wiggle_M_list[img_obj.stack_idx] - img_obj.M = updated_M - - - def clear_unused_matches(self): - """Clear up space by removing unused matches between Zimages - - Will only keep matches between each ZImage and the previous - Zimage in the stack - - """ - - for i, img_obj in enumerate(self.img_obj_list): - if i == 0: - prev_img_obj = None - else: - prev_img_obj = self.img_obj_list[i-1] - - if i == self.size - 1: - next_img_obj = None - else: - next_img_obj = self.img_obj_list[i+1] - - img_obj.reduce(prev_img_obj, next_img_obj) - - def summarize(self): - """Summarize alignment error - - Returns - ------- - summary_df: Dataframe - Pandas dataframe containin the registration error of the - alignment between each image and the previous one in the stack. - - """ - - src_img_names = [None] * self.size - dst_img_names = [None] * self.size - og_med_d_list = [None] * self.size - og_tre_list = [None] * self.size - med_d_list = [None] * self.size - - weighted_med_d_list = [None] * self.size - tre_list = [None] * self.size - shape_list = [None] * self.size - for i in range(0, self.size): - img_obj = self.img_obj_list[i] - src_img_names[i] = img_obj.name - shape_list[i] = img_obj.registered_img.shape - if i == self.reference_img_idx: - continue - - prev_img_obj = img_obj.fixed_obj - dst_img_names[i] = prev_img_obj.name - - current_to_prev_matches = img_obj.match_dict[prev_img_obj] - temp_current_pts = current_to_prev_matches.matched_kp1_xy - temp_prev_pts = current_to_prev_matches.matched_kp2_xy - - og_tre_list[i], og_med_d_list[i] = \ - warp_tools.measure_error(temp_current_pts, - temp_prev_pts, - img_obj.image.shape) - - current_pts = warp_tools.warp_xy(temp_current_pts, img_obj.M) - prev_pts = warp_tools.warp_xy(temp_prev_pts, prev_img_obj.M) - - tre_list[i], med_d_list[i] = \ - warp_tools.measure_error(current_pts, - prev_pts, - img_obj.image.shape) - - similarities = \ - convert_distance_to_similarity(current_to_prev_matches.match_distances, - current_to_prev_matches.matched_desc1.shape[0]) - - _, weighted_med_d_list[i] = \ - warp_tools.measure_error(current_pts, prev_pts, - img_obj.image.shape, similarities) - - summary_df = pd.DataFrame({ - "from": src_img_names, - "to": dst_img_names, - "original_D": og_med_d_list, - "D": med_d_list, - "D_weighted": weighted_med_d_list, - "original_TRE": og_tre_list, - "TRE": tre_list, - "shape": shape_list, - }) - - non_ref_idx = list(range(self.size)) - non_ref_idx.remove(self.reference_img_idx) - summary_df["series_d"] = warp_tools.calc_total_error(summary_df.D.values[non_ref_idx]) - summary_df["series_tre"] = warp_tools.calc_total_error(summary_df.TRE.values[non_ref_idx]) - summary_df["series_weighted_d"] = warp_tools.calc_total_error(summary_df.D_weighted.values[non_ref_idx]) - summary_df["name"] = self.name - - return summary_df - - - -def register_images(img_dir, dst_dir=None, name="registrar", - feature_detector=VggFD(), - matcher=Matcher(), transformer=EuclideanTransform(), - affine_optimizer=None, - imgs_ordered=False, reference_img_f=None, - similarity_metric="n_matches", - check_for_reflections=False, - max_scaling=3.0, align_to_reference=False, qt_emitter=None, valis_obj=None): - """ - Rigidly align collection of images - - Parameters - ---------- - img_dir : str - Path to directory containing the images that the user would like - to be registered. These images need to be single channel, uint8 images - - dst_dir : str, optional - Top directory where aliged images should be save. SerialRigidRegistrar will - be in this folder, and aligned images in the "registered_images" - sub-directory. If None, the images will not be written to file - - name : str, optional - Descriptive name of registrar, such as the sample's name - - feature_detector : FeatureDD - FeatureDD object that detects and computes image features. - - matcher : Matcher - Matcher object that will be used to match image features - - transformer : scikit-image Transform object - Transformer used to find transformation matrix that will warp each - image to the target image. - - affine_optimizer : AffineOptimzer object - Object that will minimize a cost function to find the - optimal affine transoformations - - imgs_ordered : bool - Boolean defining whether or not the order of images in img_dir - are already in the correct order. If True, then each filename should - begin with the number that indicates its position in the z-stack. If - False, then the images will be sorted by ordering a feature distance - matix. - - reference_img_f : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be the reference. - - check_for_reflections : bool, optional - Determine if alignments are improved by relfecting/mirroring/flipping - images. Optional because it requires re-detecting features in each version - of the images and then re-matching features, and so can be time consuming and - not always necessary. - - similarity_metric : str - Metric used to calculate similarity between images, which is in turn - used to build the distance matrix used to sort the images. - - summary : Dataframe - Pandas dataframe containing the median distance between matched features - before and after registration. - - align_to_reference : bool, optional - Whether or not images should be aligned to a reference image - specified by `reference_img_f`. - - qt_emitter : PySide2.QtCore.Signal, optional - Used to emit signals that update the GUI's progress bars - - Returns - ------- - registrar : SerialRigidRegistrar - SerialRigidRegistrar object contains general information about the alginments, - but also a list of Z-images. Each ZImage contains the warp information - for an image in the stack, including the transformation matrices - calculated at each step, keypoint poisions, image descriptors, and - matches with other images. See attributes from Zimage for more - information. - - """ - - tic = time() - if affine_optimizer is not None: - if transformer.__class__.__name__ != affine_optimizer.transformation: - print(Warning("Transformer is of type ", - transformer.__class__.__name__, - "but affine_optimizer optimizes the", - affine_optimizer.transformation, - ". Setting", transformer.__class__.__name__, - "as the transform to be optimized")) - - affine_optimizer.transformation = transformer.__class__.__name__ - - if transformer.__class__.__name__ == "EuclideanTransform": - matcher.scaling = False - else: - matcher.scaling = True - print(f"SR ref 1517 {reference_img_f}") - registrar = SerialRigidRegistrar(img_dir, - imgs_ordered=imgs_ordered, - reference_img_f=reference_img_f, - name=name, - align_to_reference=align_to_reference) - - print("\n======== Detecting features\n") - registrar.generate_img_obj_list(feature_detector, qt_emitter=qt_emitter) - - if valis_obj is not None: - if valis_obj.create_masks: - # Remove feature points outside of mask - for img_obj in registrar.img_obj_dict.values(): - slide_obj = valis_obj.get_slide(img_obj.name) - features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=img_obj.kp_pos_xy, mask=slide_obj.rigid_reg_mask) - n_removed = img_obj.kp_pos_xy.shape[0] - len(features_in_mask_idx) - print(f"Removed {n_removed} features outside of the mask for {slide_obj.name}") - if len(features_in_mask_idx) > 0: - img_obj.kp_pos_xy = img_obj.kp_pos_xy[features_in_mask_idx, :] - img_obj.desc = img_obj.desc[features_in_mask_idx, :] - - print("\n======== Matching images\n") - if registrar.aleady_sorted: - registrar.match_sorted_imgs(matcher, keep_unfiltered=False, - qt_emitter=qt_emitter) - - for i, img_obj in enumerate(registrar.img_obj_list): - img_obj.stack_idx = i - - else: - registrar.match_imgs(matcher, keep_unfiltered=False, - qt_emitter=qt_emitter) - - print("\n======== Sorting images\n") - registrar.build_metric_matrix(metric=similarity_metric) - registrar.sort() - - registrar.distance_metric_name = matcher.metric_name - registrar.distance_metric_type = matcher.metric_type - print("\n======== Calculating transformations\n") - registrar.get_iter_order() - if registrar.size > 2: - registrar.update_match_dicts_with_neighbor_filter(transformer, matcher) - - if check_for_reflections: - registrar.align_to_prev_check_reflections(transformer=transformer, - feature_detector=feature_detector, - matcher_obj=matcher, - keep_unfiltered=False, - qt_emitter=qt_emitter) - else: - registrar.align_to_prev(transformer=transformer, qt_emitter=qt_emitter) - - # Check current output shape. If too large, then registration failed - for img_obj in registrar.img_obj_list: - s = transform.SimilarityTransform(img_obj.M).scale - if s >= max_scaling or s <= 1/max_scaling: - print(Warning(f"Max allowed scaling is {max_scaling},\ - but was calculated as being {s}.\ - Registration failed. Maybe try using the Euclidean transform.")) - return False - - if affine_optimizer is not None: - print("\n======== Optimizing alignments\n") - registrar.optimize(affine_optimizer, qt_emitter=qt_emitter) - - registrar.finalize() - - if align_to_reference: - registrar.wiggle_to_ref(transformer) - - if dst_dir is not None: - registered_img_dir = os.path.join(dst_dir, "registered_images") - registered_data_dir = os.path.join(dst_dir, "data") - for d in [registered_img_dir, registered_data_dir]: - pathlib.Path(d).mkdir(exist_ok=True, parents=True) - - print("\n======== Summarizing alignments\n") - summary_df = registrar.summarize() - summary_file = os.path.join(registered_data_dir, name + "_results.csv") - summary_df.to_csv(summary_file, index=False) - - registrar.summary = summary_df - - print("\n======== Saving results\n") - pickle_file = os.path.join(registered_data_dir, name + "_registrar.pickle") - pickle.dump(registrar, open(pickle_file, 'wb')) - - n_digits = len(str(registrar.size)) - for img_obj in registrar.img_obj_list: - f_out = "".join([str.zfill(str(img_obj.stack_idx), n_digits), - "_", img_obj.name, ".png"]) - - io.imsave(os.path.join(registered_img_dir, f_out), - img_obj.registered_img.astype(np.uint8)) - - registrar.clear_unused_matches() - toc = time() - elapsed = toc - tic - time_string, time_units = valtils.get_elapsed_time_string(elapsed) - - print(f"\n======== Rigid registration complete in {time_string} {time_units}\n") - - return registrar diff --git a/examples/acrobat_2023/valis/slide_io.py b/examples/acrobat_2023/valis/slide_io.py deleted file mode 100644 index 0e5cb02f..00000000 --- a/examples/acrobat_2023/valis/slide_io.py +++ /dev/null @@ -1,3030 +0,0 @@ -"""Methods and classes to read and write slides in the .ome.tiff format - -""" - -import os -from skimage import io, transform -import pyvips -import numpy as np -from PIL import Image -import pathlib -import re -import multiprocessing -from joblib import Parallel, delayed, parallel_backend -import imghdr -from scipy import stats -from bs4 import BeautifulSoup -from statistics import mode -import time -import sys -import re -import itertools -import xml.etree.ElementTree as elementTree -import unicodedata -import ome_types -import jpype -from aicspylibczi import CziFile -from tqdm import tqdm -import scyjava - -from . import valtils -from . import slide_tools -from . import warp_tools - -pyvips.cache_set_max(0) - -MAX_TILE_SIZE = 2**10 -"""int: maximum tile used to read or write images""" - -BF_RDR = "bioformats" -"""str: Name of Bioformats reader.""" - -VIPS_RDR = "libvips" -"""str: Name of pyvips reader""" - -OPENSLIDE_RDR = "openslide" -"""str: Name of OpenSlide reader""" - -IMG_RDR = "skimage" -"""str: Name of image reader""" - -PIXEL_UNIT = "px" -"""str: Physical unit when the unit can't be found in the metadata""" - -MICRON_UNIT = u'\u00B5m' -"""str: Phyiscal unit for micron/micrometers""" - -ALL_OPENSLIDE_READABLE_FORMATS = [".svs", ".tif", ".vms", ".vmu", ".ndpi", ".scn", ".mrxs", ".tiff", ".svslide", ".bif"] -"""list: File extensions that OpenSlide can read""" - -BF_READABLE_FORMATS = None -"""list: File extensions that Bioformats can read. - Filled in after initializing JVM""" - -OPENSLIDE_ONLY = None -"""list: File extensions that OpenSlide can read but Bioformats can't. - Filled in after initializingJVM""" - -FormatTools = None -"""Bioformats FormatTools. - Created after initializing JVM""" - -BF_UNIT = None -"""Bioformats UNITS. - Created after initializing JVM.""" - -BF_MICROMETER = None -"""Bioformats Unit mircometer object. - Created after initializing JVM.""" - -ome = None -"""Bioformats ome from bioforamts_jar. - Created after initializing JVM.""" - -loci = None -"""Bioformats loci from bioforamts_jar. - Created after initializing JVM.""" - - -""" -NOTE: Commented out block is how to use boformats with javabrdige. -However, on conda, javabridge isn't available for python 3.9. -If using, remember to put bftools/bioformats_package.jar in the -source directory. - -Keeping the code just in case need to use javabridge again. -""" -# Bioformats + Javabridge # -#---------------------------------------# -# -# try: -# bf_jar = os.path.join(pathlib.Path(__file__).parent, "bftools/bioformats_package.jar") -# except Exception: -# # Running interactively -# bf_jar = os.path.join(os.getcwd(), "bftools/bioformats_package.jar") -# -# -# def init_jvm_javabridge(): -# """Initialize JVM for BioFormats -# """ -# -# if javabridge.get_env() is None: -# -# all_jars = javabridge.JARS + [bf_jar] -# javabridge.start_vm(class_path=all_jars, max_heap_size="10G", run_headless=True) -# -# myloglevel = "ERROR" -# rootLoggerName = javabridge.get_static_field("org/slf4j/Logger", "ROOT_LOGGER_NAME", "Ljava/lang/String;") -# rootLogger = javabridge.static_call("org/slf4j/LoggerFactory", "getLogger", -# "(Ljava/lang/String;)Lorg/slf4j/Logger;", rootLoggerName) -# logLevel = javabridge.get_static_field("ch/qos/logback/classic/Level", myloglevel, "Lch/qos/logback/classic/Level;") -# javabridge.call(rootLogger, "setLevel", "(Lch/qos/logback/classic/Level;)V", logLevel) -# -# msg = "JVM has been initialize. Be sure to call valis.kill_jvm() or slide_io.kill_jvm() at the end of your script" -# valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) -# -# # Fill in global variables that can only be created after initializing the JVM -# -# global FormatTools -# global BF_UNIT -# global BF_MICROMETER -# global BF_READABLE_FORMATS -# global OPENSLIDE_ONLY -# -# FormatTools = javabridge.JClassWrapper("loci.formats.FormatTools") -# BF_UNIT = javabridge.JClassWrapper("ome.units.UNITS") -# BF_MICROMETER = BF_UNIT.MICROMETER -# BF_READABLE_FORMATS = get_bf_readable_formats_javabridge() -# OPENSLIDE_ONLY = list(set(ALL_OPENSLIDE_READABLE_FORMATS).difference(set(BF_READABLE_FORMATS))) -# -# -# def kill_jvm_javabridge(): -# """Kill JVM for BioFormats -# """ -# javabridge.kill_vm() -# msg = "JVM has been killed. If this was due to an error, then a new Python session will need to be started" -# valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) -# -# -# def get_bf_readable_formats_javabridge(): -# """Get extensions of formats that BioFormats can read -# """ -# if javabridge.get_env() is None: -# init_jvm_javabridge() -# -# env = javabridge.get_env() -# base_reader = javabridge.make_instance('loci/formats/ImageReader', '()V') -# readers = javabridge.jutil.call(base_reader, 'getReaders', -# '()[Lloci/formats/IFormatReader;') -# all_readers = env.get_object_array_elements(readers) -# readable_formats = [] -# f_append = readable_formats.append -# for format_reader in all_readers: -# j_suffixes = javabridge.get_env().get_object_array_elements( -# javabridge.jutil.call( -# format_reader, 'getSuffixes', -# '()[Ljava/lang/String;')) -# -# for js in j_suffixes: -# suffix = javabridge.to_string(js) -# if len(suffix) > 0: -# f_append("." + suffix) -# -# javabridge.jutil.call(base_reader, 'close', '()V') -# -# return readable_formats -#---------------------------------------# - -# Bioformats/scyjava + Jpype # -#--------------------# - - -def init_jvm(jar=None, mem_gb=10): - """Initialize JVM for BioFormats - - Parameters - ---------- - mem_gb : int - Amount of memory, in GB, for JVM - """ - import jpype - if not jpype.isJVMStarted(): - global FormatTools - global BF_MICROMETER - global OPENSLIDE_ONLY - global BF_READABLE_FORMATS - global ome - global loci - - if jar is None: - - # Check if jar is bundled with source code, like in a Docker image - # Can use instead of using maven to download, which requires an unblocked connection - parent_dir = pathlib.Path(__file__).parent.resolve() - local_bf_jar = os.path.join(parent_dir, "bioformats_package.jar") - if os.path.exists(local_bf_jar): - jar = local_bf_jar - - if jar is not None: - jpype.addClassPath(jar) - jpype.startJVM(f"-Djava.awt.headless=true -Xmx{mem_gb}G", classpath=jar) - - else: - # scyjava.config.endpoints.append('ome:jxrlib-all') - # scyjava.config.endpoints.append('ome:formats-gpl') - scyjava.config.endpoints.extend(['ome:formats-gpl', 'ome:jxrlib-all']) - # scyjava.config.endpoints.append('ome:bio-formats_plugins') - - scyjava.start_jvm([f"-Xmx{mem_gb}G"]) - - loci = jpype.JPackage("loci") - ome = jpype.JPackage("ome") - loci.common.DebugTools.setRootLevel("ERROR") - - FormatTools = loci.formats.FormatTools - BF_MICROMETER = ome.units.UNITS.MICROMETER - BF_READABLE_FORMATS = get_bf_readable_formats() - OPENSLIDE_ONLY = list(set(ALL_OPENSLIDE_READABLE_FORMATS).difference(set(BF_READABLE_FORMATS))) - - msg = (f"JVM has been initialized. " - f"Be sure to call registration.kill_jvm() " - f"or slide_io.kill_jvm() at the end of your script.") - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - -def kill_jvm(): - """Kill JVM for BioFormats - """ - try: - # jpype.shutdownJVM() - scyjava.shutdown_jvm() - msg = "JVM has been killed. If this was due to an error, then a new Python session will need to be started" - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - except NameError: - pass - - -def get_bioformats_version(): - v = loci.formats.FormatTools.VERSION - - return v - - -def get_bf_readable_formats(): - """Get extensions of formats that BioFormats can read - - Returns - ------- - readable_formats : list of str - List of formats that can be read by Bioformats - - """ - - if not jpype.isJVMStarted(): - init_jvm() - - baseReader = loci.formats.ImageReader() - readers = baseReader.getReaders() - read_range = range(1, readers.length) - readable_formats = ["." + str(f) for l in [list(readers[i].getSuffixes()) for i in read_range] for f in l if len(f) > 0] - baseReader.close() - - return readable_formats - -def bf_to_numpy_dtype(bf_pixel_type, little_endian): - """Get numpy equivalent of the bioformats pixel type - - Adapted from the python-bioformats package - - Parameters - ---------- - bf_pixel_type : int - Integer indicating the Bioformats pixel type - - little_endian : bool - Whether or not the image is little endian - - Returns - ------- - dtype : numpy.dtype - Numpy dtype - - scale : int - Maximum value of `dtype` - - """ - - if bf_pixel_type == FormatTools.INT8: - dtype = np.int8 - scale = 255 - - elif bf_pixel_type == FormatTools.UINT8: - dtype = np.uint8 - scale = 255 - - elif bf_pixel_type == FormatTools.UINT16: - dtype = 'u2' - scale = 65535 - - elif bf_pixel_type == FormatTools.INT16: - dtype = 'i2' - scale = 65535 - - elif bf_pixel_type == FormatTools.UINT32: - dtype = 'u4' - scale = 2**32 - - elif bf_pixel_type == FormatTools.INT32: - dtype = 'i4' - scale = 2**32-1 - - elif bf_pixel_type == FormatTools.FLOAT: - dtype = 'f4' - scale = 1 - - elif bf_pixel_type == FormatTools.DOUBLE: - dtype = 'f8' - scale = 1 - - return dtype, scale - - -def vips2bf_dtype(vips_format): - """Get bioformats equivalent of the pyvips pixel type - - Parameters - ---------- - vips_format : str - Format of the pyvips.Image - - Returns - ------- - bf_dtype : str - String format of Bioformats datatype - - """ - - np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_format] - bf_dtype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] - - return bf_dtype - - -def bf2vips_dtype(bf_dtype): - """Get bioformats equivalent of the pyvips pixel type - - Parameters - ---------- - bf_dtype : str - String format of Bioformats datatype - - Returns - ------- - vips_format : str - Format of the pyvips.Image - - """ - - np_type = slide_tools.BF_FORMAT_NUMPY_DTYPE[bf_dtype] - vips_format = slide_tools.NUMPY_FORMAT_VIPS_DTYPE[np_type] - - return vips_format - - -def check_to_use_openslide(src_f): - """Determine if OpenSlide can be used to read the slide - - Parameters - ---------- - src_f : str - Path to slide - - Returns - ------- - use_openslide : bool - Whether or not OpenSlide can be used to read the slide. - This can happen if the file format is not readable by - OpenSlide, or if pyvips wasn't installed with OpenSlide - support. - - """ - - use_openslide = False - img_format = slide_tools.get_slide_extension(src_f) - if img_format in ALL_OPENSLIDE_READABLE_FORMATS: - try: - vips_img = pyvips.Image.new_from_file(src_f) - vips_fields = vips_img.get_fields() - if "openslide.level-count" in vips_fields: - use_openslide = True - except pyvips.error.Error as e: - valtils.print_warning(e) - - return use_openslide - - -def check_flattened_pyramid_tiff(src_f): - """Determine if a tiff is a flattened pyramid - - Determines if a slide is pyramid where each page/plane is a channel - in the pyramid. An example would be one where the plane dimensions are - something like - [(600, 600), (600, 600), (600, 600), (300, 300), (300, 300), (300, 300)] - for a 3 channel image with 2 pyramid levels. It seems that bioformats - does not recognize these as pyramid images. - - Parameters - ---------- - src_f : str - Path to slide - - Returns - ------- - is_flattended_pyramid : bool - Whether or not the slide is a flattened pyramid - - can_use_bf : bool - Whether or not Bioformats will read the slide in the same way - - slide_dimensions : ndarray - Dimensions (width, height) for each level in the pyramid - - levels_start_idx : ndarray - The indices indicating which pages/planes start the next - pyramid level - - n_channels : int - Number of channels in the slide - - """ - - vips_img = pyvips.Image.new_from_file(src_f) - vips_fields = vips_img.get_fields() - - is_flattended_pyramid = False - - if 'n-pages' in vips_fields: - n_pages = vips_img.get("n-pages") - # all_areas = [None] * n_pages - # all_dims = [None] * n_pages - # all_n_channels = [None] * n_pages - all_areas = [] - all_dims = [] - all_n_channels = [] - level_starts = [] - prev_area = None - for i in range(n_pages): - try: - page = pyvips.Image.new_from_file(src_f, page=i) - except pyvips.error.Error as e: - print(f"error at page {i}: {e}") - continue - - w = page.width - h = page.height - nc = page.bands - img_area = w*h*nc - - all_areas.append(img_area) - all_dims.append([w, h]) - all_n_channels.append(nc) - - if prev_area is None: - prev_area = img_area - level_starts.append(0) - - else: - if prev_area != img_area: - level_starts.append(i) - - prev_area = img_area - - level_starts = np.array(level_starts) - area_diff = np.diff(all_areas) - most_common_channel_count = mode(all_n_channels) - - unique_areas, _ = np.unique(all_areas, return_index=True) - n_zero_diff = len(np.where(area_diff == 0)[0]) - if most_common_channel_count == 1 and n_zero_diff >= len(unique_areas): - is_flattended_pyramid = True - - if is_flattended_pyramid: - nchannels_per_each_level = np.diff(level_starts) - last_level_channel_count = np.sum(all_n_channels[level_starts[-1]:]) - nchannels_per_each_level = np.hstack([nchannels_per_each_level, - last_level_channel_count]) - - if last_level_channel_count == 3 and nchannels_per_each_level[0] != 3: - # last level is probably a thumbnail - nchannels_per_each_level = nchannels_per_each_level[:-1] - n_channels = mode(nchannels_per_each_level) - levels_start_idx = level_starts[np.where(nchannels_per_each_level==n_channels)[0]] - slide_dimensions = np.array(all_dims)[levels_start_idx] - - else: - slide_dimensions = all_dims - levels_start_idx = np.arange(0, len(slide_dimensions)) - n_channels = most_common_channel_count - - else: - return False, None, None, None, None - # Now check if Bioformats reads it similarly # - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(src_f) - bf_levels = len(bf_reader.metadata.slide_dimensions) - bf_channels = bf_reader.metadata.n_channels - can_use_bf = bf_levels >= len(slide_dimensions) and bf_channels == n_channels - - return is_flattended_pyramid, can_use_bf, slide_dimensions, levels_start_idx, n_channels - - -# Read slides # -class MetaData(object): - """Store slide metadata - - To be filled in by a SlideReader object - - Attributes - ---------- - - name : str - Name of slide. - - series : int - Series number. - - server : str - String indicating what was used to read the metadata. - - slide_dimensions : - Dimensions of all images in the pyramid (width, height). - - is_rgb : bool - Whether or not the image is RGB. - - pixel_physical_size_xyu : - Physical size per pixel and the unit. - - channel_names : list - List of channel names. None if image is RGB - - n_channels : int - Number of channels. - - original_xml : str - Xml string created by bio-formats - - bf_datatype : str - String indicating bioformats image datatype - - optimal_tile_wh : int - Tile width and height used to open and/or save image - - """ - - def __init__(self, name, server, series=0): - """ - - Parameters - ---------- - name : str - Name of slide. - - server : str, optional - String indicating what was used to read the metadata. - - series : int, optional - Series number. - - """ - - self.name = name - self.series = series - self.server = server - self.slide_dimensions = [] - self.is_rgb = None - self.pixel_physical_size_xyu = [] - self.channel_names = None - self.n_channels = 0 - self.original_xml = None - self.bf_datatype = None - self.optimal_tile_wh = 1024 - - -class SlideReader(object): - """Read slides and get metadata - - Attributes - ---------- - slide_f : str - Path to slide - - metadata : MetaData - MetaData containing some basic metadata about the slide - - series : int - Image series - - """ - - def __init__(self, src_f, *args, **kwargs): - """ - Parameters - ----------- - src_f : str - Path to slide - - """ - - self.src_f = src_f - self.metadata = None - self.series = 0 - - def slide2vips(self, level, xywh=None, *args, **kwargs): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - def slide2image(self, level, xywh=None, *args, **kwargs): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - def guess_image_type(self): - f"""Guess if image is {slide_tools.IHC_NAME} or {slide_tools.IF_NAME} - - Brightfield : RGB or uint8 + 3 channels (after removing alpha) - Immunofluorescence: != 3 channels and not RGB - - Returns - ------- - img_type : str - Image type - - """ - - if self.metadata.is_rgb: - img_type = slide_tools.IHC_NAME - else: - img_type = slide_tools.IF_NAME - - return img_type - - def scale_physical_size(self, level): - """Get resolution pyramid level - - Scale resolution to be for requested pyramid level - - Parameters - ---------- - level : int - Pyramid level - - Returns - ------- - level_xy_per_px: tuple - - """ - - level_0_shape = self.metadata.slide_dimensions[0] - level_shape = self.metadata.slide_dimensions[level] - scale_x = level_0_shape[0]/level_shape[0] - scale_y = level_0_shape[1]/level_shape[1] - - level_xy_per_px = (scale_x * self.metadata.pixel_physical_size_xyu[0], - scale_y * self.metadata.pixel_physical_size_xyu[1], - self.metadata.pixel_physical_size_xyu[2]) - - return level_xy_per_px - - def create_metadata(self): - """ Create and fill in a MetaData object - - Returns - ------- - metadata : MetaData - MetaData object containing metadata about slide - - """ - - def get_channel_index(self, channel): - - if isinstance(channel, int): - matching_channel_idx = channel - - elif isinstance(channel, str): - matching_channels = [i for i in range(self.metadata.n_channels) if - re.search(channel.lower(), self.metadata.channel_names[i].lower()) - is not None] - - if len(matching_channels) == 0: - msg = f"Cannot find channel '{channel}' in {self.src_f}. Using channel 0" - valtils.print_warning(msg) - matching_channel_idx = 0 - - elif len(matching_channels) > 1: - all_matching_channels = ", ".join([f"'{self.metadata.channel_names[i]}'" for i in matching_channels]) - msg = f"Fount multiple channels that match '{channel}' in {self.src_f}. These are: {all_matching_channels}. Using channel 0" - valtils.print_warning(msg) - matching_channel_idx = 0 - - else: - matching_channel_idx = matching_channels[0] - - return matching_channel_idx - - def get_channel(self, level, series, channel): - """Get channel from slide - - Parameters - ---------- - level : int - Pyramid level - - series : int - Series number - - channel : str, int - Either the name of the channel (string), or the index of the channel (int) - - Returns - ------- - img_channel : ndarray - Specified channel sliced from the slide/image - - """ - - matching_channel_idx = self.get_channel_index(channel) - image = self.slide2image(level=level, series=series) - img_channel = image[..., matching_channel_idx] - - return img_channel - - def _check_rgb(self, *args, **kwargs): - """Determine if image is RGB - - Returns - ------- - is_rgb : bool - Whether or not the image is RGB - - """ - - def _get_channel_names(self, *args, **kwargs): - """Get names of each channel - - Get list of channel names - - Returns - ------- - channel_names : list - List of channel names - - """ - - def _get_slide_dimensions(self, *args, **kwargs): - """Get dimensions of slide at all pyramid levels - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - """ - - def _get_pixel_physical_size(self, *args, **kwargs): - """Get resolution of slide - - Returns - ------- - res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' - - Notes - ----- - If physical unit is micron, it must be u'\u00B5m', - not mu (u'\u03bcm') or u. - - """ - - -class BioFormatsSlideReader(SlideReader): - """Read slides using BioFormats - - Uses the packages jpype and bioformats-jar - - """ - def __init__(self, src_f, series=None, *args, **kwargs): - """ - Parameters - ----------- - src_f : str - Path to slide - - series : int - The series to be read. If `series` is None, the the `series` - will be set to the series associated with the largest image. - - """ - - init_jvm() - - self.meta_list = [None] - super().__init__(src_f=src_f, *args, **kwargs) - - try: - self.meta_list = self.create_metadata() - except Exception as e: - print(e) - kill_jvm() - - self.n_series = len(self.meta_list) - if series is None: - img_areas = [np.multiply(*meta.slide_dimensions[0]) for meta in self.meta_list] - series = np.argmax(img_areas) - msg = (f"No series provided. " - f"Selecting series with largest image, " - f"which is series {series}") - - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - self._series = series - self.series = series - - def _set_series(self, series): - self._series = series - self.metadata = self.meta_list[series] - - def _get_series(self): - return self._series - - series = property(fget=_get_series, - fset=_set_series, - doc="Slide series") - - def get_tiles_parallel(self, level, tile_bbox_list, pixel_type, series=0): - """Get tiles to slice from the slide - - """ - - n_tiles = len(tile_bbox_list) - tile_array = [None] * n_tiles - - def tile2vips_threaded(idx): - xywh = tile_bbox_list[idx] - # javabridge.attach() - # jpype.attachThreadToJVM() - jpype.java.lang.Thread.attach() - try: - tile = self.slide2image(level, series, xywh=tuple(xywh)) - except Exception as e: - print(e) - pass - # javabridge.detach() - # jpype.detachThreadFromJVM() - jpype.java.lang.Thread.detach() - - tile_array[idx] = slide_tools.numpy2vips(tile, self.metadata.pyvips_interpretation) - - - n_cpu = valtils.get_ncpus_available() - 1 - with parallel_backend("threading", n_jobs=n_cpu): - Parallel()(delayed(tile2vips_threaded)(i) for i in tqdm(range(n_tiles))) - - return tile_array - - def slide2vips(self, level, series=None, xywh=None, tile_wh=None, *args, **kwargs): - """Convert slide to pyvips.Image - - This method uses Bioformats to slice tiles from the slides, and then - stitch them together using pyvips. - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - tile_wh : int, optional - Size of tiles used to contstruct `vips_slide` - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - if series is None: - series = self.series - - else: - self.series = series - - rdr, meta = self._get_bf_objects() - pixel_type, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - slide_shape_wh = self.metadata.slide_dimensions[level] - - if tile_wh is None: - tile_wh = rdr.getOptimalTileWidth() - rdr.close() - - tile_wh = min(tile_wh, MAX_TILE_SIZE) - if np.any(slide_shape_wh < tile_wh): - tile_wh = min(slide_shape_wh) - - tile_bbox = warp_tools.get_grid_bboxes(slide_shape_wh[::-1], - tile_wh, tile_wh, inclusive=True) - - n_across = len(np.unique(tile_bbox[:, 0])) - - print(f"Converting slide to pyvips image") - vips_slide = pyvips.Image.arrayjoin( - self.get_tiles_parallel(level, tile_bbox, pixel_type, series), - across=n_across).crop(0, 0, *slide_shape_wh) - if xywh is not None: - vips_slide = vips_slide.extract_area(*xywh) - - return vips_slide - - def slide2image(self, level, series=None, xywh=None, *args, **kwargs): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 1 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - if series is None: - series = self.series - - else: - self.series = series - - rdr, meta = self._get_bf_objects() - - rdr.setSeries(series) - rdr.setResolution(level) - if xywh is None: - x = 0 - y = 0 - w = rdr.getSizeX() - h = rdr.getSizeY() - xywh = (x, y, w, h) - - if rdr.isRGB(): - img = self._read_rgb(rdr=rdr, xywh=xywh) - - else: - img = self._read_multichannel(rdr=rdr, xywh=xywh) - - rdr.close() - - return img - - def create_metadata(self): - rdr, meta = self._get_bf_objects() - meta_xml = meta.dumpXML() - try: - n_series = rdr.getSeriesCount() - i0 = rdr.getSeries() - slide_format = f"{BF_RDR}_{rdr.getFormat()}" - meta_list = [None] * n_series - for i in range(n_series): - rdr.setSeries(i) - series_name = str(meta.getImageName(i)) - temp_name = f"{os.path.split(self.src_f)[1]}_{series_name}".strip("_") - full_name = f"{temp_name}_Series_{i}" - full_name = full_name.replace(" ", "_") - - series_meta = MetaData(full_name, slide_format, series=i) - - series_meta.is_rgb = self._check_rgb(rdr) - series_meta.channel_names = self._get_channel_names(rdr, meta) - series_meta.n_channels = int(rdr.getSizeC()) - series_meta.slide_dimensions = self._get_slide_dimensions(rdr) - if series_meta.is_rgb: - series_meta.pyvips_interpretation = 'srgb' - elif series_meta.n_channels == 1: - series_meta.pyvips_interpretation = 'b-w' - else: - series_meta.pyvips_interpretation = 'multiband' - - series_meta.pixel_physical_size_xyu = self._get_pixel_physical_size(rdr, meta) - series_meta.bf_pixel_type = str(rdr.getPixelType()) - series_meta.is_little_endian = rdr.isLittleEndian() - series_meta.original_xml = str(meta_xml) - series_meta.bf_datatype = str(FormatTools.getPixelTypeString(rdr.getPixelType())) - series_meta.optimal_tile_wh = int(rdr.getOptimalTileWidth()) - meta_list[i] = series_meta - - i0 = rdr.setSeries(i0) - rdr.close() - - except Exception as e: - print(e) - rdr.close() - - return meta_list - - def _read_rgb(self, rdr, xywh): - - np_dtype, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - buffer = rdr.openBytes(0, *xywh) - img = np.frombuffer(bytes(buffer), np_dtype) - nrgb = rdr.getRGBChannelCount() - _, _, w, h = xywh - - if rdr.isInterleaved(): - img = img.reshape(h, w, nrgb) - else: - img = img.reshape(nrgb, h, w) - img = np.transpose(img, (1, 2, 0)) - - if img.shape[2] > 3: - img = img[0:3] - - return img - - def _read_multichannel(self, rdr, xywh): - _, _, w, h = xywh - n_channels = rdr.getSizeC() - np_dtype, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - if n_channels > 1: - img = np.zeros((h, w, n_channels), dtype=np_dtype) - else: - img = None - - for i in range(n_channels): - idx = rdr.getIndex(0, i, 0) # ZCT - buffer = rdr.openBytes(idx, *xywh) - if img is None: - img = np.frombuffer(bytes(buffer), np_dtype).reshape((h, w)) - else: - img[..., i] = np.frombuffer(bytes(buffer), np_dtype).reshape((h, w)) - - return img - - def _get_bf_objects(self): - """Get Bioformat objects - - Returns - ------- - - rdr : IFormatReader - IFormatReader object that is a property of a bioformats.ImageReader. - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata - - Notes - ----- - Be sure to close rdr with rdr.close() when it's no longer needed - - """ - # Javabridge # - #------------# - # env = javabridge.jutil.get_env() - # rdr = javabridge.JWrapper(javabridge.make_instance( - # 'loci/formats/ImageReader', '()V') - # ) - - # factory = javabridge.JWrapper(javabridge.make_instance( - # 'loci/common/services/ServiceFactory', '()V') - # ) - - # OMEXMLService_class = \ - # env.find_class('loci/formats/services/OMEXMLService').as_class_object() - - # Jpype # - #-------# - - rdr = loci.formats.ImageReader() - factory = loci.common.services.ServiceFactory() - OMEXMLService_class = loci.formats.services.OMEXMLService - - service = factory.getInstance(OMEXMLService_class) - ome_meta = service.createOMEXMLMetadata() - rdr.setMetadataStore(ome_meta) - rdr.setFlattenedResolutions(False) - rdr.setId(self.src_f) - meta = rdr.getMetadataStore() - - return rdr, meta - - def _check_rgb(self, rdr): - """Determine if image is RGB - - Returns - ------- - is_rgb : bool - Whether or not the image is RGB - - """ - - return rdr.isRGB() - - def _get_slide_dimensions(self, rdr): - """Get dimensions of slide at all pyramid levels - - Parameters - ---------- - rdr : IFormatReader - IFormatReader object - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - Notes - ----- - Using javabridge and python-bioformmats, this can be accessed as follows - ` - bf_slide = bioformats.ImageReader(slide_f) - bf_img_reader = javabridge.JWrapper(bf_slide.rdr.o) - - Or - with bioformats.ImageReader(slide_f) as bf_slide: - bf_img_reader = javabridge.JWrapper(bf_slide.rdr.o) - - """ - - r0 = rdr.getResolution() - n_res = rdr.getResolutionCount() - slide_dims = [None] * n_res - for j in range(n_res): - rdr.setResolution(j) - slide_dims[j] = [rdr.getSizeX(), rdr.getSizeY()] - - slide_dims = np.array(slide_dims) - - rdr.setResolution(r0) - - return slide_dims - - def _get_pixel_physical_size(self, rdr, meta): - """Get resolutions for each series - - Parameters - ---------- - rdr : IFormatReader - IFormatReader object. - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata - - Returns - ------- - res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' - - """ - current_series = rdr.getSeries() - temp_x_res = meta.getPixelsPhysicalSizeX(current_series) - if temp_x_res is not None: - x_res = float(temp_x_res.value(BF_MICROMETER).doubleValue()) - y_res = float(meta.getPixelsPhysicalSizeY(current_series).value(BF_MICROMETER).doubleValue()) - phys_unit = str(BF_MICROMETER.getSymbol()) - else: - x_res = 1 - y_res = 1 - phys_unit = PIXEL_UNIT - - res_xyu = (x_res, y_res, phys_unit) - - return res_xyu - - def _get_channel_names(self, rdr, meta): - """Get channel names of image - Parameters - ---------- - rdr : IFormatReader - IFormatReader object - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata. - - Returns - ------- - channel_names : list - List of channel names. - - """ - - nc = rdr.getSizeC() - current_series = rdr.getSeries() - if rdr.isRGB(): - channel_names = None - else: - channel_names = [""] * nc - for i in range(nc): - channel_names[i] = str(meta.getChannelName(current_series, i)) - - return channel_names - - -class VipsSlideReader(SlideReader): - """Read slides using pyvips - Pyvips includes OpenSlide and so can read those formats as well. - - Attributes - ---------- - use_openslide : bool - Whether or not openslide can be used to read this slide. - - is_ome : bool - Whether ot not the side is an ome.tiff. - - Notes - ----- - When using openslide, lower levels can only be read without distortion, - if pixman version 0.40.0 is installed. As of Oct 7, 2021, Macports only has - pixman version 0.38, which produces distorted lower level images. If using - macports may need to install from source do "./configure --prefix=/opt/local/" - when installing from source. - - """ - def __init__(self, src_f, *args, **kwargs): - super().__init__(src_f=src_f, *args, **kwargs) - self.use_openslide = check_to_use_openslide(self.src_f) - self.is_ome = False - self.metadata = self.create_metadata() - - def create_metadata(self): - - if self.use_openslide: - server = OPENSLIDE_RDR - else: - server = VIPS_RDR - - meta_name = f"{os.path.split(self.src_f)[1]}_Series(0)".strip("_") - f_extension = slide_tools.get_slide_extension(self.src_f) - if f_extension in BF_READABLE_FORMATS: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - self.is_ome = re.search("ome-tiff", bf_reader.metadata.server.lower()) is not None - - slide_meta = MetaData(meta_name, server) - vips_img = pyvips.Image.new_from_file(self.src_f) - - slide_meta.is_rgb = self._check_rgb(vips_img) - slide_meta.n_channels = vips_img.bands - if (slide_meta.is_rgb and vips_img.hasalpha() >= 1) or self.use_openslide: - # Will remove alpha channel after reading - slide_meta.n_channels = vips_img.bands - vips_img.hasalpha() - - slide_meta.slide_dimensions = self._get_slide_dimensions(vips_img) - if f_extension in BF_READABLE_FORMATS: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - slide_meta.channel_names = bf_reader.metadata.channel_names # None if RGB - # Need to update the n_channels based on bioformats metadata if toilet roll .ome.tiff - slide_meta.n_channels = bf_reader.metadata.n_channels - slide_meta.pixel_physical_size_xyu = bf_reader.metadata.pixel_physical_size_xyu - slide_meta.bf_pixel_type = bf_reader.metadata.bf_pixel_type - slide_meta.is_little_endian = bf_reader.metadata.is_little_endian - slide_meta.original_xml = bf_reader.metadata.original_xml - slide_meta.bf_datatype = bf_reader.metadata.bf_datatype - slide_meta.optimal_tile_wh = bf_reader.metadata.optimal_tile_wh - else: - slide_meta.pixel_physical_size_xyu = self._get_pixel_physical_size(vips_img) - - if slide_meta.is_rgb: - slide_meta.channel_names = None - - return slide_meta - - def _slide2vips_ome_one_series(self, level, *args, **kwargs): - """Use pyvips to read an ome.tiff image that has only 1 series - - Pyvips throws an error when trying to read other series - because they may have a different shape than the 1st one - https://github.com/libvips/pyvips/issues/262 - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - toilet_roll = pyvips.Image.new_from_file(self.src_f, n=-1, subifd=level-1) - page = pyvips.Image.new_from_file(self.src_f, n=1, subifd=level-1, access='random') - if page.interpretation == "srgb": - vips_slide = page - else: - page_height = page.height - pages = [toilet_roll.crop(0, y, toilet_roll.width, page_height) for - y in range(0, toilet_roll.height, page_height)] - - vips_slide = pages[0].bandjoin(pages[1:]) - if vips_slide.bands == 1: - vips_slide = vips_slide.copy(interpretation="b-w") - else: - vips_slide = vips_slide.copy(interpretation="multiband") - self.metadata.n_channels = vips_slide.bands - - return vips_slide - - def slide2vips(self, level, xywh=None, *args, **kwargs): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - if self.use_openslide: - vips_slide = pyvips.Image.new_from_file(self.src_f, level=level, access='random')[0:3] - - elif self.is_ome: - vips_slide = self._slide2vips_ome_one_series(level=level, *args, **kwargs) - - else: - try: - vips_slide = pyvips.Image.new_from_file(self.src_f, subifd=level-1, access='random') - except Exception as e: - if level > 0 and len(self.metadata.slide_dimensions) > 1: - # Pyramid image but each level is a page, not a SubIFD - vips_slide = pyvips.Image.new_from_file(self.src_f, page=level, access='random') - else: - # Regular images like png or jpeg don't have SubIFD or pages - vips_slide = pyvips.Image.new_from_file(self.src_f, access='random') - - if self.metadata.is_rgb and vips_slide.hasalpha() >= 1: - # Remove alpha channel - vips_slide = vips_slide.flatten() - - if xywh is not None: - vips_slide = vips_slide.extract_area(*xywh) - - return vips_slide - - def slide2image(self, level, xywh=None, *args, **kwargs): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level. - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - vips_slide = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) - vips_img = slide_tools.vips2numpy(vips_slide) - - return vips_img - - def _check_rgb(self, vips_img): - """Determine if image is RGB - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide. - - Returns - ------- - is_rgb : bool - Whether or not the image is RGB. - - """ - - return vips_img.interpretation == "srgb" - - def _get_channel_names(self, vips_img): - """Get names of each channel - - Get list of channel names. - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide. - - Returns - ------- - channel_names : list - List of channel naames. - - """ - - vips_fields = vips_img.get_fields() - channel_names = None - if 'n-pages' in vips_fields: - n_pages = vips_img.get("n-pages") - channel_names = [] - for i in range(n_pages): - page = pyvips.Image.new_from_file(self.src_f, page=i) - page_metadata = page.get("image-description") - - page_soup = BeautifulSoup(page_metadata, features="lxml") - cname = page_soup.find("name") - if cname is not None: - if cname.text not in channel_names: - channel_names.append(cname.text) - - return channel_names - - def _get_slide_dimensions(self, vips_img): - """Get dimensions of slide at all pyramid levels using openslide. - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide. - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - """ - - if self.use_openslide: - slide_dimensions = self._get_slide_dimensions_openslide(vips_img) - - elif self.is_ome: - slide_dimensions = self._get_slide_dimensions_ometiff(vips_img) - - else: - slide_dimensions = self._get_slide_dimensions_vips(vips_img) - - return slide_dimensions - - def _get_slide_dimensions_ometiff(self, *args): - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - return np.array(bf_reader.metadata.slide_dimensions) - - def _get_slide_dimensions_openslide(self, vips_img): - """Get dimensions of slide at all pyramid levels using openslide - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - """ - - n_levels = eval(vips_img.get('openslide.level-count')) - slide_dims = np.array([[eval(vips_img.get(f"openslide.level[{i}].width")), - eval(vips_img.get(f"openslide.level[{i}].height"))] - for i in range(n_levels)]) - - return slide_dims - - def _get_slide_dimensions_vips(self, vips_img): - """Get dimensions of slide at all pyramid levels using vips - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - """ - - vips_fields = vips_img.get_fields() - if 'n-pages' in vips_fields: - n_pages = vips_img.get("n-pages") - # all_dims = [None] * n_pages - # all_channels = [None] * n_pages - all_dims = [] - all_channels = [] - for i in range(n_pages): - try: - page = pyvips.Image.new_from_file(self.src_f, page=i) - except pyvips.error.Error as e: - print(f"error at page {i}: {e}") - - w = page.width - h = page.height - c = page.bands - - all_dims.append([w, h]) - all_channels.append(c) - # all_dims[i] = [w, h] - # all_channels[i] = c - - try: - most_common_channel_count = stats.mode(all_channels, keepdims=True)[0][0] - except: - most_common_channel_count = stats.mode(all_channels)[0][0] - - all_dims = np.array(all_dims) - keep_idx = np.where(all_channels == most_common_channel_count)[0] - slide_dims = all_dims[keep_idx] - - else: - slide_dims = [[vips_img.width, vips_img.height]] - - return np.array(slide_dims) - - def _get_pixel_physical_size(self, vips_img): - """Get resolution of slide - - Parameters - ---------- - vips_img : pyvips.Image - pyvips.Image of slide - - Returns - ------- - res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' - - Notes - ----- - If physical unit is micron, it must be u'\u00B5m', - not mu (u'\u03bcm') or u. - - """ - - res_xyu = None - if self.use_openslide: - x_res = eval(vips_img.get('openslide.mpp-x')) - y_res = eval(vips_img.get('openslide.mpp-y')) - vips_img.get('slide-associated-images') - phys_unit = MICRON_UNIT - else: - x_res = 1 - y_res = 1 - phys_unit = PIXEL_UNIT - - res_xyu = (x_res, y_res, phys_unit) - - return res_xyu - - -class FlattenedPyramidReader(VipsSlideReader): - """Read flattened pyramid using pyvips - Read slide pyramids where each page/plane is a channel in the pyramid. - An example would be one where the plane dimensions are - something like - [(600, 600), (600, 600), (600, 600), (300, 300), (300, 300), (300, 300)] - for a 3 channel image with 2 pyramid levels. It seems that bioformats - does not recognize these as pyramid images. - - """ - - def __init__(self, src_f, *args, **kwargs): - super().__init__(src_f, *args, **kwargs) - # BF Datatype may not match min/max values in the image - # e.g. datatype is uint32, but min and max are floats - self.metadata.img_dtype = None - self.metadata.img_dtype = self._get_dtype() - - - def create_metadata(self): - is_flattended_pyramid, bf_reads_flat, slide_dimensions,\ - levels_start_idx, n_channels = \ - check_flattened_pyramid_tiff(self.src_f) - - assert is_flattended_pyramid and not bf_reads_flat, "Trying to use FlattenedPyramidReader but slide is not a flattened pyramid" - - meta_name = f"{os.path.split(self.src_f)[1]}_Series(0)".strip("_") - server = VIPS_RDR - slide_meta = MetaData(meta_name, server) - slide_meta.slide_dimensions = slide_dimensions - slide_meta.n_channels = n_channels - slide_meta.levels_start_idx = levels_start_idx - - vips_img = pyvips.Image.new_from_file(self.src_f) - slide_meta.is_rgb = self._check_rgb(vips_img) - slide_meta.n_pages = self._get_page_count(vips_img) - f_extension = slide_tools.get_slide_extension(self.src_f) - if f_extension in BF_READABLE_FORMATS: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - channel_names = bf_reader.metadata.channel_names - slide_meta.pixel_physical_size_xyu = bf_reader.metadata.pixel_physical_size_xyu - slide_meta.is_little_endian = bf_reader.metadata.is_little_endian - slide_meta.original_xml = bf_reader.metadata.original_xml - slide_meta.optimal_tile_wh = bf_reader.metadata.optimal_tile_wh - slide_meta.bf_datatype = bf_reader.metadata.bf_datatype - - if len(channel_names) != n_channels: - channel_names = self._get_channel_names(vips_img, n_channels) - - slide_meta.channel_names = channel_names - - return slide_meta - - def slide2vips(self, level, xywh=None, *args, **kwargs): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - level_start = self.metadata.levels_start_idx[level] - vips_slide = None - level_shape = self.metadata.slide_dimensions[level] - for i in range(level_start, self.metadata.n_pages): - page = pyvips.Image.new_from_file(self.src_f, page=i, access='random') - page_shape = np.array([page.width, page.height]) - if not np.all(page_shape == level_shape): - continue - - if vips_slide is None: - vips_slide = page - else: - vips_slide = vips_slide.bandjoin(page) - - if xywh is not None: - vips_slide = vips_slide.extract_area(*xywh) - - if self.metadata.bf_datatype != self.metadata.img_dtype and self.metadata.img_dtype is not None: - # Min/max/response datatypes in xml don't match values image. - msg = (f"Bio-formats datatype is {self.metadata.bf_datatype}, " - f"but min/max/response values in xml are {self.metadata.img_dtype}. " - f"Converting to {self.metadata.img_dtype}" - ) - valtils.print_warning(msg) - vips_dtype = bf2vips_dtype(self.metadata.img_dtype) - vips_slide = vips_slide.copy(format=vips_dtype) - self.bf_datatype = self.metadata.img_dtype - - if vips_slide.bands == 1: - vips_slide = vips_slide.copy(interpretation="b-w") - elif vips_slide.bands == 3 and vips_slide.format == 'uchar': - vips_slide = vips_slide.copy(interpretation="srgb") - else: - vips_slide = vips_slide.copy(interpretation="multiband") - - return vips_slide - - def slide2image(self, level, xywh=None, *args, **kwargs): - vips_slide = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) - try: - vips_img = slide_tools.vips2numpy(vips_slide) - except pyvips.error.Error as e: - # Big hack for when get the error "tiff2vips: out of order read" even with random access - out_shape_wh = self.metadata.slide_dimensions[level] - msg1 = f"pyvips.error.Error: {e} when converting pvips.Image to numpy array" - msg2 = f"Will try to resize level 0 to have shape {out_shape_wh} and convert" - valtils.print_warning(msg1) - valtils.print_warning(msg2, None) - - s = np.mean(out_shape_wh/self.metadata.slide_dimensions[0]) - l0_slide = self.slide2vips(level=0, xywh=xywh, *args, **kwargs) - resized = l0_slide.resize(s) - vips_img = slide_tools.vips2numpy(resized) - if not np.all(vips_img.shape[0:2][::-1] == out_shape_wh): - vips_img = transform.resize(vips_img, output_shape=out_shape_wh[::-1], preserve_range=True) - - return vips_img - - def _get_channel_names(self, vips_img, n_channels): - vips_fields = vips_img.get_fields() - if 'n-pages' in vips_fields: - page = pyvips.Image.new_from_file(self.src_f, page=0) - page_metadata = page.get("image-description") - - page_soup = BeautifulSoup(page_metadata, features="lxml") - channels = page_soup.findAll("channel") - if len(channels) == 0: - channel_names = [f"C{i}" for i in range(n_channels)] - else: - channel_names = [None] * len(channels) - for cidx, chnl in enumerate(channels): - if chnl.has_attr("name"): - channel_names[cidx] = chnl["name"] - else: - channel_names[cidx] = f"C{cidx}" - - return channel_names - - def _get_page_count(self, vips_img): - vips_fields = vips_img.get_fields() - if 'n-pages' in vips_fields: - n_pages = vips_img.get("n-pages") - else: - n_pages = 0 - - return n_pages - - - def _get_dtype(self): - """Get Bio-Formats datatype from values in metadata. - - For example, BF metadata may have image datatype as - uint32, but in the image descriiption, min/max/resppnse, - are floats. This will determine if the slide should be cast - to a different dataatype to match values in metadata. - - """ - smallest_level = len(self.metadata.slide_dimensions) - 1 - vips_img = self.slide2vips(smallest_level) - vips_fields = vips_img.get_fields() - current_bf_dtype = vips2bf_dtype(vips_img.format) - if 'n-pages' in vips_fields: - page = pyvips.Image.new_from_file(self.src_f, page=0) - page_metadata = page.get("image-description") - - page_soup = BeautifulSoup(page_metadata, features="lxml") - channels = page_soup.findAll("channel") - response = page_soup.findAll("response") - if len(channels) > 0: - # Indica Labs tiff - dtypes = [None] * len(channels) - for i, chnl in enumerate(channels): - if chnl.has_attr("max"): - max_v = eval(chnl["max"]) - dtypes[i] = max_v.__class__.__name__ - - elif len(response) > 0: - # PerkinElmer-QPI tiff - dtypes = [None] * len(response) - for i, r in enumerate(response): - v = eval(r.getText("response")) - dtypes[i] = np.array([v]).dtype - dtypes[i] = v.__class__.__name__ - else: - return current_bf_dtype - - unique_dtypes = set(dtypes) - if len(unique_dtypes) > 1: - msg = "More than 1 datatype. Will not try to scale values" - valtils.print_warning(msg) - img_dtype = None - else: - img_dtype = dtypes[0] - - vals_are_floats = re.search("float", img_dtype) is not None - img_is_int = re.search("int", current_bf_dtype) is not None - if vals_are_floats and img_is_int: - max_v = vips_img.max() - - bf_px_num_type = FormatTools.pixelTypeFromString(self.metadata.bf_datatype) - temp_np_type, max_v_for_type = bf_to_numpy_dtype(bf_px_num_type, self.metadata.is_little_endian) - if temp_np_type.endswith('4'): - np_type = "float32" - elif temp_np_type.endswith('8'): - np_type = "float64" - - bf_type = slide_tools.NUMPY_FORMAT_BF_DTYPE[np_type] - else: - bf_type = current_bf_dtype - - return bf_type - -class CziJpgxrReader(SlideReader): - """Read slides and get metadata - - Attributes - ---------- - slide_f : str - Path to slide - - metadata : MetaData - MetaData containing some basic metadata about the slide - - series : int - Image series - - """ - def __init__(self, src_f, series=None, *args, **kwargs): - """ - Parameters - ----------- - src_f : str - Path to slide - - series : int - The series to be read. If `series` is None, the the `series` - will be set to the series associated with the largest image. - - """ - try: - from aicspylibczi import CziFile - except Exception as e: - msg = "Please install aicspylibczi" - print(e) - valtils.print_warning(msg) - - czi_reader = CziFile(src_f) - self.original_meta_dict = valtils.etree_to_dict(czi_reader.meta) - self.is_bgr = False - self.meta_list = [None] - # self._zoom_levels = None - super().__init__(src_f=src_f, *args, **kwargs) - - try: - self.meta_list = self.create_metadata() - except Exception as e: - print(e) - - self.n_series = len(self.meta_list) - if series is None: - img_areas = [np.multiply(*meta.slide_dimensions[0]) for meta in self.meta_list] - series = np.argmax(img_areas) - msg = (f"No series provided. " - f"Selecting series with largest image, " - f"which is series {series}") - - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - self._series = series - self.series = series - - def _set_series(self, series): - self._series = series - self.metadata = self.meta_list[series] - - def _get_series(self): - return self._series - - series = property(fget=_get_series, - fset=_set_series, - doc="Slide scene") - - def slide2vips(self, level, xywh=None, *args, **kwargs): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - if self.is_bgr: - vips_img = self._read_rgb(level=level, xywh=xywh, *args, **kwargs) - else: - print("currently only support RGB images when the CZI images are compressed using JPGXR") - vips_img = None - - return vips_img - - def slide2image(self, level, xywh=None, *args, **kwargs): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - vips_img = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) - np_img = warp_tools.vips2numpy(vips_img) - - return np_img - - def create_metadata(self): - """ Create and fill in a MetaData object - - Returns - ------- - metadata : MetaData - MetaData object containing metadata about slide - - """ - - - czi_reader = CziFile(self.src_f) - dims_dict = czi_reader.get_dims_shape() - - n_scenes = len(dims_dict) - meta_list = [None] * n_scenes - phys_size = self._get_pixel_physical_size() - - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - original_xml = bf_reader.metadata.original_xml - - for i in range(n_scenes): - - temp_name = f"{os.path.split(self.src_f)[1]}".strip("_") - full_name = f"{temp_name}_Scene_{i}" - - series_meta = MetaData(full_name, "aicspylibczi", series=i) - - series_meta.is_rgb = self._check_rgb() - - if series_meta.is_rgb: - n_channels = dims_dict[i]["A"][1] - series_meta.pyvips_interpretation = 'srgb' - else: - n_channels = dims_dict[i]["C"][1] - if n_channels == 1: - series_meta.pyvips_interpretation = 'b-w' - else: - series_meta.pyvips_interpretation = 'multiband' - - series_meta.n_channels = n_channels - series_meta.slide_dimensions = self._get_slide_dimensions(i) - series_meta.bf_datatype = slide_tools.CZI_FORMAT_TO_BF_FORMAT[czi_reader.pixel_type] - series_meta.channel_names = self._get_channel_names(series_meta) - series_meta.pixel_physical_size_xyu = phys_size - series_meta.original_xml = original_xml - series_meta._zoom_levels = self._get_zoom_levels(i) - - - meta_list[i] = series_meta - - - return meta_list - - def _get_img_meta_dict(self): - return self.original_meta_dict["ImageDocument"]["Metadata"]["Information"]["Image"] - - def _check_rgb(self, *args, **kwargs): - """Determine if image is RGB - - Returns - ------- - is_rgb : bool - Whether or not the image is RGB - - """ - czi_reader = CziFile(self.src_f) - self.is_bgr = czi_reader.pixel_type.startswith("bgr") - _is_rgb = czi_reader.pixel_type.startswith("rgb") - is_rgb =_is_rgb or self.is_bgr - - return is_rgb - - def _get_channel_names(self, meta, *args, **kwargs): - """Get names of each channel - - Get list of channel names - - Returns - ------- - channel_names : list - List of channel names - - """ - if meta.is_rgb: - return None - - img_dict = self._get_img_meta_dict() - channels = img_dict["Dimensions"]["Channels"] - channel_names = [None] * eval(img_dict["SizeC"]) - for chnl_attr in channels.values(): - chnl_name = chnl_attr["@Name"] - chnl_idx = eval(chnl_attr["@Id"].split(":")[1]) - channel_names[chnl_idx] = chnl_name - - return channel_names - - - def _get_slide_dimensions(self, scene=0, *args, **kwargs): - """Get dimensions of slide at all pyramid levels - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - """ - # if self._zoom_levels is None: - zoom_levels = self._get_zoom_levels(scene) - - czi_reader = CziFile(self.src_f) - scene_bbox = czi_reader.get_all_scene_bounding_boxes()[scene] - scence_l0_wh = np.array([scene_bbox.w, scene_bbox.h]) - slide_dimensions = np.round(scence_l0_wh*zoom_levels[..., np.newaxis]).astype(int) - - return slide_dimensions - - def _get_zoom_levels(self, scene=0): - - img_dict = self._get_img_meta_dict() - pyramid_info = img_dict["Dimensions"]["S"]["Scenes"]["Scene"][scene]["PyramidInfo"] - n_levels = eval(pyramid_info["PyramidLayersCount"]) - downsampling = eval(pyramid_info["MinificationFactor"]) - zoom_levels = (1/downsampling)**(np.arange(0, n_levels)) - - return zoom_levels - - def _get_pixel_physical_size(self, *args, **kwargs): - """Get resolution of slide - - Returns - ------- - res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' - - Notes - ----- - If physical unit is micron, it must be u'\u00B5m', - not mu (u'\u03bcm') or u. - - """ - - physical_sizes = self.original_meta_dict["ImageDocument"]["Metadata"]["Scaling"]["Items"]["Distance"] - - physical_size_xyu = [None] * 3 - physical_unit = physical_sizes[0]["DefaultUnitFormat"] - physical_size_xyu[2] = physical_unit - - if physical_unit == u'\u00B5m': - physical_scaling = 10**6 - elif physical_unit == "mm": - physical_scaling = 10**3 - elif physical_unit == "cm": - physical_scaling = 10**2 - else: - physical_scaling = 1 - - for ps in physical_sizes: - if ps["@Id"] == "X": - physical_size_xyu[0] = eval(ps["Value"])*physical_scaling - elif ps["@Id"] == "Y": - physical_size_xyu[1] = eval(ps["Value"])*physical_scaling - - return tuple(physical_size_xyu) - - - def _read_rgb(self, level=0, xywh=None, *args, **kwargs): - - czi_reader = CziFile(self.src_f) - img_dict = self._get_img_meta_dict() - bg_hex = img_dict["Dimensions"]["Channels"]["Channel"]["Color"] - bg_rgba = valtils.hex_to_rgb(bg_hex)[::-1] - - out_shape_wh = self.metadata.slide_dimensions[0] - tile_bboxes = czi_reader.get_all_mosaic_tile_bounding_boxes(C=0) - - vips_img = pyvips.Image.black(*out_shape_wh, bands=self.metadata.n_channels) + bg_rgba[0:3] - print(f"Building CZI mosaic for {valtils.get_name(self.src_f)}") - for tile_info, tile_bbox in tqdm(tile_bboxes.items()): - m = tile_info.m_index - x = tile_bbox.x - y = tile_bbox.y - - np_tile, tile_dims = czi_reader.read_image(M=m) - - slice_dims = [v - 1 for k, v in tile_dims if k not in ["Y", "X", "A"]] - - np_tile = np_tile[(*slice_dims, ...)] - if self.is_bgr: - np_tile = np_tile[..., ::-1] - - vips_tile = warp_tools.numpy2vips(np_tile) - vips_img = vips_img.insert(vips_tile, *(x, y)) - - if xywh is not None: - vips_img = vips_img.extract_area(*xywh) - - if level != 0: - scaling = self.metadata._zoom_levels[level] - vips_img = warp_tools.rescale_img(vips_img, scaling) - - vips_img = vips_img.copy(interpretation="srgb") - np_type = slide_tools.CZI_FORMAT_TO_BF_FORMAT[czi_reader.pixel_type] - vips_type = slide_tools.NUMPY_FORMAT_VIPS_DTYPE[np_type] - vips_img = vips_img.cast(vips_type) - - return vips_img - - -class ImageReader(SlideReader): - """Read image using scikit-image - - """ - - def __init__(self, src_f, *args, **kwargs): - super().__init__(src_f, *args, **kwargs) - self.metadata = self.create_metadata() - - def create_metadata(self): - server = IMG_RDR - meta_name = f"{os.path.split(self.src_f)[1]}_Series(0)".strip("_") - slide_meta = MetaData(meta_name, server) - pil_img = Image.open(self.src_f) - - slide_meta.is_rgb = self._check_rgb(pil_img) - slide_meta.channel_names = self._get_channel_names(pil_img) - slide_meta.n_channels = self._get_n_channels(pil_img) - slide_meta.pixel_physical_size_xyu = [1, 1, PIXEL_UNIT] - slide_meta.slide_dimensions = self._get_slide_dimensions(pil_img) - - f_extension = slide_tools.get_slide_extension(self.src_f) - if f_extension in BF_READABLE_FORMATS: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - slide_meta.original_xml = bf_reader.metadata.original_xml - slide_meta.bf_datatype = bf_reader.metadata.bf_datatype - pil_img.close() - - return slide_meta - - def slide2vips(self, xywh=None, *args, **kwargs): - img = self.slide2image(xywh=xywh, *args, **kwargs) - vips_img = slide_tools.numpy2vips(img) - - return vips_img - - def slide2image(self, xywh=None, *args, **kwargs): - img = io.imread(self.src_f) - - if xywh is not None: - xywh = np.array(xywh) - start_c, start_r = xywh[0:2] - end_c, end_r = xywh[0:2] + xywh[2:] - img = img[start_r:end_r, start_c:end_c] - - return img - - def _get_slide_dimensions(self, pil_img, *args, **kwargs): - """ - """ - img_dims = np.array([[pil_img.width, pil_img.height]]) - - return img_dims - - def _get_n_channels(self, pil_img, *args, **kwargs): - - n_channels = len(pil_img.getbands()) - - return n_channels - - def _check_rgb(self, pil_img, *args, **kwargs): - - is_rgb = pil_img.mode == 'RGB' - - return is_rgb - - def _get_channel_names(self, pil_img, *args, **kwargs): - is_rgb = pil_img.mode == 'RGB' - if is_rgb: - channel_names = None - else: - channel_names = pil_img.getbands() - return channel_names - - -def get_slide_reader(src_f, series=None): - """Get appropriate SlideReader - - If a slide can be read by openslide and bioformats, VipsSlideReader will be used - because it can be opened as a pyvips.Image. More common formats, like png, jpeg, - etc... will be opened with scikit-image. Everything else will be opened with - Bioformats. - - Parameters - ---------- - src_f : str - Path to slide - - series : int, optional - The series to be read. If `series` is None, the the `series` - will be set to the series associated with the largest image. - In cases where there is only 1 image in the file, `series` - will be 0. - - Returns - ------- - reader: SlideReader - SlideReader class that can read the slide and and convert them to - images or pyvips.Images at the specified level and series. They - also contain a `MetaData` object that contains information about - the slide, like dimensions at each level, physical units, etc... - - Notes - ----- - pyvips will be used to open ome-tiff images when `series` is 0 - - """ - - init_jvm() - - f_extension = slide_tools.get_slide_extension(src_f) - what_img = imghdr.what(src_f) - can_use_openslide = check_to_use_openslide(src_f) - can_only_use_openslide = f_extension in OPENSLIDE_ONLY - if can_only_use_openslide and not can_use_openslide: - msg = (f"file {os.path.split(src_f)[1]} can only be read by OpenSlide, " - f"which is required to open files with the follwing extensions: {', '.join(OPENSLIDE_ONLY)}. " - f"However, OpenSlide cannot be found. Unable to read this slide." - ) - - valtils.print_warning(msg) - - can_use_bf = f_extension in BF_READABLE_FORMATS and not can_only_use_openslide - is_tiff = f_extension == ".tiff" or f_extension == ".tif" - can_use_skimage = ".".join(f_extension.split(".")[1:]) == what_img and not is_tiff - try: - pyvips.Image.new_from_file(src_f) - can_use_pyvips = True - except: - can_use_pyvips = False - - if not can_use_openslide and not can_use_bf and not can_use_skimage and not can_use_pyvips: - fail_msg = f"Can't find reader to open {os.path.split(src_f)[1]}. May need to create a new one by subclassing SlideReader. Returning None" - valtils.print_warning(fail_msg) - - return None - - if can_use_skimage and not can_use_pyvips: - reader = ImageReader - return reader - - n_series = 1 - is_rgb = None - is_czi_jpgxr = False - if can_use_bf: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(src_f) - - n_series = bf_reader.n_series - is_ometiff = re.search("ome-tiff", bf_reader.metadata.server.lower()) is not None - is_rgb = bf_reader.metadata.is_rgb - - if f_extension == ".czi": - try: - with valtils.HiddenPrints(): - bf_reader.slide2vips(level=0, xywh=(0, 0, 5, 5)) - except Exception as e: - can_use_bf = False - # Sometimes bioformats has issues reading CZI with JPGXR compression - czi = CziFile(src_f) - comp_tree = czi.meta.findall(".//OriginalCompressionMethod")[0] - is_czi_jpgxr = comp_tree.text.lower() == "jpgxr" - - if series is None: - series = bf_reader.series - else: - is_ometiff = False - - if series is None: - series = 0 - - if is_tiff and not is_ometiff: - is_flattened_tiff, bf_reads_flat = check_flattened_pyramid_tiff(src_f)[0:2] - - else: - is_flattened_tiff = False - - if is_flattened_tiff: - if not bf_reads_flat: - reader = FlattenedPyramidReader - else: - reader = BioFormatsSlideReader - - elif can_only_use_openslide: - # E.g. .mrxs - reader = VipsSlideReader - - elif is_ometiff: - if series == 0 and n_series == 1 and is_rgb and is_rgb is not None: - # Seems pvips can only read ome.tiff if there is 1 series. - # Converting a multichannel pyvips.Image is very slow, but is fast for RGB - reader = VipsSlideReader - else: - reader = BioFormatsSlideReader - - elif can_use_pyvips: - # Use pyvips to open regular formats, like png, jpeg, bmp, etc... - reader = VipsSlideReader - - elif can_use_bf: - # Use Bioformats for images that can't be read by pyvips or skimage - - reader = BioFormatsSlideReader - - elif f_extension == ".czi" and is_czi_jpgxr: - msg = "CZI was compressed with JPGXR and could not be opened with Bioformats. Using CziJpgxrReader, which is experimental" - valtils.print_warning(msg) - reader = CziJpgxrReader - - else: - valtils.print_warning(fail_msg) - reader = None - - return reader - - -# Write slides to ome.tiff # - -def remove_control_chars(s): - """Remove control characters - - Control characters shouldn't be in some strings, like channel names. - This will remove them - - Parameters - ---------- - s : str - String to check - - Returns - ------- - control_char_removed : str - `s` with control characters removed. - - """ - - control_chars = ''.join(map(chr, itertools.chain(range(0x00,0x20), range(0x7f,0xa0)))) - control_char_re = re.compile('[%s]' % re.escape(control_chars)) - control_char_removed = control_char_re.sub('', s) - - return control_char_removed - - -def get_shape_xyzct(shape_wh, n_channels): - """Get image shape in XYZCT format - - Parameters - ---------- - shape_wh : tuple of int - Width and heigth of image - - n_channels : int - Number of channels in the image - - Returns - ------- - xyzct : tuple of int - XYZCT shape of the image - - """ - - xyzct = (*shape_wh, 1, n_channels, 1) - return xyzct - - -def create_channel(channel_id, name=None, color=None): - """Create an ome-xml channel - - Parameters - ---------- - channel_id : int - Channel number - - name : str, optinal - Channel name - - color : tuple of int - Channel color - - Returns - ------- - new_channel : ome_types.model.channel.Channel - Channel object - - """ - - if name is not None: - unicode_name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore') - decoded_name = unicode_name.decode('unicode_escape') - decoded_name = remove_control_chars(decoded_name) - - else: - decoded_name = None - - new_channel = ome_types.model.Channel(id=f"Channel:{channel_id}") - if name is not None: - new_channel.name = decoded_name - if color is not None: - - if len(color) == 3: - new_channel.color = tuple([*color, 1]) - elif len(color) == 4: - if color[3] == 0: - # color has alpha, won't be shown because it's 0 - color = tuple([*color[:3], 1]) - new_channel.color = tuple(color) - - return new_channel - - -@valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, channel_names=None, colormap=None): - """Create new ome-xmml object - - Parameters - ------- - shape_xyzct : tuple of int - XYZCT shape of image - - bf_dtype : str - String format of Bioformats datatype - - is_rgb : bool - Whether or not the image is RGB - - pixel_physical_size_xyu : tuple, optional - Physical size per pixel and the unit. - - channel_names : list, optional - List of channel names. - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - - Returns - ------- - new_ome : ome_types.model.OME - ome_types.model.OME object containing ome-xml metadata - - """ - - x, y, z, c, t = shape_xyzct - new_ome = ome_types.OME() - new_img = ome_types.model.Image( - id="Image:0", - pixels=ome_types.model.Pixels( - id="Pixels:0", - size_x=x, - size_y=y, - size_z=z, - size_c=c, - size_t=t, - type=bf_dtype, - dimension_order='XYZCT', - metadata_only=True - ) - ) - - if pixel_physical_size_xyu is not None: - phys_x, phys_y, phys_u = pixel_physical_size_xyu - new_img.pixels.physical_size_x = phys_x - new_img.pixels.physical_size_x_unit = phys_u - new_img.pixels.physical_size_y = phys_y - new_img.pixels.physical_size_y_unit = phys_u - - if is_rgb: - rgb_channel = ome_types.model.Channel(id='Channel:0:0', samples_per_pixel=3) - new_img.pixels.channels = [rgb_channel] - - else: - - if channel_names is None: - channel_names = [f"C{i}" for i in range(c)] - - default_colors = slide_tools.get_matplotlib_channel_colors(c) - default_colormap = {channel_names[i]:tuple(default_colors[i]) for i in range(c)} - if colormap is not None: - if len(colormap) != c: - colormap = default_colormap - msg = "Number of colors in colormap not same as the number of channels. Using default colormap" - valtils.print_warning(msg) - else: - colormap = default_colormap - - channels = [create_channel(i, name=channel_names[i], color=colormap[channel_names[i]]) for i in range(c)] - new_img.pixels.channels = channels - - new_ome = ome_types.model.OME() - new_ome.images.append(new_img) - - return new_ome - - -@valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def update_xml_for_new_img(current_ome_xml_str, new_xyzct, bf_dtype, is_rgb, series, pixel_physical_size_xyu=None, channel_names=None, colormap=None): - """Update dimensions ome-xml metadata - - Used to create a new ome-xmlthat reflects changes in an image, such as its shape - - If `current_ome_xml_str` is invalid or None, a new ome-xml will be created - - Parameters - ------- - current_ome_xml_str : str - ome-xml string that needs to be updated - - new_xyzct : tuple of int - XYZCT shape of image - - bf_dtype : str - String format of Bioformats datatype - - is_rgb : bool - Whether or not the image is RGB - - pixel_physical_size_xyu : tuple, optional - Physical size per pixel and the unit. - - channel_names : list, optional - List of channel names. - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added - - Returns - ------- - new_ome : ome_types.model.OME - ome_types.model.OME object containing ome-xml metadata - - """ - - og_valid_xml = True - og_ome = None - if current_ome_xml_str is not None: - try: - elementTree.fromstring(current_ome_xml_str) - og_ome = ome_types.from_xml(current_ome_xml_str, parser="xmlschema") - - if colormap is None: - # Get original channel colors - img = og_ome.images[series] - colormap = {c.name: c.color.as_rgb_tuple() for c in img.pixels.channels} - all_rgb = set(list(colormap.values())) - nc = len(img.pixels.channels) - if len(all_rgb) == 1 and nc > 1: - # Original image didn't have colors - default_colors = slide_tools.get_matplotlib_channel_colors(nc) - colormap = {img.pixels.channels[i].name: tuple(default_colors[i]) for i in range(nc)} - - except elementTree.ParseError as e: - print(e) - msg = "xml in original file is invalid or missing. Will create one" - valtils.print_warning(msg) - og_valid_xml = False - - else: - og_valid_xml = False - - temp_new_ome = create_ome_xml(new_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu, channel_names, colormap=colormap) - - if og_valid_xml: - new_ome = og_ome.copy() - new_ome.images = temp_new_ome.images - else: - new_ome = temp_new_ome - - return new_ome - - - -@valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def warp_and_save_slide(src_f, dst_f, transformation_src_shape_rc, transformation_dst_shape_rc, - aligned_slide_shape_rc, M=None, dxdy=None, - level=0, series=None, interp_method="bicubic", - bbox_xywh=None, bg_color=None, colormap=None, - tile_wh=None, compression="lzw"): - - """ Warp and save a slide - - Warp slide according to `M` and/or `dxdy`, then save as an ome.tiff image. - - Parameters - ---------- - src_f : str - Path to slide - - dst_f : str - Path indicating where the warped slide will be saved. - - transformation_src_shape_rc : (int, int) - Shape of the image used to find the rigid transformations (row, col) - - transformation_dst_shape_rc : (int, int) - Shape of image with shape `in_shape_rc`, after being warped, - i.e. the shape of the registered image. - - aligned_slide_shape_rc : (int, int) - Shape of the warped slide (row, col) - - M : ndarray, optional - 3x3 Affine transformation matrix to perform rigid warp. - Found by aligning the target/fixed image to source/moving image. - If `M` was found the other way around, then `M` will need to be inverted - using np.linalg.inv() - - dxdy : list, optional - A list containing the x-axis (column) displacement and y-axis (row) displacements. - Found by aligning the target/fixed image to source/moving image. - If `dxdy` was found the other way around, then `dxdy` will need to be inverted, - which can be done using `warp_tools.get_inverse_field` - - level : int, optional - Pyramid level to warp an save - - series : int, optional - Series number of image - - interp_method : str, optional - Interpolation method - - bbox_xywh : tuple - Bounding box to crop warped slide. Should be in reference the - warped slide. - - bg_color : optional, list - Background color, if `None`, then the background color will be black - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added - - tile_wh : int, optional - Tile width and height used to save image - - compression : str - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See https://libvips.github.io/pyvips/enums.html#pyvips.enums.ForeignTiffCompression for more details. - - """ - - warped_slide = slide_tools.warp_slide(src_f=src_f, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - aligned_slide_shape_rc=aligned_slide_shape_rc, - M=M, - dxdy=dxdy, - level=level, - series=series, - interp_method=interp_method, - bbox_xywh=bbox_xywh, - bg_color=bg_color) - - # Get OMEXML and update with new dimensions - reader_cls = get_slide_reader(src_f, series=series) # Get slide reader class - reader = reader_cls(src_f, series=series) # Get reader - slide_meta = reader.metadata - if slide_meta.pixel_physical_size_xyu[2] == PIXEL_UNIT: - px_phys_size = None - else: - px_phys_size = reader.scale_physical_size(level) - - bf_dtype = vips2bf_dtype(warped_slide.format) - out_xyczt = get_shape_xyzct((warped_slide.width, warped_slide.height), warped_slide.bands) - ome_xml_obj = update_xml_for_new_img(slide_meta.original_xml, - new_xyzct=out_xyczt, - bf_dtype=bf_dtype, - is_rgb=reader.metadata.is_rgb, - series=series, - pixel_physical_size_xyu=px_phys_size, - channel_names=reader.metadata.channel_names, - colormap=colormap - ) - - ome_xml = ome_xml_obj.to_xml() - if tile_wh is None: - tile_wh = slide_meta.optimal_tile_wh - if level != 0: - down_sampling = np.mean(slide_meta.slide_dimensions[level]/slide_meta.slide_dimensions[0]) - tile_wh = int(np.round(tile_wh*down_sampling)) - tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 - if tile_wh < 16: - tile_wh = 16 - if np.any(np.array(out_xyczt[0:2]) < tile_wh): - tile_wh = min(out_xyczt[0:2]) - - save_ome_tiff(warped_slide, dst_f=dst_f, ome_xml=ome_xml, - tile_wh=tile_wh, compression=compression) - - -def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=1024, compression="lzw"): - """Save an image in the ome.tiff format using pyvips - - Parameters - --------- - img : pyvips.Image, ndarray - Image to be saved. If a numpy array is provided, it will be converted - to a pyvips.Image. - - ome_xml : str, optional - ome-xml string describing image's metadata. If None, it will be createdd - - tile_wh : int - Tile shape used to save `img`. Used to create a square tile, so `tile_wh` - is both the width and height. - - compression : str - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See pyips for more details. - - """ - - dst_dir = os.path.split(dst_f)[0] - pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - - if not isinstance(img, pyvips.vimage.Image): - img = slide_tools.numpy2vips(img) - - if img.format in ["float", "double"] and compression != "lzw": - msg = f"Image has type {img.format} but compression method {compression} will convert image to uint8. To avoid this, compression is being changed to 'lzw" - compression = "lzw" - valtils.print_warning(msg) - - dst_f_extension = slide_tools.get_slide_extension(dst_f) - if dst_f_extension != ".ome.tiff": - dst_dir, out_f = os.path.split(dst_f) - new_out_f = out_f.split(dst_f_extension)[0] + ".ome.tiff" - new_dst_f = os.path.join(dst_dir, new_out_f) - msg = f"{out_f} is not an ome.tiff. Changing dst_f to {new_dst_f}" - valtils.print_warning(msg) - dst_f = new_dst_f - - # Get ome-xml metadata # - xyzct = get_shape_xyzct((img.width, img.height), img.bands) - is_rgb = img.interpretation == "srgb" - bf_dtype = vips2bf_dtype(img.format) - if ome_xml is None: - # Create minimal ome-xml - ome_xml_obj = create_ome_xml(xyzct, bf_dtype, is_rgb) - else: - # Verify that vips image and ome-xml match - ome_xml_obj = ome_types.from_xml(ome_xml, parser="xmlschema") - ome_img = ome_xml_obj.images[0].pixels - match_dict = {"same_x": ome_img.size_x == img.width, - "same_y": ome_img.size_y == img.height, - "same_c": ome_img.size_c == img.bands, - "same_type": ome_img.type.name.lower() == bf_dtype - } - - if not all(list(match_dict.values())): - msg = f"mismatch in ome-xml and image: {str(match_dict)}. Will create ome-xml" - valtils.print_warning(msg) - ome_xml_obj = create_ome_xml(xyzct, bf_dtype, is_rgb) - - ome_xml_obj.creator = f"pyvips version {pyvips.__version__}" - ome_metadata = ome_xml_obj.to_xml() - - # Save ome-tiff using vips # - image_height = img.height - image_bands = img.bands - if is_rgb: - img = img.copy(interpretation="srgb") - else: - img = pyvips.Image.arrayjoin(img.bandsplit(), across=1) - img = img.copy(interpretation="b-w") - - img.set_type(pyvips.GValue.gint_type, "page-height", image_height) - img.set_type(pyvips.GValue.gstr_type, "image-description", ome_metadata) - - - # Set up progress bar # - bar_len = 100 - if is_rgb: - total = 100 - else: - total = 100*image_bands - tic = time.time() - - save_ome_tiff.n_complete = -1 - save_ome_tiff.current_im = None - def eval_handler(im, progress): - if save_ome_tiff.current_im != progress.im: - save_ome_tiff.n_complete += 1 - save_ome_tiff.current_im = progress.im - count = save_ome_tiff.n_complete*100 + progress.percent - filled_len = int(round(bar_len * count / float(total))) - percents = round(100.0 * count / float(total), 1) - bar = '=' * filled_len + '-' * (bar_len - filled_len) - toc = time.time() - processing_time_h = round((toc - tic)/(60), 3) - - sys.stdout.write('[%s] %s%s %s %s %s\r' % (bar, percents, '%', 'in', processing_time_h, "minutes")) - sys.stdout.flush() - - try: - img.set_progress(True) - img.signal_connect("eval", eval_handler) - except pyvips.error.Error: - msg = "Unable to create progress bar for pyvips. May need to update libvips to >= 8.11" - valtils.print_warning(msg) - - print(f"saving {dst_f} ({img.width} x {image_height} and {image_bands} channels)") - - # Write image # - tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 - if np.any(np.array(xyzct[0:2]) < tile_wh): - # Image is smaller than the tile # - min_dim = min(xyzct[0:2]) - tile_wh = int((min_dim - min_dim %16)) - if tile_wh < 16: - tile_wh = 16 - - print("") - img.tiffsave(dst_f, compression=compression, tile=True, - tile_width=tile_wh, tile_height=tile_wh, - pyramid=True, subifd=True, bigtiff=True, lossless=True) - - # Print total time to completion # - toc = time.time() - processing_time_seconds = toc-tic - processing_time, processing_time_unit = valtils.get_elapsed_time_string(processing_time_seconds) - - bar = '=' * bar_len - sys.stdout.write('[%s] %s%s %s %s %s\r' % (bar, 100.0, '%', 'in', processing_time, processing_time_unit)) - sys.stdout.flush() - sys.stdout.write('\nComplete\n') - print("") - - -@valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def convert_to_ome_tiff(src_f, dst_f, level, series=None, xywh=None, - colormap=None, tile_wh=None, compression="lzw"): - """Convert an image to an ome.tiff image - - Saves a new copy of the image as a tiled pyramid ome.tiff with valid ome-xml. - Uses pyvips to save the image. Currently only writes a single series. - - Parameters - ---------- - src_f : str - Path to image to be converted - - dst_f : str - Path indicating where the image should be saved. - - level : int - Pyramid level to be converted. - - series : str - Series to be converted. - - xywh : tuple of int, optional - The region of the slide to be converted. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added - - tile_wh : int - Tile shape used to save the image. Used to create a square tile, - so `tile_wh` is both the width and height. - - compression : str - Compression method used to save ome.tiff . Default is lzw, but can also - be jpeg or jp2k. See pyips for more details. - - """ - - dst_dir = os.path.join(dst_f)[0] - pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - - reader_cls = get_slide_reader(src_f, series) - reader = reader_cls(src_f, series=series) - slide_meta = reader.metadata - if series is None: - series = reader.metadata.series - - vips_img = reader.slide2vips(level=level, series=series, xywh=xywh) - bf_dtype = vips2bf_dtype(vips_img.format) - out_xyczt = get_shape_xyzct((vips_img.width, vips_img.height), slide_meta.n_channels) - - if slide_meta.pixel_physical_size_xyu[2] == PIXEL_UNIT: - px_phys_size = None - else: - if level == 0: - px_phys_size = slide_meta.pixel_physical_size_xyu - else: - px_phys_size = reader.scale_physical_size(level) - - ome_obj = update_xml_for_new_img(slide_meta.original_xml, - new_xyzct=out_xyczt, - bf_dtype=bf_dtype, - is_rgb=slide_meta.is_rgb, - series=series, - pixel_physical_size_xyu=px_phys_size, - channel_names=slide_meta.channel_names, - colormap=colormap - ) - - ome_obj.creator = f"pyvips version {pyvips.__version__}" - ome_xml_str = ome_obj.to_xml() - if tile_wh is None: - tile_wh = slide_meta.optimal_tile_wh - - if tile_wh > MAX_TILE_SIZE: - tile_wh = MAX_TILE_SIZE - - save_ome_tiff(vips_img, dst_f, ome_xml_str, tile_wh=tile_wh, compression=compression) diff --git a/examples/acrobat_2023/valis/slide_tools.py b/examples/acrobat_2023/valis/slide_tools.py deleted file mode 100644 index dd13dedf..00000000 --- a/examples/acrobat_2023/valis/slide_tools.py +++ /dev/null @@ -1,397 +0,0 @@ -""" -Methods to work with slides, after being opened using slide_io - -""" - -import os -import pyvips -import numpy as np -import colour -from matplotlib import cm -import re -import imghdr -import sys -from collections import Counter -from . import warp_tools -from . import slide_io -from . import viz -from . import preprocessing - -IHC_NAME = "brightfield" -IF_NAME = "fluorescence" -MULTI_MODAL_NAME = "multi" -TYPE_IMG_NAME = "img" -TYPE_SLIDE_NAME = "slide" -BG_AUTO_FILL_STR = "auto" - -NUMPY_FORMAT_VIPS_DTYPE = { - 'uint8': 'uchar', - 'int8': 'char', - 'uint16': 'ushort', - 'int16': 'short', - 'uint32': 'uint', - 'int32': 'int', - 'float32': 'float', - 'float64': 'double', - 'complex64': 'complex', - 'complex128': 'dpcomplex', - } - - -VIPS_FORMAT_NUMPY_DTYPE = { - 'uchar': np.uint8, - 'char': np.int8, - 'ushort': np.uint16, - 'short': np.int16, - 'uint': np.uint32, - 'int': np.int32, - 'float': np.float32, - 'double': np.float64, - 'complex': np.complex64, - 'dpcomplex': np.complex128, -} - -NUMPY_FORMAT_BF_DTYPE = {'uint8': 'uint8', - 'int8': 'int8', - 'uint16': 'uint16', - 'int16': 'int16', - 'uint32': 'uint32', - 'int32': 'int32', - 'float32': 'float', - 'float64': 'double'} - -CZI_FORMAT_NUMPY_DTYPE = { - "gray8": "uint8", - "gray16": "uint16", - "gray32": "uint32", - "gray32float": "float32", - "bgr24": "uint8", - "bgr48": "uint16", - "invalid": "uint8", -} - -CZI_FORMAT_TO_BF_FORMAT = {k:NUMPY_FORMAT_BF_DTYPE[v] for k,v in CZI_FORMAT_NUMPY_DTYPE.items()} - -BF_FORMAT_NUMPY_DTYPE = {v:k for k, v in NUMPY_FORMAT_BF_DTYPE.items()} - - -def vips2numpy(vi): - """ - https://github.com/libvips/pyvips/blob/master/examples/pil-numpy-pyvips.py - - """ - - img = np.ndarray(buffer=vi.write_to_memory(), - dtype=VIPS_FORMAT_NUMPY_DTYPE[vi.format], - shape=[vi.height, vi.width, vi.bands]) - if vi.bands == 1: - img = img[..., 0] - - return img - - -def numpy2vips(a, pyvips_interpretation=None): - """ - - """ - - if a.ndim > 2: - height, width, bands = a.shape - else: - height, width = a.shape - bands = 1 - - linear = a.reshape(width * height * bands) - if linear.dtype.byteorder == ">": - #vips seems to expect the array to be little endian, but `a` is big endian - linear.byteswap(inplace=True) - - vi = pyvips.Image.new_from_memory(linear.data, width, height, bands, - NUMPY_FORMAT_VIPS_DTYPE[a.dtype.name]) - - if pyvips_interpretation is not None: - vi = vi.copy(interpretation=pyvips_interpretation) - return vi - - -def get_slide_extension(src_f): - """Get slide format - - Parameters - ---------- - src_f : str - Path to slide - - Returns - ------- - slide_format : str - Slide format. - - """ - - f = os.path.split(src_f)[1] - if re.search(".ome.tif", f): - format_split = -2 - else: - format_split = -1 - slide_format = "." + ".".join(f.split(".")[format_split:]) - - return slide_format - - -def get_img_type(img_f): - """Determine if file is a slide or an image - - Parameters - ---------- - img_f : str - Path to image - - Returns - ------- - kind : str - Type of file, either 'image', 'slide', or None if they type - could not be determined - - """ - - if os.path.isdir(img_f): - return None - - f_extension = get_slide_extension(img_f) - what_img = imghdr.what(img_f) - if slide_io.BF_READABLE_FORMATS is None: - slide_io.init_jvm() - - can_use_bf = f_extension in slide_io.BF_READABLE_FORMATS - can_use_openslide = slide_io.check_to_use_openslide(img_f) - is_tiff = f_extension == ".tiff" or f_extension == ".tif" - can_use_skimage = ".".join(f_extension.split(".")[1:]) == what_img and not is_tiff - - kind = None - if can_use_skimage: - kind = TYPE_IMG_NAME - elif can_use_bf or can_use_openslide: - kind = TYPE_SLIDE_NAME - - return kind - - -def determine_if_staining_round(src_dir): - """Determine if path contains an image split across different files - - Checks to see if files in the directory belong to a single image. - An example is a folder of several .ndpi images, with a single .ndpis - file. This method assumes that if there is a single file that has - a different extension than the other images then the path contains - a set of files (e.g. 3 .npdi images) that can be read using a - single file (e.g. 1 .ndpis image). - - Parameters - ---------- - src_dir : str - Path to directory containing the images - - Returns - ------- - multifile_img : bool - Whether or not the path contains an image split across different files - - master_img_f : str - Name of file that can be used to open all images in `src_dir` - - """ - - if not os.path.isdir(src_dir): - multifile_img = False - master_img_f = None - else: - - f_list = [os.path.join(src_dir, f) for f in os.listdir(src_dir) if get_img_type(os.path.join(src_dir, f)) is not None and not f.startswith(".")] - extensions = [get_slide_extension(f) for f in f_list] - format_counts = Counter(extensions) - format_count_values = list(format_counts.values()) - n_formats = len(format_count_values) - if n_formats > 1 and min(format_count_values) == 1: - multifile_img = True - master_img_format = list(format_counts.keys())[np.argmin(format_count_values)] - master_img_file_idx = extensions.index(master_img_format) - master_img_f = f_list[master_img_file_idx] - else: - multifile_img = False - master_img_f = None - - return multifile_img, master_img_f - - -def um_to_px(um, um_per_px): - """Conver mircon to pixel - """ - return um * 1/um_per_px - - -def warp_slide(src_f, transformation_src_shape_rc, transformation_dst_shape_rc, - aligned_slide_shape_rc, M=None, dxdy=None, - level=0, series=None, interp_method="bicubic", - bbox_xywh=None, bg_color=None): - """ Warp a slide - - Warp slide according to `M` and/or `non_rigid_dxdy` - - Parameters - ---------- - src_f : str - Path to slide - - transformation_src_shape_rc : (N, M) - Shape of the image used to find the transformations - - transformation_dst_shape_rc : (int, int) - Shape of image with shape `in_shape_rc`, after being warped, - i.e. the shape of the registered image. - - aligned_slide_shape_rc : (int, int) - Shape of the warped slide. - - scaled_out_shape_rc : optional, (int, int) - Shape of scaled image (with shape out_shape_rc) after warping - - M : ndarray, optional - 3x3 Affine transformation matrix to perform rigid warp - - dxdy : ndarray, optional - An array containing the x-axis (column) displacement, - and y-axis (row) displacement applied after the rigid transformation - - level : int, optional - Pyramid level - - series : int, optional - Series number - - interp_method : str, optional - - bbox_xywh : tuple - Bounding box to crop warped slide. Should be in refernce the - warped slide - - Returns - ------- - vips_warped : pyvips.Image - A warped copy of the slide specified by `src_f` - - """ - reader_cls = slide_io.get_slide_reader(src_f, series=series) - reader = reader_cls(src_f, series=series) - if series is None: - series = reader.series - - vips_slide = reader.slide2vips(level=level, series=series) - if M is None and dxdy is None: - return vips_slide - - vips_warped = warp_tools.warp_img(img=vips_slide, M=M, bk_dxdy=dxdy, - transformation_dst_shape_rc=transformation_dst_shape_rc, - out_shape_rc=aligned_slide_shape_rc, - transformation_src_shape_rc=transformation_src_shape_rc, - bbox_xywh=bbox_xywh, - bg_color=bg_color, - interp_method=interp_method) - - return vips_warped - - -def get_matplotlib_channel_colors(n_colors, name="gist_rainbow", min_lum=0.5, min_c=0.2): - """Get channel colors using matplotlib colormaps - - Parameters - ---------- - n_colors : int - Number of colors needed. - - name : str - Name of matplotlib colormap - - min_lum : float - Minimum luminosity allowed - - min_c : float - Minimum colorfulness allowed - - Returns - -------- - channel_colors : ndarray - RGB values for each of the `n_colors` - - """ - n = 200 - if n_colors > n: - n = n_colors - all_colors = cm.get_cmap(name)(np.linspace(0, 1, n))[..., 0:3] - - # Only allow bright colors # - jch = preprocessing.rgb2jch(all_colors) - all_colors = all_colors[(jch[..., 0] >= min_lum) & (jch[..., 1] >= min_c)] - channel_colors = viz.get_n_colors(all_colors, n_colors) - channel_colors = (255*channel_colors).astype(np.uint8) - - return channel_colors - - -def turbo_channel_colors(n_colors): - """Channel colors using the Turbo colormap - - Gets channel colors from the Turbo colormap - https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html - These are not percepually uniform, but are better than jet. - - Parameters - ---------- - n_colors : int - Number of colors needed. - - Returns - -------- - channel_colors : ndarray - RGB values for each of the `n_colors` - - """ - - turbo = viz.turbo_cmap()[40:-40] - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - cam16 = colour.convert(turbo, 'sRGB', "CAM16UCS") - cam16[..., 0] *= 1.1 - brighter_turbo = colour.convert(cam16, "CAM16UCS", 'sRGB') - - brighter_turbo = np.clip(brighter_turbo, 0, 1) - - channel_colors = viz.get_n_colors(brighter_turbo, n_colors) - channel_colors = (255*channel_colors).astype(np.uint8) - - return channel_colors - - -def perceptually_uniform_channel_colors(n_colors): - """Channel colors using a perceptually uniform colormap - - Gets perceptually uniform channel colors using the - JzAzBz colormap. - - See https://www.osapublishing.org/DirectPDFAccess/BA34298D-D6DF-42BA-A704279555676BA8_368272/oe-25-13-15131.pdf?da=1&id=368272&seq=0&mobile=no - - Parameters - ---------- - n_colors : int - Number of colors needed. - - Returns - -------- - channel_colors : ndarray - RGB values for each of the `n_colors` - - """ - cmap = viz.jzazbz_cmap() - channel_colors = viz.get_n_colors(cmap, n_colors) - channel_colors = (channel_colors*255).astype(np.uint8) - - return channel_colors diff --git a/examples/acrobat_2023/valis/valtils.py b/examples/acrobat_2023/valis/valtils.py deleted file mode 100644 index eae2cc88..00000000 --- a/examples/acrobat_2023/valis/valtils.py +++ /dev/null @@ -1,151 +0,0 @@ -import re -import os -from colorama import init as color_init -from colorama import Fore, Style -import functools -import pyvips -import warnings -import contextlib -from collections import defaultdict - -color_init() - -def print_warning(msg, warning_type=UserWarning, rgb=Fore.RED): - """Print warning message with color - """ - warning_msg = f"{rgb}{msg}{Style.RESET_ALL}" - if warning_type is None: - print(warning_msg) - else: - warnings.simplefilter('always', warning_type) - warnings.warn(warning_msg, warning_type) - - -def deprecated_args(**aliases): - def deco(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - rename_kwargs(f.__name__, kwargs, aliases) - return f(*args, **kwargs) - - return wrapper - - return deco - - -def rename_kwargs(func_name, kwargs, aliases): - for alias, new in aliases.items(): - if alias in kwargs: - if new in kwargs: - raise TypeError('{} received both {} and {}'.format( - func_name, alias, new)) - - msg = f'{alias} is deprecated; use {new} instead' - print_warning(msg, DeprecationWarning) - - kwargs[new] = kwargs.pop(alias) - - -@contextlib.contextmanager -def HiddenPrints(): - with contextlib.redirect_stdout(open(os.devnull, 'w')): - yield - - -def get_name(f): - """ - To get an object's name, remove image type extension from filename - """ - if re.search("\.", f) is None: - # Extension already removed - return f - - f = os.path.split(f)[-1] - - if f.endswith(".ome.tiff") or f.endswith(".ome.tif"): - back_slice_idx = 2 - else: - back_slice_idx = 1 - - img_name = "".join([".".join(f.split(".")[:-back_slice_idx])]) - - return img_name - - -def sort_nicely(l): - """Sort the given list in the way that humans expect. - """ - l.sort(key=lambda s: [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)]) - - -def get_elapsed_time_string(elapsed_time, rounding=3): - """Format elapsed time - - Parameters - ---------- - elapsed_time : float - Elapsed time in seconds - - rounding : int - Number of decimal places to round - - Returns - ------- - scaled_time : float - Scaled amount of elapsed time - - time_unit : str - Time unit, either seconds, minutes, or hours - - """ - - if elapsed_time < 60: - scaled_time = elapsed_time - time_unit = "seconds" - - elif 60 <= elapsed_time < 60 ** 2: - scaled_time = elapsed_time / 60 - time_unit = "minutes" - - else: - scaled_time = elapsed_time / (60 ** 2) - time_unit = "hours" - - scaled_time = round(scaled_time, rounding) - - return scaled_time, time_unit - - -def get_vips_version(): - v = f"{pyvips.vips_lib.VIPS_MAJOR_VERSION}.{pyvips.vips_lib.VIPS_MINOR_VERSION}.{pyvips.vips_lib.VIPS_MICRO_VERSION}" - return v - - -def etree_to_dict(t): - d = {t.tag: {} if t.attrib else None} - children = list(t) - if children: - dd = defaultdict(list) - for dc in map(etree_to_dict, children): - for k, v in dc.items(): - dd[k].append(v) - d = {t.tag: {k: v[0] if len(v) == 1 else v - for k, v in dd.items()}} - if t.attrib: - d[t.tag].update(('@' + k, v) - for k, v in t.attrib.items()) - if t.text: - text = t.text.strip() - if children or t.attrib: - if text: - d[t.tag]['#text'] = text - else: - d[t.tag] = text - return d - - -def hex_to_rgb(value): - value = value.lstrip('#') - lv = len(value) - return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) - diff --git a/examples/acrobat_2023/valis/viz.py b/examples/acrobat_2023/valis/viz.py deleted file mode 100644 index 143d04a9..00000000 --- a/examples/acrobat_2023/valis/viz.py +++ /dev/null @@ -1,797 +0,0 @@ -"""Various functions used to visualize registration results - -""" -import colour -import matplotlib.pyplot as plt -from skimage import draw, color, exposure, transform -from scipy.cluster.hierarchy import dendrogram -from scipy.spatial import distance -import numpy as np -import numba as nb -from . import warp_tools -import cv2 -import platform -# JzAzBz # -DXDY_CSPACE = "JzAzBz" -DXDY_CRANGE = (0, 0.025) -DXDY_LRANGE = (0.004, 0.015) - -if platform.system() == 'Windows' or platform.system() == 'Darwin': - uniTupleDtype = np.int32 -else: - uniTupleDtype = np.int64 - - -# CAM16-UCS # -# DXDY_CSPACE = "CAM16UCS" -# DXDY_CRANGE = (0, 0.5) -# DXDY_LRANGE = (0.5, 0.9) - - -def draw_outline(img, mask, clr=(100, 240, 39), thickness=2): - """Draw mask outline around an image - - Parameters - ---------- - img : ndarray - Image on which to draw the mask outline - - - mask : ndarray - Mask to draw outline of - - clr : ndarray - RGB (0-255) color of outline - - thicknes : int - Thickness of outline - - Returns - ------- - outline_img : ndarray - Copy of `img` with the outline of `mask` drawn on it - - """ - - outline_img = img.copy() - if outline_img.ndim == 2: - outline_img = np.dstack([outline_img for i in range(3)]) - - outline_img = outline_img.astype(np.uint8) - - detection_mask = 255*(mask != 0).astype(np.uint8) - contours, _ = cv2.findContours(detection_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - int_color = [int(i) for i in clr] - for cnt in contours: - outline_img = cv2.drawContours(outline_img, [cnt], 0, color=int_color, thickness=thickness) - - return outline_img - - -def draw_features(kp_xy, image, n_features=500): - """Draw keypoints on a image - - """ - - image = exposure.rescale_intensity(image, out_range=(0, 255)) - if image.ndim == 2: - feature_img = color.gray2rgb(image) - else: - feature_img = image.copy() - - rad = int(np.mean(feature_img.shape) / 100) - - n_kp = len(kp_xy) - if n_kp < n_features: - n_features = n_kp - - for c, r in kp_xy[0:n_features].astype(np.int): - circ_r, circ_c = draw.circle_perimeter(r, c, rad, shape=image.shape[0:2]) - feature_img[circ_r, circ_c] = np.random.randint(0, 256, 3) - - return feature_img - - -def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal'): - """Draw feature matches between two images - - Parameters - ---------- - src_img : ndarray - Image associated with `kp1_xy` - - kp1_xy : ndarray - xy coordinates of feature points found in `src_img` - - dst_img : ndarray - Image associated with `kp2_xy` - - kp2_xy : ndarray - xy coordinates of feature points found in `dst_img` - - rad : int - Radius of circles used to draw feature points - - alignment : string - How to stack the images, either 'veritcal' or 'horizontal'. - - Returns - ------- - feature_img : ndarray - Image show corresponding features of `src_img` and `dst_img` - - """ - - all_dims = np.array([src_img.shape, dst_img.shape]) - out_shape = np.max(all_dims, axis=0)[0:2] - - padded_src, src_T = warp_tools.pad_img(src_img, out_shape) - padded_dst, dst_T = warp_tools.pad_img(dst_img, out_shape) - - if alignment.lower().startswith("v"): - feature_img = np.vstack([padded_src, padded_dst]) - dst_xy_shift = np.array([0, out_shape[0]]) - else: - feature_img = np.hstack([padded_src, padded_dst]) - dst_xy_shift = np.array([out_shape[1], 0]) - - if feature_img.ndim == 2: - feature_img = color.gray2rgb(feature_img).astype(np.uint8) - - dst_T[0:2, 2] -= dst_xy_shift - dst_xy_in_feature_img = warp_tools.warp_xy(kp2_xy, M=dst_T) - src_xy_in_feature_img = warp_tools.warp_xy(kp1_xy, M=src_T) - - n_pt = np.min([kp1_xy.shape[0], kp2_xy.shape[0]]) - cmap = (255*jzazbz_cmap()).astype(np.uint8) - all_color_idx = np.arange(0, cmap.shape[0]) - colors = cmap[np.random.choice(all_color_idx, n_pt), :] - for i in range(n_pt): - - xy1 = src_xy_in_feature_img[i] - xy2 = dst_xy_in_feature_img[i] - pt_color = colors[i] - - circ_rc_1 = draw.ellipse(*xy1[::-1], rad, rad, shape=feature_img.shape) - circ_rc_2 = draw.ellipse(*xy2[::-1], rad, rad, shape=feature_img.shape) - line_rc = np.array(draw.line_aa(*np.round(xy1[::-1]).astype(int), *np.round(xy2[::-1]).astype(int))) - line_rc[0] = np.clip(line_rc[0], 0, feature_img.shape[0]-1).astype(int) - line_rc[1] = np.clip(line_rc[1], 0, feature_img.shape[1]-1).astype(int) - - feature_img[line_rc[0].astype(int), line_rc[1].astype(int)] = pt_color*line_rc[2][..., np.newaxis] - feature_img[circ_rc_1] = pt_color - feature_img[circ_rc_2] = pt_color - - return feature_img - - -def draw_clusterd_D(D, optimal_Z): - """Draw clustered distance matrix with dendrograms along the axes - - """ - - fig = plt.figure() - axdendro = fig.add_axes([0.013, 0.05, 0.1, 0.798]) - - Z = dendrogram(optimal_Z, orientation='left', link_color_func=lambda k: "black") - axdendro.set_xticks([]) - axdendro.set_yticks([]) - axdendro.axis('off') - axdendro.invert_yaxis() - - axdendro_top = fig.add_axes([0.115, 0.85, 0.6, 0.14]) - Z_top = dendrogram(optimal_Z, orientation='top', link_color_func=lambda k: "black") - axdendro_top.set_xticks([]) - axdendro_top.set_yticks([]) - axdendro_top.axis('off') - - axmatrix = fig.add_axes([0.115, 0.05, 0.6, 0.798]) - im = axmatrix.matshow(D, aspect='auto', cmap="plasma_r") - axmatrix.set_xticks([]) - axmatrix.set_yticks([]) - - axcolor = fig.add_axes([0.75, 0.05, 0.03, 0.798]) - plt.colorbar(im, cax=axcolor) - - -# Non-rigid visualization # -@nb.njit() -def get_grid(shape, grid_spacing, thickness=1): - """ - Get points for a grid. Can be used to view deformation field - - Parameters - ---------- - shape : (int, int) - dimensions of image upon which the grid will be drawn - - grid_spacing : int - Space between grid points - - thickness : int, optional - line thickness - - Returns - ------- - grid_rows, grid_cols : 2 ndarray - 2, 1D arrays, which each element corresponding to a point in the grid - """ - - all_rows = [] - all_cols = [] - row_add_idx = 0 - for k in range(thickness): - for i in np.arange(grid_spacing - thickness, shape[0] + thickness, grid_spacing): - for j in np.arange(0, shape[1]): - if k%2 == 0: - r = i + row_add_idx - elif k%2 != 0: - r = i - row_add_idx - - if r >= 0 and r < shape[0]: - all_rows.append(r) - all_cols.append(j) - - if k % 2 == 0: - row_add_idx += 1 - - col_add_idx = 0 - for k in range(thickness): - for j in np.arange(grid_spacing - thickness, shape[1], grid_spacing): - for i in np.arange(0, shape[0]): - if k%2 == 0: - c = j + col_add_idx - elif k%2 != 0: - c = j - col_add_idx - - if c >= 0 and c < shape[1]: - all_rows.append(i) - all_cols.append(c) - - if k % 2 == 0: - col_add_idx += 1 - - return np.array(all_rows, dtype=uniTupleDtype), np.array(all_cols, dtype=uniTupleDtype) - - -def jzazbz_cmap(luminosity=0.012, colorfulness=0.02, max_h=260): - """ - Get colormap based on JzAzBz colorspace, which has good hue linearity. - Already preceptually uniform. - - Parameters - ---------- - luminosity : float, optional - - colorfulness : float, optional - - max_h : int, optional - - """ - - h = np.deg2rad(np.arange(0, 360)) - a = colorfulness * np.cos(h) - b = colorfulness * np.sin(h) - j = np.repeat(luminosity, len(h)) - - jzazbz = np.dstack([j, a, b]) - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(jzazbz, 'JzAzBz', 'sRGB') - - rgb = np.clip(rgb, 0, 1)[0] - if max_h != 360: - rgb = rgb[0:max_h] - - return rgb - -def cam16ucs_cmap(luminosity=0.8, colorfulness=0.5, max_h=300): - """ - Get colormap based on CAM16-UCS colorspace. - - Parameters - ---------- - luminosity : float, optional - - colorfulness : float, optional - - max_h : int, optional - - """ - - h = np.deg2rad(np.arange(0, 360)) - a = colorfulness * np.cos(h) - b = colorfulness * np.sin(h) - j = np.repeat(luminosity, len(h)) - - eps = np.finfo("float").eps - cam = np.dstack([j, a+eps, b+eps]) - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(cam, 'CAM16UCS', 'sRGB') - - rgb = np.clip(rgb, 0, 1)[0] - if max_h != 360: - rgb = rgb[0:max_h] - - return rgb - - -def make_cbar(rgb, bar_height=30): - cbar = np.tile(rgb[np.newaxis].T, bar_height).T - - return cbar - - -def rgb_triangle_cmap(): - - total_n = 360 - n = total_n//3 - max_v = 0.9 - min_v = 1 - max_v - - tri_edges_x = np.hstack([np.linspace(min_v, max_v, n*2), np.linspace(max_v - 1/n, min_v + 1/n, n)]) - tri_edges_y = np.hstack([np.linspace(min_v, max_v, n), np.linspace(max_v - 1/n, min_v, n), np.repeat(min_v, n)]) - - tri_edges_xy = np.dstack([tri_edges_x, tri_edges_y])[0] - - T = np.array([[-1., -0.5], - [0., 1.]]) - - bary_xy = np.array([np.linalg.inv(T) @ (tri_edges_xy[i] - np.array([ 1., 0.])) for i in range(len(tri_edges_xy))]) - bary_z = 1 - np.sum(bary_xy, axis=1) - rgb = np.array([bary_xy[:, 0], bary_xy[:, 1], bary_z]).T - - return rgb - - -def turbo_cmap(): - """Turbo colormap - https://gist.github.com/mikhailov-work/ee72ba4191942acecc03fe6da94fc73f - """ - # The look-up table contains 256 entries. Each entry is a floating point sRGB triplet. - # To use it with matplotlib, pass cmap=ListedColormap(turbo_colormap_data) as an arg to imshow() (don't forget "from matplotlib.colors import ListedColormap"). - # If you have a typical 8-bit greyscale image, you can use the 8-bit value to index into this LUT directly. - # The floating point color values can be converted to 8-bit sRGB via multiplying by 255 and casting/flooring to an integer. Saturation should not be required for IEEE-754 compliant arithmetic. - # If you have a floating point value in the range [0,1], you can use interpolate() to linearly interpolate between the entries. - # If you have 16-bit or 32-bit integer values, convert them to floating point values on the [0,1] range and then use interpolate(). Doing the interpolation in floating point will reduce banding. - # If some of your values may lie outside the [0,1] range, use interpolate_or_clip() to highlight them. - - turbo_colormap_data = np.array([[0.18995, 0.07176, 0.23217], [0.19483, 0.08339, 0.26149], [0.19956, 0.09498, 0.29024], - [0.20415, 0.10652, 0.31844], [0.20860, 0.11802, 0.34607], [0.21291, 0.12947, 0.37314], - [0.21708, 0.14087, 0.39964], [0.22111, 0.15223, 0.42558], [0.22500, 0.16354, 0.45096], - [0.22875, 0.17481, 0.47578], [0.23236, 0.18603, 0.50004], [0.23582, 0.19720, 0.52373], - [0.23915, 0.20833, 0.54686], [0.24234, 0.21941, 0.56942], [0.24539, 0.23044, 0.59142], - [0.24830, 0.24143, 0.61286], [0.25107, 0.25237, 0.63374], [0.25369, 0.26327, 0.65406], - [0.25618, 0.27412, 0.67381], [0.25853, 0.28492, 0.69300], [0.26074, 0.29568, 0.71162], - [0.26280, 0.30639, 0.72968], [0.26473, 0.31706, 0.74718], [0.26652, 0.32768, 0.76412], - [0.26816, 0.33825, 0.78050], [0.26967, 0.34878, 0.79631], [0.27103, 0.35926, 0.81156], - [0.27226, 0.36970, 0.82624], [0.27334, 0.38008, 0.84037], [0.27429, 0.39043, 0.85393], - [0.27509, 0.40072, 0.86692], [0.27576, 0.41097, 0.87936], [0.27628, 0.42118, 0.89123], - [0.27667, 0.43134, 0.90254], [0.27691, 0.44145, 0.91328], [0.27701, 0.45152, 0.92347], - [0.27698, 0.46153, 0.93309], [0.27680, 0.47151, 0.94214], [0.27648, 0.48144, 0.95064], - [0.27603, 0.49132, 0.95857], [0.27543, 0.50115, 0.96594], [0.27469, 0.51094, 0.97275], - [0.27381, 0.52069, 0.97899], [0.27273, 0.53040, 0.98461], [0.27106, 0.54015, 0.98930], - [0.26878, 0.54995, 0.99303], [0.26592, 0.55979, 0.99583], [0.26252, 0.56967, 0.99773], - [0.25862, 0.57958, 0.99876], [0.25425, 0.58950, 0.99896], [0.24946, 0.59943, 0.99835], - [0.24427, 0.60937, 0.99697], [0.23874, 0.61931, 0.99485], [0.23288, 0.62923, 0.99202], - [0.22676, 0.63913, 0.98851], [0.22039, 0.64901, 0.98436], [0.21382, 0.65886, 0.97959], - [0.20708, 0.66866, 0.97423], [0.20021, 0.67842, 0.96833], [0.19326, 0.68812, 0.96190], - [0.18625, 0.69775, 0.95498], [0.17923, 0.70732, 0.94761], [0.17223, 0.71680, 0.93981], - [0.16529, 0.72620, 0.93161], [0.15844, 0.73551, 0.92305], [0.15173, 0.74472, 0.91416], - [0.14519, 0.75381, 0.90496], [0.13886, 0.76279, 0.89550], [0.13278, 0.77165, 0.88580], - [0.12698, 0.78037, 0.87590], [0.12151, 0.78896, 0.86581], [0.11639, 0.79740, 0.85559], - [0.11167, 0.80569, 0.84525], [0.10738, 0.81381, 0.83484], [0.10357, 0.82177, 0.82437], - [0.10026, 0.82955, 0.81389], [0.09750, 0.83714, 0.80342], [0.09532, 0.84455, 0.79299], - [0.09377, 0.85175, 0.78264], [0.09287, 0.85875, 0.77240], [0.09267, 0.86554, 0.76230], - [0.09320, 0.87211, 0.75237], [0.09451, 0.87844, 0.74265], [0.09662, 0.88454, 0.73316], - [0.09958, 0.89040, 0.72393], [0.10342, 0.89600, 0.71500], [0.10815, 0.90142, 0.70599], - [0.11374, 0.90673, 0.69651], [0.12014, 0.91193, 0.68660], [0.12733, 0.91701, 0.67627], - [0.13526, 0.92197, 0.66556], [0.14391, 0.92680, 0.65448], [0.15323, 0.93151, 0.64308], - [0.16319, 0.93609, 0.63137], [0.17377, 0.94053, 0.61938], [0.18491, 0.94484, 0.60713], - [0.19659, 0.94901, 0.59466], [0.20877, 0.95304, 0.58199], [0.22142, 0.95692, 0.56914], - [0.23449, 0.96065, 0.55614], [0.24797, 0.96423, 0.54303], [0.26180, 0.96765, 0.52981], - [0.27597, 0.97092, 0.51653], [0.29042, 0.97403, 0.50321], [0.30513, 0.97697, 0.48987], - [0.32006, 0.97974, 0.47654], [0.33517, 0.98234, 0.46325], [0.35043, 0.98477, 0.45002], - [0.36581, 0.98702, 0.43688], [0.38127, 0.98909, 0.42386], [0.39678, 0.99098, 0.41098], - [0.41229, 0.99268, 0.39826], [0.42778, 0.99419, 0.38575], [0.44321, 0.99551, 0.37345], - [0.45854, 0.99663, 0.36140], [0.47375, 0.99755, 0.34963], [0.48879, 0.99828, 0.33816], - [0.50362, 0.99879, 0.32701], [0.51822, 0.99910, 0.31622], [0.53255, 0.99919, 0.30581], - [0.54658, 0.99907, 0.29581], [0.56026, 0.99873, 0.28623], [0.57357, 0.99817, 0.27712], - [0.58646, 0.99739, 0.26849], [0.59891, 0.99638, 0.26038], [0.61088, 0.99514, 0.25280], - [0.62233, 0.99366, 0.24579], [0.63323, 0.99195, 0.23937], [0.64362, 0.98999, 0.23356], - [0.65394, 0.98775, 0.22835], [0.66428, 0.98524, 0.22370], [0.67462, 0.98246, 0.21960], - [0.68494, 0.97941, 0.21602], [0.69525, 0.97610, 0.21294], [0.70553, 0.97255, 0.21032], - [0.71577, 0.96875, 0.20815], [0.72596, 0.96470, 0.20640], [0.73610, 0.96043, 0.20504], - [0.74617, 0.95593, 0.20406], [0.75617, 0.95121, 0.20343], [0.76608, 0.94627, 0.20311], - [0.77591, 0.94113, 0.20310], [0.78563, 0.93579, 0.20336], [0.79524, 0.93025, 0.20386], - [0.80473, 0.92452, 0.20459], [0.81410, 0.91861, 0.20552], [0.82333, 0.91253, 0.20663], - [0.83241, 0.90627, 0.20788], [0.84133, 0.89986, 0.20926], [0.85010, 0.89328, 0.21074], - [0.85868, 0.88655, 0.21230], [0.86709, 0.87968, 0.21391], [0.87530, 0.87267, 0.21555], - [0.88331, 0.86553, 0.21719], [0.89112, 0.85826, 0.21880], [0.89870, 0.85087, 0.22038], - [0.90605, 0.84337, 0.22188], [0.91317, 0.83576, 0.22328], [0.92004, 0.82806, 0.22456], - [0.92666, 0.82025, 0.22570], [0.93301, 0.81236, 0.22667], [0.93909, 0.80439, 0.22744], - [0.94489, 0.79634, 0.22800], [0.95039, 0.78823, 0.22831], [0.95560, 0.78005, 0.22836], - [0.96049, 0.77181, 0.22811], [0.96507, 0.76352, 0.22754], [0.96931, 0.75519, 0.22663], - [0.97323, 0.74682, 0.22536], [0.97679, 0.73842, 0.22369], [0.98000, 0.73000, 0.22161], - [0.98289, 0.72140, 0.21918], [0.98549, 0.71250, 0.21650], [0.98781, 0.70330, 0.21358], - [0.98986, 0.69382, 0.21043], [0.99163, 0.68408, 0.20706], [0.99314, 0.67408, 0.20348], - [0.99438, 0.66386, 0.19971], [0.99535, 0.65341, 0.19577], [0.99607, 0.64277, 0.19165], - [0.99654, 0.63193, 0.18738], [0.99675, 0.62093, 0.18297], [0.99672, 0.60977, 0.17842], - [0.99644, 0.59846, 0.17376], [0.99593, 0.58703, 0.16899], [0.99517, 0.57549, 0.16412], - [0.99419, 0.56386, 0.15918], [0.99297, 0.55214, 0.15417], [0.99153, 0.54036, 0.14910], - [0.98987, 0.52854, 0.14398], [0.98799, 0.51667, 0.13883], [0.98590, 0.50479, 0.13367], - [0.98360, 0.49291, 0.12849], [0.98108, 0.48104, 0.12332], [0.97837, 0.46920, 0.11817], - [0.97545, 0.45740, 0.11305], [0.97234, 0.44565, 0.10797], [0.96904, 0.43399, 0.10294], - [0.96555, 0.42241, 0.09798], [0.96187, 0.41093, 0.09310], [0.95801, 0.39958, 0.08831], - [0.95398, 0.38836, 0.08362], [0.94977, 0.37729, 0.07905], [0.94538, 0.36638, 0.07461], - [0.94084, 0.35566, 0.07031], [0.93612, 0.34513, 0.06616], [0.93125, 0.33482, 0.06218], - [0.92623, 0.32473, 0.05837], [0.92105, 0.31489, 0.05475], [0.91572, 0.30530, 0.05134], - [0.91024, 0.29599, 0.04814], [0.90463, 0.28696, 0.04516], [0.89888, 0.27824, 0.04243], - [0.89298, 0.26981, 0.03993], [0.88691, 0.26152, 0.03753], [0.88066, 0.25334, 0.03521], - [0.87422, 0.24526, 0.03297], [0.86760, 0.23730, 0.03082], [0.86079, 0.22945, 0.02875], - [0.85380, 0.22170, 0.02677], [0.84662, 0.21407, 0.02487], [0.83926, 0.20654, 0.02305], - [0.83172, 0.19912, 0.02131], [0.82399, 0.19182, 0.01966], [0.81608, 0.18462, 0.01809], - [0.80799, 0.17753, 0.01660], [0.79971, 0.17055, 0.01520], [0.79125, 0.16368, 0.01387], - [0.78260, 0.15693, 0.01264], [0.77377, 0.15028, 0.01148], [0.76476, 0.14374, 0.01041], - [0.75556, 0.13731, 0.00942], [0.74617, 0.13098, 0.00851], [0.73661, 0.12477, 0.00769], - [0.72686, 0.11867, 0.00695], [0.71692, 0.11268, 0.00629], [0.70680, 0.10680, 0.00571], - [0.69650, 0.10102, 0.00522], [0.68602, 0.09536, 0.00481], [0.67535, 0.08980, 0.00449], - [0.66449, 0.08436, 0.00424], [0.65345, 0.07902, 0.00408], [0.64223, 0.07380, 0.00401], - [0.63082, 0.06868, 0.00401], [0.61923, 0.06367, 0.00410], [0.60746, 0.05878, 0.00427], - [0.59550, 0.05399, 0.00453], [0.58336, 0.04931, 0.00486], [0.57103, 0.04474, 0.00529], - [0.55852, 0.04028, 0.00579], [0.54583, 0.03593, 0.00638], [0.53295, 0.03169, 0.00705], - [0.51989, 0.02756, 0.00780], [0.50664, 0.02354, 0.00863], [0.49321, 0.01963, 0.00955], - [0.47960, 0.01583, 0.01055]]) - - return turbo_colormap_data - - -def get_n_colors(rgb, n): - """ - Pick n most different colors in rgb. Differences based of rgb values in the CAM16UCS colorspace - Based on https://larssonjohan.com/post/2016-10-30-farthest-points/ - """ - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < rgb.max() <= 255 and np.issubdtype(rgb.dtype, np.integer): - cam = colour.convert(rgb/255, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(rgb, 'sRGB', 'CAM16UCS') - - sq_D = distance.cdist(cam, cam) - max_D = sq_D.max() - most_dif_2Didx = np.where(sq_D == max_D) # 2 most different colors - most_dif_img1 = most_dif_2Didx[0][0] - most_dif_img2 = most_dif_2Didx[1][0] - rgb_idx = [most_dif_img1, most_dif_img2] - - possible_idx = list(range(sq_D.shape[0])) - possible_idx.remove(most_dif_img1) - possible_idx.remove(most_dif_img2) - - for new_color in range(2, n): - max_d_idx = np.argmax([np.min(sq_D[i, rgb_idx]) for i in possible_idx]) - new_rgb_idx = possible_idx[max_d_idx] - rgb_idx.append(new_rgb_idx) - possible_idx.remove(new_rgb_idx) - - return rgb[rgb_idx] - - -@nb.njit(fastmath=True, cache=True) -def blend_colors(img, colors, scale_by): - """ Color an image by blending - - Parameters - ---------- - img : ndarray - Image containing the raw data (float 32) - - colors : ndarray - Colors for each channel in `img` - - scale_by : int - How to scale each channel. "image" will scale the channel - by the maximum value in the image. "channel" will scale - the channel by the maximum in the channel - - Returns - ------- - blended_img : ndarray - A colored version of `img` - - """ - - - if len(colors) > 1: - n_channel_colors = colors.shape[1] - else: - n_channel_colors = len(colors) - - if img.ndim > 2: - r, c, nc = img.shape[:3] - else: - nc = 1 - r, c = img.shape[2] - - eps = 1.0000000000000001e-15 - sum_img = img.sum(axis=2) + eps - if scale_by == "image": - img_max = img.max() - - blended_img = np.zeros((r, c, n_channel_colors)) - for i in range(nc): - # relative image is how bright the channel will be - if scale_by != "image": - channel_max = img[..., i].max() - relative_img = img[..., i] / channel_max - else: - relative_img = img[..., i]/img_max - - # blending is how to weight the mix of colors, similar to an alpha channel - blending = img[..., i]/sum_img - for j in range(colors.shape[1]): - channel_color = colors[i, j] - blended_img[..., j] += channel_color * relative_img * blending - - return blended_img - - -def color_multichannel(multichannel_img, marker_colors, rescale_channels=False, normalize_by="image", cspace="Hunter Lab"): - """Color a multichannel image to view as RGB - - Parameters - ---------- - multichannel_img : ndarray - Image to color - - marker_colors : ndarray - sRGB colors for each channel. - - rescale_channels : bool - If True, then each channel will be scaled between 0 and 1 before - building the composite RGB image. This will allow markers to 'pop' - in areas where they are expressed in isolation, but can also make - it appear more marker is expressed than there really is. - - normalize_by : str, optionaal - "image" will produce an image where all values are scaled between - 0 and the highest intensity in the composite image. This will produce - an image where one can see the expression of each marker relative to - the others, making it easier to compare marker expression levels. - - "channel" will first scale the intensity of each channel, and then - blend all of the channels together. This will allow one to see the - relative expression of each marker, but won't allow one to directly - compare the expression of markers across channels. - - cspace : str - Colorspace in which `marker_colors` will be blended. - JzAzBz, Hunter Lab, and sRGB all work well. But, see - colour.COLOURSPACE_MODELS for other possible colorspaces - - Returns - ------- - rgb : ndarray - An sRGB version of `multichannel_img` - - """ - - if rescale_channels: - multichannel_img = np.dstack([exposure.rescale_intensity(multichannel_img[..., i].astype(float), in_range="image", out_range=(0, 1)) for i in range(multichannel_img.shape[2])]) - - is_srgb = cspace.lower() == "srgb" - is_srgb_01 = True - if 1 < marker_colors.max() <= 255 and np.issubdtype(marker_colors.dtype, np.integer): - srgb_01 = marker_colors/255 - is_srgb_01 = False - - else: - srgb_01 = marker_colors - eps = np.finfo("float").eps - if not is_srgb: - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - cspace_colors = colour.convert(srgb_01 + eps, 'sRGB', cspace) - else: - cspace_colors = srgb_01 - - blended_img = blend_colors(multichannel_img, cspace_colors, normalize_by) - if not is_srgb: - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - srgb_blended = colour.convert(blended_img + eps, cspace, 'sRGB') - 2*eps - else: - srgb_blended = blended_img - - srgb_blended = np.clip(srgb_blended, 0, 1) - if not is_srgb_01: - srgb_blended = (255 * srgb_blended).astype(marker_colors.dtype) - - return srgb_blended - - -def color_dxdy(dx, dy, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, cspace=DXDY_CSPACE): - """ - Color displacement, where larger displacements are more colorful, - and, if scale_l=True, brighter. - - Parameters - ---------- - dx: array - 1D Array containing the displacement in the X (column) direction - - dy: array - 1D Array containing the displacement in the Y (row) direction - - c_range: (float, float) - Minimum and maximum colorfulness in JzAzBz colorspace - - l_range: (float, float) - Minimum and maximum luminosity in JzAzBz colorspace - - scale_l: boolean - Scale the luminosity based on magnitude of displacement - - Returns - ------- - displacement_rgb : array - RGB (0, 255) color for each displacement, with the same shape as dx and dy - - """ - - initial_shape = dx.shape - - dx = dx.reshape(-1) - dy = dy.reshape(-1) - if np.all(dx==0) and np.all(dy==0): - # No displacements. Return grey image - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - bg_rgb = colour.convert(np.dstack([l_range[0], 0, 0]), cspace, 'sRGB')*255 - - displacement_rgb = np.full((*initial_shape, 3), bg_rgb).astype(np.uint8) - return displacement_rgb - - eps = np.finfo("float").eps - magnitude = np.sqrt(dx ** 2 + dy ** 2 + eps) - C = exposure.rescale_intensity(magnitude, in_range=(0, magnitude.max()), out_range=tuple(c_range)) - H = np.arctan2(dy.T, dx.T) - A, B = C * np.cos(H), C * np.sin(H) - J = exposure.rescale_intensity(magnitude, in_range=(0, magnitude.max()), out_range=tuple(l_range)) - - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(np.dstack([J, A+eps, B+eps]), cspace, 'sRGB') - - displacement_rgb = (255*np.clip(rgb, 0, 1)).astype(np.uint8).reshape((*initial_shape, 3)) - - return displacement_rgb - - -def displacement_legend(): - - X = np.linspace(-1, 1, 100) - Y = np.linspace(-1, 1, 100) - - X, Y = np.meshgrid(X, Y) - R = np.sqrt(X ** 2 + Y ** 2) - C = np.sin(R) - - C = exposure.rescale_intensity(C, out_range=(0, 1)) - - grad = np.linspace(-1, 1, X.shape[0]) - grad = np.resize(grad, X.shape) - dx = grad*C - dy = grad.T * C - - displacement_legend = color_dxdy(dx, dy, DXDY_CRANGE, DXDY_LRANGE, cspace=DXDY_CSPACE) - - return displacement_legend - - -def draw_displacement_legend(): - leg = displacement_legend() - fig, ax = plt.subplots() - plt.locator_params(nbins=10) - ax.imshow(leg) - ax.set_xticklabels(["", "--", "", "", "", "", "0", "", "", "", "+"]) - ax.set_yticklabels(["", "+", "", "", "", "", "0", "", "", "", "--"]) - ax.set_xlabel('dx') - ax.set_ylabel('dy') - - -def color_displacement_grid(bk_dx, bk_dy, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, thickness=None, grid_spacing_ratio=0.02, cspace=DXDY_CSPACE): - """Color a displacement grid - """ - - grid_spacing = np.max(np.array(bk_dx.shape)*grid_spacing_ratio).astype(int) - min_dim = np.min(bk_dx.shape) - - if thickness is None: - thickness = int(np.ceil((grid_spacing/min_dim)*15)) - - if thickness < 1: - thickness = 1 - - grid_r, grid_c = get_grid(bk_dx.shape, grid_spacing, thickness) - grid_colors = color_dxdy(bk_dx[grid_r, grid_c], bk_dy[grid_r, grid_c], c_range=c_range, l_range=l_range, cspace=cspace) - - # Warp image of grid - grid_img = np.zeros((*bk_dx.shape, 3)) - grid_img[grid_r, grid_c] = grid_colors - - img_r, img_c = np.indices(bk_dx.shape) - img_warp_r = img_r + bk_dy[img_r, img_c] - img_warp_c = img_c + bk_dx[img_r, img_c] - - img_warp_r = np.clip(img_warp_r, 0, bk_dx.shape[0] - 1) - img_warp_c = np.clip(img_warp_c, 0, bk_dx.shape[1] - 1) - - warped_hcl = [None] * 3 - for i in range(3): - warped_hcl[i] = transform.warp(grid_img[..., i], np.array([img_warp_r, img_warp_c])) - - grid_img = np.dstack(warped_hcl).astype(np.uint8) - - return grid_img - - -def draw_trimesh(shape_rc, tri_verts, tri_faces, thickness=2): - """Draw a triangular mesh - """ - - tri_img = np.zeros(shape_rc) - for face in tri_faces: - verts = tri_verts[face] - # make sure points are clockwise - cx, cy = np.mean(verts, axis=0) - pt_order = np.argsort([np.rad2deg(np.arctan2(xy[1]-cy, xy[0]-cx)) - for xy in verts]) - # draw points - pts = verts[pt_order, :].reshape(-1, 1, 2).astype(int) - tri_img = cv2.polylines(tri_img, [pts], True, 1, thickness, - lineType=cv2.LINE_AA) - - return tri_img.astype(float) - - - -def color_displacement_tri_grid(bk_dx, bk_dy, img=None, n_grid_pts=25, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, thickness=None, cspace=DXDY_CSPACE): - """View how a displacement warps a triangular mesh. - """ - - shape = np.array(bk_dx.shape) - grid_spacing = int(np.min(np.round(shape/n_grid_pts))) - - new_r = shape[0] - shape[0] % grid_spacing + grid_spacing - sample_y = np.arange(0, new_r + grid_spacing, grid_spacing) - - new_c = shape[1] - shape[1] % grid_spacing + grid_spacing - sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) - - padded_shape = np.array([new_r+1, new_c+1]) - - padding_T = warp_tools.get_padding_matrix(shape, padded_shape) - - padded_dx = transform.warp(bk_dx, padding_T, output_shape=padded_shape, preserve_range=True) - padded_dy = transform.warp(bk_dy, padding_T, output_shape=padded_shape, preserve_range=True) - - min_dim = np.min(padded_dy.shape) - if thickness is None: - thickness = int(np.ceil((grid_spacing/min_dim)*15)) - if thickness < 1: - thickness = 1 - - tri_verts, tri_faces = warp_tools.get_triangular_mesh(sample_x, sample_y) - warped_xy = warp_tools.warp_xy(tri_verts, bk_dxdy=[padded_dx, padded_dy]) - - inv_T = np.linalg.inv(padding_T) - trimesh_img = draw_trimesh(padded_shape, warped_xy, tri_faces, thickness=thickness) - trimesh_img = transform.warp(trimesh_img, inv_T, output_shape=shape, preserve_range=True) - colored_displacement = color_dxdy(bk_dx, bk_dy, c_range=c_range, l_range=l_range, cspace=cspace) - - if img is not None: - assert img.shape[0:2] == trimesh_img.shape[0:2], print(f"mismatch in shape between `img` {img.shape[0:2]} and displacement fields {trimesh_img.shape[0:2]}") - mesh_pos = trimesh_img > 0 - out_img = img.copy() - out_img[mesh_pos] = colored_displacement[mesh_pos] - else: - out_img = trimesh_img[..., np.newaxis] * colored_displacement - - return out_img - diff --git a/examples/acrobat_2023/valis/warp_tools.py b/examples/acrobat_2023/valis/warp_tools.py deleted file mode 100644 index d37382ab..00000000 --- a/examples/acrobat_2023/valis/warp_tools.py +++ /dev/null @@ -1,3429 +0,0 @@ -import multiprocessing -from scipy.optimize import fmin_l_bfgs_b -from scipy import ndimage, spatial -import shapely -from shapely.ops import unary_union -from shapely.strtree import STRtree -from shapely.geometry import Polygon, MultiPolygon -import matplotlib.pyplot as plt -import numpy as np -from joblib import Parallel, delayed, parallel_backend -from skimage import draw, restoration, transform, filters, morphology -import tqdm -import cv2 -from PIL import Image, ImageDraw -import numpy as np -import weightedstats -import warnings -import pyvips -from interpolation.splines import UCGrid, filter_cubic, eval_cubic -import SimpleITK as sitk -from colorama import Fore -import os -import re -from copy import deepcopy -from . import valtils - -pyvips.cache_set_max(0) - - -def is_pyvips_22(): - pvips_ver = pyvips.__version__.split(".") - pyvips_22 = eval(pvips_ver[0]) >= 2 and eval(pvips_ver[1]) >= 2 - return pyvips_22 - - -def get_ref_img_idx(img_f_list, ref_img_name=None): - """Get index of reference image - - Parameters - ---------- - img_f_list : list of str - List of image file names - - ref_img_name : str, optional - Filename of image that will be treated as the center of the stack. - If None, the index of the middle image will be returned. - - Returns - ------- - ref_img_idx : int - Index of reference image in img_f_list. Warnings are raised - if `ref_img_name` matches either 0 or more than 1 images in `img_f_list`. - - """ - - n_imgs = len(img_f_list) - if ref_img_name is None: - if n_imgs == 2: - ref_img_idx = 0 - else: - ref_img_idx = n_imgs // 2 - - else: - ref_img_name = valtils.get_name(os.path.split(ref_img_name)[1]) - img_names = [valtils.get_name(f).lower() for f in img_f_list] - name_matches = [re.search(ref_img_name.lower(), n) for n in img_names] - ref_img_idx = [i for i in range(n_imgs) if name_matches[i] is not None] - n_matches = len(ref_img_idx) - - if n_matches == 0: - ref_img_idx = n_imgs // 2 - warning_msg = (f"No files in img_f_list match {ref_img_name}" - f"Returning middle image, which is {img_f_list[ref_img_idx]}") - - valtils.print_warning(warning_msg) - - elif n_matches == 1: - ref_img_idx = ref_img_idx[0] - - elif n_matches > 1: - macthing_files = ", ".join(img_f_list[i] for i in ref_img_idx) - ref_img_idx = ref_img_idx[0] - warning_msg = (f"More than 1 file in img_f_list matches {ref_img_name}. " - f"These files are: {macthing_files}. " - f"Returning first match, which is {img_f_list[ref_img_idx]}") - - valtils.print_warning(warning_msg) - - - return ref_img_idx - - -def get_alignment_indices(n_imgs, ref_img_idx=None): - """Get indices to align in stack. - - Indices go from bottom to center, then top to center. In each case, - the alignments go from closest to the center, to next closet, etc... - The reference image is exclued from this list. - For example, if `ref_img_idx` is 2, then the order is - [(1, 2), (0, 1), (3, 2), ..., (`n_imgs`-1, `n_imgs` - 2)]. - - Parameters - ---------- - n_imgs : int - Number of images in the stack - - ref_img_idx : int, optional - Position of reference image. If None, then this will set to - the center of the stack - - Returns - ------- - matching_indices : list of tuples - Each element of `matching_indices` contains a tuple of stack - indices. The first value is the index of the moving/current/from - image, while the second value is the index of the moving/next/to - image. - - """ - - if ref_img_idx is None: - ref_img_idx = n_imgs//2 - - matching_indices = [None] * (n_imgs - 1) - idx = 0 - for i in reversed(range(0, ref_img_idx)): - current_idx = i - next_idx = i + 1 - matching_indices[idx] = (current_idx, next_idx) - idx += 1 - - for i in range(ref_img_idx, n_imgs-1): - current_idx = i + 1 - next_idx = i - matching_indices[idx] = (current_idx, next_idx) - idx += 1 - - return matching_indices - - -def calc_memory_size_gb(shape, nchannels, np_dtype): - """Estimate amount of space an image will take up, in Gb - """ - - bitdepth = "".join(re.findall(r'\d+', np_dtype)) - if len(bitdepth) > 0: - bitdepth = eval(bitdepth) - else: - bitdepth = 1 - - n_px = nchannels*np.multiply(*shape) - gb = ((n_px*8)/bitdepth)/(2**30) - - return gb - - -def remove_invasive_displacements(bk_dxdy, M, src_shape_rc, out_shape_rc, inpaint_holes=False): - """Remove displacements that would distort the image edges - Finds areas where areas outside of the image get brought inside. Can - happen if displacements are combined. - - Parameters - ---------- - bk_dxdy : list - Displacement fields [x, y] - - M : ndarray - 3x3 transformation matrix - - src_shape_rc : tuple - Shape (row, col) of the image before affine transform - - Returns - ------- - new_dxdy : list - `bk_dxdy` but with invasive displacements set to 0 - - """ - - new_dx = bk_dxdy[0].copy() - new_dy = bk_dxdy[1].copy() - if M is not None: - affine_mask = warp_img(np.full(src_shape_rc, 255, dtype=np.uint8), M, out_shape_rc=out_shape_rc, interp_method="nearest") - if not np.all(out_shape_rc == bk_dxdy[0].shape): - affine_mask = resize_img(affine_mask, bk_dxdy[0].shape, interp_method="nearest") - new_dx[affine_mask == 0] = 0 - new_dy[affine_mask == 0] = 0 - - else: - affine_mask = np.full(out_shape_rc, 255, dtype=np.uint8) - - inv_mask = 255*(affine_mask == 0).astype(np.uint8) - inv_nr = warp_img(inv_mask, bk_dxdy=bk_dxdy) - out_to_in = ((inv_nr > 0) & (affine_mask > 0)) - - selem = morphology.disk(3) - out_to_in = morphology.binary_dilation(out_to_in, selem) - - new_dy = bk_dxdy[1].copy() - new_dx = bk_dxdy[0].copy() - - new_dx[out_to_in] = 0 - new_dy[out_to_in] = 0 - - nr_img = np.round(warp_img(affine_mask, bk_dxdy=[new_dx, new_dy])).astype(np.uint8) - - holes_mask = ((nr_img == 0) & (affine_mask > 0)) - holes_mask = 255*(morphology.binary_dilation(holes_mask, selem)).astype(np.uint8) - - if inpaint_holes and holes_mask.max() > 0: - new_dx = cv2.inpaint(new_dx.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA) - new_dy = cv2.inpaint(new_dy.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA) - else: - new_dx[holes_mask > 0] = 0 - new_dy[holes_mask > 0] = 0 - - new_dxdy = np.array([new_dx, new_dy]) - - return new_dxdy - - -def rescale_img(img, scaling): - is_array = False - if not isinstance(img, pyvips.Image): - is_array = True - img = numpy2vips(img) - - resized = img.resize(scaling) - if is_array: - resized = vips2numpy(resized) - - return resized - - -def resize_img(img, out_shape_rc, interp_method="bicubic"): - - is_array = False - if not isinstance(img, pyvips.Image): - is_array = True - img = numpy2vips(img) - - out_h, out_w = out_shape_rc - - src_shape_rc = np.array([img.height, img.width]) - sy, sx = (np.array(out_shape_rc)/src_shape_rc) - S = [sx, 0, 0, sy] - - interpolator = pyvips.Interpolate.new(interp_method) - resized = img.affine(S, - oarea=[0, 0, out_w, out_h], - interpolate=interpolator, - premultiplied=True - ) - - if is_array: - resized = vips2numpy(resized) - - return resized - - -def scale_dxdy(dxdy, out_shape_rc): - if isinstance(dxdy, np.ndarray): - vips_dxdy = numpy2vips(np.dstack(dxdy)) - else: - vips_dxdy = dxdy - - sxy = (np.array(out_shape_rc)/np.array([vips_dxdy.height, vips_dxdy.width]))[::-1] - scaled_dx = float(sxy[0])*vips_dxdy[0] - scaled_dy = float(sxy[1])*vips_dxdy[1] - scaled_dxdy = scaled_dx.bandjoin(scaled_dy) - scaled_dxdy = resize_img(scaled_dxdy, out_shape_rc) - - return scaled_dxdy - - -def get_src_img_shape_and_M(M, transformation_src_shape_rc, transformation_dst_shape_rc, dst_shape_rc): - """Determine the size of an image that, when warped, will have the same relative position - as in the original transformation dst image. - - For exmample, used to determine how large a source image needs to be in order to - be warped to land in the an image with shape dst_shape_rc. - - Parameters - ---------- - - M : ndarray - 3x3 affine transformation matrix to perform rigid warp on image with - shape `transformation_src_shape_rc`. The shape of this warped image - would be `transformation_dst_shape_rc`. - - transformation_dst_shape_rc : (int, int) - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - Returns - ------- - src_shape_rc : (int, int) - Shape of scaled image that can warped to an image with shape `dst_shape_rc` - - scaled_M : ndarray - A scaled version of `M` that will warp an image with shape `src_shape_rc` - - """ - img_corners_xy = get_corners_of_image(transformation_src_shape_rc)[::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc - ) - - dst_sxy = (np.array(dst_shape_rc)/np.array(transformation_dst_shape_rc))[::-1] - scaled_warped_corners = dst_sxy*warped_corners - scaled_M = scale_M(M, *dst_sxy) - - scaled_unwarped_corners = warp_xy(scaled_warped_corners, M=np.linalg.inv(scaled_M)) - src_slide_bbox = xy2bbox(scaled_unwarped_corners) - src_shape_rc = np.round(src_slide_bbox[2:] + src_slide_bbox[:2]).astype(int) - - return src_shape_rc, scaled_M - - -def save_img(dst_f, img, thumbnail_size=None): - """Save an image using pyvips - - Parameters - ---------- - dst_f : str - Filename for saved image - - img : ndarray, pyvips.Image - Image to be saved. Numpy arrays will be converted to pvips.Image - - thumbnail_size : optional, int - If not None, the image will be resized to fit within this size - - """ - if not isinstance(img, pyvips.Image): - vips_img = numpy2vips(img) - else: - vips_img = img - - if thumbnail_size is not None: - vips_wh = np.array([vips_img.width, vips_img.height]) - s = np.min(thumbnail_size/vips_wh) - if s < 1: - out_img = vips_img.resize(s) - else: - out_img = vips_img - else: - out_img = vips_img - - out_img.write_to_file(dst_f) - - -def get_pts_in_bbox(xy, xywh): - x0, y0 = xywh[0:2] - x1, y1 = xywh[0:2] + xywh[2:] - in_bbox_idx = np.where((xy[:, 0] >= x0) & (xy[:, 0] < x1) & (xy[:, 1] >= y0) & (xy[:, 1] < y1)==True)[0] - xy_in_bbox = xy[in_bbox_idx] - return xy_in_bbox, in_bbox_idx - - -def get_img_dimensions(img_f): - """ - Get image dimensions (width, height) without opening file - - Parameters - ---------- - img_f: str - Path to image - - Returns - ------- - img_dims : [(w, h)] - Image dimensions (width, height) - - """ - img = Image.open(img_f) - return img.size[0:2] - - -def get_shape(img): - """ Get shape of image (row, col, nchannels) - - Parameters - ---------- - - img : numpy.array, pyvips.Image - Image to get shape of - - Returns - ------- - shape_rc : numpy.array - Number of rows and columns and channels in the image - - """ - - if isinstance(img, pyvips.Image): - shape_rc = np.array([img.height, img.width]) - ndim = img.bands - else: - shape_rc = np.array(img.shape[0:2]) - - if img.ndim > 2: - ndim = img.shape[2] - else: - ndim = 1 - - shape = np.array([*shape_rc, ndim]) - - return shape - - -def apply_mask(img, mask): - """Mask an image - - """ - mask_is_vips = isinstance(mask, pyvips.Image) - if not mask_is_vips: - vips_mask = numpy2vips(mask) - else: - vips_mask = mask - - img_is_vips = isinstance(img, pyvips.Image) - if not img_is_vips: - vips_img = numpy2vips(img) - else: - vips_img = img.copy() - - masked_img = (vips_mask == 0).ifthenelse(0, vips_img) - - if not img_is_vips: - masked_img = vips2numpy(masked_img) - - return masked_img - - -def get_grid_bboxes(shape_rc, bbox_w, bbox_h, inclusive=False): - """ - Get list of bbox xywh for an image with shape shape_rc. Returned array ordered such that the bounding boxes go - left to right, top to bottom, starting at the top left of the image (r=0, c=0) - - Parameters - ---------- - shape_rc: (n_row, n_col) - Shape of the image - - bbox_w: int - Width of each bounding box - - bbox_h: int - Height of each bounding box - - inclusive: bool - If True, bbox_list will inclide boxes that go to edege of image, even if their width/height is smaller than - bbox_w or bbox_h. Default is False. - - Returns - ------- - bbox_list : [N, 4] array - Array containing the top left xy coordinates, width, height of each bounding box. Bounding boxes go from - left to right, top to bottom. - - Example - -------- - img_shape = (100, 200) - bbox_w = 20 - bbox_h = 20 - - bbox_list = get_grid_bboxes(img_shape, bbox_w, bbox_h) - - """ - - temp_x = np.arange(0, shape_rc[1], bbox_w) - temp_y = np.arange(0, shape_rc[0], bbox_h) - - if inclusive: - if shape_rc[1] not in temp_x: - temp_x = np.hstack([temp_x, shape_rc[1]]) - - if shape_rc[0] not in temp_y: - temp_y = np.hstack([temp_y, shape_rc[0]]) - - tl_y, tl_x = np.meshgrid(temp_y, temp_x, indexing="ij") - bbox_list = [[tl_x[i, j], - tl_y[i, j], - tl_x[i+1, j+1] - tl_x[i, j], - tl_y[i+1, j+1] - tl_y[i, j]] - for i in range(len(temp_y)-1) - for j in range(len(temp_x)-1)] - - return np.array(bbox_list) - - -def expand_bbox(bbox_xywh, expand, shape_rc=None): - new_xy = bbox_xywh[0:2] - expand - new_xy[new_xy < 0] = 0 - new_x, new_y = new_xy - - new_w, new_h = bbox_xywh[2:] + 2*expand - - if shape_rc is not None: - h, w = shape_rc - if new_x + new_w >= w: - new_w = w - new_x - - if new_y + new_h >= h: - new_h = h - new_y - - return np.array([*new_xy, new_w, new_h]) - - -def stitch_tiles(tile_list, tile_bboxes, nrow, ncol, overlap): - """ - #. Blend across row, added tiles to the right edge - #. Blend each row to bottom of the one above - """ - - is_array = False - if not isinstance(tile_list[0], pyvips.Image): - is_array = True - tile_list = [numpy2vips(tile) for tile in tile_list] - - row_mosaics = [None] * nrow - col_range = range(0, ncol) - for i in range(nrow): - col_tiles = [tile_list[index2d_to_1d(i, j, ncol=ncol)] for j in col_range] - row_mosaic = col_tiles[0] - - for j in range(1, ncol): - tile_idx = index2d_to_1d(i, j, ncol) - - # Get offset of where to merge right tile - right_bbbox = tile_bboxes[tile_idx] - left_idx = tile_idx - 1 - left_bbox = tile_bboxes[left_idx] - left_tile_br = left_bbox[2:] + left_bbox[:2] - x_offset, _ = left_tile_br - right_bbbox[:2] - offset = x_offset - row_mosaic.width - right_tile = col_tiles[j] - - row_mosaic = row_mosaic.merge(right_tile, "horizontal", offset, 0, mblend=overlap) - row_mosaics[i] = row_mosaic - - stitched = row_mosaics[0] - for i in range(1, nrow): - bottom_idx = index2d_to_1d(i, 0, ncol) - bottom_bbbox = tile_bboxes[bottom_idx] - - top_bbox = tile_bboxes[bottom_idx - ncol] - top_br = top_bbox[2:] + top_bbox[:2] - _, y_offset = top_br - bottom_bbbox[:2] - v_offset = y_offset - stitched.height - - bottom = row_mosaics[i] - stitched = stitched.merge(bottom, "vertical", 0, v_offset, mblend=overlap) - - if is_array: - stitched = vips2numpy(stitched) - - return stitched - - -def index2d_to_1d(row, col, ncol): - idx = (ncol*row) + col - - return idx - - -def index1d_to_2d(idx, ncol): - row = idx // ncol - col = idx % ncol - - return row, col - - -def get_triangular_mesh(x_pos, y_pos): - """Get a triangular mesh - - Parameters - ---------- - x_pos : ndarray - X-positions of each vertex - - y_pos : int - Y-positions of each vertex - - Returns - ------- - tri_verts : ndarray - X-Y coordinates of vertices - - tri_faces : ndarray - Indices of the vertices of each mesh face - - """ - - tl_y, tl_x = np.meshgrid(y_pos, x_pos, indexing="ij") - grid_boxes_wh = [[tl_x[i, j], - tl_y[i, j], - tl_x[i+1, j+1] - tl_x[i, j], - tl_y[i+1, j+1] - tl_y[i, j]] - for i in range(len(y_pos)-1) - for j in range(len(x_pos)-1)] - - grid_boxes_xy = [bbox2xy(wh) for wh in grid_boxes_wh] - vert_dict = {} - tri_faces = [] - current_max_vert_id = 0 - for bbox_xy in grid_boxes_xy: - bbox = xy2bbox(bbox_xy) - bbox_center_xy = tuple(bbox[0:2] + bbox[2:]/2) - bbox_tuples = [tuple(xy) for xy in bbox_xy] - for vert in bbox_tuples: - if not vert in vert_dict: - vert_dict[vert] = current_max_vert_id - current_max_vert_id += 1 - - vert_dict[bbox_center_xy] = current_max_vert_id - current_max_vert_id += 1 - - # 4 triangles in bbox. Bbbox : 0=TL, 1=TR, 2=BR, 3=BL # - # Each sorted clockwise, with A= being most top left - left_face = [vert_dict[bbox_tuples[0]], - vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[3]]] - - top_face = [vert_dict[bbox_tuples[0]], - vert_dict[bbox_tuples[1]], - vert_dict[bbox_center_xy]] - - right_face = [vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[1]], - vert_dict[bbox_tuples[2]]] - - btm_face = [vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[2]], - vert_dict[bbox_tuples[3]]] - - tri_faces.extend([left_face, top_face, right_face, btm_face]) - - - temp_tri_verts = list(vert_dict.keys()) - tri_verts = np.array([temp_tri_verts[i] for i in vert_dict.values()]) - tri_faces = np.array(tri_faces) - - return tri_verts, tri_faces - - -def mattes_mi(img1, img2, nbins=50, mask=None): - """Measure Mattes mutual information between 2 images. - - Parameters - ---------- - img1 : ndarray - First image with shape (N, M) - - img1 : ndarray - Second image with shape (N, M) - - nbins : int - Number of histogram bins - - mask : ndarray, None - Mask with shape (N, M) that indiates where the metric - should be calulated. If None, the metric will be calculated - for all NxM pixels. - - Returns - ------- - mmi : float - Mattes mutation inormation - - """ - - reg = sitk.ImageRegistrationMethod() - reg.SetMetricSamplingStrategy(reg.NONE) - reg.SetInitialTransform(sitk.Transform(2, sitk.sitkIdentity)) - reg.SetMetricAsMattesMutualInformation(numberOfHistogramBins=nbins) - if mask is not None: - sitk_mask = sitk.GetImageFromArray(mask) - reg.SetMetricFixedMask(sitk_mask) - reg.SetMetricMovingMask(sitk_mask) - - if not np.issubdtype(img1.dtype, np.floating): - img1 = img1.astype(float) - - if not np.issubdtype(img2.dtype, np.floating): - img2 = img2.astype(float) - - mmi = reg.MetricEvaluate(sitk.GetImageFromArray(img1), sitk.GetImageFromArray(img2)) - - return -1*mmi - - -def calc_rotated_shape(w, h, degree): - ### https://stackoverflow.com/questions/3231176/how-to-get-size-of-a-rotated-rectangle - - rad = np.deg2rad(degree) - new_w = np.abs(w * np.cos(rad)) + np.abs(h * np.sin(rad)) - new_h = np.abs(w * np.sin(rad)) + np.abs(h * np.cos(rad)) - - - return new_w, new_h - - -def order_points(pts_xy): - """ - Order points in clockwise order (TL, TR, BR, BL) - https://www.pyimagesearch.com/2016/03/21/ordering-coordinates-clockwise-with-python-and-opencv/ - - Parameters - ---------- - pts_xy : [N, 2] array - Points to order clockwise, in xy coordinates - - Returns - ------- - cw_pts_xy : [N, 2] array - Points ordered clockwise, in xy coordinates - - """ - - ### https://math.stackexchange.com/questions/978642/how-to-sort-vertices-of-a-polygon-in-counter-clockwise-order - - # warnings.warn("Outpout is now clockwise. May need update functions that call this") - # sort the points based on their x-coordinates - xSorted = pts_xy[np.argsort(pts_xy[:, 0]), :] - # grab the left-most and right-most points from the sorted - # x-roodinate points - leftMost = xSorted[:2, :] - rightMost = xSorted[2:, :] - - # now, sort the left-most coordinates according to their - # y-coordinates so we can grab the top-left and bottom-left - # points, respectively - leftMost = leftMost[np.argsort(leftMost[:, 1]), :] - (tl, bl) = leftMost - # now that we have the top-left coordinate, use it as an - # anchor to calculate the Euclidean distance between the - # top-left and right-most points; by the Pythagorean - # theorem, the point with the largest distance will be - # our bottom-right point - D = spatial.distance.cdist(tl[np.newaxis], rightMost, "euclidean")[0] - (br, tr) = rightMost[np.argsort(D)[::-1], :] - - # return the coordinates in top-left, top-right, - # bottom-right, and bottom-left order - cw_pts_xy = np.array([tl, tr, br, bl], dtype="float32") - - return cw_pts_xy - - -def get_resize_M(in_shape_rc, out_shape_rc): - - in_corners = get_corners_of_image(in_shape_rc) - out_corners = get_corners_of_image(out_shape_rc) - sy, sx = out_corners[2]/in_corners[2] - - resize_M = np.identity(3) - resize_M[0, 0] = sx - resize_M[1, 1] = sy - - return resize_M - - -def get_corners_of_image(shape_rc): - """ - Get corners of image in clockwise order (TL, TR, BR, BL) - - Parameters - ---------- - shape_rc : (int, int) - Chape of image that corners come from - - Returns - ------- - corners_rc : 4 x 2 array - Array with positions of each corner, sorted clockwise, and in row-col coordinates - - """ - - max_x = shape_rc[1] - max_y = shape_rc[0] - bl = [0, 0] - br = [max_x, 0] - tl = [0, max_y] - tr = [max_x, max_y] - - corners = np.array([bl, br, tr, tl]) - corners_rc = corners[:, ::-1] - - return corners_rc - - -def _numpy2vips_pre_22(a): - """ - From https://stackoverflow.com/questions/61138272/efficiently-saving-tiles-to-a-bigtiff-image - - """ - dtype_to_format = { - 'uint8': 'uchar', - 'int8': 'char', - 'uint16': 'ushort', - 'int16': 'short', - 'uint32': 'uint', - 'int32': 'int', - 'float32': 'float', - 'float64': 'double', - 'complex64': 'complex', - 'complex128': 'dpcomplex', - } - - if a.ndim > 2: - height, width, bands = a.shape - else: - height, width = a.shape - bands = 1 - - linear = a.reshape(width * height * bands) - vi = pyvips.Image.new_from_memory(linear.data, width, height, bands, - dtype_to_format[str(a.dtype)]) - return vi - - -def _vips2numpy_pre_22(vi): - """ - https://github.com/libvips/pyvips/blob/master/examples/pil-numpy-pyvips.py - - """ - format_to_dtype = { - 'uchar': np.uint8, - 'char': np.int8, - 'ushort': np.uint16, - 'short': np.int16, - 'uint': np.uint32, - 'int': np.int32, - 'float': np.float32, - 'double': np.float64, - 'complex': np.complex64, - 'dpcomplex': np.complex128, - } - - img = np.ndarray(buffer=vi.write_to_memory(), - dtype=format_to_dtype[vi.format], - shape=[vi.height, vi.width, vi.bands]) - if vi.bands == 1: - img = img[..., 0] - - return img - - -def _vips2numpy_22(vi): - img = vi.numpy() - - return img - - -def _numpy2vips_22(a): - vi = pyvips.Image.new_from_array(a) - - return vi - - -def numpy2vips(a): - - if is_pyvips_22(): - vi = _numpy2vips_22(a) - else: - vi = _numpy2vips_pre_22(a) - - return vi - -def vips2numpy(vi): - if is_pyvips_22(): - a = _vips2numpy_22(vi) - else: - a = _vips2numpy_pre_22(vi) - - return a - - -def pad_img(img, padded_shape): - padding_T = get_padding_matrix(img.shape[0:2], padded_shape) - padded_img = warp_img(img, padding_T, out_shape_rc=padded_shape) - - return padded_img, padding_T - -def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, - transformation_src_shape_rc=None, - transformation_dst_shape_rc=None, - bbox_xywh=None, - bg_color=None, - interp_method="bicubic"): - """Warp an image using rigid and/or non-rigid transformations - - Warp an image using the trasformations defined by `M` and the optional - displacement field, `bk_dxdy`. Transformations will be scaled so that - they can be applied to the image. - - Parameters - ---------- - img : ndarray, optional - Image to be warped - - M : ndarray, optional - 3x3 Affine transformation matrix to perform rigid warp - - bk_dxdy : ndarray, optional - A list containing the backward x-axis (column) displacement, - and y-axis (row) displacement applied after the rigid transformation. - - out_shape_rc : tuple of int - Shape of the `img` after warping. - - transformation_src_shape_rc : tuple of int - Shape of image that was used to find the transformations M and/or `bk_dxdy`. - For example, this could be the original image in which features - were detected - - transformation_dst_shape_rc : tuple of int - Shape of image with shape transformation_src_shape_rc after - being warped. Should be specified if `img` is a rescaled - version of the image for which the `M` and `bk_dxdy` were found. - - bbox_xywh : tuple - Bounding box to crop warped image. Should be in reference to the image - with shape = `out_shape_rc`, which may or not be the same as - `transformation_dst_shape_rc`. For example, to crop a region - from a large warped slide, `bbox_xywh` should refer to an area - in that warped slide, not an area in the image used to find the - transformation. - - bg_color : optional, list - Background color, if `None`, then the background color will be black - - interp_method : str, optional - - Returns - ------- - warped : ndarray, pyvips.Image - Warped version of `img` - - """ - - is_array = False - if not isinstance(img, pyvips.Image): - is_array = True - img = numpy2vips(img) - - src_shape_rc = np.array([img.height, img.width]) - if transformation_src_shape_rc is None: - transformation_src_shape_rc = src_shape_rc - - # Determine shape of unscaled output. If not provided, find shape big enough to avoid cropping - if transformation_dst_shape_rc is None: - if bk_dxdy is not None: - if isinstance(bk_dxdy, pyvips.Image): - transformation_dst_shape_rc = np.array([bk_dxdy.height, bk_dxdy.width]) - else: - transformation_dst_shape_rc = bk_dxdy[0].shape - elif out_shape_rc is not None: - transformation_dst_shape_rc = out_shape_rc - else: - transformation_src_corners_rc = get_corners_of_image(transformation_src_shape_rc) - warped_transformation_src_corners_xy = warp_xy(transformation_src_corners_rc[:, ::-1], M) - transformation_dst_shape_rc = np.ceil(np.max(warped_transformation_src_corners_xy[:, ::-1], axis=0)).astype(int) - - # Determine shape of scaled output - if out_shape_rc is None: - out_shape_rc = transformation_dst_shape_rc - - src_shape_rc = np.array(src_shape_rc) - transformation_src_shape_rc = np.array(transformation_src_shape_rc) - out_shape_rc = np.array(out_shape_rc) - transformation_dst_shape_rc = np.array(transformation_dst_shape_rc) - - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors( - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=out_shape_rc, - bk_dxdy=bk_dxdy) - if bbox_xywh is not None: - do_crop = True - else: - do_crop = False - - # Determine if any transformations need to be done - if M is not None: - do_rigid = True - else: - do_rigid = False - - if bk_dxdy is not None: - do_non_rigid = True - else: - do_non_rigid = False - - if not any([do_rigid, do_non_rigid, do_crop]): - if is_array: - img = vips2numpy(img) - return img - - # Do transformations - if bg_color is None: - bg_color = [0] * img.bands - bg_extender = pyvips.enums.Extend.BLACK - else: - bg_extender = pyvips.enums.Extend.BACKGROUND - bg_color = list(bg_color) - - interpolator = pyvips.Interpolate.new(interp_method) - if do_rigid: - if not np.all(src_sxy == 1): - img_corners_xy = get_corners_of_image(src_shape_rc)[::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, - dst_shape_rc=out_shape_rc) - M_tform = transform.ProjectiveTransform() - M_tform.estimate(warped_corners, img_corners_xy) - warp_M = M_tform.params - - else: - warp_M = M - - tx, ty = warp_M[:2, 2] - warp_M = np.linalg.inv(warp_M) - vips_M = warp_M[:2, :2].reshape(-1).tolist() - affine_warped = img.affine(vips_M, - oarea=[0, 0, out_shape_rc[1], out_shape_rc[0]], - interpolate=interpolator, - idx=-tx, - idy=-ty, - premultiplied=True, - background=bg_color, - extend=bg_extender - ) - else: - affine_warped = img - - if do_non_rigid: - # Scale dxdy map - if not isinstance(bk_dxdy, pyvips.Image): - temp_dxdy = numpy2vips(np.dstack(bk_dxdy)) - else: - temp_dxdy = bk_dxdy - - if dst_sxy is not None: - scaled_dx = float(dst_sxy[0]) * temp_dxdy[0] - scaled_dy = float(dst_sxy[1]) * temp_dxdy[1] - vips_dxdy = scaled_dx.bandjoin(scaled_dy) - else: - vips_dxdy = temp_dxdy - - if dst_sxy is not None: - S = [dst_sxy[0], 0, 0, dst_sxy[1]] - else: - S = [1.0, 0.0, 0.0, 1.0] - - - warp_dxdy = vips_dxdy.affine(S, - oarea=[0, 0, out_shape_rc[1], out_shape_rc[0]], - interpolate=interpolator, - premultiplied=True) - - index = pyvips.Image.xyz(affine_warped.width, affine_warped.height) - warp_index = (index[0] + warp_dxdy[0]).bandjoin(index[1] + warp_dxdy[1]) - - try: - #Option to set backround color in mapim added in libvips 8.13 - warped = affine_warped.mapim(warp_index, - premultiplied=True, - background=bg_color, - extend=bg_extender, - interpolate=interpolator) - - except pyvips.error.Error: - warped = affine_warped.mapim(warp_index, interpolate=interpolator) - if bg_color is not None: - warped = (warped == 0).ifthenelse(bg_color, warped) - - else: - warped = affine_warped - - if bbox_xywh is not None: - warped = warped.extract_area(*bbox_xywh) - - if is_array: - warped = vips2numpy(warped) - - return warped - - -def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, bk_dxdy=None, bg_color=None, interp_method="bicubic"): - """Unwarp an image using rigid and/or non-rigid transformations - - Unwarp an image using the trasformations defined by `M` and the optional - displacement field, `bk_dxdy`. This is accomplished by inverting `M` and - using the "foward" displacements in `fwd_dxdy`. If `fwd_dxdy` is not provided, - `bk_dxdy` will be inverted. Transformations will be scaled so that they can be applied to the images - with different sizes. - - Parameters - ---------- - img : ndarray, optional - Image to be warped - - M : ndarray, optional - 3x3 Affine transformation matrix to perform rigid warp - - fwd_dxdy : ndarray, optional - A list containing the forward x-axis (column) displacement, - and y-axis (row) displacements. - - transformation_src_shape_rc : tuple of int - Shape of image that was used to find the transformations M and/or `bk_dxdy`. - For example, this could be the original image in which features - were detected - - transformation_dst_shape_rc : tuple of int - Shape of image with shape transformation_src_shape_rc after - being warped. Should be specified if `img` is a rescaled - version of the image for which the `M` and `bk_dxdy` were found. - - src_shape_rc : tuple of int - Shape of the `img` before warping. - - bg_color : optional, list - Background color, if `None`, then the background color will be black - - interp_method : str, optional - - Returns - ------- - warped : ndarray, pyvips.Image - Warped version of `img` - - """ - - do_non_rigid = bk_dxdy is not None or fwd_dxdy is not None - do_rigid = M is not None - - if not do_rigid and not do_non_rigid: - return img - - is_array = False - if not isinstance(img, pyvips.Image): - is_array = True - img = numpy2vips(img) - - warped_src_shape_rc = np.array([img.height, img.width]) - if transformation_dst_shape_rc is None: - transformation_dst_shape_rc = warped_src_shape_rc - - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=warped_src_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) - - # Do transformations - if bg_color is None: - bg_color = [0] * img.bands - bg_extender = pyvips.enums.Extend.BLACK - else: - bg_extender = pyvips.enums.Extend.BACKGROUND - bg_color = list(bg_color) - - interpolator = pyvips.Interpolate.new(interp_method) - # Undo non-rigid transformation # - if do_non_rigid: - if bk_dxdy is not None and fwd_dxdy is None: - fwd_dxdy = get_inverse_field(bk_dxdy) - - if not isinstance(fwd_dxdy, pyvips.Image): - temp_dxdy = numpy2vips(np.dstack(fwd_dxdy)) - else: - temp_dxdy = fwd_dxdy - - if dst_sxy is not None: - scaled_dx = float(dst_sxy[0]) * temp_dxdy[0] - scaled_dy = float(dst_sxy[1]) * temp_dxdy[1] - vips_dxdy = scaled_dx.bandjoin(scaled_dy) - else: - vips_dxdy = temp_dxdy - - if dst_sxy is not None: - S = [dst_sxy[0], 0, 0, dst_sxy[1]] - else: - S = [1.0, 0.0, 0.0, 1.0] - - warp_dxdy = vips_dxdy.affine(S, - oarea=[0, 0, img.width, img.height], - interpolate=interpolator, - premultiplied=True) - - index = pyvips.Image.xyz(img.width, img.height) - warp_index = (index[0] + warp_dxdy[0]).bandjoin(index[1] + warp_dxdy[1]) - - try: - #Option to set backround color in mapim added in libvips 8.13 - nr_warped = img.mapim(warp_index, - premultiplied=True, - background=bg_color, - extend=bg_extender, - interpolate=interpolator) - - except pyvips.error.Error: - nr_warped = img.mapim(warp_index, interpolate=interpolator) - if bg_color is not None: - nr_warped = (nr_warped == 0).ifthenelse(bg_color, nr_warped) - - else: - nr_warped = img - - if do_rigid: - - img_corners_xy = get_corners_of_image(src_shape_rc)[::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, - dst_shape_rc=warped_src_shape_rc) - M_tform = transform.ProjectiveTransform() - M_tform.estimate(img_corners_xy, warped_corners) - warp_M = M_tform.params - - tx, ty = warp_M[:2, 2] - warp_M = np.linalg.inv(warp_M) - vips_M = warp_M[:2, :2].reshape(-1).tolist() - warped = img.affine(vips_M, - oarea=[0, 0, src_shape_rc[1], src_shape_rc[0]], - interpolate=interpolator, - idx=-tx, - idy=-ty, - premultiplied=True, - background=bg_color, - extend=bg_extender - ) - - else: - warped = nr_warped - - - if is_array: - warped = vips2numpy(warped) - - return warped - - -def warp_img_from_to(img, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, - from_dst_shape_rc=None, from_bk_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_bk_dxdy=None, to_fwd_dxdy=None, bg_color=None, interp_method="bicubic"): - """Warp image onto another - - Warps `img` to registered coordinates using the "from" parameters, and then uses - the inverse "to" parameters to warp that image to the "to" image's coordinate system. - Can be useful for transfering annotations from one image to another. - - Note: If `img` is a labeled image, it is recommended to set `interp_method` to "nearest" - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - from_M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp in the "from" image - - from_transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation in the - "from" image. For example, this could be the original image in - which features were detected in the "from" image. - - from_transformation_dst_shape_rc : (int, int) - Shape (row, col) of registered image. As the "from" and "to" images have been registered, - this shape should be the same for both images. - - from_src_shape_rc : optional, (int, int) - Shape of the unwarped image from which the points originated. For example, - this could be a larger/smaller version of the "from" image that was - used for feature detection. - - from_dst_shape_rc : optional, (int, int) - Shape of from image (with shape `src_shape_rc`) after warping - - from_bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y in the "from" image. - dx = bk_dxdy[0], and dy=bk_dxdy[1]. - - from_fwd_dxdy : ndarray - Inverse of `from_bk_dxdy` - - to_M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp in the "to" image - - to_transformation_src_shape_rc : optional, (int, int) - Shape of "to" image that was used to find the transformations. - For example, this could be the original image in which features were detected - - to_src_shape_rc : optional, (int, int) - Shape of the unwarped "to" image to which the points will be warped. For example, - this could be a larger/smaller version of the "to" image that was - used for feature detection. - - to_bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y in the "to" image. - dx = bk_dxdy[0], and dy=bk_dxdy[1]. - - to_fwd_dxdy : ndarray - Inverse of `to_bk_dxdy` - - bg_color : optional, list - Background color, if `None`, then the background color will be black - - interp_method : str, optional - - Returns - ------- - in_target_space : ndarray, pvips.Image - `img` warped onto the "to" image - - """ - - - in_reg_space = warp_img(img, - M=from_M, - bk_dxdy=from_bk_dxdy, - out_shape_rc=from_dst_shape_rc, - transformation_src_shape_rc=from_transformation_src_shape_rc, - transformation_dst_shape_rc=from_transformation_dst_shape_rc, - bg_color=bg_color, - interp_method=interp_method - ) - - in_target_space = warp_img_inv(img=in_reg_space, - M=to_M, - fwd_dxdy=to_fwd_dxdy, - transformation_src_shape_rc=to_transformation_src_shape_rc, - transformation_dst_shape_rc=to_transformation_dst_shape_rc, - src_shape_rc=to_src_shape_rc, - bk_dxdy=to_bk_dxdy, - bg_color=bg_color, - interp_method=interp_method - ) - - return in_target_space - - -def crop_img(img, xywh): - is_array = False - if not isinstance(img, pyvips.Image): - is_array = True - img = numpy2vips(img) - - wh = np.round(xywh[2:]).astype(int) - cropped = img.extract_area(*xywh[:2], *wh) - if is_array: - cropped = vips2numpy(cropped) - - return cropped - - -def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, - dst_shape_rc=None, transformation_src_shape_rc=None, - src_shape_rc=None, return_xy=False): - """Get map to warp an image - Get a coordinate map that will perform the warp defined by M and the optional displacement field, dxdy - Map can be scaled so that it can be applied to an image with shape unwarped_out_shape_rc - Result is returned as a pyvips.Image, but it can be converted to a numpy array. - - Parameters - ---------- - M : ndarray, optional - 3x3 Affine transformation matrix to perform rigid warp - - dxdy : ndarray, optional - A list containing the x-axis (column) displacement, and y-axis (row) - displacement. Will be applied after `M` (if available) - - transformation_dst_shape_rc : tuple of int - Shape of the image with shape transformation_src_shape_rc after warping. - This could be the shape of the original image after being warped - - dst_shape_rc : tuple of int, optional - Shape of image (with shape out_shape_rc) after warping - - transformation_src_shape_rc : tuple of int - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - src_shape_rc : tuple of int, optional - Shape of the image to which the transform will be applied. For example, this could be a larger/smaller - version of the image that was used for feature detection. - - - Returns - ------- - coord_map : ndarry - A 2band numpy array that has location of each pixel in - `src_shape_rc` the warped image (with shape `dst_shape_rc`) - - """ - - - if M is None and dxdy is None: - warnings.warn("Please provide `M` and/or `dxdy`") - return None - - if dxdy is None and transformation_dst_shape_rc is None: - warnings.warn("Please provide `transformation_dst_shape_rc`") - return None - - if dxdy is not None and transformation_dst_shape_rc is None: - transformation_dst_shape_rc = dxdy[0].shape - - if dst_shape_rc is None: - dst_shape_rc = transformation_dst_shape_rc - - if src_shape_rc is None: - src_shape_rc = transformation_src_shape_rc - - - if np.all(transformation_dst_shape_rc == dst_shape_rc): - grid_r, grid_c = np.indices(transformation_dst_shape_rc) - - else: - scaled_y = np.linspace(0, dst_shape_rc[0], num=transformation_dst_shape_rc[0]) - scaled_x = np.linspace(0, dst_shape_rc[1], num=transformation_dst_shape_rc[1]) - grid_y, grid_x = np.meshgrid(scaled_y, scaled_x, indexing="ij") - scaled_xy = np.dstack([grid_x.reshape(-1), grid_y.reshape(-1)])[0] - sy, sx = np.array(dst_shape_rc)/np.array(transformation_dst_shape_rc) - S = transform.SimilarityTransform(scale=(sx, sy)) - src_xy_pos = S.inverse(scaled_xy) - grid_r, grid_c = src_xy_pos[:, 1].reshape(transformation_dst_shape_rc), src_xy_pos[:, 0].reshape(transformation_dst_shape_rc) - - if dxdy is None: - r_in_src = grid_r - c_in_src = grid_c - else: - r_in_src = grid_r + dxdy[1] - c_in_src = grid_c + dxdy[0] - - if M is not None: - tformer = transform.ProjectiveTransform(matrix=M) - xy_pos_in_src = tformer(np.dstack([c_in_src.reshape(-1), r_in_src.reshape(-1)])[0]) - xy_pos_in_src = [xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc)] - - else: - xy_pos_in_src = [c_in_src, r_in_src] - - if np.any(transformation_src_shape_rc != src_shape_rc): - in_scale_y, in_scale_x = np.array(src_shape_rc)/np.array(transformation_src_shape_rc) - in_S = transform.SimilarityTransform(scale=(in_scale_x, in_scale_y)) - xy_pos_in_src = in_S(np.dstack([xy_pos_in_src[0].reshape(-1), xy_pos_in_src[1].reshape(-1)])[0]) - xy_pos_in_src = [xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc)] - - if return_xy: - c1, c2 = 0, 1 - else: - c1, c2 = 1, 0 - - coord_map = np.array([xy_pos_in_src[c1], xy_pos_in_src[c2]]) - - return coord_map - - -def get_padding_matrix(img_shape_rc, out_shape_rc): - img_h, img_w = img_shape_rc - out_h, out_w = out_shape_rc - - d_h = (out_h - img_h) - d_w = (out_w - img_w) - - h_pad = d_h/2 - w_pad = d_w/2 - T = np.identity(3).astype(np.float64) - T[0, 2] = -w_pad - T[1, 2] = -h_pad - - return T - - -def get_reflection_M(reflect_x, reflect_y, shape_rc): - """Get transformation matrix to reflect an image - - Parameters - ---------- - - reflect_x : bool - Whether or not to reflect the x-axis (columns) - - reflecct y : bool - Whether or not to reflect the y-axis (rows) - - shape_rc : tuple of int - Shape of the image being reflected - - Returns - ------- - reflection_M : ndarray - Transformation matrix that will reflect an image along the - specified axes. - - """ - - reflection_M = np.eye(3) - if reflect_x: - reflection_M[0, 0] *= -1 - reflection_M[0, 2] += shape_rc[1] - 1 - - if reflect_y: - reflection_M[1, 1] *= -1 - reflection_M[1, 2] += shape_rc[0] - 1 - - return reflection_M - - -def get_img_area(img_shape_rc, M=None): - - prev_img_corners = get_corners_of_image(img_shape_rc)[:, ::-1] - - if M is not None: - prev_img_corners = warp_xy(prev_img_corners, M) - - prev_img_corners = order_points(prev_img_corners) - prev_area = 0.5*np.abs(np.dot(prev_img_corners[:, 0],np.roll(prev_img_corners[:, 1],1))-np.dot(prev_img_corners[:, 1],np.roll(prev_img_corners[:, 0],1))) - return prev_area - - -def get_overlap_mask(img1, img2): - mask = np.zeros_like(img1) - mask[img1 > 0] += 1 - mask[img2 > 0] += 1 - mask[mask != 2] = 0 - - return mask - - -def center_and_get_translation_matrix(img_shape_rc, x, y, w, h): - ''' - x, y, w, h attributes or - :param img_shape_rc: - :param x: - :param y: - :param w: - :param h: - :return: - ''' - - # Center smaller image inside larger image # - img_center_w = int(img_shape_rc[1] / 2) - img_center_h = int(img_shape_rc[0] / 2) - - - out_center_w = int(w / 2) + x - out_center_h = int(h / 2) + y - - x_center_shift = img_center_w - out_center_w - y_center_shift = img_center_h - out_center_h - - T = np.array([[1, 0, -x_center_shift], [0, 1, -y_center_shift]]).astype(np.float64) - - return T - - -def get_affine_transformation_params(M): - """ - Get individula components affine transformation. - Based on properties in skimage._geometric.AffineTransform - - Parameters - ---------- - M : (3,3) array - Transformation matrix found one of scikit-image's transformation objects - - Returns - ------- - (tx, ty) : (float, float) - Translation in X and Y direction - - rotation : float - Counter clockwise rotation, in radians - - (scale_x, scale_y) : (float, float) - Scale in the X and Y dimensions - - shear : float - Shear angle in counter-clockwise direction as radians. - - """ - - scale_x = np.sqrt(M[0, 0] ** 2 + M[1, 0] ** 2) - scale_y = np.sqrt(M[0, 1] ** 2 + M[1, 1] ** 2) - rotation = np.arctan2(M[1, 0], M[0, 0]) - tx, ty = M[0:2, 2] - shear = np.arctan2(-M[0, 1], M[1, 1]) - rotation - - return (tx, ty), rotation, (scale_x, scale_y), shear - - -def decompose_affine_transformation(M): - """ - Get individula components affine transformation. - Based on properties in skimage._geometric.AffineTransform - - Parameters - ---------- - M : (3,3) array - Transformation matrix found one of scikit-image's transformation objects - - Returns - ------- - T : (3,3) array - Translation matrix - - R : (3,3) array - counter-clockwise rotation matrix - - S : (3,3) array - Scaling matrix - - H : (3,3) array - Shear matrix - - """ - - txy, rotation, sxy, shear = get_affine_transformation_params(M) - - T = transform.AffineTransform(translation=txy).params - R = transform.AffineTransform(rotation=rotation).params - S = transform.AffineTransform(scale=sxy).params - H = transform.AffineTransform(shear=shear).params - - return T, R, S, H - - -def get_rotate_around_center_M(img_shape, rotation_rad): - #Based on skimage warp.rotate, but can have scaling at end - rows, cols = img_shape[0:2] - - # rotation around center - center = np.array((cols, rows)) / 2. - 0.5 - tform1 = transform.SimilarityTransform(translation=center) - tform2 = transform.SimilarityTransform(rotation=rotation_rad) - tform3 = transform.SimilarityTransform(translation=-center) - tform = tform3 + tform2 + tform1 - return tform.params - - -def calc_d(pt1, pt2): - """ - Calculate euclidean disrances between each pair coresponding points in pt1 and pt2 - - Parameters - ---------- - pt1 : (2, N) array - Array of N 2D points - - pt2 : (2, N) array - Array of N 2D points - - Returns - ------- - d : [N] - distnace between correspoing points in pt1 and pt2 - """ - - d = np.sqrt(np.sum((pt1 - pt2)**2, axis=1)) - return d - - -def get_mesh(shape, grid_spacing, bbox_rc_wh=None, inclusive=False): - """Get meshgrid for given shape and spacing. - - Can provide bbox positions to limit gridsize in image - - Parameters - ---------- - shape : tuple - Number of rows and columns in image - - grid_spacing : int - Number of pixels between gridpoints - - bbox_rc_wh : tuple - (row, column, width, height) of bounding box - - inclusive : bool - Whether or not to include image edges - - """ - - if bbox_rc_wh is not None: - min_r = bbox_rc_wh[0] - max_r = bbox_rc_wh[0] + bbox_rc_wh[3] - min_c = bbox_rc_wh[1] - max_c = bbox_rc_wh[1] + bbox_rc_wh[2] - else: - - min_r = 0 - min_c = 0 - max_r = shape[0] - max_c = shape[1] - - r_grid_pts = np.arange(min_r, max_r, grid_spacing) - c_grid_pts = np.arange(min_c, max_c, grid_spacing) - - if inclusive: - if max(r_grid_pts) != shape[0]-1: - r_grid_pts = np.hstack([r_grid_pts, shape[0]-1]) - - if max(c_grid_pts) != shape[1]-1: - c_grid_pts = np.hstack([c_grid_pts, shape[1]-1]) - - return np.meshgrid(r_grid_pts, c_grid_pts, indexing="ij") - - -def smooth_dxdy(dxdy, grid_spacing_ratio=0.015, sigma_ratio=0.005, - method="gauss"): - """Smooth displacement fields - - Use cubic interpolation to smooth displacement field - - Parameters - ---------- - dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the - x and y directions - - grid_spacing_ratio : float - Fraction of image shape that should be used for spacing - between points in grid used to smooth displacement fields. - Larger values will do more smoothing. Only used if method - is "cubic" - - sigma_ratio : float - Determines the amount of Gaussian smoothing, as - sigma = max(shape) *sigma_ratio. Larger values do more - smoothing. Only used if method is "gauss" - - method : str - If "gauss", then a Gaussian blur will be applied to the - deformation fields, using sigma defined by sigma_ratio. - If "cubic", then cubic interpolation is used to smooth - the fields, using grid_spacing_ratio to determine - the sampling points. - - Returns - ------- - smooth_dxdy : ndarray - Smoothed copy of dxdy - - """ - - dx, dy = dxdy - if method.lower().startswith("c"): - grid_spacing_x = dx.shape[1]*grid_spacing_ratio - grid_spacing_y = dx.shape[0]*grid_spacing_ratio - grid_spacing = int(np.mean([grid_spacing_x, grid_spacing_y])) - - subgrid_r, subgrid_c = get_mesh(dx.shape, grid_spacing, inclusive=True) - - grid = UCGrid((0.0, float(dx.shape[1]), int(subgrid_r.shape[1])), - (0.0, float(dx.shape[0]), int(subgrid_r.shape[0]))) - - grid_y, grid_x = np.indices(dx.shape) - grid_xy = np.dstack([grid_x.reshape(-1), grid_y.reshape(-1)]).astype(float)[0] - - dx_cubic_coeffs = filter_cubic(grid, dx[subgrid_r, subgrid_c]).T - dy_cubic_coeffs = filter_cubic(grid, dy[subgrid_r, subgrid_c]).T - smooth_dx = eval_cubic(grid, dx_cubic_coeffs, grid_xy).reshape(dx.shape) - smooth_dy = eval_cubic(grid, dy_cubic_coeffs, grid_xy).reshape(dx.shape) - - elif method.lower().startswith("g"): - sigma = sigma_ratio*np.max(dx.shape) - smooth_dx = filters.gaussian(dx, sigma=sigma) - smooth_dy = filters.gaussian(dy, sigma=sigma) - - return np.dstack([smooth_dx, smooth_dy]) - - -def get_inverse_field(backwards_xy_deltas, n_inter=10): - """ - Invert transform - """ - - sitk_bk_dxdy = sitk.GetImageFromArray(np.dstack(backwards_xy_deltas), isVector=True) - sitk_fw_dxdy = sitk.IterativeInverseDisplacementField(sitk_bk_dxdy, numberOfIterations=n_inter) - fwd_dxdy = sitk.GetArrayFromImage(sitk_fw_dxdy) - fwd_dxdy = [fwd_dxdy[..., 0], fwd_dxdy[..., 1]] - - return fwd_dxdy - - -def warp_xy_rigid(xy, inv_matrix): - """ Warp points - - Warp xy given an inverse transformation matrix found using one of scikit-image's transform objects - Inverse matrix should have been found using tform(dst, src) - Adpated from skimage._geometric.ProjectiveTransform._apply_mat - Changed so that inverse matrix (found using dst -> src) automatically inverted to warp points forward (src -> dst) - """ - xy = np.array(xy, copy=False, ndmin=2) - - x, y = np.transpose(xy) - src_pts = np.vstack((x, y, np.ones_like(x))) - try: - dst_pts = src_pts.T @ np.linalg.inv(inv_matrix).T - except np.linalg.LinAlgError : - print("Singular matrix") - dst_pts = src_pts.T @ np.linalg.pinv(inv_matrix).T - - # below, we will divide by the last dimension of the homogeneous - # coordinate matrix. In order to avoid division by zero, - # we replace exact zeros in this column with a very small number. - dst_pts[dst_pts[:, 2] == 0, 2] = np.finfo(float).eps - # rescale to homogeneous coordinates - dst_pts[:, :2] /= dst_pts[:, 2:3] - return dst_pts[:, :2] - - -def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, dst_shape_rc=None, bk_dxdy=None, fwd_dxdy=None): - """Get scaling factors needed to warp points - - If a returned value is None, it means there is no need to scale the image - Returns - ------- - src_sxy : ndarray - Scaling to go from transformation_src_shape_rc -> src_shape_rc (i.e. transformation_src_shape_rc/src_shape_rc) - - dst_sxy : ndarray - When `bk_dxdy` or `fwd_dxdy` is None, this is the scaling to go from - transformation_dst_shape_rc -> dst_shape_rc (i.e. dst_shape_rc/transformation_dst_shape_rc). - - When `bk_dxdy` or `fwd_dxdy` are provided, this is the scaling that goes from the - displacement -> `dst_shape_rc` - - displacement_sxy : - Scaling for dxdy for when non-rigid transformations found using an - image with a size different than transformation_dst_shape_rc. - - For example, if displacement was found on an image 2x the one with - `transformation_dst_shape_rc`, this would be 2. Used to warp points - from position in image with shape transformation_dst_shape_rc to position - in `bk_dxdy` or `fwd_dxdy`. - - displacement_shape_rc : (int, int) - Shape of displacement field used for non-rigid transforms - - """ - do_non_rigid = bk_dxdy is not None or fwd_dxdy is not None - - # convert shapes to arrays - if src_shape_rc is not None: - src_shape_rc = np.array(src_shape_rc) - - if transformation_src_shape_rc is not None: - transformation_src_shape_rc = np.array(transformation_src_shape_rc) - - if dst_shape_rc is not None: - dst_shape_rc = np.array(dst_shape_rc) - - if transformation_dst_shape_rc is not None: - transformation_dst_shape_rc = np.array(transformation_dst_shape_rc) - - # Get input scaling - if transformation_src_shape_rc is not None and src_shape_rc is not None: - # Scale points to where they would be in image with transformation_src_shape_rc - if np.all(transformation_src_shape_rc == src_shape_rc): - src_sxy = None - else: - src_sxy = (src_shape_rc/transformation_src_shape_rc)[::-1] - else: - src_sxy = None - - # Get output shapes - non_rigid_is_array = False - if bk_dxdy is not None or fwd_dxdy is not None: - if bk_dxdy is not None: - if not isinstance(bk_dxdy, pyvips.Image): - non_rigid_is_array = True - if fwd_dxdy is not None: - if not isinstance(fwd_dxdy, pyvips.Image): - non_rigid_is_array = True - - if do_non_rigid: - if bk_dxdy is not None: - if non_rigid_is_array: - displacement_shape_rc = np.array(bk_dxdy[0].shape) - else: - displacement_shape_rc = np.array([bk_dxdy.height, bk_dxdy.width]) - elif fwd_dxdy is not None: - if non_rigid_is_array: - displacement_shape_rc = np.array(fwd_dxdy[0].shape) - else: - displacement_shape_rc = np.array([fwd_dxdy.height, fwd_dxdy.width]) - - if transformation_dst_shape_rc is None and do_non_rigid: - transformation_dst_shape_rc = displacement_shape_rc - - if dst_shape_rc is None and transformation_dst_shape_rc is not None: - dst_shape_rc = transformation_dst_shape_rc - - # Get output scalings - if do_non_rigid: - if not np.all(transformation_dst_shape_rc == displacement_shape_rc): - # non-rigid found on scaled image - displacement_sxy = (displacement_shape_rc/transformation_dst_shape_rc)[::-1] - dst_sxy = (dst_shape_rc/displacement_shape_rc)[::-1] - else: - displacement_sxy = None - dst_sxy = (dst_shape_rc/transformation_dst_shape_rc)[::-1] - - if np.all(dst_sxy == 1): - dst_sxy = None - else: - # Determine how to scale to images for position in image with shape = dst_shape_rc - dst_sxy = None - displacement_shape_rc = None - displacement_sxy = None - if transformation_dst_shape_rc is not None and dst_shape_rc is not None: - if not np.all(dst_shape_rc == transformation_dst_shape_rc): - dst_sxy = (dst_shape_rc/transformation_dst_shape_rc)[::-1] - - return src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc - - - -def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=None, dst_sxy=None, displacement_sxy=None, displacement_shape_rc=None, pt_buffer=100): - """Warp single point when the displacement fields are pyvips.Image objects - - """ - do_non_rigid = vips_bk_dxdy is not None or vips_fwd_dxdy is not None - - if src_sxy is not None: - in_src_xy = xy/src_sxy - - else: - in_src_xy = xy - - if M is not None: - rigid_xy = warp_xy_rigid(in_src_xy, M).astype(float)[0] - if not do_non_rigid: - if dst_sxy is not None: - return rigid_xy*dst_sxy - else: - return rigid_xy - else: - rigid_xy = in_src_xy - - if displacement_sxy is not None: - # displacement was found on scaled version of the rigidly registered image. - # So move points into new displacement field - rigid_xy *= displacement_sxy - - - bbox_xy_tl = (rigid_xy - pt_buffer//2).astype(int) - bbox_xy_br = np.ceil(rigid_xy + pt_buffer//2).astype(int) - bbox_x01 = np.clip(np.array([bbox_xy_tl[0], bbox_xy_br[0]]), 0, displacement_shape_rc[1]) - bbox_y01 = np.clip(np.array([bbox_xy_tl[1], bbox_xy_br[1]]), 0, displacement_shape_rc[0]) - - bbox_w = -int(np.subtract(*bbox_x01)) - bbox_h = -int(np.subtract(*bbox_y01)) - region_bbox_xywh = np.array([bbox_x01[0], bbox_y01[0], bbox_w, bbox_h]) - - # Move point to position in tile - rigid_xy_in_tile = rigid_xy - region_bbox_xywh[:2] - - # Get region dxdy - if vips_bk_dxdy is None and vips_fwd_dxdy is not None: - vips_region_dxdy = vips_fwd_dxdy.extract_area(*region_bbox_xywh) - region_dxdy = vips2numpy(vips_region_dxdy) - elif vips_bk_dxdy is not None and vips_fwd_dxdy is None: - vips_region_bk_dxdy = vips_bk_dxdy.extract_area(*region_bbox_xywh) - region_bk_dxdy = vips2numpy(vips_region_bk_dxdy) - region_dxdy = np.dstack(get_inverse_field(region_bk_dxdy[..., 0], region_bk_dxdy[..., 1])) - - grid = UCGrid((0.0, float(bbox_w-1), int(bbox_w)), - (0.0, float(bbox_h-1), int(bbox_h))) - - dx_cubic_coeffs = filter_cubic(grid, region_dxdy[..., 0]).T - dy_cubic_coeffs = filter_cubic(grid, region_dxdy[..., 1]).T - - new_x = region_bbox_xywh[0] + rigid_xy_in_tile[0] + eval_cubic(grid, dx_cubic_coeffs, rigid_xy_in_tile) - new_y = region_bbox_xywh[1] + rigid_xy_in_tile[1] + eval_cubic(grid, dy_cubic_coeffs, rigid_xy_in_tile) - - nonrigid_xy = np.array([new_x, new_y]) - if dst_sxy is not None: - nonrigid_xy *= dst_sxy - - return nonrigid_xy - - -def _warp_xy_vips(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, pt_buffer=100): - """ - Warp xy points using M and/or bk_dxdy/fwd_dxdy. - Used when `vips_bk_dxdy` or `vips_fwd_dxdy` is a pyvips.Image - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - vips_bk_dxdy : pyvips.Image - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. If `bk_dxdy` is not None, but - `fwd_dxdy` is None, then `bk_dxdy` will be inverted to warp `xy`. - - vips_fwd_dxdy : pyvips.Image - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - This is what is actually used to warp the points. - - pt_buffer : int - This method slices the region surrounding the point from the displacement fields. - The `pt_buffer` determines the size of the window around the point. - - Returns - ------- - warped_xy : [P, 2] array - Array of warped xy coordinates for P points - - """ - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=vips_bk_dxdy, fwd_dxdy=vips_fwd_dxdy) - - - warped_xy = np.vstack([_warp_pt_vips(pt, M, vips_bk_dxdy=vips_bk_dxdy, vips_fwd_dxdy=vips_fwd_dxdy, src_sxy=src_sxy, dst_sxy=dst_sxy, displacement_sxy=displacement_sxy, displacement_shape_rc=displacement_shape_rc, pt_buffer=pt_buffer) for pt in xy]) - - return warped_xy - - -def _warp_xy_numpy(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, - bk_dxdy=None, fwd_dxdy=None): - """ - Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. If `bk_dxdy` is not None, but - `fwd_dxdy` is None, then `bk_dxdy` will be inverted to warp `xy`. - - fwd_dxdy : ndarray - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - This is what is actually used to warp the points. - - Returns - ------- - warped_xy : [P, 2] array - Array of warped xy coordinates for P points - - """ - - do_non_rigid = bk_dxdy is not None or fwd_dxdy is not None - - if M is None and not do_non_rigid: - return xy - - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) - if src_sxy is not None: - in_src_xy = xy/src_sxy - else: - in_src_xy = xy - - if M is not None: - rigid_xy = warp_xy_rigid(in_src_xy, M).astype(float) - if not do_non_rigid: - if dst_sxy is not None: - return rigid_xy*dst_sxy - else: - return rigid_xy - else: - rigid_xy = in_src_xy - - if displacement_sxy is not None: - # displacement was found on scaled version of the rigidly registered image. - # So move points into new displacement field - rigid_xy *= displacement_sxy - - if bk_dxdy is not None and fwd_dxdy is None: - fwd_dxdy = get_inverse_field(bk_dxdy) - - grid = UCGrid((0.0, float(displacement_shape_rc[1]-1), int(displacement_shape_rc[1])), - (0.0, float(displacement_shape_rc[0]-1), int(displacement_shape_rc[0]))) - - dx_cubic_coeffs = filter_cubic(grid, fwd_dxdy[0]).T - dy_cubic_coeffs = filter_cubic(grid, fwd_dxdy[1]).T - - new_x = rigid_xy[:, 0] + eval_cubic(grid, dx_cubic_coeffs, rigid_xy) - new_y = rigid_xy[:, 1] + eval_cubic(grid, dy_cubic_coeffs, rigid_xy) - - nonrigid_xy = np.dstack([new_x, new_y])[0] - if dst_sxy is not None: - nonrigid_xy *= dst_sxy - - return nonrigid_xy - - -def warp_xy(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, - bk_dxdy=None, fwd_dxdy=None, pt_buffer=100): - """ - Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - bk_dxdy : ndarray, pyvips.Image - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. If `bk_dxdy` is not None, but - `fwd_dxdy` is None, then `bk_dxdy` will be inverted to warp `xy`. - - fwd_dxdy : ndarray, pyvips.Image - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - This is what is actually used to warp the points. - - pt_buffer : int - If `bk_dxdy` or `fwd_dxdy` are pyvips.Image object, then - pt_buffer` determines the size of the window around the point used to - get the local displacements. - - - Returns - ------- - warped_xy : [P, 2] array - Array of warped xy coordinates for P points - - """ - - do_non_rigid = bk_dxdy is not None or fwd_dxdy is not None - - if M is None and not do_non_rigid: - return xy - - if isinstance(bk_dxdy, pyvips.Image) or isinstance(fwd_dxdy, pyvips.Image): - warped_xy = _warp_xy_vips(xy, M, transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - vips_bk_dxdy=bk_dxdy, vips_fwd_dxdy=fwd_dxdy, pt_buffer=pt_buffer) - else: - warped_xy = _warp_xy_numpy(xy, M, transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) - return warped_xy - - -def warp_xy_inv(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, dst_shape_rc=None, bk_dxdy=None, fwd_dxdy=None): - """Warp points from registered coordinates to original coordinates - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. This is what is actually used to warp the points. - - fwd_dxdy : ndarray - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - If `fwd_dxdy` is not None, but - `bk_dxdy` is None, then `fwd_dxdy` will be inverted to warp `xy`. - - """ - do_non_rigid = bk_dxdy is not None or fwd_dxdy is not None - - if M is None and not do_non_rigid: - return xy - - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) - - if dst_sxy is not None: - xy_in_reg_img = xy/dst_sxy - else: - xy_in_reg_img = xy - - # Get points into position in the rigid image # - if do_non_rigid: - if fwd_dxdy is not None and bk_dxdy is None: - bk_dxdy = get_inverse_field(fwd_dxdy) - - xy_in_rigid = warp_xy(xy_in_reg_img, fwd_dxdy=bk_dxdy) - if displacement_sxy is not None: - xy_in_rigid /= displacement_sxy - else: - xy_in_rigid = xy_in_reg_img - - if M is not None: - xy_inv = warp_xy(xy_in_rigid, M=np.linalg.inv(M)) - else: - xy_inv = xy_in_rigid - - if src_sxy is not None: - xy_inv *= src_sxy - - return xy_inv - - -def warp_xy_from_to(xy, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, from_src_shape_rc=None, - from_dst_shape_rc=None,from_bk_dxdy=None, from_fwd_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_dst_shape_rc=None, to_bk_dxdy=None, to_fwd_dxdy=None): - """Warp points in one image to their position in another unregistered image - - Takes a set of points found in the unwarped "from" image, and warps them to their - position in the unwarped "to" image. - - Parameters - ---------- - xy : ndarray - [P, 2] array of xy coordinates for P points - - from_M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp in the "from" image - - from_transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation in the - "from" image. For example, this could be the original image in - which features were detected in the "from" image. - - from_transformation_dst_shape_rc : (int, int) - Shape (row, col) of registered image. As the "from" and "to" images have been registered, - this shape should be the same for both images. - - from_src_shape_rc : optional, (int, int) - Shape of the unwarped image from which the points originated. For example, - this could be a larger/smaller version of the "from" image that was - used for feature detection. - - from_dst_shape_rc : optional, (int, int) - Shape of from image (with shape `src_shape_rc`) after warping - - from_bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y in the "from" image. - dx = bk_dxdy[0], and dy=bk_dxdy[1]. - - from_fwd_dxdy : ndarray - Inverse of `from_bk_dxdy` - - to_M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp in the "to" image - - to_transformation_src_shape_rc : optional, (int, int) - Shape of "to" image that was used to find the transformations. - For example, this could be the original image in which features were detected - - to_src_shape_rc : optional, (int, int) - Shape of the unwarped "to" image to which the points will be warped. For example, - this could be a larger/smaller version of the "to" image that was - used for feature detection. - - to_dst_shape_rc : optional, (int, int) - Shape of to image (with shape `src_shape_rc`) after warping - - to_bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in the x and y in the "to" image. - dx = bk_dxdy[0], and dy=bk_dxdy[1]. - - to_fwd_dxdy : ndarray - Inverse of `to_bk_dxdy` - - Returns - ------- - xy_in_to : ndarray - position of `xy` in the unwarped "to" image - - """ - - xy_in_reg_space = warp_xy(xy, M=from_M, - transformation_src_shape_rc=from_transformation_src_shape_rc, - transformation_dst_shape_rc=from_transformation_dst_shape_rc, - src_shape_rc=from_src_shape_rc, - dst_shape_rc=from_dst_shape_rc, - bk_dxdy=from_bk_dxdy, - fwd_dxdy=from_fwd_dxdy - ) - - xy_in_to_space = warp_xy_inv(xy_in_reg_space, M=to_M, - transformation_src_shape_rc=to_transformation_src_shape_rc, - transformation_dst_shape_rc=to_transformation_dst_shape_rc, - src_shape_rc=to_src_shape_rc, - dst_shape_rc=to_dst_shape_rc, - bk_dxdy=to_bk_dxdy, - fwd_dxdy=to_fwd_dxdy - ) - return xy_in_to_space - - -def clip_xy(xy, shape_rc): - """Clip xy coordintaes to be within image - - """ - clipped_x = np.clip(xy[:, 0], 0, shape_rc[1]) - clipped_y = np.clip(xy[:, 1], 0, shape_rc[0]) - - clipped_xy = np.dstack([clipped_x, clipped_y])[0] - return clipped_xy - - -def _warp_shapely(geom, warp_fxn, warp_kwargs, shift_xy=None): - """Warp a shapely geometry - Based on shapely.ops.trasform - - """ - if "dst_shape_rc" in warp_kwargs: - dst_shape_rc = warp_kwargs["dst_shape_rc"] - elif "to_dst_shape_rc" in warp_kwargs: - dst_shape_rc = warp_kwargs["to_dst_shape_rc"] - else: - dst_shape_rc = None - - if geom.is_empty: - return type(geom)([]) - if geom.geom_type in ("Point", "LineString", "LinearRing", "Polygon"): - if geom.geom_type in ("Point", "LineString", "LinearRing"): - warped_xy = warp_fxn(np.vstack(geom.coords), **warp_kwargs) - if shift_xy is not None: - warped_xy -= shift_xy - if dst_shape_rc is not None: - warped_xy = clip_xy(warped_xy, dst_shape_rc) - - return type(geom)(warped_xy.tolist()) - - elif geom.geom_type == "Polygon": - shell_xy = warp_fxn(np.vstack(geom.exterior.coords), **warp_kwargs) - if shift_xy is not None: - shell_xy -= shift_xy - - if dst_shape_rc is not None: - shell_xy = clip_xy(shell_xy, dst_shape_rc) - - shell = type(geom.exterior)(shell_xy.tolist()) - holes = [] - for ring in geom.interiors: - holes_xy = warp_fxn(np.vstack(ring.coords), **warp_kwargs) - if shift_xy is not None: - holes_xy -= shift_xy - if dst_shape_rc is not None: - holes_xy = clip_xy(holes_xy, dst_shape_rc) - - holes.append(type(ring)(holes_xy)) - - return type(geom)(shell, holes) - - elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection": - return type(geom)([_warp_shapely(part, warp_fxn, warp_kwargs) for part in geom.geoms]) - else: - raise shapely.errors.GeometryTypeError(f"Type {geom.geom_type!r} not recognized") - - -def warp_shapely_geom(geom, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, - bk_dxdy=None, fwd_dxdy=None, pt_buffer=100, shift_xy=None): - """ - Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy - - Parameters - ---------- - geom : shapely.geometery - Shapely geom to warp - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - bk_dxdy : ndarray, pyvips.Image - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. If `bk_dxdy` is not None, but - `fwd_dxdy` is None, then `bk_dxdy` will be inverted to warp `xy`. - - fwd_dxdy : ndarray, pyvips.Image - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - This is what is actually used to warp the points. - - pt_buffer : int - If `bk_dxdy` or `fwd_dxdy` are pyvips.Image object, then - pt_buffer` determines the size of the window around the point used to - get the local displacements. - - shift_xy : tuple of int, optional - How much to shift the geom after being warped - - Returns - ------- - warped_geom : shapely.geom - Warped `geom` - - """ - - warp_kwargs = {"M":M, - "transformation_src_shape_rc": transformation_src_shape_rc, - "transformation_dst_shape_rc": transformation_dst_shape_rc, - "src_shape_rc": src_shape_rc, - "dst_shape_rc": dst_shape_rc, - 'bk_dxdy': bk_dxdy, - "fwd_dxdy": fwd_dxdy, - "pt_buffer": pt_buffer} - - if shift_xy is not None: - shift_xy = np.array(shift_xy) - - warped_geom = _warp_shapely(geom, warp_xy, warp_kwargs, shift_xy) - - return warped_geom - - - -def warp_shapely_geom_from_to(geom, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, from_src_shape_rc=None, - from_dst_shape_rc=None,from_bk_dxdy=None, from_fwd_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_dst_shape_rc=None, to_bk_dxdy=None, to_fwd_dxdy=None): - """ - Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy - - Parameters - ---------- - geom : shapely.geometery - Shapely geom to warp - - M : ndarray, optional - 3x3 affine transformation matrix to perform rigid warp - - transformation_src_shape_rc : (int, int) - Shape of image that was used to find the transformation. - For example, this could be the original image in which features were detected - - transformation_dst_shape_rc : (int, int), optional - Shape of the image with shape `transformation_src_shape_rc` after warping. - This could be the shape of the original image after applying `M`. - - src_shape_rc : optional, (int, int) - Shape of the image from which the points originated. For example, - this could be a larger/smaller version of the image that was - used for feature detection. - - dst_shape_rc : optional, (int, int) - Shape of image (with shape `src_shape_rc`) after warping - - bk_dxdy : ndarray, pyvips.Image - (2, N, M) numpy array of pixel displacements in the x and y - directions from the reference image. dx = bk_dxdy[0], - and dy=bk_dxdy[1]. If `bk_dxdy` is not None, but - `fwd_dxdy` is None, then `bk_dxdy` will be inverted to warp `xy`. - - fwd_dxdy : ndarray, pyvips.Image - Inverse of bk_dxdy. dx = fwd_dxdy[0], and dy=fwd_dxdy[1]. - This is what is actually used to warp the points. - - pt_buffer : int - If `bk_dxdy` or `fwd_dxdy` are pyvips.Image object, then - pt_buffer` determines the size of the window around the point used to - get the local displacements. - - - Returns - ------- - warped_geom : shapely.geom - Warped `geom` - - """ - - warp_kwargs = {"from_M": from_M, - "from_transformation_src_shape_rc": from_transformation_src_shape_rc, - "from_transformation_dst_shape_rc": from_transformation_dst_shape_rc, - "from_src_shape_rc": from_src_shape_rc, - "from_dst_shape_rc":from_dst_shape_rc, - "from_bk_dxdy":from_bk_dxdy, - "from_fwd_dxdy":from_fwd_dxdy, - "to_M":to_M, - "to_transformation_src_shape_rc": to_transformation_src_shape_rc, - "to_transformation_dst_shape_rc": to_transformation_dst_shape_rc, - "to_src_shape_rc": to_src_shape_rc, - "to_dst_shape_rc": to_dst_shape_rc, "to_bk_dxdy": to_bk_dxdy, - "to_fwd_dxdy":to_fwd_dxdy} - - warped_geom = _warp_shapely(geom, warp_xy_from_to, warp_kwargs) - - return warped_geom - - -def get_inside_mask_idx(xy, mask): - """Remove points outside of mask - - Remove points that are outside of the mask - - Parameters - ---------- - xy : ndarray - (P, 2) array containing P points (xy coordinates) - - mask : ndarray - (N, M) unit8 array where 255 indicates the region of interest - 0 indicates background - - Returns - ------- - inside_mask_idx : ndarray - (Q) array containing the indices of points inside the mask. - - """ - mask_cnt, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE) - - inside_mask = np.array([cv2.pointPolygonTest(mask_cnt[0], - tuple(xy[i]), - False) - for i in range(xy.shape[0])]) - - inside_mask_idx = np.where(inside_mask == 1.0)[0] - - return inside_mask_idx - - -def mask2xy(mask): - if mask.ndim > 2: - mask_y, mask_x = np.where(np.all(mask > 0, axis=2)) - else: - mask_y, mask_x = np.where(mask > 0) - min_x = np.min(mask_x) - max_x = np.max(mask_x) - min_y = np.min(mask_y) - max_y = np.max(mask_y) - - bbox = np.array([ - [min_x, min_y], - [max_x+1, min_y], - [max_x+1, max_y+1], - [min_x, max_y+1] - ]) - - return bbox - - -def bbox2mask(x, y, w, h, shape): - mask = np.zeros(shape, dtype=np.uint8) - mask[y:y+h+1, x:x+w+1] = 255 - - return mask - - -def xy2bbox(xy): - min_x = np.min(xy[:, 0]) - max_x = np.max(xy[:, 0]) - min_y = np.min(xy[:, 1]) - max_y = np.max(xy[:, 1]) - w = abs(max_x - min_x) - h = abs(max_y - min_y) - - return(np.array([min_x, min_y, w, h])) - - -def bbox2xy(xywh): - """ - Get xy coordinates of bounding box, clockwise from top-left, i.e. TL, TR, BR, BL - - Parameters - ----------- - xywh: [4, ] array - (top left x-coordinate, top left y coordiante, width, height) of a bounding box - - Returns - ------- - bbox_xy: [4, 2] array - XY coordinates of bounding box, clockwise from top-left, i.e. TL, TR, BR, BL - - - Example - ------- - xywh = [10, 12, 5, 5] - bbox_corners = bbox2xy(xywh) - """ - x, y, w, h = xywh - tl = [x, y] - tr = [x + w, y] - br = [x + w, y + h] - bl = [x, y + h] - bbox_xy = np.array([tl, tr, br, bl]) - - return bbox_xy - - -def get_xy_inside_mask(xy, mask): - """Get indices of `xy` that are inside `mask` - - Parameters - ---------- - xy : ndarray - [N, 2] array of coordinates to check - - mask : ndarray - Binary image where 255 is considered foreground - - - Returns - ------- - keep_idx : ndarray - Indices of `xy` that are inside of `mask` - """ - - mask_cnt, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - mask_polys = [shapely.geometry.Polygon(np.squeeze(cnt)) for cnt in mask_cnt if len(cnt) > 2] - in_mask = np.zeros(xy.shape[0]) - for i, pt_xy in enumerate(xy): - pt = shapely.geometry.Point(pt_xy) - for poly in mask_polys: - if poly.within(pt) or poly.contains(pt): - in_mask[i] = 1 - break - - keep_idx = np.where(in_mask > 0)[0] - - - # draw_img = np.dstack([mask]*3) - # from skimage import draw - # for i in range(xy.shape[0]): - # circ_pos = draw.disk(xy[i][::-1], radius=3) - # if i in keep_idx: - # clr = [0, 255, 0] - # else: - # clr = [255, 0, 0] - - # draw_img[circ_pos] = clr - - # io.imsave(os.path.join(registrar.dst_dir, f"{slide_obj.name}_pt.png"), draw_img) - - - return keep_idx - - -def calc_total_error(error): - """ - Calculate error for alignments. Average error, weighted by proximiity to center of stack. - Errors towards the center of the stack should carry greater weight, because they throw off a larger number of slices - than errors in slides closer to the ends. - - """ - n = len(error) - mid_pt = n / 2 - # Errors in middle carry larger weight, since it throws off other half - dist_from_center = n - (np.abs(np.arange(0, n) - mid_pt)) - error_weights = dist_from_center / dist_from_center.sum() - weighted_error = np.average(error, weights=error_weights) - return weighted_error - - -def measure_error(src_xy, dst_xy, shape, feature_similarity=None): - """ - Calculates the relative Target Registration Error (rTRE) and median Euclidean distance between a set of corresponding - points (https://anhir.grand-challenge.org/Performance_Metrics/). If feature_similarity is not None, then - distances are weighted by feature similarity. More similar features should ideally be closer together. - - Parameters - ---------- - src_xy : [N, 2] array - XY coordinates of features in src image. Each element should correspond to a matching feature coordinate in dst_xy - - dst_xy : [N, 2] array - XY coordinates of features dst image. Each element should correspond to a matching feature coordinate in src_xy - - shape: (int, int) - number of rows and columns in the image. Should be same for src and dst images - - feature_similarity: optional, [N] - similarity of corresponding features in src image and dst image. Used to weight the median distance - - Returns - ------- - med_tre : float - Median relative Target Registration Error (rTRE) between images - - med_d : float - Median Euclidean distance between src_xy and dst_xy, optinally weighted by feature similarity - - """ - d = np.sqrt((src_xy[:, 0]-dst_xy[:, 0])**2 + (src_xy[:, 1]-dst_xy[:, 1])**2) - rtre = d/np.sqrt(np.sum(np.power(shape, 2))) - med_tre = np.median(rtre) - - if feature_similarity is not None: - med_d = weightedstats.weighted_median(d.tolist(), feature_similarity.tolist()) - else: - med_d = np.median(d) - - - return med_tre, med_d - - -def scale_M(M, scale_x, scale_y): - """Scale transformation matrix - - http://answers.opencv.org/question/26173/the-relationship-between-homography-matrix-and-scaling-images/ - - Parameters - ---------- - M : ndarray - 3x3 transformation matrix - - scale_x : float - How much to scale the transformation along the x-axis - - scale_y : float - How much to scale the transformation along the y-axis - - Returns - ------- - scaled_M : ndarray - 3x3 transformation matrix for use in an image with a - different shape - - """ - S = np.identity(3) - S[0, 0] = scale_x - S[1, 1] = scale_y - scaled_M = S @ M @ np.linalg.inv(S) - return scaled_M - - -def get_overlapping_poly(mesh_poly_coords): - """Clips mesh faces that overlap - - mesh_poly_coords : list of ndarray - List of poylgon vertices for each mesh faces. - - """ - buffer_v = 0.01 - poly_l = [Polygon(verts).buffer(-buffer_v) for verts in np.round(mesh_poly_coords, 2)] - s = STRtree(poly_l) - n_poly = len(poly_l) - overlapping_poly_list = [] - poly_diffs = [] - - def clip_poly(i): - poly = poly_l[i] - if not poly.is_valid: - overlapping_poly_list.append(poly.buffer(buffer_v)) - return None - - others = unary_union([p for p in s.query(poly) if p != poly and p.is_valid]) - intersection = poly.intersection(others) - - if intersection.area != 0: - - overlapping_poly_list.append(poly) - diff = others.difference(poly) - if isinstance(diff, MultiPolygon): - for g in diff.geoms: - poly_diffs.append(g.buffer(buffer_v)) - else: - poly_diffs.append(diff.buffer(buffer_v)) - - n_cpu = valtils.get_ncpus_available() - 1 - with parallel_backend("threading", n_jobs=n_cpu): - Parallel()(delayed(clip_poly)(i) for i in tqdm.tqdm(range(n_poly))) - - return overlapping_poly_list, poly_diffs - - -def untangle(dxdy, n_grid_pts=50, penalty=10e-6, mask=None): - """Remove tangles caused by 2D displacement - Based on method described in - "Foldover-free maps in 50 lines of code" Garanzha et al. 2021. - - Parameters - ---------- - dxdy : ndarray - 2xMxN array of displacement fields - - n_grid_pts : int, optional - Number of grid points to sample, in each dimension - - penalty : float - How much to penalize tangles - - mask : ndarray - Mask indicating which areas should be untangled - - Returns - ------- - untangled_dxdy : ndarray - Copy of `dxdy`, but with displacements adjusted so that they - won't introduce tangles. - - """ - - qut = QuadUntangler(dxdy, n_grid_pts=n_grid_pts, fold_penalty=penalty) - mesh = qut.mesh - if mask is not None: - frozen_mask = mask.copy() - if np.any(mask.shape != mesh.padded_shape): - padding_T = get_padding_matrix(mask.shape, mesh.padded_shape) - frozen_mask = transform.warp(frozen_mask, padding_T, output_shape=mesh.padded_shape, preserve_range=True) - # Freeze regions that aren't folded - frozen_mask[0:frozen_mask.shape[0]-1, 0:mesh.c_offset] = 0 # left - frozen_mask[0:frozen_mask.shape[0]-1, frozen_mask.shape[1]-mesh.c_offset : frozen_mask.shape[1]-1] = 0 # right - frozen_mask[0:mesh.r_offset, 0:frozen_mask.shape[1]-1] = 0 # top - frozen_mask[frozen_mask.shape[0]-mesh.r_offset : frozen_mask.shape[0] - 1, 0:frozen_mask.shape[1]-1] = 0 # bottom - frozen_point = frozen_mask[mesh.sample_pos_xy[:, 1].astype(int), - mesh.sample_pos_xy[:, 0].astype(int)].reshape(-1) == 0 - - qut.mesh.boundary = frozen_point - - - untangled_mesh = qut.untangle() - qut.mesh.x = untangled_mesh - untangled_coords = np.dstack([untangled_mesh[:mesh.nverts], untangled_mesh[mesh.nverts:]])[0] - untangled_coords *= mesh.scaling - untangled_dx = (mesh.sample_pos_xy[:, 0] - untangled_coords[:, 0]).reshape((mesh.nr, mesh.nc)) - untangled_dy = (mesh.sample_pos_xy[:, 1] - untangled_coords[:, 1]).reshape((mesh.nr, mesh.nc)) - - padded_shape = mesh.padded_shape - grid = UCGrid((0.0, float(padded_shape[1]), int(mesh.nc)), - (0.0, float(padded_shape[0]), int(mesh.nr))) - - dx_cubic_coeffs = filter_cubic(grid, untangled_dx).T - dy_cubic_coeffs = filter_cubic(grid, untangled_dy).T - - img_y, img_x = np.indices(padded_shape) - img_xy = np.dstack([img_x.reshape(-1), img_y.reshape(-1)]).astype(float)[0] - untangled_dx = eval_cubic(grid, dx_cubic_coeffs, img_xy).reshape(padded_shape) - untangled_dy = eval_cubic(grid, dy_cubic_coeffs, img_xy).reshape(padded_shape) - - inv_T = np.linalg.inv(mesh.padding_T) - untangled_dx = transform.warp(untangled_dx, inv_T, output_shape=mesh.shape_rc, preserve_range=True) - untangled_dy = transform.warp(untangled_dy, inv_T, output_shape=mesh.shape_rc, preserve_range=True) - untangled_dxdy = np.array([untangled_dx, untangled_dy]) - - return untangled_dxdy - - -def remove_folds_in_dxdy(dxdy, n_grid_pts=50, method="inpaint", paint_size=5000, fold_penalty=1e-6): - """Remove folds in displacement fields - - Find and remove folds in displacement fields - - Parameters - --------- - method : str, optional - "inpaint" will use inpainting to fill in areas idenetified - as containing folds. "regularize" will unfold those regions - using the mehod described in "Foldover-free maps in 50 lines of code" - Garanzha et al. 2021. - - n_grid_pts : int - Number of gridpoints used to detect folds. Also the number - of gridpoints to use when regularizing he mesh when - `method` = "regularize". - - paint_size : int - Used to determine how much to resize the image to have efficient inpainting. - Larger values = longer processing time. Only used if `method` = "inpaint". - - fold_penalty : float - How much to penalize folding/stretching. Larger values will make - the deformation field more uniform. Only used if `method` = "regularize" - - Returns - ------- - no_folds_dxdy : ndarray - An array containing the x-axis (column) displacement, and y-axis (row) - displacement after removing folds. - - """ - - # Use triangular mesh to find regions with folds - # TriangleMesh will warp triangle points using dxdy to determine location vertices in warped image - # It is assumed dxdy is a backwards transform found by registering images. - # Because TriMesh is warping points, the inverse of dxdy is used. - # Any image create from these points can be warped to their original position using dxdy - - valtils.print_warning("Looking for folds", None, rgb=Fore.YELLOW) - tri_mesh = TriangleMesh(dxdy, n_grid_pts) - padded_shape = tri_mesh.padded_shape - - tri_verts_xy = np.dstack([tri_mesh.x[:tri_mesh.nverts], tri_mesh.x[tri_mesh.nverts:]])[0]*tri_mesh.scaling - tri_xy = np.array([tri_verts_xy[t, :] for t in tri_mesh.tri]) - - overlapping_poly_list, poly_diff_list = get_overlapping_poly(tri_xy) - poly_overlap_mask = np.zeros(padded_shape, dtype=np.uint8) - for poly in overlapping_poly_list: - poly_r, poly_c = draw.polygon(*poly.exterior.xy[::-1], shape=padded_shape) - poly_overlap_mask[poly_r, poly_c] = 255 - - # Warp mask back to original image. Should isolaate regions that will cause folding - warp_map = get_warp_map(dxdy=tri_mesh.padded_dxdy) - src_folds_mask = transform.warp(poly_overlap_mask, warp_map, preserve_range=True) - src_folds_mask[src_folds_mask != 0] = 255 - src_folds_mask = ndimage.binary_fill_holes(src_folds_mask).astype(np.uint8)*255 - - folded_area = len(np.where(src_folds_mask > 0)[0]) - if folded_area == 0: - return dxdy - - if method == 'regularize': - valtils.print_warning("Removing folds using regularizaation", None, rgb=Fore.YELLOW) - # Untanlge folded regions using regularization - qut = QuadUntangler(dxdy, n_grid_pts=n_grid_pts, fold_penalty=fold_penalty) - mesh = qut.mesh - frozen_mask = src_folds_mask.copy() - # Freeze regions that aren't folded - frozen_mask[0:frozen_mask.shape[0]-1, 0:mesh.c_offset] = 0 # left - frozen_mask[0:frozen_mask.shape[0]-1, frozen_mask.shape[1]-mesh.c_offset : frozen_mask.shape[1]-1] = 0 # right - frozen_mask[0:mesh.r_offset, 0:frozen_mask.shape[1]-1] = 0 # top - frozen_mask[frozen_mask.shape[0]-mesh.r_offset : frozen_mask.shape[0] - 1, 0:frozen_mask.shape[1]-1] = 0 # bottom - frozen_point = frozen_mask[mesh.sample_pos_xy[:, 1].astype(int), - mesh.sample_pos_xy[:, 0].astype(int)].reshape(-1) == 0 - - qut.mesh.boundary = frozen_point - - # Untangle and interpolate - untangled_mesh = qut.untangle() - qut.mesh.x = untangled_mesh - untangled_coords = np.dstack([untangled_mesh[:mesh.nverts], untangled_mesh[mesh.nverts:]])[0] - untangled_coords *= mesh.scaling - untangled_dx = (mesh.sample_pos_xy[:, 0] - untangled_coords[:, 0]).reshape((mesh.nr, mesh.nc)) - untangled_dy = (mesh.sample_pos_xy[:, 1] - untangled_coords[:, 1]).reshape((mesh.nr, mesh.nc)) - - grid = UCGrid((0.0, float(padded_shape[1]), int(mesh.nc)), - (0.0, float(padded_shape[0]), int(mesh.nr))) - - dx_cubic_coeffs = filter_cubic(grid, untangled_dx).T - dy_cubic_coeffs = filter_cubic(grid, untangled_dy).T - - img_y, img_x = np.indices(padded_shape) - img_xy = np.dstack([img_x.reshape(-1), img_y.reshape(-1)]).astype(float)[0] - no_folds_dx = eval_cubic(grid, dx_cubic_coeffs, img_xy).reshape(padded_shape) - no_folds_dy = eval_cubic(grid, dy_cubic_coeffs, img_xy).reshape(padded_shape) - - else: - - s = np.sqrt(paint_size)/np.sqrt(folded_area) - if s > 1: - s = 1 - - inpaint_mask = transform.rescale(src_folds_mask, s, preserve_range=True) - - to_paint_dx = transform.rescale(tri_mesh.padded_dxdy[0], s, preserve_range=True) - painted_dx = restoration.inpaint_biharmonic(to_paint_dx, inpaint_mask) - smooth_dx = transform.resize(painted_dx, tri_mesh.padded_shape, preserve_range=True) - - to_paint_dy = transform.rescale(tri_mesh.padded_dxdy[1], s, preserve_range=True) - painted_dy = restoration.inpaint_biharmonic(to_paint_dy, inpaint_mask) - smooth_dy = transform.resize(painted_dy, tri_mesh.padded_shape, preserve_range=True) - - blending_mask = filters.gaussian(src_folds_mask, 1) - no_folds_dx = blending_mask*smooth_dx + (1-blending_mask)*tri_mesh.padded_dxdy[0] - no_folds_dy = blending_mask*smooth_dy + (1-blending_mask)*tri_mesh.padded_dxdy[1] - - # Crop to original shape # - no_folds_dx = transform.warp(no_folds_dx, inv_T, output_shape=tri_mesh.shape_rc, preserve_range=True) - no_folds_dy = transform.warp(no_folds_dy, inv_T, output_shape=tri_mesh.shape_rc, preserve_range=True) - no_folds_dxdy = np.array([no_folds_dx, no_folds_dy]) - - return no_folds_dxdy - - -class QuadMesh(object): - - def __init__(self, dxdy, n_grid_pts=50): - shape = np.array(dxdy[0].shape) - self.shape_rc = shape - grid_spacing = int(np.min(np.round(shape/n_grid_pts))) - - new_r = shape[0] - shape[0] % grid_spacing + grid_spacing - self.r_padding = new_r - shape[0] - sample_y = np.floor(np.arange(0, new_r + grid_spacing, grid_spacing)) - - new_c = shape[1] - shape[1] % grid_spacing + grid_spacing - sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) - self.c_padding = new_c - shape[1] - - nr = len(sample_y) - nc = len(sample_x) - padded_shape = np.array([new_r+1, new_c+1]) - self.padded_shape = padded_shape - y_center, x_center = padded_shape/2 - self.nverts = nr*nc - self.nr = nr - self.nc = nc - - self.r_offset, self.c_offset = (padded_shape - shape)//2 - - # Pad displacement # - self.padding_T = get_padding_matrix(shape, padded_shape) - - padded_dx = transform.warp(dxdy[0], self.padding_T, output_shape=padded_shape, preserve_range=True) - padded_dy = transform.warp(dxdy[1], self.padding_T, output_shape=padded_shape, preserve_range=True) - - self.padded_dxdy = np.array([padded_dx, padded_dy]) - # Flattend indices for each pixel in a quadrat - quads = [[r*nc + c, r*nc + c + 1, (r+1)*nc + c + 1, (r+1)*nc + c] for r in range(nr-1) for c in range(nc-1)] - self.quads = quads - self.boundary = [None] * self.nverts - - for i in range(self.nverts): - r_idx = i // nc - c_idx = i % nc - r = sample_y[r_idx] - c = sample_x[c_idx] - if r <= y_center or r >= new_r - y_center or c <= x_center or c >= new_c - x_center: - self.boundary[i] = True - - else: - self.boundary[i] = False - - sample_pos_y, sample_pos_x = np.meshgrid(sample_y, sample_x, indexing="ij") - unwarped_xy = np.dstack([sample_pos_x.reshape(-1), sample_pos_y.reshape(-1)])[0].astype(float) - self.sample_pos_xy = unwarped_xy - sample_xy = warp_xy(unwarped_xy, M=None, bk_dxdy=[padded_dx, padded_dy]) - self.warped_xy = sample_xy - scaled_coords = self.scale_coords(sample_xy) - self.x = np.hstack([scaled_coords[:, 0], scaled_coords[:, 1]]) - - def scale_coords(self, xy): - max_side = np.max(self.padded_shape) - scaled_coords = xy/max_side - self.scaling = max_side - - return scaled_coords - - - def __str__(self): - ret = "" - for v in range(self.nverts): - ret = ret + ("v %f %f 0\n" % (self.x[v], self.x[v+self.nverts])) - for f in self.quads: - ret = ret + ("f %d %d %d %d\n" % (f[0]+1, f[1]+1, f[2]+1, f[3]+1)) - return ret - - def show(self): - res = 1000 - off = 100 - image = Image.new(mode='L', size=(res, res), color=255) - draw = ImageDraw.Draw(image) - - for quad in self.quads: - for e in range(4): - i = quad[e] - j = quad[(e+1)%4] - - line = ((off+self.x[i]*res/2, off+self.x[i+self.nverts]*res/2), (off+self.x[j]*res/2, off+self.x[j+self.nverts]*res/2)) - draw.line(line, fill=128) - del draw - image.show() - - -class TriangleMesh(object): - def __init__(self, dxdy, n_grid_pts=50): - shape = np.array(dxdy[0].shape) - self.shape_rc = shape - grid_spacing = int(np.min(np.round(shape/n_grid_pts))) - - new_r = shape[0] - shape[0] % grid_spacing + grid_spacing - self.r_padding = new_r - shape[0] - sample_y = np.floor(np.arange(0, new_r + grid_spacing, grid_spacing)) - - new_c = shape[1] - shape[1] % grid_spacing + grid_spacing - sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) - self.c_padding = new_c - shape[1] - - nr = len(sample_y) - nc = len(sample_x) - padded_shape = np.array([new_r+1, new_c+1]) - self.padded_shape = padded_shape - self.r_offset, self.c_offset = (padded_shape - shape)//2 - - self.nverts = nr*nc - self.nr = nr - self.nc = nc - y_center, x_center = padded_shape/2 - - self.padding_T = get_padding_matrix(shape, padded_shape) - - padded_dx = transform.warp(dxdy[0], self.padding_T, output_shape=padded_shape, preserve_range=True) - padded_dy = transform.warp(dxdy[1], self.padding_T, output_shape=padded_shape, preserve_range=True) - - self.padded_dxdy = np.array([padded_dx, padded_dy]) - - # Get triangle vertices - sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) - sample_y = np.arange(0, new_r + grid_spacing, grid_spacing) - - tri_verts, tri_faces = get_triangular_mesh(sample_x, sample_y) - self.nverts = tri_verts.shape[0] - self.tri_verts = tri_verts - self.boundary = [None] * self.nverts - for i in range(self.nverts): - c, r = tri_verts[i] - - if r <= y_center or r >= new_r - y_center or c <= x_center or c >= new_c - x_center: - self.boundary[i] = True - else: - self.boundary[i] = False - - sample_xy = warp_xy(tri_verts, M=None, bk_dxdy=[padded_dx, padded_dy]) - self.warped_xy = sample_xy - - self.tri = tri_faces - self.nfacets = len(self.tri) - self.vert = self.scale_coords(sample_xy) - self.x = np.hstack([self.vert[:, 0], self.vert[:, 1]]) - - def scale_coords(self, xy): - - max_side = np.max(self.padded_shape) - scaled_coords = xy/max_side - self.scaling = max_side - - return scaled_coords - - -class QuadUntangler(object): - def __init__(self, dxdy, fold_penalty=1e-6, n_grid_pts=50): - self.shape = np.array(dxdy[0].shape) - self.mesh = QuadMesh(dxdy, n_grid_pts) - self.mesh_type = self.mesh.__class__.__name__ - self.n_grid_pts = n_grid_pts - self.n = self.mesh.nverts - self.fold_penalty = fold_penalty - - def untangle(self): - n = self.n - mesh = self.mesh - Q = [np.matrix('-1,-1;1,0;0,0;0,1'), np.matrix('-1,0;1,-1;0,1;0,0'), # quadratures for - np.matrix('0,0;0,-1;1,1;-1,0'), np.matrix('0,-1;0,0;1,0;-1,1') ] # every quad corner - - def jacobian(U, qc, quad): - return np.matrix([[U[quad[0] ], U[quad[1] ], U[quad[2] ], U[quad[3] ]], - [U[quad[0]+n], U[quad[1]+n], U[quad[2]+n], U[quad[3]+n]]]) * Q[qc] - - mindet = min([np.linalg.det( jacobian(mesh.x, qc, quad) ) for quad in mesh.quads for qc in range(4)]) - eps = np.sqrt(1e-6**2 + min(mindet, 0)**2) # the regularization parameter e - eps *= 1/self.fold_penalty - - def energy(U): # compute the energy and its gradient for the map u - F,G = 0, np.zeros(2*n) - for quad in mesh.quads: # sum over all quads - for qc in range(4): # evaluate the Jacobian matrix for every quad corner - J = jacobian(U, qc, quad) - det = np.linalg.det(J) - chi = det/2 + np.sqrt(eps**2 + det**2)/2 # the penalty function - chip = .5 + det/(2*np.sqrt(eps**2 + det**2)) # its derivative - - f = np.trace(np.transpose(J)*J)/chi # quad corner shape quality - F += f - dfdj = (2*J - np.matrix([[J[1,1],-J[1,0]],[-J[0,1],J[0,0]]])*f*chip)/chi - dfdu = Q[qc] * np.transpose(dfdj) # chain rule for the actual variables - for i,v in enumerate(quad): - if (mesh.boundary[v]): continue # the boundary verts are locked - G[v ] += dfdu[i,0] - G[v+n] += dfdu[i,1] - return F,G - - # factr are: 1e12 for low accuracy; 1e7 for moderate accuracy; 10.0 for extremely high accuracy. - factr = 1e7 - untangled = fmin_l_bfgs_b(energy, mesh.x, factr=factr)[0] # inner L-BFGS loop - - return untangled - - -class _TriUntangler(object): - def __init__(self, dxdy, n_grid_pts=50): - self.shape = np.array(dxdy[0].shape) - - # self.mesh = QuadMesh(dxdy, n_grid_pts) - self.mesh = TriangleMesh(dxdy, n_grid_pts) - self.mesh_type = self.mesh.__class__.__name__ - self.n_grid_pts = n_grid_pts - self.n = self.mesh.nverts - self.n_tri = len(self.mesh.tri) - - def triangle_area2d(self, a, b, c): - x = 0 - y = 1 - tri_area = .5*((b[y]-a[y])*(b[x]+a[x]) + (c[y]-b[y])*(c[x]+b[x]) + (a[y]-c[y])*(a[x]+c[x])) - return tri_area - - def triangle_aspect_ratio_2d(self, a, b, c): - - l1 = np.linalg.norm(b-a) - l2 = np.linalg.norm(c-b) - l3 = np.linalg.norm(a-c) - lmax = max([l1, l2, l3]) - - return lmax*(l1+l2+l3)/(4.*np.sqrt(3.)*self.triangle_area2d(a, b, c)) - - - def setup(self): - area = [None] * self.n_tri - ref_tri = [None] * self.n_tri - for t, faces in enumerate(self.mesh.tri): - - - ax, bx, cx = self.mesh.x[self.mesh.tri[t]] - ay, by, cy = self.mesh.x[self.mesh.tri[t]+ self.mesh.nverts] - - A = np.array([ax, ay]) - B = np.array([bx, by]) - C = np.array([cx, cy]) - - - area[t] = self.triangle_area2d(A, B, C) - - ar = self.triangle_aspect_ratio_2d(A, B, C) - if ar > 10: - #if the aspect ratio is bad, assign an equilateral reference triangle - l1 = np.linalg.norm(B-A) - l2 = np.linalg.norm(C-B) - l3 = np.linalg.norm(A-C) - a = (l1 + l2 + l3)/3 # edge length is the average of the original triangle - area[t] = np.sqrt(3.)/4.*a*a - A = np.array([0., 0.]) - B = np.array([a, 0.]) - C = np.array([a/2., np.sqrt(3.)/2.*a]) - - ST = np.matrix([B-A, C-A]) - ST_invert_transpose = np.linalg.inv(ST).T - ref_tri[t] = np.array([[-1, -1], [1, 0], [0, 1] ]) @ ST_invert_transpose - - self.area = area - self.ref_tri = ref_tri - - - def untangle(self): - self.setup() - def evaluate_jacobian(X, t): - J = np.matrix(np.zeros((2, 2))) - for i in range(3): - for d in range(2): - J[d] += self.ref_tri[t][i, d] + X[self.mesh.tri[t][i] + self.n*d] - - K = np.array([[J[1, 1], -J[1, 0]], - [-J[0, 1], J[0, 0]] - ]) - - det = np.linalg.det(J) - - return J, K, det - - mindet = np.inf - for t in range(self.n_tri): - _, _, det = evaluate_jacobian(self.mesh.x, t) - mindet = np.min([mindet, det]) - - eps = np.sqrt(1e-6**2 + min(mindet, 0)**2) # the regularization parameter e - theta = 1./128 - - def chi(eps, det): - if det < 0: - return (det + np.sqrt(eps*eps + det*det) + 10**-6)*.5 - else: - return .5*eps*eps / (np.sqrt(eps*eps + det*det) - det + 10**-6) - - def chi_deriv(eps, det): - return .5+det/(2.*np.sqrt(eps*eps + det*det + 10**-6)) - - - def energy(U): - - - F,G = 0, np.zeros(2*self.n) - - for t in range(self.n_tri): - J, K, det = evaluate_jacobian(U, t) - - c1 = chi(eps, det) - c2 = chi_deriv(eps, det) - - f = np.trace(np.transpose(J)*J)/c1 # corner shape quality - g = (1+det*det)/c1 - - F += ((1-theta)*f + theta*g)*self.area[t] - - for dim in range(2): - - a = J[dim] # tangent basis - b = K[dim] # dual basis - dfda = (a*2. - b*f*c2)/c1 - dgda = b*(2*det-g*c2)/c1 - for i in range(3): - v = self.mesh.tri[t][i] - if self.mesh.boundary[v]: continue # the boundary verts are locked - # og_pos = G[v+ dim*self.n] - G[v+ dim*self.n] += (self.ref_tri[t][i] @ np.transpose(dfda*(1.-theta) + dgda*theta))*self.area[t] - # new_pos = G[v+ dim*self.n] - # print(new_pos - og_pos) - return F, G - - n_iter = 3 - - for i in range(n_iter): - - self.mesh.x = fmin_l_bfgs_b(energy, self.mesh.x, factr=1e12)[0] # inner L-BFGS loop - # updated_xy = self.mesh.x.reshape((self.n, 2)) - updated_xy = np.dstack([self.mesh.x[self.n:], self.mesh.x[:self.n]])[0] - # plt.triplot(updated_xy[:, 0], -updated_xy[:, 1], self.mesh.tri, linewidth=0.5) - plt.triplot(updated_xy[:, 1], -updated_xy[:, 0], self.mesh.tri, linewidth=0.5) - plt.axis("equal") - plt.savefig(f"{i}_smooth_mesh.png") - plt.close() - diff --git a/examples/acrobat_2023/valis_acrobat_2023.py b/examples/acrobat_2023/valis_acrobat_2023.py deleted file mode 100644 index 3cb235b6..00000000 --- a/examples/acrobat_2023/valis_acrobat_2023.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -Code used for the ACROBAT 2023 Grand Challenge -""" - -import pathlib -import os -import shutil -from time import time -import argparse -import numpy as np -import pandas as pd -import pyvips - -from valis import registration, warp_tools, preprocessing, slide_io -from valis.micro_rigid_registrar import MicroRigidRegistrar - -ANON_COL = "anon_id" -PT_COL = "point_id" -SRC_COL = "wsi_source" -TARGET_COL = "wsi_target" -LANDMARK_COL = "landmarks_csv" -DST_DIR_COL = "output_dir_name" - -SRC_MMP = "mpp_source" -SRC_X_COL = "x_source" -SRC_Y_COL = "y_source" - -TARGET_X_COL = "x_target" -TARGET_Y_COL = "y_target" -TARGET_MMP = "mpp_target" - -TARGET_IMG_NAME = "registered_source_image.ome.tiff" -TARGET_LANDMARK_NAME = "registered_landmarks.csv" - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser(description='Register images for the 2023 ACROBAT Grand Challenge') - parser.add_argument('-PATH_TABLE_CSV', type=str, help='csv containing image pairs and landmarks') - parser.add_argument('-PATH_IMAGES_DIR', type=str, help='csv containing image pairs and landmarks') - parser.add_argument('-PATH_ANNOT_DIR', type=str, help='csv containing image pairs and landmarks') - parser.add_argument('-PATH_OUTPUT_DIR', type=str, help='csv containing image pairs and landmarks') - parser.add_argument('-row_idx', type=int, help='name/row of sample') - parser.add_argument('-micro_reg_fraction', type=str, help='Size of images used for 2nd registation') - parser.add_argument('-use_masks', type=str, help='Whether or not to use masks for registration') - - args = vars(parser.parse_args()) - - micro_reg_fraction = args["micro_reg_fraction"] - row_idx = args["row_idx"] - img_src_dir = args["PATH_IMAGES_DIR"] - landmark_src_dir = args["PATH_ANNOT_DIR"] - parent_dst_dir = args["PATH_OUTPUT_DIR"] - input_table = args["PATH_TABLE_CSV"] - use_masks = eval(args["use_masks"]) - - micro_reg_fraction = eval(micro_reg_fraction) - input_df = pd.read_csv(input_table) - sample_row = input_df.iloc[row_idx] - - src_img_f = sample_row[SRC_COL] - target_img_f = sample_row[TARGET_COL] - landmarks_f = os.path.join(landmark_src_dir, sample_row[LANDMARK_COL]) - sample_id = str(sample_row[DST_DIR_COL]) - dst_dir = os.path.join(parent_dst_dir, sample_id) - - print(sample_id) - # Do initial registration, setting the reference image to be the H&E image - img_list = [os.path.join(img_src_dir, d) for d in [target_img_f, src_img_f]] - - start = time() - registrar = registration.Valis("./", os.path.split(dst_dir)[0], - crop="reference", - reference_img_f=target_img_f, - align_to_reference=True, - img_list=img_list, - create_masks=use_masks, - max_processed_image_dim_px=500, - max_non_rigid_registration_dim_px=2000, - micro_rigid_registrar_cls=MicroRigidRegistrar, - name=sample_id) - - rigid_registrar, non_rigid_registrar, error_df = registrar.register(brightfield_processing_cls=preprocessing.StainFlattener, - brightfield_processing_kwargs={"adaptive_eq":True}) - - # Determine how large of an image to use for micro registration - img_dims = np.array([slide_obj.slide_dimensions_wh[0] for slide_obj in registrar.slide_dict.values()]) - min_max_size = np.min([np.max(d) for d in img_dims]) - img_areas = [np.multiply(*d) for d in img_dims] - max_img_w, max_img_h = tuple(img_dims[np.argmax(img_areas)]) - - if micro_reg_fraction == 1.0: - # Full size image - micro_reg_size = None - if isinstance(micro_reg_fraction, float): - micro_reg_size = np.floor(min_max_size*micro_reg_fraction).astype(int) - else: - micro_reg_size = micro_reg_fraction - micro_reg_fraction = micro_reg_fraction/min_max_size - - micro_reg, micro_error = registrar.register_micro(max_non_rigid_registration_dim_px=micro_reg_size, - reference_img_f=target_img_f, - align_to_reference=True, - brightfield_processing_cls=preprocessing.StainFlattener) - stop = time() - elapsed = stop - start - elapsed_min = np.round(elapsed/60, 6) - - # Create benchmarking results - pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - - non_rigid_error = np.nanmean(micro_error["non_rigid_D"].values) - rigid_error = np.nanmean(micro_error["rigid_D"].values) - apply_non_rigid = non_rigid_error < rigid_error - if not apply_non_rigid: - print(f"Non-rigid registration didn't improve alignments for {registrar.name}") - - # Read in points - pt_df = pd.read_csv(landmarks_f) - src_mpp = pt_df[SRC_MMP].values[0] - src_xy_um = pt_df[[SRC_X_COL, SRC_Y_COL]].values - src_xy_px = src_xy_um/src_mpp - - # Warp points - src_slide = registrar.get_slide(src_img_f) - ref_slide = registrar.get_slide(target_img_f) - warped_xy_px = src_slide.warp_xy_from_to(src_xy_px, ref_slide, non_rigid=apply_non_rigid) - - # Clip any points that fell outside of image - ref_slide_shape_wh = ref_slide.slide_dimensions_wh[0] - warped_xy_px[:, 0] = np.clip(warped_xy_px[:, 0], 0, ref_slide_shape_wh[0]) - warped_xy_px[:, 1] = np.clip(warped_xy_px[:, 1], 0, ref_slide_shape_wh[1]) - - dst_mpp = pt_df[TARGET_MMP].values[0] - warped_xy_um = warped_xy_px*dst_mpp - - registered_df = pd.DataFrame({ANON_COL: pt_df[ANON_COL].values, - PT_COL: pt_df[PT_COL].values, - TARGET_X_COL: warped_xy_um[:, 0], - TARGET_Y_COL: warped_xy_um[:, 1]}) - - pt_f_out = os.path.join(dst_dir, TARGET_LANDMARK_NAME) - registered_df.to_csv(pt_f_out, index=False) - - # Save registered source image - s = 1/8 - source_slide = registrar.get_slide(src_img_f) - warped_slide = source_slide.warp_slide(level=0, non_rigid=apply_non_rigid) - warped_thumb = warp_tools.rescale_img(warped_slide, s) - thumb_xyzct = slide_io.get_shape_xyzct((warped_thumb.width, warped_thumb.height), warped_thumb.bands) - bf_dtype = slide_io.vips2bf_dtype(warped_thumb.format) - thumb_px_phys_size = (dst_mpp/s, dst_mpp/s, 'µm') - - ome_xml_obj = slide_io.update_xml_for_new_img(source_slide.reader.metadata.original_xml, - new_xyzct=thumb_xyzct, - bf_dtype=bf_dtype, - is_rgb=source_slide.reader.metadata.is_rgb, - series=source_slide.reader.series, - pixel_physical_size_xyu=thumb_px_phys_size, - channel_names=source_slide.reader.metadata.channel_names - ) - - ome_xml_obj.creator = f"pyvips version {pyvips.__version__}" - ome_xml_str = ome_xml_obj.to_xml() - - dst_f = os.path.join(dst_dir, TARGET_IMG_NAME) - slide_io.save_ome_tiff(warped_thumb, dst_f, ome_xml_str) - - # Delete output not needed by acrobat - for f in os.listdir(dst_dir): - if not f in [TARGET_LANDMARK_NAME, TARGET_IMG_NAME]: - full_f = os.path.join(dst_dir, f) - if os.path.isdir(full_f): - # remove directories - shutil.rmtree(full_f) - else: - # remove files - os.remove(full_f) - - registration.kill_jvm() \ No newline at end of file diff --git a/examples/acrobat_2023/valis_acrobat_2023_method.pdf b/examples/acrobat_2023/valis_acrobat_2023_method.pdf deleted file mode 100644 index 38c747b1..00000000 Binary files a/examples/acrobat_2023/valis_acrobat_2023_method.pdf and /dev/null differ diff --git a/examples/acrobat_grand_challenge.py b/examples/acrobat_grand_challenge.py deleted file mode 100644 index 5692d515..00000000 --- a/examples/acrobat_grand_challenge.py +++ /dev/null @@ -1,388 +0,0 @@ -""" -Code used for the ACROBAT Grand Challenge - -Shows how to create a custom image processor (AcrobatProcessor), -perform micro registration with a mask, warp points, and run -using command line arguments. - -""" - -import pathlib -import os -import shutil -from time import time -import argparse -import imghdr -import numpy as np -import pandas as pd -import re -from skimage import color as skcolor, draw, exposure, filters, morphology -import colour -import cv2 - -from valis import registration, valtils, warp_tools, viz, preprocessing -from valis.preprocessing import ColorfulStandardizer, DEFAULT_COLOR_STD_C - -DRAW_IMG_SIZE = 500 - -def get_lines_img(img, v_ksize, h_ksize): - - v_krnl = np.ones((1, v_ksize)) - edges_no_v_lines = morphology.opening(img, v_krnl) - - h_krnl = np.ones((h_ksize, 1)) - edges_no_h_lines = morphology.opening(img, h_krnl) - - no_lines_img = np.dstack([edges_no_v_lines, edges_no_h_lines]).min(axis=2) - lines_img = np.dstack([edges_no_v_lines, edges_no_h_lines]).max(axis=2) - lines_diff = lines_img - no_lines_img - - return lines_diff - - -class AcrobatProcessor(ColorfulStandardizer): - """Preprocess images for registration - - Standardizes colorfulness, removes black borders, and subtracts the background - - """ - - def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - - def create_mask(self, od_clip=0.25, lines_diff_t=0.025): - """ - Use optical density to create mask. - - Ended up working better without masks though - """ - od_img = preprocessing.rgb2od(self.image) - od_summary = od_img.min(axis=2) - - line_img = get_lines_img(od_summary, 15, 15) - - lines_mask = 255*(line_img > lines_diff_t).astype(np.uint8) - very_dense = 255*(od_summary >= 0.1).astype(np.uint8) - - edge_artifacts = np.zeros_like(lines_mask) - edge_artifacts[lines_mask > 0] = 255 - edge_artifacts[very_dense > 0] = 255 - _, edge_artifacts = preprocessing.create_edges_mask(edge_artifacts) - - mask = 255*(edge_artifacts == 0).astype(np.uint8) - - squashed_od = np.clip(od_summary, 0, od_clip) - - bg_od, _ = filters.threshold_multiotsu(squashed_od[mask > 0]) - - bg_od = np.quantile(squashed_od[mask > 0], 0.5) - fg = 255*(squashed_od > bg_od).astype(np.uint8) - fg[mask == 0] = 0 - fg = preprocessing.mask2contours(fg, 0) - fg_lines = get_lines_img(fg, 25, 25) - fg[fg_lines > 0] = 0 - - fg = preprocessing.remove_small_obj_and_lines_by_dist(fg) - - bbox_mask = preprocessing.mask2bbox_mask(fg) - - return bbox_mask - - def process_image(self, blk_thresh=0.75, c=DEFAULT_COLOR_STD_C, invert=True, *args, **kwargs): - - # Process image using default method - std_rgb = preprocessing.standardize_colorfulness(self.image, c) - std_g = skcolor.rgb2gray(std_rgb) - - if invert: - std_g = 255 - std_g - processed_img = exposure.rescale_intensity(std_g, in_range="image", out_range=(0, 255)).astype(np.uint8) - - # Detect black borders commonly found in this dataset - cam_d, cam = preprocessing.calc_background_color_dist(self.image) - cam_black = colour.convert(np.repeat(0, 3), 'sRGB', 'CAM16UCS') - black_dist = np.sqrt(np.sum((cam - cam_black)**2, axis=2)) - dark_regions = 255*(black_dist < blk_thresh).astype(np.uint8) - dark_contours, _ = cv2.findContours(dark_regions, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - edge_artifact_mask = np.zeros_like(dark_regions) - - for cnt in dark_contours: - cnt_xy = np.squeeze(cnt, 1) - on_border_idx = np.where((cnt_xy[:, 0] == 0) | - (cnt_xy[:, 0] == dark_regions.shape[1]-1) | - (cnt_xy[:, 1] == 0) | - (cnt_xy[:, 1] == dark_regions.shape[0]-1) - )[0] - - if len(on_border_idx) > 0 : - cv2.drawContours(edge_artifact_mask, [cnt], 0, 255, -1) - - - eps = np.finfo("float").eps - with colour.utilities.suppress_warnings(colour_usage_warnings=True): - if 1 < self.image.max() <= 255 and np.issubdtype(self.image.dtype, np.integer): - cam = colour.convert(self.image/255 + eps, 'sRGB', 'CAM16UCS') - else: - cam = colour.convert(self.image + eps, 'sRGB', 'CAM16UCS') - - # Subtract background - brightest_thresh = np.quantile(cam[..., 0][edge_artifact_mask==0], 0.9) - brightest_idx = np.where(cam[..., 0] >= brightest_thresh) - brightest_pixels = processed_img[brightest_idx] - brightest_rgb = np.median(brightest_pixels, axis=0) - no_bg = processed_img - brightest_rgb - no_bg = np.clip(no_bg, 0, 255) - no_bg[edge_artifact_mask != 0] = 0 - - # Adjust range and perform adaptive histogram equalization - no_bg = (255*exposure.equalize_adapthist(no_bg/no_bg.max())).astype(np.uint8) - - return no_bg - - -def create_he_mask(he_img, j_range=(0.0, 0.9), c_range=(0.05, 1), h_range=(150, 275), h_rotation=270): - """ - Segment H&E stain in the polar CAM16-UCS colorspace - """ - jch = preprocessing.rgb2jch(he_img, h_rotation=h_rotation) - - he_mask = 255*( (jch[..., 0] >= j_range[0]) & - (jch[..., 0] < j_range[1]) & - (jch[..., 1] >= c_range[0]) & - (jch[..., 1] < c_range[1]) & - (jch[..., 2] >= h_range[0]) & - (jch[..., 2] < h_range[1]) - ).astype(np.uint8) - - he_mask = preprocessing.mask2contours(he_mask) - he_mask = preprocessing.remove_small_obj_and_lines_by_dist(he_mask) - - return he_mask - - -def create_reg_mask(reg, j_range=(0.05, 0.9), c_range=(0.05, 1), h_range=(150, 275), h_rotation=270): - """ - Mask is the bounding bbox around the H&E+ tissue - """ - - img_names = list(reg.slide_dict.keys()) - he_img_name = [n for n in img_names if re.search("_HE_", n) is not None][0] - - he_slide = reg.get_slide(he_img_name) - he_mask = create_he_mask(he_slide.image, j_range=j_range, c_range=c_range, h_range=h_range, h_rotation=h_rotation) - nr_he_mask = he_slide.warp_img(he_mask, interp_method="nearest", crop=False) - - mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(nr_he_mask)) - c0, r0 = mask_bbox[:2] - c1, r1 = mask_bbox[:2] + mask_bbox[2:] - reg_mask = np.zeros_like(nr_he_mask) - reg_mask[r0:r1, c0:c1] = 255 - - return reg_mask - - - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser(description='Register images for the ACROBAT Grand Challenge') - parser.add_argument('-src_dir', type=str, help='source image to warp') - parser.add_argument('-dst_dir', type=str, help='where to save results') - parser.add_argument('-name', type=str, help='what to call the registrar') - parser.add_argument('-micro_reg_fraction', type=str, help='Size of images used for 2nd registation') - parser.add_argument('-landmarks_f', type=str, help='location of landmarks') - args = vars(parser.parse_args()) - - src_dir = args["src_dir"] - dst_dir = args["dst_dir"] - name = args["name"] - micro_reg_fraction = args["micro_reg_fraction"] - landmarks_f = args["landmarks_f"] - - micro_reg_fraction = eval(micro_reg_fraction) - print(name) - - # Do initial registration, setting the reference image to be the H&E image - img_list = [f for f in os.listdir(src_dir) if imghdr.what(os.path.join(src_dir, f)) is not None] - img_list = [os.path.join(src_dir, f) for f in img_list] - he_img_f = [f for f in img_list if re.search("_HE_", f) is not None][0] - start = time() - - - with valtils.HiddenPrints(): - registrar = registration.Valis(src_dir, dst_dir, - crop="reference", - reference_img_f=he_img_f, - create_masks=False, - name=name) - - rigid_registrar, non_rigid_registrar, error_df = registrar.register(brightfield_processing_cls=AcrobatProcessor) - - - - # Determine how large of an image to use for micro registration - img_dims = np.array([slide_obj.slide_dimensions_wh[0] for slide_obj in registrar.slide_dict.values()]) - min_max_size = np.min([np.max(d) for d in img_dims]) - img_areas = [np.multiply(*d) for d in img_dims] - max_img_w, max_img_h = tuple(img_dims[np.argmax(img_areas)]) - - if micro_reg_fraction == 1.0: - # Full size image - micro_reg_size = None - if isinstance(micro_reg_fraction, float): - micro_reg_size = np.floor(min_max_size*micro_reg_fraction).astype(int) - else: - micro_reg_size = micro_reg_fraction - micro_reg_fraction = micro_reg_fraction/min_max_size - - # Determine if image will need to be divided into tiles - max_microreg_size = (micro_reg_size, micro_reg_size) - displacement_gb = registrar.size*warp_tools.calc_memory_size_gb(max_microreg_size, 2, "float32") - processed_img_gb = registrar.size*warp_tools.calc_memory_size_gb(max_microreg_size, 1, "uint8") - img_gb = registrar.size*warp_tools.calc_memory_size_gb(max_microreg_size, 3, "uint8") - estimated_gb = img_gb + displacement_gb + processed_img_gb - if estimated_gb > registration.TILER_THRESH_GB: - # Tiles may not have edge artifacts the AcrobatProcessor handles - img_processor = ColorfulStandardizer - else: - # Image will be higher resolution of the one used before, so use the same processor - img_processor = AcrobatProcessor - - # Perform microregistration within the bounding box of the H&E+ tissue - micro_reg_mask = create_reg_mask(registrar) - - micro_reg, micro_error = registrar.register_micro(max_non_rigid_registartion_dim_px=micro_reg_size, - reference_img_f=he_img_f, - align_to_reference=True, - mask=micro_reg_mask, - brightfield_processing_cls=img_processor - ) - - - stop = time() - elapsed = stop - start - elapsed_min = np.round(elapsed/60, 6) - - # Create benchmarking results - pt_dir = os.path.join(dst_dir, "acrobat", "landmarks") # Warp points in the IHC image. Save in a separate directory - plot_dir = os.path.join(dst_dir, "acrobat", "plots") # Will also draw the warped points on both images - for d in [pt_dir, plot_dir]: - pathlib.Path(d).mkdir(exist_ok=True, parents=True) - - draw_rad = 2 - pt_cmap = (255*viz.jzazbz_cmap()).astype(np.uint8) - - # Read in points - pt_df = pd.read_csv(landmarks_f) - sample_df = pt_df.loc[pt_df["anon_id"] == eval(name)] - sample_df["name"] = [valtils.get_name(x) for x in sample_df["anon_filename_ihc"]] - - # Get some info used to draw the landmarks - from_slides = [o.name for o in registrar.slide_dict.values()] - ref_slide = registrar.get_ref_slide() - ref_wh = ref_slide.slide_dimensions_wh[0] - from_slides.remove(ref_slide.name) - - ref_reg_img = ref_slide.processed_img - draw_ref_s = np.min(DRAW_IMG_SIZE/np.array(ref_reg_img.shape[0:2])) - draw_ref_img = warp_tools.rescale_img(ref_reg_img, draw_ref_s) - draw_ref_img = skcolor.gray2rgb(draw_ref_img) - ref_slide_to_draw_sxy = np.array(draw_ref_img.shape[0:2][::-1])/ref_slide.slide_dimensions_wh[0] - - updated_df_list = [None] * len(from_slides) - - # Warp and plot the landmarks. - for i, sname in enumerate(from_slides): - - # Convert from um to pixel in IHC image - src_slide = registrar.slide_dict[sname] - pair_df = sample_df.loc[sample_df["name"] == src_slide.name] - src_mpp = pair_df[["mpp_ihc_10X"]].values[0][0] - src_xy_um = pair_df[["ihc_x", "ihc_y"]].values - src_xy_px = src_xy_um/src_mpp - warped_xy_px = src_slide.warp_xy_from_to(src_xy_px, ref_slide) - - # Convert from pixel to um in H&E - dst_mpp = pair_df[["mpp_he_10X"]].values[0][0] - warped_xy_um = warped_xy_px*dst_mpp - pair_df[["he_x", "he_y"]] = warped_xy_um - updated_df_list[i] = pair_df - - # Estimate error using same metrics as acrobat, but using features - moving_feature_xy_warped = src_slide.warp_xy_from_to(src_slide.xy_matched_to_prev, - ref_slide, - src_pt_level=src_slide.processed_img_shape_rc - ) - - moving_feature_xy_warped[:, 0] = np.clip(moving_feature_xy_warped[:, 0], 0, ref_wh[0]) - moving_feature_xy_warped[:, 1] = np.clip(moving_feature_xy_warped[:, 1], 0, ref_wh[1]) - - ref_sxy = np.array(ref_slide.slide_dimensions_wh[0])/np.array(ref_slide.processed_img_shape_rc[::-1]) - ref_in_slide_xy = src_slide.xy_in_prev*ref_sxy - - d = warp_tools.calc_d(ref_in_slide_xy*ref_slide.resolution, moving_feature_xy_warped*src_slide.resolution) - feature_p90 = np.percentile(d, q=90) - feature_mean_d = np.mean(d) - pair_df["p90"] = feature_p90 - pair_df["mean"] = feature_mean_d - updated_df_list[i] = pair_df - - # Draw landmarks on both. Source (IHC) on left, target (H&E) on right - src_reg_img = src_slide.warp_img(src_slide.processed_img) - draw_src_s = np.min(DRAW_IMG_SIZE/np.array(src_reg_img.shape[0:2])) - draw_src_img = warp_tools.rescale_img(src_reg_img, draw_src_s) - draw_src_img = skcolor.gray2rgb(draw_src_img) - - combo_img = np.hstack([draw_src_img, draw_ref_img]) - c_shift = draw_src_img.shape[1] - - src_slide_to_draw_sxy = (np.array(draw_src_img.shape[0:2][::-1])/np.array(ref_slide.slide_dimensions_wh[0])) - warped_in_src_xy = src_slide.warp_xy(src_xy_px) - - unwarped_src_sxy = np.array(src_slide.processed_img.shape[0:2][::-1])/src_slide.slide_dimensions_wh[0] - unwarped_draw_rc = (src_xy_px*unwarped_src_sxy)[:, ::-1] - unwarped_draw_img = skcolor.gray2rgb(src_slide.processed_img) - - src_draw_rc = (warped_in_src_xy*src_slide_to_draw_sxy)[:, ::-1] - ref_draw_rc = (warped_xy_px*ref_slide_to_draw_sxy)[:, ::-1] - for pt_idx, src_pt in enumerate(src_draw_rc): - ref_pt = ref_draw_rc[pt_idx] - ref_pt[1] += c_shift - - clr = pt_cmap[np.random.choice(pt_cmap.shape[0], 1)[0]] - - src_circ = draw.disk(src_pt, draw_rad, shape=combo_img.shape) - target_circ = draw.disk(ref_pt, draw_rad, shape=combo_img.shape) - pt_line = list(draw.line(*src_pt.astype(int), *ref_pt.astype(int))) - pt_line[0] = np.clip(pt_line[0], 0, combo_img.shape[0]-1) - pt_line[1] = np.clip(pt_line[1], 0, combo_img.shape[1]-1) - pt_line = tuple(pt_line) - - combo_img[pt_line] = clr - combo_img[src_circ] = clr - combo_img[target_circ] = clr - - unwarped_circ = draw.disk(unwarped_draw_rc[pt_idx], draw_rad, shape=unwarped_draw_img.shape) - unwarped_draw_img[unwarped_circ] = clr - - pt_img_f_out = os.path.join(plot_dir, f"{registrar.name}_{src_slide.name}_to_{ref_slide.name}.png") - warp_tools.save_img(pt_img_f_out, combo_img) - - # Save the results - updated_df = pd.concat(updated_df_list) - updated_df = updated_df.drop(["name"], axis=1) - pt_f = os.path.join(pt_dir, f"{registrar.name}_landmarks.csv") - updated_df.to_csv(pt_f, index=False) - - # Delete registrar to save space on computer. - try: - reg_f = os.path.join(registrar.data_dir, f"{registrar.name}_registrar.pickle") - os.remove(reg_f) - if os.path.exists(registrar.displacements_dir): - shutil.rmtree(registrar.displacements_dir) - - except OSError as e: - print("Error: %s : %s" % (registrar.data_dir, e.strerror)) - - registration.kill_jvm() \ No newline at end of file diff --git a/examples/align_two_images.py b/examples/align_two_images.py new file mode 100644 index 00000000..4ae57e9a --- /dev/null +++ b/examples/align_two_images.py @@ -0,0 +1,1020 @@ +import argparse +import os +import shutil +import sys +import time +from valis import registration, feature_matcher, feature_detectors, preprocessing +from valis.serial_rigid import TooFewMatchesError +import numpy as np +import pyvips + +from pathlib import Path +from typing import List + +from tqdm import tqdm + +from ome_types import OME +from ome_types._autogenerated.ome_2016_06 import ( + Channel, + TiffData, + Plane, + Pixels, + UnitsLength, + Image, +) +from ome_types.model.simple_types import ChannelID, PixelsID, ImageID +from valis import slide_tools +from valis import orientation_check +import logging + +NUMPY_FORMAT_BF_DTYPE = { + "uint8": "uint8", + "int8": "int8", + "uint16": "uint16", + "int16": "int16", + "uint32": "uint32", + "int32": "int32", + "float32": "float", + "float64": "double", +} + +logger = logging.getLogger(__name__) + + +def get_parser(): + parser = argparse.ArgumentParser(description="Align two WSIs.") + parser.add_argument( + "--reference", + type=str, + help="Path to WSI in .ome.tif format. Will be used as the reference image.", + ) + parser.add_argument( + "--image", + type=str, + help="Path to WSI in .ome.tif format. Will be used as the warped image.", + ) + parser.add_argument( + "--output-dir", + type=str, + help="Output directory.", + ) + parser.add_argument( + "--max-processed-image-dim-px", + type=int, + default=2048, + help="Max side length used for feature detection / non-rigid registration. " + "Higher = better matches but more memory; 4096 OOMs on a 32GB laptop.", + ) + stain_choices = ( + "auto", + "he-hematoxylin", + "he-hematoxylin-raw", + "he-hematoxylin-sparse", + "inverted-fluorescence", + "od", + "colorful-standardizer", + "luminosity", + ) + parser.add_argument( + "--image-stain", + choices=stain_choices, + default="auto", + help="Preprocessor for --image. 'he-hematoxylin' deconvolves H&E and " + "keeps the hematoxylin (nuclei) channel, automatically falling back " + "to the sparse-dots variant if the matcher gets too few matches. " + "'he-hematoxylin-sparse' forces the sparse path. 'inverted-" + "fluorescence' un-inverts a DAPI-style image so nuclei are bright. " + "'auto' lets the script decide.", + ) + parser.add_argument( + "--reference-stain", + choices=stain_choices, + default="auto", + help="Same as --image-stain but for --reference.", + ) + parser.add_argument( + "--orientation-margin", + type=float, + default=0.0, + help="Minimum NCC margin (best - identity) required to apply a D4 " + "pre-rotation. Below this, the script falls back to identity. " + "Default 0 trusts the winning transform; raise it (e.g. 0.05) only " + "when the script's heuristic is producing wrong flips.", + ) + parser.add_argument( + "--no-script-orientation", + action="store_true", + help="Skip the script's own D4 pre-rotation entirely and let valis " + "handle reflections (via check_for_reflections=True). Useful when the " + "script's NCC-based orientation check is too noisy on weak/ambiguous " + "stain pairs.", + ) + parser.add_argument( + "--min-rigid-matches", + type=int, + default=30, + help="Minimum number of initial keypoint matches required before " + "valis's rematch step. Below this, the script aborts with a clear " + "error instead of letting valis warp the image with a degenerate " + "transform (which OOMs the rematch's feature detection).", + ) + return parser + + +class HematoxylinExtractor(preprocessing.ImageProcesser): + """Extract the hematoxylin (nuclei) channel from an H&E image. + + Pipeline: Macenko-style normalization against a standard H&E + reference (so faded slides come back up to consistent intensity), + then deconvolve using ``skimage.color.rgb2hed`` with the fixed + Ruifrok-Johnston stain matrix and keep only the H channel. + + Macenko's ``normalize_he`` picks H vs E by an angle heuristic on the + candidate stain vectors' red component. On eosin-dominant or + hematoxylin-faded slides that ordering can flip, sending eosin into + the "hematoxylin" row — yielding an output where stroma lights up + brighter than nuclei. We detect that here by checking which row + correlates with bluish pixels in the original RGB (true hematoxylin) + and swap if needed. + + Pass ``use_macenko=False`` to skip normalization entirely and just + run ``rgb2hed`` on raw RGB — useful as a fallback when Macenko + misbehaves on an atypical slide. + """ + + def create_mask(self): + from valis.preprocessing import create_tissue_mask_from_rgb + + _, tissue_mask = create_tissue_mask_from_rgb(self.image) + return tissue_mask + + def process_image( + self, + *args, + use_macenko: bool = True, + sparse: bool = False, + sparse_pct: float = 90.0, + sparse_blur_sigma: float = 1.5, + **kwargs, + ): + """Extract hematoxylin channel. + + ``sparse=True`` thresholds the H channel at the ``sparse_pct`` + percentile and zeros everything below — yielding a punctate + "nuclei dots" image that resembles a fluorescence reference. + Use this on eosin-dominant / hematoxylin-faded slides where the + smooth percentile stretch produces stromal texture instead of + isolated nuclei. + """ + from skimage.color import rgb2hed + + img = self.image + if img.ndim != 3 or img.shape[2] < 3: + raise ValueError("HematoxylinExtractor requires an RGB image") + rgb = img[..., :3] + if rgb.dtype != np.uint8: + rgb = np.clip(rgb, 0, 255).astype(np.uint8) + + rgb_for_unmix = rgb + if use_macenko: + # Macenko-style normalization to a standard H&E reference. + # Brings faded slides up to consistent stain intensity. + try: + normalized = preprocessing.normalize_he(rgb, Io=240, alpha=1, beta=0.15) + normalized = _fix_he_swap(normalized, rgb) + # Reproject normalized concentrations through the + # canonical Ruifrok-Johnston H/E vectors so rgb2hed below + # sees a canonical H&E image. + ref_stain = np.array( + [[0.5626, 0.7201, 0.4062], [0.2159, 0.8012, 0.5581]] + ) + recon_od = ref_stain.T @ normalized + recon = np.clip(240.0 * np.exp(-recon_od), 0, 255).T.reshape(rgb.shape) + rgb_for_unmix = recon.astype(np.uint8) + except Exception: + # Macenko can fail on degenerate (very faded / very dark) + # tissue. Fall back to raw RGB rather than aborting. + rgb_for_unmix = rgb + + hed = rgb2hed(rgb_for_unmix) + h = hed[..., 0] # hematoxylin channel (positive where stain is dense) + + if sparse: + # Restrict the percentile to tissue pixels so a large dark + # background doesn't pull the threshold down. Then zero + # everything below the cutoff and stretch the survivors. + from valis.preprocessing import create_tissue_mask_from_rgb + + try: + _, tissue_mask = create_tissue_mask_from_rgb(rgb) + tissue = tissue_mask > 0 + except Exception: + tissue = np.ones(h.shape, dtype=bool) + if tissue.sum() < 1000: + tissue = np.ones(h.shape, dtype=bool) + cutoff = np.percentile(h[tissue], sparse_pct) + top = np.percentile(h[tissue], 99.9) + denom = max(top - cutoff, 1e-6) + out = np.clip((h - cutoff) / denom, 0.0, 1.0) + out[~tissue] = 0 + if sparse_blur_sigma > 0: + # Smooth so each surviving spot becomes a small Gaussian + # blob with a real local maximum — gives keypoint matchers + # something with scale to lock onto. + from scipy.ndimage import gaussian_filter + + out = gaussian_filter(out.astype(np.float32), sparse_blur_sigma) + m = out.max() + if m > 0: + out = out / m + return (out * 255).astype(np.uint8) + + lo, hi = np.percentile(h, (1, 99)) + if hi <= lo: + hi = lo + 1e-6 + h = np.clip((h - lo) / (hi - lo), 0.0, 1.0) + return (h * 255).astype(np.uint8) + + +def _fix_he_swap(normalized: np.ndarray, rgb: np.ndarray) -> np.ndarray: + """Detect and correct H/E row swaps from Macenko's angle heuristic. + + Hematoxylin stains pixels blue-purple, eosin pink-red. So the true + hematoxylin concentration row should correlate positively with + (B - R) across the image, while eosin should anti-correlate. If the + rows are swapped, swap them back. + """ + flat = rgb.reshape(-1, 3).astype(np.float32) + blueness = flat[:, 2] - flat[:, 0] # B - R + # Mask to tissue pixels (anything sufficiently darker than 240 bg) + od_proxy = 255.0 - flat.mean(axis=1) + tissue = od_proxy > 15.0 + if tissue.sum() < 1000: + return normalized + b = blueness[tissue] + if b.std() < 1e-3: + return normalized + c0 = np.corrcoef(normalized[0, tissue], b)[0, 1] + c1 = np.corrcoef(normalized[1, tissue], b)[0, 1] + if np.isnan(c0) or np.isnan(c1): + return normalized + if c1 > c0: + return normalized[::-1].copy() + return normalized + + +class InvertedFluorescence(preprocessing.ImageProcesser): + """Reverse the inversion on an 'inverted DAPI' (or similar) greyscale image + so that nuclei come out bright — matching the convention of hematoxylin + deconvolution output. + """ + + def create_mask(self): + img = self.image + if img.ndim == 3: + img = img.mean(axis=-1) + img = img.astype(np.float32) + # In an inverted-fluorescence image, tissue is dark on a bright bg. + thresh = np.percentile(img, 90) + mask = (img < thresh).astype(np.uint8) * 255 + return mask + + def process_image(self, *args, **kwargs): + img = self.image + if img.ndim == 3: + img = img.mean(axis=-1) + img = img.astype(np.float32) + lo, hi = np.percentile(img, (1, 99)) + if hi <= lo: + hi = lo + 1.0 + img = np.clip((img - lo) / (hi - lo), 0.0, 1.0) + inverted = 1.0 - img + return (inverted * 255).astype(np.uint8) + + +def setup_valis_logging(): + """Configure detailed stream logging for valis logger""" + # Get the valis logger + valis_logger = logging.getLogger("valis") + valis_logger.setLevel(logging.DEBUG) + valis_logger.setLevel(logging.DEBUG) + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + + # Create detailed formatter with line number, function name, etc. + formatter = logging.Formatter( + fmt="%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s() - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + console_handler.setFormatter(formatter) + + # Add handler to logger + valis_logger.addHandler(console_handler) + valis_logger.addHandler(console_handler) + + return valis_logger + + +def pyvips_to_thumbnail_array(img: pyvips.Image, size: int) -> "np.ndarray": + """Render a pyvips image to a small numpy array for orientation scoring. + + Uses pyvips' resize so we never materialize the full slide in memory. + """ + import numpy as np + + scale = size / max(img.width, img.height) + small = img.resize(scale) + if small.bands > 1: + small = small.colourspace("b-w") + if small.format == "ushort": + small = (small >> 8).cast("uchar") + elif small.format != "uchar": + small = small.cast("uchar") + mem = small.write_to_memory() + return np.frombuffer(mem, dtype=np.uint8).reshape(small.height, small.width) + + +def pyvips_to_thumbnail_rgb_array(img: pyvips.Image, size: int) -> "np.ndarray": + """Render a pyvips image to a small RGB numpy array. Used to feed + color-aware preprocessors (HEDeconvolution, HematoxylinFixed, OD, ...) + on a thumbnail prior to the orientation check. + """ + scale = size / max(img.width, img.height) + small = img.resize(scale) + if small.format == "ushort": + small = (small >> 8).cast("uchar") + elif small.format != "uchar": + small = small.cast("uchar") + if small.bands == 1: + small = small.bandjoin([small, small]) + elif small.bands > 3: + small = small[0].bandjoin([small[1], small[2]]) + mem = small.write_to_memory() + return np.frombuffer(mem, dtype=np.uint8).reshape(small.height, small.width, 3) + + +def run_processor_on_thumbnail( + processor_spec, thumb_array: "np.ndarray", src_f: str +) -> "np.ndarray": + """Instantiate a valis ``ImageProcesser`` subclass on a thumbnail and + return its ``process_image`` output. Used so the orientation check + sees the same nuclei-bright signal that valis will register on, + rather than raw luminance of two visually dissimilar stains. + """ + cls, kwargs = processor_spec + proc = cls(image=thumb_array, src_f=src_f, level=0, series=0) + return proc.process_image(**(kwargs or {})) + + +def check_and_correct_orientation( + moving_img: pyvips.Image, + reference_img: pyvips.Image, + downsample_size: int = 2048, + margin_threshold: float = 0.05, + moving_processor=None, + reference_processor=None, + moving_src_f: str = "", + reference_src_f: str = "", +) -> tuple[pyvips.Image, "orientation_check.OrientationMatch"]: + """Find the best D4 orientation of ``moving_img`` against ``reference_img`` + and return the corrected pyvips image plus the match info. + + ``downsample_size`` is the requested thumbnail side. If either input is + smaller than that on its short side, the effective size is clamped down + so we never upsample. + + If ``moving_processor`` / ``reference_processor`` are provided, the + corresponding side is preprocessed with that valis ``ImageProcesser`` + before the NCC scoring. This is what makes the check usable across + stains: comparing raw luminance of (e.g.) H&E vs. inverted DAPI is + essentially noise, but comparing extracted hematoxylin vs. un-inverted + DAPI gives a strong, anatomically-aligned signal. + """ + max_useful = min( + reference_img.width, + reference_img.height, + moving_img.width, + moving_img.height, + ) + effective_size = min(downsample_size, max_useful) + if effective_size != downsample_size: + print( + f"[orientation] requested size {downsample_size} clamped to " + f"{effective_size} (smaller input side)" + ) + + if reference_processor is not None: + rgb = pyvips_to_thumbnail_rgb_array(reference_img, effective_size) + ref_thumb = run_processor_on_thumbnail( + reference_processor, rgb, reference_src_f + ) + else: + ref_thumb = pyvips_to_thumbnail_array(reference_img, effective_size) + if moving_processor is not None: + rgb = pyvips_to_thumbnail_rgb_array(moving_img, effective_size) + mov_thumb = run_processor_on_thumbnail(moving_processor, rgb, moving_src_f) + else: + mov_thumb = pyvips_to_thumbnail_array(moving_img, effective_size) + + match = orientation_check.find_best_orientation( + ref_thumb, mov_thumb, downsample_size=effective_size, use_gradient=True + ) + + print("\n=== Orientation check ===") + print(f" thumbnail size : {effective_size}x{effective_size}") + print(f" best transform : {match.name}") + print(f" correction needed : {orientation_check.describe(match)}") + print(f" best NCC score : {match.score:+.4f}") + identity_score = match.scores["identity"] + margin = match.score - identity_score + print(f" identity NCC score : {identity_score:+.4f} (margin: {margin:+.4f})") + print(" all scores :") + for name, score in sorted(match.scores.items(), key=lambda kv: -kv[1]): + marker = " <-- chosen" if name == match.name else "" + print(f" {name:<14s} {score:+.4f}{marker}") + inconclusive = ( + margin_threshold > 0 + and (match.k != 0 or match.mirror) + and margin < margin_threshold + ) + if inconclusive: + print( + f" >> margin {margin:+.4f} below threshold {margin_threshold}; " + "treating orientation check as inconclusive and using identity." + ) + match = orientation_check.OrientationMatch( + name="identity", + k=0, + mirror=False, + score=identity_score, + scores=match.scores, + ) + elif match.k == 0 and not match.mirror: + print(" >> images already correctly oriented; no pre-rotation applied.") + else: + print( + f" >> applying {orientation_check.describe(match)} to moving image " + "before registration." + ) + print("=========================\n") + + corrected = orientation_check.apply_d4_pyvips(moving_img, match.k, match.mirror) + return corrected, match + + +def convert_16to8_bit(image: pyvips.Image, clamp=None): + if clamp: + image = (image > clamp).ifthenelse(clamp, image) + else: + clamp = 2**16 + tile_processed = ((image / clamp) * 255).cast("uchar") + return tile_processed.colourspace("b-w") + + +def warp_new_slide( + new_img: pyvips.Image, + registrar: registration.Valis, + source_img: str, + dst_img: str, + bbox_xywh=None, +): + src_slide_obj = registrar.get_slide(source_img) + dst_slide_obj = registrar.get_slide(dst_img) + + warped_img = src_slide_obj.warp_img_from_to(new_img, to_slide_obj=dst_slide_obj) + + return warped_img + + +def create_progress_callback(): + """Create a progress callback for writing the output file.""" + pbar = tqdm(total=100, desc="Writing OME-TIFF", unit="%") + last_update = time.time() + + def eval_callback(image, progress): + if (time.time() - last_update) > 0.25: + # Set the progress bar's current iteration count directly + pbar.n = progress.percent + + # Refresh the display to show the updated value + pbar.refresh() + + eval_callback.close = lambda: pbar.close() + return eval_callback + + +def vips2bf_dtype(vips_format): + np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_format] + bf_dtype = NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] + + return bf_dtype + + +def create_ome_metadata( + width: int, + height: int, + protein_names: List[str], + protein_ids: List[str], + um_per_px: float, + dtype: str = "uint16", +) -> str: + """ + Create OME-XML metadata for single or multi-channel protein images. + + Parameters: + ----------- + width : int + Image width in pixels + height : int + Image height in pixels + protein_names : list of str + Human-readable protein name(s), one per channel + protein_ids : list of str + Protein identifier(s), one per channel + um_per_px : float + Micrometers per pixel + dtype : str + Data type (default: 'uint16') + + Returns: + -------- + str : OME-XML metadata as string + """ + + # Validate inputs + num_channels = len(protein_names) + if len(protein_ids) != num_channels: + raise ValueError( + f"Length of protein_names ({num_channels}) and protein_ids " + f"({len(protein_ids)}) must match" + ) + + if num_channels == 0: + raise ValueError("Must provide at least one protein name and ID") + + # Create the OME structure + ome = OME() + + # Create Channels + channels = [] + for c in range(num_channels): + channel = Channel( + id=ChannelID(f"Channel:0:{c}"), + name=protein_names[c], + samples_per_pixel=1, + ) + channels.append(channel) + + # Create TiffData blocks + tiff_data_blocks = [] + for c in range(num_channels): + tiff_data = TiffData( + first_z=0, + first_c=c, + first_t=0, + ifd=c, + plane_count=1, + ) + tiff_data_blocks.append(tiff_data) + + # Create Planes + planes = [] + for c in range(num_channels): + plane = Plane( + the_z=0, + the_c=c, + the_t=0, + ) + planes.append(plane) + + # Create Pixels + pixels = Pixels( + id=PixelsID("Pixels:0"), + dimension_order="XYZCT", + size_x=width, + size_y=height, + size_z=1, + size_c=num_channels, + size_t=1, + type=dtype, + big_endian=False, + physical_size_x=um_per_px, + physical_size_y=um_per_px, + physical_size_x_unit=UnitsLength.MICROMETER, + physical_size_y_unit=UnitsLength.MICROMETER, + channels=channels, + tiff_data_blocks=tiff_data_blocks, + planes=planes, + ) + + # Create Image name from protein info + if num_channels == 1: + image_name = f"{protein_names[0]} ({protein_ids[0]})" + else: + # For multichannel, create a combined name + image_name = f"Multichannel ({', '.join(protein_ids)})" + + # Create Image with pixels + image = Image( + id=ImageID("Image:0"), + name=image_name, + pixels=pixels, + ) + + # Assemble the structure + ome.images.append(image) + + # Convert to XML string + return ome.to_xml() + + +def write_ome_tiff( + images: list[pyvips.Image], + names: list[str], + output_path, + um_per_px: float, + scale: float = 1.0, +): + first_height = images[0].height + first_width = images[0].width + first_format = images[0].format + for img in images: + if img.width != first_width or img.height != first_height: + raise ValueError( + f"mismatched image size:" + f" {first_width}x{first_height} vs {img.width}x{img.height}" + ) + if img.format != first_format: + raise ValueError(f"mismatched format: {first_format} vs {img.format}") + if scale < 1.0: + images = [im.resize(scale) for im in images] + + # Only create OME metadata if ome_tiff is True + ome_xml = create_ome_metadata( + width=images[0].width, + height=images[0].height, + protein_names=names, + protein_ids=names, + um_per_px=um_per_px / scale, + dtype=vips2bf_dtype(images[0].format), + ) + + stacked = pyvips.Image.arrayjoin(images, across=1) + stacked.set_type(pyvips.GValue.gstr_type, "image-description", ome_xml) + stacked.set_type(pyvips.GValue.gint_type, "page-height", images[0].height) + + stacked.set_progress(True) + stacked.signal_connect("eval", create_progress_callback()) + + # Append remaining images + stacked.tiffsave( + output_path, + pyramid=True, + subifd=True, + tile=True, + compression="jpeg", + tile_height=256, + tile_width=256, + Q=100, + bigtiff=True, + ) + + print(f"\nSuccessfully wrote: {output_path}") + + +def main(): + # Setup logging first + setup_valis_logging() + args = get_parser().parse_args() + Path(args.output_dir).mkdir(parents=True, exist_ok=True) + + print("=== align_two_images invocation ===", flush=True) + print(f" argv : {' '.join(sys.argv)}", flush=True) + print(f" cwd : {os.getcwd()}", flush=True) + for k, v in sorted(vars(args).items()): + print(f" {k:<24}: {v}", flush=True) + print("===================================", flush=True) + + img_out_path = os.path.join(args.output_dir, os.path.basename(args.image)) + ref_out_path = os.path.join(args.output_dir, os.path.basename(args.reference)) + + # Save 8-bit copy of --reference so valis learns registration from uint8 + if not os.path.exists(ref_out_path): + ref_img = pyvips.Image.new_from_file(args.reference, page=0) + if ref_img.format == "ushort": + ref_img = convert_16to8_bit(ref_img) + ref_img.set_progress(True) + cb = create_progress_callback() + ref_img.signal_connect("eval", cb) + ref_img.tiffsave(ref_out_path) + cb.close() + + # Build a per-image processor dict so we can force nuclei-extraction on + # both sides — H&E -> hematoxylin channel, inverted DAPI -> un-inverted + # greyscale. Both end up "nuclei = bright", which gives the matcher + # something common to lock onto across modalities. We also feed these + # processors into the orientation check below so it scores nuclei-vs- + # nuclei rather than raw luminance of two visually different stains. + stain_to_processor = { + "he-hematoxylin": [HematoxylinExtractor, {}], + "he-hematoxylin-raw": [HematoxylinExtractor, {"use_macenko": False}], + "he-hematoxylin-sparse": [HematoxylinExtractor, {"sparse": True}], + "inverted-fluorescence": [InvertedFluorescence, {}], + "od": [preprocessing.OD, {}], + "colorful-standardizer": [preprocessing.ColorfulStandardizer, {}], + "luminosity": [preprocessing.Luminosity, {}], + } + + # `auto` resolves *here*, not inside valis. Valis's default for a 1-band + # uchar (assumed-fluorescence) input is to leave polarity untouched — + # which silently breaks cross-modal matching when the input is *inverted* + # DAPI (dark nuclei on bright bg). We pick a real processor based on + # bands + mean intensity so 'nuclei = bright' on both sides, which is + # what the matcher actually needs. + def _resolve_auto_stain(path: str) -> str: + peek = pyvips.Image.new_from_file(path, page=0) + thumb = pyvips_to_thumbnail_array(peek, 256) + mean = float(thumb.mean()) + if peek.bands >= 3: + return "he-hematoxylin" + # single-band: high mean => bright bg (inverted DAPI), un-invert + return "inverted-fluorescence" if mean > 127 else "luminosity" + + if args.image_stain == "auto": + args.image_stain = _resolve_auto_stain(args.image) + print(f"[auto-stain] --image -> {args.image_stain}") + if args.reference_stain == "auto": + args.reference_stain = _resolve_auto_stain(args.reference) + print(f"[auto-stain] --reference -> {args.reference_stain}") + + reference_path = os.path.join(args.output_dir, os.path.basename(args.reference)) + + # Cheap pre-alignment orientation check. If the moving image is rotated or + # mirrored relative to the reference, bake the correction into the copy + # that gets handed to Valis so registration only deals with residuals. + if args.no_script_orientation: + print( + "[orientation] script orientation check disabled (--no-script-orientation)" + ) + orient_match = orientation_check.OrientationMatch( + name="identity", k=0, mirror=False, score=0.0, scores={} + ) + else: + ref_for_check = pyvips.Image.new_from_file(ref_out_path, page=0) + moving_for_check = pyvips.Image.new_from_file(args.image, page=0) + _, orient_match = check_and_correct_orientation( + moving_img=moving_for_check, + reference_img=ref_for_check, + downsample_size=2048, + margin_threshold=args.orientation_margin, + moving_processor=stain_to_processor.get(args.image_stain), + reference_processor=stain_to_processor.get(args.reference_stain), + moving_src_f=args.image, + reference_src_f=args.reference, + ) + + needs_correction = orient_match.k != 0 or orient_match.mirror + if needs_correction: + # Write the D4-corrected moving image to a distinct filename so we + # don't collide with — and silently skip — an already-existing copy + # at ``img_out_path`` (which happens whenever ``args.output_dir`` + # is the same directory as the input). + stem, ext = os.path.splitext(os.path.basename(args.image)) + suffix = f"_k{orient_match.k}_m{int(orient_match.mirror)}" + img_out_path = os.path.join(args.output_dir, f"{stem}{suffix}{ext}") + if not os.path.exists(img_out_path): + if needs_correction: + print( + f"[orientation] writing D4-corrected copy to {img_out_path}", + flush=True, + ) + moving_full = pyvips.Image.new_from_file(args.image, page=0) + corrected = orientation_check.apply_d4_pyvips( + moving_full, orient_match.k, orient_match.mirror + ) + corrected.set_progress(True) + cb = create_progress_callback() + corrected.signal_connect("eval", cb) + corrected.tiffsave(img_out_path, bigtiff=True) + cb.close() + else: + os.symlink(os.path.abspath(args.image), img_out_path) + + # Build the processor_dict against the *final* registered file paths, + # which differ from the raw inputs when we wrote a D4-corrected copy. + image_path = img_out_path + + def _build_processor_dict( + image_stain: str, + reference_stain: str, + sparse_kwargs: dict | None = None, + ) -> dict: + pd = {} + if image_stain != "auto": + cls, kw = stain_to_processor[image_stain] + if sparse_kwargs and image_stain in ( + "he-hematoxylin", + "he-hematoxylin-sparse", + ): + kw = {**kw, **sparse_kwargs} + pd[image_path] = [cls, kw] + if reference_stain != "auto": + cls, kw = stain_to_processor[reference_stain] + if sparse_kwargs and reference_stain in ( + "he-hematoxylin", + "he-hematoxylin-sparse", + ): + kw = {**kw, **sparse_kwargs} + pd[reference_path] = [cls, kw] + return pd + + matches_dir = os.path.join(args.output_dir, "registration", "matches") + + def _dump_failed_matches(registrar): + # Try to dump whatever matches valis did find before bailing — the + # serial-rigid registrar holds the initial-pass match_dict at this + # point, which is exactly what the user needs to diagnose the + # failure. Pipeline.draw_matches isn't usable yet (relies on + # post-rigid attributes), so build the viz from match_dict directly. + try: + from valis import viz as _viz, warp_tools as _wt + + os.makedirs(matches_dir, exist_ok=True) + srr = registrar.rigid_registrar + for moving_idx, fixed_idx in srr.iter_order: + moving = srr.img_obj_list[moving_idx] + fixed = srr.img_obj_list[fixed_idx] + m = fixed.match_dict.get(moving) + if m is None: + continue + img1 = _wt.resize_img(moving.image, moving.image.shape[:2]) + img2 = _wt.resize_img(fixed.image, fixed.image.shape[:2]) + viz_img = _viz.draw_matches( + src_img=img1, + kp1_xy=m.matched_kp2_xy, + dst_img=img2, + kp2_xy=m.matched_kp1_xy, + rad=3, + alignment="horizontal", + ) + out_f = os.path.join( + matches_dir, + f"failed_{moving.name}_to_{fixed.name}_matches.png", + ) + _wt.save_img(out_f, viz_img) + print(f"Saved pre-failure match viz: {out_f}") + except Exception as draw_err: + print(f"(could not draw matches: {draw_err})") + + # Holder so the caller can grab the registrar from a failed attempt + # (so we can dump pre-failure match visualizations). + last_registrar = {"obj": None} + + def _attempt_register(processor_dict): + # Wipe any prior registration output so Valis starts fresh + # (otherwise it short-circuits on cached processed images and + # we'd re-use the previous attempt's processor outputs). + reg_dir = os.path.join(args.output_dir, "registration") + if os.path.isdir(reg_dir): + shutil.rmtree(reg_dir) + registrar = registration.Valis( + src_dir=args.output_dir, + dst_dir=args.output_dir, + name="registration", + img_list=[image_path, reference_path], + reference_img_f=reference_path, + thumbnail_size=4096, + max_processed_image_dim_px=args.max_processed_image_dim_px, + max_image_dim_px=args.max_processed_image_dim_px, + min_rigid_matches=args.min_rigid_matches, + check_for_reflections=False, + similarity_metric="euclidean", + align_to_reference=True, + ) + last_registrar["obj"] = registrar + registrar.register( + processor_dict=processor_dict if processor_dict else None, + ) + return registrar + + # Primary attempt with the user-selected stains. If `he-hematoxylin` + # fails to produce enough matches, retry once with the sparse variant + # (small bright dots resembling fluorescence nuclei) — same output is + # available explicitly via `he-hematoxylin-sparse`. + primary_pd = _build_processor_dict(args.image_stain, args.reference_stain) + try: + registrar = _attempt_register(primary_pd) + except TooFewMatchesError as e: + fb_image_stain = ( + "he-hematoxylin-sparse" + if args.image_stain == "he-hematoxylin" + else args.image_stain + ) + fb_reference_stain = ( + "he-hematoxylin-sparse" + if args.reference_stain == "he-hematoxylin" + else args.reference_stain + ) + has_fallback = ( + fb_image_stain != args.image_stain + or fb_reference_stain != args.reference_stain + ) + if not has_fallback: + if last_registrar["obj"] is not None: + _dump_failed_matches(last_registrar["obj"]) + print(f"\nALIGNMENT ABORTED: {e}", flush=True) + raise SystemExit(2) + + # Sweep sparse-extractor params: each (pct, sigma) gives a + # different density / blob-size tradeoff. We try several + # combinations before giving up so a single percentile that + # happens to be poorly tuned for this slide doesn't doom the + # whole alignment. Order goes default-first, then progressively + # denser/sparser variants. + sparse_sweep = [ + {"sparse_pct": 90.0, "sparse_blur_sigma": 1.5}, + {"sparse_pct": 85.0, "sparse_blur_sigma": 2.0}, + {"sparse_pct": 95.0, "sparse_blur_sigma": 1.0}, + {"sparse_pct": 80.0, "sparse_blur_sigma": 2.5}, + {"sparse_pct": 97.0, "sparse_blur_sigma": 1.5}, + ] + registrar = None + last_err: TooFewMatchesError | None = None + for i, sparse_kwargs in enumerate(sparse_sweep, start=1): + label = ( + f"pct={sparse_kwargs['sparse_pct']}, " + f"sigma={sparse_kwargs['sparse_blur_sigma']}" + ) + print( + f"[fallback {i}/{len(sparse_sweep)}] retrying with sparse " + f"hematoxylin extractor (image={fb_image_stain}, " + f"reference={fb_reference_stain}, {label})", + flush=True, + ) + fb_pd = _build_processor_dict( + fb_image_stain, fb_reference_stain, sparse_kwargs=sparse_kwargs + ) + try: + registrar = _attempt_register(fb_pd) + break + except TooFewMatchesError as e2: + last_err = e2 + continue + if registrar is None: + if last_registrar["obj"] is not None: + _dump_failed_matches(last_registrar["obj"]) + print( + f"\nALIGNMENT ABORTED: exhausted sparse sweep " + f"({len(sparse_sweep)} attempts). Last error: {last_err}", + flush=True, + ) + raise SystemExit(2) + + registrar.draw_matches(matches_dir) + print(f"Saved feature-match visualization to {matches_dir}") + + if not os.path.exists(os.path.join(args.output_dir, "aligned.ome.tif")): + warped_slides = [] + names = [] + + img = pyvips.Image.new_from_file(args.image, page=0) + if img.format == "ushort": + img = convert_16to8_bit(img) + if needs_correction: + img = orientation_check.apply_d4_pyvips( + img, orient_match.k, orient_match.mirror + ) + warped_img = warp_new_slide( + img, + registrar, + source_img=image_path, + dst_img=reference_path, + ) + warped_slides.append(warped_img) + names.append(os.path.basename(args.reference)) + + img = pyvips.Image.new_from_file(args.reference, page=0) + if img.format == "ushort": + img = convert_16to8_bit(img) + warped_img = warp_new_slide( + img, + registrar, + source_img=reference_path, + dst_img=reference_path, + ) + warped_slides.append(warped_img) + names.append(os.path.basename(args.reference)) + + write_ome_tiff( + images=warped_slides, + names=names, + output_path=os.path.join(args.output_dir, "aligned.ome.tif"), + um_per_px=0.2, + scale=1, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/align_two_images_requirements.txt b/examples/align_two_images_requirements.txt new file mode 100644 index 00000000..559dc929 --- /dev/null +++ b/examples/align_two_images_requirements.txt @@ -0,0 +1,108 @@ +aicspylibczi==3.3.1 +anndata==0.12.3 +annotated-types==0.7.0 +array-api-compat==1.12.0 +asttokens==3.0.0 +beautifulsoup4==4.14.2 +bounded-pool-executor==0.0.3 +cellpose==4.0.7 +certifi==2025.10.5 +cffi==2.0.0 +charset-normalizer==3.4.4 +colorama==0.4.6 +colour-science==0.4.6 +contourpy==1.3.3 +crc32c==2.8 +cycler==0.12.1 +decorator==5.2.1 +donfig==0.8.1.post1 +einops==0.8.1 +et_xmlfile==2.0.0 +executing==2.2.1 +fastcluster==1.2.6 +fastremap==1.17.7 +filelock==3.20.0 +fill_voids==2.1.1 +fonttools==4.60.1 +fsspec==2025.9.0 +geopandas==1.1.1 +h5py==3.15.1 +idna==3.11 +imagecodecs==2025.8.2 +imageio==2.37.0 +ipdb==0.13.13 +ipython==9.6.0 +ipython_pygments_lexers==1.1.1 +jedi==0.19.2 +Jinja2==3.1.6 +joblib==1.5.2 +kiwisolver==1.4.9 +kornia==0.8.1 +kornia_rs==0.1.9 +lazy_loader==0.4 +legacy-api-wrap==1.4.1 +lxml==6.0.2 +MarkupSafe==2.1.5 +matplotlib==3.10.7 +matplotlib-inline==0.1.7 +mpmath==1.3.0 +natsort==8.4.0 +networkx==3.5 +numcodecs==0.16.3 +numpy==2.2.6 +ome-types==0.6.2 +opencv-contrib-python==4.12.0.88 +opencv-contrib-python-headless==4.9.0.80 +opencv-python-headless==4.12.0.88 +openpyxl==3.1.5 +packaging==25.0 +pandas==2.3.3 +parso==0.8.5 +pexpect==4.9.0 +pillow==12.0.0 +pqdm==0.2.0 +prompt_toolkit==3.0.52 +ptyprocess==0.7.0 +pure_eval==0.2.3 +pyarrow==21.0.0 +pycparser==2.23 +pydantic==2.12.3 +pydantic-extra-types==2.10.6 +pydantic_core==2.41.4 +Pygments==2.19.2 +pyogrio==0.11.1 +pyparsing==3.2.5 +pyproj==3.7.2 +python-dateutil==2.9.0.post0 +pytz==2025.2 +pyvips==3.0.0 +PyYAML==6.0.3 +requests==2.32.5 +roifile==2025.5.10 +scikit-image==0.25.2 +scikit-learn==1.7.2 +scipy==1.16.2 +seaborn==0.13.2 +segment-anything==1.0 +setuptools==80.9.0 +shapely==2.1.2 +simpleitk==2.5.2 +six==1.17.0 +soupsieve==2.8 +stack-data==0.6.3 +sympy==1.14.0 +threadpoolctl==3.6.0 +tifffile==2025.10.16 +torch==2.9.0 +torchvision==0.24.0 +tqdm==4.67.1 +traitlets==5.14.3 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +tzdata==2025.2 +urllib3==2.5.0 +-e git+ssh://git@github.com/jeffquinn-msk/valis.git@67cf56e1f4df9608f35087fb0b812f3a98008cba#egg=valis +wcwidth==0.2.14 +weightedstats==0.4.1 +xsdata==25.7 +zarr==3.1.3 diff --git a/examples/basic_registration.py b/examples/basic_registration.py new file mode 100644 index 00000000..890c329e --- /dev/null +++ b/examples/basic_registration.py @@ -0,0 +1,50 @@ +"""Basic VALIS registration example. + +Register a directory of images, then warp and save them as ome.tiff files. +This is the minimal working example — roughly 20 lines of "real" code. + +Usage +----- + python examples/basic_registration.py --src /path/to/slides --dst /path/to/results + +The script will: + 1. Register every supported image in ``src_dir`` + 2. Print a summary table of registration error + 3. Save each registered slide as an ome.tiff in ``dst_dir/registered/`` +""" + +import argparse +import pathlib +import sys + +from valis import registration + + +def main(src_dir: str, dst_dir: str) -> None: + registrar = registration.Valis(src_dir, dst_dir) + + rigid_registrar, non_rigid_registrar, error_df = registrar.register() + + print("\nRegistration summary:") + print( + error_df[ + ["from", "to", "mean_original_D", "mean_rigid_D", "mean_non_rigid_D"] + ].to_string(index=False) + ) + + registered_dir = str(pathlib.Path(dst_dir) / "registered") + registrar.warp_and_save_slides(registered_dir) + print(f"\nWarped slides saved to: {registered_dir}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Basic VALIS registration") + parser.add_argument( + "--src", required=True, help="Directory containing source slides" + ) + parser.add_argument( + "--dst", required=True, help="Directory for registration results" + ) + args = parser.parse_args() + + main(args.src, args.dst) diff --git a/examples/example_datasets/cycif/CD20 FOXP3 CD3.ome.tiff b/examples/example_datasets/cycif/CD20 FOXP3 CD3.ome.tiff deleted file mode 100644 index d8ff9b1b..00000000 Binary files a/examples/example_datasets/cycif/CD20 FOXP3 CD3.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/cycif/CD4 CD68 CD3.ome.tiff b/examples/example_datasets/cycif/CD4 CD68 CD3.ome.tiff deleted file mode 100644 index c1de22b3..00000000 Binary files a/examples/example_datasets/cycif/CD4 CD68 CD3.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/cycif/CD8 FOXP3 CD20.ome.tiff b/examples/example_datasets/cycif/CD8 FOXP3 CD20.ome.tiff deleted file mode 100644 index cc831ca8..00000000 Binary files a/examples/example_datasets/cycif/CD8 FOXP3 CD20.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/ihc/ihc_1.ome.tiff b/examples/example_datasets/ihc/ihc_1.ome.tiff deleted file mode 100644 index 4a78fabb..00000000 Binary files a/examples/example_datasets/ihc/ihc_1.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/ihc/ihc_2.ome.tiff b/examples/example_datasets/ihc/ihc_2.ome.tiff deleted file mode 100644 index 848c37c7..00000000 Binary files a/examples/example_datasets/ihc/ihc_2.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/ihc/ihc_3.ome.tiff b/examples/example_datasets/ihc/ihc_3.ome.tiff deleted file mode 100644 index d40c70c6..00000000 Binary files a/examples/example_datasets/ihc/ihc_3.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/ihc/ihc_4.ome.tiff b/examples/example_datasets/ihc/ihc_4.ome.tiff deleted file mode 100644 index 975ff41f..00000000 Binary files a/examples/example_datasets/ihc/ihc_4.ome.tiff and /dev/null differ diff --git a/examples/example_datasets/ihc/ihc_5.ome.tiff b/examples/example_datasets/ihc/ihc_5.ome.tiff deleted file mode 100644 index eb942eba..00000000 Binary files a/examples/example_datasets/ihc/ihc_5.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/cycif/cycif.ome.tiff b/examples/expected_results/registered_slides/cycif/cycif.ome.tiff deleted file mode 100644 index e7622b38..00000000 Binary files a/examples/expected_results/registered_slides/cycif/cycif.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/ihc/ihc_1.ome.tiff b/examples/expected_results/registered_slides/ihc/ihc_1.ome.tiff deleted file mode 100644 index 812e14b5..00000000 Binary files a/examples/expected_results/registered_slides/ihc/ihc_1.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/ihc/ihc_2.ome.tiff b/examples/expected_results/registered_slides/ihc/ihc_2.ome.tiff deleted file mode 100644 index be479aaa..00000000 Binary files a/examples/expected_results/registered_slides/ihc/ihc_2.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/ihc/ihc_3.ome.tiff b/examples/expected_results/registered_slides/ihc/ihc_3.ome.tiff deleted file mode 100644 index b4746ca7..00000000 Binary files a/examples/expected_results/registered_slides/ihc/ihc_3.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/ihc/ihc_4.ome.tiff b/examples/expected_results/registered_slides/ihc/ihc_4.ome.tiff deleted file mode 100644 index a3bedc4b..00000000 Binary files a/examples/expected_results/registered_slides/ihc/ihc_4.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registered_slides/ihc/ihc_5.ome.tiff b/examples/expected_results/registered_slides/ihc/ihc_5.ome.tiff deleted file mode 100644 index d5324fe7..00000000 Binary files a/examples/expected_results/registered_slides/ihc/ihc_5.ome.tiff and /dev/null differ diff --git a/examples/expected_results/registration/cycif/data/cycif_summary.csv b/examples/expected_results/registration/cycif/data/cycif_summary.csv deleted file mode 100644 index be34b0de..00000000 --- a/examples/expected_results/registration/cycif/data/cycif_summary.csv +++ /dev/null @@ -1,4 +0,0 @@ -filename,from,to,original_D,original_rTRE,rigid_D,rigid_rTRE,non_rigid_D,non_rigid_rTRE,processed_img_shape,shape,aligned_shape,mean_original_D,mean_rigid_D,physical_units,resolution,name,rigid_time_minutes,mean_non_rigid_D,non_rigid_time_minutes -./example_datasets/cycif/CD20 FOXP3 CD3.ome.tiff,CD20 FOXP3 CD3,CD8 FOXP3 CD20,92.4402126881,0.0410167334344,2.73298369432,0.00121265475718,2.64230345041,0.00117241894114,"(768, 1024)","(1344, 1792)","(1350, 1894)",134.220801059,2.95755874292,µm,1.7607183731,cycif,0.172802416484,2.8477064519,0.33151255846 -./example_datasets/cycif/CD8 FOXP3 CD20.ome.tiff,CD8 FOXP3 CD20,,,,,,,,"(768, 1024)","(1344, 1792)","(1350, 1894)",134.220801059,2.95755874292,µm,1.7607183731,cycif,0.172802416484,2.8477064519,0.33151255846 -./example_datasets/cycif/CD4 CD68 CD3.ome.tiff,CD4 CD68 CD3,CD8 FOXP3 CD20,175.773592765,0.0779926656333,3.18090936052,0.00141140427446,3.05198955248,0.00135420114557,"(768, 1024)","(1344, 1792)","(1350, 1894)",134.220801059,2.95755874292,µm,1.7607183731,cycif,0.172802416484,2.8477064519,0.33151255846 diff --git a/examples/expected_results/registration/cycif/deformation_fields/0_CD20 FOXP3 CD3.png b/examples/expected_results/registration/cycif/deformation_fields/0_CD20 FOXP3 CD3.png deleted file mode 100644 index 41a1655f..00000000 Binary files a/examples/expected_results/registration/cycif/deformation_fields/0_CD20 FOXP3 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/deformation_fields/1_CD8 FOXP3 CD20.png b/examples/expected_results/registration/cycif/deformation_fields/1_CD8 FOXP3 CD20.png deleted file mode 100644 index 4ddfc7fc..00000000 Binary files a/examples/expected_results/registration/cycif/deformation_fields/1_CD8 FOXP3 CD20.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/deformation_fields/2_CD4 CD68 CD3.png b/examples/expected_results/registration/cycif/deformation_fields/2_CD4 CD68 CD3.png deleted file mode 100644 index 729afa2b..00000000 Binary files a/examples/expected_results/registration/cycif/deformation_fields/2_CD4 CD68 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/masks/CD20 FOXP3 CD3.png b/examples/expected_results/registration/cycif/masks/CD20 FOXP3 CD3.png deleted file mode 100644 index 345a3f7c..00000000 Binary files a/examples/expected_results/registration/cycif/masks/CD20 FOXP3 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/masks/CD4 CD68 CD3.png b/examples/expected_results/registration/cycif/masks/CD4 CD68 CD3.png deleted file mode 100644 index 65f701e7..00000000 Binary files a/examples/expected_results/registration/cycif/masks/CD4 CD68 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/masks/CD8 FOXP3 CD20.png b/examples/expected_results/registration/cycif/masks/CD8 FOXP3 CD20.png deleted file mode 100644 index 9d55a709..00000000 Binary files a/examples/expected_results/registration/cycif/masks/CD8 FOXP3 CD20.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/masks/cycif_non_rigid_mask.png b/examples/expected_results/registration/cycif/masks/cycif_non_rigid_mask.png deleted file mode 100644 index ab052e84..00000000 Binary files a/examples/expected_results/registration/cycif/masks/cycif_non_rigid_mask.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/non_rigid_registration/0_CD20 FOXP3 CD3.png b/examples/expected_results/registration/cycif/non_rigid_registration/0_CD20 FOXP3 CD3.png deleted file mode 100644 index b7c76f31..00000000 Binary files a/examples/expected_results/registration/cycif/non_rigid_registration/0_CD20 FOXP3 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/non_rigid_registration/1_CD8 FOXP3 CD20.png b/examples/expected_results/registration/cycif/non_rigid_registration/1_CD8 FOXP3 CD20.png deleted file mode 100644 index 88d0cf90..00000000 Binary files a/examples/expected_results/registration/cycif/non_rigid_registration/1_CD8 FOXP3 CD20.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/non_rigid_registration/2_CD4 CD68 CD3.png b/examples/expected_results/registration/cycif/non_rigid_registration/2_CD4 CD68 CD3.png deleted file mode 100644 index e7d7bfee..00000000 Binary files a/examples/expected_results/registration/cycif/non_rigid_registration/2_CD4 CD68 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/overlaps/cycif_non_rigid_overlap.png b/examples/expected_results/registration/cycif/overlaps/cycif_non_rigid_overlap.png deleted file mode 100644 index 2b35c092..00000000 Binary files a/examples/expected_results/registration/cycif/overlaps/cycif_non_rigid_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/overlaps/cycif_original_overlap.png b/examples/expected_results/registration/cycif/overlaps/cycif_original_overlap.png deleted file mode 100644 index 4f3e7db8..00000000 Binary files a/examples/expected_results/registration/cycif/overlaps/cycif_original_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/overlaps/cycif_rigid_overlap.png b/examples/expected_results/registration/cycif/overlaps/cycif_rigid_overlap.png deleted file mode 100644 index 438a8a8f..00000000 Binary files a/examples/expected_results/registration/cycif/overlaps/cycif_rigid_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/processed/CD20 FOXP3 CD3.png b/examples/expected_results/registration/cycif/processed/CD20 FOXP3 CD3.png deleted file mode 100644 index 84bb18ca..00000000 Binary files a/examples/expected_results/registration/cycif/processed/CD20 FOXP3 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/processed/CD4 CD68 CD3.png b/examples/expected_results/registration/cycif/processed/CD4 CD68 CD3.png deleted file mode 100644 index f993d62b..00000000 Binary files a/examples/expected_results/registration/cycif/processed/CD4 CD68 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/processed/CD8 FOXP3 CD20.png b/examples/expected_results/registration/cycif/processed/CD8 FOXP3 CD20.png deleted file mode 100644 index f150039d..00000000 Binary files a/examples/expected_results/registration/cycif/processed/CD8 FOXP3 CD20.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/rigid_registration/0_CD20 FOXP3 CD3.png b/examples/expected_results/registration/cycif/rigid_registration/0_CD20 FOXP3 CD3.png deleted file mode 100644 index 937b85ec..00000000 Binary files a/examples/expected_results/registration/cycif/rigid_registration/0_CD20 FOXP3 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/rigid_registration/1_CD8 FOXP3 CD20.png b/examples/expected_results/registration/cycif/rigid_registration/1_CD8 FOXP3 CD20.png deleted file mode 100644 index 88d0cf90..00000000 Binary files a/examples/expected_results/registration/cycif/rigid_registration/1_CD8 FOXP3 CD20.png and /dev/null differ diff --git a/examples/expected_results/registration/cycif/rigid_registration/2_CD4 CD68 CD3.png b/examples/expected_results/registration/cycif/rigid_registration/2_CD4 CD68 CD3.png deleted file mode 100644 index 6f0ea482..00000000 Binary files a/examples/expected_results/registration/cycif/rigid_registration/2_CD4 CD68 CD3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/data/ihc_summary.csv b/examples/expected_results/registration/ihc/data/ihc_summary.csv deleted file mode 100644 index f0e7c446..00000000 --- a/examples/expected_results/registration/ihc/data/ihc_summary.csv +++ /dev/null @@ -1,6 +0,0 @@ -filename,from,to,original_D,original_rTRE,rigid_D,rigid_rTRE,non_rigid_D,non_rigid_rTRE,processed_img_shape,shape,aligned_shape,mean_original_D,mean_rigid_D,physical_units,resolution,name,rigid_time_minutes,mean_non_rigid_D,non_rigid_time_minutes -./example_datasets/ihc/ihc_5.ome.tiff,ihc_5,ihc_1,8847.55686815,0.78564583663,148.540211498,0.0131900817903,121.022714932,0.0107465816316,"(1024, 902)","(2682, 2362)","(3338, 3126)",1862.06724722,90.5091493323,µm,7.96451447743,ihc,0.305461676915,64.8437977636,0.68072655201 -./example_datasets/ihc/ihc_1.ome.tiff,ihc_1,ihc_3,670.072268746,0.0595030480131,87.4344320256,0.00776425984104,62.2361651422,0.00552663002983,"(1024, 971)","(2727, 2587)","(3338, 3126)",1862.06724722,90.5091493323,µm,7.96451447743,ihc,0.305461676915,64.8437977636,0.68072655201 -./example_datasets/ihc/ihc_3.ome.tiff,ihc_3,,,,,,,,"(1024, 975)","(2717, 2587)","(3338, 3126)",1862.06724722,90.5091493323,µm,7.96451447743,ihc,0.305461676915,64.8437977636,0.68072655201 -./example_datasets/ihc/ihc_2.ome.tiff,ihc_2,ihc_3,1315.33605688,0.116792929363,83.200705394,0.00738765888546,60.7041707177,0.00539011903882,"(1024, 981)","(2817, 2699)","(3338, 3126)",1862.06724722,90.5091493323,µm,7.96451447743,ihc,0.305461676915,64.8437977636,0.68072655201 -./example_datasets/ihc/ihc_4.ome.tiff,ihc_4,ihc_2,1978.03521828,0.175638657487,91.5194498496,0.00812642421984,64.1788973869,0.00569873340569,"(1024, 922)","(2997, 2699)","(3338, 3126)",1862.06724722,90.5091493323,µm,7.96451447743,ihc,0.305461676915,64.8437977636,0.68072655201 diff --git a/examples/expected_results/registration/ihc/deformation_fields/0_ihc_5.png b/examples/expected_results/registration/ihc/deformation_fields/0_ihc_5.png deleted file mode 100644 index b2820b57..00000000 Binary files a/examples/expected_results/registration/ihc/deformation_fields/0_ihc_5.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/deformation_fields/1_ihc_1.png b/examples/expected_results/registration/ihc/deformation_fields/1_ihc_1.png deleted file mode 100644 index 5b24b294..00000000 Binary files a/examples/expected_results/registration/ihc/deformation_fields/1_ihc_1.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/deformation_fields/2_ihc_3.png b/examples/expected_results/registration/ihc/deformation_fields/2_ihc_3.png deleted file mode 100644 index f304d12c..00000000 Binary files a/examples/expected_results/registration/ihc/deformation_fields/2_ihc_3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/deformation_fields/3_ihc_2.png b/examples/expected_results/registration/ihc/deformation_fields/3_ihc_2.png deleted file mode 100644 index 1d145670..00000000 Binary files a/examples/expected_results/registration/ihc/deformation_fields/3_ihc_2.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/deformation_fields/4_ihc_4.png b/examples/expected_results/registration/ihc/deformation_fields/4_ihc_4.png deleted file mode 100644 index dff0048f..00000000 Binary files a/examples/expected_results/registration/ihc/deformation_fields/4_ihc_4.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_1.png b/examples/expected_results/registration/ihc/masks/ihc_1.png deleted file mode 100644 index 177960e2..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_1.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_2.png b/examples/expected_results/registration/ihc/masks/ihc_2.png deleted file mode 100644 index 960da839..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_2.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_3.png b/examples/expected_results/registration/ihc/masks/ihc_3.png deleted file mode 100644 index 5ccf4a25..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_4.png b/examples/expected_results/registration/ihc/masks/ihc_4.png deleted file mode 100644 index bf16cf68..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_4.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_5.png b/examples/expected_results/registration/ihc/masks/ihc_5.png deleted file mode 100644 index 0e4631f1..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_5.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/masks/ihc_non_rigid_mask.png b/examples/expected_results/registration/ihc/masks/ihc_non_rigid_mask.png deleted file mode 100644 index 5003d98b..00000000 Binary files a/examples/expected_results/registration/ihc/masks/ihc_non_rigid_mask.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/non_rigid_registration/0_ihc_5.png b/examples/expected_results/registration/ihc/non_rigid_registration/0_ihc_5.png deleted file mode 100644 index 82c486e1..00000000 Binary files a/examples/expected_results/registration/ihc/non_rigid_registration/0_ihc_5.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/non_rigid_registration/1_ihc_1.png b/examples/expected_results/registration/ihc/non_rigid_registration/1_ihc_1.png deleted file mode 100644 index eccdc3ee..00000000 Binary files a/examples/expected_results/registration/ihc/non_rigid_registration/1_ihc_1.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/non_rigid_registration/2_ihc_3.png b/examples/expected_results/registration/ihc/non_rigid_registration/2_ihc_3.png deleted file mode 100644 index 6ffcfe96..00000000 Binary files a/examples/expected_results/registration/ihc/non_rigid_registration/2_ihc_3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/non_rigid_registration/3_ihc_2.png b/examples/expected_results/registration/ihc/non_rigid_registration/3_ihc_2.png deleted file mode 100644 index c62adb7e..00000000 Binary files a/examples/expected_results/registration/ihc/non_rigid_registration/3_ihc_2.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/non_rigid_registration/4_ihc_4.png b/examples/expected_results/registration/ihc/non_rigid_registration/4_ihc_4.png deleted file mode 100644 index 3b86b240..00000000 Binary files a/examples/expected_results/registration/ihc/non_rigid_registration/4_ihc_4.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/overlaps/ihc_non_rigid_overlap.png b/examples/expected_results/registration/ihc/overlaps/ihc_non_rigid_overlap.png deleted file mode 100644 index e2199457..00000000 Binary files a/examples/expected_results/registration/ihc/overlaps/ihc_non_rigid_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/overlaps/ihc_original_overlap.png b/examples/expected_results/registration/ihc/overlaps/ihc_original_overlap.png deleted file mode 100644 index 216189a7..00000000 Binary files a/examples/expected_results/registration/ihc/overlaps/ihc_original_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/overlaps/ihc_rigid_overlap.png b/examples/expected_results/registration/ihc/overlaps/ihc_rigid_overlap.png deleted file mode 100644 index cd76f83e..00000000 Binary files a/examples/expected_results/registration/ihc/overlaps/ihc_rigid_overlap.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/processed/ihc_1.png b/examples/expected_results/registration/ihc/processed/ihc_1.png deleted file mode 100644 index 9045b719..00000000 Binary files a/examples/expected_results/registration/ihc/processed/ihc_1.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/processed/ihc_2.png b/examples/expected_results/registration/ihc/processed/ihc_2.png deleted file mode 100644 index 510ee5f0..00000000 Binary files a/examples/expected_results/registration/ihc/processed/ihc_2.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/processed/ihc_3.png b/examples/expected_results/registration/ihc/processed/ihc_3.png deleted file mode 100644 index 9da1f8a1..00000000 Binary files a/examples/expected_results/registration/ihc/processed/ihc_3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/processed/ihc_4.png b/examples/expected_results/registration/ihc/processed/ihc_4.png deleted file mode 100644 index a9be03c5..00000000 Binary files a/examples/expected_results/registration/ihc/processed/ihc_4.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/processed/ihc_5.png b/examples/expected_results/registration/ihc/processed/ihc_5.png deleted file mode 100644 index bc625b4b..00000000 Binary files a/examples/expected_results/registration/ihc/processed/ihc_5.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/rigid_registration/0_ihc_5.png b/examples/expected_results/registration/ihc/rigid_registration/0_ihc_5.png deleted file mode 100644 index 25591d7d..00000000 Binary files a/examples/expected_results/registration/ihc/rigid_registration/0_ihc_5.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/rigid_registration/1_ihc_1.png b/examples/expected_results/registration/ihc/rigid_registration/1_ihc_1.png deleted file mode 100644 index 52f25c21..00000000 Binary files a/examples/expected_results/registration/ihc/rigid_registration/1_ihc_1.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/rigid_registration/2_ihc_3.png b/examples/expected_results/registration/ihc/rigid_registration/2_ihc_3.png deleted file mode 100644 index 6ffcfe96..00000000 Binary files a/examples/expected_results/registration/ihc/rigid_registration/2_ihc_3.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/rigid_registration/3_ihc_2.png b/examples/expected_results/registration/ihc/rigid_registration/3_ihc_2.png deleted file mode 100644 index 5644aafd..00000000 Binary files a/examples/expected_results/registration/ihc/rigid_registration/3_ihc_2.png and /dev/null differ diff --git a/examples/expected_results/registration/ihc/rigid_registration/4_ihc_4.png b/examples/expected_results/registration/ihc/rigid_registration/4_ihc_4.png deleted file mode 100644 index 30a57a73..00000000 Binary files a/examples/expected_results/registration/ihc/rigid_registration/4_ihc_4.png and /dev/null differ diff --git a/examples/expected_results/roi/ihc_roi.png b/examples/expected_results/roi/ihc_roi.png deleted file mode 100644 index d1af7eea..00000000 Binary files a/examples/expected_results/roi/ihc_roi.png and /dev/null differ diff --git a/examples/extract_transforms.py b/examples/extract_transforms.py new file mode 100644 index 00000000..ca1c9ab6 --- /dev/null +++ b/examples/extract_transforms.py @@ -0,0 +1,88 @@ +"""Extract transformation matrices from a completed registration. + +After registration, each ``Slide`` object holds: + - ``slide.M`` — the 3×3 inverse rigid transformation matrix + - ``slide.bk_dxdy`` — backward non-rigid displacement field (numpy array) + - ``slide.fwd_dxdy`` — forward non-rigid displacement field (numpy array) + +This example shows how to dump those values so they can be applied in another +tool (e.g. ImageJ/FIJI, QuPath, or a custom pipeline). + +Usage +----- + # Run after basic_registration.py has been executed: + python examples/extract_transforms.py --data /path/to/results//data/ +""" + +import argparse +import glob +import json +import pathlib + +import numpy as np + +from valis import registration + + +def find_pickle(data_dir: str) -> str: + matches = glob.glob(str(pathlib.Path(data_dir) / "*.pickle")) + if not matches: + raise FileNotFoundError(f"No pickled registrar found in {data_dir}") + return matches[0] + + +def main(data_dir: str, out_dir: str) -> None: + pickle_f = find_pickle(data_dir) + registrar = registration.load_registrar(pickle_f) + + out_path = pathlib.Path(out_dir) + out_path.mkdir(parents=True, exist_ok=True) + + transforms = {} + for slide_name, slide in registrar.slide_dict.items(): + entry = { + "name": slide_name, + "src_f": slide.src_f, + "M": slide.M.tolist() if slide.M is not None else None, + "transformation_src_shape_rc": list(slide.processed_img_shape_rc), + "transformation_dst_shape_rc": list(slide.reg_img_shape_rc), + } + transforms[slide_name] = entry + + # Save displacement fields as numpy .npy files if available + bk = slide.bk_dxdy + fwd = slide.fwd_dxdy + if isinstance(bk, np.ndarray): + np.save(str(out_path / f"{slide_name}_bk_dxdy.npy"), bk) + if isinstance(fwd, np.ndarray): + np.save(str(out_path / f"{slide_name}_fwd_dxdy.npy"), fwd) + + json_f = out_path / "transforms.json" + with open(json_f, "w") as f: + json.dump(transforms, f, indent=2) + + print(f"Transformation matrices written to: {json_f}") + print(f"Displacement fields (if any) written to: {out_path}") + + # Print a quick summary table + print(f"\n{'Slide':<40} {'Has M':>6} {'Has dxdy':>9}") + print("-" * 58) + for name, entry in transforms.items(): + has_m = "yes" if entry["M"] is not None else "no" + bk_f = out_path / f"{name}_bk_dxdy.npy" + has_dxdy = "yes" if bk_f.exists() else "no" + print(f"{name:<40} {has_m:>6} {has_dxdy:>9}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Export VALIS transformation matrices") + parser.add_argument( + "--data", required=True, help="Path to the registrar's data/ directory" + ) + parser.add_argument( + "--out", default=None, help="Output directory (default: data/../transforms)" + ) + args = parser.parse_args() + + out = args.out or str(pathlib.Path(args.data).parent / "transforms") + main(args.data, out) diff --git a/examples/non_rigid_registration.py b/examples/non_rigid_registration.py new file mode 100644 index 00000000..fd881c92 --- /dev/null +++ b/examples/non_rigid_registration.py @@ -0,0 +1,80 @@ +"""Non-rigid registration example. + +Demonstrates how to enable and configure non-rigid (deformable) registration, +when to use it, and how to tune the key parameters. + +Non-rigid registration is useful when: + - Images were acquired at different times (tissue shrinkage / swelling) + - Different stains cause structural deformation + - There is non-linear distortion from the imaging system + +For most brightfield H&E / IHC series, rigid registration is sufficient. +Non-rigid registration is most valuable for cyclic immunofluorescence (CyCIF), +CODEX, or any multi-round assay where the tissue can deform between rounds. + +Usage +----- + python examples/non_rigid_registration.py --src /path/to/slides --dst /path/to/results + +Optional flags +-------------- + --no-non-rigid Run rigid-only (useful for comparison) + --compose Compose non-rigid fields serially (useful for large deformations) +""" + +import argparse +import pathlib + +from valis import registration, non_rigid_registrars +from valis.registration import RegistrationConfig, CropMode + + +def main(src_dir: str, dst_dir: str, do_non_rigid: bool, compose: bool) -> None: + if do_non_rigid: + # OpticalFlowWarper is the default and works well for most cases. + # For large deformations, try SimpleElastix (requires SimpleITK). + nr_cls = non_rigid_registrars.OpticalFlowWarper() + print("Non-rigid registration: OpticalFlowWarper") + else: + nr_cls = None + print("Non-rigid registration: disabled (rigid only)") + + config = RegistrationConfig( + non_rigid_registrar_cls=nr_cls, + compose_non_rigid=compose, + # Larger window improves non-rigid accuracy at the cost of speed + max_non_rigid_registration_dim_px=2048, + crop=CropMode.OVERLAP, + ) + + registrar = registration.Valis(src_dir, dst_dir, config=config) + rigid_registrar, non_rigid_registrar, error_df = registrar.register() + + cols = ["from", "to", "mean_rigid_D"] + if do_non_rigid: + cols.append("mean_non_rigid_D") + print("\nRegistration error:") + print(error_df[cols].to_string(index=False)) + + registered_dir = str(pathlib.Path(dst_dir) / "registered") + registrar.warp_and_save_slides(registered_dir, non_rigid=do_non_rigid) + print(f"\nWarped slides saved to: {registered_dir}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Non-rigid VALIS registration") + parser.add_argument("--src", required=True) + parser.add_argument("--dst", required=True) + parser.add_argument( + "--no-non-rigid", + action="store_true", + help="Disable non-rigid registration (rigid only)", + ) + parser.add_argument( + "--compose", + action="store_true", + help="Compose non-rigid deformation fields serially", + ) + args = parser.parse_args() + + main(args.src, args.dst, not args.no_non_rigid, args.compose) diff --git a/examples/pytorch_feature_detectors.py b/examples/pytorch_feature_detectors.py deleted file mode 100644 index 94181e03..00000000 --- a/examples/pytorch_feature_detectors.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -Compare performance of feature detectors -""" -import os -import sys -sys.path.append("/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/valis") -import time -import torch -import numpy as np -from valis import registration, valtils, feature_detectors, feature_matcher, preprocessing, viz, warp_tools - -import matplotlib.pyplot as plt - -def get_dirs(): - cwd = os.getcwd() - in_container = sys.platform == "linux" and os.getcwd() == cwd - if not in_container: - dir_split = cwd.split(os.sep) - split_idx = [i for i in range(len(dir_split)) if dir_split[i] == "valis_project"][0] - parent_dir = os.sep.join(dir_split[:split_idx+1]) - - results_dst_dir = os.path.join(parent_dir, f"valis/tests/{sys.version_info.major}{sys.version_info.minor}") - else: - parent_dir = "/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project" - results_dst_dir = os.path.join(parent_dir, f"valis/tests/docker") - - return parent_dir, results_dst_dir, in_container - - -def cnames_from_filename(src_f): - """Get channel names from file name - Note that the DAPI channel is not part of the filename - but is always the first channel. - - """ - f = valtils.get_name(src_f) - return ["DAPI"] + f.split(" ") - - -# from valis import slide_io -# slide_io.init_jvm("path/to/bioformats.jar") -# from valis import registration - -# registrar = registration.Valis(ihc_src_dir, dst_dir) -# _, _, error_df = registrar.register(reader_cls=slide_io.VipsSlideReader) - - -parent_dir, results_dst_dir, in_container = get_dirs() -results_dst_dir = os.path.join(results_dst_dir, "examples/feature_detectors") -datasets_src_dir = os.path.join(parent_dir, "valis/examples/example_datasets/") -ihc_src_dir = os.path.join(datasets_src_dir, "ihc") - -# Use DeDoDe RGB features -brightfield_processing_cls=preprocessing.OD -brightfield_processing_kwargs={"adaptive_eq":False} - -dedode_matcher_obj = feature_matcher.LightGlueMatcher(feature_detectors.DeDoDeFD(rgb=False)) -disk_matcher_obj = feature_matcher.LightGlueMatcher(feature_detectors.DiskFD(rgb=False)) -default_matcher_obj = feature_matcher.Matcher() - -matcher_list = [dedode_matcher_obj, disk_matcher_obj, default_matcher_obj] -n_matchers = len(matcher_list) -elapsed_times = n_matchers*[None] -avg_errors = n_matchers*[None] -avg_matches = n_matchers*[None] -reg_list = n_matchers*[None] - -for i, matcher in enumerate(matcher_list): - dst_dir = os.path.join(results_dst_dir, matcher.feature_detector.__class__.__name__) - start = time.time() - registrar = registration.Valis(ihc_src_dir, dst_dir, matcher=matcher) - _, _, error_df = registrar.register(brightfield_processing_cls=brightfield_processing_cls, brightfield_processing_kwargs=brightfield_processing_kwargs) - stop = time.time() - elapsed = stop - start - - registrar.draw_matches(registrar.dst_dir) - ref_slide_obj = registrar.get_ref_slide() - n_matches = [slide_obj.xy_in_prev.shape[0] for slide_obj in registrar.slide_dict.values() if slide_obj != ref_slide_obj] - - avg_errors[i] = np.max(error_df["mean_non_rigid_D"]) - reg_list[i] = registrar - elapsed_times[i] = elapsed - avg_matches[i] = np.mean(n_matches) - -default_reg_idx = [i for i in range(n_matchers) if not isinstance(matcher_list[i], feature_matcher.LightGlueMatcher)][0] -default_reg = reg_list[default_reg_idx] -ref_slide_obj = default_reg.get_ref_slide() -slide_list =[slide_obj for slide_obj in registrar.slide_dict.values() if slide_obj != ref_slide_obj] -# n_matches = [slide_obj.xy_in_prev.shape[0] for slide_obj in slide_list] -# min_matches_idx = np.argmin(n_matches) -# moving_slide_name = slide_list[min_matches_idx].name -# fixed_slide_name = slide_list[min_matches_idx].fixed_slide.name - - -for slide_obj in slide_list: - moving_slide_name = slide_obj.name - fixed_slide_name = slide_obj.fixed_slide.name - fig, axes = plt.subplots(1, 3, figsize=(10, 10)) - ax = axes.ravel() - for i, reg in enumerate(reg_list): - moving_slide = reg.get_slide(moving_slide_name) - fixed_slide = reg.get_slide(fixed_slide_name) - - if moving_slide.image.ndim == 3 and moving_slide.is_rgb: - moving_draw_img = warp_tools.resize_img(moving_slide.image, moving_slide.processed_img_shape_rc) - else: - moving_draw_img = moving_slide.pad_cropped_processed_img() - - if fixed_slide.image.ndim == 3 and fixed_slide.is_rgb: - fixed_draw_img = warp_tools.resize_img(fixed_slide.image, fixed_slide.processed_img_shape_rc) - else: - fixed_draw_img = fixed_slide.pad_cropped_processed_img() - - all_matches_img = viz.draw_matches(src_img=moving_draw_img, kp1_xy=moving_slide.xy_matched_to_prev, - dst_img=fixed_draw_img, kp2_xy=moving_slide.xy_in_prev, - rad=3, alignment='vertical') - - fd_name = matcher_list[i].feature_detector.__class__.__name__ - n_matches = moving_slide.xy_matched_to_prev.shape[0] - ax[i].imshow(all_matches_img) - ax[i].set_title(f"{fd_name}= {n_matches} matches") - ax[i].tick_params(left = False, right = False , labelleft = False , - labelbottom = False, bottom = False) - - fig.tight_layout() - # plt.tick_params(left = False, right = False , labelleft = False , - # labelbottom = False, bottom = False) - plt.savefig(os.path.join(results_dst_dir, f"{moving_slide_name}_to_{fixed_slide_name}.png")) - plt.close() - - diff --git a/examples/register_and_merge_cycif.py b/examples/register_and_merge_cycif.py deleted file mode 100644 index db56f8ce..00000000 --- a/examples/register_and_merge_cycif.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Merging of whole slide images (WSI) CyCIF images - -This example shows how to register and merge a set -of slides. In this case, there are 3 CyCIF images. - -The results directory contains several folders: - -1. *data* contains 2 files: - * a summary spreadsheet of the alignment results, such - as the registration error between each pair of slides, their - dimensions, physical units, etc... - - * a pickled version of the registrar. This can be reloaded - (unpickled) and used later. For example, one could perform - the registration locally, but then use the pickled object - to warp and save the slides on an HPC. Or, one could perform - the registration and use the registrar later to warp - points in the slide. - -2. *overlaps* contains thumbnails showing the how the images - would look if stacked without being registered, how they - look after rigid registration, and how they would look - after non-rigid registration. - -3. *rigid_registration* shows thumbnails of how each image - looks after performing rigid registration. - -4. *non_rigid_registration* shows thumbnaials of how each - image looks after non-rigid registration. - -5. *deformation_fields* contains images showing what the - non-rigid deformation would do to a triangular mesh. - These can be used to get a better sense of how the - images were altered by non-rigid warping - -6. *processed* shows thumnails of the processed images. - This are thumbnails of the images that are actually - used to perform the registration. The pre-processing - and normalization methods should try to make these - images look as similar as possible. - - -After registraation is complete, one should view the -results to determine if they aare acceptable. - -Since the slides are being merged, one may want to provide -channel names. This can be accomplished by passing a -channel names dictionary to the Valis.warp_and_merge_slides -method. - -""" - -import time -import os - -from valis import registration, valtils - -slide_src_dir = "./example_datasets/cycif" -results_dst_dir = "./expected_results/registration" - -# Create a Valis object and use it to register the slides in slide_src_dir -start = time.time() -registrar = registration.Valis(slide_src_dir, results_dst_dir) -rigid_registrar, non_rigid_registrar, error_df = registrar.register() -stop = time.time() -elapsed = stop - start - -print(f"regisration time is {elapsed/60} minutes") - - -# Merge registered channels -def cnames_from_filename(src_f): - """Get channel names from file name - Note that the DAPI channel is not part of the filename - but is always the first channel. - - """ - f = valtils.get_name(src_f) - return ["DAPI"] + f.split(" ") - - -channel_name_dict = {f: cnames_from_filename(f) for - f in registrar.original_img_list} - -dst_f = os.path.join("./expected_results/registered_slides", registrar.name, registrar.name + ".ome.tiff") -start = time.time() -merged_img, channel_names, ome_xml = registrar.warp_and_merge_slides(dst_f, - channel_name_dict=channel_name_dict, - drop_duplicates=True) -stop = time.time() -elapsed = stop - start - -print(f"Time to warp, merge, and save slides is {elapsed/60} minutes") - -registration.kill_jvm() diff --git a/examples/register_high_rez.py b/examples/register_high_rez.py deleted file mode 100644 index 721bb18d..00000000 --- a/examples/register_high_rez.py +++ /dev/null @@ -1,56 +0,0 @@ -""" Registration of whole slide images (WSI) using higher resolution images - -This example shows how to register the slides using higher resolution images. -An initial rigid transform is found using low resolition images, but the -`MicroRigidRegistrar` can be used to update that transform using feature matches -found in higher resoltion images. This can be followed up by the high resolution -non-rigid registration (i.e. micro-registration). - -""" - -import sys -sys.path.append("/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/valis") - - -andry_dir = "/Users/gatenbcd/Dropbox/Documents/Andriy/Bina_alignments/slides/NSG_from_Marusyk/NSG 48 hours_374" -mrxs_dir = "/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/resources/slides/ihc_mrxs" -andor_dir = "/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/resources/slides/ihc_bf" -cycif_dir = "/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/resources/slides/cycif" -ad_dir = "/Users/gatenbcd/Dropbox/Documents/image_processing/valis_project/resources/adenoma" -tme_dir = "/Users/gatenbcd/Dropbox/Documents/Lab_Chung/Collaboration_with_Sandy/lab_chung_hnscc/images/TME/06S17070207" -slide_src_dir = andor_dir - -import time -import os -import numpy as np -from valis import registration -from valis.micro_rigid_registrar import MicroRigidRegistrar # For high resolution rigid registration - - -slide_src_dir = "./example_datasets/ihc" -results_dst_dir = "./expected_results/registration_hi_rez" -micro_reg_fraction = 0.25 # Fraction full resolution used for non-rigid registration - -# Perform high resolution rigid registration using the MicroRigidRegistrar -start = time.time() -registrar = registration.Valis(slide_src_dir, results_dst_dir, micro_rigid_registrar_cls=MicroRigidRegistrar) -rigid_registrar, non_rigid_registrar, error_df = registrar.register() - -# Calculate what `max_non_rigid_registration_dim_px` needs to be to do non-rigid registration on an image that is 25% full resolution. -img_dims = np.array([slide_obj.slide_dimensions_wh[0] for slide_obj in registrar.slide_dict.values()]) -min_max_size = np.min([np.max(d) for d in img_dims]) -img_areas = [np.multiply(*d) for d in img_dims] -max_img_w, max_img_h = tuple(img_dims[np.argmax(img_areas)]) -micro_reg_size = np.floor(min_max_size*micro_reg_fraction).astype(int) - -# Perform high resolution non-rigid registration -micro_reg, micro_error = registrar.register_micro(max_non_rigid_registration_dim_px=micro_reg_size) - - -stop = time.time() -elapsed = stop - start -print(f"regisration time is {elapsed/60} minutes") - -# We can also plot the high resolution matches using `Valis.draw_matches`: -matches_dst_dir = os.path.join(registrar.dst_dir, "hi_rez_matches") -registrar.draw_matches(matches_dst_dir) \ No newline at end of file diff --git a/examples/register_ihc.py b/examples/register_ihc.py deleted file mode 100644 index acd3653f..00000000 --- a/examples/register_ihc.py +++ /dev/null @@ -1,76 +0,0 @@ -""" Registration of whole slide images (WSI) - -This example shows how to register, warp, and save a collection -of whole slide images (WSI) using the default parameters. - -The results directory contains several folders: - -1. *data* contains 2 files: - * a summary spreadsheet of the alignment results, such - as the registration error between each pair of slides, their - dimensions, physical units, etc... - - * a pickled version of the registrar. This can be reloaded - (unpickled) and used later. For example, one could perform - the registration locally, but then use the pickled object - to warp and save the slides on an HPC. Or, one could perform - the registration and use the registrar later to warp - points in the slide. - -2. *overlaps* contains thumbnails showing the how the images - would look if stacked without being registered, how they - look after rigid registration, and how they would look - after non-rigid registration. - -3. *rigid_registration* shows thumbnails of how each image - looks after performing rigid registration. - -4. *non_rigid_registration* shows thumbnaials of how each - image looks after non-rigid registration. - -5. *deformation_fields* contains images showing what the - non-rigid deformation would do to a triangular mesh. - These can be used to get a better sense of how the - images were altered by non-rigid warping - -6. *processed* shows thumnails of the processed images. - This are thumbnails of the images that are actually - used to perform the registration. The pre-processing - and normalization methods should try to make these - images look as similar as possible. - - -After registraation is complete, one should view the -results to determine if they aare acceptable. If they -are, then one can warp and save all of the slides. - -""" - - -import torch -import time -import os -from valis import registration - - -slide_src_dir = "./example_datasets/ihc" -results_dst_dir = "./expected_results/registration" - -# Create a Valis object and use it to register the slides in slide_src_dir -start = time.time() -registrar = registration.Valis(slide_src_dir, results_dst_dir) -rigid_registrar, non_rigid_registrar, error_df = registrar.register() -stop = time.time() -elapsed = stop - start - -# Check results in registered_slide_dst_dir. If they look good, export the registered slides -registered_slide_dst_dir = os.path.join("./expected_results/registered_slides", registrar.name) -start = time.time() -registrar.warp_and_save_slides(registered_slide_dst_dir) -stop = time.time() -elapsed = stop - start -print(f"saving {registrar.size} slides took {elapsed/60} minutes") - - -# Shutdown the JVM -registration.kill_jvm() diff --git a/examples/resume_from_saved_state.py b/examples/resume_from_saved_state.py new file mode 100644 index 00000000..a106cc02 --- /dev/null +++ b/examples/resume_from_saved_state.py @@ -0,0 +1,89 @@ +"""Resume registration from a saved Valis object. + +Once ``registrar.register()`` has been run once, VALIS pickles the registrar to +``//data/``. This example shows how to reload it and warp slides +(or point data) without rerunning the full registration. + +Usage +----- + # First run (creates the pickle): + python examples/basic_registration.py --src /path/to/slides --dst /path/to/results + + # Resume (warp slides only): + python examples/resume_from_saved_state.py --data /path/to/results//data/ + + # Or warp a set of (x, y) points from a CSV: + python examples/resume_from_saved_state.py --data /path/to/results//data/ \\ + --points slide1.tiff points.csv +""" + +import argparse +import glob +import pathlib + +import numpy as np +import pandas as pd + +from valis import registration + + +def find_pickle(data_dir: str) -> str: + matches = glob.glob(str(pathlib.Path(data_dir) / "*.pickle")) + if not matches: + raise FileNotFoundError(f"No pickled registrar found in {data_dir}") + return matches[0] + + +def main(data_dir: str, registered_dir: str, points_args: list) -> None: + pickle_f = find_pickle(data_dir) + print(f"Loading registrar from: {pickle_f}") + registrar = registration.load_registrar(pickle_f) + + # --- Warp and save slides ------------------------------------------- + pathlib.Path(registered_dir).mkdir(parents=True, exist_ok=True) + registrar.warp_and_save_slides(registered_dir) + print(f"Warped slides saved to: {registered_dir}") + + # --- Warp point coordinates (optional) -------------------------------- + if points_args: + slide_name, csv_path = points_args + slide_obj = registrar.get_slide(slide_name) + if slide_obj is None: + print( + f"Could not find slide '{slide_name}' in registrar. Available slides:" + ) + for name in registrar.slide_dict: + print(f" {name}") + return + + pts_df = pd.read_csv(csv_path) + xy = pts_df[["x", "y"]].values.astype(float) + warped_xy = slide_obj.warp_xy(xy) + out_f = str(pathlib.Path(csv_path).with_suffix("")) + "_warped.csv" + pd.DataFrame(warped_xy, columns=["x_warped", "y_warped"]).to_csv( + out_f, index=False + ) + print(f"Warped points saved to: {out_f}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Resume VALIS from saved state") + parser.add_argument( + "--data", required=True, help="Path to the registrar's data/ directory" + ) + parser.add_argument( + "--out", + default=None, + help="Where to save warped slides (default: data/../registered)", + ) + parser.add_argument( + "--points", + nargs=2, + metavar=("SLIDE_NAME", "CSV"), + help="Warp (x,y) points from CSV for the given slide name", + ) + args = parser.parse_args() + + data_dir = args.data + out_dir = args.out or str(pathlib.Path(data_dir).parent / "registered") + main(data_dir, out_dir, args.points) diff --git a/examples/warp_annotation_image.py b/examples/warp_annotation_image.py deleted file mode 100644 index 15cc5000..00000000 --- a/examples/warp_annotation_image.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Example showing how to warp an annotation image to go onto another image. - -`slide_src_dir` is where the slides to be registered are located -`results_dst_dir` is where to save the results -`slide_src_dir` is where the slides to be registered are located -`reference_img_f` is the filename of the image on which the annotations are based -`labeled_img_f` is the filename of actual annotation image -""" - -import os -import pathlib -from valis import registration, slide_io, viz, warp_tools - - - -# Perform registration. Can optinally set the reference image to be the same as the one the annotations are based on (i.e. `reference_img_f`) -registrar = registration.Valis(slide_src_dir, results_dst_dir, reference_img_f=reference_img_f) -rigid_registrar, non_rigid_registrar, error_df = registrar.register() - -# Load labeled image saved as `labeled_img_f` -labeled_img_reader_cls = slide_io.get_slide_reader(labeled_img_f) -labeled_img_reader = labeled_img_reader_cls(labeled_img_f) -labeled_img = labeled_img_reader.slide2vips(0) - -# Have reference slide warp the labeled image onto the others and save the results -reference_slide = registrar.get_slide(reference_img_f) - -annotations_dst_dir = os.path.join(registrar.dst_dir, "annotations") -pathlib.Path(annotations_dst_dir).mkdir(exist_ok=True, parents=True) - -# Create and save an annotation image for each slide -for slide_obj in registrar.slide_dict.values(): - if slide_obj == reference_slide: - continue - - # Warp the labeled image from the annotation image to this different slide # - transferred_annotation_img = reference_slide.warp_img_from_to(labeled_img, - to_slide_obj=slide_obj, - interp_method="nearest") - - # Create image metadata. Note that you could add channel names if your labeled image has a different classification in each channel - bf_dtype = slide_io.vips2bf_dtype(transferred_annotation_img.format) - xyzct = slide_io.get_shape_xyzct((transferred_annotation_img.width, transferred_annotation_img.height), transferred_annotation_img.bands) - px_phys_size = reference_slide.reader.metadata.pixel_physical_size_xyu - new_ome = slide_io.create_ome_xml(xyzct, bf_dtype, is_rgb=False, pixel_physical_size_xyu=px_phys_size) - ome_xml = new_ome.to_xml() - - # Save the labeled image as an ome.tiff # - dst_f = os.path.join(annotations_dst_dir, f"{slide_obj.name}_annotations.ome.tiff") - slide_io.save_ome_tiff(transferred_annotation_img, dst_f=dst_f, ome_xml=ome_xml) - - # Save a thumbnail with the annotation on top of the image # - small_annotation_img = warp_tools.resize_img(transferred_annotation_img, slide_obj.image.shape[0:2]) - small_annotation_img_np = warp_tools.vips2numpy(small_annotation_img) - small_img_with_annotation = viz.draw_outline(slide_obj.image, small_annotation_img_np) - thumbnail_dst_f = os.path.join(annotations_dst_dir, f"{slide_obj.name}_annotated.png") - warp_tools.save_img(thumbnail_dst_f, small_img_with_annotation) \ No newline at end of file diff --git a/examples/warp_associated_img/ihc/data/ihc_summary.csv b/examples/warp_associated_img/ihc/data/ihc_summary.csv deleted file mode 100644 index c010d004..00000000 --- a/examples/warp_associated_img/ihc/data/ihc_summary.csv +++ /dev/null @@ -1,6 +0,0 @@ -filename,from,to,original_D,original_rTRE,rigid_D,rigid_rTRE,non_rigid_D,non_rigid_rTRE,processed_img_shape,shape,aligned_shape,mean_original_D,mean_rigid_D,physical_units,resolution,name,rigid_time_minutes,mean_non_rigid_D,non_rigid_time_minutes -./valis/examples/example_datasets/ihc/ihc_5.ome.tiff,ihc_5,ihc_1,7494.33506508,0.665482373262,88.5459502564,0.00786270811322,87.4204682586,0.00776276750149,"(1024, 902)","(2682, 2362)","(3386, 3129)",1985.87502163,78.2193596797,µm,7.96451447743,ihc,0.51149478356,35.4371107763,1.00536931356 -./valis/examples/example_datasets/ihc/ihc_1.ome.tiff,ihc_1,ihc_3,763.104573067,0.0677644041816,94.7203682117,0.00841125783055,46.8838661397,0.00416333143166,"(1024, 971)","(2727, 2587)","(3386, 3129)",1985.87502163,78.2193596797,µm,7.96451447743,ihc,0.51149478356,35.4371107763,1.00536931356 -./valis/examples/example_datasets/ihc/ihc_3.ome.tiff,ihc_3,,,,,,,,"(1024, 975)","(2717, 2587)","(3386, 3129)",1985.87502163,78.2193596797,µm,7.96451447743,ihc,0.51149478356,35.4371107763,1.00536931356 -./valis/examples/example_datasets/ihc/ihc_2.ome.tiff,ihc_2,ihc_3,1995.15585663,0.177156321239,82.0285651558,0.00728358077453,35.3960347983,0.0031429280527,"(1024, 981)","(2817, 2699)","(3386, 3129)",1985.87502163,78.2193596797,µm,7.96451447743,ihc,0.51149478356,35.4371107763,1.00536931356 -./valis/examples/example_datasets/ihc/ihc_4.ome.tiff,ihc_4,ihc_2,2488.42229307,0.220958224998,65.2264066777,0.00579174647433,27.7707193765,0.0024658872722,"(1024, 922)","(2997, 2699)","(3386, 3129)",1985.87502163,78.2193596797,µm,7.96451447743,ihc,0.51149478356,35.4371107763,1.00536931356 diff --git a/examples/warp_associated_img/ihc/deformation_fields/0_ihc_5.png b/examples/warp_associated_img/ihc/deformation_fields/0_ihc_5.png deleted file mode 100644 index dcf04446..00000000 Binary files a/examples/warp_associated_img/ihc/deformation_fields/0_ihc_5.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/deformation_fields/1_ihc_1.png b/examples/warp_associated_img/ihc/deformation_fields/1_ihc_1.png deleted file mode 100644 index d0b0c703..00000000 Binary files a/examples/warp_associated_img/ihc/deformation_fields/1_ihc_1.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/deformation_fields/2_ihc_3.png b/examples/warp_associated_img/ihc/deformation_fields/2_ihc_3.png deleted file mode 100644 index 4ee22755..00000000 Binary files a/examples/warp_associated_img/ihc/deformation_fields/2_ihc_3.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/deformation_fields/3_ihc_2.png b/examples/warp_associated_img/ihc/deformation_fields/3_ihc_2.png deleted file mode 100644 index 4dfffa45..00000000 Binary files a/examples/warp_associated_img/ihc/deformation_fields/3_ihc_2.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/deformation_fields/4_ihc_4.png b/examples/warp_associated_img/ihc/deformation_fields/4_ihc_4.png deleted file mode 100644 index c2302cf7..00000000 Binary files a/examples/warp_associated_img/ihc/deformation_fields/4_ihc_4.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_1.png b/examples/warp_associated_img/ihc/masks/ihc_1.png deleted file mode 100644 index 855cfa43..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_1.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_2.png b/examples/warp_associated_img/ihc/masks/ihc_2.png deleted file mode 100644 index a3ac7e81..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_2.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_3.png b/examples/warp_associated_img/ihc/masks/ihc_3.png deleted file mode 100644 index 2363337c..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_3.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_4.png b/examples/warp_associated_img/ihc/masks/ihc_4.png deleted file mode 100644 index 72210121..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_4.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_5.png b/examples/warp_associated_img/ihc/masks/ihc_5.png deleted file mode 100644 index bc2a93d9..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_5.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/masks/ihc_non_rigid_mask.png b/examples/warp_associated_img/ihc/masks/ihc_non_rigid_mask.png deleted file mode 100644 index 94dc7976..00000000 Binary files a/examples/warp_associated_img/ihc/masks/ihc_non_rigid_mask.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/non_rigid_registration/0_ihc_5.png b/examples/warp_associated_img/ihc/non_rigid_registration/0_ihc_5.png deleted file mode 100644 index b6d7acd1..00000000 Binary files a/examples/warp_associated_img/ihc/non_rigid_registration/0_ihc_5.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/non_rigid_registration/1_ihc_1.png b/examples/warp_associated_img/ihc/non_rigid_registration/1_ihc_1.png deleted file mode 100644 index 8c1d047e..00000000 Binary files a/examples/warp_associated_img/ihc/non_rigid_registration/1_ihc_1.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/non_rigid_registration/2_ihc_3.png b/examples/warp_associated_img/ihc/non_rigid_registration/2_ihc_3.png deleted file mode 100644 index ce405a3f..00000000 Binary files a/examples/warp_associated_img/ihc/non_rigid_registration/2_ihc_3.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/non_rigid_registration/3_ihc_2.png b/examples/warp_associated_img/ihc/non_rigid_registration/3_ihc_2.png deleted file mode 100644 index 4b1d5492..00000000 Binary files a/examples/warp_associated_img/ihc/non_rigid_registration/3_ihc_2.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/non_rigid_registration/4_ihc_4.png b/examples/warp_associated_img/ihc/non_rigid_registration/4_ihc_4.png deleted file mode 100644 index 0a8d16e6..00000000 Binary files a/examples/warp_associated_img/ihc/non_rigid_registration/4_ihc_4.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/overlaps/ihc_non_rigid_overlap.png b/examples/warp_associated_img/ihc/overlaps/ihc_non_rigid_overlap.png deleted file mode 100644 index 98622ee7..00000000 Binary files a/examples/warp_associated_img/ihc/overlaps/ihc_non_rigid_overlap.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/overlaps/ihc_original_overlap.png b/examples/warp_associated_img/ihc/overlaps/ihc_original_overlap.png deleted file mode 100644 index ecf65170..00000000 Binary files a/examples/warp_associated_img/ihc/overlaps/ihc_original_overlap.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/overlaps/ihc_rigid_overlap.png b/examples/warp_associated_img/ihc/overlaps/ihc_rigid_overlap.png deleted file mode 100644 index 7cac640f..00000000 Binary files a/examples/warp_associated_img/ihc/overlaps/ihc_rigid_overlap.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/processed/ihc_1.png b/examples/warp_associated_img/ihc/processed/ihc_1.png deleted file mode 100644 index fcedf596..00000000 Binary files a/examples/warp_associated_img/ihc/processed/ihc_1.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/processed/ihc_2.png b/examples/warp_associated_img/ihc/processed/ihc_2.png deleted file mode 100644 index 3bba2797..00000000 Binary files a/examples/warp_associated_img/ihc/processed/ihc_2.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/processed/ihc_3.png b/examples/warp_associated_img/ihc/processed/ihc_3.png deleted file mode 100644 index 8c07ae8a..00000000 Binary files a/examples/warp_associated_img/ihc/processed/ihc_3.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/processed/ihc_4.png b/examples/warp_associated_img/ihc/processed/ihc_4.png deleted file mode 100644 index c2b0b294..00000000 Binary files a/examples/warp_associated_img/ihc/processed/ihc_4.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/processed/ihc_5.png b/examples/warp_associated_img/ihc/processed/ihc_5.png deleted file mode 100644 index 179752ee..00000000 Binary files a/examples/warp_associated_img/ihc/processed/ihc_5.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/rigid_registration/0_ihc_5.png b/examples/warp_associated_img/ihc/rigid_registration/0_ihc_5.png deleted file mode 100644 index ca06be13..00000000 Binary files a/examples/warp_associated_img/ihc/rigid_registration/0_ihc_5.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/rigid_registration/1_ihc_1.png b/examples/warp_associated_img/ihc/rigid_registration/1_ihc_1.png deleted file mode 100644 index 3057848a..00000000 Binary files a/examples/warp_associated_img/ihc/rigid_registration/1_ihc_1.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/rigid_registration/2_ihc_3.png b/examples/warp_associated_img/ihc/rigid_registration/2_ihc_3.png deleted file mode 100644 index ce405a3f..00000000 Binary files a/examples/warp_associated_img/ihc/rigid_registration/2_ihc_3.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/rigid_registration/3_ihc_2.png b/examples/warp_associated_img/ihc/rigid_registration/3_ihc_2.png deleted file mode 100644 index c5f10336..00000000 Binary files a/examples/warp_associated_img/ihc/rigid_registration/3_ihc_2.png and /dev/null differ diff --git a/examples/warp_associated_img/ihc/rigid_registration/4_ihc_4.png b/examples/warp_associated_img/ihc/rigid_registration/4_ihc_4.png deleted file mode 100644 index 22358d63..00000000 Binary files a/examples/warp_associated_img/ihc/rigid_registration/4_ihc_4.png and /dev/null differ diff --git a/examples/warp_other_image.py b/examples/warp_other_image.py deleted file mode 100644 index d4f4ccea..00000000 --- a/examples/warp_other_image.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Example showing how to using the registration parameters to warp another image. - -Example image will be larger version of the tissue mask -""" - -import sys -from valis import slide_io, warp_tools, valtils, registration, warp_tools -import matplotlib.pyplot as plt - -# os.getcwd() -import time -import os -from valis import registration, warp_tools -from skimage import filters, measure, morphology, segmentation, color -import numpy as np - - -slide_src_dir = "./example_datasets/ihc" -slide_src_dir = "./valis/examples/example_datasets/ihc" - -results_dst_dir = "./valis/examples/warp_associated_img/" - -# Create a Valis object and use it to register the slides in slide_src_dir -start = time.time() -registrar = registration.Valis(slide_src_dir, results_dst_dir) -rigid_registrar, non_rigid_registrar, error_df = registrar.register() -stop = time.time() -elapsed = stop - start - -print(f"regisration time is {elapsed/60} minutes") - - -# Create examples of associated images to warp. - -slide_obj = registrar.get_slide("ihc_2") -processed_img = slide_obj.pad_cropped_processed_img() -s = 2 -new_shape = np.array(processed_img.shape[0:2])*s -img = warp_tools.resize_img(slide_obj.image, new_shape) - -# In this example, we'll create a mask, based on a larger version of the image used during registration - -gray_img = 1 - color.rgb2gray(img) -mask = gray_img > filters.threshold_li(gray_img) - -# Use the associated Slide to warp the mask. Need to set `interp_method="nearest"` since this is a binary image -warped_mask = slide_obj.warp_img(mask, interp_method="nearest") - -# Use the associated Slide to warp larger version of the image -warped_scaled_img = slide_obj.warp_img(img) - -# Visualise mask overlaid on the warped image -mask_boundaries = segmentation.find_boundaries(warped_mask) -warped_scaled_img[mask_boundaries] = [0, 255, 0] - -plt.imshow(warped_scaled_img) -plt.show() - - -# Shutdown the JVM -registration.kill_jvm() diff --git a/examples/warp_points.py b/examples/warp_points.py deleted file mode 100644 index 7ab84506..00000000 --- a/examples/warp_points.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -This example shows how warp a set of ROI coordinates and use them to slice -the ROI from the registered images. - -The steps are as follows: - -1. Load a pickled Valis object that has already registered some -slides. - -2. Access the Slide object associated with the slide -from which the ROI coordinates originated. - -3. Warping of points assumes the coordinates are in pixel units. -However, here the original coordinaates are in microns, and so need to be -converted pixel units. - -4. The Slide object can now be used to warp the coordinates. -This yields the ROI coordinates in all of the registered slides. - -5. Warp and slice the ROI from each slide using the Slide's pyvips.Image -extract_area method. - -Note that because that the Slides are pyvips.Image objects, and so do -not need to be loaded into memory to do the warping. Therefore, warping -and ROI extraction is fast. - -It is also worth noting that it's important to know the pyramid level, -or image shape, from which the coordinates originated. If working with -slides, the pyramid level is probably 0, and here is corresponds to the -`COORD_LEVEL` variable. It would also be possible to input the source -image's dimensions. - -""" - - -import os -import pickle -import numpy as np -import matplotlib.pyplot as plt -import pathlib - -from valis import registration, warp_tools - -# Load a registrar that has been saved -registrar_f = "./expected_results/registration/ihc/data/ihc_registrar.pickle" -registrar = registration.load_registrar(registrar_f) -COORD_LEVEL = 0 # pyramid level from which the ROI coordinates originated. Usually 0. - -# ROI coordinates, in microns. These came from the unregistered slide "ihc_2.ome.tiff" -bbox_xywh_um = [14314, 13601, 3000, 3000] -bbox_xy_um = warp_tools.bbox2xy(bbox_xywh_um) - -# Get slide from which the ROI coordinates originated -pt_source_img_f = "ihc_2.ome.tiff" -pt_source_slide = registrar.get_slide(pt_source_img_f) - -# Convert coordinates to pixel units -um_per_px = pt_source_slide.reader.scale_physical_size(COORD_LEVEL)[0:2] -bbox_xy_px = bbox_xy_um/np.array(um_per_px) - -# Warp coordinates to position in registered slides -bbox_xy_in_registered_img = pt_source_slide.warp_xy(bbox_xy_px, - slide_level=COORD_LEVEL, - pt_level=COORD_LEVEL) - -bbox_xywh_in_registered_img = warp_tools.xy2bbox(bbox_xy_in_registered_img) -bbox_xywh_in_registered_img = np.round(bbox_xywh_in_registered_img).astype(int) - -# Create directory where images will be saved -dst_dir = "./expected_results/roi" -pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - -# Warp each slide and slice the ROI from it using each pyips.Image's "extract_area" method. -fig, axes = plt.subplots(2, 3, figsize=(12, 8), sharex=True, sharey=True) -ax = axes.ravel() -for i, slide in enumerate(registrar.slide_dict.values()): - warped_slide = slide.warp_slide(level=COORD_LEVEL) - roi_vips = warped_slide.extract_area(*bbox_xywh_in_registered_img) - roi_img = warp_tools.vips2numpy(roi_vips) - ax[i].imshow(roi_img) - ax[i].set_title(slide.name) - ax[i].set_axis_off() - -fig.delaxes(ax[5]) # Only 5 images, so remove 6th subplot -out_f = os.path.join(dst_dir, f"{registrar.name}_roi.png") -plt.tight_layout() -plt.savefig(out_f) -plt.close() - -# Opening the slide initialized the JVM, so it needs to be killed -registration.kill_jvm() diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53900402..00000000 --- a/noxfile.py +++ /dev/null @@ -1,11 +0,0 @@ -import nox - - -nox.options.default_venv_backend = "uv" - -@nox.session(python=["3.9", "3.10", "3.11", "3.12", "3.13"]) -# @nox.session(python=["3.13"]) -def tests(session): - session.install(".",) - session.install("pytest", "pytest-cov") - session.run("pytest") diff --git a/pyproject.toml b/pyproject.toml index a35fdc31..adb415a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,47 @@ -[[tool.pdm.source]] -name = "PyPI" -url = "" -verify_ssl = false [build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend" +requires = ["setuptools>=69.0.3"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +valis = ["superglue_models/weights/*"] [project] +name = "valis" +version = "1.2.0" +description = "" +readme = "README.rst" +requires-python = ">=3.11" +license = {file = "LICENSE.txt"} authors = [ {name = "Chandler Gatenbee", email = "chandlergatenbee@gmail.com"}, ] -license = {text = "MIT"} -requires-python = ">=3.9" +maintainers = [ + {name = "Chandler Gatenbee", email = "chandlergatenbee@gmail.com"}, + {name = "Jeff Quinn", email = "quinnj2@mskcc.org" }, +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", +] + dependencies = [ "beautifulsoup4<5.0.0,>=4.11.1", - "scyjava<2.0.0,>=1.8.1", "colorama<1.0.0,>=0.4.6", "colour-science>0.4.2", "fastcluster<2.0.0,>=1.2.6", - "jpype1<2.0.0,>=1.4.1", "matplotlib<4.0.0,>=3.6.3", - "numpy>1.23,<2.0.0", + "numpy", "ome-types<1.0.0,>=0.3.2", - "opencv-contrib-python-headless==4.9.0.80", + "opencv-contrib-python-headless>=4.12", "pandas>=2.0.0", "pillow>=10.3.0", "scikit-learn<2.0.0,>=1.2.0", @@ -36,18 +54,23 @@ dependencies = [ "aicspylibczi<4.0.0,>=3.1.2", "setuptools>=69.0.3", "scikit-image<1.0.0,>=0.24.0", - "kornia<1.0.0,>=0.7.3", - "einops<1.0.0,>=0.8.0", - "torchvision<1.0.0,>=0.20.1", "openpyxl<4.0.0,>=3.1.5", "simpleitk<3.0.0,>=2.4.1", "lxml>=5.4.0", "pyvips>2.2.1", +] + +[project.optional-dependencies] +# Deep-learning feature detectors/matchers (SuperPoint, SuperGlue, DISK, DeDoDe, +# LightGlue). Install with: pip install 'valis-wsi[dl]' +dl = [ "torch>=2.7.1", + "torchvision<1.0.0,>=0.20.1", + "kornia<1.0.0,>=0.7.3", + "einops<1.0.0,>=0.8.0", ] -name = "valis-wsi" -version = "1.2.0" -description = "" +# Convenience alias that includes dl deps (backward-compatible full install) +full = ["valis[dl]"] [dependency-groups] dev = [ @@ -55,4 +78,17 @@ dev = [ "sphinx>=7.4.7", "sphinx-rtd-theme>=3.0.2", "twine>=6.1.0", + "black>=24.0.0", + "ruff>=0.6.0", ] + +[tool.ruff] +extend-exclude = ["docs", "src/valis/superglue_models"] + +[tool.ruff.lint] +# Catch real bugs without enforcing stylistic cleanup. +# F821: undefined name (missing imports — the bug we just fixed) +# F811: redefinition of unused name (often a missed merge/copy-paste) +# F823: local variable referenced before assignment +# E9: syntax errors +select = ["F821", "F811", "F823", "E9"] diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..7f1a1763 --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + +if __name__ == "__main__": + setup() diff --git a/valis/superglue_models/__init__.py b/src/__init__.py similarity index 100% rename from valis/superglue_models/__init__.py rename to src/__init__.py diff --git a/src/valis/__init__.py b/src/valis/__init__.py new file mode 100644 index 00000000..5999a31b --- /dev/null +++ b/src/valis/__init__.py @@ -0,0 +1,50 @@ +__version__ = "1.2.0" + +import sys as _sys +import warnings as _warnings + +# Guard against the known import-order segfault: valis must be imported before +# any pytorch-related package (torch, torchvision, kornia, etc.). If those +# packages are already in sys.modules they must have been imported beforehand, +# which is the unsafe ordering. Raise a clear error instead of segfaulting. +# (When torch is not installed this guard is never triggered.) +_TORCH_RELATED = {"torch", "torchvision", "kornia", "einops", "timm"} +_already_imported = _TORCH_RELATED.intersection(_sys.modules) +if _already_imported: + raise ImportError( + "valis must be imported before pytorch-related packages " + f"({', '.join(sorted(_already_imported))}). " + "Move 'import valis' to the top of your script, before any torch/kornia imports." + ) + +from . import affine_optimizer +from . import feature_detectors +from . import feature_matcher +from . import non_rigid_registrars +from . import preprocessing +from . import registration +from . import serial_non_rigid +from . import serial_rigid +from . import slide_io +from . import slide_tools +from . import valtils +from . import viz +from . import warp_tools +from . import micro_rigid_registrar + +__all__ = [ + "affine_optimizer", + "feature_detectors", + "feature_matcher", + "non_rigid_registrars", + "preprocessing", + "registration", + "serial_non_rigid", + "serial_rigid", + "slide_io", + "slide_tools", + "valtils", + "viz", + "warp_tools", + "micro_rigid_registrar", +] diff --git a/valis/affine_optimizer.py b/src/valis/affine_optimizer.py similarity index 76% rename from valis/affine_optimizer.py rename to src/valis/affine_optimizer.py index 6194bb6a..f0628138 100644 --- a/valis/affine_optimizer.py +++ b/src/valis/affine_optimizer.py @@ -10,6 +10,7 @@ class that performs the optimzation. This class can be subclassed to implement to provide examples on how to subclass AffineOptimizer. """ +import logging import torch import kornia @@ -21,8 +22,9 @@ class that performs the optimzation. This class can be subclassed to implement import SimpleITK as sitk from scipy import interpolate import pathlib -from . warp_tools import get_affine_transformation_params, \ - get_corners_of_image, warp_xy +from .warp_tools import get_affine_transformation_params, get_corners_of_image, warp_xy + +logger = logging.getLogger(__name__) # Cost functions # EPS = np.finfo("float").eps @@ -32,18 +34,18 @@ def mse(arr1, arr2, mask=None): """Compute the mean squared error between two arrays.""" if mask is None: - return np.mean((arr1 - arr2)**2) + return np.mean((arr1 - arr2) ** 2) else: return np.mean((arr1[mask != 0] - arr2[mask != 0]) ** 2) def displacement(moving_image, target_image, mask=None): - """Minimize average displacement between moving_image and target_image - """ + """Minimize average displacement between moving_image and target_image""" opt_flow = cv2.optflow.createOptFlow_DeepFlow() - flow = opt_flow.calc(util.img_as_ubyte(target_image), - util.img_as_ubyte(moving_image), None) + flow = opt_flow.calc( + util.img_as_ubyte(target_image), util.img_as_ubyte(moving_image), None + ) if mask is not None: dx = flow[..., 0][mask != 0] dy = flow[..., 1][mask != 0] @@ -62,12 +64,10 @@ def cost_mse(param, reference_image, target_image, mask=None): def downsample2x(image): - """Down sample image. - """ + """Down sample image.""" offsets = [((s + 1) % 2) / 2 for s in image.shape] - slices = [slice(offset, end, 2) - for offset, end in zip(offsets, image.shape)] + slices = [slice(offset, end, 2) for offset, end in zip(offsets, image.shape)] coords = np.mgrid[slices] return ndimage.map_coordinates(image, coords, order=1) @@ -104,9 +104,7 @@ def make_transform(param): else: r, tc, tr, s = param - return transform.SimilarityTransform(rotation=r, - translation=(tc, tr), - scale=s) + return transform.SimilarityTransform(rotation=r, translation=(tc, tr), scale=s) def bin_image(img, p): @@ -141,9 +139,13 @@ def solve_abc(verts): intersection of isointensity lines """ - a = np.array([[verts[0, 0], verts[0, 1], 1], - [verts[1, 0], verts[1, 1], 1], - [verts[2, 0], verts[2, 1], 1]]) + a = np.array( + [ + [verts[0, 0], verts[0, 1], 1], + [verts[1, 0], verts[1, 1], 1], + [verts[2, 0], verts[2, 1], 1], + ] + ) b = verts[:, 2] try: @@ -176,7 +178,7 @@ def isInside(x1, y1, x2, y2, x3, y3, x, y): # Check if sum of A1, A2 and A3 # is same as A - if (A == (A1 + A2 + A3)): + if A == (A1 + A2 + A3): return 1 else: return 0 @@ -185,26 +187,24 @@ def isInside(x1, y1, x2, y2, x3, y3, x, y): def get_intersection(alpha1, alpha2, abc1, abc2): """ - Parameters - ---------- - alpha1 : float - Intensity of point in image 1 + Parameters + ---------- + alpha1 : float + Intensity of point in image 1 - alpha2 : float - Intensity of point in image 2 + alpha2 : float + Intensity of point in image 2 - abc1: [A,B,C] - Coefficients to interpolate value for triangle in image1 + abc1: [A,B,C] + Coefficients to interpolate value for triangle in image1 - abc2: [A,B,C] - Coefficients to interpolate value for corresponding triangle in image2 + abc2: [A,B,C] + Coefficients to interpolate value for corresponding triangle in image2 """ # Find interestion of isointensity lines ### intensities = np.array([alpha1 - abc1[2], alpha2 - abc2[2]]) - coef = np.array([[abc1[0], abc1[1]], - [abc2[0], abc2[1]] - ]) + coef = np.array([[abc1[0], abc1[1]], [abc2[0], abc2[1]]]) try: xy = np.linalg.inv(coef) @ intensities except np.linalg.LinAlgError: @@ -219,16 +219,22 @@ def get_verts(img, x, y, pos=0): """ if pos == 0: # Lower left - verts = np.array([[x, y, img[y, x]], # BL - [x + 1, y, img[y, x + 1]], # BR - [x, y + 1, img[y + 1, x]] # TL - ]) + verts = np.array( + [ + [x, y, img[y, x]], # BL + [x + 1, y, img[y, x + 1]], # BR + [x, y + 1, img[y + 1, x]], # TL + ] + ) if pos == 1: # Upper right - verts = np.array([[x, y+1, img[y+1, x]], # BL - [x + 1, y, img[y, x + 1]], # BR - [x+1, y + 1, img[y + 1, x + 1]] # TL - ]) + verts = np.array( + [ + [x, y + 1, img[y + 1, x]], # BL + [x + 1, y, img[y, x + 1]], # BR + [x + 1, y + 1, img[y + 1, x + 1]], # TL + ] + ) return verts @@ -252,8 +258,8 @@ def hist2d(x, y, n_bins): y_margins = np.zeros(n_bins) results = np.zeros((n_bins, n_bins)) for i in range(len(x)): - x_bin = int(_bins*((x[i]-x_min)/(x_range))) - y_bin = int(_bins*((y[i] - y_min) / (y_range))) + x_bin = int(_bins * ((x[i] - x_min) / (x_range))) + y_bin = int(_bins * ((y[i] - y_min) / (y_range))) x_margins[x_bin] += 1 y_margins[y_bin] += 1 @@ -262,8 +268,9 @@ def hist2d(x, y, n_bins): return results, x_margins, y_margins -def update_joint_H(binned_moving, binned_fixed, H, M, sample_pts, pos=0, - precalcd_abc=None): +def update_joint_H( + binned_moving, binned_fixed, H, M, sample_pts, pos=0, precalcd_abc=None +): q = H.shape[0] for i, sxy in enumerate(sample_pts): @@ -284,15 +291,25 @@ def update_joint_H(binned_moving, binned_fixed, H, M, sample_pts, pos=0, for alpha1 in range(0, q): for alpha2 in range(0, q): xy = get_intersection(alpha1, alpha2, abc1, abc2) - if xy[0] <= x_lims[0] or xy[0] >= x_lims[1] or \ - xy[1] <= y_lims[0] or xy[1] >= y_lims[1]: + if ( + xy[0] <= x_lims[0] + or xy[0] >= x_lims[1] + or xy[1] <= y_lims[0] + or xy[1] >= y_lims[1] + ): continue # Determine if intersection inside triangle ### - vote = isInside(img1_v[0, 0], img1_v[0, 1], - img1_v[1, 0], img1_v[1, 1], - img1_v[2, 0], img1_v[2, 1], - xy[0], xy[1]) + vote = isInside( + img1_v[0, 0], + img1_v[0, 1], + img1_v[1, 0], + img1_v[1, 1], + img1_v[2, 0], + img1_v[2, 1], + xy[0], + xy[1], + ) H[alpha1, alpha2] += vote @@ -304,13 +321,13 @@ def get_neighborhood(im, i, j, r): Get values in a neighborhood """ - return im[i - r:i + r + 1, j - r:j + r + 1].flatten() + return im[i - r : i + r + 1, j - r : j + r + 1].flatten() def build_P(A, B, r, mask): hood_size = (2 * r + 1) ** 2 d = 2 * hood_size - N = (A.shape[0] - 2*r)*(A.shape[1] - 2*r) + N = (A.shape[0] - 2 * r) * (A.shape[1] - 2 * r) P = np.zeros((d, N)) idx = 0 @@ -349,26 +366,24 @@ def entropy(x): Shannon's entropy """ # x += EPS ## Avoid -Inf if there is log(0) - px = x/np.sum(x) + px = x / np.sum(x) px = px[px > 0] h = -np.sum(px * np.log(px)) return h def entropy_from_c(cov_mat, d): - e = np.log(((2*np.pi*np.e) ** (d/2)) * - (np.linalg.det(cov_mat) ** 0.5) + EPS) + e = np.log(((2 * np.pi * np.e) ** (d / 2)) * (np.linalg.det(cov_mat) ** 0.5) + EPS) return e - def region_mi(A, B, mask, r=4): P = build_P(A, B, r, mask) # d x N matrix: N points with d dimensions # Center points so each dimensions is around 0 C = np.cov(P, rowvar=True, bias=True) hood_size = (2 * r + 1) ** 2 - d = hood_size*2 + d = hood_size * 2 HA = entropy_from_c(C[0:hood_size, 0:hood_size], d) HB = entropy_from_c(C[hood_size:, hood_size:], d) HC = entropy_from_c(C, d) @@ -430,10 +445,14 @@ def normalized_mutual_information(A, B, mask, n_bins=256): def sample_img(img, spacing=10): - sr, sc = np.meshgrid(np.arange(0, img.shape[0], spacing), np.arange(0, img.shape[1], spacing)) - sample_r = sr.reshape(-1) + np.random.uniform(0, spacing/2, sr.size) - sample_c = sc.reshape(-1) + np.random.uniform(0, spacing/2, sc.size) - interp = interpolate.RectBivariateSpline(np.arange(0, img.shape[0]), np.arange(0, img.shape[1]), img) + sr, sc = np.meshgrid( + np.arange(0, img.shape[0], spacing), np.arange(0, img.shape[1], spacing) + ) + sample_r = sr.reshape(-1) + np.random.uniform(0, spacing / 2, sr.size) + sample_c = sc.reshape(-1) + np.random.uniform(0, spacing / 2, sc.size) + interp = interpolate.RectBivariateSpline( + np.arange(0, img.shape[0]), np.arange(0, img.shape[1]), img + ) z = np.array([interp(sample_r[i], sample_c[i])[0][0] for i in range(len(sample_c))]) return z[(0 <= z) & (z <= img.max())] @@ -513,9 +532,16 @@ class AffineOptimizer(object): accepts_xy needs to be set to True. The default is False. """ + accepts_xy = False - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transformation="EuclideanTransform"): + def __init__( + self, + nlevels=1, + nbins=256, + optimization="Powell", + transformation="EuclideanTransform", + ): """AffineOptimizer registers moving and fixed images by minimizing a cost function Parameters @@ -578,8 +604,9 @@ def setup(self, moving, fixed, mask, initial_M=None): self.p[3] = 1 if initial_M is not None: - (tx, ty), rotation, (scale_x, scale_y), shear = \ + (tx, ty), rotation, (scale_x, scale_y), shear = ( get_affine_transformation_params(initial_M) + ) self.p[0] = rotation self.p[1] = tx @@ -588,17 +615,24 @@ def setup(self, moving, fixed, mask, initial_M=None): self.p[3] = scale_x def cost_fxn(self, fixed_image, transformed, mask): - return -normalized_mutual_information(fixed_image, transformed, mask, n_bins=self.nbins) + return -normalized_mutual_information( + fixed_image, transformed, mask, n_bins=self.nbins + ) def calc_cost(self, p): - """Static cost function passed into scipy.optimize - """ + """Static cost function passed into scipy.optimize""" transformation = make_transform(p) - transformed = transform.warp(self.pyramid_moving[self.current_level], transformation.params, order=3) + transformed = transform.warp( + self.pyramid_moving[self.current_level], transformation.params, order=3 + ) if np.all(transformed == 0): return np.inf - return self.cost_fxn(self.pyramid_fixed[self.current_level], transformed, self.pyramid_mask[self.current_level]) + return self.cost_fxn( + self.pyramid_fixed[self.current_level], + transformed, + self.pyramid_mask[self.current_level], + ) def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None): """Align images by minimizing self.cost_fxn. Aligns each level of the Gaussian pyramid, and uses previous transform @@ -639,7 +673,9 @@ def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No self.setup(moving, fixed, mask, initial_M) method = self.optimization - levels = range(self.nlevels-1, -1, -1) # Iterate from top to bottom of pyramid + levels = range( + self.nlevels - 1, -1, -1 + ) # Iterate from top to bottom of pyramid cost_list = [None] * self.nlevels other_params = None for n in levels: @@ -649,9 +685,11 @@ def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No if other_params is None: max_tc = self.pyramid_moving[self.current_level].shape[1] max_tr = self.pyramid_moving[self.current_level].shape[0] - param_bounds = [[0, np.deg2rad(360)], - [-max_tc, max_tc], - [-max_tr, max_tr]] + param_bounds = [ + [0, np.deg2rad(360)], + [-max_tc, max_tc], + [-max_tr, max_tr], + ] if self.transformation == "SimilarityTransform": param_bounds.append([self.p[3] * 0.5, self.p[3] * 2]) @@ -659,36 +697,42 @@ def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No else: param_mins = np.min(other_params, axis=0) param_maxes = np.max(other_params, axis=0) - param_bounds = [[param_mins[0], param_maxes[0]], - [2*param_mins[1], 2*param_maxes[1]], - [2*param_mins[2], 2*param_maxes[2]]] + param_bounds = [ + [param_mins[0], param_maxes[0]], + [2 * param_mins[1], 2 * param_maxes[1]], + [2 * param_mins[2], 2 * param_maxes[2]], + ] if self.transformation == "SimilarityTransform": param_bounds.append([param_mins[3], param_maxes[3]]) # Optimize # - if method.upper() == 'BH': + if method.upper() == "BH": res = optimize.basinhopping(self.calc_cost, self.p) new_p = res.x cst = res.fun - if n <= self.nlevels//2: # avoid basin-hopping in lower levels - method = 'Powell' + if n <= self.nlevels // 2: # avoid basin-hopping in lower levels + method = "Powell" - elif method == 'Nelder-Mead': - res = optimize.minimize(self.calc_cost, self.p, method=method, bounds=param_bounds) + elif method == "Nelder-Mead": + res = optimize.minimize( + self.calc_cost, self.p, method=method, bounds=param_bounds + ) new_p = res.x cst = np.float(res.fun) else: # Default is Powell, which doesn't accept bounds - res = optimize.minimize(self.calc_cost, self.p, method=method, options={"return_all": True}) + res = optimize.minimize( + self.calc_cost, self.p, method=method, options={"return_all": True} + ) new_p = res.x cst = np.float(res.fun) if hasattr(res, "allvecs"): other_params = np.vstack(res.allvecs) if n <= self.nlevels // 2: # avoid basin-hopping in lower levels - method = 'Powell' + method = "Powell" # Update # self.p = new_p @@ -698,7 +742,7 @@ def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No optimal_M = tf.params w = transform.warp(self.pyramid_moving[n], optimal_M, order=3) if np.all(w == 0): - print(Warning("Image warped out of bounds. Registration failed")) + logger.warning("Image warped out of bounds. Registration failed") return False, np.ones_like(optimal_M), cost_list tf = make_transform(self.p) @@ -708,7 +752,7 @@ def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No class AffineOptimizerMattesMI(AffineOptimizer): - """ Optimize rigid registration using Simple ITK + """Optimize rigid registration using Simple ITK AffineOptimizerMattesMI is an AffineOptimizer subclass that uses simple ITK's AdvancedMattesMutualInformation. If moving_xy and fixed_xy are also provided, then Mattes mutual information will be maximized, while the distance @@ -753,14 +797,23 @@ class AffineOptimizerMattesMI(AffineOptimizer): accepts_xy = True - def __init__(self, nlevels=4.0, nbins=32, - optimization="AdaptiveStochasticGradientDescent", transform="EuclideanTransform"): + def __init__( + self, + nlevels=4.0, + nbins=32, + optimization="AdaptiveStochasticGradientDescent", + transform="EuclideanTransform", + ): super().__init__(nlevels, nbins, optimization, transform) self.Reg = None self.accepts_xy = AffineOptimizerMattesMI.accepts_xy - self.fixed_kp_fname = os.path.join(pathlib.Path(__file__).parent, ".fixedPointSet.pts") - self.moving_kp_fname = os.path.join(pathlib.Path(__file__).parent, ".movingPointSet.pts") + self.fixed_kp_fname = os.path.join( + pathlib.Path(__file__).parent, ".fixedPointSet.pts" + ) + self.moving_kp_fname = os.path.join( + pathlib.Path(__file__).parent, ".movingPointSet.pts" + ) def cost_fxn(self, fixed_image, transformed, mask): return None @@ -778,7 +831,7 @@ def write_elastix_kp(self, kp, fname): Name of file in which to save the points """ - argfile = open(fname, 'w') + argfile = open(fname, "w") npts = kp.shape[0] argfile.writelines(f"index\n{npts}\n") for i in range(npts): @@ -818,9 +871,9 @@ def setup(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No self.fixed = fixed self.Reg = sitk.ElastixImageFilter() - rigid_map = sitk.GetDefaultParameterMap('affine') + rigid_map = sitk.GetDefaultParameterMap("affine") - rigid_map['NumberOfResolutions'] = [str(int(self.nlevels))] + rigid_map["NumberOfResolutions"] = [str(int(self.nlevels))] if self.transformation == "EuclideanTransform": rigid_map["Transform"] = ["EulerTransform"] else: @@ -852,8 +905,7 @@ def setup(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=No def calc_cost(self, p): return None - def align(self, moving, fixed, mask, initial_M=None, - moving_xy=None, fixed_xy=None): + def align(self, moving, fixed, mask, initial_M=None, moving_xy=None, fixed_xy=None): """ Optimize rigid registration @@ -903,8 +955,9 @@ def align(self, moving, fixed, mask, initial_M=None, else: scale, rotation, tx, ty = [eval(v) for v in tform_params] - M = transform.SimilarityTransform(scale=scale, rotation=rotation, - translation=(tx, ty)).params + M = transform.SimilarityTransform( + scale=scale, rotation=rotation, translation=(tx, ty) + ).params aligned = transform.warp(self.moving, M, order=3) @@ -916,9 +969,11 @@ def align(self, moving, fixed, mask, initial_M=None, if os.path.exists(self.moving_kp_fname): os.remove(self.moving_kp_fname) - tform_files = [f for f in os.listdir(".") if - f.startswith("TransformParameters.") and - f.endswith(".txt")] + tform_files = [ + f + for f in os.listdir(".") + if f.startswith("TransformParameters.") and f.endswith(".txt") + ] if len(tform_files) > 0: for f in tform_files: @@ -928,13 +983,15 @@ def align(self, moving, fixed, mask, initial_M=None, class AffineOptimizerRMI(AffineOptimizer): - def __init__(self, r=6, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): + def __init__( + self, r=6, nlevels=1, nbins=256, optimization="Powell", transform="euclidean" + ): super().__init__(nlevels, nbins, optimization, transform) self.r = r def cost_fxn(self, fixed_image, transformed, mask): - r_ratio = self.r/np.min(self.pyramid_fixed[0].shape) - level_rad = int(r_ratio*np.min(fixed_image.shape)) + r_ratio = self.r / np.min(self.pyramid_fixed[0].shape) + level_rad = int(r_ratio * np.min(fixed_image.shape)) if level_rad == 0: level_rad = 1 @@ -942,7 +999,9 @@ def cost_fxn(self, fixed_image, transformed, mask): class AffineOptimizerDisplacement(AffineOptimizer): - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): + def __init__( + self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean" + ): super().__init__(nlevels, nbins, optimization, transform) def cost_fxn(self, fixed_image, transformed, mask): @@ -951,9 +1010,11 @@ def cost_fxn(self, fixed_image, transformed, mask): class AffineOptimizerKNN(AffineOptimizer): - def __init__(self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean"): + def __init__( + self, nlevels=1, nbins=256, optimization="Powell", transform="euclidean" + ): super().__init__(nlevels, nbins, optimization, transform) - self.HA_list = [None]*nlevels + self.HA_list = [None] * nlevels def shannon_entropy(self, X, k=1): """ @@ -963,6 +1024,7 @@ def shannon_entropy(self, X, k=1): from sklearn import neighbors from scipy.special import gamma, psi + # Get distance to kth nearest neighbor knn = neighbors.NearestNeighbors(n_neighbors=k) knn.fit(X.reshape(-1, 1)) @@ -974,7 +1036,12 @@ def shannon_entropy(self, X, k=1): # volume of unit ball in d^n v_unit_ball = np.pi ** (0.5 * d) / gamma(0.5 * d + 1.0) n = len(X) - H = psi(n) - psi(k) + np.log(v_unit_ball) + (np.float(d) / np.float(n)) * (lr_k.sum()) + H = ( + psi(n) + - psi(k) + + np.log(v_unit_ball) + + (np.float(d) / np.float(n)) * (lr_k.sum()) + ) return H @@ -1010,43 +1077,57 @@ def cost_fxn(self, fixed_image, transformed, mask): class AffineOptimizerOffGrid(AffineOptimizer): - def __init__(self, nlevels, nbins=256, optimization="Powell", transform="euclidean", spacing=5): + def __init__( + self, + nlevels, + nbins=256, + optimization="Powell", + transform="euclidean", + spacing=5, + ): super().__init__(nlevels, nbins, optimization, transform) self.spacing = spacing def setup(self, moving, fixed, mask, initial_M=None): AffineOptimizer.setup(self, moving, fixed, mask, initial_M) - self.moving_interps = [self.get_interp(img) - for img in self.pyramid_moving] - self.fixed_interps = [self.get_interp(img) - for img in self.pyramid_fixed] - - self.z_range = (min(np.min(self.moving[self.nlevels - 1]), - np.min(self.fixed[self.nlevels - 1])), - max(np.max(self.moving[self.nlevels - 1]), - np.max(self.fixed[self.nlevels - 1]))) - - self.grid_spacings = [self.get_scpaing_for_levels(self.pyramid_fixed[i], self.spacing) for i in range(self.nlevels)] - self.grid_flat = [self.get_regular_grid_flat(i) - for i in range(self.nlevels)] + self.moving_interps = [self.get_interp(img) for img in self.pyramid_moving] + self.fixed_interps = [self.get_interp(img) for img in self.pyramid_fixed] + + self.z_range = ( + min( + np.min(self.moving[self.nlevels - 1]), + np.min(self.fixed[self.nlevels - 1]), + ), + max( + np.max(self.moving[self.nlevels - 1]), + np.max(self.fixed[self.nlevels - 1]), + ), + ) + + self.grid_spacings = [ + self.get_scpaing_for_levels(self.pyramid_fixed[i], self.spacing) + for i in range(self.nlevels) + ] + self.grid_flat = [self.get_regular_grid_flat(i) for i in range(self.nlevels)] def get_scpaing_for_levels(self, img_shape, max_level_spacing): max_shape = self.pyramid_fixed[self.nlevels - 1].shape - shape_ratio = np.mean([img_shape[0]/max_shape[0], - img_shape[0]/max_shape[0]]) + shape_ratio = np.mean( + [img_shape[0] / max_shape[0], img_shape[0] / max_shape[0]] + ) - level_spacing = int(max_level_spacing*shape_ratio) + level_spacing = int(max_level_spacing * shape_ratio) if level_spacing == 0: level_spacing = 1 return level_spacing def get_regular_grid_flat(self, level): - sr, sc = np.meshgrid(np.arange(0, self.pyramid_fixed[level].shape[0], - self.grid_spacings[level]), - np.arange(0, self.pyramid_fixed[level].shape[1], - self.grid_spacings[level])) + sr, sc = np.meshgrid( + np.arange(0, self.pyramid_fixed[level].shape[0], self.grid_spacings[level]), + np.arange(0, self.pyramid_fixed[level].shape[1], self.grid_spacings[level]), + ) sr = sr.reshape(-1) sc = sc.reshape(-1) @@ -1055,7 +1136,11 @@ def get_regular_grid_flat(self, level): return (filtered_sr, filtered_sc) def get_interp(self, img): - return interpolate.RectBivariateSpline(np.arange(0, img.shape[0], dtype=np.float), np.arange(0, img.shape[1], dtype=np.float), img) + return interpolate.RectBivariateSpline( + np.arange(0, img.shape[0], dtype=np.float), + np.arange(0, img.shape[1], dtype=np.float), + img, + ) def interp_point(self, zr, zc, interp, z_range): z = np.array([interp(zr[i], zc[i])[0][0] for i in range(zr.size)]) @@ -1068,25 +1153,45 @@ def calc_cost(self, p): transformation = make_transform(p) corners_rc = get_corners_of_image(self.pyramid_fixed[self.current_level].shape) warped_corners = warp_xy(corners_rc, transformation.params) - if np.any(warped_corners < 0) or \ - np.any(warped_corners[:, 0] > self.pyramid_fixed[self.current_level].shape[0]) or \ - np.any(warped_corners[:, 1] > self.pyramid_fixed[self.current_level].shape[1]): + if ( + np.any(warped_corners < 0) + or np.any( + warped_corners[:, 0] > self.pyramid_fixed[self.current_level].shape[0] + ) + or np.any( + warped_corners[:, 1] > self.pyramid_fixed[self.current_level].shape[1] + ) + ): return np.inf sr, sc = self.grid_flat[self.current_level] - sample_r = sr + np.random.uniform(0, self.grid_spacings[self.current_level] / 2, sr.size) - sample_c = sc + np.random.uniform(0, self.grid_spacings[self.current_level] / 2, sc.size) + sample_r = sr + np.random.uniform( + 0, self.grid_spacings[self.current_level] / 2, sr.size + ) + sample_c = sc + np.random.uniform( + 0, self.grid_spacings[self.current_level] / 2, sc.size + ) # Only sample points in mask warped_xy = warp_xy(np.dstack([sample_c, sample_r])[0], transformation.params) - fixed_intensities = self.interp_point(warped_xy[:, 1], warped_xy[:, 0], self.fixed_interps[self.current_level], self.z_range) - moving_intensities = self.interp_point(sample_r, sample_c, self.moving_interps[self.current_level], self.z_range) - - return self.cost_fxn(fixed_intensities, moving_intensities, self.pyramid_mask[self.current_level]) + fixed_intensities = self.interp_point( + warped_xy[:, 1], + warped_xy[:, 0], + self.fixed_interps[self.current_level], + self.z_range, + ) + moving_intensities = self.interp_point( + sample_r, sample_c, self.moving_interps[self.current_level], self.z_range + ) + + return self.cost_fxn( + fixed_intensities, moving_intensities, self.pyramid_mask[self.current_level] + ) def cost_fxn(self, fixed_intensities, transformed_intensities, mask): - """ - """ - results, _, _ = np.histogram2d(fixed_intensities, transformed_intensities, bins=self.nbins) + """ """ + results, _, _ = np.histogram2d( + fixed_intensities, transformed_intensities, bins=self.nbins + ) n = np.sum(results) results /= n diff --git a/valis/feature_detectors.py b/src/valis/feature_detectors.py similarity index 73% rename from valis/feature_detectors.py rename to src/valis/feature_detectors.py index c7492694..14485a2f 100644 --- a/valis/feature_detectors.py +++ b/src/valis/feature_detectors.py @@ -8,18 +8,27 @@ """ -import torch -import kornia +import logging import cv2 from skimage import feature, exposure from skimage import color as skcolor import numpy as np import traceback -from kornia.feature import DISK, DeDoDe from . import valtils from . import warp_tools from . import preprocessing -from .superglue_models import superpoint + +try: + import torch + import kornia + from kornia.feature import DISK, DeDoDe + from .superglue_models import superpoint + + _TORCH_AVAILABLE = True +except ImportError: + _TORCH_AVAILABLE = False + +logger = logging.getLogger(__name__) DEFAULT_FEATURE_DETECTOR = cv2.BRISK_create() @@ -127,10 +136,10 @@ def __init__(self, kp_detector=None, kp_descriptor=None, rgb=False, n_levels=1): _img = np.zeros((10, 10), dtype=np.uint8) kp_descriptor.detectAndCompute(_img, mask=None) - except: + except Exception: traceback_msg = traceback.format_exc() msg = f"{self.kp_descriptor_name} unable to both detect and compute features. Setting to {DEFAULT_FEATURE_DETECTOR.__class__.__name__}" - valtils.print_warning(msg, traceback_msg=traceback_msg) + logger.warning(f"{msg}\n{traceback_msg}") self.kp_detector = DEFAULT_FEATURE_DETECTOR @@ -187,9 +196,11 @@ def detect_and_compute(self, image, mask=None): s = 0.5 detect_img = image for i in range(self.n_levels): - print(f"detecting features in level {i} with image shape {detect_img.shape}") + logger.info( + f"detecting features in level {i} with image shape {detect_img.shape}" + ) kp_pos_xy, desc = self._detect_and_compute(detect_img, mask) - kp_pos_xy *= 1/(s**i) + kp_pos_xy *= 1 / (s**i) all_kp.append(kp_pos_xy) all_desc.append(desc) if self.n_levels > 1: @@ -200,72 +211,126 @@ def detect_and_compute(self, image, mask=None): return all_kp, all_desc + # Thin wrappers around OpenCV detectors and descriptors # class OrbFD(FeatureDD): """Uses ORB for feature detection and description""" + def __init__(self, kp_descriptor=cv2.ORB_create(MAX_FEATURES), *args, **kwargs): super().__init__(kp_descriptor=kp_descriptor, *args, **kwargs) class BriskFD(FeatureDD): """Uses BRISK for feature detection and description""" + def __init__(self, kp_descriptor=cv2.BRISK_create(), *args, **kwargs): super().__init__(kp_descriptor=kp_descriptor, *args, **kwargs) class KazeFD(FeatureDD): """Uses KAZE for feature detection and description""" + def __init__(self, kp_descriptor=cv2.KAZE_create(extended=False), *args, **kwargs): super().__init__(kp_descriptor=kp_descriptor, *args, **kwargs) class AkazeFD(FeatureDD): """Uses AKAZE for feature detection and description""" + def __init__(self, kp_descriptor=cv2.AKAZE_create(), *args, **kwargs): super().__init__(kp_descriptor=kp_descriptor, *args, **kwargs) class DaisyFD(FeatureDD): """Uses BRISK for feature detection and DAISY for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.DAISY_create(), *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=DEFAULT_FEATURE_DETECTOR, + kp_descriptor=cv2.xfeatures2d.DAISY_create(), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) class LatchFD(FeatureDD): """Uses BRISK for feature detection and LATCH for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.LATCH_create(rotationInvariance=True), *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=DEFAULT_FEATURE_DETECTOR, + kp_descriptor=cv2.xfeatures2d.LATCH_create(rotationInvariance=True), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) class BoostFD(FeatureDD): """Uses BRISK for feature detection and Boost for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.BoostDesc_create(), *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=DEFAULT_FEATURE_DETECTOR, + kp_descriptor=cv2.xfeatures2d.BoostDesc_create(), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) class VggFD(FeatureDD): """Uses BRISK for feature detection and VGG for feature description""" - def __init__(self, kp_detector=DEFAULT_FEATURE_DETECTOR, - kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=5.0), - *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=DEFAULT_FEATURE_DETECTOR, + kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=5.0), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) class OrbVggFD(FeatureDD): """Uses ORB for feature detection and VGG for feature description""" - def __init__(self, kp_detector=cv2.ORB_create(nfeatures=MAX_FEATURES, fastThreshold=0), kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=0.75), *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=cv2.ORB_create(nfeatures=MAX_FEATURES, fastThreshold=0), + kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=0.75), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) class SKOrbVggFD(FeatureDD): """Uses ORB for feature detection and VGG for feature description""" - def __init__(self, kp_detector=cv2.ORB_create(nfeatures=MAX_FEATURES, fastThreshold=0), kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=0.75), *args, **kwargs): - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + + def __init__( + self, + kp_detector=cv2.ORB_create(nfeatures=MAX_FEATURES, fastThreshold=0), + kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=0.75), + *args, + **kwargs, + ): + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) # Example of a custom detector that uses the Censure feature detector @@ -290,6 +355,7 @@ class FeatureDetector(object): Required methods are: detect """ + def __init__(self): self.detector = None @@ -308,6 +374,7 @@ def detect(self, image): # Example of how to create a feature detector using OpenCV + skimage # + class SkCensureDetector(FeatureDetector): """A CENSURE feature detector from scikit image @@ -315,6 +382,7 @@ class SkCensureDetector(FeatureDetector): OpenCV feature descriptor """ + def __init__(self, **kwargs): super().__init__() self.kp_detector = feature.CENSURE(**kwargs) @@ -345,23 +413,38 @@ def detect(self, image): kp_scales = self.kp_detector.scales unique_scales = np.unique(kp_scales) scale_diff = np.min(np.diff(unique_scales)) - kp_ocatves = np.digitize(kp_scales, np.linspace(kp_scales.min(), kp_scales.max()+scale_diff, len(unique_scales))) - cv_kp = [cv2.KeyPoint(x=self.kp_detector.keypoints[i][1], - y=self.kp_detector.keypoints[i][0], - size=int(base_patch_size*kp_ocatves[i]), - octave=kp_ocatves[i] - ) - for i in range(self.kp_detector.keypoints.shape[0]) - ] + kp_ocatves = np.digitize( + kp_scales, + np.linspace( + kp_scales.min(), kp_scales.max() + scale_diff, len(unique_scales) + ), + ) + cv_kp = [ + cv2.KeyPoint( + x=self.kp_detector.keypoints[i][1], + y=self.kp_detector.keypoints[i][0], + size=int(base_patch_size * kp_ocatves[i]), + octave=kp_ocatves[i], + ) + for i in range(self.kp_detector.keypoints.shape[0]) + ] return cv_kp class CensureVggFD(FeatureDD): - def __init__(self, kp_detector=SkCensureDetector(mode="Octagon", - max_scale=8, non_max_threshold=0.02), - kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=6.25), *args, **kwargs): - - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + def __init__( + self, + kp_detector=SkCensureDetector( + mode="Octagon", max_scale=8, non_max_threshold=0.02 + ), + kp_descriptor=cv2.xfeatures2d.VGG_create(scale_factor=6.25), + *args, + **kwargs, + ): + + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) self.kp_descriptor_name = self.__class__.__name__ self.kp_detector_name = self.__class__.__name__ @@ -374,16 +457,17 @@ def __init__(self, dasiy_arg_dict=None, *args, **kwargs): https://scikit-image.org/docs/dev/auto_examples/features_detection/plot_daisy.html#sphx-glr-auto-examples-features-detection-plot-daisy-py """ - self.dasiy_arg_dict = {"step": 4, - "radius": 15, - "rings": 3, - "histograms": 8, - "orientations": 8, - "normalization": "l1", - "sigmas": None, - "ring_radii": None, - "visualize": False - } + self.dasiy_arg_dict = { + "step": 4, + "radius": 15, + "rings": 3, + "histograms": 8, + "orientations": 8, + "normalization": "l1", + "sigmas": None, + "ring_radii": None, + "visualize": False, + } if dasiy_arg_dict is not None: self.dasiy_arg_dict.update(dasiy_arg_dict) @@ -417,7 +501,6 @@ def detect_and_compute(self, image, mask=None): class SuperPointFD(FeatureDD): - """SuperPoint `FeatureDD` Use SuperPoint to detect and describe features (`detect_and_compute`) @@ -431,8 +514,16 @@ class SuperPointFD(FeatureDD): """ - def __init__(self, keypoint_threshold=0.005, nms_radius=4, force_cpu=False, kp_descriptor=None, kp_detector=None, *args, **kwargs): - + def __init__( + self, + keypoint_threshold=0.005, + nms_radius=4, + force_cpu=False, + kp_descriptor=None, + kp_detector=None, + *args, + **kwargs, + ): """ Parameters ---------- @@ -449,10 +540,17 @@ def __init__(self, keypoint_threshold=0.005, nms_radius=4, force_cpu=False, kp_d kp_descriptor : optional, OpenCV feature descriptor """ - super().__init__(kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs) + if not _TORCH_AVAILABLE: + raise ImportError( + "SuperPointFD requires torch and kornia. " + "Install with: pip install 'valis-wsi[dl]'" + ) + super().__init__( + kp_detector=kp_detector, kp_descriptor=kp_descriptor, *args, **kwargs + ) self.keypoint_threshold = keypoint_threshold self.nms_radius = nms_radius - self.device = 'cuda' if torch.cuda.is_available() and not force_cpu else "cpu" + self.device = "cuda" if torch.cuda.is_available() and not force_cpu else "cpu" if kp_detector is None: self.kp_detector_name = "SuperPoint" @@ -467,12 +565,13 @@ def __init__(self, keypoint_threshold=0.005, nms_radius=4, force_cpu=False, kp_d self.kp_descriptor_name = kp_descriptor.__class__.__name__ self.config = { - 'superpoint': { - 'nms_radius': self.nms_radius, - 'keypoint_threshold': self.keypoint_threshold, + "superpoint": { + "nms_radius": self.nms_radius, + "keypoint_threshold": self.keypoint_threshold, "device": self.device, - 'max_keypoints': MAX_FEATURES - }} + "max_keypoints": MAX_FEATURES, + } + } def frame2tensor(self, img): float_img = exposure.rescale_intensity(img, out_range=np.float32) @@ -510,8 +609,12 @@ def compute(self, img, kp_pos_xy): descriptors = sp.convDb(cDa) descriptors = torch.nn.functional.normalize(descriptors, p=2, dim=1) - descriptors = [superpoint.sample_descriptors(k[None], d[None], 8)[0] - for k, d in zip([torch.from_numpy(kp_pos_xy.astype(np.float32))], descriptors)] + descriptors = [ + superpoint.sample_descriptors(k[None], d[None], 8)[0] + for k, d in zip( + [torch.from_numpy(kp_pos_xy.astype(np.float32))], descriptors + ) + ] descriptors = descriptors[0].detach().numpy().T else: @@ -526,11 +629,11 @@ def compute(self, img, kp_pos_xy): def detect_and_compute_sg(self, img): inp = self.frame2tensor(img) - superpoint_obj = superpoint.SuperPoint(self.config.get('superpoint', {})) - pred = superpoint_obj({'image': inp}) - pred = {**pred, **{k+'0': v for k, v in pred.items()}} - kp_pos_xy = pred['keypoints'][0].detach().numpy() - desc = pred['descriptors'][0].detach().numpy().T + superpoint_obj = superpoint.SuperPoint(self.config.get("superpoint", {})) + pred = superpoint_obj({"image": inp}) + pred = {**pred, **{k + "0": v for k, v in pred.items()}} + kp_pos_xy = pred["keypoints"][0].detach().numpy() + desc = pred["descriptors"][0].detach().numpy().T return kp_pos_xy, desc @@ -549,13 +652,27 @@ class KorniaFD(FeatureDD): """ Abstract class for feature detectors implemented in Kornia """ - def __init__(self, kp_detector=None, kp_descriptor=None, - num_features=MAX_FEATURES, rgb=False, device=None, *args, **kwargs): + + def __init__( + self, + kp_detector=None, + kp_descriptor=None, + num_features=MAX_FEATURES, + rgb=False, + device=None, + *args, + **kwargs, + ): + if not _TORCH_AVAILABLE: + raise ImportError( + f"{self.__class__.__name__} requires torch and kornia. " + "Install with: pip install 'valis-wsi[dl]'" + ) super().__init__(*args, **kwargs) self.rgb = rgb - self.device=device - self.num_features=num_features + self.device = device + self.num_features = num_features class DiskFD(KorniaFD): @@ -570,12 +687,24 @@ class DiskFD(KorniaFD): """ - def __init__(self, kp_detector=DISK, kp_descriptor=DISK, num_features=MAX_FEATURES, quant_image=True, rgb=False, device=None, *args, **kwargs): - super().__init__(rgb=rgb, device=device, num_features=num_features, *args, **kwargs) + def __init__( + self, + kp_detector=DISK, + kp_descriptor=DISK, + num_features=MAX_FEATURES, + quant_image=True, + rgb=False, + device=None, + *args, + **kwargs, + ): + super().__init__( + rgb=rgb, device=device, num_features=num_features, *args, **kwargs + ) if device is None: device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.device = device - self.disk = kp_detector.from_pretrained('depth').eval().to(self.device) + self.disk = kp_detector.from_pretrained("depth").eval().to(self.device) self.kp_descriptor_name = kp_detector.__name__ self.kp_detector_name = kp_descriptor.__name__ self.light_glue_feature_name = "disk" @@ -604,10 +733,13 @@ def _detect_and_compute(self, image, *args, **kwargs): tensor_img = preprocessing.img_to_tensor(image) with torch.inference_mode(): - res = self.disk(tensor_img.to(self.device).float(), n=self.num_features, pad_if_not_divisible=True)[0] + res = self.disk( + tensor_img.to(self.device).float(), + n=self.num_features, + pad_if_not_divisible=True, + )[0] kp_pos_xy = res.keypoints.detach().numpy() desc = res.descriptors.detach().numpy() - return kp_pos_xy, desc @@ -624,14 +756,28 @@ class DeDoDeFD(KorniaFD): """ - def __init__(self, kp_detector=DeDoDe, kp_descriptor=DeDoDe, num_features=MAX_FEATURES, quant_image=True, rgb=False, device=None, *args, **kwargs): - super().__init__(rgb=rgb, device=device, num_features=num_features, *args, **kwargs) + def __init__( + self, + kp_detector=DeDoDe, + kp_descriptor=DeDoDe, + num_features=MAX_FEATURES, + quant_image=True, + rgb=False, + device=None, + *args, + **kwargs, + ): + super().__init__( + rgb=rgb, device=device, num_features=num_features, *args, **kwargs + ) if device is None: device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.device = device self.quant_image = quant_image amp_dtype = torch.float16 if device == "cuda" else torch.float - self.dedode = kp_detector.from_pretrained(amp_dtype=amp_dtype).eval().to(self.device) + self.dedode = ( + kp_detector.from_pretrained(amp_dtype=amp_dtype).eval().to(self.device) + ) self.kp_descriptor_name = kp_detector.__name__ self.kp_detector_name = kp_descriptor.__name__ self.light_glue_feature_name = "dedodeg" @@ -659,7 +805,11 @@ def _detect_and_compute(self, image, *args, **kwargs): tensor_img = preprocessing.img_to_tensor(image) with torch.inference_mode(): - res = self.dedode(tensor_img.to(self.device).float(), n=self.num_features, pad_if_not_divisible=True) + res = self.dedode( + tensor_img.to(self.device).float(), + n=self.num_features, + pad_if_not_divisible=True, + ) kp_pos_xy = res[0].detach().squeeze(0).numpy() scores = res[1].detach().numpy() desc = res[2].detach().squeeze(0).numpy() diff --git a/valis/feature_matcher.py b/src/valis/feature_matcher.py similarity index 68% rename from valis/feature_matcher.py rename to src/valis/feature_matcher.py index 5a239c85..19a5721f 100644 --- a/valis/feature_matcher.py +++ b/src/valis/feature_matcher.py @@ -1,10 +1,8 @@ -"""Functions and classes to match and filter image features -""" -import torch -import kornia +"""Functions and classes to match and filter image features""" + +import logging import numpy as np import cv2 -import torch from copy import deepcopy from sklearn import metrics from sklearn.metrics.pairwise import pairwise_kernels @@ -12,10 +10,21 @@ import traceback from . import warp_tools, valtils, feature_detectors -from .superglue_models import matching, superglue, superpoint + +try: + import torch + import kornia + from .superglue_models import matching, superglue, superpoint + + _TORCH_AVAILABLE = True +except ImportError: + _TORCH_AVAILABLE = False + +logger = logging.getLogger(__name__) AMBIGUOUS_METRICS = set(metrics.pairwise._VALID_METRICS).intersection( - metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS.keys()) + metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS.keys() +) """set: Metrics found in both the valid metrics ang kernel methods in sklearn.metrics.pairwise. Issue is that metrics are distances, @@ -33,17 +42,13 @@ RANSAC_NAME = "RANSAC" USAC_MAGSAC_NAME = "USAC_MAGSAC" -RANSAC_DICT = { - RANSAC_NAME: cv2.RANSAC, - USAC_MAGSAC_NAME: cv2.USAC_MAGSAC -} +RANSAC_DICT = {RANSAC_NAME: cv2.RANSAC, USAC_MAGSAC_NAME: cv2.USAC_MAGSAC} DEFAULT_RANSAC_NAME = USAC_MAGSAC_NAME """str: If filter_method parameter in match_desc_and_kp is set to this, RANSAC will be used to remove poor matches """ - SUPERGLUE_FILTER_NAME = "superglue" """str: If filter_method parameter in match_desc_and_kp is set to this, only SuperGlue will be used to remove poor matches @@ -58,6 +63,7 @@ DEFAULT_FD = feature_detectors.VggFD ROTATION_ESTIMATOR_FD = feature_detectors.VggFD + def convert_distance_to_similarity(d, n_features=64): """ Convert distance to similarity @@ -103,7 +109,9 @@ def convert_similarity_to_distance(s, n_features=64): return -np.log(s + EPS) / (1 / n_features) -def filter_matches_ransac(kp1_xy, kp2_xy, ransac_val=DEFAULT_RANSAC, method=USAC_MAGSAC_NAME): +def filter_matches_ransac( + kp1_xy, kp2_xy, ransac_val=DEFAULT_RANSAC, method=USAC_MAGSAC_NAME +): f"""Remove poor matches using RANSAC Parameters @@ -143,7 +151,7 @@ def filter_matches_ransac(kp1_xy, kp2_xy, ransac_val=DEFAULT_RANSAC, method=USAC else: traceback_msg = traceback.format_exc() msg = f"Need at least 4 keypoints for RANSAC filtering, but only have {kp1_xy.shape[0]}" - valtils.print_warning(msg, traceback_msg=traceback_msg) + logger.warning(msg) filtered_src_points = kp1_xy.copy() filtered_dst_points = kp2_xy.copy() good_idx = np.arange(0, kp1_xy.shape[0]) @@ -151,8 +159,9 @@ def filter_matches_ransac(kp1_xy, kp2_xy, ransac_val=DEFAULT_RANSAC, method=USAC return filtered_src_points, filtered_dst_points, good_idx -def filter_matches_gms(kp1_xy, kp2_xy, feature_d, img1_shape, img2_shape, - scaling, thresholdFactor=6.0): +def filter_matches_gms( + kp1_xy, kp2_xy, feature_d, img1_shape, img2_shape, scaling, thresholdFactor=6.0 +): """Filter matches using GMS (Grid-based Motion Statistics) [1] This filtering method does best when there are a large number of features, @@ -212,9 +221,20 @@ def filter_matches_gms(kp1_xy, kp2_xy, feature_d, img1_shape, img2_shape, kp1 = cv2.KeyPoint_convert(kp1_xy.tolist()) kp2 = cv2.KeyPoint_convert(kp2_xy.tolist()) - matches = [cv2.DMatch(_queryIdx=i, _trainIdx=i, _imgIdx=0, _distance=feature_d[i]) for i in range(len(kp1_xy))] - gms_matches = cv2.xfeatures2d.matchGMS(img1_shape, img2_shape, kp1, kp2, matches, withRotation=True, - withScale=scaling, thresholdFactor=thresholdFactor) + matches = [ + cv2.DMatch(_queryIdx=i, _trainIdx=i, _imgIdx=0, _distance=feature_d[i]) + for i in range(len(kp1_xy)) + ] + gms_matches = cv2.xfeatures2d.matchGMS( + img1_shape, + img2_shape, + kp1, + kp2, + matches, + withRotation=True, + withScale=scaling, + thresholdFactor=thresholdFactor, + ) good_idx = np.array([d.queryIdx for d in gms_matches]) if len(good_idx) == 0: @@ -224,7 +244,11 @@ def filter_matches_gms(kp1_xy, kp2_xy, feature_d, img1_shape, img2_shape, filtered_src_points = kp1_xy[good_idx, :] filtered_dst_points = kp2_xy[good_idx, :] - return np.array(filtered_src_points), np.array(filtered_dst_points), np.array(good_idx) + return ( + np.array(filtered_src_points), + np.array(filtered_dst_points), + np.array(good_idx), + ) def filter_matches_tukey(src_xy, dst_xy, tform=transform.SimilarityTransform()): @@ -256,21 +280,21 @@ def filter_matches_tukey(src_xy, dst_xy, tform=transform.SimilarityTransform()): M = tform.params warped_xy = warp_tools.warp_xy(src_xy, M) - d = warp_tools.calc_d(warped_xy, dst_xy) + d = warp_tools.calc_d(warped_xy, dst_xy) q1 = np.quantile(d, 0.25) q3 = np.quantile(d, 0.75) - iqr = q3-q1 - inner_fence = 1.5*iqr - outer_fence = 3*iqr + iqr = q3 - q1 + inner_fence = 1.5 * iqr + outer_fence = 3 * iqr # inner fence lower and upper end - inner_fence_le = q1-inner_fence - inner_fence_ue = q3+inner_fence + inner_fence_le = q1 - inner_fence + inner_fence_ue = q3 + inner_fence # outer fence lower and upper end - outer_fence_le = q1-outer_fence - outer_fence_ue = q3+outer_fence + outer_fence_le = q1 - outer_fence + outer_fence_ue = q3 + outer_fence outliers_prob = [] outliers_poss = [] @@ -293,8 +317,7 @@ def filter_matches_tukey(src_xy, dst_xy, tform=transform.SimilarityTransform()): return src_xy_inlier, dst_xy_inlier, inliers_prob -def filter_matches(kp1_xy, kp2_xy, method=DEFAULT_MATCH_FILTER, - filtering_kwargs={}): +def filter_matches(kp1_xy, kp2_xy, method=DEFAULT_MATCH_FILTER, filtering_kwargs={}): """Use RANSAC or GMS to remove poor matches Parameters @@ -343,13 +366,23 @@ def filter_matches(kp1_xy, kp2_xy, method=DEFAULT_MATCH_FILTER, filtered_src_points, filtered_dst_points, good_idx = filter_fxn(**all_matching_args) # Do additional filtering to remove other outliers that may have been missed by RANSAC - filtered_src_points, filtered_dst_points, good_idx = filter_matches_tukey(filtered_src_points, filtered_dst_points) + filtered_src_points, filtered_dst_points, good_idx = filter_matches_tukey( + filtered_src_points, filtered_dst_points + ) return filtered_src_points, filtered_dst_points, good_idx -def match_descriptors(descriptors1, descriptors2, metric=None, - metric_type=None, p=2, max_distance=np.inf, - cross_check=True, max_ratio=1.0, metric_kwargs=None): +def match_descriptors( + descriptors1, + descriptors2, + metric=None, + metric_type=None, + p=2, + max_distance=np.inf, + cross_check=True, + max_ratio=1.0, + metric_kwargs=None, +): """Brute-force matching of descriptors For each descriptor in the first set this matcher finds the closest @@ -420,34 +453,46 @@ def match_descriptors(descriptors1, descriptors2, metric=None, if metric is None: if np.issubdtype(descriptors1.dtype, np.bool_): - metric = 'hamming' + metric = "hamming" else: - metric = 'euclidean' + metric = "euclidean" if metric_kwargs is None: metric_kwargs = {} - if metric == 'minkowski': - metric_kwargs['p'] = p + if metric == "minkowski": + metric_kwargs["p"] = p if metric in AMBIGUOUS_METRICS: - print("metric", metric, "could be a distance in pairwise_distances() or similarity in pairwise_kernels().", - "Please set metric_type. Otherwise, metric is assumed to be a distance") + logger.warning( + f"metric {metric} could be a distance in pairwise_distances() or similarity in pairwise_kernels(). " + "Please set metric_type. Otherwise, metric is assumed to be a distance" + ) if callable(metric) or metric in metrics.pairwise._VALID_METRICS: - distances = metrics.pairwise_distances(descriptors1, descriptors2, metric=metric, **metric_kwargs) + distances = metrics.pairwise_distances( + descriptors1, descriptors2, metric=metric, **metric_kwargs + ) if callable(metric) and metric_type is None: - print(Warning("Metric passed as a function or class, but the metric type not provided", - "Assuming the metric function returns a distance. If a similarity is actually returned", - "set metric_type = 'similiarity'. If metric is a distance, set metric_type = 'distance'" - "to avoid this message")) + logger.warning( + "Metric passed as a function or class, but the metric type not provided. " + "Assuming the metric function returns a distance. If a similarity is actually returned, " + "set metric_type = 'similiarity'. If metric is a distance, set metric_type = 'distance' " + "to avoid this message" + ) metric_type = "distance" if metric_type == "similarity": - distances = convert_similarity_to_distance(distances, n_features=descriptors1.shape[1]) + distances = convert_similarity_to_distance( + distances, n_features=descriptors1.shape[1] + ) if metric in metrics.pairwise.PAIRWISE_KERNEL_FUNCTIONS: - similarities = pairwise_kernels(descriptors1, descriptors2, metric=metric, **metric_kwargs) - distances = convert_similarity_to_distance(similarities, n_features=descriptors1.shape[1]) + similarities = pairwise_kernels( + descriptors1, descriptors2, metric=metric, **metric_kwargs + ) + distances = convert_similarity_to_distance( + similarities, n_features=descriptors1.shape[1] + ) if callable(metric): metric_name = metric.__name__ @@ -473,23 +518,41 @@ def match_descriptors(descriptors1, descriptors2, metric=None, distances[indices1, indices2] = np.inf second_best_indices2 = np.argmin(distances[indices1], axis=1) second_best_distances = distances[indices1, second_best_indices2] - second_best_distances[second_best_distances == 0] \ - = np.finfo(np.double).eps + second_best_distances[second_best_distances == 0] = np.finfo(np.double).eps ratio = best_distances / second_best_distances mask = ratio < max_ratio indices1 = indices1[mask] indices2 = indices2[mask] - return np.column_stack((indices1, indices2)), best_distances[indices1, indices2], metric, metric_type + return ( + np.column_stack((indices1, indices2)), + best_distances[indices1, indices2], + metric, + metric_type, + ) else: - return np.column_stack((indices1, indices2)), distances[indices1, indices2], metric_name, metric_type - - -def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detector_name=None, - metric_type=None, metric_kwargs=None, max_ratio=1.0, - filter_method=DEFAULT_MATCH_FILTER, - filtering_kwargs=None): + return ( + np.column_stack((indices1, indices2)), + distances[indices1, indices2], + metric_name, + metric_type, + ) + + +def match_desc_and_kp( + desc1, + kp1_xy, + desc2, + kp2_xy, + metric=None, + feature_detector_name=None, + metric_type=None, + metric_kwargs=None, + max_ratio=1.0, + filter_method=DEFAULT_MATCH_FILTER, + filtering_kwargs=None, +): """Match the descriptors of image 1 with those of image 2 and remove outliers. Metric can be a string to use a distance in scipy.distnce.cdist(), @@ -580,12 +643,15 @@ def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detecto elif filter_method.upper() in RANSAC_DICT.keys(): cross_check = True - matches, match_distances, metric_name, metric_type = \ - match_descriptors(desc1, desc2, metric=metric, - metric_type=metric_type, - metric_kwargs=metric_kwargs, - max_ratio=max_ratio, - cross_check=cross_check) + matches, match_distances, metric_name, metric_type = match_descriptors( + desc1, + desc2, + metric=metric, + metric_type=metric_type, + metric_kwargs=metric_kwargs, + max_ratio=max_ratio, + cross_check=cross_check, + ) desc1_match_idx = matches[:, 0] matched_kp1_xy = kp1_xy[desc1_match_idx, :] @@ -596,27 +662,47 @@ def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detecto matched_desc2 = desc2[desc2_match_idx, :] mean_unfiltered_distance = np.mean(match_distances) - mean_unfiltered_similarity = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - match_info12 = MatchInfo(matched_kp1_xy=matched_kp1_xy, matched_desc1=matched_desc1, - matches12=desc1_match_idx, matched_kp2_xy=matched_kp2_xy, - matched_desc2=matched_desc2, matches21=desc2_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=metric_name, - metric_type=metric_type, feature_detector_name=feature_detector_name) - - match_info21 = MatchInfo(matched_kp1_xy=matched_kp2_xy, matched_desc1=matched_desc2, - matches12=desc2_match_idx, matched_kp2_xy=matched_kp1_xy, - matched_desc2=matched_desc1, matches21=desc1_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=metric_name, - metric_type=metric_type, feature_detector_name=feature_detector_name) + mean_unfiltered_similarity = np.mean( + convert_distance_to_similarity(match_distances, n_features=desc1.shape[1]) + ) + + match_info12 = MatchInfo( + matched_kp1_xy=matched_kp1_xy, + matched_desc1=matched_desc1, + matches12=desc1_match_idx, + matched_kp2_xy=matched_kp2_xy, + matched_desc2=matched_desc2, + matches21=desc2_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=metric_name, + metric_type=metric_type, + feature_detector_name=feature_detector_name, + ) + + match_info21 = MatchInfo( + matched_kp1_xy=matched_kp2_xy, + matched_desc1=matched_desc2, + matches12=desc2_match_idx, + matched_kp2_xy=matched_kp1_xy, + matched_desc2=matched_desc1, + matches21=desc1_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=metric_name, + metric_type=metric_type, + feature_detector_name=feature_detector_name, + ) # Filter matches # all_filtering_kwargs = {"kp1_xy": matched_kp1_xy, "kp2_xy": matched_kp2_xy} if filtering_kwargs is None: if filter_method not in RANSAC_DICT.keys(): - print(Warning(f"filtering_kwargs not provided for {filter_method} match filtering. Will use {DEFAULT_RANSAC_NAME} instead")) + logger.warning( + f"filtering_kwargs not provided for {filter_method} match filtering. Will use {DEFAULT_RANSAC_NAME} instead" + ) filter_method = DEFAULT_RANSAC_NAME all_filtering_kwargs.update({"ransac_val": DEFAULT_RANSAC}) else: @@ -631,8 +717,9 @@ def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detecto all_filtering_kwargs.update({"feature_d": match_distances}) - filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = \ - filter_matches(matched_kp1_xy, matched_kp2_xy, filter_method, all_filtering_kwargs) + filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = filter_matches( + matched_kp1_xy, matched_kp2_xy, filter_method, all_filtering_kwargs + ) if len(good_matches_idx) > 0: filterd_match_distances = match_distances[good_matches_idx] @@ -643,9 +730,11 @@ def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detecto good_matches21 = desc2_match_idx[good_matches_idx] mean_filtered_distance = np.mean(filterd_match_distances) - mean_filtered_similarity = \ - np.mean(convert_distance_to_similarity(filterd_match_distances, - n_features=desc1.shape[1])) + mean_filtered_similarity = np.mean( + convert_distance_to_similarity( + filterd_match_distances, n_features=desc1.shape[1] + ) + ) else: filterd_match_distances = [] filterd_matched_desc1 = [] @@ -658,19 +747,35 @@ def match_desc_and_kp(desc1, kp1_xy, desc2, kp2_xy, metric=None, feature_detecto mean_filtered_similarity = 0 # Record filtered matches - filtered_match_info12 = MatchInfo(matched_kp1_xy=filtered_matched_kp1_xy, matched_desc1=filterd_matched_desc1, - matches12=good_matches12, matched_kp2_xy=filtered_matched_kp2_xy, - matched_desc2=filterd_matched_desc2, matches21=good_matches21, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=metric_name, - metric_type=metric_type, feature_detector_name=feature_detector_name) - - filtered_match_info21 = MatchInfo(matched_kp1_xy=filtered_matched_kp2_xy, matched_desc1=filterd_matched_desc2, - matches12=good_matches21, matched_kp2_xy=filtered_matched_kp1_xy, - matched_desc2=filterd_matched_desc1, matches21=good_matches12, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=metric_name, - metric_type=metric_type, feature_detector_name=feature_detector_name) + filtered_match_info12 = MatchInfo( + matched_kp1_xy=filtered_matched_kp1_xy, + matched_desc1=filterd_matched_desc1, + matches12=good_matches12, + matched_kp2_xy=filtered_matched_kp2_xy, + matched_desc2=filterd_matched_desc2, + matches21=good_matches21, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=metric_name, + metric_type=metric_type, + feature_detector_name=feature_detector_name, + ) + + filtered_match_info21 = MatchInfo( + matched_kp1_xy=filtered_matched_kp2_xy, + matched_desc1=filterd_matched_desc2, + matches12=good_matches21, + matched_kp2_xy=filtered_matched_kp1_xy, + matched_desc2=filterd_matched_desc1, + matches21=good_matches12, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=metric_name, + metric_type=metric_type, + feature_detector_name=feature_detector_name, + ) return match_info12, filtered_match_info12, match_info21, filtered_match_info21 @@ -681,14 +786,23 @@ class MatchInfo(object): All attributes are all set as parameters during initialization """ - def __init__(self, - matched_kp1_xy, matched_desc1, matches12, - matched_kp2_xy, matched_desc2, matches21, - match_distances, distance, similarity, - metric_name, metric_type, - img1_name=None, img2_name=None, - feature_detector_name=None): - + def __init__( + self, + matched_kp1_xy, + matched_desc1, + matches12, + matched_kp2_xy, + matched_desc2, + matches21, + match_distances, + distance, + similarity, + metric_name, + metric_type, + img1_name=None, + img2_name=None, + feature_detector_name=None, + ): """Stores information about matches and features Parameters @@ -808,9 +922,17 @@ class Matcher(object): """ - def __init__(self, feature_detector=DEFAULT_FD(), metric=None, metric_type=None, metric_kwargs=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - gms_threshold=15, scaling=False): + def __init__( + self, + feature_detector=DEFAULT_FD(), + metric=None, + metric_type=None, + metric_kwargs=None, + match_filter_method=DEFAULT_MATCH_FILTER, + ransac_thresh=DEFAULT_RANSAC, + gms_threshold=15, + scaling=False, + ): """ Parameters ---------- @@ -877,9 +999,18 @@ def __init__(self, feature_detector=DEFAULT_FD(), metric=None, metric_type=None, self.match_filter_method = match_filter_method self.rotation_invariant = True - def match_images(self, img1=None, desc1=None, kp1_xy=None, - img2=None, desc2=None, kp2_xy=None, additional_filtering_kwargs=None, *args, **kwargs): - + def match_images( + self, + img1=None, + desc1=None, + kp1_xy=None, + img2=None, + desc2=None, + kp2_xy=None, + additional_filtering_kwargs=None, + *args, + **kwargs, + ): """Match the descriptors of image 1 with those of image 2, Outliers removed using match_filter_method. Metric can be a string to use a distance in scipy.distnce.cdist(), or a custom distance @@ -945,13 +1076,15 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, if additional_filtering_kwargs is not None: # At this point arguments need to include: img1_shape, img2_shape # filtering_kwargs = additional_filtering_kwargs.copy() - filtering_kwargs.update({"scaling": self.scaling, - "thresholdFactor": self.gms_threshold}) + filtering_kwargs.update( + {"scaling": self.scaling, "thresholdFactor": self.gms_threshold} + ) else: - print(Warning(f"Selected {self.match_filter_method},\ - but did not provide argument\ - additional_filtering_kwargs.\ - Defaulting to RANSAC")) + logger.warning( + f"Selected {self.match_filter_method}, " + "but did not provide argument additional_filtering_kwargs. " + "Defaulting to RANSAC" + ) self.match_filter_method = DEFAULT_RANSAC_NAME filtering_kwargs = {"ransac_val": self.ransac} @@ -960,8 +1093,9 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, filtering_kwargs = {"ransac_val": self.ransac} else: - print(Warning(f"Dont know {self.match_filter_method}.\ - Defaulting to RANSAC")) + logger.warning( + f"Don't know {self.match_filter_method}. " "Defaulting to RANSAC" + ) self.match_filter_method = DEFAULT_RANSAC_NAME filtering_kwargs = {"ransac_val": self.ransac} @@ -972,25 +1106,43 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, if desc2 is None and kp2_xy is None and img2 is not None: kp2_xy, desc2 = self.feature_detector.detect_and_compute(img2) - match_info12, filtered_match_info12, match_info21, filtered_match_info21 = \ - match_desc_and_kp(desc1=desc1, kp1_xy=kp1_xy, desc2=desc2, kp2_xy=kp2_xy, - metric=self.metric, metric_type=self.metric_type, - metric_kwargs=self.metric_kwargs, - filter_method=self.match_filter_method, - filtering_kwargs=filtering_kwargs, - feature_detector_name=self.feature_name) + match_info12, filtered_match_info12, match_info21, filtered_match_info21 = ( + match_desc_and_kp( + desc1=desc1, + kp1_xy=kp1_xy, + desc2=desc2, + kp2_xy=kp2_xy, + metric=self.metric, + metric_type=self.metric_type, + metric_kwargs=self.metric_kwargs, + filter_method=self.match_filter_method, + filtering_kwargs=filtering_kwargs, + feature_detector_name=self.feature_name, + ) + ) if self.metric_name is None: self.metric_name = match_info12.metric_name return match_info12, filtered_match_info12, match_info21, filtered_match_info21 - def estimate_rotation(self, moving_img=None, fixed_img=None, moving_kp_xy=None, fixed_kp_xy=None, angle_estimator=None, *args, **kwargs): + def estimate_rotation( + self, + moving_img=None, + fixed_img=None, + moving_kp_xy=None, + fixed_kp_xy=None, + angle_estimator=None, + *args, + **kwargs, + ): """ Use a rotation invariant feature descriptor to estimate angle to rotate moving_img to align with fixed_img. Match Info should contain the matches where kp1 refers to the fixed image, and kp2 refers to the moving image """ - if (moving_kp_xy is None or fixed_kp_xy is None) and (fixed_img is not None and moving_img is not None): + if (moving_kp_xy is None or fixed_kp_xy is None) and ( + fixed_img is not None and moving_img is not None + ): if angle_estimator is None: angle_estimator = ROTATION_ESTIMATOR_FD() kp1_xy, desc1 = angle_estimator.detect_and_compute(fixed_img) @@ -1002,11 +1154,14 @@ def estimate_rotation(self, moving_img=None, fixed_img=None, moving_kp_xy=None, moving_kp_xy = match_info.matched_kp2_xy angle_estimator = transform.SimilarityTransform() - angle_estimator.estimate(fixed_kp_xy, moving_kp_xy) # Estimates inverse transform, and want 2 to align to 1 + angle_estimator.estimate( + fixed_kp_xy, moving_kp_xy + ) # Estimates inverse transform, and want 2 to align to 1 rot_deg = np.rad2deg(angle_estimator.rotation) return rot_deg + class SuperGlueMatcher(Matcher): """Use SuperGlue to match images (`match_images`) @@ -1020,12 +1175,23 @@ class SuperGlueMatcher(Matcher): """ - def __init__(self, feature_detector=feature_detectors.SuperPointFD(), weights="indoor", keypoint_threshold=0.005, nms_radius=4, - sinkhorn_iterations=100, match_threshold=0.2, force_cpu=False, - metric=None, metric_type=None, metric_kwargs=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - gms_threshold=15, scaling=False): - + def __init__( + self, + feature_detector=feature_detectors.SuperPointFD(), + weights="indoor", + keypoint_threshold=0.005, + nms_radius=4, + sinkhorn_iterations=100, + match_threshold=0.2, + force_cpu=False, + metric=None, + metric_type=None, + metric_kwargs=None, + match_filter_method=DEFAULT_MATCH_FILTER, + ransac_thresh=DEFAULT_RANSAC, + gms_threshold=15, + scaling=False, + ): """ Use SuperGlue to match images (`match_images`) @@ -1057,9 +1223,20 @@ def __init__(self, feature_detector=feature_detectors.SuperPointFD(), weights="i filter_method is "GMS". """ - super().__init__(metric=metric, metric_type=metric_type, metric_kwargs=metric_kwargs, - match_filter_method=match_filter_method, ransac_thresh=ransac_thresh, - gms_threshold=gms_threshold, scaling=scaling) + if not _TORCH_AVAILABLE: + raise ImportError( + "SuperGlueMatcher requires torch and kornia. " + "Install with: pip install 'valis-wsi[dl]'" + ) + super().__init__( + metric=metric, + metric_type=metric_type, + metric_kwargs=metric_kwargs, + match_filter_method=match_filter_method, + ransac_thresh=ransac_thresh, + gms_threshold=gms_threshold, + scaling=scaling, + ) self.feature_detector = feature_detector self.feature_name = feature_detector.__class__.__name__ @@ -1074,30 +1251,29 @@ def __init__(self, feature_detector=feature_detectors.SuperPointFD(), weights="i self.matcher = "SuperGlue" self.metric_name = "SuperGlue" self.metric_type = "distance" - self.device = 'cuda' if torch.cuda.is_available() and not force_cpu else "cpu" + self.device = "cuda" if torch.cuda.is_available() and not force_cpu else "cpu" self.config = { - 'superpoint': { - 'nms_radius': self.nms_radius, - 'keypoint_threshold': self.keypoint_threshold, - 'max_keypoints': feature_detectors.MAX_FEATURES, - 'device': self.device + "superpoint": { + "nms_radius": self.nms_radius, + "keypoint_threshold": self.keypoint_threshold, + "max_keypoints": feature_detectors.MAX_FEATURES, + "device": self.device, + }, + "superglue": { + "weights": self.weights, + "sinkhorn_iterations": self.sinkhorn_iterations, + "match_threshold": self.match_threshold, }, - 'superglue': { - 'weights': self.weights, - 'sinkhorn_iterations': self.sinkhorn_iterations, - 'match_threshold': self.match_threshold, - } } self.sg_matcher = superglue.SuperGlue(self.config["superglue"]) def frame2tensor(self, img): - tensor = torch.from_numpy(img/255.).float()[None, None].to(self.device) + tensor = torch.from_numpy(img / 255.0).float()[None, None].to(self.device) return tensor - def calc_scores(self, tensor_img, kp_xy): sp = superpoint.SuperPoint(self.config["superpoint"]) @@ -1118,8 +1294,8 @@ def calc_scores(self, tensor_img, kp_xy): scores = torch.nn.functional.softmax(scores, 1)[:, :-1] b, _, h, w = scores.shape scores = scores.permute(0, 2, 3, 1).reshape(b, h, w, 8, 8) - scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h*8, w*8) - scores = superpoint.simple_nms(scores, sp.config['nms_radius']) + scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h * 8, w * 8) + scores = superpoint.simple_nms(scores, sp.config["nms_radius"]) kp = [torch.from_numpy(kp_xy[:, ::-1].astype(int))] scores = [s[tuple(k.t())] for s, k in zip(scores, kp)] scores = scores[0].unsqueeze(dim=0) @@ -1146,12 +1322,24 @@ def prep_data(self, img, kp_xy, desc): return inp, kp_xy_inp, desc_inp, scores - def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None, kp2_xy=None, additional_filtering_kwargs=None, rotation_deg=None): + def match_images( + self, + img1=None, + desc1=None, + kp1_xy=None, + img2=None, + desc2=None, + kp2_xy=None, + additional_filtering_kwargs=None, + rotation_deg=None, + ): if img1 is not None and desc1 is None and kp1_xy is None: kp1_xy, desc1 = self.feature_detector.detect_and_compute(img1) - inp1, kp1_xy_inp, desc1_inp, scores1 = self.prep_data(img=img1, kp_xy=kp1_xy, desc=desc1) + inp1, kp1_xy_inp, desc1_inp, scores1 = self.prep_data( + img=img1, kp_xy=kp1_xy, desc=desc1 + ) if img2 is not None and desc2 is None and kp2_xy is None: if rotation_deg is None: @@ -1166,17 +1354,20 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None rotated_img = img2 rot_tform = transform.SimilarityTransform() - inp2, kp2_xy_inp, desc2_inp, scores2 = self.prep_data(img=rotated_img, kp_xy=r_kp2, desc=r_desc2) - - data = {"image0": inp1, - "descriptors0": desc1_inp, - "keypoints0": kp1_xy_inp, - "scores0": scores1, - "image1": inp2, - "descriptors1": desc2_inp, - "keypoints1": kp2_xy_inp, - "scores1": scores2 - } + inp2, kp2_xy_inp, desc2_inp, scores2 = self.prep_data( + img=rotated_img, kp_xy=r_kp2, desc=r_desc2 + ) + + data = { + "image0": inp1, + "descriptors0": desc1_inp, + "keypoints0": kp1_xy_inp, + "scores0": scores1, + "image1": inp2, + "descriptors1": desc2_inp, + "keypoints1": kp2_xy_inp, + "scores1": scores2, + } sg_pred = self.sg_matcher(data) @@ -1184,7 +1375,7 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None sg_pred.update(data) # Keep the matching keypoints and descriptors - matches, conf = sg_pred['matches0'], sg_pred['matching_scores0'] + matches, conf = sg_pred["matches0"], sg_pred["matching_scores0"] valid = matches > -1 desc1_match_idx = np.where(valid)[0] @@ -1197,34 +1388,50 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None matched_kp2_xy = rot_tform(rot_matched_kp2) matched_desc2 = r_desc2[desc2_match_idx, :] - - match_distances = np.sqrt(np.sum((matched_desc1 - matched_desc2)**2, axis=1)) - match_distances = match_distances/match_distances.max() - + match_distances = np.sqrt(np.sum((matched_desc1 - matched_desc2) ** 2, axis=1)) + match_distances = match_distances / match_distances.max() mean_unfiltered_distance = np.mean(match_distances) - mean_unfiltered_similarity = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - match_info12 = MatchInfo(matched_kp1_xy=matched_kp1_xy, matched_desc1=matched_desc1, - matches12=desc1_match_idx, matched_kp2_xy=matched_kp2_xy, - matched_desc2=matched_desc2, matches21=desc2_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) - - match_info21 = MatchInfo(matched_kp1_xy=matched_kp2_xy, matched_desc1=matched_desc2, - matches12=desc2_match_idx, matched_kp2_xy=matched_kp1_xy, - matched_desc2=matched_desc1, matches21=desc1_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) + mean_unfiltered_similarity = np.mean( + convert_distance_to_similarity(match_distances, n_features=desc1.shape[1]) + ) + + match_info12 = MatchInfo( + matched_kp1_xy=matched_kp1_xy, + matched_desc1=matched_desc1, + matches12=desc1_match_idx, + matched_kp2_xy=matched_kp2_xy, + matched_desc2=matched_desc2, + matches21=desc2_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) + + match_info21 = MatchInfo( + matched_kp1_xy=matched_kp2_xy, + matched_desc1=matched_desc2, + matches12=desc2_match_idx, + matched_kp2_xy=matched_kp1_xy, + matched_desc2=matched_desc1, + matches21=desc1_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) # # Remove outliers - filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = filter_matches_ransac(matched_kp1_xy, - matched_kp2_xy, - method=self.match_filter_method) + filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = ( + filter_matches_ransac( + matched_kp1_xy, matched_kp2_xy, method=self.match_filter_method + ) + ) if len(good_matches_idx) > 0: filterd_match_distances = match_distances[good_matches_idx] @@ -1235,9 +1442,11 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None good_matches21 = desc2_match_idx[good_matches_idx] mean_filtered_distance = np.mean(filterd_match_distances) - mean_filtered_similarity = \ - np.mean(convert_distance_to_similarity(filterd_match_distances, - n_features=desc1.shape[1])) + mean_filtered_similarity = np.mean( + convert_distance_to_similarity( + filterd_match_distances, n_features=desc1.shape[1] + ) + ) else: filterd_match_distances = [] filterd_matched_desc1 = [] @@ -1250,24 +1459,39 @@ def match_images(self, img1=None, desc1=None, kp1_xy=None, img2=None, desc2=None mean_filtered_similarity = 0 # Record filtered matches - filtered_match_info12 = MatchInfo(matched_kp1_xy=filtered_matched_kp1_xy, matched_desc1=filterd_matched_desc1, - matches12=good_matches12, matched_kp2_xy=filtered_matched_kp2_xy, - matched_desc2=filterd_matched_desc2, matches21=good_matches21, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) - - filtered_match_info21 = MatchInfo(matched_kp1_xy=filtered_matched_kp2_xy, matched_desc1=filterd_matched_desc2, - matches12=good_matches21, matched_kp2_xy=filtered_matched_kp1_xy, - matched_desc2=filterd_matched_desc1, matches21=good_matches12, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) + filtered_match_info12 = MatchInfo( + matched_kp1_xy=filtered_matched_kp1_xy, + matched_desc1=filterd_matched_desc1, + matches12=good_matches12, + matched_kp2_xy=filtered_matched_kp2_xy, + matched_desc2=filterd_matched_desc2, + matches21=good_matches21, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) + + filtered_match_info21 = MatchInfo( + matched_kp1_xy=filtered_matched_kp2_xy, + matched_desc1=filterd_matched_desc2, + matches12=good_matches21, + matched_kp2_xy=filtered_matched_kp1_xy, + matched_desc2=filterd_matched_desc1, + matches21=good_matches12, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) return match_info12, filtered_match_info12, match_info21, filtered_match_info21 + class LightGlueMatcher(Matcher): """ LightGlue feautre matcher, implemented in Kornia @@ -1280,14 +1504,26 @@ class LightGlueMatcher(Matcher): arXiv ePrint 2306.13643, 2023. """ - def __init__(self, feature_detector=None, - match_filter_method=DEFAULT_MATCH_FILTER, ransac_thresh=DEFAULT_RANSAC, - device=None, *args, **kwargs): + + def __init__( + self, + feature_detector=None, + match_filter_method=DEFAULT_MATCH_FILTER, + ransac_thresh=DEFAULT_RANSAC, + device=None, + *args, + **kwargs, + ): """ Parameters ---------- """ + if not _TORCH_AVAILABLE: + raise ImportError( + "LightGlueMatcher requires torch and kornia. " + "Install with: pip install 'valis-wsi[dl]'" + ) if device is None: device = torch.device("cuda" if torch.cuda.is_available() else "cpu") @@ -1309,29 +1545,38 @@ def get_fd(self): def set_fd(self, feature_detector): if not issubclass(feature_detector.__class__, feature_detectors.KorniaFD): - msg = (f"Using {feature_detector.__class__.__name__} with {self.__class__.__name__}. " - f"May get unexpected results, as {self.__class__.__name__} expects the descriptors to be generated by " - f"a subclass of {feature_detectors.KorniaFD.__name__}") + msg = ( + f"Using {feature_detector.__class__.__name__} with {self.__class__.__name__}. " + f"May get unexpected results, as {self.__class__.__name__} expects the descriptors to be generated by " + f"a subclass of {feature_detectors.KorniaFD.__name__}" + ) - valtils.print_warning(msg) + logger.warning(msg) self._feature_detector = feature_detector self.feature_name = feature_detector.__class__.__name__ self.metric_name = feature_detector.light_glue_feature_name - self.lg_matcher = kornia.feature.LightGlueMatcher(feature_detector.light_glue_feature_name).eval().to(self.device) - - feature_detector = property(fget=get_fd, - fset=set_fd, - doc="Get and set feature detector. Setting creates a new LightGlueMatcher with associated weights") - - - def estimate_rotation_brute_force(self, desc1, kp1, lafs1, hw1, moving_img, n_angles=4): + self.lg_matcher = ( + kornia.feature.LightGlueMatcher(feature_detector.light_glue_feature_name) + .eval() + .to(self.device) + ) + + feature_detector = property( + fget=get_fd, + fset=set_fd, + doc="Get and set feature detector. Setting creates a new LightGlueMatcher with associated weights", + ) + + def estimate_rotation_brute_force( + self, desc1, kp1, lafs1, hw1, moving_img, n_angles=4 + ): """ Use a rotation invarianet feature descriptor to estimate angle to rotate img2 to align with img1 """ t_desc1 = torch.from_numpy(desc1) - angle_step = 360//n_angles + angle_step = 360 // n_angles rotations = np.arange(0, 360, angle_step) best_kp1 = None @@ -1350,12 +1595,18 @@ def estimate_rotation_brute_force(self, desc1, kp1, lafs1, hw1, moving_img, n_an t_kp2 = torch.from_numpy(r_kp2).to(self.device) t_desc2 = torch.from_numpy(r_desc2).to(self.device) with torch.inference_mode(): - lafs2 = kornia.feature.laf_from_center_scale_ori(t_kp2[None], torch.ones(1, len(t_kp2), 1, 1, device=self.device)) - match_distances, idxs = self.lg_matcher(t_desc1, t_desc2, lafs1, lafs2, hw1=hw1, hw2=r_hw2) + lafs2 = kornia.feature.laf_from_center_scale_ori( + t_kp2[None], torch.ones(1, len(t_kp2), 1, 1, device=self.device) + ) + match_distances, idxs = self.lg_matcher( + t_desc1, t_desc2, lafs1, lafs2, hw1=hw1, hw2=r_hw2 + ) _kp1 = kp1[idxs[:, 0], :] _kp2 = r_kp2[idxs[:, 1], :] - _kp1, _kp2, good_idx = filter_matches(_kp1, _kp2, self.match_filter_method, filtering_kwargs={}) + _kp1, _kp2, good_idx = filter_matches( + _kp1, _kp2, self.match_filter_method, filtering_kwargs={} + ) r_n_matches = len(good_idx) all_match_counts[i] = r_n_matches rot_min_mean_distances = match_distances.min().detach().item() @@ -1376,7 +1627,19 @@ def estimate_rotation_brute_force(self, desc1, kp1, lafs1, hw1, moving_img, n_an return rot_deg - def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=None, rotation_deg=None, brute_force_angle=False, *args, **kwargs): + def match_images( + self, + img1, + img2, + desc1=None, + kp1_xy=None, + desc2=None, + kp2_xy=None, + rotation_deg=None, + brute_force_angle=False, + *args, + **kwargs, + ): """Match the descriptors of image 1 with those of image 2, Outliers removed using match_filter_method. Metric can be a string to use a distance in scipy.distnce.cdist(), or a custom distance @@ -1419,18 +1682,20 @@ def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=N t_kp1 = torch.from_numpy(kp1_xy).to(self.device) t_desc1 = torch.from_numpy(desc1).to(self.device) with torch.inference_mode(): - lafs1 = kornia.feature.laf_from_center_scale_ori(t_kp1[None], torch.ones(1, len(t_kp1), 1, 1, device=self.device)) + lafs1 = kornia.feature.laf_from_center_scale_ori( + t_kp1[None], torch.ones(1, len(t_kp1), 1, 1, device=self.device) + ) if kp2_xy is None and desc2 is None: if rotation_deg is None: if brute_force_angle: - rotation_deg = self.estimate_rotation_brute_force(desc1=desc1, - hw1=hw1, - kp1=kp1_xy, - lafs1=lafs1, - moving_img=img2) + rotation_deg = self.estimate_rotation_brute_force( + desc1=desc1, hw1=hw1, kp1=kp1_xy, lafs1=lafs1, moving_img=img2 + ) else: - rotation_deg = self.estimate_rotation(moving_img=img2, fixed_img=img1) + rotation_deg = self.estimate_rotation( + moving_img=img2, fixed_img=img1 + ) else: rotation_deg = 0 @@ -1452,9 +1717,13 @@ def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=N t_desc2 = torch.from_numpy(desc2).to(self.device) with torch.inference_mode(): - lafs2 = kornia.feature.laf_from_center_scale_ori(t_kp2[None], torch.ones(1, len(t_kp2), 1, 1, device=self.device)) + lafs2 = kornia.feature.laf_from_center_scale_ori( + t_kp2[None], torch.ones(1, len(t_kp2), 1, 1, device=self.device) + ) - match_distances, idxs = self.lg_matcher(t_desc1, t_desc2, lafs1, lafs2, hw1=hw1, hw2=r_hw2) + match_distances, idxs = self.lg_matcher( + t_desc1, t_desc2, lafs1, lafs2, hw1=hw1, hw2=r_hw2 + ) match_distances = match_distances.detach().numpy() idxs = idxs.detach().numpy() @@ -1468,28 +1737,46 @@ def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=N matched_desc2 = r_desc2[desc2_match_idx, :] mean_unfiltered_distance = np.mean(match_distances) - mean_unfiltered_similarity = np.mean(convert_distance_to_similarity(match_distances, n_features=desc1.shape[1])) - - match_info12 = MatchInfo(matched_kp1_xy=matched_kp1_xy, matched_desc1=matched_desc1, - matches12=desc1_match_idx, matched_kp2_xy=matched_kp2_xy, - matched_desc2=matched_desc2, matches21=desc2_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) - - match_info21 = MatchInfo(matched_kp1_xy=matched_kp2_xy, matched_desc1=matched_desc2, - matches12=desc2_match_idx, matched_kp2_xy=matched_kp1_xy, - matched_desc2=matched_desc1, matches21=desc1_match_idx, - match_distances=match_distances, distance=mean_unfiltered_distance, - similarity=mean_unfiltered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type, - feature_detector_name=self.feature_name) + mean_unfiltered_similarity = np.mean( + convert_distance_to_similarity(match_distances, n_features=desc1.shape[1]) + ) + + match_info12 = MatchInfo( + matched_kp1_xy=matched_kp1_xy, + matched_desc1=matched_desc1, + matches12=desc1_match_idx, + matched_kp2_xy=matched_kp2_xy, + matched_desc2=matched_desc2, + matches21=desc2_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) + + match_info21 = MatchInfo( + matched_kp1_xy=matched_kp2_xy, + matched_desc1=matched_desc2, + matches12=desc2_match_idx, + matched_kp2_xy=matched_kp1_xy, + matched_desc2=matched_desc1, + matches21=desc1_match_idx, + match_distances=match_distances, + distance=mean_unfiltered_distance, + similarity=mean_unfiltered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + feature_detector_name=self.feature_name, + ) # # Remove outliers - filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = filter_matches_ransac(matched_kp1_xy, - matched_kp2_xy, - method=self.match_filter_method) + filtered_matched_kp1_xy, filtered_matched_kp2_xy, good_matches_idx = ( + filter_matches_ransac( + matched_kp1_xy, matched_kp2_xy, method=self.match_filter_method + ) + ) if len(good_matches_idx) > 0: filterd_match_distances = match_distances[good_matches_idx] @@ -1500,9 +1787,11 @@ def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=N good_matches21 = desc2_match_idx[good_matches_idx] mean_filtered_distance = np.mean(filterd_match_distances) - mean_filtered_similarity = \ - np.mean(convert_distance_to_similarity(filterd_match_distances, - n_features=desc1.shape[1])) + mean_filtered_similarity = np.mean( + convert_distance_to_similarity( + filterd_match_distances, n_features=desc1.shape[1] + ) + ) else: filterd_match_distances = [] filterd_matched_desc1 = [] @@ -1515,18 +1804,32 @@ def match_images(self, img1, img2, desc1=None, kp1_xy=None, desc2=None, kp2_xy=N mean_filtered_similarity = 0 # Record filtered matches - filtered_match_info12 = MatchInfo(matched_kp1_xy=filtered_matched_kp1_xy, matched_desc1=filterd_matched_desc1, - matches12=good_matches12, matched_kp2_xy=filtered_matched_kp2_xy, - matched_desc2=filterd_matched_desc2, matches21=good_matches21, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type) - - filtered_match_info21 = MatchInfo(matched_kp1_xy=filtered_matched_kp2_xy, matched_desc1=filterd_matched_desc2, - matches12=good_matches21, matched_kp2_xy=filtered_matched_kp1_xy, - matched_desc2=filterd_matched_desc1, matches21=good_matches12, - match_distances=filterd_match_distances, distance=mean_filtered_distance, - similarity=mean_filtered_similarity, metric_name=self.metric_name, - metric_type=self.metric_type) + filtered_match_info12 = MatchInfo( + matched_kp1_xy=filtered_matched_kp1_xy, + matched_desc1=filterd_matched_desc1, + matches12=good_matches12, + matched_kp2_xy=filtered_matched_kp2_xy, + matched_desc2=filterd_matched_desc2, + matches21=good_matches21, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + ) + + filtered_match_info21 = MatchInfo( + matched_kp1_xy=filtered_matched_kp2_xy, + matched_desc1=filterd_matched_desc2, + matches12=good_matches21, + matched_kp2_xy=filtered_matched_kp1_xy, + matched_desc2=filterd_matched_desc1, + matches21=good_matches12, + match_distances=filterd_match_distances, + distance=mean_filtered_distance, + similarity=mean_filtered_similarity, + metric_name=self.metric_name, + metric_type=self.metric_type, + ) return match_info12, filtered_match_info12, match_info21, filtered_match_info21 diff --git a/valis/micro_rigid_registrar.py b/src/valis/micro_rigid_registrar.py similarity index 60% rename from valis/micro_rigid_registrar.py rename to src/valis/micro_rigid_registrar.py index d254a945..3c9a7490 100644 --- a/valis/micro_rigid_registrar.py +++ b/src/valis/micro_rigid_registrar.py @@ -1,3 +1,4 @@ +import logging import torch import kornia @@ -16,6 +17,8 @@ from pqdm.threads import pqdm +logger = logging.getLogger(__name__) + ROI_MASK = "mask" ROI_MATCHES = "matches" @@ -30,6 +33,7 @@ DEFAULT_FLOURESCENCE_CLASS = preprocessing.ChannelGetter DEFAULT_FLOURESCENCE_PROCESSING_ARGS = {"channel": "dapi", "adaptive_eq": True} + class MicroRigidRegistrar(object): """Refine rigid registration using higher resolution images @@ -75,9 +79,16 @@ class MicroRigidRegistrar(object): """ - def __init__(self, val_obj, feature_detector_cls=None, - matcher=DEFAULT_MATCHER, processor_dict=None, - scale=0.5**3, tile_wh=2**9, roi=DEFAULT_ROI): + def __init__( + self, + val_obj, + feature_detector_cls=None, + matcher=DEFAULT_MATCHER, + processor_dict=None, + scale=0.5**3, + tile_wh=2**9, + roi=DEFAULT_ROI, + ): """ Parameters @@ -124,15 +135,16 @@ def __init__(self, val_obj, feature_detector_cls=None, fd = feature_detector_cls if matcher.feature_detector is not None: - msg = (f"Replacing feature detector in {matcher.__class__.__name__} with {fd.__class__.__name__}, " - f"which was previously {matcher.feature_detector.__class__.__name__}. " - f"To avoid this, set `feature_detector_cls=None` when initializing the `Valis` object") - valtils.print_warning(msg, None) + msg = ( + f"Replacing feature detector in {matcher.__class__.__name__} with {fd.__class__.__name__}, " + f"which was previously {matcher.feature_detector.__class__.__name__}. " + f"To avoid this, set `feature_detector_cls=None` when initializing the `Valis` object" + ) + logger.warning(msg) matcher.feature_detector = fd else: fd = None - self.val_obj = val_obj self.feature_detector_cls = feature_detector_cls self.matcher = matcher @@ -140,31 +152,43 @@ def __init__(self, val_obj, feature_detector_cls=None, self.scale = scale self.tile_wh = tile_wh self.roi = roi - self.iter_order = warp_tools.get_alignment_indices(val_obj.size, val_obj.reference_img_idx) + self.iter_order = warp_tools.get_alignment_indices( + val_obj.size, val_obj.reference_img_idx + ) def create_mask(self, moving_slide, fixed_slide): - """Create mask used to define bounding box of search area - - """ + """Create mask used to define bounding box of search area""" pair_slide_list = [moving_slide, fixed_slide] if self.val_obj.create_masks: - temp_mask = self.val_obj._create_mask_from_processed(slide_list=pair_slide_list) + temp_mask = self.val_obj._create_mask_from_processed( + slide_list=pair_slide_list + ) if temp_mask.max() == 0: - temp_mask = self.val_obj._create_non_rigid_reg_mask_from_bbox(slide_list=pair_slide_list) + temp_mask = self.val_obj._create_non_rigid_reg_mask_from_bbox( + slide_list=pair_slide_list + ) else: - temp_mask = self.val_obj._create_non_rigid_reg_mask_from_bbox(slide_list=pair_slide_list) + temp_mask = self.val_obj._create_non_rigid_reg_mask_from_bbox( + slide_list=pair_slide_list + ) fixed_bbox = np.full(fixed_slide.processed_img_shape_rc, 255, dtype=np.uint8) - fixed_mask = fixed_slide.warp_img(fixed_bbox, non_rigid=False, crop=False, interp_method="nearest") + fixed_mask = fixed_slide.warp_img( + fixed_bbox, non_rigid=False, crop=False, interp_method="nearest" + ) mask = preprocessing.combine_masks(temp_mask, fixed_mask, op="and") return mask - def register(self, - brightfield_processing_cls=DEFAULT_BF_PROCESSOR, brightfield_processing_kwargs=DEFAULT_BF_PROCESSOR_KWARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS): + def register( + self, + brightfield_processing_cls=DEFAULT_BF_PROCESSOR, + brightfield_processing_kwargs=DEFAULT_BF_PROCESSOR_KWARGS, + if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, + if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + ): """ Parameters @@ -185,15 +209,24 @@ def register(self, """ - processor_dict = self.val_obj.create_img_processor_dict(brightfield_processing_cls=brightfield_processing_cls, - brightfield_processing_kwargs=brightfield_processing_kwargs, - if_processing_cls=if_processing_cls, - if_processing_kwargs=if_processing_kwargs, - processor_dict=self.processor_dict) + processor_dict = self.val_obj.create_img_processor_dict( + brightfield_processing_cls=brightfield_processing_cls, + brightfield_processing_kwargs=brightfield_processing_kwargs, + if_processing_cls=if_processing_cls, + if_processing_kwargs=if_processing_kwargs, + processor_dict=self.processor_dict, + ) # Get slides in correct order - slide_idx, slide_names = list(zip(*[[slide_obj.stack_idx, slide_obj.name] for slide_obj in self.val_obj.slide_dict.values()])) - slide_order = np.argsort(slide_idx) # sorts ascending + slide_idx, slide_names = list( + zip( + *[ + [slide_obj.stack_idx, slide_obj.name] + for slide_obj in self.val_obj.slide_dict.values() + ] + ) + ) + slide_order = np.argsort(slide_idx) # sorts ascending slide_list = [self.val_obj.slide_dict[slide_names[i]] for i in slide_order] for moving_idx, fixed_idx in self.iter_order: @@ -204,44 +237,59 @@ def register(self, mask = self.create_mask(moving_slide, fixed_slide) - self.align_slides(moving_slide, fixed_slide, processor_dict=processor_dict, mask=mask) + self.align_slides( + moving_slide, fixed_slide, processor_dict=processor_dict, mask=mask + ) def align_slides(self, moving_slide, fixed_slide, processor_dict, mask=None): moving_img = moving_slide.warp_slide(level=0, non_rigid=False, crop=False) moving_img = warp_tools.rescale_img(moving_img, self.scale) moving_shape_rc = warp_tools.get_shape(moving_img)[0:2] - moving_sxy = (moving_shape_rc/moving_slide.reg_img_shape_rc)[::-1] + moving_sxy = (moving_shape_rc / moving_slide.reg_img_shape_rc)[::-1] fixed_img = fixed_slide.warp_slide(0, non_rigid=False, crop=False) fixed_img = warp_tools.rescale_img(fixed_img, self.scale) fixed_shape_rc = warp_tools.get_shape(fixed_img)[0:2] - fixed_sxy = (fixed_shape_rc/fixed_slide.reg_img_shape_rc)[::-1] + fixed_sxy = (fixed_shape_rc / fixed_slide.reg_img_shape_rc)[::-1] # Perform Rigid registration where masks overlap aligned_slide_shape_rc = warp_tools.get_shape(moving_img)[0:2] if self.roi == ROI_MASK: small_reg_bbox = warp_tools.mask2xy(mask) elif self.roi == ROI_MATCHES: - reg_moving_xy = warp_tools.warp_xy(moving_slide.xy_matched_to_prev, moving_slide.M) + reg_moving_xy = warp_tools.warp_xy( + moving_slide.xy_matched_to_prev, moving_slide.M + ) reg_fixed_xy = warp_tools.warp_xy(moving_slide.xy_in_prev, fixed_slide.M) small_reg_bbox = np.vstack([reg_moving_xy, reg_fixed_xy]) - reg_s = (aligned_slide_shape_rc/np.array(mask.shape))[::-1] - reg_bbox = warp_tools.xy2bbox(small_reg_bbox*reg_s) - slide_mask = warp_tools.resize_img(warp_tools.numpy2vips(mask), warp_tools.get_shape(fixed_img)[0:2], interp_method="nearest") + reg_s = (aligned_slide_shape_rc / np.array(mask.shape))[::-1] + reg_bbox = warp_tools.xy2bbox(small_reg_bbox * reg_s) + slide_mask = warp_tools.resize_img( + warp_tools.numpy2vips(mask), + warp_tools.get_shape(fixed_img)[0:2], + interp_method="nearest", + ) # Collect high rez matches bbox_tiles = self.get_tiles(reg_bbox, self.tile_wh) n_tiles = len(bbox_tiles) - high_rez_moving_match_xy_list = [None]*n_tiles - high_rez_fixed_match_xy_list = [None]*n_tiles + high_rez_moving_match_xy_list = [None] * n_tiles + high_rez_fixed_match_xy_list = [None] * n_tiles - moving_processing_cls, moving_processing_kwargs = processor_dict[moving_slide.name] + moving_processing_cls, moving_processing_kwargs = processor_dict[ + moving_slide.name + ] fixed_processing_cls, fixed_processing_kwargs = processor_dict[fixed_slide.name] - match_rgb = self.matcher.feature_detector.rgb and moving_slide.is_rgb and fixed_slide.is_rgb + match_rgb = ( + self.matcher.feature_detector.rgb + and moving_slide.is_rgb + and fixed_slide.is_rgb + ) + def _match_tile(bbox_id): bbox_xy = bbox_tiles[bbox_id] @@ -251,29 +299,32 @@ def _match_tile(bbox_id): return None - moving_region, moving_processed, moving_bbox_xywh = self.process_roi(img=moving_img, - slide_obj=moving_slide, - xy=bbox_xy, - processor_cls=moving_processing_cls, - processor_kwargs=moving_processing_kwargs, - apply_mask=False, - scale=1.0, - return_rgb_only=match_rgb - ) - - fixed_region, fixed_processed, fixed_bbox_xywh = self.process_roi(img=fixed_img, - slide_obj=fixed_slide, - xy=bbox_xy, - processor_cls=fixed_processing_cls, - processor_kwargs=fixed_processing_kwargs, - apply_mask=False, - scale=1.0, - return_rgb_only=match_rgb - ) - + moving_region, moving_processed, moving_bbox_xywh = self.process_roi( + img=moving_img, + slide_obj=moving_slide, + xy=bbox_xy, + processor_cls=moving_processing_cls, + processor_kwargs=moving_processing_kwargs, + apply_mask=False, + scale=1.0, + return_rgb_only=match_rgb, + ) + + fixed_region, fixed_processed, fixed_bbox_xywh = self.process_roi( + img=fixed_img, + slide_obj=fixed_slide, + xy=bbox_xy, + processor_cls=fixed_processing_cls, + processor_kwargs=fixed_processing_kwargs, + apply_mask=False, + scale=1.0, + return_rgb_only=match_rgb, + ) if not match_rgb: - moving_normed, fixed_normed = self.norm_imgs(img_list=[moving_processed, fixed_processed]) + moving_normed, fixed_normed = self.norm_imgs( + img_list=[moving_processed, fixed_processed] + ) else: moving_normed = moving_region @@ -281,11 +332,21 @@ def _match_tile(bbox_id): try: - moving_kp, moving_desc = self.matcher.feature_detector.detect_and_compute(moving_normed) - fixed_kp, fixed_desc = self.matcher.feature_detector.detect_and_compute(fixed_normed) - - _, filtered_match_info12, _, _ = self.matcher.match_images(img1=moving_normed, desc1=moving_desc, kp1_xy=moving_kp, - img2=fixed_normed, desc2=fixed_desc, kp2_xy=fixed_kp) + moving_kp, moving_desc = ( + self.matcher.feature_detector.detect_and_compute(moving_normed) + ) + fixed_kp, fixed_desc = self.matcher.feature_detector.detect_and_compute( + fixed_normed + ) + + _, filtered_match_info12, _, _ = self.matcher.match_images( + img1=moving_normed, + desc1=moving_desc, + kp1_xy=moving_kp, + img2=fixed_normed, + desc2=fixed_desc, + kp2_xy=fixed_kp, + ) filtered_matched_moving_xy = filtered_match_info12.matched_kp1_xy filtered_matched_fixed_xy = filtered_match_info12.matched_kp2_xy @@ -295,14 +356,20 @@ def _match_tile(bbox_id): if filtered_matched_moving_xy.shape[0] < 3: return None - filtered_matched_moving_xy, filtered_matched_fixed_xy, tukey_idx = feature_matcher.filter_matches_tukey(filtered_matched_moving_xy, filtered_matched_fixed_xy, tform=transform.EuclideanTransform()) + filtered_matched_moving_xy, filtered_matched_fixed_xy, tukey_idx = ( + feature_matcher.filter_matches_tukey( + filtered_matched_moving_xy, + filtered_matched_fixed_xy, + tform=transform.EuclideanTransform(), + ) + ) matched_moving_desc = matched_moving_desc[tukey_idx, :] matched_fixed_desc = matched_fixed_desc[tukey_idx, :] if filtered_matched_moving_xy.shape[0] < 3: return None except Exception as e: - valtils.print_warning(f"Error rigidly aligning tile {bbox_id}: {e}") + logger.warning(f"Error rigidly aligning tile {bbox_id}: {e}") return None @@ -316,54 +383,93 @@ def _match_tile(bbox_id): high_rez_moving_match_xy_list[bbox_id] = matched_moving_xy high_rez_fixed_match_xy_list[bbox_id] = matched_fixed_xy - - print(f"Aligning {moving_slide.name} to {fixed_slide.name}. ROI width, height is {reg_bbox[2:]} pixels") - n_cpu = valtils.get_ncpus_available() - 1 + logger.info( + f"Aligning {moving_slide.name} to {fixed_slide.name}. ROI width, height is {reg_bbox[2:]} pixels" + ) + n_cpu = valtils.get_ncpus_available() with suppress(UserWarning): # Avoid printing warnings that not enough matches were found, which can happen frequently with this res = pqdm(range(n_tiles), _match_tile, n_jobs=n_cpu) # Remove tiles that didn't have any matches - high_rez_moving_match_xy_list = [xy for xy in high_rez_moving_match_xy_list if xy is not None] - high_rez_fixed_match_xy_list = [xy for xy in high_rez_fixed_match_xy_list if xy is not None] + high_rez_moving_match_xy_list = [ + xy for xy in high_rez_moving_match_xy_list if xy is not None + ] + high_rez_fixed_match_xy_list = [ + xy for xy in high_rez_fixed_match_xy_list if xy is not None + ] high_rez_moving_match_xy = np.vstack(high_rez_moving_match_xy_list) high_rez_fixed_match_xy = np.vstack(high_rez_fixed_match_xy_list) - temp_high_rez_moving_matched_kp_xy, temp_high_rez_fixed_matched_kp_xy, ransac_idx = feature_matcher.filter_matches_ransac(high_rez_moving_match_xy, high_rez_fixed_match_xy, 20) - high_rez_moving_matched_kp_xy, high_rez_fixed_matched_kp_xy, tukey_idx = feature_matcher.filter_matches_tukey(temp_high_rez_moving_matched_kp_xy, temp_high_rez_fixed_matched_kp_xy, tform=transform.EuclideanTransform()) - scaled_moving_kp = high_rez_moving_matched_kp_xy*(1/moving_sxy) - scaled_fixed_kp = high_rez_fixed_matched_kp_xy*(1/fixed_sxy) + ( + temp_high_rez_moving_matched_kp_xy, + temp_high_rez_fixed_matched_kp_xy, + ransac_idx, + ) = feature_matcher.filter_matches_ransac( + high_rez_moving_match_xy, high_rez_fixed_match_xy, 20 + ) + high_rez_moving_matched_kp_xy, high_rez_fixed_matched_kp_xy, tukey_idx = ( + feature_matcher.filter_matches_tukey( + temp_high_rez_moving_matched_kp_xy, + temp_high_rez_fixed_matched_kp_xy, + tform=transform.EuclideanTransform(), + ) + ) + scaled_moving_kp = high_rez_moving_matched_kp_xy * (1 / moving_sxy) + scaled_fixed_kp = high_rez_fixed_matched_kp_xy * (1 / fixed_sxy) if self.val_obj.create_masks: - moving_kp_in_og = warp_tools.warp_xy(scaled_moving_kp, M=np.linalg.inv(moving_slide.M)) - moving_features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=moving_kp_in_og, mask=moving_slide.rigid_reg_mask) - - fixed_kp_in_og = warp_tools.warp_xy(scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M)) - fixed_features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=fixed_kp_in_og, mask=fixed_slide.rigid_reg_mask) - - if len(moving_features_in_mask_idx) > 0 and len(fixed_features_in_mask_idx) > 0: - matches_in_masks = np.intersect1d(moving_features_in_mask_idx, fixed_features_in_mask_idx) + moving_kp_in_og = warp_tools.warp_xy( + scaled_moving_kp, M=np.linalg.inv(moving_slide.M) + ) + moving_features_in_mask_idx = warp_tools.get_xy_inside_mask( + xy=moving_kp_in_og, mask=moving_slide.rigid_reg_mask + ) + + fixed_kp_in_og = warp_tools.warp_xy( + scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M) + ) + fixed_features_in_mask_idx = warp_tools.get_xy_inside_mask( + xy=fixed_kp_in_og, mask=fixed_slide.rigid_reg_mask + ) + + if ( + len(moving_features_in_mask_idx) > 0 + and len(fixed_features_in_mask_idx) > 0 + ): + matches_in_masks = np.intersect1d( + moving_features_in_mask_idx, fixed_features_in_mask_idx + ) if len(matches_in_masks) > 0: scaled_moving_kp = scaled_moving_kp[matches_in_masks, :] scaled_fixed_kp = scaled_fixed_kp[matches_in_masks, :] - high_rez_moving_matched_kp_xy = high_rez_moving_matched_kp_xy[matches_in_masks, :] - high_rez_fixed_matched_kp_xy = high_rez_fixed_matched_kp_xy[matches_in_masks, :] + high_rez_moving_matched_kp_xy = high_rez_moving_matched_kp_xy[ + matches_in_masks, : + ] + high_rez_fixed_matched_kp_xy = high_rez_fixed_matched_kp_xy[ + matches_in_masks, : + ] # Estimate M using position in larger image transformer = transform.SimilarityTransform() - transformer.estimate(high_rez_fixed_matched_kp_xy, high_rez_moving_matched_kp_xy) + transformer.estimate( + high_rez_fixed_matched_kp_xy, high_rez_moving_matched_kp_xy + ) M = transformer.params # Scale for use on original processed image slide_corners_xy = warp_tools.get_corners_of_image(moving_shape_rc)[::-1] - warped_slide_corners = warp_tools.warp_xy(slide_corners_xy, M=M, - transformation_src_shape_rc=moving_shape_rc, - transformation_dst_shape_rc=fixed_shape_rc, - src_shape_rc=moving_slide.reg_img_shape_rc, - dst_shape_rc=fixed_slide.reg_img_shape_rc) + warped_slide_corners = warp_tools.warp_xy( + slide_corners_xy, + M=M, + transformation_src_shape_rc=moving_shape_rc, + transformation_dst_shape_rc=fixed_shape_rc, + src_shape_rc=moving_slide.reg_img_shape_rc, + dst_shape_rc=fixed_slide.reg_img_shape_rc, + ) M_tform = transform.ProjectiveTransform() M_tform.estimate(warped_slide_corners, slide_corners_xy) @@ -371,16 +477,30 @@ def _match_tile(bbox_id): new_M = moving_slide.M @ scaled_M - matched_moving_in_og = warp_tools.warp_xy(scaled_moving_kp, M=np.linalg.inv(moving_slide.M)) - matched_fixed_in_og = warp_tools.warp_xy(scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M)) - - og_d = np.mean(warp_tools.calc_d(warp_tools.warp_xy(moving_slide.xy_matched_to_prev, M=moving_slide.M), warp_tools.warp_xy(moving_slide.xy_in_prev, fixed_slide.M))) - new_d = np.mean(warp_tools.calc_d(warp_tools.warp_xy(matched_moving_in_og, M=new_M), warp_tools.warp_xy(matched_fixed_in_og, fixed_slide.M))) + matched_moving_in_og = warp_tools.warp_xy( + scaled_moving_kp, M=np.linalg.inv(moving_slide.M) + ) + matched_fixed_in_og = warp_tools.warp_xy( + scaled_fixed_kp, M=np.linalg.inv(fixed_slide.M) + ) + + og_d = np.mean( + warp_tools.calc_d( + warp_tools.warp_xy(moving_slide.xy_matched_to_prev, M=moving_slide.M), + warp_tools.warp_xy(moving_slide.xy_in_prev, fixed_slide.M), + ) + ) + new_d = np.mean( + warp_tools.calc_d( + warp_tools.warp_xy(matched_moving_in_og, M=new_M), + warp_tools.warp_xy(matched_fixed_in_og, fixed_slide.M), + ) + ) n_old_matches = moving_slide.xy_matched_to_prev.shape[0] n_new_matches = high_rez_fixed_matched_kp_xy.shape[0] - improved = (n_new_matches >= n_old_matches) + improved = n_new_matches >= n_old_matches if improved: res_msg = "micro rigid registration improved alignments." msg_clr = Fore.GREEN @@ -389,7 +509,7 @@ def _match_tile(bbox_id): msg_clr = Fore.YELLOW full_res_msg = f"{res_msg} N low rez matches= {n_old_matches}, N high rez matches = {n_new_matches}. Low rez D= {og_d}, high rez D={new_d}" - valtils.print_warning(full_res_msg, rgb=msg_clr) + logger.warning(full_res_msg) if improved: moving_slide.M = new_M @@ -399,22 +519,25 @@ def _match_tile(bbox_id): moving_slide.xy_matched_to_prev_in_bbox = matched_moving_in_og moving_slide.xy_in_prev_in_bbox = matched_fixed_in_og - def get_tiles(self, bbox_xywh, wh): x_step = np.min([wh, np.floor(bbox_xywh[2]).astype(int)]) y_step = np.min([wh, np.floor(bbox_xywh[3]).astype(int)]) - x_pos = np.arange(bbox_xywh[0], bbox_xywh[0]+bbox_xywh[2], x_step) + x_pos = np.arange(bbox_xywh[0], bbox_xywh[0] + bbox_xywh[2], x_step) max_x, max_y = np.round(bbox_xywh[0:2] + bbox_xywh[2:]).astype(int) if x_pos[-1] < max_x - 1: x_pos = np.array([*x_pos, max_x]) - y_pos = np.arange(bbox_xywh[1], bbox_xywh[1]+bbox_xywh[3], y_step) + y_pos = np.arange(bbox_xywh[1], bbox_xywh[1] + bbox_xywh[3], y_step) if y_pos[-1] < max_y - 1: y_pos = np.array([*y_pos, max_y]) - tile_bbox_list = [np.array([[x_pos[i], y_pos[j]], [x_pos[i+1], y_pos[j+1]]]) for j in range(len(y_pos) - 1) for i in range(len(x_pos) - 1)] + tile_bbox_list = [ + np.array([[x_pos[i], y_pos[j]], [x_pos[i + 1], y_pos[j + 1]]]) + for j in range(len(y_pos) - 1) + for i in range(len(x_pos) - 1) + ] return tile_bbox_list @@ -428,11 +551,23 @@ def norm_imgs(self, img_list): except ValueError: processed = img - normed_list[i] = exposure.rescale_intensity(processed, out_range=(0, 255)).astype(np.uint8) + normed_list[i] = exposure.rescale_intensity( + processed, out_range=(0, 255) + ).astype(np.uint8) return normed_list - def process_roi(self, img, slide_obj, xy, processor_cls, processor_kwargs, apply_mask=True, scale=0.5, return_rgb_only=False): + def process_roi( + self, + img, + slide_obj, + xy, + processor_cls, + processor_kwargs, + apply_mask=True, + scale=0.5, + return_rgb_only=False, + ): is_array = isinstance(img, np.ndarray) if is_array: vips_img = warp_tools.numpy2vips(img) @@ -450,7 +585,13 @@ def process_roi(self, img, slide_obj, xy, processor_cls, processor_kwargs, apply if return_rgb_only: return region_np, None, bbox - processor = processor_cls(region_np, src_f=slide_obj.src_f, level=0, series=slide_obj.series, reader=slide_obj.reader) + processor = processor_cls( + region_np, + src_f=slide_obj.src_f, + level=0, + series=slide_obj.series, + reader=slide_obj.reader, + ) processed_img = processor.process_image(**processor_kwargs) if apply_mask: @@ -458,4 +599,3 @@ def process_roi(self, img, slide_obj, xy, processor_cls, processor_kwargs, apply processed_img[mask == 0] = 0 return region_np, processed_img, bbox - diff --git a/valis/non_rigid_registrars.py b/src/valis/non_rigid_registrars.py similarity index 79% rename from valis/non_rigid_registrars.py rename to src/valis/non_rigid_registrars.py index 33e20d9c..7caff973 100644 --- a/valis/non_rigid_registrars.py +++ b/src/valis/non_rigid_registrars.py @@ -1,5 +1,6 @@ -"""Perform non-rigid registration -""" +"""Perform non-rigid registration""" + +import logging import torch import kornia from torchvision.models.optical_flow import raft_large, Raft_Large_Weights @@ -22,6 +23,8 @@ from . import preprocessing from . import valtils +logger = logging.getLogger(__name__) + NR_CLS_KEY = "non_rigid_registrar_cls" NR_PROCESSING_KW_KEY = "processer_kwargs" NR_PROCESSING_INIT_KW_KEY = "init_processer_kwargs" @@ -180,9 +183,9 @@ def create_mask(self): for img in img_list: temp_mask[img > 0] = 255 - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) + mask = warp_tools.bbox2mask( + *warp_tools.xy2bbox(warp_tools.mask2xy(temp_mask)), temp_mask.shape + ) return mask @@ -228,8 +231,9 @@ def register(self, moving_img, fixed_img, mask=None, **kwargs): moving_shape = warp_tools.get_shape(moving_img)[0:2] fixed_shape = warp_tools.get_shape(fixed_img)[0:2] - assert np.all(moving_shape == fixed_shape), \ - print("Images have different shapes") + assert np.all(moving_shape == fixed_shape), logger.error( + "Images have different shapes" + ) self.shape = moving_shape self.moving_img = moving_img @@ -256,9 +260,9 @@ def register(self, moving_img, fixed_img, mask=None, **kwargs): masked_moving = self.moving_img.copy() masked_fixed = self.fixed_img.copy() - bk_dxdy = self.calc(moving_img=masked_moving, - fixed_img=masked_fixed, - mask=mask, **kwargs) + bk_dxdy = self.calc( + moving_img=masked_moving, fixed_img=masked_fixed, mask=mask, **kwargs + ) if mask is not None: bk_dx = np.zeros(self.shape) @@ -299,11 +303,13 @@ def get_grid_image(self, grid_spacing=None, thickness=1, grid_spacing_ratio=0.02 if self.grid_spacing is not None: grid_spacing = int(self.grid_spacing) else: - grid_spacing = np.max(np.array(self.shape)*grid_spacing_ratio).astype(int) + grid_spacing = np.max(np.array(self.shape) * grid_spacing_ratio).astype( + int + ) - grid_r, grid_c = viz.get_grid(self.shape[:2], - grid_spacing=grid_spacing, - thickness=thickness) + grid_r, grid_c = viz.get_grid( + self.shape[:2], grid_spacing=grid_spacing, thickness=thickness + ) grid_img = np.zeros(self.shape[:2]) grid_img[grid_r, grid_c] = 255 @@ -423,8 +429,9 @@ def __init__(self, params=None): self.moving_xy = None self.fixed_xy = None - def register(self, moving_img, fixed_img, mask=None, moving_xy=None, - fixed_xy=None, **kwargs): + def register( + self, moving_img, fixed_img, mask=None, moving_xy=None, fixed_xy=None, **kwargs + ): """Register images, warping moving_img to align with fixed_img Uses backwards transforms to register images (i.e. aligning @@ -468,26 +475,26 @@ def register(self, moving_img, fixed_img, mask=None, moving_xy=None, """ if moving_xy is not None: - moving_xy, fixed_xy = self.filter_xy(moving_xy, fixed_xy, - moving_img.shape, - mask) + moving_xy, fixed_xy = self.filter_xy( + moving_xy, fixed_xy, moving_img.shape, mask + ) self.moving_xy = moving_xy self.fixed_xy = fixed_xy - warped_img, warp_grid, bk_dxdy = \ - NonRigidRegistrar.register(self, moving_img=moving_img, - fixed_img=fixed_img, - mask=mask, - moving_xy=moving_xy, - fixed_xy=fixed_xy, - **kwargs) + warped_img, warp_grid, bk_dxdy = NonRigidRegistrar.register( + self, + moving_img=moving_img, + fixed_img=fixed_img, + mask=mask, + moving_xy=moving_xy, + fixed_xy=fixed_xy, + **kwargs, + ) return warped_img, warp_grid, bk_dxdy def filter_xy(self, moving_xy, fixed_xy, img_shape_rc, mask=None): - """Remove points outside image and/or mask - - """ + """Remove points outside image and/or mask""" if mask is None: mask = np.full(img_shape_rc, 255, dtype=np.uint8) @@ -540,6 +547,7 @@ class NonRigidRegistrarGroupwise(NonRigidRegistrar): Number of images that are being registered as a group """ + def __init__(self, params=None): super().__init__(params=params) self.img_list = None @@ -557,9 +565,9 @@ def create_mask(self): for img in self.img_list: temp_mask[img > 0] = 255 - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) + mask = warp_tools.bbox2mask( + *warp_tools.xy2bbox(warp_tools.mask2xy(temp_mask)), temp_mask.shape + ) return mask def register(self, img_list, mask=None): @@ -592,7 +600,7 @@ def register(self, img_list, mask=None): self.shape = img_list[0].shape for img in img_list: - assert img.shape == self.shape, print("Images have differernt shapes") + assert img.shape == self.shape, logger.error("Images have different shapes") self.img_list = img_list self.size = len(img_list) @@ -636,16 +644,21 @@ def register(self, img_list, mask=None): self.backward_dy = backward_deformations[:, 1] n_imgs = len(self.img_list) - warp_maps = [warp_tools.get_warp_map(dxdy=[self.backward_dx[i], - self.backward_dy[i]]) - for i in range(n_imgs)] + warp_maps = [ + warp_tools.get_warp_map(dxdy=[self.backward_dx[i], self.backward_dy[i]]) + for i in range(n_imgs) + ] - warped_imgs = [transform.warp(img_list[i], warp_maps[i], preserve_range=True) - for i in range(n_imgs)] + warped_imgs = [ + transform.warp(img_list[i], warp_maps[i], preserve_range=True) + for i in range(n_imgs) + ] grid_img = self.get_grid_image(grid_spacing=16) - warped_grids = [transform.warp(grid_img, warp_maps[i], preserve_range=True) - for i in range(n_imgs)] + warped_grids = [ + transform.warp(grid_img, warp_maps[i], preserve_range=True) + for i in range(n_imgs) + ] self.warped_image = warped_imgs self.deformation_field_img = warped_grids @@ -660,8 +673,10 @@ class SimpleElastixWarper(NonRigidRegistrarXY): May optionally using corresponding points """ - def __init__(self, params=None, ammi_weight=0.33, - bending_penalty_weight=0.33, kp_weight=0.33): + + def __init__( + self, params=None, ammi_weight=0.33, bending_penalty_weight=0.33, kp_weight=0.33 + ): """ Parameters ---------- @@ -683,7 +698,6 @@ def __init__(self, params=None, ammi_weight=0.33, self.bending_penalty_weight = bending_penalty_weight self.kp_weight = kp_weight - @staticmethod def get_default_params(img_shape, grid_spacing_ratio=0.025): """ @@ -693,11 +707,14 @@ def get_default_params(img_shape, grid_spacing_ratio=0.025): for advice on parameter selection """ p = sitk.GetDefaultParameterMap("bspline") - p["Metric"] = ['AdvancedMattesMutualInformation', 'TransformBendingEnergyPenalty'] - p["MaximumNumberOfIterations"] = ['1500'] # Can try up to 2000 - p['FixedImagePyramid'] = ["FixedRecursiveImagePyramid"] - p['MovingImagePyramid'] = ["MovingRecursiveImagePyramid"] - p['Interpolator'] = ["BSplineInterpolator"] + p["Metric"] = [ + "AdvancedMattesMutualInformation", + "TransformBendingEnergyPenalty", + ] + p["MaximumNumberOfIterations"] = ["1500"] # Can try up to 2000 + p["FixedImagePyramid"] = ["FixedRecursiveImagePyramid"] + p["MovingImagePyramid"] = ["MovingRecursiveImagePyramid"] + p["Interpolator"] = ["BSplineInterpolator"] p["ImageSampler"] = ["RandomCoordinate"] p["MetricSamplingStrategy"] = ["None"] # Use all points p["UseRandomSampleRegion"] = ["true"] @@ -705,12 +722,12 @@ def get_default_params(img_shape, grid_spacing_ratio=0.025): p["NumberOfHistogramBins"] = ["32"] p["NumberOfSpatialSamples"] = ["3000"] p["NewSamplesEveryIteration"] = ["true"] - p["SampleRegionSize"] = [str(min([img_shape[1]//3, img_shape[0]//3]))] + p["SampleRegionSize"] = [str(min([img_shape[1] // 3, img_shape[0] // 3]))] p["Optimizer"] = ["AdaptiveStochasticGradientDescent"] p["ASGDParameterEstimationMethod"] = ["DisplacementDistribution"] p["HowToCombineTransforms"] = ["Compose"] - grid_spacing_x = img_shape[1]*grid_spacing_ratio - grid_spacing_y = img_shape[0]*grid_spacing_ratio + grid_spacing_x = img_shape[1] * grid_spacing_ratio + grid_spacing_y = img_shape[0] * grid_spacing_ratio grid_spacing = str(int(np.mean([grid_spacing_x, grid_spacing_y]))) p["FinalGridSpacingInPhysicalUnits"] = [grid_spacing] p["WriteResultImage"] = ["false"] @@ -754,7 +771,9 @@ def elastix_invert_transform(registed_elastix_obj, sitk_fixed): inverse_transformationFilter.SetTransformParameterMap(transf_parameter_map) inverse_transformationFilter.ComputeDeformationFieldOn() inverse_transformationFilter.Execute() - inverted_deformationField = sitk.GetArrayFromImage(inverse_transformationFilter.GetDeformationField()) + inverted_deformationField = sitk.GetArrayFromImage( + inverse_transformationFilter.GetDeformationField() + ) return inverted_deformationField @@ -771,16 +790,22 @@ def write_elastix_kp(self, kp, fname): """ - argfile = open(fname, 'w') + argfile = open(fname, "w") npts = kp.shape[0] argfile.writelines(f"index\n{npts}\n") for i in range(npts): xy = kp[i] argfile.writelines(f"{xy[0]} {xy[1]}\n") - def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, - params=None, mask=None): - + def run_elastix( + self, + moving_img, + fixed_img, + moving_xy=None, + fixed_xy=None, + params=None, + mask=None, + ): """Run SimpleElastix to register images. Can using corresponding points to aid in registration by providing @@ -815,10 +840,12 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, if moving_xy is not None and fixed_xy is not None: rand_id = np.random.randint(0, 10000) - fixed_kp_fname = os.path.join(pathlib.Path(__file__).parent, - f".{rand_id}_fixedPointSet.pts") - moving_kp_fname = os.path.join(pathlib.Path(__file__).parent, - f".{rand_id}_.movingPointSet.pts") + fixed_kp_fname = os.path.join( + pathlib.Path(__file__).parent, f".{rand_id}_fixedPointSet.pts" + ) + moving_kp_fname = os.path.join( + pathlib.Path(__file__).parent, f".{rand_id}_.movingPointSet.pts" + ) self.write_elastix_kp(fixed_xy, fixed_kp_fname) self.write_elastix_kp(moving_xy, moving_kp_fname) @@ -828,9 +855,9 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, if not self._params_provided or kp_dist_met not in current_metrics: current_metrics.append(kp_dist_met) params["Metric"] = current_metrics - weights = np.array([self.ammi_weight, - self.bending_penalty_weight, - self.kp_weight]) + weights = np.array( + [self.ammi_weight, self.bending_penalty_weight, self.kp_weight] + ) elastix_image_filter_obj.SetParameterMap(params) elastix_image_filter_obj.SetFixedPointSetFileName(fixed_kp_fname) @@ -843,7 +870,7 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, n_metrics = len(params["Metric"]) n_res = eval(params["NumberOfResolutions"][0]) for r in range(n_metrics): - params[f'Metric{r}Weight'] = [str(weights[r])]*n_res + params[f"Metric{r}Weight"] = [str(weights[r])] * n_res elastix_image_filter_obj.SetParameterMap(params) @@ -854,8 +881,9 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, elastix_image_filter_obj.SetFixedImage(sitk_fixed) if mask is not None: - sitk_mask = sitk.Cast(sitk.GetImageFromArray(mask.astype(np.uint8)), - sitk.sitkUInt8) + sitk_mask = sitk.Cast( + sitk.GetImageFromArray(mask.astype(np.uint8)), sitk.sitkUInt8 + ) elastix_image_filter_obj.SetFixedMask(sitk_mask) @@ -863,10 +891,14 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, # Get deformation field # transformixImageFilter = sitk.TransformixImageFilter() - transformixImageFilter.SetTransformParameterMap(elastix_image_filter_obj.GetTransformParameterMap()) + transformixImageFilter.SetTransformParameterMap( + elastix_image_filter_obj.GetTransformParameterMap() + ) transformixImageFilter.ComputeDeformationFieldOn() transformixImageFilter.Execute() - deformationField = sitk.GetArrayFromImage(transformixImageFilter.GetDeformationField()) + deformationField = sitk.GetArrayFromImage( + transformixImageFilter.GetDeformationField() + ) # Warp image # resultImage = elastix_image_filter_obj.GetResultImage() @@ -886,18 +918,34 @@ def run_elastix(self, moving_img, fixed_img, moving_xy=None, fixed_xy=None, if os.path.exists(moving_kp_fname): os.remove(moving_kp_fname) - tform_files = [f for f in os.listdir(".") - if f.startswith("TransformParameters.") - and f.endswith(".txt")] + tform_files = [ + f + for f in os.listdir(".") + if f.startswith("TransformParameters.") and f.endswith(".txt") + ] if len(tform_files) > 0: for f in tform_files: os.remove(f) - return resultImage, warped_grid, deformationField, elastix_image_filter_obj, transformixImageFilter - - def calc(self, moving_img, fixed_img, mask=None, - moving_xy=None, fixed_xy=None, *args, **kwargs): + return ( + resultImage, + warped_grid, + deformationField, + elastix_image_filter_obj, + transformixImageFilter, + ) + + def calc( + self, + moving_img, + fixed_img, + mask=None, + moving_xy=None, + fixed_xy=None, + *args, + **kwargs, + ): """Perform non-rigid registration using SimpleElastix. Can include corresponding points to help in registration by providing @@ -905,20 +953,27 @@ def calc(self, moving_img, fixed_img, mask=None, """ - assert moving_img.shape == fixed_img.shape,\ - print("Images have different shapes") + assert moving_img.shape == fixed_img.shape, logger.error( + "Images have different shapes" + ) if not self._params_provided: self.params = self.get_default_params(self.moving_img.shape) - warped_img, \ - warped_grid, \ - backward_deformation, \ - backward_elastix_image_filter_obj, \ - backward_transformixImageFilter = \ - self.run_elastix(moving_img, fixed_img, - moving_xy=moving_xy, fixed_xy=fixed_xy, - params=self.params, mask=mask) + ( + warped_img, + warped_grid, + backward_deformation, + backward_elastix_image_filter_obj, + backward_transformixImageFilter, + ) = self.run_elastix( + moving_img, + fixed_img, + moving_xy=moving_xy, + fixed_xy=fixed_xy, + params=self.params, + mask=mask, + ) # Record other params # self.grid_spacing = int(eval(self.params["FinalGridSpacingInPhysicalUnits"][0])) @@ -936,10 +991,17 @@ class OpticalFlowWarper(NonRigidRegistrar): Dense optical flow fields may not be diffeomorphic, and so this class provides options to smooth displacement fields. """ - def __init__(self, params=None, optical_flow_obj=cv2.optflow.createOptFlow_DeepFlow(), - n_grid_pts=50, sigma_ratio=0.005, - paint_size=5000, fold_penalty=1e-6, - smoothing_method=None): + + def __init__( + self, + params=None, + optical_flow_obj=cv2.optflow.createOptFlow_DeepFlow(), + n_grid_pts=50, + sigma_ratio=0.005, + paint_size=5000, + fold_penalty=1e-6, + smoothing_method=None, + ): """ Parameters ---------- @@ -998,33 +1060,38 @@ def __init__(self, params=None, optical_flow_obj=cv2.optflow.createOptFlow_DeepF self.optical_flow_obj = optical_flow_obj def calc(self, moving_img, fixed_img, *args, **kwargs): - if self.method in ['createOptFlow_DenseRLOF', 'createOptFlow_SimpleFlow']: + if self.method in ["createOptFlow_DenseRLOF", "createOptFlow_SimpleFlow"]: if moving_img.ndim == 2: moving_img = color.gray2rgb(moving_img) if fixed_img.ndim == 2: fixed_img = color.gray2rgb(fixed_img) - backward_flow = self.optical_flow_obj.calc(fixed_img, moving_img, - np.zeros(moving_img.shape[0:2])) + backward_flow = self.optical_flow_obj.calc( + fixed_img, moving_img, np.zeros(moving_img.shape[0:2]) + ) backward_flow = np.array([backward_flow[..., 0], backward_flow[..., 1]]) if self.smoothing_method == "gauss": - sigma = self.sigma_ratio*np.max(backward_flow[0].shape) + sigma = self.sigma_ratio * np.max(backward_flow[0].shape) smooth_dx = filters.gaussian(backward_flow[0], sigma=sigma) smooth_dy = filters.gaussian(backward_flow[1], sigma=sigma) backward_flow = np.array([smooth_dx, smooth_dy]) elif self.smoothing_method == "inpaint": - backward_flow = warp_tools.remove_folds_in_dxdy(backward_flow, - n_grid_pts=self.n_grid_pts, - paint_size=self.paint_size, - method=self.smoothing_method) + backward_flow = warp_tools.remove_folds_in_dxdy( + backward_flow, + n_grid_pts=self.n_grid_pts, + paint_size=self.paint_size, + method=self.smoothing_method, + ) elif self.smoothing_method == "regularize": - backward_flow = warp_tools.untangle(backward_flow, - n_grid_pts=self.n_grid_pts, - penalty=self.fold_penalty, - mask=self.mask) + backward_flow = warp_tools.untangle( + backward_flow, + n_grid_pts=self.n_grid_pts, + penalty=self.fold_penalty, + mask=self.mask, + ) return np.array(backward_flow) @@ -1035,7 +1102,17 @@ class RAFTWarper(NonRigidRegistrar): Dense optical flow fields may not be diffeomorphic, and so this class provides options to smooth displacement fields. """ - def __init__(self, weights=Raft_Large_Weights.DEFAULT, transform_method="pad", device=None, rgb=True, quant_img=True, *args, **kwargs): + + def __init__( + self, + weights=Raft_Large_Weights.DEFAULT, + transform_method="pad", + device=None, + rgb=True, + quant_img=True, + *args, + **kwargs, + ): """ Parameters ---------- @@ -1068,10 +1145,10 @@ def prep_img_for_raft(self, img, dim_div=8, method="pad"): """ if img.ndim == 2: - img3d = np.dstack(3*[img]) + img3d = np.dstack(3 * [img]) else: if self.quant_img: - print("quantizing image for non-rigid registration") + logger.info("quantizing image for non-rigid registration") img3d = preprocessing.quantize_image(img) else: img3d = img @@ -1089,7 +1166,7 @@ def prep_img_for_raft(self, img, dim_div=8, method="pad"): tpad = h_pad // 2 bpad = h_pad - tpad - padding = [lpad, tpad, rpad, bpad] # left, top, right and bottom + padding = [lpad, tpad, rpad, bpad] # left, top, right and bottom transformed_img = tv_transforms.Pad(padding)(torch_img) img_transform = padding @@ -1097,8 +1174,10 @@ def prep_img_for_raft(self, img, dim_div=8, method="pad"): # Resize image out_h = h + h_pad out_w = w + w_pad - transformed_img = tv_transforms.Resize(size=[out_h, out_w], antialias=False)(torch_img) - img_transform = np.array([out_w/w, out_h/h]) # Scaling in x, y + transformed_img = tv_transforms.Resize( + size=[out_h, out_w], antialias=False + )(torch_img) + img_transform = np.array([out_w / w, out_h / h]) # Scaling in x, y transformed_img_h, transformed_img_w = transformed_img.shape[-2:] assert transformed_img_h % dim_div == 0 & transformed_img_w % dim_div == 0 @@ -1106,23 +1185,34 @@ def prep_img_for_raft(self, img, dim_div=8, method="pad"): return transformed_img, img_transform def calc(self, moving_img, fixed_img, *args, **kwargs): - transformed_moving_img, moving_transform = self.prep_img_for_raft(moving_img, method=self.transform_method) - transformed_fixed_img, fixed_transform = self.prep_img_for_raft(fixed_img, method=self.transform_method) - - transformed_moving_img, transformed_fixed_img = self.weights.transforms()(transformed_moving_img, transformed_fixed_img) - - list_of_flows = self.model(transformed_fixed_img.to(self.device), transformed_moving_img.to(self.device)) + transformed_moving_img, moving_transform = self.prep_img_for_raft( + moving_img, method=self.transform_method + ) + transformed_fixed_img, fixed_transform = self.prep_img_for_raft( + fixed_img, method=self.transform_method + ) + + transformed_moving_img, transformed_fixed_img = self.weights.transforms()( + transformed_moving_img, transformed_fixed_img + ) + + list_of_flows = self.model( + transformed_fixed_img.to(self.device), + transformed_moving_img.to(self.device), + ) dxdy = list_of_flows[-1].squeeze(0).detach().numpy() if self.transform_method == "pad" and len(moving_transform) == 4: # Remove padding lp, tp, rp, bp = moving_transform - cropped_dx = dxdy[0][tp:dxdy.shape[1]-bp, lp:dxdy.shape[2]-rp] - cropped_dy = dxdy[1][tp:dxdy.shape[1]-bp, lp:dxdy.shape[2]-rp] + cropped_dx = dxdy[0][tp : dxdy.shape[1] - bp, lp : dxdy.shape[2] - rp] + cropped_dy = dxdy[1][tp : dxdy.shape[1] - bp, lp : dxdy.shape[2] - rp] backward_flow = np.array([cropped_dx, cropped_dy]) elif len(moving_transform) == 2: # Resize dxdy - scaled_dxdy = warp_tools.scale_dxdy(dxdy, out_shape_rc=moving_img.shape[0:2]) + scaled_dxdy = warp_tools.scale_dxdy( + dxdy, out_shape_rc=moving_img.shape[0:2] + ) if not isinstance(scaled_dxdy, np.ndarray): scaled_dxdy = warp_tools.vips2numpy(scaled_dxdy) @@ -1181,10 +1271,10 @@ def get_default_params(img_shape, grid_spacing_ratio=0.025): See https://simpleelastix.readthedocs.io/Introduction.html for advice on parameter selection """ p = sitk.GetDefaultParameterMap("groupwise") - p["Metric"] = ['AdvancedMattesMutualInformation'] - p["MaximumNumberOfIterations"] = ['1500'] # Can try up to 2000 - p['FixedImagePyramid'] = ["FixedRecursiveImagePyramid"] - p['MovingImagePyramid'] = ["MovingRecursiveImagePyramid"] + p["Metric"] = ["AdvancedMattesMutualInformation"] + p["MaximumNumberOfIterations"] = ["1500"] # Can try up to 2000 + p["FixedImagePyramid"] = ["FixedRecursiveImagePyramid"] + p["MovingImagePyramid"] = ["MovingRecursiveImagePyramid"] p["ImageSampler"] = ["RandomCoordinate"] p["MetricSamplingStrategy"] = ["None"] # Use all points p["UseRandomSampleRegion"] = ["true"] @@ -1194,8 +1284,8 @@ def get_default_params(img_shape, grid_spacing_ratio=0.025): p["Optimizer"] = ["AdaptiveStochasticGradientDescent"] p["ASGDParameterEstimationMethod"] = ["DisplacementDistribution"] p["HowToCombineTransforms"] = ["Compose"] - grid_spacing_x = img_shape[1]*grid_spacing_ratio - grid_spacing_y = img_shape[0]*grid_spacing_ratio + grid_spacing_x = img_shape[1] * grid_spacing_ratio + grid_spacing_y = img_shape[0] * grid_spacing_ratio grid_spacing = str(int(np.mean([grid_spacing_x, grid_spacing_y]))) p["FinalGridSpacingInPhysicalUnits"] = [grid_spacing] p["WriteResultImage"] = ["false"] @@ -1204,7 +1294,9 @@ def get_default_params(img_shape, grid_spacing_ratio=0.025): def calc(self, img_list, mask=None, *args, **kwargs): if self.params is None: - self.params = SimpleElastixGroupwiseWarper.get_default_params(self.img_list[0].shape[:2]) + self.params = SimpleElastixGroupwiseWarper.get_default_params( + self.img_list[0].shape[:2] + ) vectorOfImages = sitk.VectorOfImage() for img in img_list: @@ -1231,11 +1323,15 @@ def calc(self, img_list, mask=None, *args, **kwargs): # Get deformation fields # transformixImageFilter = sitk.TransformixImageFilter() - transformixImageFilter.SetTransformParameterMap(elastix_image_filter_obj.GetTransformParameterMap()) + transformixImageFilter.SetTransformParameterMap( + elastix_image_filter_obj.GetTransformParameterMap() + ) transformixImageFilter.SetMovingImage(image) transformixImageFilter.ComputeDeformationFieldOn() transformixImageFilter.Execute() - deformationField = sitk.GetArrayFromImage(transformixImageFilter.GetDeformationField())[..., 0:2] + deformationField = sitk.GetArrayFromImage( + transformixImageFilter.GetDeformationField() + )[..., 0:2] # Get deformation grid # grid_spacing = int(eval(self.params["FinalGridSpacingInPhysicalUnits"][0])) @@ -1253,17 +1349,22 @@ def calc(self, img_list, mask=None, *args, **kwargs): transformixImageFilter.Execute() warped_grid = sitk.GetArrayFromImage(transformixImageFilter.GetResultImage()) - tform_files = [f for f in os.listdir(".") - if f.startswith("TransformParameters.") - and f.endswith(".txt")] + tform_files = [ + f + for f in os.listdir(".") + if f.startswith("TransformParameters.") and f.endswith(".txt") + ] if len(tform_files) > 0: for f in tform_files: os.remove(f) - deformationField = np.array([[deformationField[i][..., 0], - deformationField[i][..., 1]] - for i in range(len(deformationField))]) + deformationField = np.array( + [ + [deformationField[i][..., 0], deformationField[i][..., 1]] + for i in range(len(deformationField)) + ] + ) return deformationField @@ -1339,24 +1440,40 @@ def __init__(self, params=None, tile_wh=512, tile_buffer=100): self.fwd_dxdy = None def norm_img(self, img, stats, mask=None): - normed_img = exposure.rescale_intensity(img, out_range=(0, 255)).astype(np.uint8) - normed_img = preprocessing.norm_img_stats(img=normed_img, target_stats=stats, mask=mask) - normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype(np.uint8) + normed_img = exposure.rescale_intensity(img, out_range=(0, 255)).astype( + np.uint8 + ) + normed_img = preprocessing.norm_img_stats( + img=normed_img, target_stats=stats, mask=mask + ) + normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype( + np.uint8 + ) return normed_img def norm_tiles(self, moving_img, fixed_img, tile_mask): try: - _, target_processing_stats = preprocessing.collect_img_stats([fixed_img, moving_img]) - fixed_normed = self.norm_img(img=fixed_img, stats=target_processing_stats, mask=tile_mask) - moving_normed = self.norm_img(moving_img, target_processing_stats, tile_mask) + _, target_processing_stats = preprocessing.collect_img_stats( + [fixed_img, moving_img] + ) + fixed_normed = self.norm_img( + img=fixed_img, stats=target_processing_stats, mask=tile_mask + ) + moving_normed = self.norm_img( + moving_img, target_processing_stats, tile_mask + ) except ValueError: # Norm using full image's stats if self.target_stats is not None: try: - fixed_normed = self.norm_img(fixed_img, self.target_stats, tile_mask) - moving_normed = self.norm_img(moving_img, self.target_stats, tile_mask) + fixed_normed = self.norm_img( + fixed_img, self.target_stats, tile_mask + ) + moving_normed = self.norm_img( + moving_img, self.target_stats, tile_mask + ) except ValueError: fixed_normed = fixed_img moving_normed = moving_img @@ -1366,13 +1483,14 @@ def norm_tiles(self, moving_img, fixed_img, tile_mask): return moving_normed, fixed_normed - def process_tile(self, img, img_processer_cls, processer_init_kwargs={}, processer_kwargs={}): - """Process tiles - """ + def process_tile( + self, img, img_processer_cls, processer_init_kwargs={}, processer_kwargs={} + ): + """Process tiles""" processer_init_kwargs["image"] = img - processer_init_kwargs['reader'] = deepcopy(processer_init_kwargs["reader"]) - processer_init_kwargs['level'] = 0 + processer_init_kwargs["reader"] = deepcopy(processer_init_kwargs["reader"]) + processer_init_kwargs["level"] = 0 processer = img_processer_cls(**processer_init_kwargs) try: processed_img = processer.process_image(**processer_kwargs) @@ -1402,21 +1520,29 @@ def reg_tile(self, tile_idx, lock): if moving_tile.interpretation == "srgb": # Limit registration to be inside image # Warped areas outside image have the same pixel values, usually 0 - edge_mask = 255*((np_moving.min(axis=2) != np_moving.max(axis=2)) & (np_fixed.min(axis=2) != np_fixed.max(axis=2))).astype(np.uint8) + edge_mask = 255 * ( + (np_moving.min(axis=2) != np_moving.max(axis=2)) + & (np_fixed.min(axis=2) != np_fixed.max(axis=2)) + ).astype(np.uint8) if np_mask is not None: - np_mask = 255*((edge_mask > 0) & (np_mask > 0)).astype(np.uint8) + np_mask = 255 * ((edge_mask > 0) & (np_mask > 0)).astype(np.uint8) else: np_mask = edge_mask # Check if either of the tiles are empty - is_empty = fixed_tile.max() == fixed_tile.min() or moving_tile.max() == moving_tile.min() + is_empty = ( + fixed_tile.max() == fixed_tile.min() + or moving_tile.max() == moving_tile.min() + ) if np_mask is not None: is_empty = is_empty or np_mask.max() == 0 if is_empty: # Nothing to register - empty_dxdy = pyvips.Image.black(moving_tile.width, moving_tile.height, bands=2).cast("float") + empty_dxdy = pyvips.Image.black( + moving_tile.width, moving_tile.height, bands=2 + ).cast("float") self.bk_dxdy_tiles[tile_idx] = empty_dxdy self.fwd_dxdy_tiles[tile_idx] = empty_dxdy @@ -1424,10 +1550,12 @@ def reg_tile(self, tile_idx, lock): # Process tiles if self.moving_processer_cls is not None: - moving_processed = self.process_tile(img=np_moving, - img_processer_cls=self.moving_processer_cls, - processer_init_kwargs=self.moving_processer_init_kwargs, - processer_kwargs=self.moving_processer_kwargs) + moving_processed = self.process_tile( + img=np_moving, + img_processer_cls=self.moving_processer_cls, + processer_init_kwargs=self.moving_processer_init_kwargs, + processer_kwargs=self.moving_processer_kwargs, + ) else: if np_moving.ndim > 2: @@ -1437,10 +1565,12 @@ def reg_tile(self, tile_idx, lock): moving_processed = np_moving if self.fixed_processer_cls is not None: - fixed_processed = self.process_tile(img=np_fixed, - img_processer_cls=self.fixed_processer_cls, - processer_init_kwargs=self.fixed_processer_init_kwargs, - processer_kwargs=self.fixed_processer_kwargs) + fixed_processed = self.process_tile( + img=np_fixed, + img_processer_cls=self.fixed_processer_cls, + processer_init_kwargs=self.fixed_processer_init_kwargs, + processer_kwargs=self.fixed_processer_kwargs, + ) else: if np_fixed.ndim > 2: fixed_g = np.abs(1 - skcolor.rgb2gray(np_fixed)) @@ -1448,7 +1578,9 @@ def reg_tile(self, tile_idx, lock): else: fixed_processed = np_fixed - moving_normed, fixed_normed = self.norm_tiles(moving_processed, fixed_processed, np_mask) + moving_normed, fixed_normed = self.norm_tiles( + moving_processed, fixed_processed, np_mask + ) if inspect.isclass(self.non_rigid_registrar_cls): # Need to instantiate object @@ -1459,8 +1591,12 @@ def reg_tile(self, tile_idx, lock): _, _, bk_dxdy = tile_non_rigid_reg_obj.register(moving_normed, fixed_normed) fwd_dxdy = warp_tools.get_inverse_field(bk_dxdy) - vips_tile_bk_dxdy = warp_tools.numpy2vips(np.dstack(bk_dxdy).astype(np.float32)) - vips_tile_fwd_dxdy = warp_tools.numpy2vips(np.dstack(fwd_dxdy).astype(np.float32)) + vips_tile_bk_dxdy = warp_tools.numpy2vips( + np.dstack(bk_dxdy).astype(np.float32) + ) + vips_tile_fwd_dxdy = warp_tools.numpy2vips( + np.dstack(fwd_dxdy).astype(np.float32) + ) self.bk_dxdy_tiles[tile_idx] = vips_tile_bk_dxdy self.fwd_dxdy_tiles[tile_idx] = vips_tile_fwd_dxdy @@ -1470,23 +1606,53 @@ def calc(self, *args, **kwargs): Each tile is registered and then stitched together """ - print("======== Registering tiles\n") + logger.info("======== Registering tiles") - n_cpu = valtils.get_ncpus_available() - 1 + n_cpu = valtils.get_ncpus_available() lock = multiprocessing.Lock() - args = [{"tile_idx":i, "lock":lock} for i in range(self.n_tiles)] - res = pqdm(args, self.reg_tile, n_jobs=n_cpu, unit="image", leave=None, argument_type='kwargs') - - bk_dxdy = warp_tools.stitch_tiles(self.bk_dxdy_tiles, self.expanded_bboxes, self.n_rows, self.n_cols, self.tile_buffer) - fwd_dxdy = warp_tools.stitch_tiles(self.fwd_dxdy_tiles, self.expanded_bboxes, self.n_rows, self.n_cols, self.tile_buffer) + args = [{"tile_idx": i, "lock": lock} for i in range(self.n_tiles)] + res = pqdm( + args, + self.reg_tile, + n_jobs=n_cpu, + unit="image", + leave=None, + argument_type="kwargs", + ) + + bk_dxdy = warp_tools.stitch_tiles( + self.bk_dxdy_tiles, + self.expanded_bboxes, + self.n_rows, + self.n_cols, + self.tile_buffer, + ) + fwd_dxdy = warp_tools.stitch_tiles( + self.fwd_dxdy_tiles, + self.expanded_bboxes, + self.n_rows, + self.n_cols, + self.tile_buffer, + ) return bk_dxdy, fwd_dxdy - def register(self, moving_img, fixed_img, mask=None, non_rigid_registrar_cls=OpticalFlowWarper, - moving_processer_cls=None, moving_init_processer_kwargs={}, moving_processer_kwargs=None, - fixed_processer_cls=None, fixed_init_processer_kwargs={}, fixed_processer_kwargs=None, - target_stats=None, **kwargs): + def register( + self, + moving_img, + fixed_img, + mask=None, + non_rigid_registrar_cls=OpticalFlowWarper, + moving_processer_cls=None, + moving_init_processer_kwargs={}, + moving_processer_kwargs=None, + fixed_processer_cls=None, + fixed_init_processer_kwargs={}, + fixed_processer_kwargs=None, + target_stats=None, + **kwargs, + ): """ Register images, warping moving_img to align with fixed_img @@ -1577,8 +1743,15 @@ def register(self, moving_img, fixed_img, mask=None, non_rigid_registrar_cls=Opt self.fixed_img = fixed_img self.mask = mask - temp_tile_bboxes = warp_tools.get_grid_bboxes(self.shape, self.tile_wh, self.tile_wh, inclusive=True) - self.expanded_bboxes = np.array([warp_tools.expand_bbox(bbox_xywh, self.tile_buffer, self.shape) for bbox_xywh in temp_tile_bboxes]) + temp_tile_bboxes = warp_tools.get_grid_bboxes( + self.shape, self.tile_wh, self.tile_wh, inclusive=True + ) + self.expanded_bboxes = np.array( + [ + warp_tools.expand_bbox(bbox_xywh, self.tile_buffer, self.shape) + for bbox_xywh in temp_tile_bboxes + ] + ) self.n_tiles = len(temp_tile_bboxes) self.bk_dxdy_tiles = [None] * self.n_tiles @@ -1599,5 +1772,3 @@ def register(self, moving_img, fixed_img, mask=None, non_rigid_registrar_cls=Opt self.fwd_dxdy = fwd_dxdy return warped_img, fwd_dxdy, bk_dxdy - - diff --git a/src/valis/orientation_check.py b/src/valis/orientation_check.py new file mode 100644 index 00000000..056fef67 --- /dev/null +++ b/src/valis/orientation_check.py @@ -0,0 +1,177 @@ +""" +Cheap pre-alignment check: which of the 8 D4 transforms (4 rotations x +optional mirror) of a moving image best matches a reference image. + +Run this before full registration so the expensive alignment only has to +handle small residual translation/rotation, not a 90 degree rotation or a +flipped scan. +""" + +import logging +from dataclasses import dataclass + +import numpy as np +import cv2 + +logger = logging.getLogger(__name__) + + +D4_TRANSFORMS = ( + ("identity", 0, False), + ("rot90", 1, False), + ("rot180", 2, False), + ("rot270", 3, False), + ("flip", 0, True), + ("flip_rot90", 1, True), + ("flip_rot180", 2, True), + ("flip_rot270", 3, True), +) + + +@dataclass +class OrientationMatch: + name: str + k: int # number of CCW 90-degree rotations + mirror: bool # horizontal flip applied before rotation + score: float # normalized cross-correlation in [-1, 1] + scores: dict # name -> score for all 8 transforms + + +def apply_d4(img: np.ndarray, k: int, mirror: bool) -> np.ndarray: + if mirror: + img = np.fliplr(img) + if k: + img = np.rot90(img, k=k) + return img + + +def apply_d4_pyvips(img, k: int, mirror: bool): + """Apply the same D4 transform as :func:`apply_d4` to a pyvips Image. + + numpy's ``rot90(k=1)`` is counter-clockwise; pyvips' ``rot90`` is + clockwise. The mapping below matches the numpy convention so a + transform discovered on thumbnails applies identically to the + full-resolution pyvips image. + """ + if mirror: + img = img.fliphor() + k = k % 4 + if k == 1: + img = img.rot270() # CCW 90 + elif k == 2: + img = img.rot180() + elif k == 3: + img = img.rot90() # CCW 270 == CW 90 + return img + + +def describe(match: "OrientationMatch") -> str: + """Human-readable description of what would be applied.""" + if match.k == 0 and not match.mirror: + return "identity (no rotation or flip needed)" + parts = [] + if match.mirror: + parts.append("horizontal flip") + if match.k: + parts.append(f"{match.k * 90}deg CCW rotation") + return " + ".join(parts) + + +def _to_gray(img: np.ndarray) -> np.ndarray: + if img.ndim == 3: + img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + return img + + +def _thumbnail(img: np.ndarray, size: int, use_gradient: bool) -> np.ndarray: + """Resize to a (size, size) float32 thumbnail. + + A square thumbnail is required so every D4 transform of the moving image + has the same shape as the reference, even when the originals have + different aspect ratios. The aspect distortion is identical for both + images, so it cancels out in the correlation. + """ + g = _to_gray(img) + g = cv2.resize(g, (size, size), interpolation=cv2.INTER_AREA) + g = g.astype(np.float32) + if use_gradient: + gx = cv2.Sobel(g, cv2.CV_32F, 1, 0, ksize=3) + gy = cv2.Sobel(g, cv2.CV_32F, 0, 1, ksize=3) + g = cv2.magnitude(gx, gy) + g -= g.mean() + n = np.linalg.norm(g) + if n > 0: + g /= n + return g + + +def find_best_orientation( + reference: np.ndarray, + moving: np.ndarray, + downsample_size: int = 128, + use_gradient: bool = True, +) -> OrientationMatch: + """Pick the D4 transform of ``moving`` that best matches ``reference``. + + Parameters + ---------- + reference, moving : np.ndarray + 2D grayscale or HxWx3 RGB arrays. Dimensions need not match; both + are resized to a common square thumbnail. + downsample_size : int + Side length of the square thumbnail used for scoring. 64-256 is the + useful range; larger is more discriminating but slower (cost is + O(downsample_size^2) per transform, x8 transforms). + use_gradient : bool + Score on Sobel gradient magnitude instead of raw intensity. More + robust to brightness/stain differences between modalities; turn off + if the two images are known to be intensity-comparable. + """ + if downsample_size < 8: + raise ValueError("downsample_size must be >= 8") + + # Don't upsample: cap at the smaller side of the smaller input. Going + # beyond that fabricates pixels and can't add real signal. + max_useful = min( + reference.shape[0], reference.shape[1], moving.shape[0], moving.shape[1] + ) + effective = min(downsample_size, max_useful) + if effective != downsample_size: + logger.info( + "orientation check: clamping downsample_size %d -> %d " + "(max useful given ref %dx%d and moving %dx%d)", + downsample_size, + effective, + reference.shape[1], + reference.shape[0], + moving.shape[1], + moving.shape[0], + ) + downsample_size = effective + + ref_thumb = _thumbnail(reference, downsample_size, use_gradient) + + # Build the moving thumbnail once, then apply D4 to the thumbnail. Since + # D4 commutes with the (square) resize, this matches "transform then + # downsample" while doing the expensive resize a single time. + mov_thumb = _thumbnail(moving, downsample_size, use_gradient) + + scores: dict[str, float] = {} + best = None + for name, k, mirror in D4_TRANSFORMS: + candidate = apply_d4(mov_thumb, k, mirror) + # Both arrays are zero-mean and unit-norm, so dot product == NCC. + score = float(np.tensordot(ref_thumb, candidate, axes=2)) + scores[name] = score + if best is None or score > best[3]: + best = (name, k, mirror, score) + + name, k, mirror, score = best + logger.info( + "orientation check: best=%s score=%.4f (size=%d, gradient=%s)", + name, + score, + downsample_size, + use_gradient, + ) + return OrientationMatch(name=name, k=k, mirror=mirror, score=score, scores=scores) diff --git a/valis/preprocessing.py b/src/valis/preprocessing.py similarity index 78% rename from valis/preprocessing.py rename to src/valis/preprocessing.py index 00ad13c9..01708581 100644 --- a/valis/preprocessing.py +++ b/src/valis/preprocessing.py @@ -1,6 +1,8 @@ """ Collection of pre-processing methods for aligning images """ + +import logging import torch import kornia @@ -27,8 +29,10 @@ from . import slide_io from . import warp_tools +logger = logging.getLogger(__name__) + # DEFAULT_COLOR_STD_C = 0.01 # jzazbz -DEFAULT_COLOR_STD_C = 0.2 # cam16-ucs +DEFAULT_COLOR_STD_C = 0.2 # cam16-ucs class ImageProcesser(object): @@ -95,15 +99,19 @@ def __init__(self, image, src_f, level, series, reader=None): if self.reader.metadata.is_rgb and self.image.dtype != np.uint8: self.image = exposure.rescale_intensity(self.image, out_range=np.uint8) - self.original_shape_rc = warp_tools.get_shape(image)[0:2] # Size of image passed into processor - self.uncropped_shape_rc = None # Size of uncropped image (bigger than `original_shape_rc`) - self.crop_bbox = None # bbox (x, y, w, h) of cropped area + self.original_shape_rc = warp_tools.get_shape(image)[ + 0:2 + ] # Size of image passed into processor + self.uncropped_shape_rc = ( + None # Size of uncropped image (bigger than `original_shape_rc`) + ) + self.crop_bbox = None # bbox (x, y, w, h) of cropped area self.cropped = False def create_mask(self): return np.full(self.image.shape[0:2], 255, dtype=np.uint8) - def process_image(self, *args, **kwargs): + def process_image(self, *args, **kwargs): """Pre-process image for registration Pre-process image for registration. Processed image should @@ -118,22 +126,25 @@ def process_image(self, *args, **kwargs): class ChannelGetter(ImageProcesser): - """Select channel from image - - """ + """Select channel from image""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_multichannel(self.image) return tissue_mask - def process_image(self, channel="dapi", adaptive_eq=True, invert=False, *args, **kwaargs): + def process_image( + self, channel="dapi", adaptive_eq=True, invert=False, *args, **kwaargs + ): if self.image is None: - chnl = self.reader.get_channel(channel=channel, level=self.level, series=self.series).astype(float) + chnl = self.reader.get_channel( + channel=channel, level=self.level, series=self.series + ).astype(float) else: if self.image.ndim == 2: # the image is already the channel @@ -143,10 +154,14 @@ def process_image(self, channel="dapi", adaptive_eq=True, invert=False, *args, * chnl = self.image[..., chnl_idx] if adaptive_eq: - chnl = exposure.rescale_intensity(chnl, in_range="image", out_range=(0.0, 1.0)) + chnl = exposure.rescale_intensity( + chnl, in_range="image", out_range=(0.0, 1.0) + ) chnl = exposure.equalize_adapthist(chnl) - chnl = exposure.rescale_intensity(chnl, in_range="image", out_range=(0, 255)).astype(np.uint8) + chnl = exposure.rescale_intensity( + chnl, in_range="image", out_range=(0, 255) + ).astype(np.uint8) if invert: chnl = util.invert(chnl) @@ -154,20 +169,21 @@ def process_image(self, channel="dapi", adaptive_eq=True, invert=False, *args, * class ColorfulStandardizer(ImageProcesser): - """Standardize the colorfulness of the image - - """ + """Standardize the colorfulness of the image""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_rgb(self.image) return tissue_mask - def process_image(self, c=DEFAULT_COLOR_STD_C, invert=True, adaptive_eq=False, *args, **kwargs): + def process_image( + self, c=DEFAULT_COLOR_STD_C, invert=True, adaptive_eq=False, *args, **kwargs + ): std_rgb = standardize_colorfulness(self.image, c) std_g = skcolor.rgb2gray(std_rgb) @@ -175,17 +191,20 @@ def process_image(self, c=DEFAULT_COLOR_STD_C, invert=True, adaptive_eq=False, * std_g = 255 - std_g if adaptive_eq: - std_g = exposure.equalize_adapthist(std_g/255) + std_g = exposure.equalize_adapthist(std_g / 255) - processed_img = exposure.rescale_intensity(std_g, in_range="image", out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + std_g, in_range="image", out_range=(0, 255) + ).astype(np.uint8) return processed_img class JCDist(ImageProcesser): def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): @@ -193,14 +212,18 @@ def create_mask(self): return mask - def process_image(self, p=99, metric="euclidean", adaptive_eq=True, *args, **kwargs): + def process_image( + self, p=99, metric="euclidean", adaptive_eq=True, *args, **kwargs + ): """ Calculate color distance """ jcd = jc_dist(self.image, metric=metric, p=p) if adaptive_eq: - jcd = exposure.equalize_adapthist(exposure.rescale_intensity(jcd, out_range=(0, 1))) + jcd = exposure.equalize_adapthist( + exposure.rescale_intensity(jcd, out_range=(0, 1)) + ) processed = exposure.rescale_intensity(jcd, out_range=np.uint8) @@ -208,14 +231,12 @@ def process_image(self, p=99, metric="euclidean", adaptive_eq=True, *args, **kwa class OD(ImageProcesser): - """Convert the image from RGB to optical density (OD) and calculate pixel norms. - """ + """Convert the image from RGB to optical density (OD) and calculate pixel norms.""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - - + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): @@ -229,7 +250,7 @@ def process_image(self, adaptive_eq=False, p=95, *args, **kwargs): Calculate norm of the OD image """ eps = np.finfo("float").eps - img01 = self.image/255 + img01 = self.image / 255 od = -np.log10(img01 + eps) od_norm = np.mean(od, axis=2) upper_p = np.percentile(od_norm, p) @@ -237,7 +258,9 @@ def process_image(self, adaptive_eq=False, p=95, *args, **kwargs): od_clipped = np.clip(od_norm, lower_p, upper_p) if adaptive_eq: - od_clipped = exposure.equalize_adapthist(exposure.rescale_intensity(od_clipped, out_range=(0, 1))) + od_clipped = exposure.equalize_adapthist( + exposure.rescale_intensity(od_clipped, out_range=(0, 1)) + ) processed = exposure.rescale_intensity(od_clipped, out_range=np.uint8) @@ -246,8 +269,9 @@ def process_image(self, adaptive_eq=False, p=95, *args, **kwargs): class ColorDeconvolver(ImageProcesser): def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): @@ -255,7 +279,15 @@ def create_mask(self): return mask - def process_image(self, cspace="JzAzBz", method="similarity", adaptive_eq=False, return_unmixed=False, *args, **kwargs): + def process_image( + self, + cspace="JzAzBz", + method="similarity", + adaptive_eq=False, + return_unmixed=False, + *args, + **kwargs, + ): """ Process image by enhance stained pixels and subtracting backroung pixels @@ -277,12 +309,16 @@ def process_image(self, cspace="JzAzBz", method="similarity", adaptive_eq=False, """ - unmixed_img, img_colors, fg_color_mask, color_counts = separate_colors(self.image, cspace=cspace, hue_only=False, method=method, min_colorfulness=0) + unmixed_img, img_colors, fg_color_mask, color_counts = separate_colors( + self.image, cspace=cspace, hue_only=False, method=method, min_colorfulness=0 + ) main_colors_jab = rgb2jab(img_colors, cspace=cspace) - main_jab01 = (main_colors_jab - main_colors_jab.min(axis=0))/(main_colors_jab.max(axis=0) - main_colors_jab.min(axis=0)) - bg_jab_idx = np.argmax(main_colors_jab[..., 0]) # BG is brightest + main_jab01 = (main_colors_jab - main_colors_jab.min(axis=0)) / ( + main_colors_jab.max(axis=0) - main_colors_jab.min(axis=0) + ) + bg_jab_idx = np.argmax(main_colors_jab[..., 0]) # BG is brightest bg_jab_01 = main_jab01[bg_jab_idx] color_weights = spatial.distance.cdist(main_jab01, [bg_jab_01]).T[0] @@ -298,10 +334,12 @@ def process_image(self, cspace="JzAzBz", method="similarity", adaptive_eq=False, bg_norm = np.linalg.norm(bg_stains, axis=2) bg_norm = exposure.rescale_intensity(bg_norm, out_range=(0, 1)) - processed = fg_norm*(1-bg_norm) + processed = fg_norm * (1 - bg_norm) if adaptive_eq: - processed = exposure.equalize_adapthist(exposure.rescale_intensity(processed, out_range=(0, 1))) + processed = exposure.equalize_adapthist( + exposure.rescale_intensity(processed, out_range=(0, 1)) + ) processed = exposure.rescale_intensity(processed, out_range=np.uint8) @@ -313,70 +351,78 @@ def process_image(self, cspace="JzAzBz", method="similarity", adaptive_eq=False, class Luminosity(ImageProcesser): - """Get luminosity of an RGB image - - """ + """Get luminosity of an RGB image""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_rgb(self.image) return tissue_mask - def process_image(self, *args, **kwaargs): + def process_image(self, *args, **kwaargs): lum = get_luminosity(self.image) inv_lum = 255 - lum - processed_img = exposure.rescale_intensity(inv_lum, in_range="image", out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + inv_lum, in_range="image", out_range=(0, 255) + ).astype(np.uint8) return processed_img class BgColorDistance(ImageProcesser): - """Calculate distance between each pixel and the background color - - """ + """Calculate distance between each pixel and the background color""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_rgb(self.image) return tissue_mask - def process_image(self, brightness_q=0.99, *args, **kwargs): + def process_image(self, brightness_q=0.99, *args, **kwargs): - processed_img, _ = calc_background_color_dist(self.image, brightness_q=brightness_q) - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 1)) + processed_img, _ = calc_background_color_dist( + self.image, brightness_q=brightness_q + ) + processed_img = exposure.rescale_intensity( + processed_img, in_range="image", out_range=(0, 1) + ) processed_img = exposure.equalize_adapthist(processed_img) - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + processed_img, in_range="image", out_range=(0, 255) + ).astype(np.uint8) return processed_img class StainFlattener(ImageProcesser): def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) self.n_colors = -1 - def create_mask(self): processed = self.process_image(adaptive_eq=True) # Want to ignore black background - to_thresh_mask = 255*(np.all(self.image > 25, axis=2)).astype(np.uint8) + to_thresh_mask = 255 * (np.all(self.image > 25, axis=2)).astype(np.uint8) low_t, high_t = filters.threshold_multiotsu(processed[to_thresh_mask > 0]) - tissue_mask = 255*filters.apply_hysteresis_threshold(processed, low_t, high_t).astype(np.uint8) + tissue_mask = 255 * filters.apply_hysteresis_threshold( + processed, low_t, high_t + ).astype(np.uint8) - kernel_size=3 + kernel_size = 3 tissue_mask = mask2contours(tissue_mask, kernel_size) return tissue_mask @@ -394,9 +440,9 @@ def process_image_with_mask(self, n_colors=100, q=95, max_colors=100): if n_colors > 0: self.n_colors = n_colors - clusterer = MiniBatchKMeans(n_clusters=n_colors, - reassignment_ratio=0, - n_init=3) + clusterer = MiniBatchKMeans( + n_clusters=n_colors, reassignment_ratio=0, n_init=3 + ) clusterer.fit(x) else: k, clusterer = estimate_k(x, max_k=max_colors) @@ -406,7 +452,7 @@ def process_image_with_mask(self, n_colors=100, q=95, max_colors=100): stain_rgb = jab2rgb(ss.inverse_transform(clusterer.cluster_centers_)) stain_rgb = np.clip(stain_rgb, 0, 1) - stain_rgb = np.vstack([255*stain_rgb, mean_bg_rgb]) + stain_rgb = np.vstack([255 * stain_rgb, mean_bg_rgb]) D = stainmat2decon(stain_rgb) deconvolved = deconvolve_img(self.image, D) @@ -414,7 +460,7 @@ def process_image_with_mask(self, n_colors=100, q=95, max_colors=100): d_flat = deconvolved.reshape(-1, deconvolved.shape[2]) dmax = np.percentile(d_flat, q, axis=0) for i in range(deconvolved.shape[2]): - c_dmax = dmax[i] + eps + c_dmax = dmax[i] + eps deconvolved[..., i] = np.clip(deconvolved[..., i], 0, c_dmax) deconvolved[..., i] /= c_dmax @@ -429,20 +475,20 @@ def process_image_all(self, n_colors=100, q=95, max_colors=100): x = ss.fit_transform(img_to_cluster.reshape(-1, img_to_cluster.shape[2])) if n_colors > 0: self.n_colors = n_colors - clusterer = MiniBatchKMeans(n_clusters=n_colors, - reassignment_ratio=0, - n_init=3) + clusterer = MiniBatchKMeans( + n_clusters=n_colors, reassignment_ratio=0, n_init=3 + ) clusterer.fit(x) else: k, clusterer = estimate_k(x, max_k=max_colors) self.n_colors = k - print(f"estimated {k} colors") + logger.info(f"estimated {k} colors") self.clusterer = clusterer stain_rgb = jab2rgb(ss.inverse_transform(clusterer.cluster_centers_)) stain_rgb = np.clip(stain_rgb, 0, 1) - stain_rgb = 255*stain_rgb + stain_rgb = 255 * stain_rgb stain_rgb = np.clip(stain_rgb, 0, 255) stain_rgb = np.unique(stain_rgb, axis=0) D = stainmat2decon(stain_rgb) @@ -459,7 +505,9 @@ def process_image_all(self, n_colors=100, q=95, max_colors=100): return summary_img - def process_image(self, n_colors=100, q=95, with_mask=True, adaptive_eq=True, max_colors=100): + def process_image( + self, n_colors=100, q=95, with_mask=True, adaptive_eq=True, max_colors=100 + ): """ Parameters ---------- @@ -471,36 +519,42 @@ def process_image(self, n_colors=100, q=95, with_mask=True, adaptive_eq=True, ma If `n_colors = -1`, this value sets the maximum number of color clusters """ if with_mask: - processed_img = self.process_image_with_mask(n_colors=n_colors, q=q, max_colors=max_colors) + processed_img = self.process_image_with_mask( + n_colors=n_colors, q=q, max_colors=max_colors + ) else: - processed_img = self.process_image_all(n_colors=n_colors, q=q, max_colors=max_colors) + processed_img = self.process_image_all( + n_colors=n_colors, q=q, max_colors=max_colors + ) if adaptive_eq: processed_img = exposure.equalize_adapthist(processed_img) - processed_img = exposure.rescale_intensity(processed_img, in_range="image", out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + processed_img, in_range="image", out_range=(0, 255) + ).astype(np.uint8) return processed_img class Gray(ImageProcesser): - """Get luminosity of an RGB image - - """ + """Get luminosity of an RGB image""" def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) - + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_rgb(self.image) return tissue_mask - def process_image(self, *args, **kwaargs): + def process_image(self, *args, **kwaargs): g = skcolor.rgb2gray(self.image) - processed_img = exposure.rescale_intensity(g, in_range="image", out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + g, in_range="image", out_range=(0, 255) + ).astype(np.uint8) return processed_img @@ -516,15 +570,15 @@ class HEDeconvolution(ImageProcesser): """ def __init__(self, image, src_f, level, series, *args, **kwargs): - super().__init__(image=image, src_f=src_f, level=level, - series=series, *args, **kwargs) + super().__init__( + image=image, src_f=src_f, level=level, series=series, *args, **kwargs + ) def create_mask(self): _, tissue_mask = create_tissue_mask_from_rgb(self.image) return tissue_mask - def process_image(self, stain="hem", Io=240, alpha=1, beta=0.15, *args, **kwargs): """ Reference @@ -538,7 +592,12 @@ def process_image(self, stain="hem", Io=240, alpha=1, beta=0.15, *args, **kwargs """ normalized_stains_conc = normalize_he(self.image, Io=Io, alpha=alpha, beta=beta) - processed_img = deconvolution_he(self.image, Io=Io, normalized_concentrations=normalized_stains_conc, stain=stain) + processed_img = deconvolution_he( + self.image, + Io=Io, + normalized_concentrations=normalized_stains_conc, + stain=stain, + ) return processed_img @@ -570,10 +629,16 @@ def remove_rgb_bg_in_j(img, radius=50, cspace="CAM16UCS", algo="rb", ad_eq=False filtered_j = remove_bg(jab[..., 0], radius=radius, invert=True, algo=algo) if ad_eq: - filtered_j = exposure.equalize_adapthist(exposure.rescale_intensity(filtered_j, out_range=(0, 1))) - filtered_j = exposure.rescale_intensity(filtered_j, out_range=(jab[..., 0].min(), jab[..., 0].max())) - - img_no_bg = jab2rgb(np.dstack([filtered_j, jab[..., 1], jab[..., 2]]), cspace=cspace) + filtered_j = exposure.equalize_adapthist( + exposure.rescale_intensity(filtered_j, out_range=(0, 1)) + ) + filtered_j = exposure.rescale_intensity( + filtered_j, out_range=(jab[..., 0].min(), jab[..., 0].max()) + ) + + img_no_bg = jab2rgb( + np.dstack([filtered_j, jab[..., 1], jab[..., 2]]), cspace=cspace + ) img_no_bg = np.clip(img_no_bg, 0, 1) img_no_bg = exposure.rescale_intensity(img_no_bg, out_range=np.uint8) @@ -600,10 +665,11 @@ def remove_bg_in_jch(img, radius=50, cspace="JzAzBz", algo="rb"): filtered_j = remove_bg(jch[..., 0], radius=radius, invert=False, algo=algo) filtered_c = remove_bg(jch[..., 1], radius=radius, invert=False, algo=algo) - img_inverted_no_bg = jch2rgb(np.dstack([filtered_j, filtered_c, jch[..., 2]]), cspace=cspace) + img_inverted_no_bg = jch2rgb( + np.dstack([filtered_j, filtered_c, jch[..., 2]]), cspace=cspace + ) img_no_bg = util.invert(img_inverted_no_bg) - return img_no_bg @@ -616,7 +682,7 @@ def denoise_img(img, mask=None, weight=None): sigma_scale = 400 if weight is None: - weight = sigma/sigma_scale + weight = sigma / sigma_scale denoised_img = restoration.denoise_tv_chambolle(img, weight=weight) denoised_img = exposure.rescale_intensity(denoised_img, out_range="uint8") @@ -649,20 +715,20 @@ def standardize_colorfulness(img, c=DEFAULT_COLOR_STD_C, h=0): eps = np.finfo("float").eps with colour.utilities.suppress_warnings(colour_usage_warnings=True): if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255 + eps, 'sRGB', 'CAM16UCS') + cam = colour.convert(img / 255 + eps, "sRGB", "CAM16UCS") else: - cam = colour.convert(img + eps, 'sRGB', 'CAM16UCS') + cam = colour.convert(img + eps, "sRGB", "CAM16UCS") lum = cam[..., 0] cc = np.full_like(lum, c) hc = np.full_like(lum, h) new_a, new_b = cc * np.cos(hc), cc * np.sin(hc) - new_cam = np.dstack([lum, new_a+eps, new_b+eps]) + new_cam = np.dstack([lum, new_a + eps, new_b + eps]) with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb2 = colour.convert(new_cam, 'CAM16UCS', 'sRGB') + rgb2 = colour.convert(new_cam, "CAM16UCS", "sRGB") rgb2 -= eps - rgb2 = (np.clip(rgb2, 0, 1)*255).astype(np.uint8) + rgb2 = (np.clip(rgb2, 0, 1) * 255).astype(np.uint8) return rgb2 @@ -686,9 +752,9 @@ def get_luminosity(img, **kwargs): with colour.utilities.suppress_warnings(colour_usage_warnings=True): if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255, 'sRGB', 'CAM16UCS') + cam = colour.convert(img / 255, "sRGB", "CAM16UCS") else: - cam = colour.convert(img, 'sRGB', 'CAM16UCS') + cam = colour.convert(img, "sRGB", "CAM16UCS") lum = exposure.rescale_intensity(cam[..., 0], in_range=(0, 1), out_range=(0, 255)) @@ -714,9 +780,9 @@ def calc_background_color_dist(img, brightness_q=0.99, mask=None, cspace="CAM16U eps = np.finfo("float").eps with colour.utilities.suppress_warnings(colour_usage_warnings=True): if 1 < img.max() <= 255 and np.issubdtype(img.dtype, np.integer): - cam = colour.convert(img/255 + eps, 'sRGB', cspace) + cam = colour.convert(img / 255 + eps, "sRGB", cspace) else: - cam = colour.convert(img + eps, 'sRGB', cspace) + cam = colour.convert(img + eps, "sRGB", cspace) if mask is None: brightest_thresh = np.quantile(cam[..., 0], brightness_q) @@ -726,13 +792,13 @@ def calc_background_color_dist(img, brightness_q=0.99, mask=None, cspace="CAM16U brightest_idx = np.where(cam[..., 0] >= brightest_thresh) brightest_pixels = cam[brightest_idx] bright_cam = brightest_pixels.mean(axis=0) - cam_d = np.sqrt(np.sum((cam - bright_cam)**2, axis=2)) + cam_d = np.sqrt(np.sum((cam - bright_cam) ** 2, axis=2)) return cam_d, cam def normalize_he(img: np.array, Io: int = 240, alpha: int = 1, beta: int = 0.15): - """ Normalize staining appearence of H&E stained images. + """Normalize staining appearence of H&E stained images. Parameters ---------- @@ -761,10 +827,10 @@ def normalize_he(img: np.array, Io: int = 240, alpha: int = 1, beta: int = 0.15) img = img.reshape((-1, 3)) # calculate optical density - opt_density = -np.log((img.astype(float)+1)/Io) + opt_density = -np.log((img.astype(float) + 1) / Io) # remove transparent pixels - opt_density_hat = opt_density[~np.any(opt_density 1: - rgb01 = rgb_img/255.0 + rgb01 = rgb_img / 255.0 else: rgb01 = rgb_img @@ -909,7 +982,7 @@ def stainmat2decon(stain_mat_srgb255): od_mat = rgb2od(stain_mat_srgb255) eps = np.finfo("float").eps - M = od_mat / np.linalg.norm(od_mat+eps, axis=1, keepdims=True) + M = od_mat / np.linalg.norm(od_mat + eps, axis=1, keepdims=True) M[np.isnan(M)] = 0 D = np.linalg.pinv(M) @@ -923,7 +996,18 @@ def deconvolve_img(rgb_img, D): return deconvolved_img -def view_as_scatter(img, cspace_name, cspace_fxn=None, channel_1_idx=None, channel_2_idx=None, channel_3_idx=None, log3d=False, cspace_kwargs=None, mask=None, s=3): +def view_as_scatter( + img, + cspace_name, + cspace_fxn=None, + channel_1_idx=None, + channel_2_idx=None, + channel_3_idx=None, + log3d=False, + cspace_kwargs=None, + mask=None, + s=3, +): """View colors in image, transformed used the `cspace_fxn`, as a scatterplot, where the color of each point is the corresponding RGB color Useful when trying to find color thresholds @@ -935,15 +1019,14 @@ def view_as_scatter(img, cspace_name, cspace_fxn=None, channel_1_idx=None, chann else: img_flat = img[mask > 0] - unique_colors = np.unique(img_flat, axis=0) flat_size = unique_colors.shape[0] h = 2 - while flat_size%h != 0: + while flat_size % h != 0: h += 1 - w = int(flat_size/h) + w = int(flat_size / h) rgb_block = np.reshape(unique_colors, (h, w, 3)) if cspace_fxn is None: @@ -969,7 +1052,7 @@ def view_as_scatter(img, cspace_name, cspace_fxn=None, channel_1_idx=None, chann a = a.ravel() b = b.ravel() - plt.scatter(a, b, c=unique_colors/255, s=s) + plt.scatter(a, b, c=unique_colors / 255, s=s) plt.xlabel(cspace_name[channel_1_idx]) plt.ylabel(cspace_name[channel_2_idx]) @@ -983,8 +1066,10 @@ def view_as_scatter(img, cspace_name, cspace_fxn=None, channel_1_idx=None, chann c = c.ravel() fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - ax.scatter(a, b, c, c=unique_colors, depthshade=False, edgecolor=unique_colors, lw=0) + ax = fig.add_subplot(111, projection="3d") + ax.scatter( + a, b, c, c=unique_colors, depthshade=False, edgecolor=unique_colors, lw=0 + ) plt.xlabel(cspace_name[channel_1_idx]) plt.ylabel(cspace_name[channel_2_idx]) ax.set_zlabel(cspace_name[channel_3_idx]) @@ -992,17 +1077,26 @@ def view_as_scatter(img, cspace_name, cspace_fxn=None, channel_1_idx=None, chann plt.title("Log") -def seg_jch(img, j_range=[0, 1], c_range=[0, 1], h_range=[0, 360], cspace='CAM16UCS', h_rotation=0): - """Segment image in a JCH colorspace - - """ +def seg_jch( + img, + j_range=[0, 1], + c_range=[0, 1], + h_range=[0, 360], + cspace="CAM16UCS", + h_rotation=0, +): + """Segment image in a JCH colorspace""" jch_img = rgb2jch(img, cspace=cspace, h_rotation=h_rotation) - jch_mask_idx = np.where((jch_img[..., 0] >= j_range[0]) & (jch_img[..., 0] < j_range[1]) & - (jch_img[..., 1] >= c_range[0]) & (jch_img[..., 1] < c_range[1]) & - (jch_img[..., 2] >= h_range[0]) & (jch_img[..., 2] < h_range[1]) - ) + jch_mask_idx = np.where( + (jch_img[..., 0] >= j_range[0]) + & (jch_img[..., 0] < j_range[1]) + & (jch_img[..., 1] >= c_range[0]) + & (jch_img[..., 1] < c_range[1]) + & (jch_img[..., 2] >= h_range[0]) + & (jch_img[..., 2] < h_range[1]) + ) jch_mask = np.zeros(img.shape[0:2], dtype=np.uint8) jch_mask[jch_mask_idx] = 255 @@ -1011,9 +1105,7 @@ def seg_jch(img, j_range=[0, 1], c_range=[0, 1], h_range=[0, 360], cspace='CAM16 def calc_shannon(img, n_bins=10, mask=None): - """Calculate Shannon's entropy for each pixel in `img` - - """ + """Calculate Shannon's entropy for each pixel in `img`""" img01 = exposure.rescale_intensity(img, out_range=(0, 1)) if img.ndim > 2: @@ -1022,14 +1114,14 @@ def calc_shannon(img, n_bins=10, mask=None): img01 = img01.reshape(-1) if mask is None: - x = np.round(img01*(n_bins-1)).astype(int) + x = np.round(img01 * (n_bins - 1)).astype(int) else: flat_mask = mask.reshape(-1) fg_idx = np.where(flat_mask > 0)[0] - x = np.round(img01*(n_bins-1)).astype(int)[fg_idx] + x = np.round(img01 * (n_bins - 1)).astype(int)[fg_idx] unique_x, counts = np.unique(x, return_counts=True, axis=0) - probs = counts/counts.sum() + probs = counts / counts.sum() if img.ndim > 2: prob_dict = {tuple(unique_x[i]): probs[i] for i in range(len(probs))} prob_img = np.array([prob_dict[tuple(k)] for k in x]) @@ -1092,7 +1184,7 @@ def thresh_unimodal(x, bins=256): counts, bin_edges = np.histogram(-x, bins=bins) bin_width = bin_edges[1] - bin_edges[0] - midpoints = bin_edges[0:-1] + bin_width/2 + midpoints = bin_edges[0:-1] + bin_width / 2 hist_line = LineString(np.column_stack([midpoints, counts])) peak_bin = np.argmax(counts) @@ -1105,8 +1197,8 @@ def thresh_unimodal(x, bins=256): peak_x, min_bin_x = midpoints[peak_bin], midpoints[min_bin] peak_y, min_bin_y = counts[peak_bin], counts[min_bin] - peak_m = (peak_y - min_bin_y)/(peak_x - min_bin_x + np.finfo(float).resolution) - peak_b = peak_y - peak_m*peak_x + peak_m = (peak_y - min_bin_y) / (peak_x - min_bin_x + np.finfo(float).resolution) + peak_b = peak_y - peak_m * peak_x perp_m = -peak_m + np.finfo(float).resolution n_v = len(midpoints) d = [-1] * n_v @@ -1117,29 +1209,34 @@ def thresh_unimodal(x, bins=256): x1 = midpoints[i] if x1 < peak_x: continue - y1 = peak_m*x1 + peak_b - perp_b = y1 - perp_m*x1 + y1 = peak_m * x1 + peak_b + perp_b = y1 - perp_m * x1 y2 = 0 - x2 = -perp_b/(perp_m) + x2 = -perp_b / (perp_m) perp_line_obj = LineString([[x1, y1], [x2, y2]]) if not perp_line_obj.is_valid or not hist_line.is_valid: - print("perpline is valid", perp_line_obj.is_valid, "hist line is valid", hist_line.is_valid) - print("perpline xy1, xy2", [x1, y1], [x2, y2], "m=", perp_m) + logger.debug( + "perpline is valid", + perp_line_obj.is_valid, + "hist line is valid", + hist_line.is_valid, + ) + logger.debug("perpline xy1, xy2", [x1, y1], [x2, y2], "m=", perp_m) intersection = perp_line_obj.intersection(hist_line) if intersection.is_empty: # No intersection continue - if intersection.geom_type == 'MultiPoint': + if intersection.geom_type == "MultiPoint": all_x, all_y = LineString(intersection.geoms).xy xi = all_x[-1] yi = all_y[-1] - elif intersection.geom_type == 'Point': + elif intersection.geom_type == "Point": xi, yi = intersection.xy xi = xi[0] yi = yi[0] - d[i] = np.sqrt((xi - x1)**2 + (yi - y1)**2) + d[i] = np.sqrt((xi - x1) ** 2 + (yi - y1) ** 2) all_xi[i] = xi max_d_idx = np.argmax(d) @@ -1196,7 +1293,7 @@ def estimate_k(x, max_k=100, step_size=10): done = True break - next_k_range = np.clip([best_k - k_step//2, best_k + k_step//2], 2, max_k) + next_k_range = np.clip([best_k - k_step // 2, best_k + k_step // 2], 2, max_k) kd = np.diff(next_k_range)[0] if kd == 0: done = True @@ -1226,7 +1323,7 @@ def combine_masks_by_hysteresis(mask_list, upper_t=None): to_hyst_mask = np.zeros(mshape) for m in mask_list: - if(isinstance(m, pyvips.Image)): + if isinstance(m, pyvips.Image): np_mask = warp_tools.vips2numpy(m) else: np_mask = m.copy() @@ -1235,7 +1332,9 @@ def combine_masks_by_hysteresis(mask_list, upper_t=None): if upper_t is None: upper_t = len(mask_list) - hyst_mask = 255*filters.apply_hysteresis_threshold(to_hyst_mask, 0.5, upper_t - 0.5).astype(np.uint8) + hyst_mask = 255 * filters.apply_hysteresis_threshold( + to_hyst_mask, 0.5, upper_t - 0.5 + ).astype(np.uint8) return hyst_mask @@ -1272,12 +1371,12 @@ def split_shapely_line(line_poly, step_size=10, close_line=False): else: to_split = line_poly - nseg = int(np.ceil(to_split.length/step_size)) + nseg = int(np.ceil(to_split.length / step_size)) nseg = max(2, nseg) line_seg_idx = np.linspace(0, to_split.length, nseg) - geom_list = [None] * (nseg-1) + geom_list = [None] * (nseg - 1) for i in range(nseg - 1): - pos = np.linspace(line_seg_idx[i], line_seg_idx[i+1], step_size) + pos = np.linspace(line_seg_idx[i], line_seg_idx[i + 1], step_size) seg_geom = shapely.geometry.LineString(to_split.interpolate(pos)) geom_list[i] = seg_geom @@ -1285,7 +1384,9 @@ def split_shapely_line(line_poly, step_size=10, close_line=False): def get_poly_corners(img, tolerance=2): - contours, _ = cv2.findContours(img.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours( + img.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) xy = contours[0].squeeze() new_xy = measure.subdivide_polygon(xy, degree=3, preserve_ends=True) @@ -1295,8 +1396,7 @@ def get_poly_corners(img, tolerance=2): def polygon_tortuosity(img, window_size=3): - """Calculate tortuosity of a contour in `img`. Should be masked so there is only 1 contour - """ + """Calculate tortuosity of a contour in `img`. Should be masked so there is only 1 contour""" contours_xy = get_poly_corners(img) n_over = contours_xy.shape[0] % window_size @@ -1306,22 +1406,24 @@ def polygon_tortuosity(img, window_size=3): else: wrapped_poly = contours_xy.copy() - window_edges = list(range(0, wrapped_poly.shape[0]+1, window_size)) + window_edges = list(range(0, wrapped_poly.shape[0] + 1, window_size)) n = 0 total_length = 0 tort_list = [] - for idx1 in range(0, len(window_edges)-1): + for idx1 in range(0, len(window_edges) - 1): idx2 = idx1 + 1 window_idx1 = window_edges[idx1] window_idx2 = window_edges[idx2] verts_in_window = wrapped_poly[window_idx1:window_idx2] - dist_traveled = np.sqrt(np.sum((verts_in_window[-1] - verts_in_window[0])**2)) + dist_traveled = np.sqrt(np.sum((verts_in_window[-1] - verts_in_window[0]) ** 2)) if dist_traveled == 0: continue - path_length = np.sum(np.sqrt(np.sum(np.diff(verts_in_window, axis=0)**2, axis=1))) - line_arc_chord = (path_length/dist_traveled) - 1 + path_length = np.sum( + np.sqrt(np.sum(np.diff(verts_in_window, axis=0) ** 2, axis=1)) + ) + line_arc_chord = (path_length / dist_traveled) - 1 if np.isclose(path_length, dist_traveled) and line_arc_chord < 0: line_arc_chord = 0 @@ -1332,7 +1434,7 @@ def polygon_tortuosity(img, window_size=3): if total_length == 0: poly_tort = 0 else: - poly_tort = ((n-1)/total_length)*sum(tort_list) + poly_tort = ((n - 1) / total_length) * sum(tort_list) return poly_tort @@ -1345,7 +1447,7 @@ def remove_small_obj_and_lines_by_dist(mask): dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 5) dst_t = filters.threshold_li(dist_transform[mask > 0]) - temp_sure_fg = 255*(dist_transform >= dst_t).astype(np.uint8) + temp_sure_fg = 255 * (dist_transform >= dst_t).astype(np.uint8) sure_mask = combine_masks_by_hysteresis([mask, temp_sure_fg]) return sure_mask @@ -1366,11 +1468,12 @@ def create_edges_mask(labeled_img): inner_mask = np.zeros(labeled_img.shape, dtype=np.uint8) edges_mask = np.zeros(labeled_img.shape, dtype=np.uint8) for regn in img_regions: - on_border_idx = np.where((regn.coords[:, 0] == 0) | - (regn.coords[:, 0] == labeled_img.shape[0]-1) | - (regn.coords[:, 1] == 0) | - (regn.coords[:, 1] == labeled_img.shape[1]-1) - )[0] + on_border_idx = np.where( + (regn.coords[:, 0] == 0) + | (regn.coords[:, 0] == labeled_img.shape[0] - 1) + | (regn.coords[:, 1] == 0) + | (regn.coords[:, 1] == labeled_img.shape[1] - 1) + )[0] if len(on_border_idx) == 0: inner_mask[regn.coords[:, 0], regn.coords[:, 1]] = 255 else: @@ -1379,7 +1482,14 @@ def create_edges_mask(labeled_img): return inner_mask, edges_mask -def create_tissue_mask_from_rgb(img, brightness_q=0.99, kernel_size=3, gray_thresh=0.075, light_gray_thresh=0.875, dark_gray_thresh=0.7): +def create_tissue_mask_from_rgb( + img, + brightness_q=0.99, + kernel_size=3, + gray_thresh=0.075, + light_gray_thresh=0.875, + dark_gray_thresh=0.7, +): """Create mask that only covers tissue Also remove dark regions on the edge of the slide, which could be artifacts @@ -1408,13 +1518,19 @@ def create_tissue_mask_from_rgb(img, brightness_q=0.99, kernel_size=3, gray_thre # Ignore artifacts that could throw off thresholding. These are often greyish in color jch = rgb2jch(img) - light_greys = 255*((jch[..., 1] < gray_thresh) & (jch[..., 0] < light_gray_thresh)).astype(np.uint8) - dark_greys = 255*((jch[..., 1] < gray_thresh) & (jch[..., 0] < dark_gray_thresh)).astype(np.uint8) + light_greys = 255 * ( + (jch[..., 1] < gray_thresh) & (jch[..., 0] < light_gray_thresh) + ).astype(np.uint8) + dark_greys = 255 * ( + (jch[..., 1] < gray_thresh) & (jch[..., 0] < dark_gray_thresh) + ).astype(np.uint8) grey_mask = combine_masks_by_hysteresis([light_greys, dark_greys]) color_mask = 255 - grey_mask - cam_d, cam = calc_background_color_dist(img, brightness_q=brightness_q, mask=color_mask) + cam_d, cam = calc_background_color_dist( + img, brightness_q=brightness_q, mask=color_mask + ) # Reduce intensity of thick horizontal and vertial lines, usually artifacts like edges, streaks, folds, etc... vert_knl = np.ones((1, 5)) @@ -1464,7 +1580,7 @@ def jc_dist(img, cspace="IHLS", p=99, metric="euclidean"): """ if cspace.upper() == "IHLS": - hys = colour.models.RGB_to_IHLS(img) # Hue, luminance, saturation/colorfulness + hys = colour.models.RGB_to_IHLS(img) # Hue, luminance, saturation/colorfulness j = hys[..., 1] c = hys[..., 2] @@ -1478,9 +1594,11 @@ def jc_dist(img, cspace="IHLS", p=99, metric="euclidean"): jc01 = np.dstack([j01, c01]).reshape((-1, 2)) bg_j = np.percentile(j01, p) - bg_c = np.percentile(c01, 100-p) + bg_c = np.percentile(c01, 100 - p) - jc_dist_img = spatial.distance.cdist(jc01, np.array([[bg_j, bg_c]]), metric=metric).reshape(img.shape[0:2]) + jc_dist_img = spatial.distance.cdist( + jc01, np.array([[bg_j, bg_c]]), metric=metric + ).reshape(img.shape[0:2]) return jc_dist_img @@ -1510,12 +1628,14 @@ def create_tissue_mask_with_jc_dist(img): jc_dist_img[np.isnan(jc_dist_img)] = np.nanmax(jc_dist_img) jc_t, _ = filters.threshold_multiotsu(jc_dist_img) - jc_mask = 255*(jc_dist_img > jc_t).astype(np.uint8) - jc_dist_img = exposure.equalize_adapthist(exposure.rescale_intensity(jc_dist_img, out_range=(0, 1))) + jc_mask = 255 * (jc_dist_img > jc_t).astype(np.uint8) + jc_dist_img = exposure.equalize_adapthist( + exposure.rescale_intensity(jc_dist_img, out_range=(0, 1)) + ) img_edges = filters.scharr(jc_dist_img) p_t = filters.threshold_otsu(img_edges) - edges_mask = 255*(img_edges > p_t).astype(np.uint8) + edges_mask = 255 * (img_edges > p_t).astype(np.uint8) temp_mask = edges_mask.copy() temp_mask[jc_mask == 0] = 0 @@ -1550,22 +1670,22 @@ def clean_mask(mask, img=None, rel_min_size=0.001): if r0 == 0: # Touching top lr = np.where(r.image_filled[0, :])[0] - r_filled_img[0, min(lr):max(lr)] = 255 + r_filled_img[0, min(lr) : max(lr)] = 255 if r1 == mask.shape[0]: # Touching bottom lr = np.where(r.image_filled[-1, :])[0] - r_filled_img[-1, min(lr):max(lr)] = 255 + r_filled_img[-1, min(lr) : max(lr)] = 255 if c0 == 0: tb = np.where(r.image_filled[:, 0])[0] # Touchng left border - r_filled_img[min(tb):max(tb), 0] = 255 + r_filled_img[min(tb) : max(tb), 0] = 255 if c1 == mask.shape[1]: # Touchng right border tb = np.where(r.image_filled[:, -1])[0] - r_filled_img[min(tb):max(tb), -1] = 255 + r_filled_img[min(tb) : max(tb), -1] = 255 r_filled_img = ndimage.binary_fill_holes(r_filled_img) if img is not None: @@ -1589,7 +1709,7 @@ def clean_mask(mask, img=None, rel_min_size=0.001): if feature_mask.max() == 0: feature_mask = np.sum(np.dstack(mask_list), axis=2) - feature_thresh = len(mask_list)//2 + feature_thresh = len(mask_list) // 2 feature_mask[feature_mask <= feature_thresh] = 0 feature_mask[feature_mask != 0] = 255 @@ -1600,7 +1720,7 @@ def clean_mask(mask, img=None, rel_min_size=0.001): return feature_mask region_sizes = np.array([r.area for r in feature_regions]) - min_abs_size = int(rel_min_size*np.multiply(*mask.shape[0:2])) #*kernel_size + min_abs_size = int(rel_min_size * np.multiply(*mask.shape[0:2])) # *kernel_size keep_region_idx = np.where(region_sizes > min_abs_size)[0] if len(keep_region_idx) == 0: biggest_idx = np.argmax([r.area for r in fg_regions]) @@ -1638,7 +1758,7 @@ def thresh_rel_max_region_size(mask, rel_min_size=0.2): regions = measure.regionprops(measure.label(mask)) region_sizes = [r.area for r in regions] max_size = np.max(region_sizes) - size_thresh = max_size*rel_min_size + size_thresh = max_size * rel_min_size size_thresh_mask = np.zeros_like(mask) for r in regions: if r.area > size_thresh: @@ -1653,31 +1773,37 @@ def remove_regular_shapes(mask, irreg_thresh=0.05): n_prev_regions = len(regions) n_irreg_regions = 0 for r in regions: - irreg_v = 1 - r.area/r.convex_area + irreg_v = 1 - r.area / r.convex_area if irreg_v > irreg_thresh: irreg_mask[r.slice][r.image] = 255 n_irreg_regions += 1 n_removed = n_prev_regions - n_irreg_regions if n_removed > 0: - print(f"Removed {n_removed} regularly shaped regions") + logger.info(f"Removed {n_removed} regularly shaped regions") return irreg_mask def entropy_mask(img, cspace="Hunter LAB", irreg_thresh=0.0, rel_min_size=0.2): # Detect and mask out backround (brightest and least colorful) - mean_rgb, color_mask, filtered_label_counts, color_clusterer = find_dominant_colors(img, cspace=cspace, return_xy_clusterer=True) + mean_rgb, color_mask, filtered_label_counts, color_clusterer = find_dominant_colors( + img, cspace=cspace, return_xy_clusterer=True + ) to_cluster_jab = rgb2jab(img, cspace=cspace) xy = to_cluster_jab[..., 1:].reshape((-1, 2)) clustered_xy_idx = color_clusterer.predict(xy) mean_jab = rgb2jab(mean_rgb, cspace) mean_jch = colour.models.Jab_to_JCh(mean_jab) - bg_idx = np.lexsort([mean_jch[:, 1], -mean_jch[:, 0]])[0] # Last column sorted 1st. Returns ascending order + bg_idx = np.lexsort([mean_jch[:, 1], -mean_jch[:, 0]])[ + 0 + ] # Last column sorted 1st. Returns ascending order bg_jab = mean_jab[bg_idx, :] - ab_dist = spatial.distance.cdist(color_clusterer.cluster_centers_, np.array([bg_jab[1:]])) + ab_dist = spatial.distance.cdist( + color_clusterer.cluster_centers_, np.array([bg_jab[1:]]) + ) bg_idx = np.argmin(ab_dist) bg_mask = np.zeros_like(clustered_xy_idx) bg_mask[clustered_xy_idx == bg_idx] = 255 @@ -1687,7 +1813,9 @@ def entropy_mask(img, cspace="Hunter LAB", irreg_thresh=0.0, rel_min_size=0.2): j = to_cluster_jab[..., 0].copy() j[bg_mask > 0] = 0 j = np.clip(j, 0, 1) - j = exposure.rescale_intensity(j, out_range=np.uint8) # rank filters require uint8 image + j = exposure.rescale_intensity( + j, out_range=np.uint8 + ) # rank filters require uint8 image ent_img = filters.rank.entropy(j, morphology.disk(3)) # Create mask @@ -1700,8 +1828,18 @@ def entropy_mask(img, cspace="Hunter LAB", irreg_thresh=0.0, rel_min_size=0.2): return cleaned_mask -def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.0001, n_hue_bins=360, max_colors=5, n_colors=None, hue_only=False, method="deconvolve"): - """ Creates an array where each channel corresponds to a color detected by `find_dominant_colors` +def separate_colors( + img, + cspace="JzAzBz", + min_colorfulness=0.005, + px_thresh=0.0001, + n_hue_bins=360, + max_colors=5, + n_colors=None, + hue_only=False, + method="deconvolve", +): + """Creates an array where each channel corresponds to a color detected by `find_dominant_colors` Parameters ---------- @@ -1752,15 +1890,22 @@ def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.00 """ if hue_only: - img_colors, color_mask, color_counts = find_dominant_hues(img, cspace=cspace, - min_colorfulness=min_colorfulness, - px_thresh=px_thresh, n_hue_bins=n_hue_bins) + img_colors, color_mask, color_counts = find_dominant_hues( + img, + cspace=cspace, + min_colorfulness=min_colorfulness, + px_thresh=px_thresh, + n_hue_bins=n_hue_bins, + ) else: - img_colors, color_mask, color_counts = find_dominant_colors(img, cspace=cspace, - min_colorfulness=min_colorfulness, - px_thresh=px_thresh, - max_colors=max_colors, - n_colors=n_colors) + img_colors, color_mask, color_counts = find_dominant_colors( + img, + cspace=cspace, + min_colorfulness=min_colorfulness, + px_thresh=px_thresh, + max_colors=max_colors, + n_colors=n_colors, + ) if method == "deconvolve": unmix_D = stainmat2decon(img_colors) @@ -1775,12 +1920,17 @@ def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.00 jab_min = jab_flat.min(axis=0) jab_max = jab_flat.max(axis=0) jab_range = jab_max - jab_min - jab01 = (jab_flat - jab_min)/jab_range - jab_colors01 = (jab_colors - jab_min)/jab_range + jab01 = (jab_flat - jab_min) / jab_range + jab_colors01 = (jab_colors - jab_min) / jab_range jab_img_norm = np.linalg.norm(jab01, axis=1) jab_color_norms = np.linalg.norm(jab_colors01, axis=1) - sep_img = np.dstack([jab01@jab_colors01[i]/(jab_img_norm*jab_color_norms[i]) for i in range(jab_colors.shape[0])]) + sep_img = np.dstack( + [ + jab01 @ jab_colors01[i] / (jab_img_norm * jab_color_norms[i]) + for i in range(jab_colors.shape[0]) + ] + ) sep_img = sep_img.reshape((*jab_img.shape[0:2], jab_colors.shape[0])) elif method == "similarity": @@ -1790,10 +1940,17 @@ def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.00 jab_min = jab_flat.min(axis=0) jab_max = jab_flat.max(axis=0) jab_range = jab_max - jab_min - jab01 = (jab_flat - jab_min)/jab_range - jab_colors01 = (jab_colors - jab_min)/jab_range - - dist_img = np.dstack([spatial.distance.cdist(jab01, jab_colors01[i].reshape(1, -1)).reshape(img.shape[0:2]) for i in range(jab_colors.shape[0])]) + jab01 = (jab_flat - jab_min) / jab_range + jab_colors01 = (jab_colors - jab_min) / jab_range + + dist_img = np.dstack( + [ + spatial.distance.cdist(jab01, jab_colors01[i].reshape(1, -1)).reshape( + img.shape[0:2] + ) + for i in range(jab_colors.shape[0]) + ] + ) dist_img = exposure.rescale_intensity(dist_img, out_range=(0, 1)) sep_img = 1 - dist_img @@ -1809,7 +1966,9 @@ def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.00 svm = SVC(probability=True) svm.fit(training_X, training_Y) - sep_img = svm.predict_proba(jab_flat).reshape((*img.shape[0:2], img_colors.shape[0])) + sep_img = svm.predict_proba(jab_flat).reshape( + (*img.shape[0:2], img_colors.shape[0]) + ) elif method == "one_class_svm": jab_img = rgb2jab(img, cspace=cspace) @@ -1832,12 +1991,24 @@ def separate_colors(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.00 np.random.shuffle(idx) svm = SVC(probability=True) - sep_img[..., i] = svm.fit(chnl_X[idx], chnl_Y[idx]).predict_proba(jab_flat)[..., 0].reshape(img.shape[0:2]) + sep_img[..., i] = ( + svm.fit(chnl_X[idx], chnl_Y[idx]) + .predict_proba(jab_flat)[..., 0] + .reshape(img.shape[0:2]) + ) return sep_img, img_colors, color_mask, color_counts -def find_dominant_hues(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0.0001, n_hue_bins=360, min_hue_dist=18, lamb=0): +def find_dominant_hues( + img, + cspace="JzAzBz", + min_colorfulness=0.005, + px_thresh=0.0001, + n_hue_bins=360, + min_hue_dist=18, + lamb=0, +): jab_img = rgb2jab(img, cspace=cspace) jch_img = colour.models.Jab_to_JCh(jab_img) @@ -1848,22 +2019,26 @@ def find_dominant_hues(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0 fg_jab = jab_img[fg_px] h = jch_img[..., 2][fg_px] - hue_step = 360//n_hue_bins + hue_step = 360 // n_hue_bins h_hist, h_bins = np.histogram(h, bins=np.arange(0, 360, hue_step)) if lamb > 0: # Smooth curve - h_hist = signal.cspline1d_eval(signal.cspline1d(h_hist, lamb=lamb), np.arange(0, len(h_bins))) + h_hist = signal.cspline1d_eval( + signal.cspline1d(h_hist, lamb=lamb), np.arange(0, len(h_bins)) + ) h_hist[h_hist < 0] = 0 - img_px_thresh = px_thresh*np.multiply(*img.shape[0:2]) - peak_idx, heights = signal.find_peaks(h_hist, height=img_px_thresh, distance=min_hue_dist) + img_px_thresh = px_thresh * np.multiply(*img.shape[0:2]) + peak_idx, heights = signal.find_peaks( + h_hist, height=img_px_thresh, distance=min_hue_dist + ) h_peaks = h_bins[peak_idx] n_peaks = len(h_peaks) mean_jch = np.zeros((n_peaks, 3)) hue_mask = np.full(img.shape[0:2], -1, dtype=int) hue_label = 0 for i in range(n_peaks): - h_bin_midpoint = h_peaks[i] + hue_step/2 + h_bin_midpoint = h_peaks[i] + hue_step / 2 bin_h_range = np.array([h_bin_midpoint - hue_step, h_bin_midpoint + hue_step]) bin_h_range = np.clip(bin_h_range, 0, 360) in_range_idx = np.where((h >= bin_h_range[0]) & (h < bin_h_range[1]))[0] @@ -1886,9 +2061,18 @@ def find_dominant_hues(img, cspace="JzAzBz", min_colorfulness=0.005, px_thresh=0 return mean_rgb_from_jch, hue_mask, bin_counts - -def find_dominant_colors(img, cspace="JzAzBz", min_colorfulness=0, px_thresh=0.0001, n_bins=50, max_colors=5, n_colors=None, cluster_estimation="unimodal", return_xy_clusterer=False): - """ Find most common colors in the image +def find_dominant_colors( + img, + cspace="JzAzBz", + min_colorfulness=0, + px_thresh=0.0001, + n_bins=50, + max_colors=5, + n_colors=None, + cluster_estimation="unimodal", + return_xy_clusterer=False, +): + """Find most common colors in the image Initial colors are detected by converting the image to `cspace`, and then binning the A and B channels. Peaks in this 2D histogram are then used as the initial centroids for K-means clustering @@ -1951,8 +2135,10 @@ def find_dominant_colors(img, cspace="JzAzBz", min_colorfulness=0, px_thresh=0.0 min_colorfulness = filters.threshold_otsu(jch_img[..., 1]) fg_px = np.where(jch_img[..., 1] > min_colorfulness) - ab_hist, a_bins, b_bins = np.histogram2d(jab_img[..., 1][fg_px], jab_img[..., 2][fg_px], bins=(n_bins, n_bins)) - non_zero_a_idx, non_zero_b_idx = np.where(ab_hist > px_thresh*ab_hist.size) + ab_hist, a_bins, b_bins = np.histogram2d( + jab_img[..., 1][fg_px], jab_img[..., 2][fg_px], bins=(n_bins, n_bins) + ) + non_zero_a_idx, non_zero_b_idx = np.where(ab_hist > px_thresh * ab_hist.size) non_zero_a = a_bins[non_zero_a_idx] non_zero_b = b_bins[non_zero_b_idx] weights = ab_hist[non_zero_a_idx, non_zero_b_idx] @@ -1975,7 +2161,7 @@ def find_dominant_colors(img, cspace="JzAzBz", min_colorfulness=0, px_thresh=0.0 k_intertia = [] cluster_number = [] cluster_centroids = [] - for i in range(1, max_colors+1): + for i in range(1, max_colors + 1): if i >= xy.shape[0]: continue if i == 1: @@ -1988,20 +2174,23 @@ def find_dominant_colors(img, cspace="JzAzBz", min_colorfulness=0, px_thresh=0.0 possible_idx.remove(xy_idx2) diff_idx = [xy_idx1, xy_idx2] for j in range(2, i): - max_d_idx = np.argmax([np.min(sq_D[i, diff_idx]) for i in possible_idx]) + max_d_idx = np.argmax( + [np.min(sq_D[i, diff_idx]) for i in possible_idx] + ) new_idx = possible_idx[max_d_idx] diff_idx.append(new_idx) possible_idx.remove(new_idx) k_centroids_xy = initial_xy[diff_idx] - temp_xy_clusterer = cluster.KMeans(n_clusters=i, init=k_centroids_xy, random_state=0) + temp_xy_clusterer = cluster.KMeans( + n_clusters=i, init=k_centroids_xy, random_state=0 + ) temp_xy_clusterer.fit(xy) k_intertia.append(temp_xy_clusterer.inertia_) cluster_number.append(i) cluster_centroids.append(temp_xy_clusterer.cluster_centers_) - k_intertia = np.array(k_intertia) if len(k_intertia) > 1: if cluster_estimation == "elbow": @@ -2016,13 +2205,17 @@ def find_dominant_colors(img, cspace="JzAzBz", min_colorfulness=0, px_thresh=0.0 n_colors = 1 intial_cluster_centers = initial_xy - xy_clusterer = cluster.KMeans(n_clusters=n_colors, init=intial_cluster_centers, random_state=0) + xy_clusterer = cluster.KMeans( + n_clusters=n_colors, init=intial_cluster_centers, random_state=0 + ) labels = xy_clusterer.fit_predict(xy, sample_weight=weights) unique_labels, label_counts = np.unique(labels, return_counts=True) if hasattr(xy_clusterer, "cluster_centers_"): xy_centroids = xy_clusterer.cluster_centers_ else: - xy_centroids = np.vstack([np.mean(xy[labels==i], axis=0) for i in unique_labels]) + xy_centroids = np.vstack( + [np.mean(xy[labels == i], axis=0) for i in unique_labels] + ) fg_jab = jab_img[fg_px] @@ -2032,7 +2225,9 @@ def _get_in_range(d_thresh): filtered_label_counts = [] mean_jab = [] for i, cent_xy in enumerate(xy_centroids): - dist_to_centroid = np.sqrt(np.sum((fg_jab[..., 1:3] - cent_xy)**2, axis=1)) + dist_to_centroid = np.sqrt( + np.sum((fg_jab[..., 1:3] - cent_xy) ** 2, axis=1) + ) in_range = np.where(dist_to_centroid < d_thresh)[0] if len(in_range) > 0: centroid_jab = np.mean(fg_jab[in_range], axis=0) @@ -2042,15 +2237,27 @@ def _get_in_range(d_thresh): color_label += 1 filtered_label_counts.append(label_counts[i]) - return mean_jab, filtered_label_counts, color_label, filtered_label_counts, color_mask + return ( + mean_jab, + filtered_label_counts, + color_label, + filtered_label_counts, + color_mask, + ) - dist_thresh = np.sqrt((a_bins[1] - a_bins[0])**2 + (b_bins[1] - b_bins[0])**2) + dist_thresh = np.sqrt((a_bins[1] - a_bins[0]) ** 2 + (b_bins[1] - b_bins[0]) ** 2) max_reps = 100 dscaler = 1 mean_jab = [] while len(mean_jab) == 0: - mean_jab, filtered_label_counts, color_label, filtered_label_counts, color_mask = _get_in_range(dscaler*dist_thresh) + ( + mean_jab, + filtered_label_counts, + color_label, + filtered_label_counts, + color_mask, + ) = _get_in_range(dscaler * dist_thresh) dscaler += 1 if dscaler > max_reps: @@ -2061,7 +2268,7 @@ def _get_in_range(d_thresh): mean_jab = np.hstack([mean_j[..., np.newaxis], xy_centroids]) mean_jab = np.vstack(mean_jab) - mean_rgb = 255*jab2rgb(mean_jab, cspace=cspace) + mean_rgb = 255 * jab2rgb(mean_jab, cspace=cspace) mean_rgb = np.clip(mean_rgb, 0, 255) # Sort so that most common colors are first filtered_label_counts = np.array(filtered_label_counts) @@ -2077,16 +2284,22 @@ def _get_in_range(d_thresh): def quantize_image(img, jscalar=1.0, cluster_cspace="Hunter Lab", n_colors=None): - mean_rgb, color_mask, filtered_label_counts, color_clusterer = find_dominant_colors(img, cspace=cluster_cspace, return_xy_clusterer=True, n_colors=n_colors) + mean_rgb, color_mask, filtered_label_counts, color_clusterer = find_dominant_colors( + img, cspace=cluster_cspace, return_xy_clusterer=True, n_colors=n_colors + ) to_cluster_jab = rgb2jab(img, cspace=cluster_cspace) xy = to_cluster_jab[..., 1:].reshape((-1, 2)) clustered_xy_idx = color_clusterer.predict(xy) - clustered_ab = color_clusterer.cluster_centers_[clustered_xy_idx].reshape((*img.shape[0:2], 2)) - j = jscalar*to_cluster_jab[..., 0] + clustered_ab = color_clusterer.cluster_centers_[clustered_xy_idx].reshape( + (*img.shape[0:2], 2) + ) + j = jscalar * to_cluster_jab[..., 0] clustered_jab = np.dstack([j, clustered_ab]) - clustered_img = (255*jab2rgb(clustered_jab, cspace=cluster_cspace)).astype(np.uint8) + clustered_img = (255 * jab2rgb(clustered_jab, cspace=cluster_cspace)).astype( + np.uint8 + ) return clustered_img @@ -2095,7 +2308,7 @@ def mean_color(rgb_vals, summary_fxn=np.mean): jab_vals = rgb2jab(rgb_vals) mean_jab = summary_fxn(jab_vals, axis=0) mean_rgb = jab2rgb(mean_jab) - mean_rgb = (255*np.clip(mean_rgb, 0, 1)) + mean_rgb = 255 * np.clip(mean_rgb, 0, 1) return mean_rgb @@ -2113,7 +2326,7 @@ def create_tissue_mask_from_multichannel(img, kernel_size=3): else: t = np.quantile(img, 0.01) tissue_mask[img > t] = 255 - tissue_mask = 255*ndimage.binary_fill_holes(tissue_mask).astype(np.uint8) + tissue_mask = 255 * ndimage.binary_fill_holes(tissue_mask).astype(np.uint8) concave_tissue_mask = mask2contours(tissue_mask, kernel_size=kernel_size) return tissue_mask, concave_tissue_mask @@ -2147,7 +2360,7 @@ def mask2covexhull(mask): concave_mask[r0:r1, c0:c1] += region.convex_image.astype(np.uint8) concave_mask[concave_mask != 0] = 255 - concave_mask = 255*ndimage.binary_fill_holes(concave_mask).astype(np.uint8) + concave_mask = 255 * ndimage.binary_fill_holes(concave_mask).astype(np.uint8) return concave_mask @@ -2189,7 +2402,9 @@ def mask2bbox_mask(mask, merge_bbox=True): def mask2contours(mask, kernel_size=3): kernel = morphology.disk(kernel_size) mask_dilated = cv2.dilate(mask, kernel) - contours, _ = cv2.findContours(mask_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours( + mask_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) contour_mask = np.zeros_like(mask_dilated) for cnt in contours: corners_xy = cnt.squeeze() @@ -2235,14 +2450,15 @@ def mask2contours(mask, kernel_size=3): max_x = corners_xy[:, 0][idx_at_max_y.max()] contour_w_border[max_y, min_x:max_x] = 255 - on_border_contours, _ = cv2.findContours(contour_w_border, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + on_border_contours, _ = cv2.findContours( + contour_w_border, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) for b_cnt in on_border_contours: cv2.drawContours(contour_mask, [b_cnt], 0, 255, -1) return contour_mask - def match_histograms(src_image, ref_histogram, bins=256): """ Source: https://automaticaddison.com/how-to-do-histogram-matching-using-opencv/ @@ -2255,6 +2471,7 @@ def match_histograms(src_image, ref_histogram, bins=256): :return: image_after_matching :rtype: image (array) """ + def calculate_cdf(histogram): """ This method calculates the cumulative distribution function @@ -2290,7 +2507,7 @@ def calculate_lookup(src_cdf, ref_cdf): return lookup_table # Split the images into the different color channels - src_hist, _ = np.histogram(src_image.flatten(), bins) + src_hist, _ = np.histogram(src_image.flatten(), bins) # Compute the normalized cdf for the source and reference image src_cdf = calculate_cdf(src_hist) @@ -2337,8 +2554,8 @@ def collect_img_stats(img_list, norm_percentiles=[1, 5, 95, 99], mask_list=None) n += img.size total_x += img.sum() - mean_x = total_x/n - ref_cdf = 100*np.cumsum(all_histogram)/np.sum(all_histogram) + mean_x = total_x / n + ref_cdf = 100 * np.cumsum(all_histogram) / np.sum(all_histogram) all_img_stats = np.array([len(np.where(ref_cdf <= q)[0]) for q in norm_percentiles]) all_img_stats = np.hstack([all_img_stats, mean_x]) all_img_stats = all_img_stats[np.argsort(all_img_stats)] @@ -2373,10 +2590,12 @@ def norm_img_stats(img, target_stats, mask=None): lower_knots = np.array([0]) upper_knots = np.array([300, 350, 400, 450]) src_stats_flat = np.hstack([lower_knots, src_stats_flat, upper_knots]).astype(float) - target_stats_flat = np.hstack([lower_knots, target_stats, upper_knots]).astype(float) + target_stats_flat = np.hstack([lower_knots, target_stats, upper_knots]).astype( + float + ) # Add epsilon to avoid duplicate values - eps = 100*np.finfo(float).resolution + eps = 100 * np.finfo(float).resolution eps_array = np.arange(len(src_stats_flat)) * eps src_stats_flat = src_stats_flat + eps_array target_stats_flat = target_stats_flat + eps_array @@ -2427,7 +2646,7 @@ def img_to_tensor(img, return_rgb=True): if np.issubdtype(img.dtype, np.integer): float_img = exposure.rescale_intensity(img, out_range=np.float32) elif img.max() > 1: - float_img = img/img.max() + float_img = img / img.max() else: float_img = img @@ -2436,7 +2655,7 @@ def img_to_tensor(img, return_rgb=True): if tensor_img.ndim == 2: # Single channel image if return_rgb: - tensor_img = torch.stack(3*[tensor_img]) + tensor_img = torch.stack(3 * [tensor_img]) else: tensor_img.unsqueeze(0) else: @@ -2447,6 +2666,3 @@ def img_to_tensor(img, return_rgb=True): tensor_img = tensor_img.unsqueeze(0) return tensor_img - - - diff --git a/src/valis/registration/__init__.py b/src/valis/registration/__init__.py new file mode 100644 index 00000000..f63e74a9 --- /dev/null +++ b/src/valis/registration/__init__.py @@ -0,0 +1,173 @@ +"""Registration package. + +This package replaces the monolithic ``registration.py`` module. All public +names are re-exported here so that existing code such as:: + + from valis.registration import Valis, Slide, load_registrar + from valis import registration; registration.CROP_OVERLAP + +continues to work unchanged. + +Sub-module layout +----------------- +_constants.py Module-level constants, default parameters, and shared imports. +state.py ``CropMode``, ``DisplacementField``, ``RegistrationConfig``, + ``load_registrar``. +slide.py ``Slide`` class. +pipeline.py ``Valis`` class. +""" + +# Re-export everything from _constants so ``registration.CROP_OVERLAP`` etc. work +from ._constants import ( + CONVERTED_IMG_DIR, + PROCESSED_IMG_DIR, + RIGID_REG_IMG_DIR, + NON_RIGID_REG_IMG_DIR, + DEFORMATION_FIELD_IMG_DIR, + OVERLAP_IMG_DIR, + REG_RESULTS_DATA_DIR, + MICRO_REG_DIR, + DISPLACEMENT_DIRS, + MASK_DIR, + DEFAULT_BRIGHTFIELD_CLASS, + DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, + DEFAULT_FLOURESCENCE_CLASS, + DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + DEFAULT_NORM_METHOD, + DEFAULT_FD, + DEFAULT_TRANSFORM_CLASS, + DEFAULT_MATCHER, + DEFAULT_MATCHER_FOR_SORTING, + DEFAULT_SIMILARITY_METRIC, + DEFAULT_AFFINE_OPTIMIZER_CLASS, + DEFAULT_MAX_PROCESSED_IMG_SIZE, + DEFAULT_MAX_IMG_DIM, + DEFAULT_THUMBNAIL_SIZE, + DEFAULT_MAX_NON_RIGID_REG_SIZE, + DEFAULT_MAX_MICRO_REG_SIZE, + TILER_THRESH_GB, + DEFAULT_NR_TILE_WH, + AFFINE_OPTIMIZER_KEY, + TRANSFORMER_KEY, + SIM_METRIC_KEY, + FD_KEY, + MATCHER_KEY, + MATCHER_FOR_SORTING_KEY, + NAME_KEY, + IMAGES_ORDERD_KEY, + REF_IMG_KEY, + QT_EMMITER_KEY, + TFORM_SRC_SHAPE_KEY, + TFORM_DST_SHAPE_KEY, + TFORM_MAT_KEY, + CHECK_REFLECT_KEY, + NON_RIGID_REG_CLASS_KEY, + NON_RIGID_REG_PARAMS_KEY, + NON_RIGID_USE_XY_KEY, + NON_RIGID_COMPOSE_KEY, + DEFAULT_NON_RIGID_CLASS, + DEFAULT_NON_RIGID_KWARGS, + DEFAULT_COMPRESSION, + CropMode, + CROP_OVERLAP, + CROP_REF, + CROP_NONE, + WARP_ANNO_MSG, + CONVERT_MSG, + DENOISE_MSG, + PROCESS_IMG_MSG, + NORM_IMG_MSG, + TRANSFORM_MSG, + PREP_NON_RIGID_MSG, + MEASURE_MSG, + SAVING_IMG_MSG, +) + +# Re-export shared types +from .state import ( + DisplacementField, + RegistrationConfig, + load_registrar, +) + +# Re-export the two main classes +from .slide import Slide +from .pipeline import Valis + +__all__ = [ + # Main classes + "Valis", + "Slide", + # Support classes + "CropMode", + "DisplacementField", + "RegistrationConfig", + # Functions + "load_registrar", + # Constants — directory names + "CONVERTED_IMG_DIR", + "PROCESSED_IMG_DIR", + "RIGID_REG_IMG_DIR", + "NON_RIGID_REG_IMG_DIR", + "DEFORMATION_FIELD_IMG_DIR", + "OVERLAP_IMG_DIR", + "REG_RESULTS_DATA_DIR", + "MICRO_REG_DIR", + "DISPLACEMENT_DIRS", + "MASK_DIR", + # Constants — crop modes (string aliases kept for backward compat) + "CROP_OVERLAP", + "CROP_REF", + "CROP_NONE", + # Constants — defaults + "DEFAULT_BRIGHTFIELD_CLASS", + "DEFAULT_BRIGHTFIELD_PROCESSING_ARGS", + "DEFAULT_FLOURESCENCE_CLASS", + "DEFAULT_FLOURESCENCE_PROCESSING_ARGS", + "DEFAULT_NORM_METHOD", + "DEFAULT_FD", + "DEFAULT_TRANSFORM_CLASS", + "DEFAULT_MATCHER", + "DEFAULT_MATCHER_FOR_SORTING", + "DEFAULT_SIMILARITY_METRIC", + "DEFAULT_AFFINE_OPTIMIZER_CLASS", + "DEFAULT_MAX_PROCESSED_IMG_SIZE", + "DEFAULT_MAX_IMG_DIM", + "DEFAULT_THUMBNAIL_SIZE", + "DEFAULT_MAX_NON_RIGID_REG_SIZE", + "DEFAULT_MAX_MICRO_REG_SIZE", + "TILER_THRESH_GB", + "DEFAULT_NR_TILE_WH", + "DEFAULT_NON_RIGID_CLASS", + "DEFAULT_NON_RIGID_KWARGS", + "DEFAULT_COMPRESSION", + # Constants — kwarg keys + "AFFINE_OPTIMIZER_KEY", + "TRANSFORMER_KEY", + "SIM_METRIC_KEY", + "FD_KEY", + "MATCHER_KEY", + "MATCHER_FOR_SORTING_KEY", + "NAME_KEY", + "IMAGES_ORDERD_KEY", + "REF_IMG_KEY", + "QT_EMMITER_KEY", + "TFORM_SRC_SHAPE_KEY", + "TFORM_DST_SHAPE_KEY", + "TFORM_MAT_KEY", + "CHECK_REFLECT_KEY", + "NON_RIGID_REG_CLASS_KEY", + "NON_RIGID_REG_PARAMS_KEY", + "NON_RIGID_USE_XY_KEY", + "NON_RIGID_COMPOSE_KEY", + # Constants — messages + "WARP_ANNO_MSG", + "CONVERT_MSG", + "DENOISE_MSG", + "PROCESS_IMG_MSG", + "NORM_IMG_MSG", + "TRANSFORM_MSG", + "PREP_NON_RIGID_MSG", + "MEASURE_MSG", + "SAVING_IMG_MSG", +] diff --git a/src/valis/registration/_constants.py b/src/valis/registration/_constants.py new file mode 100644 index 00000000..da9bbf2e --- /dev/null +++ b/src/valis/registration/_constants.py @@ -0,0 +1,179 @@ +"""Constants, default parameters, and imports shared across the registration package.""" + +import logging +from typing import Optional, Union + +logging.basicConfig(level=logging.WARNING) + +import torch +import kornia +import einops + +import traceback +import re +import os +import numpy as np +import pathlib +from skimage import transform, exposure, filters +from skimage import color as skcolor +from time import time +import tqdm +import pandas as pd +import pickle +import colour +import pyvips +from scipy import ndimage +import shapely +from copy import deepcopy +from pprint import pformat +import json +from colorama import Fore +from itertools import chain +import cv2 +import matplotlib.pyplot as plt + +from .. import feature_matcher +from .. import serial_rigid +from .. import feature_detectors +from .. import non_rigid_registrars +from .. import valtils +from .. import preprocessing +from .. import slide_tools +from .. import slide_io +from .. import viz +from .. import warp_tools +from .. import serial_non_rigid + +logger = logging.getLogger(__name__) + +pyvips.cache_set_max(0) + +# Destination directories # +CONVERTED_IMG_DIR = "images" +PROCESSED_IMG_DIR = "processed" +RIGID_REG_IMG_DIR = "rigid_registration" +NON_RIGID_REG_IMG_DIR = "non_rigid_registration" +DEFORMATION_FIELD_IMG_DIR = "deformation_fields" +OVERLAP_IMG_DIR = "overlaps" +REG_RESULTS_DATA_DIR = "data" +MICRO_REG_DIR = "micro_registration" +DISPLACEMENT_DIRS = os.path.join(REG_RESULTS_DATA_DIR, "displacements") +MASK_DIR = "masks" + +# Default image processing # +DEFAULT_BRIGHTFIELD_CLASS = preprocessing.OD +DEFAULT_BRIGHTFIELD_PROCESSING_ARGS = { + "adaptive_eq": False +} # {'c': preprocessing.DEFAULT_COLOR_STD_C, "h": 0} +DEFAULT_FLOURESCENCE_CLASS = preprocessing.ChannelGetter +DEFAULT_FLOURESCENCE_PROCESSING_ARGS = {"channel": "dapi", "adaptive_eq": True} +DEFAULT_NORM_METHOD = "img_stats" + +# Default rigid registration parameters # +DEFAULT_FD = feature_detectors.VggFD +DEFAULT_TRANSFORM_CLASS = transform.SimilarityTransform + +try: + DEFAULT_MATCHER = feature_matcher.LightGlueMatcher( + match_filter_method=feature_matcher.DEFAULT_RANSAC_NAME, + feature_detector=feature_detectors.DiskFD(), + ) +except ImportError: + DEFAULT_MATCHER = feature_matcher.Matcher( + match_filter_method=feature_matcher.DEFAULT_RANSAC_NAME, + feature_detector=feature_detectors.VggFD(), + ) + +DEFAULT_MATCHER_FOR_SORTING = feature_matcher.Matcher( + match_filter_method=feature_matcher.DEFAULT_RANSAC_NAME, + feature_detector=feature_detectors.VggFD(), +) +DEFAULT_SIMILARITY_METRIC = "n_matches" +DEFAULT_AFFINE_OPTIMIZER_CLASS = None +DEFAULT_MAX_PROCESSED_IMG_SIZE = 512 +DEFAULT_MAX_IMG_DIM = 1024 +DEFAULT_THUMBNAIL_SIZE = 512 +DEFAULT_MAX_NON_RIGID_REG_SIZE = 2048 +DEFAULT_MAX_MICRO_REG_SIZE = 4096 +DEFAULT_MIN_RIGID_MATCHES = ( + 0 # 0 disables the safeguard; opt-in via `min_rigid_matches` +) + +# Tiled non-rigid registration arguments +TILER_THRESH_GB = 10 +DEFAULT_NR_TILE_WH = 512 + +# Rigid registration kwarg keys # +AFFINE_OPTIMIZER_KEY = "affine_optimizer" +TRANSFORMER_KEY = "transformer" +SIM_METRIC_KEY = "similarity_metric" +FD_KEY = "feature_detector" +MATCHER_KEY = "matcher" +MATCHER_FOR_SORTING_KEY = "matcher_for_sorting" +NAME_KEY = "name" +IMAGES_ORDERD_KEY = "imgs_ordered" +REF_IMG_KEY = "reference_img_f" +QT_EMMITER_KEY = "qt_emitter" +TFORM_SRC_SHAPE_KEY = "transformation_src_shape_rc" +TFORM_DST_SHAPE_KEY = "transformation_dst_shape_rc" +TFORM_MAT_KEY = "M" +CHECK_REFLECT_KEY = "check_for_reflections" +MIN_RIGID_MATCHES_KEY = "min_rigid_matches" + +# Rigid registration kwarg keys # +NON_RIGID_REG_CLASS_KEY = "non_rigid_reg_class" +NON_RIGID_REG_PARAMS_KEY = "non_rigid_reg_params" +NON_RIGID_USE_XY_KEY = "moving_to_fixed_xy" +NON_RIGID_COMPOSE_KEY = "compose_transforms" + +# Default non-rigid registration parameters # +DEFAULT_NON_RIGID_CLASS = non_rigid_registrars.OpticalFlowWarper() +DEFAULT_NON_RIGID_KWARGS = {} + +# Cropping options +import sys as _sys + +if _sys.version_info >= (3, 11): + from enum import StrEnum as _StrEnum +else: + from enum import Enum as _Enum + + class _StrEnum(str, _Enum): + pass + + +class CropMode(_StrEnum): + """How to crop registered images. + + Being a StrEnum, string literals ("overlap", "reference", "all") are still + accepted wherever a CropMode is expected, so existing code is unaffected. + """ + + OVERLAP = "overlap" + """Crop to the area where all images overlap.""" + REFERENCE = "reference" + """Crop to the area overlapping with the reference image.""" + NONE = "all" + """No cropping — use all pixels.""" + + +# Keep module-level aliases for backward compatibility +CROP_OVERLAP = CropMode.OVERLAP +CROP_REF = CropMode.REFERENCE +CROP_NONE = CropMode.NONE + +DEFAULT_COMPRESSION = pyvips.enums.ForeignTiffCompression.DEFLATE +# Messages +WARP_ANNO_MSG = "Warping annotations" +CONVERT_MSG = "Converting images" +DENOISE_MSG = "Denoising images" +PROCESS_IMG_MSG = "Processing images" +NORM_IMG_MSG = "Normalizing images" +TRANSFORM_MSG = "Finding rigid transforms" +PREP_NON_RIGID_MSG = "Preparing images for non-rigid registration" +MEASURE_MSG = "Measuring error" +SAVING_IMG_MSG = "Saving images" + +PROCESS_IMG_MSG, NORM_IMG_MSG, DENOISE_MSG = valtils.pad_strings( + [PROCESS_IMG_MSG, NORM_IMG_MSG, DENOISE_MSG] +) diff --git a/valis/registration.py b/src/valis/registration/pipeline.py similarity index 52% rename from valis/registration.py rename to src/valis/registration/pipeline.py index fb98c30b..f1fa5ccd 100644 --- a/valis/registration.py +++ b/src/valis/registration/pipeline.py @@ -1,1506 +1,120 @@ -""" -Classes and functions to register a collection of images -""" -import logging -logging.basicConfig(level=logging.WARNING) - -import torch -import kornia -import einops - -import traceback -import re -import os -import numpy as np -import pathlib -from skimage import transform, exposure, filters -from skimage import color as skcolor -from time import time -import tqdm -import pandas as pd -import pickle -import colour -import pyvips -from scipy import ndimage -import shapely -from copy import deepcopy -from pprint import pformat -import json -from colorama import Fore -from itertools import chain -import cv2 -import matplotlib.pyplot as plt -from colorama import Fore -import jpype - -from . import feature_matcher -from . import serial_rigid -from . import feature_detectors -from . import non_rigid_registrars -from . import valtils -from . import preprocessing -from . import slide_tools -from . import slide_io -from . import viz -from . import warp_tools -from . import serial_non_rigid - -pyvips.cache_set_max(0) - -# Destination directories # -CONVERTED_IMG_DIR = "images" -PROCESSED_IMG_DIR = "processed" -RIGID_REG_IMG_DIR = "rigid_registration" -NON_RIGID_REG_IMG_DIR = "non_rigid_registration" -DEFORMATION_FIELD_IMG_DIR = "deformation_fields" -OVERLAP_IMG_DIR = "overlaps" -REG_RESULTS_DATA_DIR = "data" -MICRO_REG_DIR = "micro_registration" -DISPLACEMENT_DIRS = os.path.join(REG_RESULTS_DATA_DIR, "displacements") -MASK_DIR = "masks" - -# Default image processing # -DEFAULT_BRIGHTFIELD_CLASS = preprocessing.OD -DEFAULT_BRIGHTFIELD_PROCESSING_ARGS = {"adaptive_eq": False} #{'c': preprocessing.DEFAULT_COLOR_STD_C, "h": 0} -DEFAULT_FLOURESCENCE_CLASS = preprocessing.ChannelGetter -DEFAULT_FLOURESCENCE_PROCESSING_ARGS = {"channel": "dapi", "adaptive_eq": True} -DEFAULT_NORM_METHOD = "img_stats" - -# Default rigid registration parameters # -DEFAULT_FD = feature_detectors.VggFD -DEFAULT_TRANSFORM_CLASS = transform.SimilarityTransform -DEFAULT_MATCHER = feature_matcher.LightGlueMatcher(match_filter_method=feature_matcher.DEFAULT_RANSAC_NAME, feature_detector=feature_detectors.DiskFD()) -DEFAULT_MATCHER_FOR_SORTING = feature_matcher.Matcher(match_filter_method=feature_matcher.DEFAULT_RANSAC_NAME, feature_detector=feature_detectors.VggFD()) -DEFAULT_SIMILARITY_METRIC = "n_matches" -DEFAULT_AFFINE_OPTIMIZER_CLASS = None -DEFAULT_MAX_PROCESSED_IMG_SIZE = 512 -DEFAULT_MAX_IMG_DIM = 1024 -DEFAULT_THUMBNAIL_SIZE = 512 -DEFAULT_MAX_NON_RIGID_REG_SIZE = 2048 -DEFAULT_MAX_MICRO_REG_SIZE = 4096 - -# Tiled non-rigid registration arguments -TILER_THRESH_GB = 10 -DEFAULT_NR_TILE_WH = 512 - -# Rigid registration kwarg keys # -AFFINE_OPTIMIZER_KEY = "affine_optimizer" -TRANSFORMER_KEY = "transformer" -SIM_METRIC_KEY = "similarity_metric" -FD_KEY = "feature_detector" -MATCHER_KEY = "matcher" -MATCHER_FOR_SORTING_KEY = "matcher_for_sorting" -NAME_KEY = "name" -IMAGES_ORDERD_KEY = "imgs_ordered" -REF_IMG_KEY = "reference_img_f" -QT_EMMITER_KEY = "qt_emitter" -TFORM_SRC_SHAPE_KEY = "transformation_src_shape_rc" -TFORM_DST_SHAPE_KEY = "transformation_dst_shape_rc" -TFORM_MAT_KEY = "M" -CHECK_REFLECT_KEY = "check_for_reflections" - -# Rigid registration kwarg keys # -NON_RIGID_REG_CLASS_KEY = "non_rigid_reg_class" -NON_RIGID_REG_PARAMS_KEY = "non_rigid_reg_params" -NON_RIGID_USE_XY_KEY = "moving_to_fixed_xy" -NON_RIGID_COMPOSE_KEY = "compose_transforms" - -# Default non-rigid registration parameters # -DEFAULT_NON_RIGID_CLASS = non_rigid_registrars.OpticalFlowWarper() -DEFAULT_NON_RIGID_KWARGS = {} - -# Cropping options -CROP_OVERLAP = "overlap" -CROP_REF = "reference" -CROP_NONE = "all" - -DEFAULT_COMPRESSION=pyvips.enums.ForeignTiffCompression.DEFLATE -# Messages -WARP_ANNO_MSG = "Warping annotations" -CONVERT_MSG = "Converting images" -DENOISE_MSG = "Denoising images" -PROCESS_IMG_MSG = "Processing images" -NORM_IMG_MSG = "Normalizing images" -TRANSFORM_MSG = "Finding rigid transforms" -PREP_NON_RIGID_MSG = "Preparing images for non-rigid registration" -MEASURE_MSG = "Measuring error" -SAVING_IMG_MSG = "Saving images" - -PROCESS_IMG_MSG, NORM_IMG_MSG, DENOISE_MSG = valtils.pad_strings([PROCESS_IMG_MSG, NORM_IMG_MSG, DENOISE_MSG]) - - -def init_jvm(jar=None, mem_gb=10): - """Initialize JVM for BioFormats - """ - slide_io.init_jvm(jar=None, mem_gb=10) - - -def kill_jvm(): - """Kill JVM for BioFormats - """ - slide_io.kill_jvm() - - -def load_registrar(src_f): - """Load a Valis object - - Parameters - ---------- - src_f : string - Path to pickled Valis object - - Returns - ------- - registrar : Valis - - Valis object used for registration - - """ - try: - registrar = pickle.load(open(src_f, 'rb')) - except jpype._core.JVMNotRunning: - init_jvm() - registrar = pickle.load(open(src_f, 'rb')) - - data_dir = registrar.data_dir - read_data_dir = os.path.split(src_f)[0] - - # If registrar has moved, will need to update paths to results - # and displacement fields - if data_dir != read_data_dir: - new_dst_dir = os.path.split(read_data_dir)[0] - registrar.dst_dir = new_dst_dir - registrar.set_dst_paths() - - for slide_obj in registrar.slide_dict.values(): - slide_obj.update_results_img_paths() - - return registrar - - -class Slide(object): - """Stores registration info and warps slides/points - - `Slide` is a class that stores registration parameters - and other metadata about a slide. Once registration has been - completed, `Slide` is also able warp the slide and/or points - using the same registration parameters. Warped slides can be saved - as ome.tiff images with valid ome-xml. - - Attributes - ---------- - src_f : str - Path to slide. - - image: ndarray - Image to registered. Taken from a level in the image pyramid. - However, image may be resized to fit within the `max_image_dim_px` - argument specified when creating a `Valis` object. - - val_obj : Valis - The "parent" object that registers all of the slide. - - reader : SlideReader - Object that can read slides and collect metadata. - - original_xml : str - Xml string created by bio-formats - - img_type : str - Whether the image is "brightfield" or "fluorescence" - - is_rgb : bool - Whether or not the slide is RGB. - - slide_shape_rc : tuple of int - Dimensions of the largest resolution in the slide, in the form - of (row, col). - - series : int - Slide series to be read - - slide_dimensions_wh : ndarray - Dimensions of all images in the pyramid (width, height). - - resolution : float - Physical size of each pixel. - - units : str - Physical unit of each pixel. - - name : str - Name of the image. Usually `img_f` but with the extension removed. - - processed_img : ndarray - Image used to perform registration - - rigid_reg_mask : ndarray - Mask of convex hulls covering tissue in unregistered image. - Could be used to mask `processed_img` before rigid registration - - non_rigid_reg_mask : ndarray - Created by combining rigidly warped `rigid_reg_mask` in all - other slides. - - stack_idx : int - Position of image in sorted Z-stack - - processed_img_f : str - Path to thumbnail of the processed `image`. - - rigid_reg_img_f : str - Path to thumbnail of rigidly aligned `image`. - - non_rigid_reg_img_f : str - Path to thumbnail of non-rigidly aligned `image`. - - processed_img_shape_rc : tuple of int - Shape (row, col) of the processed image used to find the - transformation parameters. Maximum dimension will be less or - equal to the `max_processed_image_dim_px` specified when - creating a `Valis` object. As such, this may be smaller than - the image's shape. - - aligned_slide_shape_rc : tuple of int - Shape (row, col) of aligned slide, based on the dimensions in the 0th - level of they pyramid. In - - reg_img_shape_rc : tuple of int - Shape (row, col) of the registered image - - M : ndarray - Rigid transformation matrix that aligns `image` to the previous - image in the stack. Found using the processed copy of `image`. - - bk_dxdy : ndarray - (2, N, M) numpy array of pixel displacements in - the x and y directions. dx = bk_dxdy[0], and dy=bk_dxdy[1]. Used - to warp images. Found using the rigidly aligned version of the - processed image. - - fwd_dxdy : ndarray - Inverse of `bk_dxdy`. Used to warp points. - - _bk_dxdy_f : str - Path to file containing bk_dxdy, if saved - - _fwd_dxdy_f : str - Path to file containing fwd_dxdy, if saved - - _bk_dxdy_np : ndarray - `bk_dxdy` as a numpy array. Only not None if `bk_dxdy` becomes - associated with a file - - _fwd_dxdy_np : ndarray - `fwd_dxdy` as a numpy array. Only not None if `fwd_dxdy` becomes - associated with a file - - stored_dxdy : bool - Whether or not the non-rigid displacements are saved in a file - Should only occur if image is very large. - - fixed_slide : Slide - Slide object to which this one was aligned. - - xy_matched_to_prev : ndarray - Coordinates (x, y) of features in `image` that had matches in the - previous image. Will have shape (N, 2) - - xy_in_prev : ndarray - Coordinates (x, y) of features in the previous that had matches - to those in `image`. Will have shape (N, 2) - - xy_matched_to_prev_in_bbox : ndarray - Subset of `xy_matched_to_prev` that were within `overlap_mask_bbox_xywh`. - Will either have shape (N, 2) or (M, 2), with M < N. - - xy_in_prev_in_bbox : ndarray - Subset of `xy_in_prev` that were within `overlap_mask_bbox_xywh`. - Will either have shape (N, 2) or (M, 2), with M < N. - - crop : str - Crop method - - bg_px_pos_rc : tuple - Position of pixel that has the background color - - bg_color : list, optional - Color of background pixels - - is_empty : bool - True if the image is empty (i.e. contains only 1 value) - - """ - - def __init__(self, src_f, image, val_obj, reader, name=None): - """ - Parameters - ---------- - src_f : str - Path to slide. - - image: ndarray - Image to registered. Taken from a level in the image pyramid. - However, image may be resized to fit within the `max_image_dim_px` - argument specified when creating a `Valis` object. - - val_obj : Valis - The "parent" object that registers all of the slide. - - reader : SlideReader - Object that can read slides and collect metadata. - - name : str, optional - Name of slide. If None, it will be `src_f` with the extension removed - - """ - - self.src_f = src_f - self.image = image - self.val_obj = val_obj - self.reader = reader - - # Metadata # - self.is_rgb = reader.metadata.is_rgb - self.img_type = reader.guess_image_type() - self.slide_shape_rc = reader.metadata.slide_dimensions[0][::-1] - self.series = reader.series - self.slide_dimensions_wh = reader.metadata.slide_dimensions - self.resolution = np.mean(reader.metadata.pixel_physical_size_xyu[0:2]) - self.units = reader.metadata.pixel_physical_size_xyu[2] - self.original_xml = reader.metadata.original_xml - - if self.is_rgb and self.image.dtype != np.uint8: - self.image = exposure.rescale_intensity(self.image, out_range=np.uint8) - - if name is None: - name = valtils.get_name(src_f) - - self.name = name - - # To be filled in during registration # - self.processed_img = None - self.rigid_reg_mask = None - self.non_rigid_reg_mask = None - self.stack_idx = None - - self.aligned_slide_shape_rc = None - self.processed_img_shape_rc = None - self.reg_img_shape_rc = None - self.M = None - self.bk_dxdy = None - self.fwd_dxdy = None - - self.stored_dxdy = False - self._bk_dxdy_f = None - self._fwd_dxdy_f = None - self._bk_dxdy_np = None - self._fwd_dxdy_np = None - self.processed_img_f = None - self.rigid_reg_img_f = None - self.non_rigid_reg_img_f = None - - self.fixed_slide = None - self.xy_matched_to_prev = None - self.xy_in_prev = None - self.xy_matched_to_prev_in_bbox = None - self.xy_in_prev_in_bbox = None - - self.crop = None - self.bg_px_pos_rc = (0, 0) - self.bg_color = None - - self.is_empty = self.check_if_empty(image) - - self.processed_crop_bbox = None - self.uncropped_processed_img_shape_rc = None - self.rigid_cropped = False - self.M_for_cropped = None - self.rigid_reg_cropped_shape_rc = None - - def __repr__(self): - repr_str = (f'<{self.__class__.__name__}, name = {self.name}>' - f', width={self.slide_dimensions_wh[0][0]}' - f', height={self.slide_dimensions_wh[0][1]}' - f', channels={self.reader.metadata.n_channels}' - f', levels={len(self.slide_dimensions_wh)}' - f', RGB={self.is_rgb}' - f', dtype={self.image.dtype}>' - ) - return (repr_str) - - def check_if_empty(self, img): - """Check if the image is empty - - Return - ------ - is_empty : bool - Whether or not the image is empty - - """ - - is_empty = img.min() == img.max() - - return is_empty - - def slide2image(self, level, series=None, xywh=None): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - - img = self.reader.slide2image(level=level, series=series, xywh=xywh) - - return img - - def slide2vips(self, level, series=None, xywh=None): - """Convert slide to pyvips.Image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - vips_img = self.reader.slide2vips(level=level, series=series, xywh=xywh) - - return vips_img - - def get_aligned_to_ref_slide_crop_xywh(self, ref_img_shape_rc, ref_M, scaled_ref_img_shape_rc=None): - """Get bounding box used to crop slide to fit in reference image - - Parameters - ---------- - ref_img_shape_rc : tuple of int - shape of reference image used to find registration parameters, i.e. processed image) - - ref_M : ndarray - Transformation matrix for the reference image - - scaled_ref_img_shape_rc : tuple of int, optional - shape of scaled image with shape `img_shape_rc`, i.e. slide corresponding - to the image used to find the registration parameters. - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - mask : ndarray - Mask covering reference image - - """ - - mask , _ = self.val_obj.get_crop_mask(CROP_REF) - - if scaled_ref_img_shape_rc is not None: - sxy = np.array([*scaled_ref_img_shape_rc[::-1]]) / np.array([*ref_img_shape_rc[::-1]]) - else: - scaled_ref_img_shape_rc = ref_img_shape_rc - sxy = np.ones(2) - - reg_txy = -ref_M[0:2, 2] - slide_xywh = (*reg_txy*sxy, *scaled_ref_img_shape_rc[::-1]) - - return slide_xywh, mask - - def get_overlap_crop_xywh(self, warped_img_shape_rc, scaled_warped_img_shape_rc=None): - """Get bounding box used to crop slide to where all slides overlap - - Parameters - ---------- - warped_img_shape_rc : tuple of int - shape of registered image - - warped_scaled_img_shape_rc : tuple of int, optional - shape of scaled registered image (i.e. registered slied) - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - """ - mask , mask_bbox_xywh = self.val_obj.get_crop_mask(CROP_OVERLAP) - - if scaled_warped_img_shape_rc is not None: - sxy = np.array([*scaled_warped_img_shape_rc[::-1]]) / np.array([*warped_img_shape_rc[::-1]]) - else: - sxy = np.ones(2) - - to_slide_transformer = transform.SimilarityTransform(scale=sxy) - overlap_bbox = warp_tools.bbox2xy(mask_bbox_xywh) - scaled_overlap_bbox = to_slide_transformer(overlap_bbox) - scaled_overlap_xywh = warp_tools.xy2bbox(scaled_overlap_bbox) - - scaled_overlap_xywh[2:] = np.ceil(scaled_overlap_xywh[2:]) - scaled_overlap_xywh = tuple(scaled_overlap_xywh.astype(int)) - - return scaled_overlap_xywh, mask - - def get_crop_xywh(self, crop, out_shape_rc=None): - """Get bounding box used to crop aligned slide - - Parameters - ---------- - - out_shape_rc : tuple of int, optional - If crop is "reference", this should be the shape of scaled reference image, such - as the unwarped slide that corresponds to the unwarped processed reference image. - - If crop is "overlap", this should be the shape of the registered slides. - - - Returns - ------- - crop_xywh : tuple of int - Bounding box of crop area (XYWH) - - mask : ndarray - Mask, before crop - """ - - ref_slide = self.val_obj.get_ref_slide() - if crop == CROP_REF: - transformation_shape_rc = np.array(ref_slide.processed_img_shape_rc) - crop_xywh, mask = self.get_aligned_to_ref_slide_crop_xywh(ref_img_shape_rc=transformation_shape_rc, - ref_M=ref_slide.M, - scaled_ref_img_shape_rc=out_shape_rc) - elif crop == CROP_OVERLAP: - transformation_shape_rc = np.array(ref_slide.reg_img_shape_rc) - crop_xywh, mask = self.get_overlap_crop_xywh(warped_img_shape_rc=transformation_shape_rc, - scaled_warped_img_shape_rc=out_shape_rc) - - return crop_xywh, mask - - def get_crop_method(self, crop): - """Get string or logic defining how to crop the image - """ - if crop is True: - crop_method = self.crop - else: - crop_method = crop - - do_crop = crop_method in [CROP_REF, CROP_OVERLAP] - - if do_crop: - return crop_method - else: - return False - - def get_bg_color_px_pos(self, cspace="Hunter LAB"): - """Get position of pixel that has color used for background - """ - if self.img_type == slide_tools.IHC_NAME: - # RGB. Get brightest pixel - mean_rgb, color_mask, filtered_label_counts, color_clusterer = preprocessing.find_dominant_colors(self.image, cspace=cspace, return_xy_clusterer=True) - mean_jab = preprocessing.rgb2jab(mean_rgb, cspace=cspace) - mean_jch = colour.models.Jab_to_JCh(mean_jab) - - # Find highest luminosity (L) and lowest colorfulness - bg_idx = np.lexsort([mean_jch[:, 1], -mean_jch[:, 0]])[0] # Last column sorted 1st. Returns ascending order - self.bg_color = mean_rgb[bg_idx, :] - - else: - # IF. Get darkest pixel - sum_img = self.image.sum(axis=2) - bg_px = np.unravel_index(np.argmin(sum_img, axis=None), sum_img.shape) - - self.bg_px_pos_rc = bg_px - self.bg_color = list(self.image[bg_px]) - - def update_results_img_paths(self): - n_digits = len(str(self.val_obj.size)) - stack_id = str.zfill(str(self.stack_idx), n_digits) - - self.processed_img_f = os.path.join(self.val_obj.processed_dir, self.name + ".png") - self.rigid_reg_img_f = os.path.join(self.val_obj.reg_dst_dir, f"{stack_id}_f{self.name}.png") - self.non_rigid_reg_img_f = os.path.join(self.val_obj.non_rigid_dst_dir, f"{stack_id}_f{self.name}.png") - if self.stored_dxdy: - bk_dxdy_f, fwd_dxdy_f = self.get_displacement_f() - self._bk_dxdy_f = bk_dxdy_f - self._fwd_dxdy_f = fwd_dxdy_f - - def get_displacement_f(self): - bk_dxdy_f = os.path.join(self.val_obj.displacements_dir, f"{self.name}_bk_dxdy.tiff") - fwd_dxdy_f = os.path.join(self.val_obj.displacements_dir, f"{self.name}_fwd_dxdy.tiff") - - return bk_dxdy_f, fwd_dxdy_f - - def get_bk_dxdy(self): - if self._bk_dxdy_np is None and not self.stored_dxdy: - return None - - elif self.stored_dxdy: - bk_dxdy_f, _ = self.get_displacement_f() - cropped_bk_dxdy = pyvips.Image.new_from_file(bk_dxdy_f) - full_bk_dxdy = self.val_obj.pad_displacement(cropped_bk_dxdy, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - - else: - if np.any(self._bk_dxdy_np.shape[1:2] != self.val_obj._full_displacement_shape_rc): - full_bk_dxdy = self.val_obj.pad_displacement(self._bk_dxdy_np, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - else: - full_bk_dxdy = self._bk_dxdy_np - - return full_bk_dxdy - - - def set_bk_dxdy(self, bk_dxdy): - """ - Only set if an array - """ - if not isinstance(bk_dxdy, pyvips.Image): - self._bk_dxdy_np = bk_dxdy - else: - print(f"Cannot set bk_dxdy when data is type {type(bk_dxdy)}") - - bk_dxdy = property(fget=get_bk_dxdy, - fset=set_bk_dxdy, - doc="Get and set backwards displacements") - - def get_fwd_dxdy(self): - if self._fwd_dxdy_np is None and not self.stored_dxdy: - return None - - elif self.stored_dxdy: - _, fwd_dxdy_f = self.get_displacement_f() - cropped_fwd_dxdy = pyvips.Image.new_from_file(fwd_dxdy_f) - full_fwd_dxdy = self.val_obj.pad_displacement(cropped_fwd_dxdy, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - - else: - if np.any(self._fwd_dxdy_np.shape[1:2] != self.val_obj._full_displacement_shape_rc): - full_fwd_dxdy = self.val_obj.pad_displacement(self._fwd_dxdy_np, - self.val_obj._full_displacement_shape_rc, - self.val_obj._non_rigid_bbox) - else: - full_fwd_dxdy = self._fwd_dxdy_np - - return full_fwd_dxdy - - - def set_fwd_dxdy(self, fwd_dxdy): - if not isinstance(fwd_dxdy, pyvips.Image): - self._fwd_dxdy_np = fwd_dxdy - else: - print(f"Cannot set fwd_dxdy when data is type {type(fwd_dxdy)}") - - fwd_dxdy = property(fget=get_fwd_dxdy, - fset=set_fwd_dxdy, - doc="Get forward displacements") - - def warp_img(self, img=None, non_rigid=True, crop=True, interp_method="bicubic"): - """Warp an image using the registration parameters - - img : ndarray, optional - The image to be warped. If None, then Slide.image - will be warped. - - non_rigid : bool - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - Returns - ------- - warped_img : ndarray - Warped copy of `img` - - """ - - if img is None: - img = self.image - - if non_rigid: - dxdy = self.bk_dxdy - else: - dxdy = None - - if isinstance(img, pyvips.Image): - img_shape_rc = (img.height, img.width) - img_dim = img.bands - else: - img_shape_rc = img.shape[0:2] - img_dim = img.ndim - - ref_slide = self.val_obj.get_ref_slide() - - if self == ref_slide and crop == CROP_REF and np.all(warp_tools.get_shape(img)[0:2] == self.processed_img_shape_rc): - # Save on computation time and avoid interpolation/rounding issues and return the original image - return img - - if not np.all(img_shape_rc == self.processed_img_shape_rc): - msg = ("scaling transformation for image with different shape. " - "However, without knowing all of other image's shapes, " - "the scaling may not be the same for all images, and so " - "may not overlap." - ) - valtils.print_warning(msg) - same_shape = False - img_scale_rc = np.array(img_shape_rc)/(np.array(self.processed_img_shape_rc)) - out_shape_rc = self.val_obj.get_aligned_slide_shape(img_scale_rc) - - else: - same_shape = True - out_shape_rc = self.reg_img_shape_rc - - if isinstance(crop, bool) or isinstance(crop, str): - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - if not same_shape: - scaled_shape_rc = np.array(ref_slide.processed_img_shape_rc)*img_scale_rc - else: - scaled_shape_rc = ref_slide.processed_img_shape_rc - elif crop_method == CROP_OVERLAP: - scaled_shape_rc = out_shape_rc - - bbox_xywh, _ = self.get_crop_xywh(crop=crop_method, out_shape_rc=scaled_shape_rc) - else: - bbox_xywh = None - - elif isinstance(crop[0], (int, float)) and len(crop) == 4: - bbox_xywh = crop - else: - bbox_xywh = None - - if img_dim == self.image.ndim: - bg_color = self.bg_color - else: - bg_color = None - - warped_img = \ - warp_tools.warp_img(img, M=self.M, - bk_dxdy=dxdy, - out_shape_rc=out_shape_rc, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - bbox_xywh=bbox_xywh, - bg_color=bg_color, - interp_method=interp_method) - - return warped_img - - def warp_img_from_to(self, img, to_slide_obj, - dst_slide_level=0, non_rigid=True, interp_method="bicubic", bg_color=None): - - """Warp an image from this slide onto another unwarped slide - - Note that if `img` is a labeled image/mask then it is recommended to set `interp_method` to "nearest" - - Parameters - ---------- - img : ndarray, pyvips.Image - Image to warp. Should be a scaled version of the same one used for registration - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image that `img` will be warped on to - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - """ - - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(dst_slide_level) - else: - - to_slide_src_shape_rc = np.array(dst_slide_level) - - dst_scale_rc = (to_slide_src_shape_rc/np.array(to_slide_obj.processed_img_shape_rc)) - aligned_slide_shape = np.round(dst_scale_rc*np.array(to_slide_obj.reg_img_shape_rc)).astype(int) - - if non_rigid: - from_bk_dxdy = self.bk_dxdy - to_fwd_dxdy = to_slide_obj.fwd_dxdy - - else: - from_bk_dxdy = None - to_fwd_dxdy = None - - warped_img = \ - warp_tools.warp_img_from_to(img, - from_M=self.M, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_bk_dxdy=from_bk_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_fwd_dxdy=to_fwd_dxdy, - bg_color=bg_color, - interp_method=interp_method - ) - - return warped_img - - @valtils.deprecated_args(crop_to_overlap="crop") - def warp_slide(self, level, non_rigid=True, crop=True, - src_f=None, interp_method="bicubic", reader=None): - """Warp a slide using registration parameters - - Parameters - ---------- - level : int - Pyramid level to be warped - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - src_f : str, optional - Path of slide to be warped. If None (the default), Slide.src_f - will be used. Otherwise, the file to which `src_f` points to should - be an alternative copy of the slide, such as one that has undergone - processing (e.g. stain segmentation), has a mask applied, etc... - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - """ - if src_f is None: - src_f = self.src_f - - if non_rigid: - bk_dxdy = self.bk_dxdy - else: - bk_dxdy = None - - if level != 0: - if not np.issubdtype(type(level), np.integer): - msg = "Need slide level to be an integer indicating pyramid level" - valtils.print_warning(msg) - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if isinstance(crop, bool) or isinstance(crop, str): - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[level][::-1] - - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - slide_bbox_xywh, _ = self.get_crop_xywh(crop=crop_method, - out_shape_rc=scaled_aligned_shape_rc) - - if crop_method == CROP_REF: - assert np.all(slide_bbox_xywh[2:] == scaled_aligned_shape_rc[::-1]) - if src_f == self.src_f and self == ref_slide: - # Shouldn't need to warp, but do checks just in case - no_rigid = True - no_non_rigid = True - if self.M is not None: - sxy = (scaled_aligned_shape_rc/self.processed_img_shape_rc)[::-1] - scaled_txy = sxy*self.M[:2, 2] - no_transforms = all(self.M[:2, :2].reshape(-1) == [1, 0, 0, 1]) - crop_to_origin = np.all(np.abs(slide_bbox_xywh[0:2] + scaled_txy) < 1) - no_rigid = no_transforms and crop_to_origin - - if self.bk_dxdy is not None: - no_non_rigid = self.bk_dxdy.min() == 0 and self.bk_dxdy.max() == 0 - - if no_rigid and no_non_rigid: - # Don't need to warp, so return original reference image - ref_img = self.reader.slide2vips(level=level) - return ref_img - - else: - slide_bbox_xywh = None - - elif isinstance(crop[0], (int, float)) and len(crop) == 4: - slide_bbox_xywh = crop - else: - slide_bbox_xywh = None - - if src_f == self.src_f: - bg_color = self.bg_color - else: - bg_color = None - - if reader is None: - reader = self.reader - - warped_slide = slide_tools.warp_slide(src_f, M=self.M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - aligned_slide_shape_rc=aligned_slide_shape, - dxdy=bk_dxdy, level=level, series=self.series, - interp_method=interp_method, - bbox_xywh=slide_bbox_xywh, - bg_color=bg_color, - reader=reader) - return warped_slide - - def warp_and_save_slide(self, dst_f, level=0, non_rigid=True, - crop=True, src_f=None, - channel_names=None, - colormap=slide_io.CMAP_AUTO, - interp_method="bicubic", - tile_wh=None, - compression=DEFAULT_COMPRESSION, - Q=100, - pyramid=True, - reader=None): - - """Warp and save a slide - - Slides will be saved in the ome.tiff format. - - Parameters - ---------- - dst_f : str - Path to were the warped slide will be saved. - - level : int - Pyramid level to be warped - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True - - crop: bool, str - How to crop the registered images. If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initializing the `Valis object`. - - channel_names : list, optional - List of channel names. If None, then Slide.reader - will attempt to find the channel names associated with `src_f`. - - colormap : dict, optional - Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. - If None (default), the channel colors from `current_ome_xml_str` will be used, if available. - If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added - - src_f : str, optional - Path of slide to be warped. If None (the default), Slide.src_f - will be used. Otherwise, the file to which `src_f` points to should - be an alternative copy of the slide, such as one that has undergone - processing (e.g. stain segmentation), has a mask applied, etc... - - interp_method : str - Interpolation method used when warping slide. Default is "bicubic" - - tile_wh : int, optional - Tile width and height used to save image - - compression : str - Compression method used to save ome.tiff. See pyips for more details. - - Q : int - Q factor for lossy compression - - pyramid : bool - Whether or not to save an image pyramid. - """ - - if src_f is None: - src_f = self.src_f - - if reader is None: - if src_f != self.src_f: - slide_reader_cls = slide_io.get_slide_reader(src_f) - reader = slide_reader_cls(src_f) - else: - reader = self.reader - - warped_slide = self.warp_slide(level=level, - non_rigid=non_rigid, - crop=crop, - interp_method=interp_method, - src_f=src_f, - reader=reader) - - # Get ome-xml # - ref_slide = self.val_obj.get_ref_slide() - pixel_physical_size_xyu = ref_slide.reader.scale_physical_size(level) - - ome_xml_obj = slide_io.update_xml_for_new_img(img=warped_slide, - reader=reader, - level=level, - channel_names=channel_names, - colormap=colormap, - pixel_physical_size_xyu=pixel_physical_size_xyu) - - ome_xml = ome_xml_obj.to_xml() - - out_shape_wh = warp_tools.get_shape(warped_slide)[0:2][::-1] - tile_wh = slide_io.get_tile_wh(reader=reader, - level=level, - out_shape_wh=out_shape_wh) - - slide_io.save_ome_tiff(warped_slide, dst_f=dst_f, ome_xml=ome_xml, - tile_wh=tile_wh, compression=compression, - Q=Q, pyramid=pyramid) - - - def warp_xy(self, xy, M=None, slide_level=0, pt_level=0, - non_rigid=True, crop=True): - """Warp points using registration parameters - - Warps `xy` to their location in the registered slide/image - - Parameters - ---------- - xy : ndarray - (N, 2) array of points to be warped. Must be x,y coordinates - - slide_level: int, tuple, optional - Pyramid level of the slide. Used to scale transformation matrices. - Can also be the shape of the warped image (row, col) into which - the points should be warped. Default is 0. - - pt_level: int, tuple, optional - Pyramid level from which the points origingated. For example, if - `xy` are from the centroids of cell segmentation performed on the - full resolution image, this should be 0. Alternatively, the value can - be a tuple of the image's shape (row, col) from which the points came. - For example, if `xy` are bounding box coordinates from an analysis on - a lower resolution image, then pt_level is that lower resolution - image's shape (row, col). Default is 0. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True. - - crop: bool, str - Apply crop to warped points by shifting points to the mask's origin. - Note that this can result in negative coordinates, but might be useful - if wanting to draw the coordinates on the registered slide, such as - annotation coordinates. - - If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - """ - if M is None: - M = self.M - - if np.issubdtype(type(pt_level), np.integer): - pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] - else: - pt_dim_rc = np.array(pt_level) - - if np.issubdtype(type(slide_level), np.integer): - if slide_level != 0: - if np.issubdtype(type(slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(slide_level) - else: - aligned_slide_shape = np.array(slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - else: - aligned_slide_shape = np.array(slide_level) - - if non_rigid: - fwd_dxdy = self.fwd_dxdy - else: - fwd_dxdy = None - - warped_xy = warp_tools.warp_xy(xy, M=M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - src_shape_rc=pt_dim_rc, - dst_shape_rc=aligned_slide_shape, - fwd_dxdy=fwd_dxdy) - - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - if isinstance(slide_level, int): - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[slide_level][::-1] - else: - if len(slide_level) == 2: - scaled_aligned_shape_rc = slide_level - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) - warped_xy -= crop_bbox_xywh[0:2] - - return warped_xy - - def warp_xy_from_to(self, xy, to_slide_obj, src_slide_level=0, src_pt_level=0, - dst_slide_level=0, non_rigid=True): - - """Warp points from this slide to another unwarped slide - - Takes a set of points found in this unwarped slide, and warps them to - their position in the unwarped "to" slide. - - Parameters - ---------- - xy : ndarray - (N, 2) array of points to be warped. Must be x,y coordinates - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - src_pt_level: int, tuple, optional - Pyramid level of the slide/image in which `xy` originated. - For example, if `xy` are from the centroids of cell segmentation - performed on the unwarped full resolution image, this should be 0. - Alternatively, the value can be a tuple of the image's shape (row, col) - from which the points came. For example, if `xy` are bounding - box coordinates from an analysis on a lower resolution image, - then pt_level is that lower resolution image's shape (row, col). - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image in to `xy` will be warped. - Similar to `src_pt_level`, if `dst_slide_level` is an int then - the points will be warped to that pyramid level. If `dst_slide_level` - is the "to" image's shape (row, col), then the points will be warped - to their location in an image with that same shape. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - """ - - if np.issubdtype(type(src_pt_level), np.integer): - src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] - else: - src_pt_dim_rc = np.array(src_pt_level) - - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - else: - to_slide_src_shape_rc = np.array(dst_slide_level) - - if src_slide_level != 0: - if np.issubdtype(type(src_slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(src_slide_level) - else: - aligned_slide_shape = np.array(src_slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if non_rigid: - src_fwd_dxdy = self.fwd_dxdy - dst_bk_dxdy = to_slide_obj.bk_dxdy - - else: - src_fwd_dxdy = None - dst_bk_dxdy = None - - xy_in_unwarped_to_img = \ - warp_tools.warp_xy_from_to(xy=xy, - from_M=self.M, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_src_shape_rc=src_pt_dim_rc, - from_fwd_dxdy=src_fwd_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_dst_shape_rc=aligned_slide_shape, - to_bk_dxdy=dst_bk_dxdy - ) - - return xy_in_unwarped_to_img - - def warp_geojson(self, geojson_f, M=None, slide_level=0, pt_level=0, - non_rigid=True, crop=True): - """Warp geometry using registration parameters - - Warps geometries to their location in the registered slide/image - - Parameters - ---------- - geojson_f : str - Path to geojson file containing the annotation geometries. Assumes - coordinates are in pixels. - - slide_level: int, tuple, optional - Pyramid level of the slide. Used to scale transformation matrices. - Can also be the shape of the warped image (row, col) into which - the points should be warped. Default is 0. - - pt_level: int, tuple, optional - Pyramid level from which the points origingated. For example, if - `xy` are from the centroids of cell segmentation performed on the - full resolution image, this should be 0. Alternatively, the value can - be a tuple of the image's shape (row, col) from which the points came. - For example, if `xy` are bounding box coordinates from an analysis on - a lower resolution image, then pt_level is that lower resolution - image's shape (row, col). Default is 0. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. Default is True. - - crop: bool, str - Apply crop to warped points by shifting points to the mask's origin. - Note that this can result in negative coordinates, but might be useful - if wanting to draw the coordinates on the registered slide, such as - annotation coordinates. - - If `True`, then the same crop used - when initializing the `Valis` object will be used. If `False`, the - image will not be cropped. If "overlap", the warped slide will be - cropped to include only areas where all images overlapped. - "reference" crops to the area that overlaps with the reference image, - defined by `reference_img_f` when initialzing the `Valis object`. - - """ - if M is None: - M = self.M - - if np.issubdtype(type(pt_level), np.integer): - pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] - else: - pt_dim_rc = np.array(pt_level) - - if np.issubdtype(type(slide_level), np.integer): - if slide_level != 0: - if np.issubdtype(type(slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(slide_level) - else: - aligned_slide_shape = np.array(slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - else: - aligned_slide_shape = np.array(slide_level) - - if non_rigid: - fwd_dxdy = self.fwd_dxdy - else: - fwd_dxdy = None - - with open(geojson_f) as f: - annotation_geojson = json.load(f) - - crop_method = self.get_crop_method(crop) - if crop_method is not False: - if crop_method == CROP_REF: - ref_slide = self.val_obj.get_ref_slide() - if isinstance(slide_level, int): - scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[slide_level][::-1] - else: - if len(slide_level) == 2: - scaled_aligned_shape_rc = slide_level - elif crop_method == CROP_OVERLAP: - scaled_aligned_shape_rc = aligned_slide_shape - - crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) - shift_xy = crop_bbox_xywh[0:2] - else: - shift_xy = None - - warped_features = [None]*len(annotation_geojson["features"]) - for i, ft in tqdm.tqdm(enumerate(annotation_geojson["features"]), desc=WARP_ANNO_MSG, unit="annotation"): - geom = shapely.geometry.shape(ft["geometry"]) - warped_geom = warp_tools.warp_shapely_geom(geom, M=M, - transformation_src_shape_rc=self.processed_img_shape_rc, - transformation_dst_shape_rc=self.reg_img_shape_rc, - src_shape_rc=pt_dim_rc, - dst_shape_rc=aligned_slide_shape, - fwd_dxdy=fwd_dxdy, - shift_xy=shift_xy) - warped_ft = deepcopy(ft) - warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) - warped_features[i] = warped_ft - - warped_geojson = {"type":annotation_geojson["type"], "features":warped_features} - - return warped_geojson - - def warp_geojson_from_to(self, geojson_f, to_slide_obj, src_slide_level=0, src_pt_level=0, - dst_slide_level=0, non_rigid=True): - """Warp geoms in geojson file from annotation slide to another unwarped slide - - Takes a set of geometries found in this annotation slide, and warps them to - their position in the unwarped "to" slide. - - Parameters - ---------- - geojson_f : str - Path to geojson file containing the annotation geometries. Assumes - coordinates are in pixels. - - to_slide_obj : Slide - Slide to which the points will be warped. I.e. `xy` - will be warped from this Slide to their position in - the unwarped slide associated with `to_slide_obj`. - - src_pt_level: int, tuple, optional - Pyramid level of the slide/image in which `xy` originated. - For example, if `xy` are from the centroids of cell segmentation - performed on the unwarped full resolution image, this should be 0. - Alternatively, the value can be a tuple of the image's shape (row, col) - from which the points came. For example, if `xy` are bounding - box coordinates from an analysis on a lower resolution image, - then pt_level is that lower resolution image's shape (row, col). - - dst_slide_level: int, tuple, optional - Pyramid level of the slide/image in to `xy` will be warped. - Similar to `src_pt_level`, if `dst_slide_level` is an int then - the points will be warped to that pyramid level. If `dst_slide_level` - is the "to" image's shape (row, col), then the points will be warped - to their location in an image with that same shape. - - non_rigid : bool, optional - Whether or not to conduct non-rigid warping. If False, - then only a rigid transformation will be applied. - - Returns - ------- - warped_geojson : dict - Dictionry of warped geojson geometries - - """ - - if np.issubdtype(type(src_pt_level), np.integer): - src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] - else: - src_pt_dim_rc = np.array(src_pt_level) +"""Valis pipeline orchestrator. - if np.issubdtype(type(dst_slide_level), np.integer): - to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][::-1] - else: - to_slide_src_shape_rc = np.array(dst_slide_level) - - if src_slide_level != 0: - if np.issubdtype(type(src_slide_level), np.integer): - aligned_slide_shape = self.val_obj.get_aligned_slide_shape(src_slide_level) - else: - aligned_slide_shape = np.array(src_slide_level) - else: - aligned_slide_shape = self.aligned_slide_shape_rc - - if non_rigid: - src_fwd_dxdy = self.fwd_dxdy - dst_bk_dxdy = to_slide_obj.bk_dxdy +Import ``Valis`` and ``RegistrationConfig`` from ``valis.registration`` +rather than this sub-module. +""" - else: - src_fwd_dxdy = None - dst_bk_dxdy = None - - with open(geojson_f) as f: - annotation_geojson = json.load(f) - - warped_features = [None]*len(annotation_geojson["features"]) - for i, ft in tqdm.tqdm(enumerate(annotation_geojson["features"]), desc=WARP_ANNO_MSG, unit="annotation"): - geom = shapely.geometry.shape(ft["geometry"]) - warped_geom = warp_tools.warp_shapely_geom_from_to(geom=geom, - from_M=self.M, - from_transformation_dst_shape_rc=self.reg_img_shape_rc, - from_transformation_src_shape_rc=self.processed_img_shape_rc, - from_dst_shape_rc=aligned_slide_shape, - from_src_shape_rc=src_pt_dim_rc, - from_fwd_dxdy=src_fwd_dxdy, - to_M=to_slide_obj.M, - to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, - to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, - to_src_shape_rc=to_slide_src_shape_rc, - to_dst_shape_rc=aligned_slide_shape, - to_bk_dxdy=dst_bk_dxdy - ) - - warped_ft = deepcopy(ft) - warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) - warped_features[i] = warped_ft - - warped_geojson = {"type":annotation_geojson["type"], "features":warped_features} - - return warped_geojson - - def pad_cropped_processed_img(self): - """ - Pad cropped processed image to have original dimensions - """ - vips_img = warp_tools.numpy2vips(self.processed_img) +import logging +from typing import Optional, Union - padded = vips_img.embed(self.processed_crop_bbox[0], self.processed_crop_bbox[1], - self.uncropped_processed_img_shape_rc[1], self.uncropped_processed_img_shape_rc[0], - extend=pyvips.enums.Extend.BLACK - ) - scaled_padded = warp_tools.resize_img(padded, self.processed_img_shape_rc) - scaled_padded_np = warp_tools.vips2numpy(scaled_padded) +import os +import pathlib +import pickle +import re +import traceback +from copy import deepcopy +from itertools import chain +from pprint import pformat +from time import time - return scaled_padded_np +import cv2 +import colour +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pyvips +import shapely +import tqdm +from colorama import Fore +from scipy import ndimage +from skimage import color as skcolor, exposure, filters, transform + +from .. import feature_matcher +from .. import feature_detectors +from .. import micro_rigid_registrar +from .. import non_rigid_registrars +from .. import preprocessing +from .. import serial_non_rigid +from .. import serial_rigid +from .. import slide_io +from .. import slide_tools +from .. import valtils +from .. import viz +from .. import warp_tools + +from ._constants import ( + CONVERTED_IMG_DIR, + PROCESSED_IMG_DIR, + RIGID_REG_IMG_DIR, + NON_RIGID_REG_IMG_DIR, + DEFORMATION_FIELD_IMG_DIR, + OVERLAP_IMG_DIR, + REG_RESULTS_DATA_DIR, + MICRO_REG_DIR, + DISPLACEMENT_DIRS, + MASK_DIR, + DEFAULT_BRIGHTFIELD_CLASS, + DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, + DEFAULT_FLOURESCENCE_CLASS, + DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + DEFAULT_NORM_METHOD, + DEFAULT_FD, + DEFAULT_TRANSFORM_CLASS, + DEFAULT_MATCHER, + DEFAULT_MATCHER_FOR_SORTING, + DEFAULT_SIMILARITY_METRIC, + DEFAULT_AFFINE_OPTIMIZER_CLASS, + DEFAULT_MAX_PROCESSED_IMG_SIZE, + DEFAULT_MAX_IMG_DIM, + DEFAULT_THUMBNAIL_SIZE, + DEFAULT_MAX_NON_RIGID_REG_SIZE, + DEFAULT_MAX_MICRO_REG_SIZE, + DEFAULT_MIN_RIGID_MATCHES, + TILER_THRESH_GB, + DEFAULT_NR_TILE_WH, + AFFINE_OPTIMIZER_KEY, + TRANSFORMER_KEY, + SIM_METRIC_KEY, + FD_KEY, + MATCHER_KEY, + MATCHER_FOR_SORTING_KEY, + NAME_KEY, + IMAGES_ORDERD_KEY, + REF_IMG_KEY, + QT_EMMITER_KEY, + TFORM_SRC_SHAPE_KEY, + TFORM_DST_SHAPE_KEY, + TFORM_MAT_KEY, + CHECK_REFLECT_KEY, + MIN_RIGID_MATCHES_KEY, + NON_RIGID_REG_CLASS_KEY, + NON_RIGID_REG_PARAMS_KEY, + NON_RIGID_USE_XY_KEY, + NON_RIGID_COMPOSE_KEY, + DEFAULT_NON_RIGID_CLASS, + DEFAULT_NON_RIGID_KWARGS, + DEFAULT_COMPRESSION, + CROP_OVERLAP, + CROP_REF, + CROP_NONE, + CropMode, + WARP_ANNO_MSG, + CONVERT_MSG, + DENOISE_MSG, + PROCESS_IMG_MSG, + NORM_IMG_MSG, + TRANSFORM_MSG, + PREP_NON_RIGID_MSG, + MEASURE_MSG, + SAVING_IMG_MSG, +) +from .slide import Slide +from .state import DisplacementField, RegistrationConfig, load_registrar + +logger = logging.getLogger(__name__) + + +_UNSET = object() class Valis(object): @@ -1508,8 +122,8 @@ class Valis(object): Implements the registration pipeline described in "VALIS: Virtual Alignment of pathoLogy Image Series" by Gatenbee et al. - This pipeline will read images and whole slide images (WSI) using pyvips, - bioformats, or openslide, and so should work with a wide variety of formats. + This pipeline will read images and whole slide images (WSI) using pyvips + or openslide, and so should work with a wide variety of formats. VALIS can perform both rigid and non-rigid registration. The registered slides can be saved as ome.tiff slides that can be used in downstream analyses. The ome.tiff format is opensource and widely supported, being readable in several @@ -1549,6 +163,37 @@ class Valis(object): In addition to warping images and slides, VALIS can also warp point data, such as cell centoids or ROI coordinates. + Choosing a warp method + ---------------------- + After calling ``register()``, use whichever method matches your target: + + ========================================== ==================================== + Goal Method + ========================================== ==================================== + Warp a numpy image array ``Slide.warp_img()`` + Warp between two specific slides ``Slide.warp_img_from_to()`` + Transform (x, y) point coordinates ``Slide.warp_xy()`` + Transform GeoJSON annotation geometries ``Slide.warp_geojson()`` + Warp all slides and write to disk ``Valis.warp_and_save_slides()`` + Merge multi-channel slides into one file ``Valis.warp_and_merge_slides()`` + ========================================== ==================================== + + Choosing a crop mode + -------------------- + The ``crop`` parameter (accepts a string or ``CropMode`` enum) controls how + the registered output is trimmed: + + ======================== ================================================ + Value Behaviour + ======================== ================================================ + ``"overlap"`` Keep only the region where **all** images overlap. + Best for series where slides shift significantly. + ``"reference"`` Keep the region that overlaps with the reference + image. Good when one slide is "ground truth". + ``"all"`` No cropping — preserve every pixel (may contain + black fill where slides do not overlap). + ======================== ================================================ + Attributes ---------- name : str @@ -1759,38 +404,50 @@ class Valis(object): View ome.tiff, located at merged_slide_dst_f """ - @valtils.deprecated_args(max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px", img_type="image_type") - def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, - feature_detector_cls=None, - transformer_cls=DEFAULT_TRANSFORM_CLASS, - affine_optimizer_cls=DEFAULT_AFFINE_OPTIMIZER_CLASS, - similarity_metric=DEFAULT_SIMILARITY_METRIC, - matcher=DEFAULT_MATCHER, - matcher_for_sorting=DEFAULT_MATCHER_FOR_SORTING, - imgs_ordered=False, - non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, - non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, - compose_non_rigid=False, - img_list=None, - reference_img_f=None, - align_to_reference=False, - do_rigid=True, - crop=None, - create_masks=True, - denoise_rigid=False, - crop_for_rigid_reg=True, - check_for_reflections=False, - resolution_xyu=None, - slide_dims_dict_wh=None, - max_image_dim_px=DEFAULT_MAX_IMG_DIM, - max_processed_image_dim_px=DEFAULT_MAX_PROCESSED_IMG_SIZE, - max_non_rigid_registration_dim_px=DEFAULT_MAX_NON_RIGID_REG_SIZE, - thumbnail_size=DEFAULT_THUMBNAIL_SIZE, - norm_method=DEFAULT_NORM_METHOD, - micro_rigid_registrar_cls=None, - micro_rigid_registrar_params={}, - qt_emitter=None): + @valtils.deprecated_args( + max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px", + img_type="image_type", + ) + def __init__( + self, + src_dir: Union[str, pathlib.Path], + dst_dir: Union[str, pathlib.Path], + series: Optional[int] = None, + name: Optional[str] = None, + image_type: Optional[str] = None, + config: Optional["RegistrationConfig"] = None, + feature_detector_cls=None, + transformer_cls=DEFAULT_TRANSFORM_CLASS, + affine_optimizer_cls=DEFAULT_AFFINE_OPTIMIZER_CLASS, + similarity_metric=DEFAULT_SIMILARITY_METRIC, + matcher=DEFAULT_MATCHER, + matcher_for_sorting=DEFAULT_MATCHER_FOR_SORTING, + imgs_ordered=_UNSET, + non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, + non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, + compose_non_rigid=_UNSET, + img_list=None, + reference_img_f=None, + align_to_reference=False, + do_rigid=_UNSET, + crop=None, + create_masks=True, + denoise_rigid=_UNSET, + crop_for_rigid_reg=_UNSET, + check_for_reflections=_UNSET, + min_rigid_matches=DEFAULT_MIN_RIGID_MATCHES, + resolution_xyu=None, + slide_dims_dict_wh=None, + max_image_dim_px=DEFAULT_MAX_IMG_DIM, + max_processed_image_dim_px=DEFAULT_MAX_PROCESSED_IMG_SIZE, + max_non_rigid_registration_dim_px=DEFAULT_MAX_NON_RIGID_REG_SIZE, + thumbnail_size=DEFAULT_THUMBNAIL_SIZE, + norm_method=DEFAULT_NORM_METHOD, + micro_rigid_registrar_cls=None, + micro_rigid_registrar_params={}, + qt_emitter=None, + ): """ src_dir: str Path to directory containing the slides that will be registered. @@ -2008,6 +665,73 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, """ + # Apply RegistrationConfig values as defaults for any param still at its + # sentinel (the module-level default). Explicit keyword args take precedence. + if config is not None: + if feature_detector_cls is None: + feature_detector_cls = config.feature_detector_cls + if matcher is DEFAULT_MATCHER: + matcher = config.matcher + if matcher_for_sorting is DEFAULT_MATCHER_FOR_SORTING: + matcher_for_sorting = config.matcher_for_sorting + if transformer_cls is DEFAULT_TRANSFORM_CLASS: + transformer_cls = config.transformer_cls + if affine_optimizer_cls is DEFAULT_AFFINE_OPTIMIZER_CLASS: + affine_optimizer_cls = config.affine_optimizer_cls + if similarity_metric == DEFAULT_SIMILARITY_METRIC: + similarity_metric = config.similarity_metric + if non_rigid_registrar_cls is DEFAULT_NON_RIGID_CLASS: + non_rigid_registrar_cls = config.non_rigid_registrar_cls + if non_rigid_reg_params is DEFAULT_NON_RIGID_KWARGS: + non_rigid_reg_params = config.non_rigid_reg_params + if compose_non_rigid is _UNSET: + compose_non_rigid = config.compose_non_rigid + if micro_rigid_registrar_cls is None: + micro_rigid_registrar_cls = config.micro_rigid_registrar_cls + if not micro_rigid_registrar_params: + micro_rigid_registrar_params = config.micro_rigid_registrar_params + if imgs_ordered is _UNSET: + imgs_ordered = config.imgs_ordered + if do_rigid is _UNSET: + do_rigid = config.do_rigid + if denoise_rigid is _UNSET: + denoise_rigid = config.denoise_rigid + if crop_for_rigid_reg is _UNSET: + crop_for_rigid_reg = config.crop_for_rigid_reg + if check_for_reflections is _UNSET: + check_for_reflections = config.check_for_reflections + if max_image_dim_px == DEFAULT_MAX_IMG_DIM: + max_image_dim_px = config.max_image_dim_px + if max_processed_image_dim_px == DEFAULT_MAX_PROCESSED_IMG_SIZE: + max_processed_image_dim_px = config.max_processed_image_dim_px + if max_non_rigid_registration_dim_px == DEFAULT_MAX_NON_RIGID_REG_SIZE: + max_non_rigid_registration_dim_px = ( + config.max_non_rigid_registration_dim_px + ) + if crop is None and config.crop is not None: + crop = config.crop + if create_masks: + create_masks = config.create_masks + if thumbnail_size == DEFAULT_THUMBNAIL_SIZE: + thumbnail_size = config.thumbnail_size + if norm_method == DEFAULT_NORM_METHOD: + norm_method = config.norm_method + + # Resolve any boolean kwargs left at the _UNSET sentinel (i.e. neither + # explicitly passed nor overridden by a config) to their real defaults. + if compose_non_rigid is _UNSET: + compose_non_rigid = False + if imgs_ordered is _UNSET: + imgs_ordered = False + if do_rigid is _UNSET: + do_rigid = True + if denoise_rigid is _UNSET: + denoise_rigid = False + if crop_for_rigid_reg is _UNSET: + crop_for_rigid_reg = True + if check_for_reflections is _UNSET: + check_for_reflections = False + # Get name, based on src directory if name is None: if src_dir.endswith(os.path.sep): @@ -2029,11 +753,25 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, elif hasattr(img_list, "__iter__"): self.original_img_list = list(img_list) else: - msg = (f"Cannot upack `img_list`, which is type {type(img_list).__name__}. " - "Please provide an iterable object (list, tuple, array, etc...) that has the location of the images") - valtils.print_warning(msg, rgb=Fore.RED) + msg = ( + f"Cannot upack `img_list`, which is type {type(img_list).__name__}. " + "Please provide an iterable object (list, tuple, array, etc...) that has the location of the images" + ) + logger.warning(msg) else: + src_path = pathlib.Path(src_dir) + if not src_path.exists(): + raise FileNotFoundError(f"src_dir does not exist: {src_dir}") + if not src_path.is_dir(): + raise NotADirectoryError(f"src_dir is not a directory: {src_dir}") self.get_imgs_in_dir() + if len(self.original_img_list) == 0: + raise ValueError(f"No supported images found in {src_dir}") + if len(self.original_img_list) < 2: + raise ValueError( + f"At least 2 images are required for registration, " + f"but only {len(self.original_img_list)} was found in {src_dir}" + ) if reference_img_f is not None: img_names = [valtils.get_name(f) for f in self.original_img_list] @@ -2048,15 +786,16 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, self.original_img_list.append(reference_img_f) else: print_f = os.path.split(reference_img_f)[1] - msg = (f"{print_f} specified as the reference image, " - f"but it is not in `img_list`. Not possible to append {print_f} to `img_list` " - f"because `imgs_ordered`={imgs_ordered}. " - f"Please add {print_f} to `img_list` at the desired location, " - f"or set `imgs_ordered`=False if order of image alignment is not already known") - valtils.print_warning(msg, rgb=Fore.RED) + msg = ( + f"{print_f} specified as the reference image, " + f"but it is not in `img_list`. Not possible to append {print_f} to `img_list` " + f"because `imgs_ordered`={imgs_ordered}. " + f"Please add {print_f} to `img_list` at the desired location, " + f"or set `imgs_ordered`=False if order of image alignment is not already known" + ) + logger.warning(msg) return None - self.original_img_list = [str(x) for x in self.original_img_list] if self.name_dict is None: self.name_dict = self.get_img_names(self.original_img_list) @@ -2085,7 +824,7 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, if max_image_dim_px < max_processed_image_dim_px: msg = f"max_image_dim_px is {max_image_dim_px} but needs to be less or equal to {max_processed_image_dim_px}. Setting max_image_dim_px to {max_processed_image_dim_px}" - valtils.print_warning(msg) + logger.warning(msg) max_image_dim_px = max_processed_image_dim_px self.max_image_dim_px = max_image_dim_px @@ -2112,26 +851,30 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, fd = feature_detector_cls if matcher.feature_detector is not None: - msg = (f"Replacing feature detector in {matcher.__class__.__name__} with {fd.__class__.__name__}, " - f"which was previously {matcher.feature_detector.__class__.__name__}. " - f"To avoid this, set `feature_detector_cls=None` when initializing the `Valis` object") - valtils.print_warning(msg, None) + msg = ( + f"Replacing feature detector in {matcher.__class__.__name__} with {fd.__class__.__name__}, " + f"which was previously {matcher.feature_detector.__class__.__name__}. " + f"To avoid this, set `feature_detector_cls=None` when initializing the `Valis` object" + ) + logger.warning(msg) matcher.feature_detector = fd else: fd = None - self._set_rigid_reg_kwargs(name=name, - feature_detector=fd, - similarity_metric=similarity_metric, - matcher=matcher, - matcher_for_sorting=matcher_for_sorting, - transformer=transformer_cls, - affine_optimizer=affine_optimizer_cls, - imgs_ordered=imgs_ordered, - reference_img_f=reference_img_f, - check_for_reflections=check_for_reflections, - qt_emitter=qt_emitter) - + self._set_rigid_reg_kwargs( + name=name, + feature_detector=fd, + similarity_metric=similarity_metric, + matcher=matcher, + matcher_for_sorting=matcher_for_sorting, + transformer=transformer_cls, + affine_optimizer=affine_optimizer_cls, + imgs_ordered=imgs_ordered, + reference_img_f=reference_img_f, + check_for_reflections=check_for_reflections, + min_rigid_matches=min_rigid_matches, + qt_emitter=qt_emitter, + ) # Setup non-rigid registration # self.non_rigid_registrar = None @@ -2149,12 +892,14 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, self.compose_non_rigid = compose_non_rigid if non_rigid_registrar_cls is not None: - self._set_non_rigid_reg_kwargs(name=name, - non_rigid_reg_class=non_rigid_registrar_cls, - non_rigid_reg_params=non_rigid_reg_params, - reference_img_f=reference_img_f, - compose_non_rigid=compose_non_rigid, - qt_emitter=qt_emitter) + self._set_non_rigid_reg_kwargs( + name=name, + non_rigid_reg_class=non_rigid_registrar_cls, + non_rigid_reg_params=non_rigid_reg_params, + reference_img_f=reference_img_f, + compose_non_rigid=compose_non_rigid, + qt_emitter=qt_emitter, + ) # Info realted to saving images to view results # self.mask_dict = None @@ -2181,16 +926,26 @@ def __init__(self, src_dir, dst_dir, series=None, name=None, image_type=None, self._empty_slides = {} def __repr__(self): - repr_str = (f'<{self.__class__.__name__}, name = {self.name}>' - f', size={self.size}>' - ) - return (repr_str) - - def _set_rigid_reg_kwargs(self, name, feature_detector, similarity_metric, - matcher, matcher_for_sorting, transformer, affine_optimizer, - imgs_ordered, reference_img_f, - check_for_reflections, qt_emitter): - + repr_str = ( + f"<{self.__class__.__name__}, name = {self.name}>" f", size={self.size}>" + ) + return repr_str + + def _set_rigid_reg_kwargs( + self, + name, + feature_detector, + similarity_metric, + matcher, + matcher_for_sorting, + transformer, + affine_optimizer, + imgs_ordered, + reference_img_f, + check_for_reflections, + qt_emitter, + min_rigid_matches=DEFAULT_MIN_RIGID_MATCHES, + ): """Set rigid registration kwargs Keyword arguments will be passed to `serial_rigid.register_images` @@ -2201,31 +956,44 @@ def _set_rigid_reg_kwargs(self, name, feature_detector, similarity_metric, else: afo = affine_optimizer - self.rigid_reg_kwargs = {NAME_KEY: name, - FD_KEY: feature_detector, - SIM_METRIC_KEY: similarity_metric, - TRANSFORMER_KEY: transformer(), - MATCHER_KEY: matcher, - MATCHER_FOR_SORTING_KEY: matcher_for_sorting, - AFFINE_OPTIMIZER_KEY: afo, - REF_IMG_KEY: reference_img_f, - IMAGES_ORDERD_KEY: imgs_ordered, - CHECK_REFLECT_KEY: check_for_reflections, - QT_EMMITER_KEY: qt_emitter - } + self.rigid_reg_kwargs = { + NAME_KEY: name, + FD_KEY: feature_detector, + SIM_METRIC_KEY: similarity_metric, + TRANSFORMER_KEY: transformer(), + MATCHER_KEY: matcher, + MATCHER_FOR_SORTING_KEY: matcher_for_sorting, + AFFINE_OPTIMIZER_KEY: afo, + REF_IMG_KEY: reference_img_f, + IMAGES_ORDERD_KEY: imgs_ordered, + CHECK_REFLECT_KEY: check_for_reflections, + MIN_RIGID_MATCHES_KEY: min_rigid_matches, + QT_EMMITER_KEY: qt_emitter, + } # Save methods as strings since some objects cannot be pickled # - self.feature_descriptor_str = self.rigid_reg_kwargs[MATCHER_KEY].feature_detector.kp_descriptor_name - self.feature_detector_str = self.rigid_reg_kwargs[MATCHER_KEY].feature_detector.kp_detector_name + self.feature_descriptor_str = self.rigid_reg_kwargs[ + MATCHER_KEY + ].feature_detector.kp_descriptor_name + self.feature_detector_str = self.rigid_reg_kwargs[ + MATCHER_KEY + ].feature_detector.kp_detector_name self.transform_str = self.rigid_reg_kwargs[TRANSFORMER_KEY].__class__.__name__ self.similarity_metric = self.rigid_reg_kwargs[SIM_METRIC_KEY] self.match_filter_method = matcher.__class__.__name__ self.imgs_ordered = imgs_ordered - def _set_non_rigid_reg_kwargs(self, name, non_rigid_reg_class, non_rigid_reg_params, - reference_img_f, compose_non_rigid, qt_emitter): + def _set_non_rigid_reg_kwargs( + self, + name, + non_rigid_reg_class, + non_rigid_reg_params, + reference_img_f, + compose_non_rigid, + qt_emitter, + ): """Set non-rigid registration kwargs Keyword arguments will be passed to `serial_non_rigid.register_images` @@ -2242,13 +1010,14 @@ def _set_non_rigid_reg_kwargs(self, name, non_rigid_reg_class, non_rigid_reg_par else: nr = None - self.non_rigid_reg_kwargs = {NAME_KEY: name, - NON_RIGID_REG_CLASS_KEY: nr, - NON_RIGID_REG_PARAMS_KEY: non_rigid_reg_params, - REF_IMG_KEY: reference_img_f, - QT_EMMITER_KEY: qt_emitter, - NON_RIGID_COMPOSE_KEY: compose_non_rigid - } + self.non_rigid_reg_kwargs = { + NAME_KEY: name, + NON_RIGID_REG_CLASS_KEY: nr, + NON_RIGID_REG_PARAMS_KEY: non_rigid_reg_params, + REF_IMG_KEY: reference_img_f, + QT_EMMITER_KEY: qt_emitter, + NON_RIGID_COMPOSE_KEY: compose_non_rigid, + } self.non_rigid_reg_class_str = nr.__class__.__name__ @@ -2272,11 +1041,11 @@ def _add_empty_slides(self): self.slide_dict[slide_name] = slide_obj def get_imgs_in_dir(self): - """Get all images in Valis.src_dir - - """ + """Get all images in Valis.src_dir""" - full_path_list = [os.path.join(self.src_dir, f) for f in os.listdir(self.src_dir)] + full_path_list = [ + os.path.join(self.src_dir, f) for f in os.listdir(self.src_dir) + ] self.original_img_list = [] img_names = [] for f in full_path_list: @@ -2295,8 +1064,13 @@ def get_imgs_in_dir(self): else: # Some formats, like .mrxs have the main file but # data in a subdirectory with the same name - escape_dir = re.escape(dir_name) # Remove special characters - matching_f = [ff for ff in full_path_list if re.search(escape_dir, ff) is not None and os.path.split(ff)[1] != dir_name] + escape_dir = re.escape(dir_name) # Remove special characters + matching_f = [ + ff + for ff in full_path_list + if re.search(escape_dir, ff) is not None + and os.path.split(ff)[1] != dir_name + ] if len(matching_f) == 1: if not matching_f[0] in self.original_img_list: @@ -2306,24 +1080,29 @@ def get_imgs_in_dir(self): elif len(matching_f) > 1: msg = f"found {len(matching_f)} matches for {dir_name}: {', '.join(matching_f)}" - valtils.print_warning(msg, rgb=Fore.RED) + logger.warning(msg) elif len(matching_f) == 0: msg = f"Can't find slide file associated with {dir_name}" - valtils.print_warning(msg, rgb=Fore.RED) + logger.warning(msg) # Final check to make sure that all images are readable - self.original_img_list = [f for f in self.original_img_list if slide_tools.get_slide_extension(f) in slide_io.ALL_READABLE_FORMATS and len(slide_tools.get_slide_extension(f)) > 0] + self.original_img_list = [ + f + for f in self.original_img_list + if slide_tools.get_slide_extension(f) in slide_io.ALL_READABLE_FORMATS + and len(slide_tools.get_slide_extension(f)) > 0 + ] def set_dst_paths(self): - """Set paths to where the results will be saved. - - """ + """Set paths to where the results will be saved.""" self.img_dir = os.path.join(self.dst_dir, CONVERTED_IMG_DIR) self.processed_dir = os.path.join(self.dst_dir, PROCESSED_IMG_DIR) self.reg_dst_dir = os.path.join(self.dst_dir, RIGID_REG_IMG_DIR) self.non_rigid_dst_dir = os.path.join(self.dst_dir, NON_RIGID_REG_IMG_DIR) - self.deformation_field_dir = os.path.join(self.dst_dir, DEFORMATION_FIELD_IMG_DIR) + self.deformation_field_dir = os.path.join( + self.dst_dir, DEFORMATION_FIELD_IMG_DIR + ) self.overlap_dir = os.path.join(self.dst_dir, OVERLAP_IMG_DIR) self.data_dir = os.path.join(self.dst_dir, REG_RESULTS_DATA_DIR) self.displacements_dir = os.path.join(self.dst_dir, DISPLACEMENT_DIRS) @@ -2362,6 +1141,8 @@ def get_slide(self, src_f): default_name = valtils.get_name(src_f) + slide_obj = None + if src_f in self.name_dict.keys(): # src_f is full path to image assigned_name = self.name_dict[src_f] @@ -2387,18 +1168,25 @@ def get_slide(self, src_f): elif default_name in self._dup_names_dict: # default name has multiple matches n_matching = len(self._dup_names_dict[default_name]) - possible_names_dict = {f: self.name_dict[f] for f in self._dup_names_dict[default_name]} - - msg = (f"\n{src_f} matches {n_matching} images in this dataset:\n" - f"{pformat(self._dup_names_dict[default_name])}" - f"\n\nPlease see `Valis.name_dict` to find correct name in " - f"the dictionary. Either key (filenmae) or value (assigned name) will work:\n" - f"{pformat(possible_names_dict)}") - - valtils.print_warning(msg, rgb=Fore.RED) + possible_names_dict = { + f: self.name_dict[f] for f in self._dup_names_dict[default_name] + } + + msg = ( + f"\n{src_f} matches {n_matching} images in this dataset:\n" + f"{pformat(self._dup_names_dict[default_name])}" + f"\n\nPlease see `Valis.name_dict` to find correct name in " + f"the dictionary. Either key (filenmae) or value (assigned name) will work:\n" + f"{pformat(possible_names_dict)}" + ) + + logger.warning(msg) slide_obj = None - return slide_obj + if slide_obj: + return slide_obj + + raise KeyError(f"{src_f} not found") def get_ref_slide(self): ref_slide = self.get_slide(self.reference_img_f) @@ -2426,8 +1214,9 @@ def get_img_names(self, img_list): """ - img_df = pd.DataFrame({"img_f": img_list, - "name": [valtils.get_name(f) for f in img_list]}) + img_df = pd.DataFrame( + {"img_f": img_list, "name": [valtils.get_name(f) for f in img_list]} + ) names_dict = {f: valtils.get_name(f) for f in img_list} count_df = img_df["name"].value_counts() @@ -2439,12 +1228,12 @@ def get_img_names(self, img_list): z = len(str(len(dup_paths))) msg = f"Detected {len(dup_paths)} images that would be named {dup_name}" - valtils.print_warning(msg, rgb=Fore.RED) + logger.warning(msg) for j, p in enumerate(dup_paths): new_name = f"{names_dict[p]}_{str(j).zfill(z)}" msg = f"Renmaing {p} to {new_name} in Valis.slide_dict)" - valtils.print_warning(msg) + logger.warning(msg) names_dict[p] = new_name return names_dict @@ -2463,25 +1252,26 @@ def check_for_duplicated_names(self, img_list): else: default_names_dict[default_name].append(f) - self._dup_names_dict = {k: v for k, v in default_names_dict.items() if len(v) > 1} + self._dup_names_dict = { + k: v for k, v in default_names_dict.items() if len(v) > 1 + } - def create_img_reader_dict(self, reader_dict=None, default_reader=None, series=None): + def create_img_reader_dict( + self, reader_dict=None, default_reader=None, series=None + ): if reader_dict is None: named_reader_dict = {} else: - named_reader_dict = {valtils.get_name(f): reader_dict[f] for f in reader_dict.keys()} + named_reader_dict = { + valtils.get_name(f): reader_dict[f] for f in reader_dict.keys() + } for i, slide_f in enumerate(self.original_img_list): slide_name = valtils.get_name(slide_f) if slide_name not in named_reader_dict: if default_reader is None: - try: - slide_reader_cls = slide_io.get_slide_reader(slide_f, series=series) - except Exception as e: - traceback_msg = traceback.format_exc() - msg = f"Attempting to get reader for {slide_f} created the following error:\n{e}" - valtils.print_warning(msg, rgb=Fore.RED, traceback_msg=traceback_msg) + slide_reader_cls = slide_io.get_slide_reader(slide_f, series=series) else: slide_reader_cls = default_reader @@ -2489,7 +1279,9 @@ def create_img_reader_dict(self, reader_dict=None, default_reader=None, series=N else: slide_reader_info = named_reader_dict[slide_name] - if isinstance(slide_reader_info, list) or isinstance(slide_reader_info, tuple): + if isinstance(slide_reader_info, list) or isinstance( + slide_reader_info, tuple + ): if len(slide_reader_info) == 2: slide_reader_cls, slide_reader_kwargs = slide_reader_info elif len(slide_reader_info) == 1: @@ -2503,12 +1295,7 @@ def create_img_reader_dict(self, reader_dict=None, default_reader=None, series=N # Provided reader, but no kwargs slide_reader = slide_reader_info slide_reader_kwargs = {} - try: - slide_reader = slide_reader_cls(src_f=slide_f, **slide_reader_kwargs) - except Exception as e: - traceback_msg = traceback.format_exc() - msg = f"Attempting to read {slide_f} created the following error:\n{e}" - valtils.print_warning(msg, rgb=Fore.RED, traceback_msg=traceback_msg) + slide_reader = slide_reader_cls(src_f=slide_f, **slide_reader_kwargs) named_reader_dict[slide_name] = slide_reader @@ -2532,16 +1319,18 @@ def convert_imgs(self, series=None, reader_dict=None, reader_cls=None): """ - named_reader_dict = self.create_img_reader_dict(reader_dict=reader_dict, - default_reader=reader_cls, - series=series) + named_reader_dict = self.create_img_reader_dict( + reader_dict=reader_dict, default_reader=reader_cls, series=series + ) img_types = [] self.size = 0 for f in tqdm.tqdm(self.original_img_list, desc=CONVERT_MSG, unit="image"): slide_name = valtils.get_name(f) reader = named_reader_dict[slide_name] slide_dims = reader.metadata.slide_dimensions - levels_in_range = np.where(slide_dims.max(axis=1) <= self.max_image_dim_px)[0] + levels_in_range = np.where(slide_dims.max(axis=1) <= self.max_image_dim_px)[ + 0 + ] if len(levels_in_range) > 0: level = levels_in_range[0] - 1 @@ -2552,23 +1341,25 @@ def convert_imgs(self, series=None, reader_dict=None, reader_cls=None): vips_img = reader.slide2vips(level=level) - scaling = np.min(self.max_image_dim_px/np.array([vips_img.width, vips_img.height])) + scaling = np.min( + self.max_image_dim_px / np.array([vips_img.width, vips_img.height]) + ) if scaling < 1: vips_img = warp_tools.rescale_img(vips_img, scaling) img = warp_tools.vips2numpy(vips_img) - - - slide_name = self.name_dict[f] slide_obj = Slide(f, img, self, reader, name=slide_name) slide_obj.crop = self.crop # Will overwrite data if provided. Can occur if reading images, not the actual slides # if self.slide_dims_dict_wh is not None: - matching_slide = [k for k in self.slide_dims_dict_wh.keys() - if valtils.get_name(k) == slide_obj.name][0] + matching_slide = [ + k + for k in self.slide_dims_dict_wh.keys() + if valtils.get_name(k) == slide_obj.name + ][0] slide_dims = self.slide_dims_dict_wh[matching_slide] if slide_dims.ndim == 1: @@ -2581,7 +1372,7 @@ def convert_imgs(self, series=None, reader_dict=None, reader_cls=None): if slide_obj.is_empty: msg = f"{slide_obj.name} appears to be empty and will be skipped during registration" - valtils.print_warning(msg) + logger.warning(msg) self._empty_slides[slide_obj.name] = slide_obj continue @@ -2606,30 +1397,31 @@ def check_img_max_dims(self): """ - og_img_sizes_wh = np.array([slide_obj.image.shape[0:2][::-1] for slide_obj in self.slide_dict.values()]) + og_img_sizes_wh = np.array( + [slide_obj.image.shape[0:2][::-1] for slide_obj in self.slide_dict.values()] + ) img_max_dims = og_img_sizes_wh.max(axis=1) min_max_wh = img_max_dims.min() - scaling_for_og_imgs = min_max_wh/img_max_dims + scaling_for_og_imgs = min_max_wh / img_max_dims if np.any(scaling_for_og_imgs < 1): msg = f"Smallest image is less than max_image_dim_px. parameter max_image_dim_px is being set to {min_max_wh}" - valtils.print_warning(msg) + logger.warning(msg) self.max_image_dim_px = min_max_wh for slide_obj in self.slide_dict.values(): # Rescale images - scaling = self.max_image_dim_px/max(slide_obj.image.shape[0:2]) + scaling = self.max_image_dim_px / max(slide_obj.image.shape[0:2]) assert scaling <= self.max_image_dim_px if scaling < 1: slide_obj.image = warp_tools.rescale_img(slide_obj.image, scaling) if self.max_processed_image_dim_px > self.max_image_dim_px: msg = f"parameter max_processed_image_dim_px also being updated to {self.max_image_dim_px}" - valtils.print_warning(msg) + logger.warning(msg) self.max_processed_image_dim_px = self.max_image_dim_px def create_original_composite_img(self, rigid_registrar): - """Create imaage showing how images overlap before registration - """ + """Create imaage showing how images overlap before registration""" min_r = np.inf max_r = 0 @@ -2637,13 +1429,22 @@ def create_original_composite_img(self, rigid_registrar): max_c = 0 composite_img_list = [None] * self.size - thumbnail_s = np.min(self.thumbnail_size/np.array(rigid_registrar.img_obj_list[0].padded_shape_rc)) + thumbnail_s = np.min( + self.thumbnail_size + / np.array(rigid_registrar.img_obj_list[0].padded_shape_rc) + ) for i, img_obj in enumerate(rigid_registrar.img_obj_list): img = img_obj.image - padded_img = transform.warp(img, img_obj.T, preserve_range=True, - output_shape=img_obj.padded_shape_rc) + padded_img = transform.warp( + img, + img_obj.T, + preserve_range=True, + output_shape=img_obj.padded_shape_rc, + ) - composite_img_list[i] = warp_tools.rescale_img(padded_img, scaling=thumbnail_s) + composite_img_list[i] = warp_tools.rescale_img( + padded_img, scaling=thumbnail_s + ) img_corners_rc = warp_tools.get_corners_of_image(img.shape[0:2]) warped_corners_xy = warp_tools.warp_xy(img_corners_rc[:, ::-1], img_obj.T) @@ -2653,34 +1454,38 @@ def create_original_composite_img(self, rigid_registrar): max_c = max(warped_corners_xy[:, 0].max(), max_c) overlap_img = self.draw_overlap_img(img_list=composite_img_list) - min_r = int(min_r*thumbnail_s) - max_r = int(np.ceil(max_r*thumbnail_s)) - min_c = int(min_c*thumbnail_s) - max_c = int(np.ceil(max_c*thumbnail_s)) + min_r = int(min_r * thumbnail_s) + max_r = int(np.ceil(max_r * thumbnail_s)) + min_c = int(min_c * thumbnail_s) + max_c = int(np.ceil(max_c * thumbnail_s)) overlap_img = overlap_img[min_r:max_r, min_c:max_c] return overlap_img def measure_original_mmi(self, img1, img2): - """Measure Mattes mutation inormation between 2 unregistered images. - """ + """Measure Mattes mutation inormation between 2 unregistered images.""" dst_rc = np.max([img1.shape, img2.shape], axis=1) padded_img_list = [None] * self.size for i, img in enumerate([img1, img2]): T = warp_tools.get_padding_matrix(img.shape, dst_rc) - padded_img = transform.warp(img, T, preserve_range=True, output_shape=dst_rc) + padded_img = transform.warp( + img, T, preserve_range=True, output_shape=dst_rc + ) padded_img_list[i] = padded_img og_mmi = warp_tools.mattes_mi(padded_img_list[0], padded_img_list[1]) return og_mmi - def create_img_processor_dict(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None): + def create_img_processor_dict( + self, + brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, + brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, + if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, + if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + processor_dict=None, + ): """Create dictionary to get processors for each image Create dictionary to get processors for each image. If an image is not in `processing_dict`, @@ -2720,7 +1525,9 @@ def create_img_processor_dict(self, brightfield_processing_cls=DEFAULT_BRIGHTFIE if processor_dict is None: named_processing_dict = {} else: - named_processing_dict = {self.get_slide(f).name: processor_dict[f] for f in processor_dict.keys()} + named_processing_dict = { + self.get_slide(f).name: processor_dict[f] for f in processor_dict.keys() + } for i, slide_obj in enumerate(self.slide_dict.values()): @@ -2749,7 +1556,10 @@ def create_img_processor_dict(self, brightfield_processing_cls=DEFAULT_BRIGHTFIE processing_cls = if_processing_cls processing_kwargs = if_processing_kwargs - named_processing_dict[slide_obj.name] = [processing_cls, processing_kwargs] + named_processing_dict[slide_obj.name] = [ + processing_cls, + processing_kwargs, + ] return named_processing_dict @@ -2757,18 +1567,29 @@ def get_roi_for_processing(self, slide_obj, processing_cls, mask=None): # First, create mask from whole image if mask is None: - mask_level = slide_tools.get_level_idx(slide_obj.slide_dimensions_wh, self.max_processed_image_dim_px) - 1 + mask_level = ( + slide_tools.get_level_idx( + slide_obj.slide_dimensions_wh, self.max_processed_image_dim_px + ) + - 1 + ) mask_level = max(0, mask_level) - mask_generator = processing_cls(image=slide_obj.image, - src_f=slide_obj.src_f, - level=mask_level, - series=slide_obj.series, - reader=slide_obj.reader) + mask_generator = processing_cls( + image=slide_obj.image, + src_f=slide_obj.src_f, + level=mask_level, + series=slide_obj.series, + reader=slide_obj.reader, + ) mask = mask_generator.create_mask() - mask_s = warp_tools.get_shape(mask)[0:2]/warp_tools.get_shape(slide_obj.image)[0:2] - assert np.isclose(mask_s[0], mask_s[1], atol=10**-2), print("mask does not appear to based on scaled copy of Slide's image") + mask_s = ( + warp_tools.get_shape(mask)[0:2] / warp_tools.get_shape(slide_obj.image)[0:2] + ) + assert np.isclose( + mask_s[0], mask_s[1], atol=10**-2 + ), "mask does not appear to based on scaled copy of Slide's image" if np.any(mask.shape[0:2] != slide_obj.image.shape[0:2]): mask = warp_tools.resize_img(mask, slide_obj.image.shape[0:2]) @@ -2778,16 +1599,20 @@ def get_roi_for_processing(self, slide_obj, processing_cls, mask=None): # Use mask to crop image # Determine how large image needs to be so that cropped region has same max dimension as the current image - crop_s = np.min(self.max_processed_image_dim_px/small_cropped_shape_wh) - crop_max_dim = np.max(np.array(mask.shape[0:2])*crop_s) + crop_s = np.min(self.max_processed_image_dim_px / small_cropped_shape_wh) + crop_max_dim = np.max(np.array(mask.shape[0:2]) * crop_s) # Draw bbox around crop - crop_level = slide_tools.get_level_idx(slide_obj.slide_dimensions_wh, crop_max_dim) - 1 + crop_level = ( + slide_tools.get_level_idx(slide_obj.slide_dimensions_wh, crop_max_dim) - 1 + ) crop_level = max(0, crop_level) - crop_resize_s = np.min(crop_max_dim/slide_obj.slide_dimensions_wh[crop_level]) - img_to_crop = warp_tools.rescale_img(slide_obj.slide2vips(level=crop_level), scaling=crop_resize_s) + crop_resize_s = np.min(crop_max_dim / slide_obj.slide_dimensions_wh[crop_level]) + img_to_crop = warp_tools.rescale_img( + slide_obj.slide2vips(level=crop_level), scaling=crop_resize_s + ) - crop_bbox = mask_bbox*crop_s + crop_bbox = mask_bbox * crop_s cropped_vips = img_to_crop.extract_area(*crop_bbox) cropped = warp_tools.vips2numpy(cropped_vips) @@ -2800,51 +1625,74 @@ def process_imgs(self, processor_dict): if os.path.exists(self.processed_dir): n_in_processed_dir = len(os.listdir(self.processed_dir)) if n_in_processed_dir != self.size: - msg = (f"It appears the destination directory, {self.dst_dir}, is being reused, but that the number of images (n={self.size})" - f" in this run differs than previous runs (n={n_in_processed_dir})." - f" This may result in confusing naming of thumbnail files, but the actual registration should be unaffected." - f" To avoid this, please either delete {self.dst_dir}, or change the" - f" `name` parameter when initializing the Valis object to something besides `None` or '{self.name}'") - valtils.print_warning(msg) + msg = ( + f"It appears the destination directory, {self.dst_dir}, is being reused, but that the number of images (n={self.size})" + f" in this run differs than previous runs (n={n_in_processed_dir})." + f" This may result in confusing naming of thumbnail files, but the actual registration should be unaffected." + f" To avoid this, please either delete {self.dst_dir}, or change the" + f" `name` parameter when initializing the Valis object to something besides `None` or '{self.name}'" + ) + logger.warning(msg) pathlib.Path(self.processed_dir).mkdir(exist_ok=True, parents=True) - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values(), desc=PROCESS_IMG_MSG, unit="image")): + for i, slide_obj in enumerate( + tqdm.tqdm(self.slide_dict.values(), desc=PROCESS_IMG_MSG, unit="image") + ): processing_cls, processing_kwargs = processor_dict[slide_obj.name] if self.crop_for_rigid_reg: slide_obj.rigid_cropped = True - img_to_process, mask, uncropped_unscaled_processed_shape_rc, uncropped_shape_rc, crop_bbox = self.get_roi_for_processing(slide_obj, processing_cls) + ( + img_to_process, + mask, + uncropped_unscaled_processed_shape_rc, + uncropped_shape_rc, + crop_bbox, + ) = self.get_roi_for_processing(slide_obj, processing_cls) else: slide_obj.rigid_cropped = False # Create later: mask, original_processed_shape_rc, uncropped_shape_rc, crop_bbox img_shape_rc = warp_tools.get_shape(slide_obj.image)[0:2] if np.max(img_shape_rc) > self.max_processed_image_dim_px: - processing_s = np.min(self.max_processed_image_dim_px/img_shape_rc) - img_to_process = warp_tools.rescale_img(slide_obj.image, processing_s) + processing_s = np.min( + self.max_processed_image_dim_px / img_shape_rc + ) + img_to_process = warp_tools.rescale_img( + slide_obj.image, processing_s + ) else: img_to_process = slide_obj.image - processing_level = slide_tools.get_level_idx(slide_obj.slide_dimensions_wh, self.max_processed_image_dim_px) - 1 + processing_level = ( + slide_tools.get_level_idx( + slide_obj.slide_dimensions_wh, self.max_processed_image_dim_px + ) + - 1 + ) processing_level = max(0, processing_level) - processor = processing_cls(image=img_to_process, - src_f=slide_obj.src_f, - level=processing_level, - series=slide_obj.series, - reader=slide_obj.reader) + processor = processing_cls( + image=img_to_process, + src_f=slide_obj.src_f, + level=processing_level, + series=slide_obj.series, + reader=slide_obj.reader, + ) try: processed_img = processor.process_image(**processing_kwargs) except TypeError: # processor.process_image doesn't take kwargs processed_img = processor.process_image() - processed_img = exposure.rescale_intensity(processed_img, out_range=(0, 255)).astype(np.uint8) + processed_img = exposure.rescale_intensity( + processed_img, out_range=(0, 255) + ).astype(np.uint8) # Ensure processed image shape is within specified limit processed_shape_rc = warp_tools.get_shape(processed_img)[0:2] if np.max(processed_shape_rc) > self.max_processed_image_dim_px: - s = np.min(self.max_processed_image_dim_px/processed_shape_rc) + s = np.min(self.max_processed_image_dim_px / processed_shape_rc) processed_img = warp_tools.rescale_img(processed_img, s) processed_shape_rc = warp_tools.get_shape(processed_img)[0:2] @@ -2855,8 +1703,10 @@ def process_imgs(self, processor_dict): if self.create_masks: mask = processor.create_mask() - mask_s = warp_tools.get_shape(mask)[0:2]/processed_shape_rc - assert np.isclose(mask_s[0], mask_s[1], atol=10**-2), print("mask does not appear to based on scaled copy of Slide's image") + mask_s = warp_tools.get_shape(mask)[0:2] / processed_shape_rc + assert np.isclose( + mask_s[0], mask_s[1], atol=10**-2 + ), "mask does not appear to based on scaled copy of Slide's image" if np.any(mask.shape[0:2] != processed_shape_rc): mask = warp_tools.resize_img(mask, processed_shape_rc) @@ -2877,18 +1727,27 @@ def process_imgs(self, processor_dict): # Save thumbnails of mask if self.crop_for_rigid_reg or self.create_masks: pathlib.Path(self.mask_dir).mkdir(exist_ok=True, parents=True) - thumbnail_mask = self.create_thumbnail(mask, thumbnail_size=self.thumbnail_size) + thumbnail_mask = self.create_thumbnail( + mask, thumbnail_size=self.thumbnail_size + ) if slide_obj.img_type == slide_tools.IHC_NAME: - thumbnail_img = self.create_thumbnail(slide_obj.image, thumbnail_size=self.thumbnail_size) + thumbnail_img = self.create_thumbnail( + slide_obj.image, thumbnail_size=self.thumbnail_size + ) else: - thumbnail_img = self.create_thumbnail(slide_obj.pad_cropped_processed_img(), thumbnail_size=self.thumbnail_size) + thumbnail_img = self.create_thumbnail( + slide_obj.pad_cropped_processed_img(), + thumbnail_size=self.thumbnail_size, + ) thumbnail_mask_outline = viz.draw_outline(thumbnail_img, thumbnail_mask) - outline_f_out = os.path.join(self.mask_dir, f'{slide_obj.name}.png') + outline_f_out = os.path.join(self.mask_dir, f"{slide_obj.name}.png") warp_tools.save_img(outline_f_out, thumbnail_mask_outline) if self.norm_method is not None: - self.target_processing_hist, self.target_processing_stats = self.normalize_images() + self.target_processing_hist, self.target_processing_stats = ( + self.normalize_images() + ) def crop_rigid_reg_mask(self, slide_obj, mask=None): if mask is None: @@ -2898,7 +1757,11 @@ def crop_rigid_reg_mask(self, slide_obj, mask=None): return mask vips_mask = warp_tools.numpy2vips(mask) - scaled_mask = warp_tools.resize_img(vips_mask, slide_obj.uncropped_processed_img_shape_rc, interp_method="nearest") + scaled_mask = warp_tools.resize_img( + vips_mask, + slide_obj.uncropped_processed_img_shape_rc, + interp_method="nearest", + ) cropped_mask = scaled_mask.extract_area(*slide_obj.processed_crop_bbox) if isinstance(mask, np.ndarray): cropped_mask = warp_tools.vips2numpy(cropped_mask) @@ -2906,10 +1769,14 @@ def crop_rigid_reg_mask(self, slide_obj, mask=None): return cropped_mask def denoise_images(self): - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values(), desc=DENOISE_MSG, unit="image")): + for i, slide_obj in enumerate( + tqdm.tqdm(self.slide_dict.values(), desc=DENOISE_MSG, unit="image") + ): if slide_obj.rigid_reg_mask is None: is_ihc = slide_obj.img_type == slide_tools.IHC_NAME - _, tissue_mask = preprocessing.create_tissue_mask(slide_obj.image, is_ihc) + _, tissue_mask = preprocessing.create_tissue_mask( + slide_obj.image, is_ihc + ) mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(tissue_mask)) c0, r0 = mask_bbox[:2] c1, r1 = mask_bbox[:2] + mask_bbox[2:] @@ -2918,13 +1785,16 @@ def denoise_images(self): else: denoise_mask = slide_obj.rigid_reg_mask - denoise_mask = self.crop_rigid_reg_mask(slide_obj=slide_obj, mask=denoise_mask) - denoised = preprocessing.denoise_img(slide_obj.processed_img, mask=denoise_mask) + denoise_mask = self.crop_rigid_reg_mask( + slide_obj=slide_obj, mask=denoise_mask + ) + denoised = preprocessing.denoise_img( + slide_obj.processed_img, mask=denoise_mask + ) warp_tools.save_img(slide_obj.processed_img_f, denoised) def normalize_images(self, all_histogram=None, all_img_stats=None): - """Normalize intensity values in images - """ + """Normalize intensity values in images""" img_list = [None] * self.size mask_list = [None] * self.size for i, slide_obj in enumerate(self.slide_dict.values()): @@ -2934,30 +1804,37 @@ def normalize_images(self, all_histogram=None, all_img_stats=None): mask_list[i] = self.crop_rigid_reg_mask(slide_obj) if all_histogram is None or all_img_stats is None: - all_histogram, all_img_stats = preprocessing.collect_img_stats(img_list, mask_list=mask_list) + all_histogram, all_img_stats = preprocessing.collect_img_stats( + img_list, mask_list=mask_list + ) - for i, slide_obj in enumerate(tqdm.tqdm(self.slide_dict.values(), desc=NORM_IMG_MSG, unit="image")): + for i, slide_obj in enumerate( + tqdm.tqdm(self.slide_dict.values(), desc=NORM_IMG_MSG, unit="image") + ): img = img_list[i] if self.norm_method == "histo_match": normed_img = preprocessing.match_histograms(img, all_histogram) elif self.norm_method == "img_stats": normed_img = preprocessing.norm_img_stats(img, all_img_stats) - normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype(np.uint8) + normed_img = exposure.rescale_intensity( + normed_img, out_range=(0, 255) + ).astype(np.uint8) slide_obj.processed_img = normed_img warp_tools.save_img(slide_obj.processed_img_f, normed_img) return all_histogram, all_img_stats - def create_thumbnail(self, img, rescale_color=False, thumbnail_size=DEFAULT_THUMBNAIL_SIZE): - """Create thumbnail image to view results - """ + def create_thumbnail( + self, img, rescale_color=False, thumbnail_size=DEFAULT_THUMBNAIL_SIZE + ): + """Create thumbnail image to view results""" is_vips = isinstance(img, pyvips.Image) img_shape = warp_tools.get_shape(img) - scaling = np.min(thumbnail_size/np.array(img_shape[:2])) + scaling = np.min(thumbnail_size / np.array(img_shape[:2])) if scaling < 1: thumbnail = warp_tools.rescale_img(img, scaling) else: @@ -2967,7 +1844,9 @@ def create_thumbnail(self, img, rescale_color=False, thumbnail_size=DEFAULT_THUM if is_vips: # Convert to numpy to rescale thumbnail = warp_tools.vips2numpy(img) - thumbnail = exposure.rescale_intensity(thumbnail, out_range=(0, 255)).astype(np.uint8) + thumbnail = exposure.rescale_intensity( + thumbnail, out_range=(0, 255) + ).astype(np.uint8) if is_vips: # Convert back to pyvips @@ -2985,7 +1864,9 @@ def draw_overlap_img(self, img_list, blending="weighted"): overlap_img = viz.create_overlap_img(img_list, cmap=cmap, blending=blending) overlap_img = exposure.equalize_adapthist(overlap_img) - overlap_img = exposure.rescale_intensity(overlap_img, out_range=(0, 255)).astype(np.uint8) + overlap_img = exposure.rescale_intensity( + overlap_img, out_range=(0, 255) + ).astype(np.uint8) return overlap_img def get_ref_img_mask(self): @@ -3003,8 +1884,9 @@ def get_ref_img_mask(self): ref_shape_wh = ref_slide.processed_img_shape_rc[::-1] uw_mask = np.full(ref_shape_wh[::-1], 255, dtype=np.uint8) - mask = warp_tools.warp_img(uw_mask, ref_slide.M, - out_shape_rc=ref_slide.reg_img_shape_rc) + mask = warp_tools.warp_img( + uw_mask, ref_slide.M, out_shape_rc=ref_slide.reg_img_shape_rc + ) reg_txy = -ref_slide.M[0:2, 2] mask_bbox_xywh = np.array([*reg_txy, *ref_shape_wh]) @@ -3027,19 +1909,25 @@ def get_all_overlap_mask(self): ref_slide = self.get_ref_slide() combo_mask = np.zeros(self.aligned_img_shape_rc, dtype=int) for slide_obj in self.slide_dict.values(): - warped_img_mask = warp_tools.warp_img(slide_obj.rigid_reg_mask, - M=slide_obj.M, - out_shape_rc=slide_obj.reg_img_shape_rc, - interp_method="nearest") + warped_img_mask = warp_tools.warp_img( + slide_obj.rigid_reg_mask, + M=slide_obj.M, + out_shape_rc=slide_obj.reg_img_shape_rc, + interp_method="nearest", + ) combo_mask[warped_img_mask > 0] += 1 - temp_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, self.size-0.5).astype(np.uint8) + temp_mask = 255 * filters.apply_hysteresis_threshold( + combo_mask, 0.5, self.size - 0.5 + ).astype(np.uint8) if temp_mask.max() == 0: - lt, ht, _ = filters.threshold_multiotsu(combo_mask, 4) - temp_mask = 255*filters.apply_hysteresis_threshold(combo_mask, lt, ht).astype(np.uint8) + lt, ht, _ = filters.threshold_multiotsu(combo_mask, 4) + temp_mask = 255 * filters.apply_hysteresis_threshold( + combo_mask, lt, ht + ).astype(np.uint8) - mask = 255*ndimage.binary_fill_holes(temp_mask).astype(np.uint8) + mask = 255 * ndimage.binary_fill_holes(temp_mask).astype(np.uint8) mask = preprocessing.mask2contours(mask) mask_bbox_xywh = warp_tools.xy2bbox(warp_tools.mask2xy(mask)) @@ -3066,9 +1954,7 @@ def get_null_overlap_mask(self): return mask, mask_bbox_xywh def create_crop_masks(self): - """Create masks based on rigid registration - - """ + """Create masks based on rigid registration""" mask_dict = {} mask_dict[CROP_REF] = self.get_ref_img_mask() mask_dict[CROP_OVERLAP] = self.get_all_overlap_mask() @@ -3121,17 +2007,31 @@ def extract_rigid_transforms_from_serial_rigid(self, rigid_registrar): rx, ry = img_obj.reflection_M[[0, 1], [0, 1]] < 0 any_reflections = any([rx, ry]) if any_reflections: - uncropped_reflect_M = warp_tools.get_reflection_M(rx, ry, slide_obj.processed_img_shape_rc) - kp1_xy = warp_tools.warp_xy(match_info.matched_kp1_xy, img_obj.reflection_M) + uncropped_reflect_M = warp_tools.get_reflection_M( + rx, ry, slide_obj.processed_img_shape_rc + ) + kp1_xy = warp_tools.warp_xy( + match_info.matched_kp1_xy, img_obj.reflection_M + ) else: kp1_xy = match_info.matched_kp1_xy - s = np.array(slide_obj.processed_img_shape_rc)/np.array(slide_obj.uncropped_processed_img_shape_rc) - kp1_xy_in_uncropped_scaled = s*(kp1_xy + slide_obj.processed_crop_bbox[0:2]) - - prev_s = np.array(prev_slide_obj.processed_img_shape_rc)/np.array(prev_slide_obj.uncropped_processed_img_shape_rc) - kp2_xy_in_uncropped_scaled = prev_s*(match_info.matched_kp2_xy + prev_slide_obj.processed_crop_bbox[0:2]) - kp2_xy_in_uncropped_warped = warp_tools.warp_xy(kp2_xy_in_uncropped_scaled, M=prev_M) + s = np.array(slide_obj.processed_img_shape_rc) / np.array( + slide_obj.uncropped_processed_img_shape_rc + ) + kp1_xy_in_uncropped_scaled = s * ( + kp1_xy + slide_obj.processed_crop_bbox[0:2] + ) + + prev_s = np.array(prev_slide_obj.processed_img_shape_rc) / np.array( + prev_slide_obj.uncropped_processed_img_shape_rc + ) + kp2_xy_in_uncropped_scaled = prev_s * ( + match_info.matched_kp2_xy + prev_slide_obj.processed_crop_bbox[0:2] + ) + kp2_xy_in_uncropped_warped = warp_tools.warp_xy( + kp2_xy_in_uncropped_scaled, M=prev_M + ) # Estimate transform M_tform = rigid_registrar.transformer @@ -3142,16 +2042,19 @@ def extract_rigid_transforms_from_serial_rigid(self, rigid_registrar): else: scaled_M = M_tform.params - scaled_M = rigid_registrar.check_M(kp1_xy_in_uncropped_scaled, kp2_xy_in_uncropped_warped, scaled_M) + scaled_M = rigid_registrar.check_M( + kp1_xy_in_uncropped_scaled, kp2_xy_in_uncropped_warped, scaled_M + ) prev_M = scaled_M - slide_M_dict[slide_obj.name] = scaled_M cropped_M_dict[slide_obj.name] = img_obj.M # Update match dictionary - uncropped_matches = {slide_obj.name: kp1_xy_in_uncropped_scaled, - prev_slide_obj.name: kp2_xy_in_uncropped_scaled} + uncropped_matches = { + slide_obj.name: kp1_xy_in_uncropped_scaled, + prev_slide_obj.name: kp2_xy_in_uncropped_scaled, + } matches_dict[slide_obj.name] = uncropped_matches @@ -3162,7 +2065,9 @@ def extract_rigid_transforms_from_serial_rigid(self, rigid_registrar): max_y = 0 for slide_obj in self.slide_dict.values(): temp_M = slide_M_dict[slide_obj.name] - corners_xy = warp_tools.get_corners_of_image(slide_obj.processed_img_shape_rc)[:, ::-1] + corners_xy = warp_tools.get_corners_of_image( + slide_obj.processed_img_shape_rc + )[:, ::-1] warped_corners = warp_tools.warp_xy(corners_xy, M=temp_M) min_x = np.min([np.min(warped_corners[:, 0]), min_x]) @@ -3183,18 +2088,30 @@ def extract_rigid_transforms_from_serial_rigid(self, rigid_registrar): M = slide_M_dict[slide_obj.name] @ pad_T slide_M_dict[slide_obj.name] = M - cropped_registerd_out_shape_rc = rigid_registrar.img_obj_list[0].registered_shape_rc - - return slide_M_dict, registerd_out_shape_rc, cropped_M_dict, cropped_registerd_out_shape_rc, matches_dict + cropped_registerd_out_shape_rc = rigid_registrar.img_obj_list[ + 0 + ].registered_shape_rc + return ( + slide_M_dict, + registerd_out_shape_rc, + cropped_M_dict, + cropped_registerd_out_shape_rc, + matches_dict, + ) def get_cropped_img_for_rigid_warp(self, slide_obj): - level = slide_tools.get_level_idx(slide_obj.slide_dimensions_wh, np.max(slide_obj.uncropped_processed_img_shape_rc)) + level = slide_tools.get_level_idx( + slide_obj.slide_dimensions_wh, + np.max(slide_obj.uncropped_processed_img_shape_rc), + ) if level > 0: level -= 1 vips_img = slide_obj.slide2vips(level) - vips_img = warp_tools.resize_img(vips_img, slide_obj.uncropped_processed_img_shape_rc) + vips_img = warp_tools.resize_img( + vips_img, slide_obj.uncropped_processed_img_shape_rc + ) vips_cropped_img = vips_img.extract_area(*slide_obj.processed_crop_bbox) cropped_img = warp_tools.vips2numpy(vips_cropped_img) @@ -3219,13 +2136,14 @@ def rigid_register_partial(self, tform_dict=None): If None, then all rigid M will be the identity matrix """ - # Still need to sort images # - rigid_registrar = serial_rigid.SerialRigidRegistrar(self.processed_dir, - imgs_ordered=self.imgs_ordered, - reference_img_f=self.reference_img_f, - name=self.name, - align_to_reference=self.align_to_reference) + rigid_registrar = serial_rigid.SerialRigidRegistrar( + self.processed_dir, + imgs_ordered=self.imgs_ordered, + reference_img_f=self.reference_img_f, + name=self.name, + align_to_reference=self.align_to_reference, + ) feature_detector = self.rigid_reg_kwargs[FD_KEY] matcher = self.rigid_reg_kwargs[MATCHER_KEY] @@ -3240,21 +2158,26 @@ def rigid_register_partial(self, tform_dict=None): # Remove feature points outside of mask for img_obj in rigid_registrar.img_obj_dict.values(): slide_obj = self.get_slide(img_obj.name) - features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=img_obj.kp_pos_xy, mask=slide_obj.rigid_reg_mask) + features_in_mask_idx = warp_tools.get_xy_inside_mask( + xy=img_obj.kp_pos_xy, mask=slide_obj.rigid_reg_mask + ) if len(features_in_mask_idx) > 0: img_obj.kp_pos_xy = img_obj.kp_pos_xy[features_in_mask_idx, :] img_obj.desc = img_obj.desc[features_in_mask_idx, :] - # print("\n======== Matching images\n") if rigid_registrar.aleady_sorted: - rigid_registrar.match_sorted_imgs(matcher_for_sorting, keep_unfiltered=False, valis_obj=self) + rigid_registrar.match_sorted_imgs( + matcher_for_sorting, keep_unfiltered=False, valis_obj=self + ) for i, img_obj in enumerate(rigid_registrar.img_obj_list): img_obj.stack_idx = i else: - rigid_registrar.match_imgs(matcher_for_sorting, keep_unfiltered=False, valis_obj=self) + rigid_registrar.match_imgs( + matcher_for_sorting, keep_unfiltered=False, valis_obj=self + ) # print("\n======== Sorting images\n") rigid_registrar.build_metric_matrix(metric=similarity_metric) @@ -3264,33 +2187,49 @@ def rigid_register_partial(self, tform_dict=None): rigid_registrar.distance_metric_name = matcher.metric_name rigid_registrar.distance_metric_type = matcher.metric_type - do_rematch = (matcher_for_sorting.__class__.__name__ != matcher.__class__.__name__) \ - or (matcher_for_sorting.feature_detector.__class__.__name__ != matcher.feature_detector.__class__.__name__) + do_rematch = ( + matcher_for_sorting.__class__.__name__ != matcher.__class__.__name__ + ) or ( + matcher_for_sorting.feature_detector.__class__.__name__ + != matcher.feature_detector.__class__.__name__ + ) if do_rematch: - msg = (f"Images sorted using {matcher_for_sorting.__class__.__name__} features. " - f"Will now use {matcher.__class__.__name__} to match images using {matcher.feature_detector.__class__.__name__} features") + msg = ( + f"Images sorted using {matcher_for_sorting.__class__.__name__} features. " + f"Will now use {matcher.__class__.__name__} to match images using {matcher.feature_detector.__class__.__name__} features" + ) # Images sorted with different feature_detector, but need to matched using matcher's feature_detector - valtils.print_warning(msg, None) - rigid_registrar.rematch(matcher_obj=matcher, keep_unfiltered=False, valis_obj=self) + logger.warning(msg) + rigid_registrar.rematch( + matcher_obj=matcher, keep_unfiltered=False, valis_obj=self + ) if rigid_registrar.size > 2: - rigid_registrar.update_match_dicts_with_neighbor_filter(transformer, matcher) + rigid_registrar.update_match_dicts_with_neighbor_filter( + transformer, matcher + ) if self.reference_img_f is not None: ref_name = self.name_dict[self.reference_img_f] else: ref_name = valtils.get_name(rigid_registrar.reference_img_f) if self.do_rigid is not False: - msg = " ".join([f"Best to specify `{REF_IMG_KEY}` when manually providing `{TFORM_MAT_KEY}`.", - f"Setting this image to be {ref_name}"]) + msg = " ".join( + [ + f"Best to specify `{REF_IMG_KEY}` when manually providing `{TFORM_MAT_KEY}`.", + f"Setting this image to be {ref_name}", + ] + ) - valtils.print_warning(msg) + logger.warning(msg) # Get output shapes # if tform_dict is None: - named_tform_dict = {o.name: {"M":np.eye(3)} for o in rigid_registrar.img_obj_list} + named_tform_dict = { + o.name: {"M": np.eye(3)} for o in rigid_registrar.img_obj_list + } else: - named_tform_dict = {valtils.get_name(k):v for k, v in tform_dict.items()} + named_tform_dict = {valtils.get_name(k): v for k, v in tform_dict.items()} # Get output shapes # rigid_ref_obj = rigid_registrar.img_obj_dict[ref_name] @@ -3308,8 +2247,10 @@ def rigid_register_partial(self, tform_dict=None): # Assume M was found by aligning to level 0 reference temp_out_shape_rc = ref_slide_obj.slide_dimensions_wh[0][::-1] - ref_to_reg_sxy = (np.array(rigid_ref_obj.image.shape)/np.array(ref_tform_src_shape_rc))[::-1] - out_rc = np.round(temp_out_shape_rc*ref_to_reg_sxy).astype(int) + ref_to_reg_sxy = ( + np.array(rigid_ref_obj.image.shape) / np.array(ref_tform_src_shape_rc) + )[::-1] + out_rc = np.round(temp_out_shape_rc * ref_to_reg_sxy).astype(int) else: out_rc = rigid_ref_obj.image.shape @@ -3333,12 +2274,17 @@ def rigid_register_partial(self, tform_dict=None): else: og_dst_shape_rc = ref_slide_obj.slide_dimensions_wh[0][::-1] - img_corners_xy = warp_tools.get_corners_of_image(matching_rigid_obj.image.shape)[::-1] - warped_corners = warp_tools.warp_xy(img_corners_xy, M=temp_M, - transformation_src_shape_rc=og_src_shape_rc, - transformation_dst_shape_rc=og_dst_shape_rc, - src_shape_rc=matching_rigid_obj.image.shape, - dst_shape_rc=out_rc) + img_corners_xy = warp_tools.get_corners_of_image( + matching_rigid_obj.image.shape + )[::-1] + warped_corners = warp_tools.warp_xy( + img_corners_xy, + M=temp_M, + transformation_src_shape_rc=og_src_shape_rc, + transformation_dst_shape_rc=og_dst_shape_rc, + src_shape_rc=matching_rigid_obj.image.shape, + dst_shape_rc=out_rc, + ) M_tform = transform.ProjectiveTransform() M_tform.estimate(warped_corners, img_corners_xy) for_reg_M = M_tform.params @@ -3346,16 +2292,22 @@ def rigid_register_partial(self, tform_dict=None): matching_rigid_obj.M = for_reg_M # Find M if not provided - for moving_idx, fixed_idx in tqdm.tqdm(rigid_registrar.iter_order, desc=TRANSFORM_MSG, unit="image"): + for moving_idx, fixed_idx in tqdm.tqdm( + rigid_registrar.iter_order, desc=TRANSFORM_MSG, unit="image" + ): img_obj = rigid_registrar.img_obj_list[moving_idx] if img_obj.name in scaled_M_dict: - print(f"Skipping {img_obj.name}, which has affine transform provided.") + logger.info( + f"Skipping {img_obj.name}, which has affine transform provided." + ) continue prev_img_obj = rigid_registrar.img_obj_list[fixed_idx] img_obj.fixed_obj = prev_img_obj - print(f"finding M for {img_obj.name}, which is being aligned to {prev_img_obj.name}") + logger.info( + f"finding M for {img_obj.name}, which is being aligned to {prev_img_obj.name}" + ) if fixed_idx == rigid_registrar.reference_img_idx: prev_M = np.eye(3) @@ -3373,9 +2325,9 @@ def rigid_register_partial(self, tform_dict=None): for img_obj in rigid_registrar.img_obj_list: img_obj.M_inv = np.linalg.inv(img_obj.M) - img_obj.registered_img = warp_tools.warp_img(img=img_obj.image, - M=img_obj.M, - out_shape_rc=out_rc) + img_obj.registered_img = warp_tools.warp_img( + img=img_obj.image, M=img_obj.M, out_shape_rc=out_rc + ) img_obj.registered_shape_rc = img_obj.registered_img.shape[0:2] @@ -3396,12 +2348,14 @@ def rigid_register(self): if self.denoise_rigid: self.denoise_images() - print("\n==== Rigid registration\n") + logger.info("\n==== Rigid registration\n") if self.do_rigid is True: - rigid_registrar = serial_rigid.register_images(img_dir=self.processed_dir, - align_to_reference=self.align_to_reference, - valis_obj=self, - **self.rigid_reg_kwargs) + rigid_registrar = serial_rigid.register_images( + img_dir=self.processed_dir, + align_to_reference=self.align_to_reference, + valis_obj=self, + **self.rigid_reg_kwargs, + ) else: if isinstance(self.do_rigid, dict): # User provided transforms @@ -3417,7 +2371,7 @@ def rigid_register(self): if rigid_registrar is False: msg = "Rigid registration failed" - valtils.print_warning(msg, rgb=Fore.RED) + logger.warning(msg) return False @@ -3426,8 +2380,13 @@ def rigid_register(self): ref_slide = self.slide_dict[valtils.get_name(rigid_registrar.reference_img_f)] self.reference_img_f = ref_slide.src_f - rigid_transform_dict, rigid_reg_shape, cropped_M_dict, cropped_registerd_out_shape_rc, rigid_matches_dict = \ - self.extract_rigid_transforms_from_serial_rigid(rigid_registrar) + ( + rigid_transform_dict, + rigid_reg_shape, + cropped_M_dict, + cropped_registerd_out_shape_rc, + rigid_matches_dict, + ) = self.extract_rigid_transforms_from_serial_rigid(rigid_registrar) self.aligned_img_shape_rc = rigid_reg_shape n_digits = len(str(rigid_registrar.size)) @@ -3438,8 +2397,13 @@ def rigid_register(self): slide_obj.M = rigid_transform_dict[slide_obj.name] slide_obj.reg_img_shape_rc = rigid_reg_shape slide_obj.stack_idx = slide_reg_obj.stack_idx - slide_obj.rigid_reg_img_f = os.path.join(self.reg_dst_dir, - str.zfill(str(slide_obj.stack_idx), n_digits) + "_" + slide_obj.name + ".png") + slide_obj.rigid_reg_img_f = os.path.join( + self.reg_dst_dir, + str.zfill(str(slide_obj.stack_idx), n_digits) + + "_" + + slide_obj.name + + ".png", + ) if slide_obj.image.ndim > 2: # Won't know if single channel image is processed RGB (bight bg) or IF channel (dark bg) slide_obj.get_bg_color_px_pos() @@ -3467,8 +2431,10 @@ def rigid_register(self): # Create original overlap image # pathlib.Path(self.overlap_dir).mkdir(exist_ok=True, parents=True) self.original_overlap_img = self.create_original_composite_img(rigid_registrar) - original_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_original_overlap.png") - warp_tools.save_img(original_overlap_img_fout, self.original_overlap_img) + original_overlap_img_fout = os.path.join( + self.overlap_dir, self.name + "_original_overlap.png" + ) + warp_tools.save_img(original_overlap_img_fout, self.original_overlap_img) pathlib.Path(self.reg_dst_dir).mkdir(exist_ok=True, parents=True) @@ -3477,16 +2443,34 @@ def rigid_register(self): # Processed image may have been denoised for rigid registration. Replace with unblurred image for img_obj in rigid_registrar.img_obj_list: matching_slide = self.slide_dict[img_obj.name] - reg_img = warp_tools.warp_img(matching_slide.processed_img, M=img_obj.M, out_shape_rc=img_obj.registered_shape_rc) + reg_img = warp_tools.warp_img( + matching_slide.processed_img, + M=img_obj.M, + out_shape_rc=img_obj.registered_shape_rc, + ) img_obj.registered_img = reg_img img_obj.image = matching_slide.processed_img - rigid_img_list = [self.create_thumbnail(img_obj.registered_img, thumbnail_size=self.thumbnail_size) for img_obj in rigid_registrar.img_obj_list] - thumbnail_s = np.min(np.array(rigid_img_list[0].shape)/np.array(rigid_registrar.img_obj_list[0].registered_img.shape[0:2])) + rigid_img_list = [ + self.create_thumbnail( + img_obj.registered_img, thumbnail_size=self.thumbnail_size + ) + for img_obj in rigid_registrar.img_obj_list + ] + thumbnail_s = np.min( + np.array(rigid_img_list[0].shape) + / np.array(rigid_registrar.img_obj_list[0].registered_img.shape[0:2]) + ) self.rigid_overlap_img = self.draw_overlap_img(img_list=rigid_img_list) - rigid_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_rigid_overlap.png") - warp_tools.save_img(rigid_overlap_img_fout, self.rigid_overlap_img, thumbnail_size=self.thumbnail_size) + rigid_overlap_img_fout = os.path.join( + self.overlap_dir, self.name + "_rigid_overlap.png" + ) + warp_tools.save_img( + rigid_overlap_img_fout, + self.rigid_overlap_img, + thumbnail_size=self.thumbnail_size, + ) # Overwrite black and white processed images # for slide_name, slide_obj in self.slide_dict.items(): @@ -3496,23 +2480,44 @@ def rigid_register(self): img_to_warp = slide_obj.pad_cropped_processed_img() else: img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - - warped_img = slide_obj.warp_img(img_to_warp, non_rigid=False, crop=self.crop) - warp_tools.save_img(slide_obj.rigid_reg_img_f, warped_img.astype(np.uint8), thumbnail_size=self.thumbnail_size) + img_to_warp = warp_tools.resize_img( + img_to_warp, slide_obj.processed_img_shape_rc + ) + + warped_img = slide_obj.warp_img( + img_to_warp, non_rigid=False, crop=self.crop + ) + warp_tools.save_img( + slide_obj.rigid_reg_img_f, + warped_img.astype(np.uint8), + thumbnail_size=self.thumbnail_size, + ) # Replace processed image with a thumbnail # - warp_tools.save_img(slide_obj.processed_img_f, slide_reg_obj.image, thumbnail_size=self.thumbnail_size) + warp_tools.save_img( + slide_obj.processed_img_f, + slide_reg_obj.image, + thumbnail_size=self.thumbnail_size, + ) return rigid_registrar def micro_rigid_register(self): - micro_rigid_registar = self.micro_rigid_registrar_cls(val_obj=self, **self.micro_rigid_registrar_params) + micro_rigid_registar = self.micro_rigid_registrar_cls( + val_obj=self, **self.micro_rigid_registrar_params + ) micro_rigid_registar.register() # Not all pairs will have keept high resolution M, so re-estimate M based on final matches - slide_idx, slide_names = list(zip(*[[slide_obj.stack_idx, slide_obj.name] for slide_obj in self.slide_dict.values()])) - slide_order = np.argsort(slide_idx) # sorts ascending + slide_idx, slide_names = list( + zip( + *[ + [slide_obj.stack_idx, slide_obj.name] + for slide_obj in self.slide_dict.values() + ] + ) + ) + slide_order = np.argsort(slide_idx) # sorts ascending slide_list = [self.slide_dict[slide_names[i]] for i in slide_order] ref_slide = self.get_ref_slide() for moving_idx, fixed_idx in self.iter_order: @@ -3530,16 +2535,30 @@ def micro_rigid_register(self): prev_M = transformer.params - # Draw in same order as regular rigid registration - draw_list = [self.slide_dict[img_obj.name] for img_obj in self.rigid_registrar.img_obj_list] - - thumbnail_s = np.min(self.thumbnail_size/np.array(draw_list[0].reg_img_shape_rc)) - rigid_img_list = [warp_tools.rescale_img(slide_obj.warp_img(slide_obj.pad_cropped_processed_img(), non_rigid=False), scaling=thumbnail_s) for slide_obj in draw_list] + draw_list = [ + self.slide_dict[img_obj.name] + for img_obj in self.rigid_registrar.img_obj_list + ] + + thumbnail_s = np.min( + self.thumbnail_size / np.array(draw_list[0].reg_img_shape_rc) + ) + rigid_img_list = [ + warp_tools.rescale_img( + slide_obj.warp_img( + slide_obj.pad_cropped_processed_img(), non_rigid=False + ), + scaling=thumbnail_s, + ) + for slide_obj in draw_list + ] self.micro_rigid_overlap_img = self.draw_overlap_img(rigid_img_list) - micro_rigid_overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_micro_rigid_overlap.png") + micro_rigid_overlap_img_fout = os.path.join( + self.overlap_dir, self.name + "_micro_rigid_overlap.png" + ) warp_tools.save_img(micro_rigid_overlap_img_fout, self.micro_rigid_overlap_img) # Overwrite rigid registration results and update rigid registrar @@ -3548,9 +2567,17 @@ def micro_rigid_register(self): img_to_warp = slide_obj.processed_img else: img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - warped_img = slide_obj.warp_img(img_to_warp, non_rigid=False, crop=self.crop) - warp_tools.save_img(slide_obj.rigid_reg_img_f, warped_img.astype(np.uint8), thumbnail_size=self.thumbnail_size) + img_to_warp = warp_tools.resize_img( + img_to_warp, slide_obj.processed_img_shape_rc + ) + warped_img = slide_obj.warp_img( + img_to_warp, non_rigid=False, crop=self.crop + ) + warp_tools.save_img( + slide_obj.rigid_reg_img_f, + warped_img.astype(np.uint8), + thumbnail_size=self.thumbnail_size, + ) if slide_obj.fixed_slide is None: continue @@ -3560,15 +2587,29 @@ def micro_rigid_register(self): rigid_img_obj = self.rigid_registrar.img_obj_dict[slide_obj.name] rigid_img_obj.M = slide_obj.M rigid_img_obj.M_inv = np.linalg.inv(slide_obj.M) - rigid_img_obj.registered_img = slide_obj.warp_img(img_to_warp, non_rigid=False, crop=False) - - rigid_img_obj.match_dict[fixed_rigid_obj].matched_kp1_xy = slide_obj.xy_matched_to_prev - rigid_img_obj.match_dict[fixed_rigid_obj].matched_kp2_xy = slide_obj.xy_in_prev - rigid_img_obj.match_dict[fixed_rigid_obj].n_matches = slide_obj.xy_in_prev.shape[0] - - fixed_rigid_obj.match_dict[rigid_img_obj].matched_kp1_xy = slide_obj.xy_in_prev - fixed_rigid_obj.match_dict[rigid_img_obj].matched_kp2_xy = slide_obj.xy_matched_to_prev - fixed_rigid_obj.match_dict[rigid_img_obj].n_matches = slide_obj.xy_in_prev.shape[0] + rigid_img_obj.registered_img = slide_obj.warp_img( + img_to_warp, non_rigid=False, crop=False + ) + + rigid_img_obj.match_dict[fixed_rigid_obj].matched_kp1_xy = ( + slide_obj.xy_matched_to_prev + ) + rigid_img_obj.match_dict[fixed_rigid_obj].matched_kp2_xy = ( + slide_obj.xy_in_prev + ) + rigid_img_obj.match_dict[fixed_rigid_obj].n_matches = ( + slide_obj.xy_in_prev.shape[0] + ) + + fixed_rigid_obj.match_dict[rigid_img_obj].matched_kp1_xy = ( + slide_obj.xy_in_prev + ) + fixed_rigid_obj.match_dict[rigid_img_obj].matched_kp2_xy = ( + slide_obj.xy_matched_to_prev + ) + fixed_rigid_obj.match_dict[rigid_img_obj].n_matches = ( + slide_obj.xy_in_prev.shape[0] + ) def draw_matches(self, dst_dir, thumbnail_size=DEFAULT_THUMBNAIL_SIZE): """Draw and save images of matching features @@ -3582,8 +2623,15 @@ def draw_matches(self, dst_dir, thumbnail_size=DEFAULT_THUMBNAIL_SIZE): dst_dir = str(dst_dir) pathlib.Path(dst_dir).mkdir(exist_ok=True, parents=True) - slide_idx, slide_names = list(zip(*[[slide_obj.stack_idx, slide_obj.name] for slide_obj in self.slide_dict.values()])) - slide_order = np.argsort(slide_idx) # sorts ascending + slide_idx, slide_names = list( + zip( + *[ + [slide_obj.stack_idx, slide_obj.name] + for slide_obj in self.slide_dict.values() + ] + ) + ) + slide_order = np.argsort(slide_idx) # sorts ascending slide_list = [self.slide_dict[slide_names[i]] for i in slide_order] for moving_idx, fixed_idx in self.iter_order: moving_slide = slide_list[moving_idx] @@ -3591,27 +2639,41 @@ def draw_matches(self, dst_dir, thumbnail_size=DEFAULT_THUMBNAIL_SIZE): # RGB draw images if moving_slide.image.ndim == 3 and moving_slide.is_rgb: - moving_draw_img = warp_tools.resize_img(moving_slide.image, moving_slide.processed_img_shape_rc) + moving_draw_img = warp_tools.resize_img( + moving_slide.image, moving_slide.processed_img_shape_rc + ) else: moving_draw_img = moving_slide.pad_cropped_processed_img() if fixed_slide.image.ndim == 3 and fixed_slide.is_rgb: - fixed_draw_img = warp_tools.resize_img(fixed_slide.image, fixed_slide.processed_img_shape_rc) + fixed_draw_img = warp_tools.resize_img( + fixed_slide.image, fixed_slide.processed_img_shape_rc + ) else: fixed_draw_img = fixed_slide.pad_cropped_processed_img() - all_matches_img = viz.draw_matches(src_img=moving_draw_img, kp1_xy=moving_slide.xy_matched_to_prev, - dst_img=fixed_draw_img, kp2_xy=moving_slide.xy_in_prev, - rad=3, alignment='horizontal') - all_matches_img = self.create_thumbnail(all_matches_img, thumbnail_size=thumbnail_size) - matches_f_out = os.path.join(dst_dir, f"{self.name}_{moving_slide.name}_to_{fixed_slide.name}_matches.png") + all_matches_img = viz.draw_matches( + src_img=moving_draw_img, + kp1_xy=moving_slide.xy_matched_to_prev, + dst_img=fixed_draw_img, + kp2_xy=moving_slide.xy_in_prev, + rad=3, + alignment="horizontal", + ) + all_matches_img = self.create_thumbnail( + all_matches_img, thumbnail_size=thumbnail_size + ) + matches_f_out = os.path.join( + dst_dir, + f"{self.name}_{moving_slide.name}_to_{fixed_slide.name}_matches.png", + ) warp_tools.save_img(matches_f_out, all_matches_img) def create_non_rigid_reg_mask(self): """ Get mask for non-rigid registration """ - print("Creating non-rigid mask") + logger.info("Creating non-rigid mask") if self.create_masks: non_rigid_mask = self._create_mask_from_processed() else: @@ -3623,28 +2685,34 @@ def create_non_rigid_reg_mask(self): # Save thumbnail of mask ref_slide = self.get_ref_slide() if ref_slide.img_type == slide_tools.IHC_NAME: - ref_img = warp_tools.resize_img(ref_slide.image, ref_slide.processed_img_shape_rc) + ref_img = warp_tools.resize_img( + ref_slide.image, ref_slide.processed_img_shape_rc + ) else: ref_img = ref_slide.pad_cropped_processed_img() warped_ref_img = ref_slide.warp_img(img=ref_img, non_rigid=False, crop=CROP_REF) pathlib.Path(self.mask_dir).mkdir(exist_ok=True, parents=True) - thumbnail_img = self.create_thumbnail(warped_ref_img, thumbnail_size=self.thumbnail_size) + thumbnail_img = self.create_thumbnail( + warped_ref_img, thumbnail_size=self.thumbnail_size + ) - draw_mask = warp_tools.resize_img(non_rigid_mask, ref_slide.reg_img_shape_rc, interp_method="nearest") + draw_mask = warp_tools.resize_img( + non_rigid_mask, ref_slide.reg_img_shape_rc, interp_method="nearest" + ) _, overlap_mask_bbox_xywh = self.get_crop_mask(CROP_REF) draw_mask = warp_tools.crop_img(draw_mask, overlap_mask_bbox_xywh.astype(int)) - thumbnail_mask = self.create_thumbnail(draw_mask, thumbnail_size=self.thumbnail_size) + thumbnail_mask = self.create_thumbnail( + draw_mask, thumbnail_size=self.thumbnail_size + ) thumbnail_mask_outline = viz.draw_outline(thumbnail_img, thumbnail_mask) - outline_f_out = os.path.join(self.mask_dir, f'{self.name}_non_rigid_mask.png') + outline_f_out = os.path.join(self.mask_dir, f"{self.name}_non_rigid_mask.png") warp_tools.save_img(outline_f_out, thumbnail_mask_outline) def _create_non_rigid_reg_mask_from_bbox(self, slide_list=None): - """Mask will be bounding box of image overlaps - - """ + """Mask will be bounding box of image overlaps""" ref_slide = self.get_ref_slide() combo_mask = np.zeros(ref_slide.reg_img_shape_rc, dtype=int) @@ -3653,7 +2721,9 @@ def _create_non_rigid_reg_mask_from_bbox(self, slide_list=None): for slide_obj in slide_list: img_bbox = np.full(slide_obj.processed_img_shape_rc, 255, dtype=np.uint8) - rigid_mask = slide_obj.warp_img(img_bbox, non_rigid=False, crop=False, interp_method="nearest") + rigid_mask = slide_obj.warp_img( + img_bbox, non_rigid=False, crop=False, interp_method="nearest" + ) combo_mask[rigid_mask > 0] += 1 n = len(slide_list) @@ -3676,38 +2746,59 @@ def _create_mask_from_processed(self, slide_list=None): for i, slide_obj in enumerate(slide_list): # Determine where images overlap - rigid_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=False, crop=False, interp_method="nearest") + rigid_mask = slide_obj.warp_img( + slide_obj.rigid_reg_mask, + non_rigid=False, + crop=False, + interp_method="nearest", + ) combo_mask[rigid_mask > 0] += 1 # Caclulate running average padded_processed = slide_obj.pad_cropped_processed_img() padded_processed[slide_obj.rigid_reg_mask == 0] = 0 - padded_processed[slide_obj.rigid_reg_mask > 0] = exposure.rescale_intensity(padded_processed[slide_obj.rigid_reg_mask > 0], out_range=(0, 255)) - for_summary = slide_obj.warp_img(padded_processed, non_rigid=False, crop=False).astype(float) + padded_processed[slide_obj.rigid_reg_mask > 0] = exposure.rescale_intensity( + padded_processed[slide_obj.rigid_reg_mask > 0], out_range=(0, 255) + ) + for_summary = slide_obj.warp_img( + padded_processed, non_rigid=False, crop=False + ).astype(float) for_summary = exposure.rescale_intensity(for_summary, out_range=(0, 1)) for_summary = exposure.equalize_adapthist(for_summary) summary_img = np.dstack([for_summary, summary_img]).max(axis=2) summary_img /= summary_img.max() - hyst_thresh = min(self.size-0.5, 2) - combo_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, hyst_thresh).astype(np.uint8) # At least 2 masks are touching + hyst_thresh = min(self.size - 0.5, 2) + combo_mask = 255 * filters.apply_hysteresis_threshold( + combo_mask, 0.5, hyst_thresh + ).astype( + np.uint8 + ) # At least 2 masks are touching # Remake masks, weighting by summary image weighted_combo_mask = np.zeros(self.aligned_img_shape_rc, dtype=int) weighted_mask_list = [None] * self.size for i, slide_obj in enumerate(slide_list): - warped_processed = slide_obj.warp_img(slide_obj.pad_cropped_processed_img(), non_rigid=False, crop=False).astype(float) + warped_processed = slide_obj.warp_img( + slide_obj.pad_cropped_processed_img(), non_rigid=False, crop=False + ).astype(float) if combo_mask.max() > 0: warped_processed[combo_mask == 0] = 0 - weighted_processed = summary_img*(warped_processed/warped_processed.max()) + weighted_processed = summary_img * ( + warped_processed / warped_processed.max() + ) weighted_processed = exposure.equalize_adapthist(weighted_processed) wt, _ = filters.threshold_multiotsu(weighted_processed) - weighted_mask = 255*(weighted_processed > wt).astype(np.uint8) + weighted_mask = 255 * (weighted_processed > wt).astype(np.uint8) weighted_mask = preprocessing.mask2contours(weighted_mask, 1) weighted_mask_list[i] = weighted_mask weighted_combo_mask[weighted_mask > 0] += 1 - temp_non_rigid_mask = 255*filters.apply_hysteresis_threshold(weighted_combo_mask, 0.5, hyst_thresh).astype(np.uint8) # At least 2 masks are touching + temp_non_rigid_mask = 255 * filters.apply_hysteresis_threshold( + weighted_combo_mask, 0.5, hyst_thresh + ).astype( + np.uint8 + ) # At least 2 masks are touching overlap_mask = preprocessing.mask2bbox_mask(temp_non_rigid_mask) return overlap_mask @@ -3724,13 +2815,20 @@ def _create_non_rigid_reg_mask_from_rigid_masks(self, slide_list=None): combo_mask = np.zeros(self.aligned_img_shape_rc, dtype=int) for i, slide_obj in enumerate(slide_list): - rigid_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=False, crop=False, interp_method="nearest") + rigid_mask = slide_obj.warp_img( + slide_obj.rigid_reg_mask, + non_rigid=False, + crop=False, + interp_method="nearest", + ) combo_mask[rigid_mask > 0] += 1 - temp_mask = 255*filters.apply_hysteresis_threshold(combo_mask, 0.5, self.size-0.5).astype(np.uint8) + temp_mask = 255 * filters.apply_hysteresis_threshold( + combo_mask, 0.5, self.size - 0.5 + ).astype(np.uint8) # Draw convex hull around each region - final_mask = 255*ndimage.binary_fill_holes(temp_mask).astype(np.uint8) + final_mask = 255 * ndimage.binary_fill_holes(temp_mask).astype(np.uint8) final_mask = preprocessing.mask2contours(final_mask) return final_mask @@ -3746,10 +2844,14 @@ def pad_displacement(self, dxdy, out_shape_rc, bbox_xywh): if bbox_xywh is None: full_dxdy = vips_dxdy else: - full_dxdy = vips_dxdy.embed(bbox_xywh[0], bbox_xywh[1], - out_shape_rc[1], out_shape_rc[0], - extend=pyvips.enums.Extend.BLACK, - background=[0,0]) + full_dxdy = vips_dxdy.embed( + bbox_xywh[0], + bbox_xywh[1], + out_shape_rc[1], + out_shape_rc[0], + extend=pyvips.enums.Extend.BLACK, + background=[0, 0], + ) if is_array: full_dxdy = warp_tools.vips2numpy(full_dxdy) @@ -3757,10 +2859,9 @@ def pad_displacement(self, dxdy, out_shape_rc, bbox_xywh): return full_dxdy - def get_nr_tiling_params(self, non_rigid_registrar_cls, - processor_dict, - img_specific_args, - tile_wh): + def get_nr_tiling_params( + self, non_rigid_registrar_cls, processor_dict, img_specific_args, tile_wh + ): """Get extra parameters need for tiled non-rigid registration processor_dict : dict @@ -3780,28 +2881,42 @@ def get_nr_tiling_params(self, non_rigid_registrar_cls, # Add registration parameters tiled_non_rigid_reg_params = {} - tiled_non_rigid_reg_params[non_rigid_registrars.NR_CLS_KEY] = non_rigid_registrar_cls + tiled_non_rigid_reg_params[non_rigid_registrars.NR_CLS_KEY] = ( + non_rigid_registrar_cls + ) if self.norm_method is not None: - tiled_non_rigid_reg_params[non_rigid_registrars.NR_STATS_KEY] = self.target_processing_stats + tiled_non_rigid_reg_params[non_rigid_registrars.NR_STATS_KEY] = ( + self.target_processing_stats + ) tiled_non_rigid_reg_params[non_rigid_registrars.NR_TILE_WH_KEY] = tile_wh - tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_CLASS_KEY] = processing_cls - tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_KW_KEY] = tiler_rigid_processing_kwargs - tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY] = {"src_f": slide_obj.src_f, - "series": slide_obj.series, - "reader": deepcopy(slide_obj.reader) - } + tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_CLASS_KEY] = ( + processing_cls + ) + tiled_non_rigid_reg_params[non_rigid_registrars.NR_PROCESSING_KW_KEY] = ( + tiler_rigid_processing_kwargs + ) + tiled_non_rigid_reg_params[ + non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY + ] = { + "src_f": slide_obj.src_f, + "series": slide_obj.series, + "reader": deepcopy(slide_obj.reader), + } img_specific_args[slide_obj.name] = tiled_non_rigid_reg_params non_rigid_registrar_cls = non_rigid_registrars.NonRigidTileRegistrar return non_rigid_registrar_cls, img_specific_args - def prep_images_for_large_non_rigid_registration(self, max_img_dim, - processor_dict, - updating_non_rigid=False, - mask=None, rgb=False): - + def prep_images_for_large_non_rigid_registration( + self, + max_img_dim, + processor_dict, + updating_non_rigid=False, + mask=None, + rgb=False, + ): """Scale and process images for non-rigid registration using larger images Parameters @@ -3849,24 +2964,35 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, warp_full_img = max_img_dim is None if not warp_full_img: - all_max_dims = [np.any(np.max(slide_obj.slide_dimensions_wh, axis=1) >= max_img_dim) for slide_obj in self.slide_dict.values()] + all_max_dims = [ + np.any(np.max(slide_obj.slide_dimensions_wh, axis=1) >= max_img_dim) + for slide_obj in self.slide_dict.values() + ] if not np.all(all_max_dims): - img_maxes = [np.max(slide_obj.slide_dimensions_wh, axis=1)[0] for slide_obj in self.slide_dict.values()] + img_maxes = [ + np.max(slide_obj.slide_dimensions_wh, axis=1)[0] + for slide_obj in self.slide_dict.values() + ] smallest_img_max = np.min(img_maxes) - msg = (f"Requested size of images for non-rigid registration was {max_img_dim}. " + msg = ( + f"Requested size of images for non-rigid registration was {max_img_dim}. " f"However, not all images are this large. Setting `max_non_rigid_registration_dim_px` to " - f"{smallest_img_max}, which is the largest dimension of the smallest image") - valtils.print_warning(msg) + f"{smallest_img_max}, which is the largest dimension of the smallest image" + ) + logger.warning(msg) max_img_dim = smallest_img_max ref_slide = self.get_ref_slide() - max_s = np.min(ref_slide.slide_dimensions_wh[0]/np.array(ref_slide.processed_img_shape_rc[::-1])) + max_s = np.min( + ref_slide.slide_dimensions_wh[0] + / np.array(ref_slide.processed_img_shape_rc[::-1]) + ) if mask is None: if warp_full_img: s = max_s else: - s = np.min(max_img_dim/np.array(ref_slide.processed_img_shape_rc)) + s = np.min(max_img_dim / np.array(ref_slide.processed_img_shape_rc)) else: # Determine how big image would have to be to get mask with maxmimum dimension = max_img_dim if isinstance(mask, pyvips.Image): @@ -3874,10 +3000,14 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, else: mask_shape_rc = np.array(mask.shape[0:2]) - to_reg_mask_sxy = (mask_shape_rc/np.array(ref_slide.reg_img_shape_rc))[::-1] + to_reg_mask_sxy = (mask_shape_rc / np.array(ref_slide.reg_img_shape_rc))[ + ::-1 + ] if not np.all(to_reg_mask_sxy == 1): # Resize just in case it's huge. Only need bounding box - reg_size_mask = warp_tools.resize_img(mask, ref_slide.reg_img_shape_rc, interp_method="nearest") + reg_size_mask = warp_tools.resize_img( + mask, ref_slide.reg_img_shape_rc, interp_method="nearest" + ) else: reg_size_mask = mask reg_size_mask_xy = warp_tools.mask2xy(reg_size_mask) @@ -3886,7 +3016,7 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, if warp_full_img: s = max_s else: - s = np.min(max_img_dim/np.array(to_reg_mask_wh)) + s = np.min(max_img_dim / np.array(to_reg_mask_wh)) if s < max_s: full_out_shape = self.get_aligned_slide_shape(s) @@ -3898,10 +3028,12 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, mask_bbox_xywh = None else: # If masking, the area will be smaller. Get bounding box - mask_sxy = (full_out_shape/mask_shape_rc)[::-1] - mask_bbox_xywh = list(warp_tools.xy2bbox(mask_sxy*reg_size_mask_xy)) + mask_sxy = (full_out_shape / mask_shape_rc)[::-1] + mask_bbox_xywh = list(warp_tools.xy2bbox(mask_sxy * reg_size_mask_xy)) mask_bbox_xywh[2:] = np.round(mask_bbox_xywh[2:]).astype(int) - mask_bbox_max_xy = np.array(mask_bbox_xywh[0:2]) + np.array(mask_bbox_xywh[2:]) + mask_bbox_max_xy = np.array(mask_bbox_xywh[0:2]) + np.array( + mask_bbox_xywh[2:] + ) if np.any(mask_bbox_max_xy > full_out_shape[::-1]): # due to rounding , bbox is too big mask_shift_xy = mask_bbox_max_xy - full_out_shape[::-1] + 1 @@ -3914,18 +3046,25 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, vips_micro_reg_mask = warp_tools.numpy2vips(mask) else: vips_micro_reg_mask = mask - vips_micro_reg_mask = warp_tools.resize_img(vips_micro_reg_mask, full_out_shape, interp_method="nearest") - vips_micro_reg_mask = warp_tools.crop_img(img=vips_micro_reg_mask, xywh=mask_bbox_xywh) - - if ref_slide.reader.metadata.bf_datatype is not None: - np_dtype = slide_tools.BF_FORMAT_NUMPY_DTYPE[ref_slide.reader.metadata.bf_datatype] - else: - # Assuming images not read by bio-formats are RGB read using from openslide or png, jpeg, etc... - np_dtype = "uint8" - - displacement_gb = self.size*warp_tools.calc_memory_size_gb(full_out_shape, 2, "float32") - processed_img_gb = self.size*warp_tools.calc_memory_size_gb(out_shape, 1, "uint8") - img_gb = self.size*warp_tools.calc_memory_size_gb(out_shape, ref_slide.reader.metadata.n_channels, np_dtype) + vips_micro_reg_mask = warp_tools.resize_img( + vips_micro_reg_mask, full_out_shape, interp_method="nearest" + ) + vips_micro_reg_mask = warp_tools.crop_img( + img=vips_micro_reg_mask, xywh=mask_bbox_xywh + ) + + # Default to uint8 for RGB images, or get dtype from reader if available + np_dtype = "uint8" + + displacement_gb = self.size * warp_tools.calc_memory_size_gb( + full_out_shape, 2, "float32" + ) + processed_img_gb = self.size * warp_tools.calc_memory_size_gb( + out_shape, 1, "uint8" + ) + img_gb = self.size * warp_tools.calc_memory_size_gb( + out_shape, ref_slide.reader.metadata.n_channels, np_dtype + ) # Size of full displacement fields, all larger processed images, and an image that will be processed estimated_gb = img_gb + displacement_gb + processed_img_gb @@ -3934,22 +3073,31 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, # Avoid having huge displacement fields saved in registrar. use_tiler = True - scaled_img_list = [None] * self.size # Note, will actually warp after normalization if not using tiler + scaled_img_list = [ + None + ] * self.size # Note, will actually warp after normalization if not using tiler scaled_mask_list = [None] * self.size img_names_list = [None] * self.size img_f_list = [None] * self.size slide_mask_list = [None] * self.size # print("\n======== Preparing images for non-rigid registration\n") - for slide_obj in tqdm.tqdm(self.slide_dict.values(), desc=PREP_NON_RIGID_MSG, unit="image"): + for slide_obj in tqdm.tqdm( + self.slide_dict.values(), desc=PREP_NON_RIGID_MSG, unit="image" + ): # Get image to warp. Likely a larger image scaled down to specified shape # - src_img_shape_rc, src_M = warp_tools.get_src_img_shape_and_M(transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - dst_shape_rc=full_out_shape, - M=slide_obj.M) + src_img_shape_rc, src_M = warp_tools.get_src_img_shape_and_M( + transformation_src_shape_rc=slide_obj.processed_img_shape_rc, + transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, + dst_shape_rc=full_out_shape, + M=slide_obj.M, + ) if max_img_dim is not None: - closest_img_levels = np.where(np.max(slide_obj.slide_dimensions_wh, axis=1) < np.max(src_img_shape_rc))[0] + closest_img_levels = np.where( + np.max(slide_obj.slide_dimensions_wh, axis=1) + < np.max(src_img_shape_rc) + )[0] if len(closest_img_levels) > 0: closest_img_level = closest_img_levels[0] - 1 else: @@ -3968,30 +3116,45 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, dxdy = None # Get mask covering tissue - temp_slide_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=dxdy is not None, crop=False, interp_method="nearest") + temp_slide_mask = slide_obj.warp_img( + slide_obj.rigid_reg_mask, + non_rigid=dxdy is not None, + crop=False, + interp_method="nearest", + ) temp_slide_mask = warp_tools.numpy2vips(temp_slide_mask) - slide_mask = warp_tools.resize_img(temp_slide_mask, full_out_shape, interp_method="nearest") + slide_mask = warp_tools.resize_img( + temp_slide_mask, full_out_shape, interp_method="nearest" + ) if mask_bbox_xywh is not None: slide_mask = warp_tools.crop_img(slide_mask, mask_bbox_xywh) # Get mask that covers image - temp_processing_mask = pyvips.Image.black(img_to_warp.width, img_to_warp.height).invert() - processing_mask = warp_tools.warp_img(img=temp_processing_mask, M=slide_obj.M, + temp_processing_mask = pyvips.Image.black( + img_to_warp.width, img_to_warp.height + ).invert() + processing_mask = warp_tools.warp_img( + img=temp_processing_mask, + M=slide_obj.M, bk_dxdy=dxdy, transformation_src_shape_rc=slide_obj.processed_img_shape_rc, transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, out_shape_rc=full_out_shape, bbox_xywh=mask_bbox_xywh, - interp_method="nearest") + interp_method="nearest", + ) if not use_tiler and rgb and slide_obj.is_rgb: - warped_img = warp_tools.warp_img(img=img_to_warp, M=slide_obj.M, + warped_img = warp_tools.warp_img( + img=img_to_warp, + M=slide_obj.M, bk_dxdy=dxdy, transformation_src_shape_rc=slide_obj.processed_img_shape_rc, transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, out_shape_rc=full_out_shape, bbox_xywh=mask_bbox_xywh, - bg_color=slide_obj.bg_color) + bg_color=slide_obj.bg_color, + ) warped_img_np = warp_tools.vips2numpy(warped_img) scaled_img_list[slide_obj.stack_idx] = warped_img_np @@ -4000,35 +3163,46 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, img_to_warp_np = warp_tools.vips2numpy(img_to_warp) processing_cls, processing_kwargs = processor_dict[slide_obj.name] non_rigid_processing_kwargs = deepcopy(processing_kwargs) - processor = processing_cls(image=img_to_warp_np, - src_f=slide_obj.src_f, - level=closest_img_level, - series=slide_obj.series, - reader=slide_obj.reader) + processor = processing_cls( + image=img_to_warp_np, + src_f=slide_obj.src_f, + level=closest_img_level, + series=slide_obj.series, + reader=slide_obj.reader, + ) try: - processed_img = processor.process_image(**non_rigid_processing_kwargs) + processed_img = processor.process_image( + **non_rigid_processing_kwargs + ) except TypeError: # processor.process_image doesn't take kwargs processed_img = processor.process_image() - warped_img = exposure.rescale_intensity(processed_img, out_range=(0, 255)).astype(np.uint8) + warped_img = exposure.rescale_intensity( + processed_img, out_range=(0, 255) + ).astype(np.uint8) scaled_img_list[slide_obj.stack_idx] = processed_img else: if not warp_full_img: - warped_img = warp_tools.warp_img(img=img_to_warp, M=slide_obj.M, - bk_dxdy=dxdy, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - out_shape_rc=full_out_shape, - bbox_xywh=mask_bbox_xywh, - bg_color=slide_obj.bg_color) + warped_img = warp_tools.warp_img( + img=img_to_warp, + M=slide_obj.M, + bk_dxdy=dxdy, + transformation_src_shape_rc=slide_obj.processed_img_shape_rc, + transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, + out_shape_rc=full_out_shape, + bbox_xywh=mask_bbox_xywh, + bg_color=slide_obj.bg_color, + ) else: - warped_img = slide_obj.warp_slide(0, non_rigid=updating_non_rigid, crop=mask_bbox_xywh) + warped_img = slide_obj.warp_slide( + 0, non_rigid=updating_non_rigid, crop=mask_bbox_xywh + ) scaled_img_list[slide_obj.stack_idx] = warped_img # Get mask if mask is not None: - slide_mask = (vips_micro_reg_mask==0).ifthenelse(0, slide_mask) + slide_mask = (vips_micro_reg_mask == 0).ifthenelse(0, slide_mask) # Update lists img_f_list[slide_obj.stack_idx] = slide_obj.src_f @@ -4036,46 +3210,58 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, scaled_mask_list[slide_obj.stack_idx] = processing_mask slide_mask_list[slide_obj.stack_idx] = slide_mask - # Normalize images. Since they are ROI, probably have different image stats # Warp after normalization, since padding after warping can create a lot of empty space that throws off normalization if not use_tiler and self.norm_method is not None and not rgb: - all_histogram, all_img_stats = preprocessing.collect_img_stats(scaled_img_list) + all_histogram, all_img_stats = preprocessing.collect_img_stats( + scaled_img_list + ) for i, img in enumerate(scaled_img_list): if self.norm_method == "histo_match": normed_img = preprocessing.match_histograms(img, all_histogram) elif self.norm_method == "img_stats": normed_img = preprocessing.norm_img_stats(img, all_img_stats) else: - print(f"Don't recognize `norm_metthod`={self.norm_method}") + logger.error(f"Don't recognize `norm_metthod`={self.norm_method}") normed_img = img - normed_img = exposure.rescale_intensity(normed_img, out_range=(0, 255)).astype(np.uint8) + normed_img = exposure.rescale_intensity( + normed_img, out_range=(0, 255) + ).astype(np.uint8) slide_obj = self.get_slide(img_f_list[i]) - processed_warped_img = warp_tools.warp_img(img=normed_img, M=slide_obj.M, + processed_warped_img = warp_tools.warp_img( + img=normed_img, + M=slide_obj.M, bk_dxdy=dxdy, transformation_src_shape_rc=slide_obj.processed_img_shape_rc, transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, out_shape_rc=full_out_shape, - bbox_xywh=mask_bbox_xywh) + bbox_xywh=mask_bbox_xywh, + ) scaled_img_list[i] = processed_warped_img - - img_dict = {serial_non_rigid.IMG_LIST_KEY: scaled_img_list, - serial_non_rigid.IMG_F_LIST_KEY: img_f_list, - serial_non_rigid.MASK_LIST_KEY: scaled_mask_list, - serial_non_rigid.IMG_NAME_KEY: img_names_list - } + img_dict = { + serial_non_rigid.IMG_LIST_KEY: scaled_img_list, + serial_non_rigid.IMG_F_LIST_KEY: img_f_list, + serial_non_rigid.MASK_LIST_KEY: scaled_mask_list, + serial_non_rigid.IMG_NAME_KEY: img_names_list, + } if ref_slide.non_rigid_reg_mask is not None: vips_nr_mask = warp_tools.numpy2vips(ref_slide.non_rigid_reg_mask) - scaled_non_rigid_mask = warp_tools.resize_img(vips_nr_mask, full_out_shape, interp_method="nearest") + scaled_non_rigid_mask = warp_tools.resize_img( + vips_nr_mask, full_out_shape, interp_method="nearest" + ) if mask is not None: - scaled_non_rigid_mask = scaled_non_rigid_mask.extract_area(*mask_bbox_xywh) - scaled_non_rigid_mask = (vips_micro_reg_mask == 0).ifthenelse(0, scaled_non_rigid_mask) + scaled_non_rigid_mask = scaled_non_rigid_mask.extract_area( + *mask_bbox_xywh + ) + scaled_non_rigid_mask = (vips_micro_reg_mask == 0).ifthenelse( + 0, scaled_non_rigid_mask + ) if not use_tiler: scaled_non_rigid_mask = warp_tools.vips2numpy(scaled_non_rigid_mask) else: @@ -4086,7 +3272,14 @@ def prep_images_for_large_non_rigid_registration(self, max_img_dim, else: final_max_img_dim = max_img_dim - return img_dict, final_max_img_dim, scaled_non_rigid_mask, full_out_shape, mask_bbox_xywh, use_tiler + return ( + img_dict, + final_max_img_dim, + scaled_non_rigid_mask, + full_out_shape, + mask_bbox_xywh, + use_tiler, + ) def clean_dxdy(self): @@ -4096,64 +3289,111 @@ def clean_dxdy(self): continue # Find where there are non-rigid displacement creates tears img_mask = np.full(slide_obj.processed_img_shape_rc, 255, dtype=np.uint8) - r_warped_mask = warp_tools.warp_img(img_mask, - M=slide_obj.M, - out_shape_rc=slide_obj.reg_img_shape_rc, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc) + r_warped_mask = warp_tools.warp_img( + img_mask, + M=slide_obj.M, + out_shape_rc=slide_obj.reg_img_shape_rc, + transformation_src_shape_rc=slide_obj.processed_img_shape_rc, + transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, + ) nr_warped_mask = slide_obj.warp_img(img_mask, crop=False) tears = r_warped_mask - nr_warped_mask tears[tears != 255] = 0 - large_tears = warp_tools.resize_img(tears, slide_obj.bk_dxdy[0].shape, interp_method="nearest") - inv_tears = warp_tools.warp_img(large_tears, bk_dxdy=slide_obj.fwd_dxdy, interp_method="nearest") + large_tears = warp_tools.resize_img( + tears, slide_obj.bk_dxdy[0].shape, interp_method="nearest" + ) + inv_tears = warp_tools.warp_img( + large_tears, bk_dxdy=slide_obj.fwd_dxdy, interp_method="nearest" + ) # Find regions that are in tissue mask but outside of non-rigid mask - rigid_reg_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=False, crop=False) + rigid_reg_mask = slide_obj.warp_img( + slide_obj.rigid_reg_mask, non_rigid=False, crop=False + ) rigid_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(rigid_reg_mask)) c0, r0 = rigid_bbox[:2] c1, r1 = rigid_bbox[:2] + rigid_bbox[2:] rigid_bbox_mask = np.zeros_like(rigid_reg_mask) rigid_bbox_mask[r0:r1, c0:c1] = 255 - temp_missing_mask = cv2.bitwise_xor(slide_obj.non_rigid_reg_mask, rigid_reg_mask) - r_nr_intersection = cv2.bitwise_and(slide_obj.non_rigid_reg_mask, rigid_reg_mask) + temp_missing_mask = cv2.bitwise_xor( + slide_obj.non_rigid_reg_mask, rigid_reg_mask + ) + r_nr_intersection = cv2.bitwise_and( + slide_obj.non_rigid_reg_mask, rigid_reg_mask + ) combined_mask = cv2.bitwise_or(r_nr_intersection, temp_missing_mask) - small_missing_mask = cv2.bitwise_xor(slide_obj.non_rigid_reg_mask, combined_mask) + small_missing_mask = cv2.bitwise_xor( + slide_obj.non_rigid_reg_mask, combined_mask + ) + missing_mask = warp_tools.resize_img( + small_missing_mask, + slide_obj.bk_dxdy[0].shape, + interp_method="nearest", + ) inpaint_mask = cv2.bitwise_or(inv_tears, missing_mask) inpaint_mask[inpaint_mask != 0] = 255 if inpaint_mask.max() == 0: - print(f"no defects in {slide_obj.name}") + logger.info(f"no defects in {slide_obj.name}") continue cv_inpaint_method = cv2.INPAINT_NS - inpainted_bk_dx = cv2.inpaint(slide_obj.bk_dxdy[0].astype(np.float32), inpaint_mask, 3, cv_inpaint_method) - inpainted_bk_dy = cv2.inpaint(slide_obj.bk_dxdy[1].astype(np.float32), inpaint_mask, 3, cv_inpaint_method) + inpainted_bk_dx = cv2.inpaint( + slide_obj.bk_dxdy[0].astype(np.float32), + inpaint_mask, + 3, + cv_inpaint_method, + ) + inpainted_bk_dy = cv2.inpaint( + slide_obj.bk_dxdy[1].astype(np.float32), + inpaint_mask, + 3, + cv_inpaint_method, + ) inpainted_bk_dxdy = np.array([inpainted_bk_dx, inpainted_bk_dy]) - warped_rigid_reg_mask = slide_obj.warp_img(slide_obj.rigid_reg_mask, non_rigid=True, crop=False) - warped_rigid_reg_mask = warp_tools.resize_img(warped_rigid_reg_mask, slide_obj.bk_dxdy[0].shape, interp_method="nearest") - large_rigid_mask = warp_tools.resize_img(rigid_reg_mask, slide_obj.bk_dxdy[0].shape, interp_method="nearest") + warped_rigid_reg_mask = slide_obj.warp_img( + slide_obj.rigid_reg_mask, non_rigid=True, crop=False + ) + warped_rigid_reg_mask = warp_tools.resize_img( + warped_rigid_reg_mask, + slide_obj.bk_dxdy[0].shape, + interp_method="nearest", + ) + large_rigid_mask = warp_tools.resize_img( + rigid_reg_mask, slide_obj.bk_dxdy[0].shape, interp_method="nearest" + ) reg_mask = cv2.bitwise_or(warped_rigid_reg_mask, large_rigid_mask) - reg_mask = warp_tools.resize_img(reg_mask, slide_obj.bk_dxdy[0].shape, interp_method="nearest") + reg_mask = warp_tools.resize_img( + reg_mask, slide_obj.bk_dxdy[0].shape, interp_method="nearest" + ) inpainted_bk_dxdy[0][reg_mask == 0] = 0 inpainted_bk_dxdy[1][reg_mask == 0] = 0 inpainted_fwd_dxdy = warp_tools.get_inverse_field(inpainted_bk_dxdy) - inpainted_bk_dxdy = np.array([warp_tools.crop_img(inpainted_bk_dxdy[0], self._non_rigid_bbox), warp_tools.crop_img(inpainted_bk_dxdy[1], self._non_rigid_bbox)]) - inpainted_fwd_dxdy = np.array([warp_tools.crop_img(inpainted_fwd_dxdy[0], self._non_rigid_bbox), warp_tools.crop_img(inpainted_fwd_dxdy[1], self._non_rigid_bbox)]) + inpainted_bk_dxdy = np.array( + [ + warp_tools.crop_img(inpainted_bk_dxdy[0], self._non_rigid_bbox), + warp_tools.crop_img(inpainted_bk_dxdy[1], self._non_rigid_bbox), + ] + ) + inpainted_fwd_dxdy = np.array( + [ + warp_tools.crop_img(inpainted_fwd_dxdy[0], self._non_rigid_bbox), + warp_tools.crop_img(inpainted_fwd_dxdy[1], self._non_rigid_bbox), + ] + ) slide_obj.bk_dxdy = inpainted_bk_dxdy slide_obj.fwd_dxdy = np.array(inpainted_fwd_dxdy) - def non_rigid_register(self, rigid_registrar, processor_dict): - """Non-rigidly register slides Non-rigidly register slides after performing rigid registration. @@ -4179,15 +3419,21 @@ def non_rigid_register(self, rigid_registrar, processor_dict): """ - ref_slide = self.get_ref_slide() self.create_non_rigid_reg_mask() non_rigid_reg_mask = ref_slide.non_rigid_reg_mask - non_rigid_reg_mask_bbox = warp_tools.xy2bbox(warp_tools.mask2xy(non_rigid_reg_mask)) + non_rigid_reg_mask_bbox = warp_tools.xy2bbox( + warp_tools.mask2xy(non_rigid_reg_mask) + ) cropped_mask_shape_rc = non_rigid_reg_mask_bbox[2:][::-1] - nr_on_scaled_img = self.max_processed_image_dim_px != self.max_non_rigid_registration_dim_px or \ - (non_rigid_reg_mask is not None and np.any(cropped_mask_shape_rc != ref_slide.reg_img_shape_rc)) + nr_on_scaled_img = ( + self.max_processed_image_dim_px != self.max_non_rigid_registration_dim_px + or ( + non_rigid_reg_mask is not None + and np.any(cropped_mask_shape_rc != ref_slide.reg_img_shape_rc) + ) + ) using_tiler = False @@ -4197,23 +3443,35 @@ def non_rigid_register(self, rigid_registrar, processor_dict): if nr_on_scaled_img: # Use higher resolution and/or roi for non-rigid - nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler = \ - self.prep_images_for_large_non_rigid_registration(max_img_dim=self.max_non_rigid_registration_dim_px, - processor_dict=processor_dict, - mask=non_rigid_reg_mask, - rgb=nr_in_rgb) + ( + nr_reg_src, + max_img_dim, + non_rigid_reg_mask, + full_out_shape_rc, + mask_bbox_xywh, + using_tiler, + ) = self.prep_images_for_large_non_rigid_registration( + max_img_dim=self.max_non_rigid_registration_dim_px, + processor_dict=processor_dict, + mask=non_rigid_reg_mask, + rgb=nr_in_rgb, + ) self._non_rigid_bbox = mask_bbox_xywh self.max_non_rigid_registration_dim_px = max_img_dim if using_tiler: - non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params(self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY], - processor_dict=processor_dict, - img_specific_args=None, - tile_wh=DEFAULT_NR_TILE_WH) + non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params( + self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY], + processor_dict=processor_dict, + img_specific_args=None, + tile_wh=DEFAULT_NR_TILE_WH, + ) # Update args to use tiled non-rigid registrar - self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY] = non_rigid_registrar_cls + self.non_rigid_reg_kwargs[NON_RIGID_REG_CLASS_KEY] = ( + non_rigid_registrar_cls + ) else: nr_reg_src = rigid_registrar @@ -4223,13 +3481,15 @@ def non_rigid_register(self, rigid_registrar, processor_dict): self._full_displacement_shape_rc = full_out_shape_rc - non_rigid_registrar = serial_non_rigid.register_images(src=nr_reg_src, - align_to_reference=self.align_to_reference, - img_params = img_specific_args, - **self.non_rigid_reg_kwargs) + non_rigid_registrar = serial_non_rigid.register_images( + src=nr_reg_src, + align_to_reference=self.align_to_reference, + img_params=img_specific_args, + **self.non_rigid_reg_kwargs, + ) self.end_non_rigid_time = time() - for d in [self.non_rigid_dst_dir, self.deformation_field_dir]: + for d in [self.non_rigid_dst_dir, self.deformation_field_dir]: pathlib.Path(d).mkdir(exist_ok=True, parents=True) self.non_rigid_registrar = non_rigid_registrar @@ -4240,8 +3500,13 @@ def non_rigid_register(self, rigid_registrar, processor_dict): bk_for_crop_for_rigid_reg = {} fwd_for_crop_for_rigid_reg = {} - ref_src = (ref_slide.uncropped_processed_img_shape_rc/ref_slide.processed_img_shape_rc) - rescaled_dxdy_shape_rc = np.ceil(np.array(ref_slide.reg_img_shape_rc)*ref_src).astype(int) + ref_src = ( + ref_slide.uncropped_processed_img_shape_rc + / ref_slide.processed_img_shape_rc + ) + rescaled_dxdy_shape_rc = np.ceil( + np.array(ref_slide.reg_img_shape_rc) * ref_src + ).astype(int) bbox_x = np.inf bbox_y = np.inf @@ -4255,32 +3520,57 @@ def non_rigid_register(self, rigid_registrar, processor_dict): crop_T[0:2, 2] = slide_obj.processed_crop_bbox[0:2] inv_M = np.linalg.inv(crop_T @ rigid_obj.M) - uncropped_corners_xy = warp_tools.get_corners_of_image(slide_obj.uncropped_processed_img_shape_rc)[:, ::-1] - warped_uncropped_corners = warp_tools.warp_xy(uncropped_corners_xy, - M=slide_obj.M, - transformation_src_shape_rc=slide_obj.processed_img_shape_rc, - transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, - src_shape_rc=slide_obj.uncropped_processed_img_shape_rc, - dst_shape_rc = rescaled_dxdy_shape_rc - ) + uncropped_corners_xy = warp_tools.get_corners_of_image( + slide_obj.uncropped_processed_img_shape_rc + )[:, ::-1] + warped_uncropped_corners = warp_tools.warp_xy( + uncropped_corners_xy, + M=slide_obj.M, + transformation_src_shape_rc=slide_obj.processed_img_shape_rc, + transformation_dst_shape_rc=slide_obj.reg_img_shape_rc, + src_shape_rc=slide_obj.uncropped_processed_img_shape_rc, + dst_shape_rc=rescaled_dxdy_shape_rc, + ) dispalcement_transformer = transform.ProjectiveTransform() - dispalcement_transformer.estimate(warped_uncropped_corners, uncropped_corners_xy) + dispalcement_transformer.estimate( + warped_uncropped_corners, uncropped_corners_xy + ) displacement_M = dispalcement_transformer.params - bk_dxdy_in_original = warp_tools.warp_img(np.dstack(nr_obj.bk_dxdy), - M=inv_M, - out_shape_rc=slide_obj.uncropped_processed_img_shape_rc) - warped_bk_dxdy = warp_tools.warp_img(bk_dxdy_in_original, M=displacement_M, out_shape_rc=rescaled_dxdy_shape_rc) - bk_for_crop_for_rigid_reg[slide_obj.name] = np.array([warped_bk_dxdy[..., 0], warped_bk_dxdy[..., 1]]) - - fwd_dxdy_in_original = warp_tools.warp_img(np.dstack(nr_obj.fwd_dxdy), - M=inv_M, - out_shape_rc=slide_obj.uncropped_processed_img_shape_rc) - warped_fwd_dxdy = warp_tools.warp_img(fwd_dxdy_in_original, M=displacement_M, out_shape_rc=rescaled_dxdy_shape_rc) - fwd_for_crop_for_rigid_reg[slide_obj.name] = np.array([warped_fwd_dxdy[..., 0], warped_fwd_dxdy[..., 1]]) - - displacement_corners = warp_tools.get_corners_of_image(nr_obj.bk_dxdy[0].shape)[:, ::-1] - displacement_bbox_in_rigid = warp_tools.xy2bbox(warp_tools.warp_xy(displacement_corners, M=inv_M @ displacement_M)) + bk_dxdy_in_original = warp_tools.warp_img( + np.dstack(nr_obj.bk_dxdy), + M=inv_M, + out_shape_rc=slide_obj.uncropped_processed_img_shape_rc, + ) + warped_bk_dxdy = warp_tools.warp_img( + bk_dxdy_in_original, + M=displacement_M, + out_shape_rc=rescaled_dxdy_shape_rc, + ) + bk_for_crop_for_rigid_reg[slide_obj.name] = np.array( + [warped_bk_dxdy[..., 0], warped_bk_dxdy[..., 1]] + ) + + fwd_dxdy_in_original = warp_tools.warp_img( + np.dstack(nr_obj.fwd_dxdy), + M=inv_M, + out_shape_rc=slide_obj.uncropped_processed_img_shape_rc, + ) + warped_fwd_dxdy = warp_tools.warp_img( + fwd_dxdy_in_original, + M=displacement_M, + out_shape_rc=rescaled_dxdy_shape_rc, + ) + fwd_for_crop_for_rigid_reg[slide_obj.name] = np.array( + [warped_fwd_dxdy[..., 0], warped_fwd_dxdy[..., 1]] + ) + + displacement_corners = warp_tools.get_corners_of_image( + nr_obj.bk_dxdy[0].shape + )[:, ::-1] + displacement_bbox_in_rigid = warp_tools.xy2bbox( + warp_tools.warp_xy(displacement_corners, M=inv_M @ displacement_M) + ) bbox_x = min(displacement_bbox_in_rigid[0], bbox_x) bbox_y = min(displacement_bbox_in_rigid[1], bbox_y) @@ -4295,18 +3585,31 @@ def non_rigid_register(self, rigid_registrar, processor_dict): overlap_mask, overlap_mask_bbox_xywh = self.get_crop_mask(self.crop) overlap_mask_bbox_xywh = overlap_mask_bbox_xywh.astype(int) - thumbnail_s = np.min(self.thumbnail_size/warp_tools.get_shape(non_rigid_registrar.non_rigid_obj_list[0].registered_img)[0:2]) - non_rigid_img_list = [warp_tools.rescale_img(nr_img_obj.registered_img, thumbnail_s) for nr_img_obj in non_rigid_registrar.non_rigid_obj_list] + thumbnail_s = np.min( + self.thumbnail_size + / warp_tools.get_shape( + non_rigid_registrar.non_rigid_obj_list[0].registered_img + )[0:2] + ) + non_rigid_img_list = [ + warp_tools.rescale_img(nr_img_obj.registered_img, thumbnail_s) + for nr_img_obj in non_rigid_registrar.non_rigid_obj_list + ] if isinstance(non_rigid_img_list[0], pyvips.Image): non_rigid_img_list = [warp_tools.vips2numpy(x) for x in non_rigid_img_list] if non_rigid_img_list[0].ndim == 3: # Non-rigid performed on RGB images - non_rigid_img_list = [ (255*(1 - skcolor.rgb2gray(img))).astype(np.uint8) for img in non_rigid_img_list] + non_rigid_img_list = [ + (255 * (1 - skcolor.rgb2gray(img))).astype(np.uint8) + for img in non_rigid_img_list + ] - self.non_rigid_overlap_img = self.draw_overlap_img(img_list=non_rigid_img_list) + self.non_rigid_overlap_img = self.draw_overlap_img(img_list=non_rigid_img_list) - overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_non_rigid_overlap.png") + overlap_img_fout = os.path.join( + self.overlap_dir, self.name + "_non_rigid_overlap.png" + ) warp_tools.save_img(overlap_img_fout, self.non_rigid_overlap_img) n_digits = len(str(self.size)) @@ -4329,17 +3632,38 @@ def non_rigid_register(self, rigid_registrar, processor_dict): slide_obj._bk_dxdy_f = bk_dxdy_f slide_obj._fwd_dxdy_f = fwd_dxdy_f # Save space by only writing the necessary areas. Most displacements may be 0 - if np.all(warp_tools.get_shape(slide_nr_reg_obj.bk_dxdy)[0:2][::-1] > mask_bbox_xywh[2:]): - cropped_bk_dxdy = slide_nr_reg_obj.bk_dxdy.extract_area(*mask_bbox_xywh) - cropped_fwd_dxdy = slide_nr_reg_obj.fwd_dxdy.extract_area(*mask_bbox_xywh) + if np.all( + warp_tools.get_shape(slide_nr_reg_obj.bk_dxdy)[0:2][::-1] + > mask_bbox_xywh[2:] + ): + cropped_bk_dxdy = slide_nr_reg_obj.bk_dxdy.extract_area( + *mask_bbox_xywh + ) + cropped_fwd_dxdy = slide_nr_reg_obj.fwd_dxdy.extract_area( + *mask_bbox_xywh + ) else: cropped_bk_dxdy = slide_nr_reg_obj.bk_dxdy cropped_fwd_dxdy = slide_nr_reg_obj.fwd_dxdy - cropped_bk_dxdy.cast("float").tiffsave(slide_obj._bk_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - cropped_fwd_dxdy.cast("float").tiffsave(slide_obj._fwd_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) - - slide_obj.nr_rigid_reg_img_f = os.path.join(self.non_rigid_dst_dir, img_save_id + "_" + slide_obj.name + ".png") + cropped_bk_dxdy.cast("float").tiffsave( + slide_obj._bk_dxdy_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) + cropped_fwd_dxdy.cast("float").tiffsave( + slide_obj._fwd_dxdy_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) + + slide_obj.nr_rigid_reg_img_f = os.path.join( + self.non_rigid_dst_dir, img_save_id + "_" + slide_obj.name + ".png" + ) for slide_name, slide_obj in self.slide_dict.items(): img_save_id = str.zfill(str(slide_obj.stack_idx), n_digits) @@ -4348,26 +3672,36 @@ def non_rigid_register(self, rigid_registrar, processor_dict): img_to_warp = slide_obj.pad_cropped_processed_img() else: img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) + img_to_warp = warp_tools.resize_img( + img_to_warp, slide_obj.processed_img_shape_rc + ) warped_img = slide_obj.warp_img(img_to_warp, non_rigid=True, crop=self.crop) - warp_tools.save_img(slide_obj.nr_rigid_reg_img_f, warped_img, thumbnail_size=self.thumbnail_size) + warp_tools.save_img( + slide_obj.nr_rigid_reg_img_f, + warped_img, + thumbnail_size=self.thumbnail_size, + ) # Draw displacements on image actually used in non-rigid. Might be higher resolution if not isinstance(slide_nr_reg_obj.bk_dxdy, pyvips.Image): draw_dxdy = np.dstack(slide_nr_reg_obj.bk_dxdy) else: - #pyvips + # pyvips draw_dxdy = slide_nr_reg_obj.bk_dxdy dxdy_shape = warp_tools.get_shape(draw_dxdy) - thumbnail_scaling = np.min(self.thumbnail_size/np.array(dxdy_shape[0:2])) - thumbnail_bk_dxdy = self.create_thumbnail(draw_dxdy, thumbnail_size=self.thumbnail_size) + thumbnail_scaling = np.min(self.thumbnail_size / np.array(dxdy_shape[0:2])) + thumbnail_bk_dxdy = self.create_thumbnail( + draw_dxdy, thumbnail_size=self.thumbnail_size + ) thumbnail_bk_dxdy *= float(thumbnail_scaling) if isinstance(thumbnail_bk_dxdy, pyvips.Image): thumbnail_bk_dxdy = warp_tools.vips2numpy(thumbnail_bk_dxdy) - draw_img = warp_tools.resize_img(slide_nr_reg_obj.registered_img, thumbnail_bk_dxdy[..., 0].shape) + draw_img = warp_tools.resize_img( + slide_nr_reg_obj.registered_img, thumbnail_bk_dxdy[..., 0].shape + ) if isinstance(draw_img, pyvips.Image): draw_img = warp_tools.vips2numpy(draw_img) @@ -4376,12 +3710,18 @@ def non_rigid_register(self, rigid_registrar, processor_dict): if draw_img.ndim == 2: draw_img = np.dstack([draw_img] * 3) - thumbanil_deform_grid = viz.draw_displacement_vector_field(dxdy=[-thumbnail_bk_dxdy[..., 0], -thumbnail_bk_dxdy[..., 1]], - img=draw_img, - spacing=10) + thumbanil_deform_grid = viz.draw_displacement_vector_field( + dxdy=[-thumbnail_bk_dxdy[..., 0], -thumbnail_bk_dxdy[..., 1]], + img=draw_img, + spacing=10, + ) - deform_img_f = os.path.join(self.deformation_field_dir, img_save_id + "_" + slide_obj.name + ".png") - warp_tools.save_img(deform_img_f, thumbanil_deform_grid, thumbnail_size=self.thumbnail_size) + deform_img_f = os.path.join( + self.deformation_field_dir, img_save_id + "_" + slide_obj.name + ".png" + ) + warp_tools.save_img( + deform_img_f, thumbanil_deform_grid, thumbnail_size=self.thumbnail_size + ) return non_rigid_registrar @@ -4424,7 +3764,7 @@ def measure_error(self): "aligned_shape" is the shape of the registered full resolution slide - "physical_units" are the names of the pixels physcial unit, e.g. u'\u00B5m' + "physical_units" are the names of the pixels physcial unit, e.g. u'\u00b5m' "resolution" is the physical unit per pixel @@ -4463,7 +3803,9 @@ def measure_error(self): ref_diagonal = np.sqrt(np.sum(np.power(ref_slide.processed_img_shape_rc, 2))) measure_idx = [] - for slide_obj in tqdm.tqdm(self.slide_dict.values(), desc=MEASURE_MSG, unit="image"): + for slide_obj in tqdm.tqdm( + self.slide_dict.values(), desc=MEASURE_MSG, unit="image" + ): i = slide_obj.stack_idx slide_name = slide_obj.name @@ -4481,26 +3823,31 @@ def measure_error(self): prev_slide_obj = slide_obj.fixed_slide to_list[i] = prev_slide_obj.name - img_T = warp_tools.get_padding_matrix(slide_obj.processed_img_shape_rc, - slide_obj.reg_img_shape_rc) + img_T = warp_tools.get_padding_matrix( + slide_obj.processed_img_shape_rc, slide_obj.reg_img_shape_rc + ) - prev_T = warp_tools.get_padding_matrix(prev_slide_obj.processed_img_shape_rc, - prev_slide_obj.reg_img_shape_rc) + prev_T = warp_tools.get_padding_matrix( + prev_slide_obj.processed_img_shape_rc, prev_slide_obj.reg_img_shape_rc + ) + prev_kp_in_slide = prev_slide_obj.warp_xy( + slide_obj.xy_in_prev, + M=prev_T, + pt_level=prev_slide_obj.processed_img_shape_rc, + non_rigid=False, + ) - prev_kp_in_slide = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_T, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=False) - - current_kp_in_slide = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=img_T, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=False) + current_kp_in_slide = slide_obj.warp_xy( + slide_obj.xy_matched_to_prev, + M=img_T, + pt_level=slide_obj.processed_img_shape_rc, + non_rigid=False, + ) og_d = warp_tools.calc_d(prev_kp_in_slide, current_kp_in_slide) - og_rtre = og_d/ref_diagonal + og_rtre = og_d / ref_diagonal median_og_tre = np.median(og_rtre) og_d *= slide_obj.resolution median_d_og = np.median(og_d) @@ -4508,20 +3855,22 @@ def measure_error(self): all_og_d[i] = median_d_og all_og_tre[i] = median_og_tre + prev_warped_rigid = prev_slide_obj.warp_xy( + slide_obj.xy_in_prev, + M=prev_slide_obj.M, + pt_level=prev_slide_obj.processed_img_shape_rc, + non_rigid=False, + ) - prev_warped_rigid = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_slide_obj.M, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=False) - - current_warped_rigid = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=slide_obj.M, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=False) - + current_warped_rigid = slide_obj.warp_xy( + slide_obj.xy_matched_to_prev, + M=slide_obj.M, + pt_level=slide_obj.processed_img_shape_rc, + non_rigid=False, + ) rigid_d = warp_tools.calc_d(prev_warped_rigid, current_warped_rigid) - rtre = rigid_d/ref_diagonal + rtre = rigid_d / ref_diagonal median_rigid_tre = np.median(rtre) rigid_d *= slide_obj.resolution median_d_rigid = np.median(rigid_d) @@ -4531,18 +3880,22 @@ def measure_error(self): all_rigid_tre[i] = median_rigid_tre if slide_obj.bk_dxdy is not None: - prev_warped_nr = prev_slide_obj.warp_xy(slide_obj.xy_in_prev, - M=prev_slide_obj.M, - pt_level= prev_slide_obj.processed_img_shape_rc, - non_rigid=True) - - current_warped_nr = slide_obj.warp_xy(slide_obj.xy_matched_to_prev, - M=slide_obj.M, - pt_level= slide_obj.processed_img_shape_rc, - non_rigid=True) - - nr_d = warp_tools.calc_d(prev_warped_nr, current_warped_nr) - nrtre = nr_d/ref_diagonal + prev_warped_nr = prev_slide_obj.warp_xy( + slide_obj.xy_in_prev, + M=prev_slide_obj.M, + pt_level=prev_slide_obj.processed_img_shape_rc, + non_rigid=True, + ) + + current_warped_nr = slide_obj.warp_xy( + slide_obj.xy_matched_to_prev, + M=slide_obj.M, + pt_level=slide_obj.processed_img_shape_rc, + non_rigid=True, + ) + + nr_d = warp_tools.calc_d(prev_warped_nr, current_warped_nr) + nrtre = nr_d / ref_diagonal mean_nr_tre = np.median(nrtre) nr_d *= slide_obj.resolution @@ -4555,49 +3908,55 @@ def measure_error(self): median_og_tre = np.average(np.array(all_og_tre)[measure_idx], weights=weights) mean_rigid_d = np.average(np.array(all_rigid_d)[measure_idx], weights=weights) - median_rigid_tre = np.average(np.array(all_rigid_tre)[measure_idx], weights=weights) - - rigid_min = (self.end_rigid_time - self.start_time)/60 - - self.summary_df = pd.DataFrame({ - "filename": path_list, - "from":from_list, - "to": to_list, - "original_D": all_og_d, - "original_rTRE": all_og_tre, - "rigid_D": all_rigid_d, - "rigid_rTRE": all_rigid_tre, - "non_rigid_D": all_nr_d, - "non_rigid_rTRE": all_nr_tre, - "processed_img_shape": processed_img_shape_list, - "shape": shape_list, - "aligned_shape": [tuple(outshape)]*self.size, - "mean_original_D": [mean_og_d]*self.size, - "mean_rigid_D": [mean_rigid_d]*self.size, - "physical_units":unit_list, - "resolution":resolution_list, - "name": [self.name]*self.size, - "rigid_time_minutes" : [rigid_min]*self.size - }) + median_rigid_tre = np.average( + np.array(all_rigid_tre)[measure_idx], weights=weights + ) + + rigid_min = (self.end_rigid_time - self.start_time) / 60 + + self.summary_df = pd.DataFrame( + { + "filename": path_list, + "from": from_list, + "to": to_list, + "original_D": all_og_d, + "original_rTRE": all_og_tre, + "rigid_D": all_rigid_d, + "rigid_rTRE": all_rigid_tre, + "non_rigid_D": all_nr_d, + "non_rigid_rTRE": all_nr_tre, + "processed_img_shape": processed_img_shape_list, + "shape": shape_list, + "aligned_shape": [tuple(outshape)] * self.size, + "mean_original_D": [mean_og_d] * self.size, + "mean_rigid_D": [mean_rigid_d] * self.size, + "physical_units": unit_list, + "resolution": resolution_list, + "name": [self.name] * self.size, + "rigid_time_minutes": [rigid_min] * self.size, + } + ) if any([d for d in all_nr_d if d is not None]): mean_nr_d = np.average(np.array(all_nr_d)[measure_idx], weights=weights) mean_nr_tre = np.average(np.array(all_nr_tre)[measure_idx], weights=weights) - non_rigid_min = (self.end_non_rigid_time - self.start_time)/60 + non_rigid_min = (self.end_non_rigid_time - self.start_time) / 60 - self.summary_df["mean_non_rigid_D"] = [mean_nr_d]*self.size - self.summary_df["non_rigid_time_minutes"] = [non_rigid_min]*self.size + self.summary_df["mean_non_rigid_D"] = [mean_nr_d] * self.size + self.summary_df["non_rigid_time_minutes"] = [non_rigid_min] * self.size return self.summary_df - def register(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None, - reader_cls=None, - reader_dict=None): - + def register( + self, + brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, + brightfield_processing_kwargs: dict = DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, + if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, + if_processing_kwargs: dict = DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + processor_dict: Optional[dict] = None, + reader_cls=None, + reader_dict: Optional[dict] = None, + ) -> tuple: """Register a collection of images This function will convert the slides to images, pre-process and normalize them, and @@ -4702,7 +4061,7 @@ def register(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, "aligned_shape" is the shape of the registered full resolution slide - "physical_units" are the names of the pixels physcial unit, e.g. u'\u00B5m' + "physical_units" are the names of the pixels physcial unit, e.g. u'\u00b5m' "resolution" is the physical unit per pixel @@ -4717,73 +4076,67 @@ def register(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, """ self.start_time = time() - try: - print("\n==== Converting images\n") - self.convert_imgs(series=self.series, reader_cls=reader_cls, reader_dict=reader_dict) - - print("\n==== Processing images\n") - slide_processors = self.create_img_processor_dict(brightfield_processing_cls=brightfield_processing_cls, - brightfield_processing_kwargs=brightfield_processing_kwargs, - if_processing_cls=if_processing_cls, - if_processing_kwargs=if_processing_kwargs, - processor_dict=processor_dict) - - self.brightfield_procsseing_fxn_str = brightfield_processing_cls.__name__ - self.if_processing_fxn_str = if_processing_cls.__name__ - self.process_imgs(processor_dict=slide_processors) - - # print("\n==== Rigid registration\n") - rigid_registrar = self.rigid_register() - aligned_slide_shape_rc = self.get_aligned_slide_shape(0) - self.aligned_slide_shape_rc = aligned_slide_shape_rc - self.iter_order = rigid_registrar.iter_order - for slide_obj in self.slide_dict.values(): - slide_obj.aligned_slide_shape_rc = aligned_slide_shape_rc - - if self.micro_rigid_registrar_cls is not None: - print("\n==== Micro-rigid registration\n") - self.micro_rigid_register() - - if rigid_registrar is False: - return None, None, None - - if self.non_rigid_registrar_cls is not None: - print("\n==== Non-rigid registration\n") - non_rigid_registrar = self.non_rigid_register(rigid_registrar, slide_processors) + logger.info("\n==== Converting images\n") + self.convert_imgs( + series=self.series, reader_cls=reader_cls, reader_dict=reader_dict + ) + + logger.info("\n==== Processing images\n") + slide_processors = self.create_img_processor_dict( + brightfield_processing_cls=brightfield_processing_cls, + brightfield_processing_kwargs=brightfield_processing_kwargs, + if_processing_cls=if_processing_cls, + if_processing_kwargs=if_processing_kwargs, + processor_dict=processor_dict, + ) + + self.brightfield_procsseing_fxn_str = brightfield_processing_cls.__name__ + self.if_processing_fxn_str = if_processing_cls.__name__ + self.process_imgs(processor_dict=slide_processors) + + # print("\n==== Rigid registration\n") + rigid_registrar = self.rigid_register() + aligned_slide_shape_rc = self.get_aligned_slide_shape(0) + self.aligned_slide_shape_rc = aligned_slide_shape_rc + self.iter_order = rigid_registrar.iter_order + for slide_obj in self.slide_dict.values(): + slide_obj.aligned_slide_shape_rc = aligned_slide_shape_rc - else: - non_rigid_registrar = None + if self.micro_rigid_registrar_cls is not None: + logger.info("\n==== Micro-rigid registration\n") + self.micro_rigid_register() + if rigid_registrar is False: + return None, None, None - self._add_empty_slides() + if self.non_rigid_registrar_cls is not None: + logger.info("\n==== Non-rigid registration\n") + non_rigid_registrar = self.non_rigid_register( + rigid_registrar, slide_processors + ) - print("\n==== Measuring error\n") - error_df = self.measure_error() - self.error_df = error_df - self.cleanup() + else: + non_rigid_registrar = None - pathlib.Path(self.data_dir).mkdir(exist_ok=True, parents=True) - f_out = os.path.join(self.data_dir, self.name + "_registrar.pickle") - self.reg_f = f_out - pickle.dump(self, open(f_out, 'wb')) + self._add_empty_slides() - data_f_out = os.path.join(self.data_dir, self.name + "_summary.csv") - error_df.to_csv(data_f_out, index=False) + logger.info("\n==== Measuring error\n") + error_df = self.measure_error() + self.error_df = error_df + self.cleanup() - except Exception as e: - traceback_msg = traceback.format_exc() - valtils.print_warning(e, rgb=Fore.RED, traceback_msg=traceback_msg) - if slide_io.ome is not None: - # Only kill JVM if it has been initialized - kill_jvm() - return None, None, None + pathlib.Path(self.data_dir).mkdir(exist_ok=True, parents=True) + f_out = os.path.join(self.data_dir, self.name + "_registrar.pickle") + self.reg_f = f_out + pickle.dump(self, open(f_out, "wb")) + data_f_out = os.path.join(self.data_dir, self.name + "_summary.csv") + error_df.to_csv(data_f_out, index=False) return rigid_registrar, non_rigid_registrar, error_df def cleanup(self): - """Remove objects that can't be pickled - """ + """Remove objects that can't be pickled""" self.rigid_reg_kwargs[FD_KEY] = None self.rigid_reg_kwargs[AFFINE_OPTIMIZER_KEY] = None self.rigid_reg_kwargs[MATCHER_KEY] = None @@ -4794,16 +4147,24 @@ def cleanup(self): self.micro_rigid_registrar_cls = None self.non_rigid_registrar = None - @valtils.deprecated_args(max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px") - def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, - brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, - if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, - if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, - processor_dict=None, - max_non_rigid_registration_dim_px=DEFAULT_MAX_MICRO_REG_SIZE, - non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, - non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, - reference_img_f=None, align_to_reference=False, mask=None, tile_wh=DEFAULT_NR_TILE_WH): + @valtils.deprecated_args( + max_non_rigid_registartion_dim_px="max_non_rigid_registration_dim_px" + ) + def register_micro( + self, + brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, + brightfield_processing_kwargs=DEFAULT_BRIGHTFIELD_PROCESSING_ARGS, + if_processing_cls=DEFAULT_FLOURESCENCE_CLASS, + if_processing_kwargs=DEFAULT_FLOURESCENCE_PROCESSING_ARGS, + processor_dict=None, + max_non_rigid_registration_dim_px=DEFAULT_MAX_MICRO_REG_SIZE, + non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, + non_rigid_reg_params=DEFAULT_NON_RIGID_KWARGS, + reference_img_f=None, + align_to_reference=False, + mask=None, + tile_wh=DEFAULT_NR_TILE_WH, + ): """Improve alingment of microfeatures by performing second non-rigid registration on larger images Caclculates additional non-rigid deformations using a larger image @@ -4860,14 +4221,19 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, """ if self.max_non_rigid_registration_dim_px >= max_non_rigid_registration_dim_px: - if self.max_non_rigid_registration_dim_px > max_non_rigid_registration_dim_px: + if ( + self.max_non_rigid_registration_dim_px + > max_non_rigid_registration_dim_px + ): comp = "larger than the" else: comp = "equal to the" - msg = (f"Have already non-rigidly aligned images with size = {self.max_non_rigid_registration_dim_px} " - f"which is {comp} specified size of {max_non_rigid_registration_dim_px}. " - f"Please set `max_non_rigid_registration_dim_px` to a larger value.") - valtils.print_warning(msg) + msg = ( + f"Have already non-rigidly aligned images with size = {self.max_non_rigid_registration_dim_px} " + f"which is {comp} specified size of {max_non_rigid_registration_dim_px}. " + f"Please set `max_non_rigid_registration_dim_px` to a larger value." + ) + logger.warning(msg) return None, self.error_df # Remove empty slides @@ -4880,12 +4246,13 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, if ref_slide.non_rigid_reg_mask is not None: mask = ref_slide.non_rigid_reg_mask.copy() - slide_processors = self.create_img_processor_dict(brightfield_processing_cls=brightfield_processing_cls, - brightfield_processing_kwargs=brightfield_processing_kwargs, - if_processing_cls=if_processing_cls, - if_processing_kwargs=if_processing_kwargs, - processor_dict=processor_dict) - + slide_processors = self.create_img_processor_dict( + brightfield_processing_cls=brightfield_processing_cls, + brightfield_processing_kwargs=brightfield_processing_kwargs, + if_processing_cls=if_processing_cls, + if_processing_kwargs=if_processing_kwargs, + processor_dict=processor_dict, + ) if non_rigid_registrar_cls is not None: if isinstance(non_rigid_registrar_cls, type): @@ -4897,41 +4264,54 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, non_rigid_registrar_obj = non_rigid_registrar_cls reg_in_rgb = non_rigid_registrar_obj.rgb - nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler = \ - self.prep_images_for_large_non_rigid_registration(max_img_dim=max_non_rigid_registration_dim_px, - processor_dict=slide_processors, - updating_non_rigid=True, - mask=mask, - rgb=reg_in_rgb) + ( + nr_reg_src, + max_img_dim, + non_rigid_reg_mask, + full_out_shape_rc, + mask_bbox_xywh, + using_tiler, + ) = self.prep_images_for_large_non_rigid_registration( + max_img_dim=max_non_rigid_registration_dim_px, + processor_dict=slide_processors, + updating_non_rigid=True, + mask=mask, + rgb=reg_in_rgb, + ) img_specific_args = None write_dxdy = isinstance(ref_slide.bk_dxdy, pyvips.Image) if using_tiler: # Have determined that these images will be too big - msg = (f"Registration would more than {TILER_THRESH_GB} GB if all images opened in memory. " - f"Will use NonRigidTileRegistrar to register cooresponding tiles to reduce memory consumption, " - f"but this method is experimental") + msg = ( + f"Registration would more than {TILER_THRESH_GB} GB if all images opened in memory. " + f"Will use NonRigidTileRegistrar to register cooresponding tiles to reduce memory consumption, " + f"but this method is experimental" + ) - valtils.print_warning(msg) + logger.warning(msg) write_dxdy = True - non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params(non_rigid_registrar_obj, - processor_dict=slide_processors, - img_specific_args=img_specific_args, - tile_wh=tile_wh) + non_rigid_registrar_cls, img_specific_args = self.get_nr_tiling_params( + non_rigid_registrar_obj, + processor_dict=slide_processors, + img_specific_args=img_specific_args, + tile_wh=tile_wh, + ) non_rigid_registrar_obj = non_rigid_registrar_cls() - print("\n==== Performing microregistration\n") - non_rigid_registrar = serial_non_rigid.register_images(src=nr_reg_src, - non_rigid_reg_class=non_rigid_registrar_cls, - non_rigid_reg_params=non_rigid_reg_params, - reference_img_f=reference_img_f, - mask=non_rigid_reg_mask, - align_to_reference=align_to_reference, - name=self.name, - img_params=img_specific_args - ) + logger.info("\n==== Performing microregistration\n") + non_rigid_registrar = serial_non_rigid.register_images( + src=nr_reg_src, + non_rigid_reg_class=non_rigid_registrar_cls, + non_rigid_reg_params=non_rigid_reg_params, + reference_img_f=reference_img_f, + mask=non_rigid_reg_mask, + align_to_reference=align_to_reference, + name=self.name, + img_params=img_specific_args, + ) pathlib.Path(self.micro_reg_dir).mkdir(exist_ok=True, parents=True) out_shape = full_out_shape_rc @@ -4947,36 +4327,55 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, nr_obj = non_rigid_registrar.non_rigid_obj_dict[slide_obj.name] # Will be combining original and new dxdy as pyvips Images if not isinstance(slide_obj.bk_dxdy[0], pyvips.Image): - vips_current_bk_dxdy = warp_tools.numpy2vips(np.dstack(slide_obj.bk_dxdy)).cast("float") - vips_current_fwd_dxdy = warp_tools.numpy2vips(np.dstack(slide_obj.fwd_dxdy)).cast("float") + vips_current_bk_dxdy = warp_tools.numpy2vips( + np.dstack(slide_obj.bk_dxdy) + ).cast("float") + vips_current_fwd_dxdy = warp_tools.numpy2vips( + np.dstack(slide_obj.fwd_dxdy) + ).cast("float") else: vips_current_bk_dxdy = slide_obj.bk_dxdy vips_current_fwd_dxdy = slide_obj.fwd_dxdy if not isinstance(nr_obj.bk_dxdy, pyvips.Image): - vips_new_bk_dxdy = warp_tools.numpy2vips(np.dstack(nr_obj.bk_dxdy)).cast("float") - vips_new_fwd_dxdy = warp_tools.numpy2vips(np.dstack(nr_obj.fwd_dxdy)).cast("float") + vips_new_bk_dxdy = warp_tools.numpy2vips( + np.dstack(nr_obj.bk_dxdy) + ).cast("float") + vips_new_fwd_dxdy = warp_tools.numpy2vips( + np.dstack(nr_obj.fwd_dxdy) + ).cast("float") else: vips_new_bk_dxdy = nr_obj.bk_dxdy vips_new_fwd_dxdy = nr_obj.fwd_dxdy if np.any(non_rigid_registrar.shape != full_out_shape_rc): # Micro-registration performed on sub-region. Need to put in full image - vips_new_bk_dxdy = self.pad_displacement(vips_new_bk_dxdy, full_out_shape_rc, mask_bbox_xywh) - vips_new_fwd_dxdy = self.pad_displacement(vips_new_fwd_dxdy, full_out_shape_rc, mask_bbox_xywh) + vips_new_bk_dxdy = self.pad_displacement( + vips_new_bk_dxdy, full_out_shape_rc, mask_bbox_xywh + ) + vips_new_fwd_dxdy = self.pad_displacement( + vips_new_fwd_dxdy, full_out_shape_rc, mask_bbox_xywh + ) # Scale original dxdy to match scaled shape of new dxdy - slide_sxy = (np.array(out_shape)/np.array([vips_current_bk_dxdy.height, vips_current_bk_dxdy.width]))[::-1] + slide_sxy = ( + np.array(out_shape) + / np.array([vips_current_bk_dxdy.height, vips_current_bk_dxdy.width]) + )[::-1] if not np.all(slide_sxy == 1): - scaled_bk_dx = float(slide_sxy[0])*vips_current_bk_dxdy[0] - scaled_bk_dy = float(slide_sxy[1])*vips_current_bk_dxdy[1] + scaled_bk_dx = float(slide_sxy[0]) * vips_current_bk_dxdy[0] + scaled_bk_dy = float(slide_sxy[1]) * vips_current_bk_dxdy[1] vips_current_bk_dxdy = scaled_bk_dx.bandjoin(scaled_bk_dy) - vips_current_bk_dxdy = warp_tools.resize_img(vips_current_bk_dxdy, out_shape) + vips_current_bk_dxdy = warp_tools.resize_img( + vips_current_bk_dxdy, out_shape + ) - scaled_fwd_dx = float(slide_sxy[0])*vips_current_fwd_dxdy[0] - scaled_fwd_dy = float(slide_sxy[1])*vips_current_fwd_dxdy[1] + scaled_fwd_dx = float(slide_sxy[0]) * vips_current_fwd_dxdy[0] + scaled_fwd_dy = float(slide_sxy[1]) * vips_current_fwd_dxdy[1] vips_current_fwd_dxdy = scaled_fwd_dx.bandjoin(scaled_fwd_dy) - vips_current_fwd_dxdy = warp_tools.resize_img(vips_current_fwd_dxdy, out_shape) + vips_current_fwd_dxdy = warp_tools.resize_img( + vips_current_fwd_dxdy, out_shape + ) vips_updated_bk_dxdy = vips_current_bk_dxdy + vips_new_bk_dxdy vips_updated_fwd_dxdy = vips_current_fwd_dxdy + vips_new_fwd_dxdy @@ -4986,8 +4385,12 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, np_updated_bk_dxdy = warp_tools.vips2numpy(vips_updated_bk_dxdy) np_updated_fwd_dxdy = warp_tools.vips2numpy(vips_updated_fwd_dxdy) - slide_obj.bk_dxdy = np.array([np_updated_bk_dxdy[..., 0], np_updated_bk_dxdy[..., 1]]) - slide_obj.fwd_dxdy = np.array([np_updated_fwd_dxdy[..., 0], np_updated_fwd_dxdy[..., 1]]) + slide_obj.bk_dxdy = np.array( + [np_updated_bk_dxdy[..., 0], np_updated_bk_dxdy[..., 1]] + ) + slide_obj.fwd_dxdy = np.array( + [np_updated_fwd_dxdy[..., 0], np_updated_fwd_dxdy[..., 1]] + ) else: pathlib.Path(self.displacements_dir).mkdir(exist_ok=True, parents=True) slide_obj.stored_dxdy = True @@ -5001,22 +4404,46 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, cropped_fwd_dxdy = vips_updated_fwd_dxdy.extract_area(*mask_bbox_xywh) if not os.path.exists(slide_obj._bk_dxdy_f): - cropped_bk_dxdy.cast("float").tiffsave(slide_obj._bk_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) + cropped_bk_dxdy.cast("float").tiffsave( + slide_obj._bk_dxdy_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) else: # Don't seem to be able to overwrite directly because also accessing it? disp_dir, temp_bk_f = os.path.split(slide_obj._bk_dxdy_f) full_temp_dx_f = os.path.join(disp_dir, f".temp_{temp_bk_f}") - cropped_bk_dxdy.cast("float").tiffsave(full_temp_dx_f, compression="lzw", lossless=True, tile=True, bigtiff=True) + cropped_bk_dxdy.cast("float").tiffsave( + full_temp_dx_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) os.remove(slide_obj._bk_dxdy_f) os.rename(full_temp_dx_f, slide_obj._bk_dxdy_f) if not os.path.exists(slide_obj._fwd_dxdy_f): - cropped_fwd_dxdy.cast("float").tiffsave(slide_obj._fwd_dxdy_f, compression="lzw", lossless=True, tile=True, bigtiff=True) + cropped_fwd_dxdy.cast("float").tiffsave( + slide_obj._fwd_dxdy_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) else: disp_dir, temp_fwd_f = os.path.split(slide_obj._fwd_dxdy_f) full_temp_fwd_f = os.path.join(disp_dir, f".temp_{temp_fwd_f}") - cropped_fwd_dxdy.cast("float").tiffsave(full_temp_fwd_f, compression="lzw", lossless=True, tile=True, bigtiff=True) + cropped_fwd_dxdy.cast("float").tiffsave( + full_temp_fwd_f, + compression="lzw", + lossless=True, + tile=True, + bigtiff=True, + ) os.remove(slide_obj._fwd_dxdy_f) os.rename(full_temp_fwd_f, slide_obj._fwd_dxdy_f) @@ -5030,31 +4457,45 @@ def register_micro(self, brightfield_processing_cls=DEFAULT_BRIGHTFIELD_CLASS, else: img_to_warp = slide_obj.image - img_to_warp = warp_tools.resize_img(img_to_warp, slide_obj.processed_img_shape_rc) - micro_reg_img = slide_obj.warp_img(img_to_warp, non_rigid=True, crop=self.crop) + img_to_warp = warp_tools.resize_img( + img_to_warp, slide_obj.processed_img_shape_rc + ) + micro_reg_img = slide_obj.warp_img( + img_to_warp, non_rigid=True, crop=self.crop + ) img_save_id = str.zfill(str(slide_obj.stack_idx), n_digits) - micro_fout = os.path.join(self.micro_reg_dir, f"{img_save_id}_{slide_obj.name}.png") - micro_thumb = self.create_thumbnail(micro_reg_img, thumbnail_size=self.thumbnail_size) + micro_fout = os.path.join( + self.micro_reg_dir, f"{img_save_id}_{slide_obj.name}.png" + ) + micro_thumb = self.create_thumbnail( + micro_reg_img, thumbnail_size=self.thumbnail_size + ) warp_tools.save_img(micro_fout, micro_thumb) - processed_micro_reg_img = slide_obj.warp_img(slide_obj.pad_cropped_processed_img()) - thumbnail_s = np.min(self.thumbnail_size/np.array(processed_micro_reg_img.shape[0:2])) - micro_reg_imgs[slide_obj.stack_idx] = warp_tools.rescale_img(processed_micro_reg_img, thumbnail_s) + processed_micro_reg_img = slide_obj.warp_img( + slide_obj.pad_cropped_processed_img() + ) + thumbnail_s = np.min( + self.thumbnail_size / np.array(processed_micro_reg_img.shape[0:2]) + ) + micro_reg_imgs[slide_obj.stack_idx] = warp_tools.rescale_img( + processed_micro_reg_img, thumbnail_s + ) # Add empty slides back and save results for empty_slide_name, empty_slide in self._empty_slides.items(): self.slide_dict[empty_slide_name] = empty_slide self.size += 1 - pickle.dump(self, open(self.reg_f, 'wb')) + pickle.dump(self, open(self.reg_f, "wb")) micro_overlap = self.draw_overlap_img(micro_reg_imgs) self.micro_reg_overlap_img = micro_overlap overlap_img_fout = os.path.join(self.overlap_dir, self.name + "_micro_reg.png") warp_tools.save_img(overlap_img_fout, micro_overlap) - print("\n==== Measuring error\n") + logger.info("\n==== Measuring error\n") error_df = self.measure_error() self.error_df = error_df data_f_out = os.path.join(self.data_dir, self.name + "_summary.csv") @@ -5081,18 +4522,22 @@ def get_aligned_slide_shape(self, level): if np.issubdtype(type(level), np.integer): n_levels = len(ref_slide.slide_dimensions_wh) if level >= n_levels: - msg = (f"requested to scale transformation for pyramid level {level}, ", + msg = ( + f"requested to scale transformation for pyramid level {level}, ", f"but the image only has {n_levels} (starting from 0). ", - f"Will use level {level-1}, which is the smallest level") - valtils.print_warning(msg) + f"Will use level {level-1}, which is the smallest level", + ) + logger.warning(msg) level = level - 1 slide_shape_rc = ref_slide.slide_dimensions_wh[level][::-1] - s_rc = (slide_shape_rc/np.array(ref_slide.processed_img_shape_rc)) + s_rc = slide_shape_rc / np.array(ref_slide.processed_img_shape_rc) else: s_rc = level - aligned_out_shape_rc = np.ceil(np.array(ref_slide.reg_img_shape_rc)*s_rc).astype(int) + aligned_out_shape_rc = np.ceil( + np.array(ref_slide.reg_img_shape_rc) * s_rc + ).astype(int) return aligned_out_shape_rc @@ -5104,11 +4549,19 @@ def get_sorted_img_f_list(self): return src_f_list @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") - def warp_and_save_slides(self, dst_dir, level=0, non_rigid=True, - crop=True, - colormap=slide_io.CMAP_AUTO, - interp_method="bicubic", - tile_wh=None, compression=DEFAULT_COMPRESSION, Q=100, pyramid=True): + def warp_and_save_slides( + self, + dst_dir: Union[str, pathlib.Path], + level: int = 0, + non_rigid: bool = True, + crop: Union[bool, str, CropMode] = True, + colormap=slide_io.CMAP_AUTO, + interp_method: str = "bicubic", + tile_wh: Optional[int] = None, + compression=DEFAULT_COMPRESSION, + Q: int = 100, + pyramid: bool = True, + ) -> None: f"""Warp and save all slides @@ -5163,7 +4616,9 @@ def warp_and_save_slides(self, dst_dir, level=0, non_rigid=True, if isinstance(colormap, str) and colormap == slide_io.CMAP_AUTO: cmap_is_str = True else: - named_color_map = {self.get_slide(x).name:colormap[x] for x in colormap.keys()} + named_color_map = { + self.get_slide(x).name: colormap[x] for x in colormap.keys() + } for src_f in tqdm.tqdm(src_f_list, desc=SAVING_IMG_MSG, unit="image"): slide_obj = self.get_slide(src_f) @@ -5173,42 +4628,57 @@ def warp_and_save_slides(self, dst_dir, level=0, non_rigid=True, updated_channel_names = None elif colormap is not None: chnl_names = slide_obj.reader.metadata.channel_names - updated_channel_names = slide_io.check_channel_names(chnl_names, is_rgb, nc=slide_obj.reader.metadata.n_channels) + updated_channel_names = slide_io.check_channel_names( + chnl_names, is_rgb, nc=slide_obj.reader.metadata.n_channels + ) try: if not cmap_is_str and named_color_map is not None: slide_cmap = named_color_map[slide_obj.name] else: slide_cmap = colormap - slide_cmap = slide_io.check_colormap(colormap=slide_cmap, channel_names=updated_channel_names) + slide_cmap = slide_io.check_colormap( + colormap=slide_cmap, channel_names=updated_channel_names + ) except Exception as e: traceback_msg = traceback.format_exc() msg = f"Could not create colormap for the following reason:{e}" - valtils.print_warning(msg, traceback_msg=traceback_msg) + logger.warning(msg) dst_f = os.path.join(dst_dir, slide_obj.name + ".ome.tiff") - slide_obj.warp_and_save_slide(dst_f=dst_f, level=level, - non_rigid=non_rigid, - crop=crop, - src_f=slide_obj.src_f, - interp_method=interp_method, - colormap=slide_cmap, - tile_wh=tile_wh, - compression=compression, - channel_names=updated_channel_names, - Q=Q, - pyramid=pyramid) - + slide_obj.warp_and_save_slide( + dst_f=dst_f, + level=level, + non_rigid=non_rigid, + crop=crop, + src_f=slide_obj.src_f, + interp_method=interp_method, + colormap=slide_cmap, + tile_wh=tile_wh, + compression=compression, + channel_names=updated_channel_names, + Q=Q, + pyramid=pyramid, + ) @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") - def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, - crop=True, channel_name_dict=None, - src_f_list=None, colormap=slide_io.CMAP_AUTO, - drop_duplicates=True, tile_wh=None, - interp_method="bicubic", compression=DEFAULT_COMPRESSION, - Q=100, pyramid=True): - + def warp_and_merge_slides( + self, + dst_f=None, + level=0, + non_rigid=True, + crop=True, + channel_name_dict=None, + src_f_list=None, + colormap=slide_io.CMAP_AUTO, + drop_duplicates=True, + tile_wh=None, + interp_method="bicubic", + compression=DEFAULT_COMPRESSION, + Q=100, + pyramid=True, + ): """Warp and merge registered slides Parameters @@ -5282,10 +4752,17 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, """ if channel_name_dict is not None: - channel_name_dict_by_name = {valtils.get_name(k):channel_name_dict[k] for k in channel_name_dict} + channel_name_dict_by_name = { + valtils.get_name(k): channel_name_dict[k] for k in channel_name_dict + } else: - channel_name_dict_by_name = {slide_obj.name: [f"{c} ({slide_obj.name})" for c in slide_obj.reader.metadata.channel_names] - for slide_obj in self.slide_dict.values()} + channel_name_dict_by_name = { + slide_obj.name: [ + f"{c} ({slide_obj.name})" + for c in slide_obj.reader.metadata.channel_names + ] + for slide_obj in self.slide_dict.values() + } if src_f_list is None: # Save in the sorted order. Will still be original order if imgs_ordered= True @@ -5294,7 +4771,11 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, all_channel_names = [] merged_slide = None - expected_channel_order = list(chain.from_iterable([channel_name_dict_by_name[valtils.get_name(f)] for f in src_f_list])) + expected_channel_order = list( + chain.from_iterable( + [channel_name_dict_by_name[valtils.get_name(f)] for f in src_f_list] + ) + ) if drop_duplicates: expected_channel_order = list(dict.fromkeys(expected_channel_order)) @@ -5302,20 +4783,23 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, slide_name = valtils.get_name(os.path.split(f)[1]) slide_obj = self.slide_dict[slide_name] - warped_slide = slide_obj.warp_slide(level, non_rigid=non_rigid, - crop=crop, - interp_method=interp_method) + warped_slide = slide_obj.warp_slide( + level, non_rigid=non_rigid, crop=crop, interp_method=interp_method + ) keep_idx = list(range(warped_slide.bands)) slide_channel_names = channel_name_dict_by_name[slide_obj.name] if drop_duplicates: - keep_idx = [idx for idx in range(len(slide_channel_names)) if - slide_channel_names[idx] not in all_channel_names] + keep_idx = [ + idx + for idx in range(len(slide_channel_names)) + if slide_channel_names[idx] not in all_channel_names + ] if len(keep_idx) == 0: - msg= f"Have already added all channels in {slide_channel_names}. Ignoring {slide_name}" - valtils.print_warning(msg) + msg = f"Have already added all channels in {slide_channel_names}. Ignoring {slide_name}" + logger.warning(msg) continue if drop_duplicates and warped_slide.bands != len(keep_idx): @@ -5325,7 +4809,9 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, warped_slide = keep_channels[0] else: warped_slide = keep_channels[0].bandjoin(keep_channels[1:]) - print(f"merging {', '.join(slide_channel_names)} from {slide_obj.name}") + logger.info( + f"merging {', '.join(slide_channel_names)} from {slide_obj.name}" + ) if merged_slide is None: merged_slide = warped_slide @@ -5349,12 +4835,18 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, slide_obj = self.get_ref_slide() px_phys_size = slide_obj.reader.scale_physical_size(level) bf_dtype = slide_io.vips2bf_dtype(merged_slide.format) - out_xyczt = slide_io.get_shape_xyzct((merged_slide.width, merged_slide.height), merged_slide.bands) - - ome_xml_obj = slide_io.create_ome_xml(out_xyczt, bf_dtype, is_rgb=False, - pixel_physical_size_xyu=px_phys_size, - channel_names=all_channel_names, - colormap=cmap_dict) + out_xyczt = slide_io.get_shape_xyzct( + (merged_slide.width, merged_slide.height), merged_slide.bands + ) + + ome_xml_obj = slide_io.create_ome_xml( + out_xyczt, + bf_dtype, + is_rgb=False, + pixel_physical_size_xyu=px_phys_size, + channel_names=all_channel_names, + colormap=cmap_dict, + ) ome_xml = ome_xml_obj.to_xml() if dst_f is not None: @@ -5363,15 +4855,18 @@ def warp_and_merge_slides(self, dst_f=None, level=0, non_rigid=True, ref_slide = self.get_ref_slide() if tile_wh is None: - tile_wh = slide_io.get_tile_wh(reader=ref_slide.reader, - level=level, - out_shape_wh=out_xyczt[0:2]) - - slide_io.save_ome_tiff(merged_slide, dst_f=dst_f, - ome_xml=ome_xml,tile_wh=tile_wh, - compression=compression, Q=Q, pyramid=pyramid) + tile_wh = slide_io.get_tile_wh( + reader=ref_slide.reader, level=level, out_shape_wh=out_xyczt[0:2] + ) + + slide_io.save_ome_tiff( + merged_slide, + dst_f=dst_f, + ome_xml=ome_xml, + tile_wh=tile_wh, + compression=compression, + Q=Q, + pyramid=pyramid, + ) return merged_slide, all_channel_names, ome_xml - - - diff --git a/src/valis/registration/slide.py b/src/valis/registration/slide.py new file mode 100644 index 00000000..5de1c9b4 --- /dev/null +++ b/src/valis/registration/slide.py @@ -0,0 +1,1532 @@ +"""Slide class — stores registration state and warps images/points. + +Import ``Slide`` from ``valis.registration`` rather than this sub-module. +""" + +import logging +from typing import Optional, Union + +import json +import os +import pathlib +from copy import deepcopy + +import colour +import cv2 +import numpy as np +import pyvips +import shapely +import tqdm +from skimage import exposure, transform + +from .. import warp_tools +from .. import slide_io +from .. import slide_tools +from .. import preprocessing +from .. import valtils +from ._constants import ( + CROP_OVERLAP, + CROP_REF, + CROP_NONE, + CropMode, + DEFAULT_COMPRESSION, + WARP_ANNO_MSG, +) +from .state import DisplacementField + +logger = logging.getLogger(__name__) + + +class Slide(object): + """Stores registration info and warps slides/points + + `Slide` is a class that stores registration parameters + and other metadata about a slide. Once registration has been + completed, `Slide` is also able warp the slide and/or points + using the same registration parameters. Warped slides can be saved + as ome.tiff images with valid ome-xml. + + Attributes + ---------- + src_f : str + Path to slide. + + image: ndarray + Image to registered. Taken from a level in the image pyramid. + However, image may be resized to fit within the `max_image_dim_px` + argument specified when creating a `Valis` object. + + val_obj : Valis + The "parent" object that registers all of the slide. + + reader : SlideReader + Object that can read slides and collect metadata. + + original_xml : str + Xml string created by bio-formats + + img_type : str + Whether the image is "brightfield" or "fluorescence" + + is_rgb : bool + Whether or not the slide is RGB. + + slide_shape_rc : tuple of int + Dimensions of the largest resolution in the slide, in the form + of (row, col). + + series : int + Slide series to be read + + slide_dimensions_wh : ndarray + Dimensions of all images in the pyramid (width, height). + + resolution : float + Physical size of each pixel. + + units : str + Physical unit of each pixel. + + name : str + Name of the image. Usually `img_f` but with the extension removed. + + processed_img : ndarray + Image used to perform registration + + rigid_reg_mask : ndarray + Mask of convex hulls covering tissue in unregistered image. + Could be used to mask `processed_img` before rigid registration + + non_rigid_reg_mask : ndarray + Created by combining rigidly warped `rigid_reg_mask` in all + other slides. + + stack_idx : int + Position of image in sorted Z-stack + + processed_img_f : str + Path to thumbnail of the processed `image`. + + rigid_reg_img_f : str + Path to thumbnail of rigidly aligned `image`. + + non_rigid_reg_img_f : str + Path to thumbnail of non-rigidly aligned `image`. + + processed_img_shape_rc : tuple of int + Shape (row, col) of the processed image used to find the + transformation parameters. Maximum dimension will be less or + equal to the `max_processed_image_dim_px` specified when + creating a `Valis` object. As such, this may be smaller than + the image's shape. + + aligned_slide_shape_rc : tuple of int + Shape (row, col) of aligned slide, based on the dimensions in the 0th + level of they pyramid. In + + reg_img_shape_rc : tuple of int + Shape (row, col) of the registered image + + M : ndarray + Rigid transformation matrix that aligns `image` to the previous + image in the stack. Found using the processed copy of `image`. + + bk_dxdy : ndarray + (2, N, M) numpy array of pixel displacements in + the x and y directions. dx = bk_dxdy[0], and dy=bk_dxdy[1]. Used + to warp images. Found using the rigidly aligned version of the + processed image. + + fwd_dxdy : ndarray + Inverse of `bk_dxdy`. Used to warp points. + + _bk_dxdy_f : str + Path to file containing bk_dxdy, if saved + + _fwd_dxdy_f : str + Path to file containing fwd_dxdy, if saved + + _bk_dxdy_np : ndarray + `bk_dxdy` as a numpy array. Only not None if `bk_dxdy` becomes + associated with a file + + _fwd_dxdy_np : ndarray + `fwd_dxdy` as a numpy array. Only not None if `fwd_dxdy` becomes + associated with a file + + stored_dxdy : bool + Whether or not the non-rigid displacements are saved in a file + Should only occur if image is very large. + + fixed_slide : Slide + Slide object to which this one was aligned. + + xy_matched_to_prev : ndarray + Coordinates (x, y) of features in `image` that had matches in the + previous image. Will have shape (N, 2) + + xy_in_prev : ndarray + Coordinates (x, y) of features in the previous that had matches + to those in `image`. Will have shape (N, 2) + + xy_matched_to_prev_in_bbox : ndarray + Subset of `xy_matched_to_prev` that were within `overlap_mask_bbox_xywh`. + Will either have shape (N, 2) or (M, 2), with M < N. + + xy_in_prev_in_bbox : ndarray + Subset of `xy_in_prev` that were within `overlap_mask_bbox_xywh`. + Will either have shape (N, 2) or (M, 2), with M < N. + + crop : str + Crop method + + bg_px_pos_rc : tuple + Position of pixel that has the background color + + bg_color : list, optional + Color of background pixels + + is_empty : bool + True if the image is empty (i.e. contains only 1 value) + + """ + + def __init__(self, src_f, image, val_obj, reader, name=None): + """ + Parameters + ---------- + src_f : str + Path to slide. + + image: ndarray + Image to registered. Taken from a level in the image pyramid. + However, image may be resized to fit within the `max_image_dim_px` + argument specified when creating a `Valis` object. + + val_obj : Valis + The "parent" object that registers all of the slide. + + reader : SlideReader + Object that can read slides and collect metadata. + + name : str, optional + Name of slide. If None, it will be `src_f` with the extension removed + + """ + + self.src_f = src_f + self.image = image + self.val_obj = val_obj + self.reader = reader + + # Metadata # + self.is_rgb = reader.metadata.is_rgb + self.img_type = reader.guess_image_type() + self.slide_shape_rc = reader.metadata.slide_dimensions[0][::-1] + self.series = reader.series + self.slide_dimensions_wh = reader.metadata.slide_dimensions + self.resolution = np.mean(reader.metadata.pixel_physical_size_xyu[0:2]) + self.units = reader.metadata.pixel_physical_size_xyu[2] + self.original_xml = reader.metadata.original_xml + + if self.is_rgb and self.image.dtype != np.uint8: + self.image = exposure.rescale_intensity(self.image, out_range=np.uint8) + + if name is None: + name = valtils.get_name(src_f) + + self.name = name + + # To be filled in during registration # + self.processed_img = None + self.rigid_reg_mask = None + self.non_rigid_reg_mask = None + self.stack_idx = None + + self.aligned_slide_shape_rc = None + self.processed_img_shape_rc = None + self.reg_img_shape_rc = None + self.M = None + self.bk_dxdy = None + self.fwd_dxdy = None + + self.stored_dxdy = False + self._bk_dxdy_f = None + self._fwd_dxdy_f = None + self._bk_dxdy_np = None + self._fwd_dxdy_np = None + self.processed_img_f = None + self.rigid_reg_img_f = None + self.non_rigid_reg_img_f = None + + self.fixed_slide = None + self.xy_matched_to_prev = None + self.xy_in_prev = None + self.xy_matched_to_prev_in_bbox = None + self.xy_in_prev_in_bbox = None + + self.crop = None + self.bg_px_pos_rc = (0, 0) + self.bg_color = None + + self.is_empty = self.check_if_empty(image) + + self.processed_crop_bbox = None + self.uncropped_processed_img_shape_rc = None + self.rigid_cropped = False + self.M_for_cropped = None + self.rigid_reg_cropped_shape_rc = None + + def __repr__(self): + repr_str = ( + f"<{self.__class__.__name__}, name = {self.name}>" + f", width={self.slide_dimensions_wh[0][0]}" + f", height={self.slide_dimensions_wh[0][1]}" + f", channels={self.reader.metadata.n_channels}" + f", levels={len(self.slide_dimensions_wh)}" + f", RGB={self.is_rgb}" + f", dtype={self.image.dtype}>" + ) + return repr_str + + def check_if_empty(self, img): + """Check if the image is empty + + Return + ------ + is_empty : bool + Whether or not the image is empty + + """ + + is_empty = img.min() == img.max() + + return is_empty + + def slide2image(self, level, series=None, xywh=None): + """Convert slide to image + + Parameters + ----------- + level : int + Pyramid level + + series : int, optional + Series number. Defaults to 0 + + xywh : tuple of int, optional + The region to be sliced from the slide. If None, + then the entire slide will be converted. Otherwise + xywh is the (top left x, top left y, width, height) of + the region to be sliced. + + Returns + ------- + img : ndarray + An image of the slide or the region defined by xywh + + """ + + img = self.reader.slide2image(level=level, series=series, xywh=xywh) + + return img + + def slide2vips(self, level, series=None, xywh=None): + """Convert slide to pyvips.Image + + Parameters + ----------- + level : int + Pyramid level + + series : int, optional + Series number. Defaults to 0 + + xywh : tuple of int, optional + The region to be sliced from the slide. If None, + then the entire slide will be converted. Otherwise + xywh is the (top left x, top left y, width, height) of + the region to be sliced. + + Returns + ------- + vips_slide : pyvips.Image + An of the slide or the region defined by xywh + + """ + + vips_img = self.reader.slide2vips(level=level, series=series, xywh=xywh) + + return vips_img + + def get_aligned_to_ref_slide_crop_xywh( + self, ref_img_shape_rc, ref_M, scaled_ref_img_shape_rc=None + ): + """Get bounding box used to crop slide to fit in reference image + + Parameters + ---------- + ref_img_shape_rc : tuple of int + shape of reference image used to find registration parameters, i.e. processed image) + + ref_M : ndarray + Transformation matrix for the reference image + + scaled_ref_img_shape_rc : tuple of int, optional + shape of scaled image with shape `img_shape_rc`, i.e. slide corresponding + to the image used to find the registration parameters. + + Returns + ------- + crop_xywh : tuple of int + Bounding box of crop area (XYWH) + + mask : ndarray + Mask covering reference image + + """ + + mask, _ = self.val_obj.get_crop_mask(CROP_REF) + + if scaled_ref_img_shape_rc is not None: + sxy = np.array([*scaled_ref_img_shape_rc[::-1]]) / np.array( + [*ref_img_shape_rc[::-1]] + ) + else: + scaled_ref_img_shape_rc = ref_img_shape_rc + sxy = np.ones(2) + + reg_txy = -ref_M[0:2, 2] + slide_xywh = (*reg_txy * sxy, *scaled_ref_img_shape_rc[::-1]) + + return slide_xywh, mask + + def get_overlap_crop_xywh( + self, warped_img_shape_rc, scaled_warped_img_shape_rc=None + ): + """Get bounding box used to crop slide to where all slides overlap + + Parameters + ---------- + warped_img_shape_rc : tuple of int + shape of registered image + + warped_scaled_img_shape_rc : tuple of int, optional + shape of scaled registered image (i.e. registered slied) + + Returns + ------- + crop_xywh : tuple of int + Bounding box of crop area (XYWH) + + """ + mask, mask_bbox_xywh = self.val_obj.get_crop_mask(CROP_OVERLAP) + + if scaled_warped_img_shape_rc is not None: + sxy = np.array([*scaled_warped_img_shape_rc[::-1]]) / np.array( + [*warped_img_shape_rc[::-1]] + ) + else: + sxy = np.ones(2) + + to_slide_transformer = transform.SimilarityTransform(scale=sxy) + overlap_bbox = warp_tools.bbox2xy(mask_bbox_xywh) + scaled_overlap_bbox = to_slide_transformer(overlap_bbox) + scaled_overlap_xywh = warp_tools.xy2bbox(scaled_overlap_bbox) + + scaled_overlap_xywh[2:] = np.ceil(scaled_overlap_xywh[2:]) + scaled_overlap_xywh = tuple(scaled_overlap_xywh.astype(int)) + + return scaled_overlap_xywh, mask + + def get_crop_xywh(self, crop, out_shape_rc=None): + """Get bounding box used to crop aligned slide + + Parameters + ---------- + + out_shape_rc : tuple of int, optional + If crop is "reference", this should be the shape of scaled reference image, such + as the unwarped slide that corresponds to the unwarped processed reference image. + + If crop is "overlap", this should be the shape of the registered slides. + + + Returns + ------- + crop_xywh : tuple of int + Bounding box of crop area (XYWH) + + mask : ndarray + Mask, before crop + """ + + ref_slide = self.val_obj.get_ref_slide() + if crop == CROP_REF: + transformation_shape_rc = np.array(ref_slide.processed_img_shape_rc) + crop_xywh, mask = self.get_aligned_to_ref_slide_crop_xywh( + ref_img_shape_rc=transformation_shape_rc, + ref_M=ref_slide.M, + scaled_ref_img_shape_rc=out_shape_rc, + ) + elif crop == CROP_OVERLAP: + transformation_shape_rc = np.array(ref_slide.reg_img_shape_rc) + crop_xywh, mask = self.get_overlap_crop_xywh( + warped_img_shape_rc=transformation_shape_rc, + scaled_warped_img_shape_rc=out_shape_rc, + ) + + return crop_xywh, mask + + def get_crop_method(self, crop): + """Get string or logic defining how to crop the image""" + if crop is True: + crop_method = self.crop + else: + crop_method = crop + + do_crop = crop_method in [CROP_REF, CROP_OVERLAP] + + if do_crop: + return crop_method + else: + return False + + def get_bg_color_px_pos(self, cspace="Hunter LAB"): + """Get position of pixel that has color used for background""" + if self.img_type == slide_tools.IHC_NAME: + # RGB. Get brightest pixel + mean_rgb, color_mask, filtered_label_counts, color_clusterer = ( + preprocessing.find_dominant_colors( + self.image, cspace=cspace, return_xy_clusterer=True + ) + ) + mean_jab = preprocessing.rgb2jab(mean_rgb, cspace=cspace) + mean_jch = colour.models.Jab_to_JCh(mean_jab) + + # Find highest luminosity (L) and lowest colorfulness + bg_idx = np.lexsort([mean_jch[:, 1], -mean_jch[:, 0]])[ + 0 + ] # Last column sorted 1st. Returns ascending order + self.bg_color = mean_rgb[bg_idx, :] + + else: + # IF. Get darkest pixel + sum_img = self.image.sum(axis=2) + bg_px = np.unravel_index(np.argmin(sum_img, axis=None), sum_img.shape) + + self.bg_px_pos_rc = bg_px + self.bg_color = list(self.image[bg_px]) + + def update_results_img_paths(self): + n_digits = len(str(self.val_obj.size)) + stack_id = str.zfill(str(self.stack_idx), n_digits) + + self.processed_img_f = os.path.join( + self.val_obj.processed_dir, self.name + ".png" + ) + self.rigid_reg_img_f = os.path.join( + self.val_obj.reg_dst_dir, f"{stack_id}_f{self.name}.png" + ) + self.non_rigid_reg_img_f = os.path.join( + self.val_obj.non_rigid_dst_dir, f"{stack_id}_f{self.name}.png" + ) + if self.stored_dxdy: + bk_dxdy_f, fwd_dxdy_f = self.get_displacement_f() + self._bk_dxdy_f = bk_dxdy_f + self._fwd_dxdy_f = fwd_dxdy_f + + def get_displacement_f(self): + bk_dxdy_f = os.path.join( + self.val_obj.displacements_dir, f"{self.name}_bk_dxdy.tiff" + ) + fwd_dxdy_f = os.path.join( + self.val_obj.displacements_dir, f"{self.name}_fwd_dxdy.tiff" + ) + + return bk_dxdy_f, fwd_dxdy_f + + def get_bk_dxdy(self): + if self._bk_dxdy_np is None and not self.stored_dxdy: + return None + + elif self.stored_dxdy: + bk_dxdy_f, _ = self.get_displacement_f() + cropped_bk_dxdy = pyvips.Image.new_from_file(bk_dxdy_f) + full_bk_dxdy = self.val_obj.pad_displacement( + cropped_bk_dxdy, + self.val_obj._full_displacement_shape_rc, + self.val_obj._non_rigid_bbox, + ) + + else: + if np.any( + self._bk_dxdy_np.shape[1:2] != self.val_obj._full_displacement_shape_rc + ): + full_bk_dxdy = self.val_obj.pad_displacement( + self._bk_dxdy_np, + self.val_obj._full_displacement_shape_rc, + self.val_obj._non_rigid_bbox, + ) + else: + full_bk_dxdy = self._bk_dxdy_np + + return full_bk_dxdy + + def set_bk_dxdy(self, bk_dxdy): + """ + Only set if an array + """ + if not isinstance(bk_dxdy, pyvips.Image): + self._bk_dxdy_np = bk_dxdy + else: + logger.error(f"Cannot set bk_dxdy when data is type {type(bk_dxdy)}") + + bk_dxdy = property( + fget=get_bk_dxdy, fset=set_bk_dxdy, doc="Get and set backwards displacements" + ) + + def get_fwd_dxdy(self): + if self._fwd_dxdy_np is None and not self.stored_dxdy: + return None + + elif self.stored_dxdy: + _, fwd_dxdy_f = self.get_displacement_f() + cropped_fwd_dxdy = pyvips.Image.new_from_file(fwd_dxdy_f) + full_fwd_dxdy = self.val_obj.pad_displacement( + cropped_fwd_dxdy, + self.val_obj._full_displacement_shape_rc, + self.val_obj._non_rigid_bbox, + ) + + else: + if np.any( + self._fwd_dxdy_np.shape[1:2] != self.val_obj._full_displacement_shape_rc + ): + full_fwd_dxdy = self.val_obj.pad_displacement( + self._fwd_dxdy_np, + self.val_obj._full_displacement_shape_rc, + self.val_obj._non_rigid_bbox, + ) + else: + full_fwd_dxdy = self._fwd_dxdy_np + + return full_fwd_dxdy + + def set_fwd_dxdy(self, fwd_dxdy): + if not isinstance(fwd_dxdy, pyvips.Image): + self._fwd_dxdy_np = fwd_dxdy + else: + logger.error(f"Cannot set fwd_dxdy when data is type {type(fwd_dxdy)}") + + fwd_dxdy = property( + fget=get_fwd_dxdy, fset=set_fwd_dxdy, doc="Get forward displacements" + ) + + def warp_img( + self, + img: Optional[np.ndarray] = None, + non_rigid: bool = True, + crop: Union[bool, str, "CropMode"] = True, + interp_method: str = "bicubic", + ) -> np.ndarray: + """Warp an image using the registration parameters + + img : ndarray, optional + The image to be warped. If None, then Slide.image + will be warped. + + non_rigid : bool + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. + + crop: bool, str, or CropMode + How to crop the registered images. If `True`, then the same crop used + when initializing the `Valis` object will be used. If `False`, the + image will not be cropped. If "overlap" or CropMode.OVERLAP, the warped + slide will be cropped to include only areas where all images overlapped. + "reference" or CropMode.REFERENCE crops to the area that overlaps with + the reference image, defined by `reference_img_f` when initializing + the `Valis` object. + + interp_method : str + Interpolation method used when warping slide. Default is "bicubic" + + Returns + ------- + warped_img : ndarray + Warped copy of `img` + + """ + + if img is None: + img = self.image + + if non_rigid: + dxdy = self.bk_dxdy + else: + dxdy = None + + if isinstance(img, pyvips.Image): + img_shape_rc = (img.height, img.width) + img_dim = img.bands + else: + img_shape_rc = img.shape[0:2] + img_dim = img.ndim + + ref_slide = self.val_obj.get_ref_slide() + + if ( + self == ref_slide + and crop == CROP_REF + and np.all(warp_tools.get_shape(img)[0:2] == self.processed_img_shape_rc) + ): + # Save on computation time and avoid interpolation/rounding issues and return the original image + return img + + if not np.all(img_shape_rc == self.processed_img_shape_rc): + msg = ( + "scaling transformation for image with different shape. " + "However, without knowing all of other image's shapes, " + "the scaling may not be the same for all images, and so " + "may not overlap." + ) + logger.warning(msg) + same_shape = False + img_scale_rc = np.array(img_shape_rc) / ( + np.array(self.processed_img_shape_rc) + ) + out_shape_rc = self.val_obj.get_aligned_slide_shape(img_scale_rc) + + else: + same_shape = True + out_shape_rc = self.reg_img_shape_rc + + if isinstance(crop, bool) or isinstance(crop, str): + crop_method = self.get_crop_method(crop) + if crop_method is not False: + if crop_method == CROP_REF: + if not same_shape: + scaled_shape_rc = ( + np.array(ref_slide.processed_img_shape_rc) * img_scale_rc + ) + else: + scaled_shape_rc = ref_slide.processed_img_shape_rc + elif crop_method == CROP_OVERLAP: + scaled_shape_rc = out_shape_rc + + bbox_xywh, _ = self.get_crop_xywh( + crop=crop_method, out_shape_rc=scaled_shape_rc + ) + else: + bbox_xywh = None + + elif isinstance(crop[0], (int, float)) and len(crop) == 4: + bbox_xywh = crop + else: + bbox_xywh = None + + if img_dim == self.image.ndim: + bg_color = self.bg_color + else: + bg_color = None + + warped_img = warp_tools.warp_img( + img, + M=self.M, + bk_dxdy=dxdy, + out_shape_rc=out_shape_rc, + transformation_src_shape_rc=self.processed_img_shape_rc, + transformation_dst_shape_rc=self.reg_img_shape_rc, + bbox_xywh=bbox_xywh, + bg_color=bg_color, + interp_method=interp_method, + ) + + return warped_img + + def warp_img_from_to( + self, + img, + to_slide_obj, + dst_slide_level=0, + non_rigid=True, + interp_method="bicubic", + bg_color=None, + ): + """Warp an image from this slide onto another unwarped slide + + Note that if `img` is a labeled image/mask then it is recommended to set `interp_method` to "nearest" + + Parameters + ---------- + img : ndarray, pyvips.Image + Image to warp. Should be a scaled version of the same one used for registration + + to_slide_obj : Slide + Slide to which the points will be warped. I.e. `xy` + will be warped from this Slide to their position in + the unwarped slide associated with `to_slide_obj`. + + dst_slide_level: int, tuple, optional + Pyramid level of the slide/image that `img` will be warped on to + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. + + """ + + if np.issubdtype(type(dst_slide_level), np.integer): + to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][ + ::-1 + ] + aligned_slide_shape = self.val_obj.get_aligned_slide_shape(dst_slide_level) + else: + + to_slide_src_shape_rc = np.array(dst_slide_level) + + dst_scale_rc = to_slide_src_shape_rc / np.array( + to_slide_obj.processed_img_shape_rc + ) + aligned_slide_shape = np.round( + dst_scale_rc * np.array(to_slide_obj.reg_img_shape_rc) + ).astype(int) + + if non_rigid: + from_bk_dxdy = self.bk_dxdy + to_fwd_dxdy = to_slide_obj.fwd_dxdy + + else: + from_bk_dxdy = None + to_fwd_dxdy = None + + warped_img = warp_tools.warp_img_from_to( + img, + from_M=self.M, + from_transformation_src_shape_rc=self.processed_img_shape_rc, + from_transformation_dst_shape_rc=self.reg_img_shape_rc, + from_dst_shape_rc=aligned_slide_shape, + from_bk_dxdy=from_bk_dxdy, + to_M=to_slide_obj.M, + to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, + to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, + to_src_shape_rc=to_slide_src_shape_rc, + to_fwd_dxdy=to_fwd_dxdy, + bg_color=bg_color, + interp_method=interp_method, + ) + + return warped_img + + @valtils.deprecated_args(crop_to_overlap="crop") + def warp_slide( + self, + level, + non_rigid=True, + crop=True, + src_f=None, + interp_method="bicubic", + reader=None, + ): + """Warp a slide using registration parameters + + Parameters + ---------- + level : int + Pyramid level to be warped + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. Default is True + + crop: bool, str + How to crop the registered images. If `True`, then the same crop used + when initializing the `Valis` object will be used. If `False`, the + image will not be cropped. If "overlap", the warped slide will be + cropped to include only areas where all images overlapped. + "reference" crops to the area that overlaps with the reference image, + defined by `reference_img_f` when initialzing the `Valis object`. + + src_f : str, optional + Path of slide to be warped. If None (the default), Slide.src_f + will be used. Otherwise, the file to which `src_f` points to should + be an alternative copy of the slide, such as one that has undergone + processing (e.g. stain segmentation), has a mask applied, etc... + + interp_method : str + Interpolation method used when warping slide. Default is "bicubic" + + """ + if src_f is None: + src_f = self.src_f + + if non_rigid: + bk_dxdy = self.bk_dxdy + else: + bk_dxdy = None + + if level != 0: + if not np.issubdtype(type(level), np.integer): + msg = "Need slide level to be an integer indicating pyramid level" + logger.warning(msg) + aligned_slide_shape = self.val_obj.get_aligned_slide_shape(level) + else: + aligned_slide_shape = self.aligned_slide_shape_rc + + if isinstance(crop, bool) or isinstance(crop, str): + crop_method = self.get_crop_method(crop) + if crop_method is not False: + if crop_method == CROP_REF: + ref_slide = self.val_obj.get_ref_slide() + scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[level][::-1] + + elif crop_method == CROP_OVERLAP: + scaled_aligned_shape_rc = aligned_slide_shape + + slide_bbox_xywh, _ = self.get_crop_xywh( + crop=crop_method, out_shape_rc=scaled_aligned_shape_rc + ) + + if crop_method == CROP_REF: + assert np.all(slide_bbox_xywh[2:] == scaled_aligned_shape_rc[::-1]) + if src_f == self.src_f and self == ref_slide: + # Shouldn't need to warp, but do checks just in case + no_rigid = True + no_non_rigid = True + if self.M is not None: + sxy = ( + scaled_aligned_shape_rc / self.processed_img_shape_rc + )[::-1] + scaled_txy = sxy * self.M[:2, 2] + no_transforms = all( + self.M[:2, :2].reshape(-1) == [1, 0, 0, 1] + ) + crop_to_origin = np.all( + np.abs(slide_bbox_xywh[0:2] + scaled_txy) < 1 + ) + no_rigid = no_transforms and crop_to_origin + + if self.bk_dxdy is not None: + no_non_rigid = ( + self.bk_dxdy.min() == 0 and self.bk_dxdy.max() == 0 + ) + + if no_rigid and no_non_rigid: + # Don't need to warp, so return original reference image + ref_img = self.reader.slide2vips(level=level) + return ref_img + + else: + slide_bbox_xywh = None + + elif isinstance(crop[0], (int, float)) and len(crop) == 4: + slide_bbox_xywh = crop + else: + slide_bbox_xywh = None + + if src_f == self.src_f: + bg_color = self.bg_color + else: + bg_color = None + + if reader is None: + reader = self.reader + + warped_slide = slide_tools.warp_slide( + src_f, + M=self.M, + transformation_src_shape_rc=self.processed_img_shape_rc, + transformation_dst_shape_rc=self.reg_img_shape_rc, + aligned_slide_shape_rc=aligned_slide_shape, + dxdy=bk_dxdy, + level=level, + series=self.series, + interp_method=interp_method, + bbox_xywh=slide_bbox_xywh, + bg_color=bg_color, + reader=reader, + ) + return warped_slide + + def warp_and_save_slide( + self, + dst_f, + level=0, + non_rigid=True, + crop=True, + src_f=None, + channel_names=None, + colormap=slide_io.CMAP_AUTO, + interp_method="bicubic", + tile_wh=None, + compression=DEFAULT_COMPRESSION, + Q=100, + pyramid=True, + reader=None, + ): + """Warp and save a slide + + Slides will be saved in the ome.tiff format. + + Parameters + ---------- + dst_f : str + Path to were the warped slide will be saved. + + level : int + Pyramid level to be warped + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. Default is True + + crop: bool, str + How to crop the registered images. If `True`, then the same crop used + when initializing the `Valis` object will be used. If `False`, the + image will not be cropped. If "overlap", the warped slide will be + cropped to include only areas where all images overlapped. + "reference" crops to the area that overlaps with the reference image, + defined by `reference_img_f` when initializing the `Valis object`. + + channel_names : list, optional + List of channel names. If None, then Slide.reader + will attempt to find the channel names associated with `src_f`. + + colormap : dict, optional + Dictionary of channel colors, where the key is the channel name, and the value the color as rgb255. + If None (default), the channel colors from `current_ome_xml_str` will be used, if available. + If None, and there are no channel colors in the `current_ome_xml_str`, then no colors will be added + + src_f : str, optional + Path of slide to be warped. If None (the default), Slide.src_f + will be used. Otherwise, the file to which `src_f` points to should + be an alternative copy of the slide, such as one that has undergone + processing (e.g. stain segmentation), has a mask applied, etc... + + interp_method : str + Interpolation method used when warping slide. Default is "bicubic" + + tile_wh : int, optional + Tile width and height used to save image + + compression : str + Compression method used to save ome.tiff. See pyips for more details. + + Q : int + Q factor for lossy compression + + pyramid : bool + Whether or not to save an image pyramid. + """ + + if src_f is None: + src_f = self.src_f + + if reader is None: + if src_f != self.src_f: + slide_reader_cls = slide_io.get_slide_reader(src_f) + reader = slide_reader_cls(src_f) + else: + reader = self.reader + + warped_slide = self.warp_slide( + level=level, + non_rigid=non_rigid, + crop=crop, + interp_method=interp_method, + src_f=src_f, + reader=reader, + ) + + # Get ome-xml # + ref_slide = self.val_obj.get_ref_slide() + pixel_physical_size_xyu = ref_slide.reader.scale_physical_size(level) + + ome_xml_obj = slide_io.update_xml_for_new_img( + img=warped_slide, + reader=reader, + level=level, + channel_names=channel_names, + colormap=colormap, + pixel_physical_size_xyu=pixel_physical_size_xyu, + ) + + ome_xml = ome_xml_obj.to_xml() + + out_shape_wh = warp_tools.get_shape(warped_slide)[0:2][::-1] + tile_wh = slide_io.get_tile_wh( + reader=reader, level=level, out_shape_wh=out_shape_wh + ) + + slide_io.save_ome_tiff( + warped_slide, + dst_f=dst_f, + ome_xml=ome_xml, + tile_wh=tile_wh, + compression=compression, + Q=Q, + pyramid=pyramid, + ) + + def warp_xy( + self, + xy: np.ndarray, + M: Optional[np.ndarray] = None, + slide_level: Union[int, tuple[int, int]] = 0, + pt_level: Union[int, tuple[int, int]] = 0, + non_rigid: bool = True, + crop: Union[bool, str, "CropMode"] = True, + ) -> np.ndarray: + """Warp points using registration parameters + + Warps `xy` to their location in the registered slide/image + + Parameters + ---------- + xy : ndarray + (N, 2) array of points to be warped. Must be x,y coordinates + + slide_level: int, tuple, optional + Pyramid level of the slide. Used to scale transformation matrices. + Can also be the shape of the warped image (row, col) into which + the points should be warped. Default is 0. + + pt_level: int, tuple, optional + Pyramid level from which the points origingated. For example, if + `xy` are from the centroids of cell segmentation performed on the + full resolution image, this should be 0. Alternatively, the value can + be a tuple of the image's shape (row, col) from which the points came. + For example, if `xy` are bounding box coordinates from an analysis on + a lower resolution image, then pt_level is that lower resolution + image's shape (row, col). Default is 0. + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. Default is True. + + crop: bool, str + Apply crop to warped points by shifting points to the mask's origin. + Note that this can result in negative coordinates, but might be useful + if wanting to draw the coordinates on the registered slide, such as + annotation coordinates. + + If `True`, then the same crop used + when initializing the `Valis` object will be used. If `False`, the + image will not be cropped. If "overlap", the warped slide will be + cropped to include only areas where all images overlapped. + "reference" crops to the area that overlaps with the reference image, + defined by `reference_img_f` when initialzing the `Valis object`. + + """ + if M is None: + M = self.M + + if np.issubdtype(type(pt_level), np.integer): + pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] + else: + pt_dim_rc = np.array(pt_level) + + if np.issubdtype(type(slide_level), np.integer): + if slide_level != 0: + if np.issubdtype(type(slide_level), np.integer): + aligned_slide_shape = self.val_obj.get_aligned_slide_shape( + slide_level + ) + else: + aligned_slide_shape = np.array(slide_level) + else: + aligned_slide_shape = self.aligned_slide_shape_rc + else: + aligned_slide_shape = np.array(slide_level) + + if non_rigid: + fwd_dxdy = self.fwd_dxdy + else: + fwd_dxdy = None + + warped_xy = warp_tools.warp_xy( + xy, + M=M, + transformation_src_shape_rc=self.processed_img_shape_rc, + transformation_dst_shape_rc=self.reg_img_shape_rc, + src_shape_rc=pt_dim_rc, + dst_shape_rc=aligned_slide_shape, + fwd_dxdy=fwd_dxdy, + ) + crop_method = self.get_crop_method(crop) + if crop_method is not False: + if crop_method == CROP_REF: + ref_slide = self.val_obj.get_ref_slide() + if isinstance(slide_level, int): + scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[ + slide_level + ][::-1] + else: + if len(slide_level) == 2: + scaled_aligned_shape_rc = slide_level + elif crop_method == CROP_OVERLAP: + scaled_aligned_shape_rc = aligned_slide_shape + + crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) + warped_xy -= crop_bbox_xywh[0:2] + + return warped_xy + + def warp_xy_from_to( + self, + xy, + to_slide_obj, + src_slide_level=0, + src_pt_level=0, + dst_slide_level=0, + non_rigid=True, + ): + """Warp points from this slide to another unwarped slide + + Takes a set of points found in this unwarped slide, and warps them to + their position in the unwarped "to" slide. + + Parameters + ---------- + xy : ndarray + (N, 2) array of points to be warped. Must be x,y coordinates + + to_slide_obj : Slide + Slide to which the points will be warped. I.e. `xy` + will be warped from this Slide to their position in + the unwarped slide associated with `to_slide_obj`. + + src_pt_level: int, tuple, optional + Pyramid level of the slide/image in which `xy` originated. + For example, if `xy` are from the centroids of cell segmentation + performed on the unwarped full resolution image, this should be 0. + Alternatively, the value can be a tuple of the image's shape (row, col) + from which the points came. For example, if `xy` are bounding + box coordinates from an analysis on a lower resolution image, + then pt_level is that lower resolution image's shape (row, col). + + dst_slide_level: int, tuple, optional + Pyramid level of the slide/image in to `xy` will be warped. + Similar to `src_pt_level`, if `dst_slide_level` is an int then + the points will be warped to that pyramid level. If `dst_slide_level` + is the "to" image's shape (row, col), then the points will be warped + to their location in an image with that same shape. + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. + + """ + + if np.issubdtype(type(src_pt_level), np.integer): + src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] + else: + src_pt_dim_rc = np.array(src_pt_level) + + if np.issubdtype(type(dst_slide_level), np.integer): + to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][ + ::-1 + ] + else: + to_slide_src_shape_rc = np.array(dst_slide_level) + + if src_slide_level != 0: + if np.issubdtype(type(src_slide_level), np.integer): + aligned_slide_shape = self.val_obj.get_aligned_slide_shape( + src_slide_level + ) + else: + aligned_slide_shape = np.array(src_slide_level) + else: + aligned_slide_shape = self.aligned_slide_shape_rc + + if non_rigid: + src_fwd_dxdy = self.fwd_dxdy + dst_bk_dxdy = to_slide_obj.bk_dxdy + + else: + src_fwd_dxdy = None + dst_bk_dxdy = None + + xy_in_unwarped_to_img = warp_tools.warp_xy_from_to( + xy=xy, + from_M=self.M, + from_transformation_dst_shape_rc=self.reg_img_shape_rc, + from_transformation_src_shape_rc=self.processed_img_shape_rc, + from_dst_shape_rc=aligned_slide_shape, + from_src_shape_rc=src_pt_dim_rc, + from_fwd_dxdy=src_fwd_dxdy, + to_M=to_slide_obj.M, + to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, + to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, + to_src_shape_rc=to_slide_src_shape_rc, + to_dst_shape_rc=aligned_slide_shape, + to_bk_dxdy=dst_bk_dxdy, + ) + + return xy_in_unwarped_to_img + + def warp_geojson( + self, + geojson_f: Union[str, pathlib.Path], + M: Optional[np.ndarray] = None, + slide_level: Union[int, tuple[int, int]] = 0, + pt_level: Union[int, tuple[int, int]] = 0, + non_rigid: bool = True, + crop: Union[bool, str, "CropMode"] = True, + ) -> dict: + """Warp geometry using registration parameters + + Warps geometries to their location in the registered slide/image + + Parameters + ---------- + geojson_f : str or Path + Path to geojson file containing the annotation geometries. Assumes + coordinates are in pixels. + + slide_level: int, tuple, optional + Pyramid level of the slide. Used to scale transformation matrices. + Can also be the shape of the warped image (row, col) into which + the points should be warped. Default is 0. + + pt_level: int, tuple, optional + Pyramid level from which the points origingated. For example, if + `xy` are from the centroids of cell segmentation performed on the + full resolution image, this should be 0. Alternatively, the value can + be a tuple of the image's shape (row, col) from which the points came. + For example, if `xy` are bounding box coordinates from an analysis on + a lower resolution image, then pt_level is that lower resolution + image's shape (row, col). Default is 0. + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. Default is True. + + crop: bool, str + Apply crop to warped points by shifting points to the mask's origin. + Note that this can result in negative coordinates, but might be useful + if wanting to draw the coordinates on the registered slide, such as + annotation coordinates. + + If `True`, then the same crop used + when initializing the `Valis` object will be used. If `False`, the + image will not be cropped. If "overlap", the warped slide will be + cropped to include only areas where all images overlapped. + "reference" crops to the area that overlaps with the reference image, + defined by `reference_img_f` when initialzing the `Valis object`. + + """ + if M is None: + M = self.M + + if np.issubdtype(type(pt_level), np.integer): + pt_dim_rc = self.slide_dimensions_wh[pt_level][::-1] + else: + pt_dim_rc = np.array(pt_level) + + if np.issubdtype(type(slide_level), np.integer): + if slide_level != 0: + if np.issubdtype(type(slide_level), np.integer): + aligned_slide_shape = self.val_obj.get_aligned_slide_shape( + slide_level + ) + else: + aligned_slide_shape = np.array(slide_level) + else: + aligned_slide_shape = self.aligned_slide_shape_rc + else: + aligned_slide_shape = np.array(slide_level) + + if non_rigid: + fwd_dxdy = self.fwd_dxdy + else: + fwd_dxdy = None + + with open(geojson_f) as f: + annotation_geojson = json.load(f) + + crop_method = self.get_crop_method(crop) + if crop_method is not False: + if crop_method == CROP_REF: + ref_slide = self.val_obj.get_ref_slide() + if isinstance(slide_level, int): + scaled_aligned_shape_rc = ref_slide.slide_dimensions_wh[ + slide_level + ][::-1] + else: + if len(slide_level) == 2: + scaled_aligned_shape_rc = slide_level + elif crop_method == CROP_OVERLAP: + scaled_aligned_shape_rc = aligned_slide_shape + + crop_bbox_xywh, _ = self.get_crop_xywh(crop_method, scaled_aligned_shape_rc) + shift_xy = crop_bbox_xywh[0:2] + else: + shift_xy = None + + warped_features = [None] * len(annotation_geojson["features"]) + for i, ft in tqdm.tqdm( + enumerate(annotation_geojson["features"]), + desc=WARP_ANNO_MSG, + unit="annotation", + ): + geom = shapely.geometry.shape(ft["geometry"]) + warped_geom = warp_tools.warp_shapely_geom( + geom, + M=M, + transformation_src_shape_rc=self.processed_img_shape_rc, + transformation_dst_shape_rc=self.reg_img_shape_rc, + src_shape_rc=pt_dim_rc, + dst_shape_rc=aligned_slide_shape, + fwd_dxdy=fwd_dxdy, + shift_xy=shift_xy, + ) + warped_ft = deepcopy(ft) + warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) + warped_features[i] = warped_ft + + warped_geojson = { + "type": annotation_geojson["type"], + "features": warped_features, + } + + return warped_geojson + + def warp_geojson_from_to( + self, + geojson_f, + to_slide_obj, + src_slide_level=0, + src_pt_level=0, + dst_slide_level=0, + non_rigid=True, + ): + """Warp geoms in geojson file from annotation slide to another unwarped slide + + Takes a set of geometries found in this annotation slide, and warps them to + their position in the unwarped "to" slide. + + Parameters + ---------- + geojson_f : str + Path to geojson file containing the annotation geometries. Assumes + coordinates are in pixels. + + to_slide_obj : Slide + Slide to which the points will be warped. I.e. `xy` + will be warped from this Slide to their position in + the unwarped slide associated with `to_slide_obj`. + + src_pt_level: int, tuple, optional + Pyramid level of the slide/image in which `xy` originated. + For example, if `xy` are from the centroids of cell segmentation + performed on the unwarped full resolution image, this should be 0. + Alternatively, the value can be a tuple of the image's shape (row, col) + from which the points came. For example, if `xy` are bounding + box coordinates from an analysis on a lower resolution image, + then pt_level is that lower resolution image's shape (row, col). + + dst_slide_level: int, tuple, optional + Pyramid level of the slide/image in to `xy` will be warped. + Similar to `src_pt_level`, if `dst_slide_level` is an int then + the points will be warped to that pyramid level. If `dst_slide_level` + is the "to" image's shape (row, col), then the points will be warped + to their location in an image with that same shape. + + non_rigid : bool, optional + Whether or not to conduct non-rigid warping. If False, + then only a rigid transformation will be applied. + + Returns + ------- + warped_geojson : dict + Dictionry of warped geojson geometries + + """ + + if np.issubdtype(type(src_pt_level), np.integer): + src_pt_dim_rc = self.slide_dimensions_wh[src_pt_level][::-1] + else: + src_pt_dim_rc = np.array(src_pt_level) + + if np.issubdtype(type(dst_slide_level), np.integer): + to_slide_src_shape_rc = to_slide_obj.slide_dimensions_wh[dst_slide_level][ + ::-1 + ] + else: + to_slide_src_shape_rc = np.array(dst_slide_level) + + if src_slide_level != 0: + if np.issubdtype(type(src_slide_level), np.integer): + aligned_slide_shape = self.val_obj.get_aligned_slide_shape( + src_slide_level + ) + else: + aligned_slide_shape = np.array(src_slide_level) + else: + aligned_slide_shape = self.aligned_slide_shape_rc + + if non_rigid: + src_fwd_dxdy = self.fwd_dxdy + dst_bk_dxdy = to_slide_obj.bk_dxdy + + else: + src_fwd_dxdy = None + dst_bk_dxdy = None + + with open(geojson_f) as f: + annotation_geojson = json.load(f) + + warped_features = [None] * len(annotation_geojson["features"]) + for i, ft in tqdm.tqdm( + enumerate(annotation_geojson["features"]), + desc=WARP_ANNO_MSG, + unit="annotation", + ): + geom = shapely.geometry.shape(ft["geometry"]) + warped_geom = warp_tools.warp_shapely_geom_from_to( + geom=geom, + from_M=self.M, + from_transformation_dst_shape_rc=self.reg_img_shape_rc, + from_transformation_src_shape_rc=self.processed_img_shape_rc, + from_dst_shape_rc=aligned_slide_shape, + from_src_shape_rc=src_pt_dim_rc, + from_fwd_dxdy=src_fwd_dxdy, + to_M=to_slide_obj.M, + to_transformation_src_shape_rc=to_slide_obj.processed_img_shape_rc, + to_transformation_dst_shape_rc=to_slide_obj.reg_img_shape_rc, + to_src_shape_rc=to_slide_src_shape_rc, + to_dst_shape_rc=aligned_slide_shape, + to_bk_dxdy=dst_bk_dxdy, + ) + + warped_ft = deepcopy(ft) + warped_ft["geometry"] = shapely.geometry.mapping(warped_geom) + warped_features[i] = warped_ft + + warped_geojson = { + "type": annotation_geojson["type"], + "features": warped_features, + } + + return warped_geojson + + def pad_cropped_processed_img(self): + """ + Pad cropped processed image to have original dimensions + """ + vips_img = warp_tools.numpy2vips(self.processed_img) + + padded = vips_img.embed( + self.processed_crop_bbox[0], + self.processed_crop_bbox[1], + self.uncropped_processed_img_shape_rc[1], + self.uncropped_processed_img_shape_rc[0], + extend=pyvips.enums.Extend.BLACK, + ) + scaled_padded = warp_tools.resize_img(padded, self.processed_img_shape_rc) + scaled_padded_np = warp_tools.vips2numpy(scaled_padded) + + return scaled_padded_np diff --git a/src/valis/registration/state.py b/src/valis/registration/state.py new file mode 100644 index 00000000..925db108 --- /dev/null +++ b/src/valis/registration/state.py @@ -0,0 +1,284 @@ +"""Serialization helpers and shared types for the registration package. + +Contains ``load_registrar``, ``DisplacementField``, ``CropMode``, and +``RegistrationConfig``. Import these from ``valis.registration`` directly; +the sub-module layout is an implementation detail. +""" + +import logging +import os +import pathlib +import pickle +from dataclasses import dataclass, field +from typing import Optional, Union + +import numpy as np +import pyvips + +from ._constants import ( + CropMode, + DEFAULT_MATCHER, + DEFAULT_MATCHER_FOR_SORTING, + DEFAULT_TRANSFORM_CLASS, + DEFAULT_AFFINE_OPTIMIZER_CLASS, + DEFAULT_SIMILARITY_METRIC, + DEFAULT_NON_RIGID_CLASS, + DEFAULT_NON_RIGID_KWARGS, + DEFAULT_MAX_IMG_DIM, + DEFAULT_MAX_PROCESSED_IMG_SIZE, + DEFAULT_MAX_NON_RIGID_REG_SIZE, + DEFAULT_THUMBNAIL_SIZE, + DEFAULT_NORM_METHOD, +) +from .. import warp_tools + +logger = logging.getLogger(__name__) + + +def load_registrar(src_f: Union[str, pathlib.Path]) -> "Valis": # noqa: F821 + """Load a Valis object + + Parameters + ---------- + src_f : str or Path + Path to pickled Valis object + + Returns + ------- + registrar : Valis + + Valis object used for registration + + """ + registrar = pickle.load(open(src_f, "rb")) + + data_dir = registrar.data_dir + read_data_dir = os.path.split(src_f)[0] + + # If registrar has moved, will need to update paths to results + # and displacement fields + if data_dir != read_data_dir: + new_dst_dir = os.path.split(read_data_dir)[0] + registrar.dst_dir = new_dst_dir + registrar.set_dst_paths() + + for slide_obj in registrar.slide_dict.values(): + slide_obj.update_results_img_paths() + + return registrar + + +class DisplacementField: + """Owns a single displacement field, transparently backed by memory or disk. + + Encapsulates the three-way storage state that ``Slide`` previously managed + with ``stored_dxdy``, ``_bk_dxdy_np``, and ``_bk_dxdy_f`` attributes. + + Parameters + ---------- + array : ndarray, optional + In-memory numpy displacement array. + path : str or Path, optional + Path to a TIFF on disk (used when ``stored_dxdy=True``). + """ + + def __init__( + self, + array: Optional[np.ndarray] = None, + path: Optional[Union[str, pathlib.Path]] = None, + ): + self._array: Optional[np.ndarray] = array + self._path: Optional[pathlib.Path] = pathlib.Path(path) if path else None + + # ------------------------------------------------------------------ + # Properties + + @property + def is_empty(self) -> bool: + return self._array is None and self._path is None + + @property + def is_on_disk(self) -> bool: + return self._path is not None + + # ------------------------------------------------------------------ + # Data access + + def as_numpy(self) -> Optional[np.ndarray]: + """Return displacement as a numpy array (lazy-loads from disk if needed).""" + if self._array is not None: + return self._array + if self._path is not None and self._path.exists(): + vips_img = pyvips.Image.new_from_file(str(self._path)) + return warp_tools.vips2numpy(vips_img) + return None + + def as_vips(self) -> Optional[pyvips.Image]: + """Return displacement as a pyvips Image (lazy-loads from disk if needed).""" + if self._path is not None and self._path.exists(): + return pyvips.Image.new_from_file(str(self._path)) + if self._array is not None: + return warp_tools.numpy2vips(self._array) + return None + + # ------------------------------------------------------------------ + # Persistence + + def set_array(self, array: np.ndarray) -> None: + """Store displacement as an in-memory numpy array.""" + if isinstance(array, pyvips.Image): + raise TypeError( + "DisplacementField.set_array() expects a numpy array; " + "use set_path() for pyvips-backed storage." + ) + self._array = array + + def set_path(self, path: Union[str, pathlib.Path]) -> None: + """Record the on-disk path (does not read from or write to it).""" + self._path = pathlib.Path(path) + + def save(self, path: Union[str, pathlib.Path]) -> None: + """Save the in-memory array to *path* as a TIFF.""" + if self._array is None: + raise ValueError("No in-memory array to save.") + path = pathlib.Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + vips_img = warp_tools.numpy2vips(self._array) + vips_img.tiffsave(str(path)) + self._path = path + + def load(self, path: Union[str, pathlib.Path]) -> None: + """Load the displacement from *path* into memory.""" + self._path = pathlib.Path(path) + self._array = None # force lazy reload on next as_numpy() call + + +@dataclass +class RegistrationConfig: + """Groups the registration-flow parameters for ``Valis``. + + Passing a ``RegistrationConfig`` to ``Valis.__init__`` via the ``config`` + keyword is the preferred way to configure the pipeline for new code. + All individual keyword arguments on ``Valis.__init__`` continue to work + unchanged for backward compatibility; if both are supplied the explicit + keyword argument takes precedence over the value in ``config``. + + Parameters + ---------- + feature_detector_cls : + FeatureDD class for feature detection. ``None`` means use whatever + is set on ``matcher``. + matcher : + Matcher used after image ordering. + matcher_for_sorting : + Matcher used to build the image-dissimilarity matrix. + transformer_cls : + Scikit-image rigid transform class. + affine_optimizer_cls : + AffineOptimizer class, or ``None`` to skip optimisation. + similarity_metric : + Metric for building the dissimilarity matrix (``"n_matches"`` or a + scipy distance string). + non_rigid_registrar_cls : + NonRigidRegistrar instance, or ``None`` to skip non-rigid registration. + non_rigid_reg_params : + Keyword arguments forwarded to ``non_rigid_registrar_cls``. + compose_non_rigid : + Whether to compose non-rigid deformation fields serially. + micro_rigid_registrar_cls : + MicroRigidRegistrar class for high-resolution refinement, or ``None``. + micro_rigid_registrar_params : + Keyword arguments forwarded to ``micro_rigid_registrar_cls``. + imgs_ordered : + ``True`` if images are already in the correct order. + do_rigid : + ``True`` to perform rigid registration. + denoise_rigid : + ``True`` to denoise processed images before rigid registration. + crop_for_rigid_reg : + ``True`` to zoom into the tissue mask before rigid registration. + check_for_reflections : + ``True`` to test whether flipped/mirrored images align better. + max_image_dim_px : + Maximum width/height of images loaded from pyramid levels. + max_processed_image_dim_px : + Maximum width/height of processed images used for feature detection. + max_non_rigid_registration_dim_px : + Maximum width/height used for non-rigid registration. + crop : + Default crop mode for warped output. Accepts a ``CropMode`` value or + the equivalent string (``"overlap"``, ``"reference"``, ``"all"``). + create_masks : + ``True`` to create and apply tissue masks during registration. + thumbnail_size : + Maximum width/height of the result thumbnails written to ``dst_dir``. + norm_method : + Normalisation strategy: ``"img_stats"``, ``"histo_match"``, or ``None``. + + Examples + -------- + Use the defaults (equivalent to calling ``Valis`` with no configuration + kwargs): + + >>> from valis.registration import Valis, RegistrationConfig + >>> registrar = Valis(src_dir, dst_dir, config=RegistrationConfig()) + + IHC-optimised preset — larger processing window, no non-rigid step: + + >>> cfg = RegistrationConfig.for_ihc() + >>> registrar = Valis(src_dir, dst_dir, config=cfg) + """ + + feature_detector_cls: object = None + matcher: object = field(default_factory=lambda: DEFAULT_MATCHER) + matcher_for_sorting: object = field( + default_factory=lambda: DEFAULT_MATCHER_FOR_SORTING + ) + transformer_cls: type = DEFAULT_TRANSFORM_CLASS + affine_optimizer_cls: Optional[type] = DEFAULT_AFFINE_OPTIMIZER_CLASS + similarity_metric: str = DEFAULT_SIMILARITY_METRIC + non_rigid_registrar_cls: object = field( + default_factory=lambda: DEFAULT_NON_RIGID_CLASS + ) + non_rigid_reg_params: dict = field(default_factory=dict) + compose_non_rigid: bool = False + micro_rigid_registrar_cls: Optional[type] = None + micro_rigid_registrar_params: dict = field(default_factory=dict) + imgs_ordered: bool = False + do_rigid: bool = True + denoise_rigid: bool = False + crop_for_rigid_reg: bool = True + check_for_reflections: bool = False + max_image_dim_px: int = DEFAULT_MAX_IMG_DIM + max_processed_image_dim_px: int = DEFAULT_MAX_PROCESSED_IMG_SIZE + max_non_rigid_registration_dim_px: int = DEFAULT_MAX_NON_RIGID_REG_SIZE + crop: Optional[Union[str, "CropMode"]] = None + create_masks: bool = True + thumbnail_size: int = DEFAULT_THUMBNAIL_SIZE + norm_method: Optional[str] = DEFAULT_NORM_METHOD + + @classmethod + def for_ihc(cls) -> "RegistrationConfig": + """Preset for brightfield IHC/H&E image series. + + Uses a larger processing window for better feature coverage and skips + non-rigid registration, which is often unnecessary for well-prepared + brightfield sections. + """ + return cls( + max_processed_image_dim_px=1024, + non_rigid_registrar_cls=None, + crop=CropMode.REFERENCE, + ) + + @classmethod + def for_cycif(cls) -> "RegistrationConfig": + """Preset for cyclic immunofluorescence (CyCIF) image series. + + Enables non-rigid registration and uses overlap cropping to handle + the field-of-view shifts common in multi-round imaging. + """ + return cls( + non_rigid_registrar_cls=DEFAULT_NON_RIGID_CLASS, + crop=CropMode.OVERLAP, + ) diff --git a/valis/serial_non_rigid.py b/src/valis/serial_non_rigid.py similarity index 76% rename from valis/serial_non_rigid.py rename to src/valis/serial_non_rigid.py index de345f69..b4ce9c51 100644 --- a/valis/serial_non_rigid.py +++ b/src/valis/serial_non_rigid.py @@ -1,7 +1,6 @@ -"""Classes and functions to perform serial non-rigid registration of a set of images - -""" +"""Classes and functions to perform serial non-rigid registration of a set of images""" +import logging import torch import kornia @@ -24,11 +23,14 @@ from . import preprocessing from . import slide_tools +logger = logging.getLogger(__name__) + IMG_LIST_KEY = "img_list" IMG_F_LIST_KEY = "img_f_list" IMG_NAME_KEY = "name_list" MASK_LIST_KEY = "mask_list" + def get_matching_xy_from_rigid_registrar(rigid_registrar, ref_img_name=None): """Get matching keypoints to use in serial non-rigid registration @@ -101,8 +103,11 @@ def get_imgs_from_dir(src_dir): List of masks used for registration """ - img_f_list = [f for f in os.listdir(src_dir) if - slide_tools.get_img_type(os.path.join(src_dir, f)) is not None] + img_f_list = [ + f + for f in os.listdir(src_dir) + if slide_tools.get_img_type(os.path.join(src_dir, f)) is not None + ] valtils.sort_nicely(img_f_list) @@ -150,9 +155,12 @@ def get_imgs_rigid_reg(serial_rigid_reg): # Moving mask temp_mask = np.full_like(img_obj.image, 255) - img_mask = warp_tools.warp_img(temp_mask, M=img_obj.M, - out_shape_rc=img_obj.registered_img.shape, - interp_method="nearest") + img_mask = warp_tools.warp_img( + temp_mask, + M=img_obj.M, + out_shape_rc=img_obj.registered_img.shape, + interp_method="nearest", + ) mask_list[i] = img_mask return img_list, img_f_list, img_names, mask_list @@ -217,7 +225,7 @@ def get_imgs_from_dict(img_dict): class NonRigidZImage(object): - """ Class that store info about an image, including both + """Class that store info about an image, including both rigid and non-rigid registration parameters Attributes @@ -256,7 +264,9 @@ class NonRigidZImage(object): """ - def __init__(self, reg_obj, image, name, stack_idx, moving_xy=None, fixed_xy=None, mask=None): + def __init__( + self, reg_obj, image, name, stack_idx, moving_xy=None, fixed_xy=None, mask=None + ): """ Parameters ---------- @@ -344,8 +354,8 @@ def split_params(self, params, non_rigid_reg_class): init_arg_list = inspect.getfullargspec(non_rigid_reg_class.__init__).args reg_arg_list = inspect.getfullargspec(non_rigid_reg_class.register).args - init_kwargs = {k:v for k, v in params.items() if k in init_arg_list} - reg_kwargs = {k:v for k, v in params.items() if k in reg_arg_list} + init_kwargs = {k: v for k, v in params.items() if k in init_arg_list} + reg_kwargs = {k: v for k, v in params.items() if k in reg_arg_list} else: init_kwargs = {} @@ -353,8 +363,14 @@ def split_params(self, params, non_rigid_reg_class): return init_kwargs, reg_kwargs - def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, - bk_dxdy=None, params=None, mask=None): + def calc_deformation( + self, + registered_fixed_image, + non_rigid_reg_obj, + bk_dxdy=None, + params=None, + mask=None, + ): """ Finds the non-rigid deformation fields that align this ("moving") image to the "fixed" image @@ -413,11 +429,12 @@ def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, for_reg_dxdy = bk_dxdy if self.reg_obj.from_rigid_reg: - for_reg_dxdy = warp_tools.remove_invasive_displacements(for_reg_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) + for_reg_dxdy = warp_tools.remove_invasive_displacements( + for_reg_dxdy, + M=M, + src_shape_rc=unwarped_shape, + out_shape_rc=og_reg_shape_rc, + ) moving_img = warp_tools.warp_img(self.image, bk_dxdy=for_reg_dxdy) if reg_mask is not None: @@ -430,16 +447,25 @@ def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, if self.is_vips: bk_dxdy = pyvips.Image.black(self.shape[1], self.shape[0], bands=2) else: - bk_dxdy = np.array([np.zeros(self.shape[0:2]), np.zeros(self.shape[0:2])]) + bk_dxdy = np.array( + [np.zeros(self.shape[0:2]), np.zeros(self.shape[0:2])] + ) init_kwargs, reg_kwargs = self.split_params(params, non_rigid_reg_obj) - if self.moving_xy is not None and self.fixed_xy is not None and \ - issubclass(non_rigid_reg_obj.__class__, non_rigid_registrars.NonRigidRegistrarXY): + if ( + self.moving_xy is not None + and self.fixed_xy is not None + and issubclass( + non_rigid_reg_obj.__class__, non_rigid_registrars.NonRigidRegistrarXY + ) + ): if for_reg_dxdy is not None: # Update positions # fwd_dxdy = warp_tools.get_inverse_field(for_reg_dxdy) fixed_xy = warp_tools.warp_xy(self.fixed_xy, M=None, fwd_dxdy=fwd_dxdy) - moving_xy = warp_tools.warp_xy(self.moving_xy, M=None, fwd_dxdy=fwd_dxdy) + moving_xy = warp_tools.warp_xy( + self.moving_xy, M=None, fwd_dxdy=fwd_dxdy + ) else: fixed_xy = self.fixed_xy moving_xy = self.moving_xy @@ -450,25 +476,28 @@ def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, xy_args = {"moving_xy": moving_xy, "fixed_xy": fixed_xy} reg_kwargs.update(xy_args) - warped_moving, moving_grid_img, moving_bk_dxdy = \ - non_rigid_reg_obj.register(moving_img=moving_img, - fixed_img=registered_fixed_image, - mask=reg_mask, - **reg_kwargs) + warped_moving, moving_grid_img, moving_bk_dxdy = non_rigid_reg_obj.register( + moving_img=moving_img, + fixed_img=registered_fixed_image, + mask=reg_mask, + **reg_kwargs, + ) if self.reg_obj.from_rigid_reg: - moving_bk_dxdy = warp_tools.remove_invasive_displacements(moving_bk_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) + moving_bk_dxdy = warp_tools.remove_invasive_displacements( + moving_bk_dxdy, + M=M, + src_shape_rc=unwarped_shape, + out_shape_rc=og_reg_shape_rc, + ) if not self.check_if_vips(moving_bk_dxdy): if reg_mask is not None: # Only add new transformations moving_bk_dxdy = self.mask_dxdy(moving_bk_dxdy, reg_mask) - bk_dxdy_from_ref = np.array([bk_dxdy[0] + moving_bk_dxdy[0], - bk_dxdy[1] + moving_bk_dxdy[1]]) + bk_dxdy_from_ref = np.array( + [bk_dxdy[0] + moving_bk_dxdy[0], bk_dxdy[1] + moving_bk_dxdy[1]] + ) else: if reg_mask is not None: moving_bk_dxdy = self.mask_dxdy(moving_bk_dxdy, reg_mask) @@ -479,11 +508,12 @@ def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, img_bk_dxdy = self.mask_dxdy(img_bk_dxdy, reg_mask) if self.reg_obj.from_rigid_reg: - img_bk_dxdy = warp_tools.remove_invasive_displacements(img_bk_dxdy, - M=M, - src_shape_rc=unwarped_shape, - out_shape_rc=og_reg_shape_rc - ) + img_bk_dxdy = warp_tools.remove_invasive_displacements( + img_bk_dxdy, + M=M, + src_shape_rc=unwarped_shape, + out_shape_rc=og_reg_shape_rc, + ) self.bk_dxdy = img_bk_dxdy if hasattr(non_rigid_reg_obj, "fwd_dxdy"): # Already calculated @@ -495,9 +525,9 @@ def calc_deformation(self, registered_fixed_image, non_rigid_reg_obj, # If dxdy is a pyvips.Image, it's likely the displacement is too large to draw self.warped_grid = viz.color_displacement_grid(*self.bk_dxdy) - self.registered_img = warp_tools.warp_img(self.image, - bk_dxdy=self.bk_dxdy, - out_shape_rc=self.shape) + self.registered_img = warp_tools.warp_img( + self.image, bk_dxdy=self.bk_dxdy, out_shape_rc=self.shape + ) return bk_dxdy_from_ref @@ -567,8 +597,16 @@ class SerialNonRigidRegistrar(object): """ - def __init__(self, src, reference_img_f=None, moving_to_fixed_xy=None, - mask=None, name=None, align_to_reference=False, compose_transforms=True): + def __init__( + self, + src, + reference_img_f=None, + moving_to_fixed_xy=None, + mask=None, + name=None, + align_to_reference=False, + compose_transforms=True, + ): """ Parameters ---------- @@ -645,7 +683,9 @@ def __init__(self, src, reference_img_f=None, moving_to_fixed_xy=None, elif isinstance(src, dict): self.from_rigid_reg = False else: - valtils.print_warning(f"src must be either a SerialRigidRegistrar, string, or dictionary") + logger.warning( + f"src must be either a SerialRigidRegistrar, string, or dictionary" + ) return None self.name = name @@ -667,12 +707,13 @@ def __init__(self, src, reference_img_f=None, moving_to_fixed_xy=None, if self.align_to_reference is False and reference_img_f is not None: og_ref_name = valtils.get_name(reference_img_f) - msg = (f"The reference was specified as {og_ref_name} ", - f"but `align_to_reference` is `False`, and so images will be aligned serially. ", - f"If you would like all images to be directly aligned to {og_ref_name}, " - f"then set `align_to_reference` to `True`") - valtils.print_warning(msg) - + msg = ( + f"The reference was specified as {og_ref_name} ", + f"but `align_to_reference` is `False`, and so images will be aligned serially. ", + f"If you would like all images to be directly aligned to {og_ref_name}, " + f"then set `align_to_reference` to `True`", + ) + logger.warning(msg) def get_shape(self, img): if isinstance(img, pyvips.Image): @@ -687,19 +728,18 @@ def create_mask(self): for nr_img_obj in self.non_rigid_obj_list: temp_mask[nr_img_obj.image > 0] = 255 - mask = warp_tools.bbox2mask(*warp_tools.xy2bbox( - warp_tools.mask2xy(temp_mask)), - temp_mask.shape) + mask = warp_tools.bbox2mask( + *warp_tools.xy2bbox(warp_tools.mask2xy(temp_mask)), temp_mask.shape + ) return mask def set_mask(self, mask): - """Set mask and get its bounding box - """ + """Set mask and get its bounding box""" if mask is not None: if isinstance(mask, bool) and self.from_rigid_reg: mask = self.src.overlap_mask - mask = np.clip(mask.astype(int)*255, 0, 255).astype(np.uint8) + mask = np.clip(mask.astype(int) * 255, 0, 255).astype(np.uint8) else: mask = self.create_mask() @@ -708,23 +748,22 @@ def set_mask(self, mask): self.mask = mask self.mask_bbox_xywh = mask_bbox_xywh - def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=None): - """Create non_rigid_obj_list - - """ + def generate_non_rigid_obj_list( + self, reference_img_f=None, moving_to_fixed_xy=None + ): + """Create non_rigid_obj_list""" if self.from_rigid_reg: - img_list, img_f_list, img_names, mask_list = \ - get_imgs_rigid_reg(self.src) + img_list, img_f_list, img_names, mask_list = get_imgs_rigid_reg(self.src) else: if isinstance(self.src, str): - img_list, img_f_list, img_names, mask_list = \ - get_imgs_from_dir(self.src) + img_list, img_f_list, img_names, mask_list = get_imgs_from_dir(self.src) # overwrite `src` because all info now in NonRigidZImages self.src = "dictionary" elif isinstance(self.src, dict): - img_list, img_f_list, img_names, mask_list = \ - get_imgs_from_dict(self.src) + img_list, img_f_list, img_names, mask_list = get_imgs_from_dict( + self.src + ) self.size = len(img_list) self.shape = self.get_shape(img_list[0]) @@ -744,8 +783,9 @@ def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=N if self.from_rigid_reg and isinstance(moving_to_fixed_xy, bool): if moving_to_fixed_xy: - moving_to_fixed_xy = \ - get_matching_xy_from_rigid_registrar(self.src, reference_name) + moving_to_fixed_xy = get_matching_xy_from_rigid_registrar( + self.src, reference_name + ) else: moving_to_fixed_xy = None @@ -753,8 +793,9 @@ def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=N for i, img in enumerate(img_list): img_shape = self.get_shape(img) - assert np.all(img_shape == self.shape), \ - valtils.print_warning("Images must all have the shape") + assert np.all(img_shape == self.shape), logger.warning( + "Images must all have the shape" + ) img_name = img_names[i] mask = mask_list[i] @@ -768,12 +809,17 @@ def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=N fixed_xy = xy_coords[1] else: msg = "moving_to_fixed_xy is not a dictionary. Will be ignored" - valtils.print_warning(msg) - - nr_obj = NonRigidZImage(self, img, img_name, stack_idx=i, - moving_xy=moving_xy, - fixed_xy=fixed_xy, - mask=mask) + logger.warning(msg) + + nr_obj = NonRigidZImage( + self, + img, + img_name, + stack_idx=i, + moving_xy=moving_xy, + fixed_xy=fixed_xy, + mask=mask, + ) if i == ref_img_idx: # Set reference image attributes # @@ -783,14 +829,26 @@ def generate_non_rigid_obj_list(self, reference_img_f=None, moving_to_fixed_xy=N nr_obj.fwd_dxdy = [zero_displacement, zero_displacement] nr_obj.warped_grid = viz.color_displacement_grid(*nr_obj.bk_dxdy) else: - nr_obj.bk_dxdy = pyvips.Image.black(nr_obj.shape[1], nr_obj.shape[0], bands=2) - nr_obj.fwd_dxdy = pyvips.Image.black(nr_obj.shape[1], nr_obj.shape[0], bands=2) + nr_obj.bk_dxdy = pyvips.Image.black( + nr_obj.shape[1], nr_obj.shape[0], bands=2 + ) + nr_obj.fwd_dxdy = pyvips.Image.black( + nr_obj.shape[1], nr_obj.shape[0], bands=2 + ) nr_obj.registered_img = img.copy() self.non_rigid_obj_list[i] = nr_obj - def update_img_params(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_params=None, moving_name=None, fixed_name=None, is_tiler=False): + def update_img_params( + self, + non_rigid_reg_obj, + non_rigid_reg_params=None, + img_params=None, + moving_name=None, + fixed_name=None, + is_tiler=False, + ): """ Update img params for non-rigid-registration """ @@ -805,18 +863,32 @@ def update_img_params(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_pa indv_img_params = img_params if is_tiler: - #Tiler needs processor arguments for moving and fixed images - assert moving_name in img_params and fixed_name in img_params, "Tiled registration requires image processors for each image" + # Tiler needs processor arguments for moving and fixed images + assert ( + moving_name in img_params and fixed_name in img_params + ), "Tiled registration requires image processors for each image" moving_dict = img_params[moving_name] - indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_KEY] = moving_dict[non_rigid_registrars.NR_PROCESSING_CLASS_KEY] - indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_INIT_KW_KEY] = moving_dict[non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY] - indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_KW_KEY] = moving_dict[non_rigid_registrars.NR_PROCESSING_KW_KEY] + indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_KEY] = moving_dict[ + non_rigid_registrars.NR_PROCESSING_CLASS_KEY + ] + indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_INIT_KW_KEY] = ( + moving_dict[non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY] + ) + indv_img_params[non_rigid_registrars.NR_TILE_MOVING_P_KW_KEY] = moving_dict[ + non_rigid_registrars.NR_PROCESSING_KW_KEY + ] fixed_dict = img_params[fixed_name] - indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_KEY] = fixed_dict[non_rigid_registrars.NR_PROCESSING_CLASS_KEY] - indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_INIT_KW_KEY] = fixed_dict[non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY] - indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_KW_KEY] = fixed_dict[non_rigid_registrars.NR_PROCESSING_KW_KEY] + indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_KEY] = fixed_dict[ + non_rigid_registrars.NR_PROCESSING_CLASS_KEY + ] + indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_INIT_KW_KEY] = ( + fixed_dict[non_rigid_registrars.NR_PROCESSING_INIT_KW_KEY] + ) + indv_img_params[non_rigid_registrars.NR_TILE_FIXED_P_KW_KEY] = fixed_dict[ + non_rigid_registrars.NR_PROCESSING_KW_KEY + ] if non_rigid_reg_params is not None and indv_img_params is not None: @@ -839,8 +911,9 @@ def update_img_params(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_pa return updated_params - - def register_serial(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_params=None): + def register_serial( + self, non_rigid_reg_obj, non_rigid_reg_params=None, img_params=None + ): """Non-rigidly align images in serial Parameters ---------- @@ -860,8 +933,14 @@ def register_serial(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_para self.non_rigid_reg_params = non_rigid_reg_params iter_order = warp_tools.get_alignment_indices(self.size, self.ref_img_idx) - is_tiler = non_rigid_reg_obj.__class__.__name__ == non_rigid_registrars.NonRigidTileRegistrar.__name__ - for moving_idx, fixed_idx in tqdm(iter_order, desc="Finding non-rigid transforms", unit="image"): + is_tiler = ( + non_rigid_reg_obj.__class__.__name__ + == non_rigid_registrars.NonRigidTileRegistrar.__name__ + ) + updated_dxdy = None + for moving_idx, fixed_idx in tqdm( + iter_order, desc="Finding non-rigid transforms", unit="image" + ): moving_obj = self.non_rigid_obj_list[moving_idx] fixed_obj = self.non_rigid_obj_list[fixed_idx] @@ -873,7 +952,9 @@ def register_serial(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_para if moving_obj.mask is not None: if self.mask is not None: - reg_mask = preprocessing.combine_masks(self.mask, moving_obj.mask, op="and") + reg_mask = preprocessing.combine_masks( + self.mask, moving_obj.mask, op="and" + ) else: reg_mask = moving_obj.mask @@ -882,16 +963,25 @@ def register_serial(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_para else: reg_mask is None - nr_reg_params = self.update_img_params(non_rigid_reg_obj, non_rigid_reg_params, img_params, moving_name=moving_obj.name, fixed_name=fixed_obj.name, is_tiler=is_tiler) - updated_dxdy = moving_obj.calc_deformation(registered_fixed_image=fixed_obj.registered_img, - non_rigid_reg_obj=non_rigid_reg_obj, - bk_dxdy=current_dxdy, - params=nr_reg_params, - mask=reg_mask - ) - - - def register_to_ref(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_params=None): + nr_reg_params = self.update_img_params( + non_rigid_reg_obj, + non_rigid_reg_params, + img_params, + moving_name=moving_obj.name, + fixed_name=fixed_obj.name, + is_tiler=is_tiler, + ) + updated_dxdy = moving_obj.calc_deformation( + registered_fixed_image=fixed_obj.registered_img, + non_rigid_reg_obj=non_rigid_reg_obj, + bk_dxdy=current_dxdy, + params=nr_reg_params, + mask=reg_mask, + ) + + def register_to_ref( + self, non_rigid_reg_obj, non_rigid_reg_params=None, img_params=None + ): """Non-rigidly align images to a reference image Parameters ---------- @@ -910,20 +1000,31 @@ def register_to_ref(self, non_rigid_reg_obj, non_rigid_reg_params=None, img_para self.non_rigid_reg_params = non_rigid_reg_params ref_nr_obj = self.non_rigid_obj_list[self.ref_img_idx] ref_img = ref_nr_obj.image - is_tiler = non_rigid_reg_obj.__class__.__name__ == non_rigid_registrars.NonRigidTileRegistrar.__name__ - for moving_idx in tqdm(range(self.size), desc="Finding non-rigid transforms", unit="image"): + is_tiler = ( + non_rigid_reg_obj.__class__.__name__ + == non_rigid_registrars.NonRigidTileRegistrar.__name__ + ) + for moving_idx in tqdm( + range(self.size), desc="Finding non-rigid transforms", unit="image" + ): moving_obj = self.non_rigid_obj_list[moving_idx] if moving_obj.stack_idx == self.ref_img_idx: continue overlap_mask = None - nr_reg_params = self.update_img_params(non_rigid_reg_obj, non_rigid_reg_params, img_params, moving_name=moving_obj.name, fixed_name=ref_nr_obj.name, is_tiler=is_tiler) + nr_reg_params = self.update_img_params( + non_rigid_reg_obj, + non_rigid_reg_params, + img_params, + moving_name=moving_obj.name, + fixed_name=ref_nr_obj.name, + is_tiler=is_tiler, + ) - moving_obj.calc_deformation(ref_img, - non_rigid_reg_obj, - params=nr_reg_params, - mask=overlap_mask) + moving_obj.calc_deformation( + ref_img, non_rigid_reg_obj, params=nr_reg_params, mask=overlap_mask + ) def register_groupwise(self, non_rigid_reg_class, non_rigid_reg_params=None): """Non-rigidly align images as a group @@ -946,10 +1047,16 @@ def register_groupwise(self, non_rigid_reg_class, non_rigid_reg_params=None): img_list = [nr_img_obj.image for nr_img_obj in self.non_rigid_obj_list] non_rigid_reg = non_rigid_reg_class(params=non_rigid_reg_params) - print("\n======== Registering images (non-rigid)\n") - warped_imgs, warped_grids, backward_deformations = non_rigid_reg.register(img_list, self.mask) + logger.info("Registering images (non-rigid)") + warped_imgs, warped_grids, backward_deformations = non_rigid_reg.register( + img_list, self.mask + ) - for i, nr_img_obj in tqdm(enumerate(self.non_rigid_obj_list), desc="Aligning images", unit="annotation"): + for i, nr_img_obj in tqdm( + enumerate(self.non_rigid_obj_list), + desc="Aligning images", + unit="annotation", + ): nr_img_obj.registered_img = warped_imgs[i] nr_img_obj.bk_dxdy = backward_deformations[i] nr_img_obj.warped_grid = viz.color_displacement_grid(*nr_img_obj.bk_dxdy) @@ -984,7 +1091,7 @@ def register(self, non_rigid_reg_class, non_rigid_reg_params, img_params=None): """ if img_params is not None: - named_img_params = {valtils.get_name(k):v for k, v in img_params.items()} + named_img_params = {valtils.get_name(k): v for k, v in img_params.items()} else: named_img_params = None @@ -997,15 +1104,22 @@ def register(self, non_rigid_reg_class, non_rigid_reg_params, img_params=None): else: non_rigid_reg_obj = non_rigid_reg_class - if issubclass(non_rigid_reg_obj.__class__, non_rigid_registrars.NonRigidRegistrarGroupwise): + if issubclass( + non_rigid_reg_obj.__class__, non_rigid_registrars.NonRigidRegistrarGroupwise + ): self.register_groupwise(non_rigid_reg_obj, non_rigid_reg_params) elif self.align_to_reference: - self.register_to_ref(non_rigid_reg_obj, non_rigid_reg_params, img_params=named_img_params) + self.register_to_ref( + non_rigid_reg_obj, non_rigid_reg_params, img_params=named_img_params + ) else: - self.register_serial(non_rigid_reg_obj, non_rigid_reg_params, img_params=named_img_params) + self.register_serial( + non_rigid_reg_obj, non_rigid_reg_params, img_params=named_img_params + ) - self.non_rigid_obj_dict = {img_obj.name: img_obj for img_obj - in self.non_rigid_obj_list} + self.non_rigid_obj_dict = { + img_obj.name: img_obj for img_obj in self.non_rigid_obj_list + } def summarize(self): """Summarize alignment error @@ -1028,10 +1142,12 @@ def summarize(self): tre_list = [None] * self.size src_img_names[self.ref_img_idx] = self.ref_img_name - shape_list[self.ref_img_idx] = self.non_rigid_obj_list[self.ref_img_idx].image.shape + shape_list[self.ref_img_idx] = self.non_rigid_obj_list[ + self.ref_img_idx + ].image.shape iter_order = warp_tools.get_alignment_indices(self.size, self.ref_img_idx) - print("\n======== Summarizing registration\n") + logger.info("Summarizing registration") for moving_idx, fixed_idx in tqdm(iter_order): moving_obj = self.non_rigid_obj_list[moving_idx] fixed_obj = self.non_rigid_obj_list[fixed_idx] @@ -1039,36 +1155,42 @@ def summarize(self): dst_img_names[moving_idx] = fixed_obj.name shape_list[moving_idx] = moving_obj.image.shape - og_tre_list[moving_idx], og_med_d_list[moving_idx] = \ - warp_tools.measure_error(moving_obj.moving_xy, - moving_obj.fixed_xy, - moving_obj.image.shape) - - warped_moving_xy = warp_tools.warp_xy(moving_obj.moving_xy, - M=None, - fwd_dxdy=moving_obj.fwd_dxdy) - - warped_fixed_xy = warp_tools.warp_xy(moving_obj.fixed_xy, - M=None, - fwd_dxdy=moving_obj.fwd_dxdy) - - tre_list[moving_idx], med_d_list[moving_idx] = \ - warp_tools.measure_error(warped_moving_xy, - warped_fixed_xy, - moving_obj.image.shape) - - summary_df = pd.DataFrame({ - "from": src_img_names, - "to": dst_img_names, - "original_D": og_med_d_list, - "D": med_d_list, - "original_TRE": og_tre_list, - "TRE": tre_list, - "shape": shape_list, - }) + og_tre_list[moving_idx], og_med_d_list[moving_idx] = ( + warp_tools.measure_error( + moving_obj.moving_xy, moving_obj.fixed_xy, moving_obj.image.shape + ) + ) + + warped_moving_xy = warp_tools.warp_xy( + moving_obj.moving_xy, M=None, fwd_dxdy=moving_obj.fwd_dxdy + ) + + warped_fixed_xy = warp_tools.warp_xy( + moving_obj.fixed_xy, M=None, fwd_dxdy=moving_obj.fwd_dxdy + ) + + tre_list[moving_idx], med_d_list[moving_idx] = warp_tools.measure_error( + warped_moving_xy, warped_fixed_xy, moving_obj.image.shape + ) + + summary_df = pd.DataFrame( + { + "from": src_img_names, + "to": dst_img_names, + "original_D": og_med_d_list, + "D": med_d_list, + "original_TRE": og_tre_list, + "TRE": tre_list, + "shape": shape_list, + } + ) to_summarize_idx = [i for i in range(self.size) if i != self.ref_img_idx] - summary_df["series_d"] = warp_tools.calc_total_error(np.array(med_d_list)[to_summarize_idx]) - summary_df["series_tre"] = warp_tools.calc_total_error(np.array(tre_list)[to_summarize_idx]) + summary_df["series_d"] = warp_tools.calc_total_error( + np.array(med_d_list)[to_summarize_idx] + ) + summary_df["series_tre"] = warp_tools.calc_total_error( + np.array(tre_list)[to_summarize_idx] + ) summary_df["name"] = self.name self.summary_df = summary_df @@ -1076,11 +1198,20 @@ def summarize(self): return summary_df -def register_images(src, non_rigid_reg_class=non_rigid_registrars.OpticalFlowWarper(), - non_rigid_reg_params=None, dst_dir=None, - reference_img_f=None, moving_to_fixed_xy=None, - mask=None, name=None, align_to_reference=False, - img_params=None, compose_transforms=True, qt_emitter=None): +def register_images( + src, + non_rigid_reg_class=non_rigid_registrars.OpticalFlowWarper(), + non_rigid_reg_params=None, + dst_dir=None, + reference_img_f=None, + moving_to_fixed_xy=None, + mask=None, + name=None, + align_to_reference=False, + img_params=None, + compose_transforms=True, + qt_emitter=None, +): """ Parameters ---------- @@ -1165,11 +1296,15 @@ def register_images(src, non_rigid_reg_class=non_rigid_registrars.OpticalFlowWar """ tic = time() - nr_reg = SerialNonRigidRegistrar(src=src, reference_img_f=reference_img_f, - moving_to_fixed_xy=moving_to_fixed_xy, - mask=mask, name=name, - align_to_reference=align_to_reference, - compose_transforms=compose_transforms) + nr_reg = SerialNonRigidRegistrar( + src=src, + reference_img_f=reference_img_f, + moving_to_fixed_xy=moving_to_fixed_xy, + mask=mask, + name=name, + align_to_reference=align_to_reference, + compose_transforms=compose_transforms, + ) nr_reg.register(non_rigid_reg_class, non_rigid_reg_params, img_params=img_params) @@ -1180,29 +1315,34 @@ def register_images(src, non_rigid_reg_class=non_rigid_registrars.OpticalFlowWar for d in [registered_img_dir, registered_data_dir, registered_grids_dir]: pathlib.Path(d).mkdir(exist_ok=True, parents=True) - print("\n======== Saving results\n") + logger.info("Saving results") if moving_to_fixed_xy is not None: summary_df = nr_reg.summarize() summary_file = os.path.join(registered_data_dir, name + "_results.csv") summary_df.to_csv(summary_file, index=False) - pickle_file = os.path.join(registered_data_dir, name + "_non_rigid_registrar.pickle") - pickle.dump(nr_reg, open(pickle_file, 'wb')) + pickle_file = os.path.join( + registered_data_dir, name + "_non_rigid_registrar.pickle" + ) + pickle.dump(nr_reg, open(pickle_file, "wb")) for img_obj in nr_reg.non_rigid_obj_list: f_out = f"{img_obj.name}.png" - io.imsave(os.path.join(registered_img_dir, f_out), - img_obj.registered_img.astype(np.uint8)) + io.imsave( + os.path.join(registered_img_dir, f_out), + img_obj.registered_img.astype(np.uint8), + ) - colord_tri_grid = viz.color_displacement_tri_grid(img_obj.bk_dxdy[0], - img_obj.bk_dxdy[1]) + colord_tri_grid = viz.color_displacement_tri_grid( + img_obj.bk_dxdy[0], img_obj.bk_dxdy[1] + ) io.imsave(os.path.join(registered_grids_dir, f_out), colord_tri_grid) toc = time() elapsed = toc - tic time_string, time_units = valtils.get_elapsed_time_string(elapsed) - print(f"\n======== Non-rigid registration complete in {time_string} {time_units}\n") + logger.info(f"Non-rigid registration complete in {time_string} {time_units}") return nr_reg diff --git a/valis/serial_rigid.py b/src/valis/serial_rigid.py similarity index 67% rename from valis/serial_rigid.py rename to src/valis/serial_rigid.py index 3ecd4f2d..ecc3a965 100644 --- a/valis/serial_rigid.py +++ b/src/valis/serial_rigid.py @@ -1,6 +1,6 @@ -"""Classes and functions to perform serial rigid registration of a set of images -""" +"""Classes and functions to perform serial rigid registration of a set of images""" +import logging import torch import kornia import numpy as np @@ -29,7 +29,18 @@ from .feature_detectors import VggFD from .feature_matcher import Matcher, convert_distance_to_similarity, GMS_NAME from . import feature_matcher -from . import valtils + +logger = logging.getLogger(__name__) + + +class TooFewMatchesError(RuntimeError): + """Raised when an initial-pass keypoint match count is below the + configured ``min_rigid_matches`` threshold. Continuing past this + point typically produces an unusable registration: with too few + correspondences, the rematch step fits a degenerate + SimilarityTransform whose extreme scale can also blow up the + warped-image canvas and OOM the subsequent feature detection. + """ DENOISE_MSG = "Denoising images" @@ -40,8 +51,23 @@ OPTIMIZING_MSG = "Optimizing transforms" FINALIZING_MSG = "Finalizing" -msg_list = [DENOISE_MSG, FEATURE_MSG, MATCHING_MSG, TRANSFORM_MSG, FINALIZING_MSG, OPTIMIZING_MSG] -DENOISE_MSG, FEATURE_MSG, MATCHING_MSG, TRANSFORM_MSG, FINALIZING_MSG, OPTIMIZING_MSG = valtils.pad_strings(msg_list) +msg_list = [ + DENOISE_MSG, + FEATURE_MSG, + MATCHING_MSG, + TRANSFORM_MSG, + FINALIZING_MSG, + OPTIMIZING_MSG, +] +( + DENOISE_MSG, + FEATURE_MSG, + MATCHING_MSG, + TRANSFORM_MSG, + FINALIZING_MSG, + OPTIMIZING_MSG, +) = valtils.pad_strings(msg_list) + def get_image_files(img_dir, imgs_ordered=False): """Get images filenames in img_dir @@ -68,8 +94,11 @@ def get_image_files(img_dir, imgs_ordered=False): """ - img_list = [f for f in os.listdir(img_dir) if - slide_tools.get_img_type(os.path.join(img_dir, f)) is not None] + img_list = [ + f + for f in os.listdir(img_dir) + if slide_tools.get_img_type(os.path.join(img_dir, f)) is not None + ] if imgs_ordered: valtils.sort_nicely(img_list) @@ -102,7 +131,7 @@ def get_max_image_dimensions(img_list): def order_Dmat(D): - """ Cluster distance matrix and sort + """Cluster distance matrix and sort Leaf sorting is accomplished using optimal leaf ordering (Bar-Joseph 2001) @@ -127,7 +156,7 @@ def order_Dmat(D): D = D.copy() sq_D = squareform(D) - Z = linkage(sq_D, 'single', preserve_input=True) + Z = linkage(sq_D, "single", preserve_input=True) optimal_Z = optimal_leaf_ordering(Z, sq_D) ordered_leaves = leaves_list(optimal_Z) @@ -410,8 +439,14 @@ class SerialRigidRegistrar(object): """ - def __init__(self, img_dir, imgs_ordered=False, reference_img_f=None, - name=None, align_to_reference=False): + def __init__( + self, + img_dir, + imgs_ordered=False, + reference_img_f=None, + name=None, + align_to_reference=False, + ): """Class that performs serial rigid registration Parameters @@ -470,11 +505,13 @@ def __init__(self, img_dir, imgs_ordered=False, reference_img_f=None, if self.align_to_reference is False and reference_img_f is not None: og_ref_name = valtils.get_name(reference_img_f) - msg = (f"The reference was specified as {og_ref_name} ", - f"but `align_to_reference` is `False`, and so images will be aligned serially *towards* the reference image. ", - f"If you would like all images to be *directly* aligned to {og_ref_name}, " - f"then set `align_to_reference` to `True`. Note that in both cases, {og_ref_name} will remain unwarped.") - valtils.print_warning(msg) + msg = ( + f"The reference was specified as {og_ref_name} ", + f"but `align_to_reference` is `False`, and so images will be aligned serially *towards* the reference image. ", + f"If you would like all images to be *directly* aligned to {og_ref_name}, " + f"then set `align_to_reference` to `True`. Note that in both cases, {og_ref_name} will remain unwarped.", + ) + logger.warning(msg) def generate_img_obj_list(self, feature_detector, valis_obj=None, qt_emitter=None): """Create a list of ZImage objects @@ -495,15 +532,16 @@ def generate_img_obj_list(self, feature_detector, valis_obj=None, qt_emitter=Non """ # NOTE tried parallelizing, but it's actually slower # - sorted_img_list = [io.imread(os.path.join(self.img_dir, f), True) - for f in self.img_file_list] + sorted_img_list = [ + io.imread(os.path.join(self.img_dir, f), True) for f in self.img_file_list + ] out_w, out_h = get_max_image_dimensions(sorted_img_list) # Get dimensions if images were rotated 45 degrees rad_45 = np.deg2rad(45) - max_new_w = out_w*np.cos(rad_45) + out_h*np.sin(rad_45) - max_new_h = out_w*np.sin(rad_45) + out_h*np.cos(rad_45) + max_new_w = out_w * np.cos(rad_45) + out_h * np.sin(rad_45) + max_new_h = out_w * np.sin(rad_45) + out_h * np.cos(rad_45) max_dist = np.ceil(np.max([out_w, out_h, max_new_h, max_new_w])).astype(int) out_shape = (max_dist, max_dist) @@ -516,11 +554,17 @@ def generate_img_obj_list(self, feature_detector, valis_obj=None, qt_emitter=Non img_name = valtils.get_name(img_f) img_obj = ZImage(img, os.path.join(self.img_dir, img_f), i, name=img_name) img_obj.padded_shape_rc = out_shape - img_obj.T = warp_tools.get_padding_matrix(img.shape, img_obj.padded_shape_rc) + img_obj.T = warp_tools.get_padding_matrix( + img.shape, img_obj.padded_shape_rc + ) if feature_detector is not None: - detect_img = self.get_fd_detection_img(img_obj, feature_detector=feature_detector, valis_obj=valis_obj) - img_obj.kp_pos_xy, img_obj.desc = feature_detector.detect_and_compute(detect_img) + detect_img = self.get_fd_detection_img( + img_obj, feature_detector=feature_detector, valis_obj=valis_obj + ) + img_obj.kp_pos_xy, img_obj.desc = feature_detector.detect_and_compute( + detect_img + ) img_obj_list[i] = img_obj self.img_obj_dict[img_name] = img_obj @@ -530,7 +574,15 @@ def generate_img_obj_list(self, feature_detector, valis_obj=None, qt_emitter=Non self.img_obj_list = img_obj_list self.features = feature_detector.__class__.__name__ - def match_sorted_imgs(self, matcher_obj, keep_unfiltered=False, valis_obj=None, use_images=False, parallel=True, qt_emitter=None): + def match_sorted_imgs( + self, + matcher_obj, + keep_unfiltered=False, + valis_obj=None, + use_images=False, + parallel=True, + qt_emitter=None, + ): """Conduct feature matching between images that have already been sorted. Results will be stored in each ZImage's match_dict @@ -551,31 +603,69 @@ def match_sorted_imgs(self, matcher_obj, keep_unfiltered=False, valis_obj=None, for i in range(self.size): img_obj = self.img_obj_list[i] if i > 0: - prev_img_obj = self.img_obj_list[i-1] - self.match_img_obj_pairs(img_obj, prev_img_obj, matcher_obj, keep_unfiltered=keep_unfiltered, valis_obj=valis_obj) + prev_img_obj = self.img_obj_list[i - 1] + self.match_img_obj_pairs( + img_obj, + prev_img_obj, + matcher_obj, + keep_unfiltered=keep_unfiltered, + valis_obj=valis_obj, + ) if i < self.size - 1: - next_img_obj = self.img_obj_list[i+1] - self.match_img_obj_pairs(img_obj, next_img_obj, matcher_obj, keep_unfiltered=keep_unfiltered, valis_obj=valis_obj) - - def match_img_obj_pairs(self, img_obj_1, img_obj_2, matcher_obj, valis_obj=None, keep_unfiltered=False, qt_emitter=None): + next_img_obj = self.img_obj_list[i + 1] + self.match_img_obj_pairs( + img_obj, + next_img_obj, + matcher_obj, + keep_unfiltered=keep_unfiltered, + valis_obj=valis_obj, + ) + + def match_img_obj_pairs( + self, + img_obj_1, + img_obj_2, + matcher_obj, + valis_obj=None, + keep_unfiltered=False, + qt_emitter=None, + ): if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":img_obj_1.image.shape[0:2], "img2_shape": img_obj_2.image.shape[0:2]} + filter_kwargs = { + "img1_shape": img_obj_1.image.shape[0:2], + "img2_shape": img_obj_2.image.shape[0:2], + } else: filter_kwargs = {} - img1 = self.get_fd_detection_img(img_obj_1, matcher_obj.feature_detector, valis_obj) - img2 = self.get_fd_detection_img(img_obj_2, matcher_obj.feature_detector, valis_obj) - - unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=img1, desc1=img_obj_1.desc, kp1_xy=img_obj_1.kp_pos_xy, - img2=img2, desc2=img_obj_2.desc, kp2_xy=img_obj_2.kp_pos_xy, - additional_filtering_kwargs=filter_kwargs, - sorting_images=True) - + img1 = self.get_fd_detection_img( + img_obj_1, matcher_obj.feature_detector, valis_obj + ) + img2 = self.get_fd_detection_img( + img_obj_2, matcher_obj.feature_detector, valis_obj + ) + + ( + unfiltered_match_info12, + filtered_match_info12, + unfiltered_match_info21, + filtered_match_info21, + ) = matcher_obj.match_images( + img1=img1, + desc1=img_obj_1.desc, + kp1_xy=img_obj_1.kp_pos_xy, + img2=img2, + desc2=img_obj_2.desc, + kp2_xy=img_obj_2.kp_pos_xy, + additional_filtering_kwargs=filter_kwargs, + sorting_images=True, + ) if len(filtered_match_info12.matched_kp1_xy) == 0: - warnings.warn(f"{len(filtered_match_info12.matched_kp1_xy)} between {img_obj_1.name} and {img_obj_2.name}") + warnings.warn( + f"{len(filtered_match_info12.matched_kp1_xy)} between {img_obj_1.name} and {img_obj_2.name}" + ) # Update match dictionaries # if keep_unfiltered: unfiltered_match_info12.set_names(img_obj_1.name, img_obj_2.name) @@ -593,7 +683,9 @@ def match_img_obj_pairs(self, img_obj_1, img_obj_2, matcher_obj, valis_obj=None, if qt_emitter is not None: qt_emitter.emit(1) - def match_imgs(self, matcher_obj, keep_unfiltered=False, valis_obj=None, qt_emitter=None): + def match_imgs( + self, matcher_obj, keep_unfiltered=False, valis_obj=None, qt_emitter=None + ): """Conduct feature matching between all pairs of images. Results will be stored in each ZImage's match_dict @@ -613,31 +705,51 @@ def match_imgs(self, matcher_obj, keep_unfiltered=False, valis_obj=None, qt_emit def match_img_obj(i, matcher_obj, keep_unfiltered, valis_obj): img_obj_1 = self.img_obj_list[i] - for j in np.arange(i+1, self.size): + for j in np.arange(i + 1, self.size): img_obj_2 = self.img_obj_list[j] - self.match_img_obj_pairs(img_obj_1, img_obj_2, matcher_obj, keep_unfiltered=keep_unfiltered, valis_obj=valis_obj) - - match_adj_fxn = functools.partial(match_img_obj, matcher_obj=matcher_obj, keep_unfiltered=keep_unfiltered, valis_obj=valis_obj) - - n_cpu = valtils.get_ncpus_available() - 1 - res = pqdm(range(self.size), match_adj_fxn, n_jobs=n_cpu, desc=MATCHING_MSG, unit="image", leave=None) + self.match_img_obj_pairs( + img_obj_1, + img_obj_2, + matcher_obj, + keep_unfiltered=keep_unfiltered, + valis_obj=valis_obj, + ) + + match_adj_fxn = functools.partial( + match_img_obj, + matcher_obj=matcher_obj, + keep_unfiltered=keep_unfiltered, + valis_obj=valis_obj, + ) + + n_cpu = valtils.get_ncpus_available() + res = pqdm( + range(self.size), + match_adj_fxn, + n_jobs=n_cpu, + desc=MATCHING_MSG, + unit="image", + leave=None, + ) def get_common_desc(self, current_img_obj, neighbor_obj, nf_kp_idx): """Get descriptors that correspond to filtered neighbor points - Parameters - ---------- - nf_kp_idx : ndarray - Indicies of already matched keypoints that were found after - neighbonr filtering + Parameters + ---------- + nf_kp_idx : ndarray + Indicies of already matched keypoints that were found after + neighbonr filtering """ neighbor_match_info12 = current_img_obj.match_dict[neighbor_obj] nf_kp = neighbor_match_info12.matched_kp1_xy[nf_kp_idx] nf_desc = neighbor_match_info12.matched_desc1[nf_kp_idx] - return nf_desc, nf_kp + return nf_desc, nf_kp - def get_neighbor_matches_idx(self, img_obj, prev_img_obj, next_img_obj, window_size=100): + def get_neighbor_matches_idx( + self, img_obj, prev_img_obj, next_img_obj, window_size=100 + ): """Get indices of features found in both neighbors Returns @@ -654,17 +766,24 @@ def get_neighbor_matches_idx(self, img_obj, prev_img_obj, next_img_obj, window_s xy_to_prev = img_obj.match_dict[prev_img_obj].matched_kp1_xy xy_to_next = next_img_obj.match_dict[img_obj].matched_kp2_xy - to_prev_density, _, _, xy_to_prev_idx = stats.binned_statistic_2d(xy_to_prev[:, 0], xy_to_prev[:, 1], None, "count", bins=[row_bins, col_bins]) - to_next_density, _, _, xy_to_next_idx = stats.binned_statistic_2d(xy_to_next[:, 0], xy_to_next[:, 1], None, "count", bins=[row_bins, col_bins]) + to_prev_density, _, _, xy_to_prev_idx = stats.binned_statistic_2d( + xy_to_prev[:, 0], xy_to_prev[:, 1], None, "count", bins=[row_bins, col_bins] + ) + to_next_density, _, _, xy_to_next_idx = stats.binned_statistic_2d( + xy_to_next[:, 0], xy_to_next[:, 1], None, "count", bins=[row_bins, col_bins] + ) - shared_pts, _, _ = np.intersect1d(xy_to_prev_idx, xy_to_next_idx, return_indices=True, assume_unique=False) + shared_pts, _, _ = np.intersect1d( + xy_to_prev_idx, xy_to_next_idx, return_indices=True, assume_unique=False + ) nf_next_idx = np.where(np.isin(xy_to_next_idx, shared_pts))[0] nf_prev_idx = np.where(np.isin(xy_to_prev_idx, shared_pts))[0] - return nf_prev_idx, nf_next_idx - def neighbor_match_filtering(self, img_obj, prev_img_obj, next_img_obj, tform, window_size=50): + def neighbor_match_filtering( + self, img_obj, prev_img_obj, next_img_obj, tform, window_size=50 + ): """Remove poor matches by keeping only the matches found in neighbors Parameters @@ -701,17 +820,18 @@ def neighbor_match_filtering(self, img_obj, prev_img_obj, next_img_obj, tform, w """ def measure_d(src_xy, dst_xy, tform, M=None): - """Measure distance between warped corresponding points - """ + """Measure distance between warped corresponding points""" if M is None: tform.estimate(src=dst_xy, dst=src_xy) M = tform.params warped_xy = warp_tools.warp_xy(src_xy, M) - d = np.median(warp_tools.calc_d(warped_xy, dst_xy)) + d = np.median(warp_tools.calc_d(warped_xy, dst_xy)) return d, M - nf_prev_idx, nf_next_idx = self.get_neighbor_matches_idx(img_obj, prev_img_obj, next_img_obj, window_size=window_size) + nf_prev_idx, nf_next_idx = self.get_neighbor_matches_idx( + img_obj, prev_img_obj, next_img_obj, window_size=window_size + ) to_prev_match_info12 = img_obj.match_dict[prev_img_obj] to_prev_match_info21 = prev_img_obj.match_dict[img_obj] @@ -722,19 +842,23 @@ def measure_d(src_xy, dst_xy, tform, M=None): nf_moving_kp = to_prev_match_info12.matched_kp1_xy[nf_prev_idx, :] nf_fixed_kp = to_prev_match_info12.matched_kp2_xy[nf_prev_idx, :] - original_with_neighbor_filter_d, _ = measure_d(nf_moving_kp, - nf_fixed_kp, - tform) + original_with_neighbor_filter_d, _ = measure_d( + nf_moving_kp, nf_fixed_kp, tform + ) - original_d, _ = measure_d(to_prev_match_info12.matched_kp1_xy, - to_prev_match_info12.matched_kp2_xy, - tform) + original_d, _ = measure_d( + to_prev_match_info12.matched_kp1_xy, + to_prev_match_info12.matched_kp2_xy, + tform, + ) improved = original_with_neighbor_filter_d <= original_d if improved: - msg = (f"Neighbor match filtering improved alignment, reducing error from {original_d:.4} to {original_with_neighbor_filter_d:.4}, " - f"but also descreased the number of matches from {to_prev_match_info12.n_matches} to {len(nf_prev_idx)}") - valtils.print_warning(msg, None, Fore.GREEN) + msg = ( + f"Neighbor match filtering improved alignment, reducing error from {original_d:.4} to {original_with_neighbor_filter_d:.4}, " + f"but also descreased the number of matches from {to_prev_match_info12.n_matches} to {len(nf_prev_idx)}" + ) + logger.info(msg) # neighbor filtering improved alignment # To update img_obj @@ -754,7 +878,9 @@ def measure_d(src_xy, dst_xy, tform, M=None): else: return improved, to_prev_match_info12, to_prev_match_info21 - def update_match_dicts_with_neighbor_filter(self, tform, matcher_obj, window_size=50): + def update_match_dicts_with_neighbor_filter( + self, tform, matcher_obj, window_size=50 + ): """Remove poor matches by keeping only the matches found in neighbors Parameters @@ -771,7 +897,7 @@ def update_match_dicts_with_neighbor_filter(self, tform, matcher_obj, window_siz for moving_idx, fixed_idx in self.iter_order: img_obj = self.img_obj_list[moving_idx] - next_idx = 1*(moving_idx - fixed_idx) + moving_idx + next_idx = 1 * (moving_idx - fixed_idx) + moving_idx assert abs(next_idx - fixed_idx) == 2 if next_idx < 0 or next_idx >= self.size: @@ -780,12 +906,17 @@ def update_match_dicts_with_neighbor_filter(self, tform, matcher_obj, window_siz prev_img_obj = self.img_obj_list[fixed_idx] next_img_obj = self.img_obj_list[next_idx] - improved, updated_prev_match_info12, updated_prev_match_info21 = \ - self.neighbor_match_filtering(img_obj, prev_img_obj, next_img_obj, tform, - window_size=window_size) + improved, updated_prev_match_info12, updated_prev_match_info21 = ( + self.neighbor_match_filtering( + img_obj, prev_img_obj, next_img_obj, tform, window_size=window_size + ) + ) if improved: - new_matches[img_obj.name] = [updated_prev_match_info12, updated_prev_match_info21] + new_matches[img_obj.name] = [ + updated_prev_match_info12, + updated_prev_match_info21, + ] # Update matches for moving_idx, fixed_idx in self.iter_order: @@ -799,12 +930,15 @@ def update_match_dicts_with_neighbor_filter(self, tform, matcher_obj, window_siz img_obj.match_dict[prev_img_obj] = img_obj_new_matches[0] prev_img_obj.match_dict[img_obj] = img_obj_new_matches[1] - def get_fd_detection_img(self, img_obj, feature_detector, valis_obj=None): if feature_detector.rgb and valis_obj is not None: slide_obj = valis_obj.get_slide(img_obj.name) if slide_obj.is_rgb: - detect_img, mask, original_shape_rc, uncropped_shape_rc, crop_bbox = valis_obj.get_roi_for_processing(slide_obj, processing_cls=None, mask=slide_obj.rigid_reg_mask) + detect_img, mask, original_shape_rc, uncropped_shape_rc, crop_bbox = ( + valis_obj.get_roi_for_processing( + slide_obj, processing_cls=None, mask=slide_obj.rigid_reg_mask + ) + ) else: detect_img = img_obj.image else: @@ -812,15 +946,51 @@ def get_fd_detection_img(self, img_obj, feature_detector, valis_obj=None): return detect_img - def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): + def rematch( + self, + matcher_obj, + valis_obj=None, + keep_unfiltered=False, + min_matches=0, + ): """ Reclaculate features using feature detecror in `matcher_obj`, and then match with `matcher_obj`. Use existing features to estimate rotation. Apply rotation to moving image, detect features, and match. + Parameters + ---------- + min_matches : int, optional + Minimum initial-pass match count required between any pair + of images before this method will proceed. If any pair has + fewer matches, a ``TooFewMatchesError`` is raised. The + default of 0 preserves legacy behavior. Setting a positive + value (e.g. 10-30) avoids the silent failure mode where a + handful of spurious matches produces a wildly-scaled + SimilarityTransform, which both ruins the registration and + can OOM the rematch's feature detector by warping the + moving image into a multi-thousand-pixel canvas. """ + if min_matches and min_matches > 0: + for moving_idx, fixed_idx in self.iter_order: + img_obj = self.img_obj_list[moving_idx] + prev_img_obj = self.img_obj_list[fixed_idx] + match_info = prev_img_obj.match_dict.get(img_obj) + n = 0 if match_info is None else len(match_info.matched_kp1_xy) + if n < min_matches: + raise TooFewMatchesError( + f"Only {n} initial keypoint matches between " + f"{img_obj.name} and {prev_img_obj.name} " + f"(threshold {min_matches}). Continuing would " + "produce a degenerate rigid transform; aborting." + ) + ref_img_obj = self.img_obj_list[self.reference_img_idx] - ref_img = self.get_fd_detection_img(ref_img_obj, feature_detector=matcher_obj.feature_detector, valis_obj=valis_obj) + ref_img = self.get_fd_detection_img( + ref_img_obj, + feature_detector=matcher_obj.feature_detector, + valis_obj=valis_obj, + ) ref_kp, ref_desc = matcher_obj.feature_detector.detect_and_compute(ref_img) # Need existing matches to estimate rotation. Will dictionaries below to update matches once complete @@ -832,12 +1002,18 @@ def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): updated_kp = {ref_img_obj.name: ref_kp} updated_desc = {ref_img_obj.name: ref_desc} - for moving_idx, fixed_idx in tqdm(self.iter_order, desc=REMATCHING_MSG, unit="image", leave=None): + for moving_idx, fixed_idx in tqdm( + self.iter_order, desc=REMATCHING_MSG, unit="image", leave=None + ): img_obj = self.img_obj_list[moving_idx] prev_img_obj = self.img_obj_list[fixed_idx] - detect_img = self.get_fd_detection_img(img_obj, feature_detector=matcher_obj.feature_detector, valis_obj=valis_obj) + detect_img = self.get_fd_detection_img( + img_obj, + feature_detector=matcher_obj.feature_detector, + valis_obj=valis_obj, + ) if fixed_idx == self.reference_img_idx: prev_img = ref_img @@ -847,7 +1023,10 @@ def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): prev_detect_img = ref_img if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":prev_img_obj.image.shape[0:2], "img2_shape": img_obj.image.shape[0:2]} + filter_kwargs = { + "img1_shape": prev_img_obj.image.shape[0:2], + "img2_shape": img_obj.image.shape[0:2], + } else: filter_kwargs = {} @@ -859,10 +1038,18 @@ def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): rotation_tform = transform.SimilarityTransform() rotation_tform.estimate(fixed_kp_xy, moving_kp_xy) - img_corners_xy = warp_tools.get_corners_of_image(detect_img.shape[0:2])[:, ::-1] - warped_corners_xy = warp_tools.warp_xy(img_corners_xy, M=rotation_tform.params) - max_corners_rc = np.ceil(np.max(warped_corners_xy[:, ::-1], axis=0)).astype(int) - min_corners_rc = np.floor(np.min(warped_corners_xy[:, ::-1], axis=0)).astype(int) + img_corners_xy = warp_tools.get_corners_of_image(detect_img.shape[0:2])[ + :, ::-1 + ] + warped_corners_xy = warp_tools.warp_xy( + img_corners_xy, M=rotation_tform.params + ) + max_corners_rc = np.ceil( + np.max(warped_corners_xy[:, ::-1], axis=0) + ).astype(int) + min_corners_rc = np.floor( + np.min(warped_corners_xy[:, ::-1], axis=0) + ).astype(int) M = rotation_tform.params.copy() warped_shape_rc = max_corners_rc.copy() if np.any(min_corners_rc < 0): @@ -883,49 +1070,69 @@ def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): else: bg_color = None - rotated_img = warp_tools.warp_img(detect_img, M=M, out_shape_rc=warped_shape_rc, bg_color=bg_color) + rotated_img = warp_tools.warp_img( + detect_img, M=M, out_shape_rc=warped_shape_rc, bg_color=bg_color + ) else: rotated_img = detect_img M = np.eye(3) - rot_kp, rot_desc = matcher_obj.feature_detector.detect_and_compute(rotated_img) + rot_kp, rot_desc = matcher_obj.feature_detector.detect_and_compute( + rotated_img + ) kp_in_og = warp_tools.warp_xy(rot_kp, np.linalg.inv(M)) updated_kp[img_obj.name] = kp_in_og updated_desc[img_obj.name] = rot_desc - - match_info12, filtered_match_info12, match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=prev_img, img2=rotated_img, - desc1=prev_desc, kp1_xy=prev_kp, - desc2=rot_desc, kp2_xy=rot_kp) + match_info12, filtered_match_info12, match_info21, filtered_match_info21 = ( + matcher_obj.match_images( + img1=prev_img, + img2=rotated_img, + desc1=prev_desc, + kp1_xy=prev_kp, + desc2=rot_desc, + kp2_xy=rot_kp, + ) + ) # Update match info keypoints to their location in original, unrotated image - prev_matched_kp_in_og = warp_tools.warp_xy(match_info12.matched_kp1_xy, M=np.linalg.inv(prev_M)) - matched_kp_in_og = warp_tools.warp_xy(match_info12.matched_kp2_xy, M=np.linalg.inv(M)) + prev_matched_kp_in_og = warp_tools.warp_xy( + match_info12.matched_kp1_xy, M=np.linalg.inv(prev_M) + ) + matched_kp_in_og = warp_tools.warp_xy( + match_info12.matched_kp2_xy, M=np.linalg.inv(M) + ) match_info12.matched_kp1_xy = prev_matched_kp_in_og match_info12.matched_kp2_xy = matched_kp_in_og match_info21.matched_kp1_xy = matched_kp_in_og match_info21.matched_kp2_xy = prev_matched_kp_in_og - filtered_matched_kp_in_og = warp_tools.warp_xy(filtered_match_info12.matched_kp2_xy, M=np.linalg.inv(M)) - prev_filtered_matched_kp_in_og = warp_tools.warp_xy(filtered_match_info12.matched_kp1_xy, M=np.linalg.inv(prev_M)) + filtered_matched_kp_in_og = warp_tools.warp_xy( + filtered_match_info12.matched_kp2_xy, M=np.linalg.inv(M) + ) + prev_filtered_matched_kp_in_og = warp_tools.warp_xy( + filtered_match_info12.matched_kp1_xy, M=np.linalg.inv(prev_M) + ) filtered_match_info12.matched_kp1_xy = prev_filtered_matched_kp_in_og filtered_match_info12.matched_kp2_xy = filtered_matched_kp_in_og filtered_match_info21.matched_kp1_xy = filtered_matched_kp_in_og filtered_match_info21.matched_kp2_xy = prev_filtered_matched_kp_in_og - updated_filtered_matches12[prev_img_obj.name][img_obj.name] = filtered_match_info12 - updated_filtered_matches21[img_obj.name][prev_img_obj.name] = filtered_match_info21 + updated_filtered_matches12[prev_img_obj.name][ + img_obj.name + ] = filtered_match_info12 + updated_filtered_matches21[img_obj.name][ + prev_img_obj.name + ] = filtered_match_info21 if keep_unfiltered: updated_matches12[prev_img_obj.name][img_obj.name] = match_info12 updated_matches21[img_obj.name][prev_img_obj.name] = match_info21 - # Update for next step. Use rotated info prev_img_obj = img_obj prev_img = rotated_img @@ -936,7 +1143,10 @@ def rematch(self, matcher_obj, valis_obj=None, keep_unfiltered=False): def _estimate_error(kp1, kp2): error_estimator = transform.SimilarityTransform() - error_estimator.estimate(kp1, kp2) + try: + error_estimator.estimate(kp1, kp2) + except np.linalg.LinAlgError: + return np.inf warped_1 = error_estimator(kp1) estimated_d = np.mean(warp_tools.calc_d(warped_1, kp2)) @@ -956,14 +1166,20 @@ def _estimate_error(kp1, kp2): img_obj.kp_pos_xy = new_kp img_obj.desc = new_desc - old_matches = img_obj.match_dict[prev_img_obj] # Here, img_obj = 1, prev_img_obj = 2 + old_matches = img_obj.match_dict[ + prev_img_obj + ] # Here, img_obj = 1, prev_img_obj = 2 new_matches21 = updated_filtered_matches21[img_obj.name][prev_img_obj.name] n_old_matches = old_matches.n_matches n_new_matches = new_matches21.n_matches - old_d = _estimate_error(old_matches.matched_kp1_xy, old_matches.matched_kp2_xy) - new_d = _estimate_error(new_matches21.matched_kp1_xy, new_matches21.matched_kp2_xy) + old_d = _estimate_error( + old_matches.matched_kp1_xy, old_matches.matched_kp2_xy + ) + new_d = _estimate_error( + new_matches21.matched_kp1_xy, new_matches21.matched_kp2_xy + ) lower_d = new_d < old_d more_matches = n_new_matches > n_old_matches @@ -979,13 +1195,14 @@ def _estimate_error(kp1, kp2): comparision = "worse" action = "ignore new matches" - msg = (f"When matching {img_obj.name} to {prev_img_obj.name}, the results are {comparision} when using {new_matcher_str}. " - f"Number of matches: {new_matcher_str} = {n_new_matches}, {old_matcher_str} = {n_old_matches}. " - f"Mean distance between matches: {new_matcher_str} = {new_d:.4}, {old_matcher_str} = {old_d:.4}. " - f"Will {action} when estimating rigid transform.\n" - ) + msg = ( + f"When matching {img_obj.name} to {prev_img_obj.name}, the results are {comparision} when using {new_matcher_str}. " + f"Number of matches: {new_matcher_str} = {n_new_matches}, {old_matcher_str} = {n_old_matches}. " + f"Mean distance between matches: {new_matcher_str} = {new_d:.4}, {old_matcher_str} = {old_d:.4}. " + f"Will {action} when estimating rigid transform.\n" + ) - valtils.print_warning(msg, None, rgb=msg_clr) + logger.info(msg) if not lower_d and not more_matches: continue @@ -993,23 +1210,30 @@ def _estimate_error(kp1, kp2): new_matches12 = updated_filtered_matches12[prev_img_obj.name][img_obj.name] if comparision == "mixed": - match_feature_name = f"{old_matches.feature_detector_name} + {matcher_obj.feature_name}" - _updated_kp1 = np.vstack([new_matches21.matched_kp1_xy, old_matches.matched_kp1_xy]) - _updated_kp2 = np.vstack([new_matches21.matched_kp2_xy, old_matches.matched_kp2_xy]) - updated_kp1, updated_kp2, good_idx = feature_matcher.filter_matches_ransac(_updated_kp1, _updated_kp2) + match_feature_name = ( + f"{old_matches.feature_detector_name} + {matcher_obj.feature_name}" + ) + _updated_kp1 = np.vstack( + [new_matches21.matched_kp1_xy, old_matches.matched_kp1_xy] + ) + _updated_kp2 = np.vstack( + [new_matches21.matched_kp2_xy, old_matches.matched_kp2_xy] + ) + updated_kp1, updated_kp2, good_idx = ( + feature_matcher.filter_matches_ransac(_updated_kp1, _updated_kp2) + ) combined_n_matches = len(good_idx) new_matches21.matched_kp1_xy = updated_kp1 new_matches21.matched_kp2_xy = updated_kp2 new_matches21.n_matches = combined_n_matches - new_matches12.matched_kp1_xy = updated_kp2 #Here, img_obj is 2 + new_matches12.matched_kp1_xy = updated_kp2 # Here, img_obj is 2 new_matches12.matched_kp2_xy = updated_kp1 new_matches12.n_matches = combined_n_matches else: match_feature_name = matcher_obj.feature_name - new_matches21.feature_detector_name = match_feature_name new_matches12.feature_detector_name = match_feature_name @@ -1017,12 +1241,28 @@ def _estimate_error(kp1, kp2): prev_img_obj.match_dict[img_obj] = new_matches12 if keep_unfiltered: - new_unfiltered_matches21 = updated_matches21[img_obj.name][prev_img_obj.name] - new_unfiltered_matches12 = updated_matches12[prev_img_obj.name][img_obj.name] - old_unfiltered_matches = img_obj.match_dict[prev_img_obj] # Here, img_obj = 1, prev_img_obj = 2 - - updated_unfiltered_kp1 = np.vstack([new_unfiltered_matches21.matched_kp1_xy, old_unfiltered_matches.matched_kp1_xy]) - updated_unfiltered_kp2 = np.vstack([new_unfiltered_matches21.matched_kp2_xy, old_unfiltered_matches.matched_kp2_xy]) + new_unfiltered_matches21 = updated_matches21[img_obj.name][ + prev_img_obj.name + ] + new_unfiltered_matches12 = updated_matches12[prev_img_obj.name][ + img_obj.name + ] + old_unfiltered_matches = img_obj.match_dict[ + prev_img_obj + ] # Here, img_obj = 1, prev_img_obj = 2 + + updated_unfiltered_kp1 = np.vstack( + [ + new_unfiltered_matches21.matched_kp1_xy, + old_unfiltered_matches.matched_kp1_xy, + ] + ) + updated_unfiltered_kp2 = np.vstack( + [ + new_unfiltered_matches21.matched_kp2_xy, + old_unfiltered_matches.matched_kp2_xy, + ] + ) combined_n_unfiltered_matches = updated_unfiltered_kp2.shape[0] @@ -1030,7 +1270,9 @@ def _estimate_error(kp1, kp2): new_unfiltered_matches21.matched_kp2_xy = updated_unfiltered_kp2 new_unfiltered_matches21.n_matches = combined_n_unfiltered_matches - new_unfiltered_matches12.matched_kp1_xy = updated_unfiltered_kp2 #Here, img_obj is 2 + new_unfiltered_matches12.matched_kp1_xy = ( + updated_unfiltered_kp2 # Here, img_obj is 2 + ) new_unfiltered_matches12.matched_kp2_xy = updated_unfiltered_kp1 new_unfiltered_matches12.n_matches = combined_n_unfiltered_matches @@ -1079,7 +1321,7 @@ def build_metric_matrix(self, metric="n_matches"): max_d = distance_mat.max() # Make sure that image has highest similarity with itself - similarity_mat[np.diag_indices_from(similarity_mat)] += max_s*0.01 + similarity_mat[np.diag_indices_from(similarity_mat)] += max_s * 0.01 # Scale metrics between 0 and 1 similarity_mat = (similarity_mat - min_s) / (max_s - min_s) @@ -1131,7 +1373,14 @@ def get_iter_order(self): prev_img_obj = self.img_obj_list[fixed_idx] img_obj.fixed_obj = prev_img_obj - def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=None, keep_unfiltered=False, qt_emitter=None): + def align_to_prev_check_reflections( + self, + transformer, + matcher_obj, + valis_obj=None, + keep_unfiltered=False, + qt_emitter=None, + ): """Use key points to align current image to previous image in the stack, but checking if reflection improves alignment Parameters @@ -1155,7 +1404,9 @@ def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=No """ ref_img_obj = self.img_obj_list[self.reference_img_idx] - for moving_idx, fixed_idx in tqdm(self.iter_order, desc=TRANSFORM_MSG, unit="image", leave=None): + for moving_idx, fixed_idx in tqdm( + self.iter_order, desc=TRANSFORM_MSG, unit="image", leave=None + ): img_obj = self.img_obj_list[moving_idx] prev_img_obj = self.img_obj_list[fixed_idx] @@ -1163,15 +1414,26 @@ def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=No prev_M = ref_img_obj.T.copy() if matcher_obj.match_filter_method == GMS_NAME: - filter_kwargs = {"img1_shape":img_obj.image.shape[0:2], "img2_shape": prev_img_obj.image.shape[0:2]} + filter_kwargs = { + "img1_shape": img_obj.image.shape[0:2], + "img2_shape": prev_img_obj.image.shape[0:2], + } else: filter_kwargs = {} # Estimate current error without reflections. Don't need to re-detect and match features to_prev_match_info = img_obj.match_dict[prev_img_obj] - transformer.estimate(to_prev_match_info.matched_kp2_xy, to_prev_match_info.matched_kp1_xy) - unreflected_warped_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, transformer.params) - _, unreflected_d = warp_tools.measure_error(to_prev_match_info.matched_kp2_xy, unreflected_warped_src_xy, prev_img_obj.image.shape) + transformer.estimate( + to_prev_match_info.matched_kp2_xy, to_prev_match_info.matched_kp1_xy + ) + unreflected_warped_src_xy = warp_tools.warp_xy( + to_prev_match_info.matched_kp1_xy, transformer.params + ) + _, unreflected_d = warp_tools.measure_error( + to_prev_match_info.matched_kp2_xy, + unreflected_warped_src_xy, + prev_img_obj.image.shape, + ) reflected_d_vals = [unreflected_d] reflection_M = [np.eye(3)] @@ -1180,35 +1442,93 @@ def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=No reflected_matches21 = [prev_img_obj.match_dict[img_obj]] if keep_unfiltered and prev_img_obj in img_obj.unfiltered_match_dict: - unfiltered_reflected_matches12 = [img_obj.unfiltered_match_dict[prev_img_obj]] - unfiltered_reflected_matches21 = [prev_img_obj.unfiltered_match_dict[img_obj]] + unfiltered_reflected_matches12 = [ + img_obj.unfiltered_match_dict[prev_img_obj] + ] + unfiltered_reflected_matches21 = [ + prev_img_obj.unfiltered_match_dict[img_obj] + ] # Estimate error with reflections dst_xy = warp_tools.warp_xy(prev_img_obj.kp_pos_xy, prev_M) - prev_detect_img = self.get_fd_detection_img(prev_img_obj, feature_detector=matcher_obj.feature_detector, valis_obj=valis_obj) - prev_warped = warp_tools.warp_img(prev_detect_img, prev_M, out_shape_rc=prev_img_obj.padded_shape_rc) - - detect_img = self.get_fd_detection_img(img_obj, feature_detector=matcher_obj.feature_detector, valis_obj=valis_obj) + prev_detect_img = self.get_fd_detection_img( + prev_img_obj, + feature_detector=matcher_obj.feature_detector, + valis_obj=valis_obj, + ) + prev_warped = warp_tools.warp_img( + prev_detect_img, prev_M, out_shape_rc=prev_img_obj.padded_shape_rc + ) + + detect_img = self.get_fd_detection_img( + img_obj, + feature_detector=matcher_obj.feature_detector, + valis_obj=valis_obj, + ) for rx in [False, True]: for ry in [False, True]: if not rx and not ry: continue rM = warp_tools.get_reflection_M(rx, ry, img_obj.image.shape) - reflected_img = warp_tools.warp_img(detect_img, rM @ img_obj.T, out_shape_rc=img_obj.padded_shape_rc) - - reflected_src_xy, reflected_desc = matcher_obj.feature_detector.detect_and_compute(reflected_img) - unfiltered_match_info12, filtered_match_info12, unfiltered_match_info21, filtered_match_info21 = \ - matcher_obj.match_images(img1=reflected_img, desc1=reflected_desc, kp1_xy=reflected_src_xy, - img2=prev_warped, desc2=prev_img_obj.desc, kp2_xy=dst_xy, - additional_filtering_kwargs=filter_kwargs, - **filter_kwargs) + reflected_img = warp_tools.warp_img( + detect_img, rM @ img_obj.T, out_shape_rc=img_obj.padded_shape_rc + ) + + reflected_src_xy, reflected_desc = ( + matcher_obj.feature_detector.detect_and_compute(reflected_img) + ) + ( + unfiltered_match_info12, + filtered_match_info12, + unfiltered_match_info21, + filtered_match_info21, + ) = matcher_obj.match_images( + img1=reflected_img, + desc1=reflected_desc, + kp1_xy=reflected_src_xy, + img2=prev_warped, + desc2=prev_img_obj.desc, + kp2_xy=dst_xy.astype(reflected_src_xy.dtype), + additional_filtering_kwargs=filter_kwargs, + **filter_kwargs, + ) # Record info # - _ = transformer.estimate(filtered_match_info12.matched_kp2_xy, filtered_match_info12.matched_kp1_xy) - reflected_warped_src_xy = warp_tools.warp_xy(filtered_match_info12.matched_kp1_xy, transformer.params) - _, reflected_d = warp_tools.measure_error(filtered_match_info12.matched_kp2_xy, reflected_warped_src_xy, prev_img_obj.padded_shape_rc) + n_kp = len(filtered_match_info12.matched_kp1_xy) + if n_kp < 3: + logger.warning( + f"Reflection check (rx={rx}, ry={ry}) for {img_obj.name}: " + f"only {n_kp} matches after filtering — skipping this " + "orientation (cannot fit similarity transform)." + ) + reflected_d_vals.append(np.inf) + reflection_M.append(rM) + transforms.append(np.eye(3)) + reflected_matches12.append(filtered_match_info12) + reflected_matches21.append(filtered_match_info21) + if keep_unfiltered: + unfiltered_reflected_matches12.append( + unfiltered_match_info12 + ) + unfiltered_reflected_matches21.append( + unfiltered_match_info21 + ) + continue + + _ = transformer.estimate( + filtered_match_info12.matched_kp2_xy, + filtered_match_info12.matched_kp1_xy, + ) + reflected_warped_src_xy = warp_tools.warp_xy( + filtered_match_info12.matched_kp1_xy, transformer.params + ) + _, reflected_d = warp_tools.measure_error( + filtered_match_info12.matched_kp2_xy, + reflected_warped_src_xy, + prev_img_obj.padded_shape_rc, + ) reflected_d_vals.append(reflected_d) reflection_M.append(rM) transforms.append(transformer.params) @@ -1217,21 +1537,37 @@ def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=No img_inv_M = np.linalg.inv(rM @ img_obj.T) prev_img_inv_M = np.linalg.inv(prev_M) - filtered_match_info12.matched_kp1_xy = warp_tools.warp_xy(filtered_match_info12.matched_kp1_xy, img_inv_M) - filtered_match_info12.matched_kp2_xy = warp_tools.warp_xy(filtered_match_info12.matched_kp2_xy, prev_img_inv_M) + filtered_match_info12.matched_kp1_xy = warp_tools.warp_xy( + filtered_match_info12.matched_kp1_xy, img_inv_M + ) + filtered_match_info12.matched_kp2_xy = warp_tools.warp_xy( + filtered_match_info12.matched_kp2_xy, prev_img_inv_M + ) - filtered_match_info21.matched_kp1_xy = warp_tools.warp_xy(filtered_match_info21.matched_kp1_xy, prev_img_inv_M) - filtered_match_info21.matched_kp2_xy = warp_tools.warp_xy(filtered_match_info21.matched_kp2_xy, img_inv_M) + filtered_match_info21.matched_kp1_xy = warp_tools.warp_xy( + filtered_match_info21.matched_kp1_xy, prev_img_inv_M + ) + filtered_match_info21.matched_kp2_xy = warp_tools.warp_xy( + filtered_match_info21.matched_kp2_xy, img_inv_M + ) reflected_matches12.append(filtered_match_info12) reflected_matches21.append(filtered_match_info21) if keep_unfiltered: - unfiltered_match_info12.matched_kp1_xy = warp_tools.warp_xy(unfiltered_match_info12.matched_kp1_xy, img_inv_M) - unfiltered_match_info12.matched_kp2_xy = warp_tools.warp_xy(unfiltered_match_info12.matched_kp2_xy, prev_img_inv_M) - - unfiltered_match_info21.matched_kp1_xy = warp_tools.warp_xy(unfiltered_match_info21.matched_kp1_xy, prev_img_inv_M) - unfiltered_match_info21.matched_kp2_xy = warp_tools.warp_xy(unfiltered_match_info21.matched_kp2_xy, img_inv_M) + unfiltered_match_info12.matched_kp1_xy = warp_tools.warp_xy( + unfiltered_match_info12.matched_kp1_xy, img_inv_M + ) + unfiltered_match_info12.matched_kp2_xy = warp_tools.warp_xy( + unfiltered_match_info12.matched_kp2_xy, prev_img_inv_M + ) + + unfiltered_match_info21.matched_kp1_xy = warp_tools.warp_xy( + unfiltered_match_info21.matched_kp1_xy, prev_img_inv_M + ) + unfiltered_match_info21.matched_kp2_xy = warp_tools.warp_xy( + unfiltered_match_info21.matched_kp2_xy, img_inv_M + ) unfiltered_reflected_matches12.append(unfiltered_match_info12) unfiltered_reflected_matches21.append(unfiltered_match_info21) @@ -1245,23 +1581,27 @@ def align_to_prev_check_reflections(self, transformer, matcher_obj, valis_obj=No ref_x, ref_y = best_reflect_M[[0, 1], [0, 1]] < 0 if ref_x or ref_y: - msg = f'detected relfections between {img_obj.name} and {prev_img_obj.name} along the' + msg = f"detected relfections between {img_obj.name} and {prev_img_obj.name} along the" if ref_x and ref_y: - msg = f'{msg} x and y axes' + msg = f"{msg} x and y axes" elif ref_x: - msg = f'{msg} x axis' + msg = f"{msg} x axis" elif ref_y: - msg = f'{msg} y axis' - msg = f'{msg}. Will include reflection for {img_obj.name}' - valtils.print_warning(msg) + msg = f"{msg} y axis" + msg = f"{msg}. Will include reflection for {img_obj.name}" + logger.warning(msg) # Update matches img_obj.match_dict[prev_img_obj] = reflected_matches12[best_idx] prev_img_obj.match_dict[img_obj] = reflected_matches21[best_idx] if keep_unfiltered: - img_obj.unfiltered_match_dict[prev_img_obj] = unfiltered_reflected_matches12[best_idx] - prev_img_obj.unfiltered_match_dict[img_obj] = unfiltered_reflected_matches21[best_idx] + img_obj.unfiltered_match_dict[prev_img_obj] = ( + unfiltered_reflected_matches12[best_idx] + ) + prev_img_obj.unfiltered_match_dict[img_obj] = ( + unfiltered_reflected_matches21[best_idx] + ) if qt_emitter is not None: qt_emitter.emit(1) @@ -1284,7 +1624,9 @@ def align_to_prev(self, transformer, qt_emitter=None): if qt_emitter is not None: qt_emitter.emit(1) - for moving_idx, fixed_idx in tqdm(self.iter_order, desc=TRANSFORM_MSG, unit="image", leave=None): + for moving_idx, fixed_idx in tqdm( + self.iter_order, desc=TRANSFORM_MSG, unit="image", leave=None + ): img_obj = self.img_obj_list[moving_idx] prev_img_obj = self.img_obj_list[fixed_idx] img_obj.fixed_obj = prev_img_obj @@ -1302,9 +1644,9 @@ def align_to_prev(self, transformer, qt_emitter=None): M = transformer.params M = self.check_M(src_xy, dst_xy, M) - if np.all(M==np.eye(3)): + if np.all(M == np.eye(3)): msg = f"Rigid registration between {img_obj.name} and {prev_img_obj.name} appears to have failed. Will not rigidly warp {img_obj.name}" - valtils.print_warning(msg) + logger.warning(msg) M = np.eye(3) img_obj.to_prev_A = M @@ -1314,7 +1656,6 @@ def align_to_prev(self, transformer, qt_emitter=None): if qt_emitter is not None: qt_emitter.emit(1) - def optimize(self, affine_optimizer, qt_emitter=None): """Refine alignment by minimizing a metric @@ -1332,12 +1673,15 @@ def optimize(self, affine_optimizer, qt_emitter=None): """ ref_img_obj = self.img_obj_list[self.reference_img_idx] - ref_warped = warp_tools.warp_img(ref_img_obj.image, M=ref_img_obj.T, - out_shape_rc=ref_img_obj.padded_shape_rc) + ref_warped = warp_tools.warp_img( + ref_img_obj.image, M=ref_img_obj.T, out_shape_rc=ref_img_obj.padded_shape_rc + ) if qt_emitter is not None: qt_emitter.emit(1) - for moving_idx, fixed_idx in tqdm(self.iter_order, desc=OPTIMIZING_MSG, unit="image", leave=None): + for moving_idx, fixed_idx in tqdm( + self.iter_order, desc=OPTIMIZING_MSG, unit="image", leave=None + ): img_obj = self.img_obj_list[moving_idx] prev_img_obj = self.img_obj_list[fixed_idx] @@ -1346,27 +1690,29 @@ def optimize(self, affine_optimizer, qt_emitter=None): prev_M = ref_img_obj.T M = img_obj.reflection_M @ img_obj.T @ img_obj.to_prev_A - warped_img = warp_tools.warp_img(img_obj.image, - M=M, - out_shape_rc=img_obj.padded_shape_rc) + warped_img = warp_tools.warp_img( + img_obj.image, M=M, out_shape_rc=img_obj.padded_shape_rc + ) to_prev_match_info = img_obj.match_dict[prev_img_obj] before_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, M) - before_dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - before_tre, before_med_d = warp_tools.measure_error(before_src_xy, - before_dst_xy, - warped_img.shape) + before_dst_xy = warp_tools.warp_xy( + to_prev_match_info.matched_kp2_xy, prev_M + ) + before_tre, before_med_d = warp_tools.measure_error( + before_src_xy, before_dst_xy, warped_img.shape + ) # Get mask img_mask = np.ones(img_obj.image.shape[0:2], dtype=np.uint8) - warped_img_mask = warp_tools.warp_img(img_mask, - M=M, - out_shape_rc=img_obj.padded_shape_rc) + warped_img_mask = warp_tools.warp_img( + img_mask, M=M, out_shape_rc=img_obj.padded_shape_rc + ) prev_img_mask = np.ones(prev_img_obj.image.shape[0:2], dtype=np.uint8) - warped_prev_img_mask = warp_tools.warp_img(prev_img_mask, - M=prev_M, - out_shape_rc=prev_img_obj.padded_shape_rc) + warped_prev_img_mask = warp_tools.warp_img( + prev_img_mask, M=prev_M, out_shape_rc=prev_img_obj.padded_shape_rc + ) mask = np.zeros(warped_img_mask.shape, dtype=np.uint8) mask[(warped_img_mask != 0) & (warped_prev_img_mask != 0)] = 255 @@ -1380,26 +1726,32 @@ def optimize(self, affine_optimizer, qt_emitter=None): fixed_xy = None with valtils.HiddenPrints(): - _, optimal_M, _ = affine_optimizer.align(moving=warped_img, fixed=prev_img, - mask=mask, initial_M=None, - moving_xy=moving_xy, - fixed_xy=fixed_xy) + _, optimal_M, _ = affine_optimizer.align( + moving=warped_img, + fixed=prev_img, + mask=mask, + initial_M=None, + moving_xy=moving_xy, + fixed_xy=fixed_xy, + ) # Keep optimal M if it actually improved alignment initial_cst = affine_optimizer.cost_fxn(warped_img, prev_img, mask) - after_src_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp1_xy, M @ optimal_M) + after_src_xy = warp_tools.warp_xy( + to_prev_match_info.matched_kp1_xy, M @ optimal_M + ) after_dst_xy = warp_tools.warp_xy(to_prev_match_info.matched_kp2_xy, prev_M) - optimal_reg_img = warp_tools.warp_img(warped_img, - M=optimal_M, - out_shape_rc=img_obj.padded_shape_rc) + optimal_reg_img = warp_tools.warp_img( + warped_img, M=optimal_M, out_shape_rc=img_obj.padded_shape_rc + ) after_cst = affine_optimizer.cost_fxn(optimal_reg_img, prev_img, mask) - after_tre, after_med_d = warp_tools.measure_error(after_src_xy, - after_dst_xy, - warped_img.shape) + after_tre, after_med_d = warp_tools.measure_error( + after_src_xy, after_dst_xy, warped_img.shape + ) if after_cst is not None and initial_cst is not None: lower_cost = after_cst <= initial_cst @@ -1411,10 +1763,12 @@ def optimize(self, affine_optimizer, qt_emitter=None): prev_img = optimal_reg_img img_obj.optimal_M = optimal_M else: - msg = (f"Somehow optimization made things worse. " - f"Cost was {initial_cst} but is now {after_cst}" - f"KP medD was {before_med_d}, but is now {after_med_d}.") - valtils.print_warning(msg) + msg = ( + f"Somehow optimization made things worse. " + f"Cost was {initial_cst} but is now {after_cst}" + f"KP medD was {before_med_d}, but is now {after_med_d}." + ) + logger.warning(msg) prev_img = warped_img prev_M = M @ img_obj.optimal_M @@ -1423,8 +1777,7 @@ def optimize(self, affine_optimizer, qt_emitter=None): qt_emitter.emit(1) def calc_warped_img_size(self): - """Determine the shape of the registered images - """ + """Determine the shape of the registered images""" min_x = np.inf max_x = 0 min_y = np.inf @@ -1446,8 +1799,7 @@ def calc_warped_img_size(self): return np.array([h, w]) def finalize(self): - """Combine transformation matrices and get final shape of registered images - """ + """Combine transformation matrices and get final shape of registered images""" min_x = np.inf max_x = 0 @@ -1479,9 +1831,9 @@ def finalize(self): img_obj.crop_T = crop_T img_obj.M = M_list[i] @ crop_T img_obj.M_inv = np.linalg.inv(img_obj.M) - img_obj.registered_img = warp_tools.warp_img(img=img_obj.image, - M=img_obj.M, - out_shape_rc=(h, w)) + img_obj.registered_img = warp_tools.warp_img( + img=img_obj.image, M=img_obj.M, out_shape_rc=(h, w) + ) img_obj.registered_shape_rc = img_obj.registered_img.shape[0:2] @@ -1529,8 +1881,12 @@ def wiggle_to_ref(self, transformer): matches = img_obj.match_dict[img_obj.fixed_obj] - rigid_reg_moving_xy = warp_tools.warp_xy(matches.matched_kp1_xy, M=img_obj.M) - rigid_reg_fixed_xy = warp_tools.warp_xy(matches.matched_kp2_xy, M=img_obj.fixed_obj.M) + rigid_reg_moving_xy = warp_tools.warp_xy( + matches.matched_kp1_xy, M=img_obj.M + ) + rigid_reg_fixed_xy = warp_tools.warp_xy( + matches.matched_kp2_xy, M=img_obj.fixed_obj.M + ) transformer.estimate(src=rigid_reg_fixed_xy, dst=rigid_reg_moving_xy) @@ -1573,12 +1929,12 @@ def clear_unused_matches(self): if i == 0: prev_img_obj = None else: - prev_img_obj = self.img_obj_list[i-1] + prev_img_obj = self.img_obj_list[i - 1] if i == self.size - 1: next_img_obj = None else: - next_img_obj = self.img_obj_list[i+1] + next_img_obj = self.img_obj_list[i + 1] img_obj.reduce(prev_img_obj, next_img_obj) @@ -1616,57 +1972,75 @@ def summarize(self): temp_current_pts = current_to_prev_matches.matched_kp1_xy temp_prev_pts = current_to_prev_matches.matched_kp2_xy - og_tre_list[i], og_med_d_list[i] = \ - warp_tools.measure_error(temp_current_pts, - temp_prev_pts, - img_obj.image.shape) + og_tre_list[i], og_med_d_list[i] = warp_tools.measure_error( + temp_current_pts, temp_prev_pts, img_obj.image.shape + ) current_pts = warp_tools.warp_xy(temp_current_pts, img_obj.M) prev_pts = warp_tools.warp_xy(temp_prev_pts, prev_img_obj.M) - tre_list[i], med_d_list[i] = \ - warp_tools.measure_error(current_pts, - prev_pts, - img_obj.image.shape) - - similarities = \ - convert_distance_to_similarity(current_to_prev_matches.match_distances, - current_to_prev_matches.matched_desc1.shape[0]) - - _, weighted_med_d_list[i] = \ - warp_tools.measure_error(current_pts, prev_pts, - img_obj.image.shape, similarities) - - summary_df = pd.DataFrame({ - "from": src_img_names, - "to": dst_img_names, - "original_D": og_med_d_list, - "D": med_d_list, - "D_weighted": weighted_med_d_list, - "original_TRE": og_tre_list, - "TRE": tre_list, - "shape": shape_list, - }) + tre_list[i], med_d_list[i] = warp_tools.measure_error( + current_pts, prev_pts, img_obj.image.shape + ) + + similarities = convert_distance_to_similarity( + current_to_prev_matches.match_distances, + current_to_prev_matches.matched_desc1.shape[0], + ) + + _, weighted_med_d_list[i] = warp_tools.measure_error( + current_pts, prev_pts, img_obj.image.shape, similarities + ) + + summary_df = pd.DataFrame( + { + "from": src_img_names, + "to": dst_img_names, + "original_D": og_med_d_list, + "D": med_d_list, + "D_weighted": weighted_med_d_list, + "original_TRE": og_tre_list, + "TRE": tre_list, + "shape": shape_list, + } + ) non_ref_idx = list(range(self.size)) non_ref_idx.remove(self.reference_img_idx) - summary_df["series_d"] = warp_tools.calc_total_error(summary_df.D.values[non_ref_idx]) - summary_df["series_tre"] = warp_tools.calc_total_error(summary_df.TRE.values[non_ref_idx]) - summary_df["series_weighted_d"] = warp_tools.calc_total_error(summary_df.D_weighted.values[non_ref_idx]) + summary_df["series_d"] = warp_tools.calc_total_error( + summary_df.D.values[non_ref_idx] + ) + summary_df["series_tre"] = warp_tools.calc_total_error( + summary_df.TRE.values[non_ref_idx] + ) + summary_df["series_weighted_d"] = warp_tools.calc_total_error( + summary_df.D_weighted.values[non_ref_idx] + ) summary_df["name"] = self.name return summary_df -def register_images(img_dir, dst_dir=None, name="registrar", - matcher=Matcher(), - matcher_for_sorting=Matcher(), - transformer=EuclideanTransform(), - affine_optimizer=None, - imgs_ordered=False, reference_img_f=None, - similarity_metric="n_matches", - check_for_reflections=False, - max_scaling=3.0, align_to_reference=False, qt_emitter=None, valis_obj=None, *args, **kwargs): +def register_images( + img_dir, + dst_dir=None, + name="registrar", + matcher=Matcher(), + matcher_for_sorting=Matcher(), + transformer=EuclideanTransform(), + affine_optimizer=None, + imgs_ordered=False, + reference_img_f=None, + similarity_metric="n_matches", + check_for_reflections=False, + max_scaling=3.0, + align_to_reference=False, + qt_emitter=None, + valis_obj=None, + min_rigid_matches=0, + *args, + **kwargs, +): """ Rigidly align collection of images @@ -1745,12 +2119,11 @@ def register_images(img_dir, dst_dir=None, name="registrar", tic = time() if affine_optimizer is not None: if transformer.__class__.__name__ != affine_optimizer.transformation: - print(Warning("Transformer is of type ", - transformer.__class__.__name__, - "but affine_optimizer optimizes the", - affine_optimizer.transformation, - ". Setting", transformer.__class__.__name__, - "as the transform to be optimized")) + logger.warning( + f"Transformer is of type {transformer.__class__.__name__} " + f"but affine_optimizer optimizes the {affine_optimizer.transformation}. " + f"Setting {transformer.__class__.__name__} as the transform to be optimized" + ) affine_optimizer.transformation = transformer.__class__.__name__ @@ -1761,50 +2134,68 @@ def register_images(img_dir, dst_dir=None, name="registrar", matcher.scaling = True matcher_for_sorting.scaling = True - registrar = SerialRigidRegistrar(img_dir, - imgs_ordered=imgs_ordered, - reference_img_f=reference_img_f, - name=name, - align_to_reference=align_to_reference) - + registrar = SerialRigidRegistrar( + img_dir, + imgs_ordered=imgs_ordered, + reference_img_f=reference_img_f, + name=name, + align_to_reference=align_to_reference, + ) # Filter `registrar.img_file_list` to only include images in valis_obj.slide_dict # Can be useful if `img_dir` has images from previous runs, some of which aren't needed this time if valis_obj is not None: - keep_imgs = [f for f in registrar.img_file_list if valtils.get_name(f) in valis_obj.slide_dict.keys()] + keep_imgs = [ + f + for f in registrar.img_file_list + if valtils.get_name(f) in valis_obj.slide_dict.keys() + ] registrar.img_file_list = keep_imgs registrar.size = len(keep_imgs) valis_obj.rigid_registrar = registrar # print("\n======== Detecting features\n") - registrar.generate_img_obj_list(feature_detector=matcher_for_sorting.feature_detector, valis_obj=valis_obj, qt_emitter=qt_emitter) + registrar.generate_img_obj_list( + feature_detector=matcher_for_sorting.feature_detector, + valis_obj=valis_obj, + qt_emitter=qt_emitter, + ) if valis_obj is not None: if valis_obj.create_masks: # Remove feature points outside of mask for img_obj in registrar.img_obj_dict.values(): slide_obj = valis_obj.get_slide(img_obj.name) - reg_mask = valis_obj.crop_rigid_reg_mask(slide_obj, mask=slide_obj.rigid_reg_mask) + reg_mask = valis_obj.crop_rigid_reg_mask( + slide_obj, mask=slide_obj.rigid_reg_mask + ) reg_mask = preprocessing.mask2bbox_mask(reg_mask) - features_in_mask_idx = warp_tools.get_xy_inside_mask(xy=img_obj.kp_pos_xy, mask=reg_mask) + features_in_mask_idx = warp_tools.get_xy_inside_mask( + xy=img_obj.kp_pos_xy, mask=reg_mask + ) if len(features_in_mask_idx) > 0: img_obj.kp_pos_xy = img_obj.kp_pos_xy[features_in_mask_idx, :] img_obj.desc = img_obj.desc[features_in_mask_idx, :] # print("\n======== Matching images\n") if registrar.aleady_sorted: - registrar.match_sorted_imgs(matcher_for_sorting, keep_unfiltered=False, - valis_obj=valis_obj, - qt_emitter=qt_emitter) + registrar.match_sorted_imgs( + matcher_for_sorting, + keep_unfiltered=False, + valis_obj=valis_obj, + qt_emitter=qt_emitter, + ) for i, img_obj in enumerate(registrar.img_obj_list): img_obj.stack_idx = i else: - registrar.match_imgs(matcher_obj=matcher_for_sorting, - keep_unfiltered=False, - valis_obj=valis_obj, - qt_emitter=qt_emitter) + registrar.match_imgs( + matcher_obj=matcher_for_sorting, + keep_unfiltered=False, + valis_obj=valis_obj, + qt_emitter=qt_emitter, + ) # print("\n======== Sorting images\n") registrar.build_metric_matrix(metric=similarity_metric) @@ -1814,36 +2205,67 @@ def register_images(img_dir, dst_dir=None, name="registrar", registrar.distance_metric_name = matcher.metric_name registrar.distance_metric_type = matcher.metric_type # Recalculate features and match using feature detector built into the matcher - do_rematch = (matcher_for_sorting.__class__.__name__ != matcher.__class__.__name__) \ - or (matcher_for_sorting.feature_detector.__class__.__name__ != matcher.feature_detector.__class__.__name__) + do_rematch = ( + matcher_for_sorting.__class__.__name__ != matcher.__class__.__name__ + ) or ( + matcher_for_sorting.feature_detector.__class__.__name__ + != matcher.feature_detector.__class__.__name__ + ) if do_rematch: - msg = (f"Images sorted using {matcher_for_sorting.feature_detector.__class__.__name__} features. " - f"Will now use {matcher.__class__.__name__} to match images using {matcher.feature_detector.__class__.__name__} features") + msg = ( + f"Images sorted using {matcher_for_sorting.feature_detector.__class__.__name__} features. " + f"Will now use {matcher.__class__.__name__} to match images using {matcher.feature_detector.__class__.__name__} features" + ) # Images sorted with feature_detector, but need to matched using matcher's feature_detector - valtils.print_warning(msg, None) - registrar.rematch(matcher_obj=matcher, valis_obj=valis_obj, keep_unfiltered=False) + logger.info(msg) + registrar.rematch( + matcher_obj=matcher, + valis_obj=valis_obj, + keep_unfiltered=False, + min_matches=min_rigid_matches, + ) + elif min_rigid_matches and min_rigid_matches > 0: + # No rematch will happen, but still enforce the minimum-matches + # contract so callers get the same loud failure regardless of + # which feature detector / matcher combination they configured. + for moving_idx, fixed_idx in registrar.iter_order: + img_obj = registrar.img_obj_list[moving_idx] + prev_img_obj = registrar.img_obj_list[fixed_idx] + match_info = prev_img_obj.match_dict.get(img_obj) + n = 0 if match_info is None else len(match_info.matched_kp1_xy) + if n < min_rigid_matches: + raise TooFewMatchesError( + f"Only {n} initial keypoint matches between " + f"{img_obj.name} and {prev_img_obj.name} " + f"(threshold {min_rigid_matches}). Continuing would " + "produce a degenerate rigid transform; aborting." + ) # print("\n======== Calculating transformations\n") if registrar.size > 2: registrar.update_match_dicts_with_neighbor_filter(transformer, matcher) if check_for_reflections: - registrar.align_to_prev_check_reflections(transformer=transformer, - matcher_obj=matcher, - valis_obj=valis_obj, - keep_unfiltered=False, - qt_emitter=qt_emitter) + registrar.align_to_prev_check_reflections( + transformer=transformer, + matcher_obj=matcher, + valis_obj=valis_obj, + keep_unfiltered=False, + qt_emitter=qt_emitter, + ) else: registrar.align_to_prev(transformer=transformer, qt_emitter=qt_emitter) # Check current output shape. If too large, then registration failed for img_obj in registrar.img_obj_list: s = transform.SimilarityTransform(img_obj.M).scale - if s >= max_scaling or s <= 1/max_scaling: - print(Warning(f"Max allowed scaling is {max_scaling},\ - but was calculated as being {s}.\ - Registration failed. Maybe try using the Euclidean transform.")) + if s >= max_scaling or s <= 1 / max_scaling: + logger.error( + f"Max allowed scaling is {max_scaling}, " + f"but was calculated as being {s}. " + f"Registration failed. Maybe try using the Euclidean transform." + ) return False if affine_optimizer is not None: @@ -1871,21 +2293,24 @@ def register_images(img_dir, dst_dir=None, name="registrar", # print("\n======== Saving results\n") pickle_file = os.path.join(registered_data_dir, name + "_registrar.pickle") - pickle.dump(registrar, open(pickle_file, 'wb')) + pickle.dump(registrar, open(pickle_file, "wb")) n_digits = len(str(registrar.size)) for img_obj in registrar.img_obj_list: - f_out = "".join([str.zfill(str(img_obj.stack_idx), n_digits), - "_", img_obj.name, ".png"]) + f_out = "".join( + [str.zfill(str(img_obj.stack_idx), n_digits), "_", img_obj.name, ".png"] + ) - io.imsave(os.path.join(registered_img_dir, f_out), - img_obj.registered_img.astype(np.uint8)) + io.imsave( + os.path.join(registered_img_dir, f_out), + img_obj.registered_img.astype(np.uint8), + ) registrar.clear_unused_matches() toc = time() elapsed = toc - tic time_string, time_units = valtils.get_elapsed_time_string(elapsed) - print(f"\n======== Rigid registration complete in {time_string} {time_units}\n") + logger.info(f"Rigid registration complete in {time_string} {time_units}") return registrar diff --git a/valis/slide_io.py b/src/valis/slide_io.py similarity index 56% rename from valis/slide_io.py rename to src/valis/slide_io.py index a2df2abd..8b557264 100644 --- a/valis/slide_io.py +++ b/src/valis/slide_io.py @@ -1,8 +1,8 @@ -"""Methods and classes to read and write slides in the .ome.tiff format +"""Methods and classes to read and write slides in the .ome.tiff format""" -""" import torch import kornia +import logging import os from skimage import io, transform @@ -18,15 +18,13 @@ from statistics import mode import time import sys -import re import itertools import xml.etree.ElementTree as elementTree import unicodedata import ome_types -import jpype from aicspylibczi import CziFile +from tifffile import TiffFile from tqdm import tqdm -import scyjava from difflib import get_close_matches import traceback @@ -34,8 +32,8 @@ from . import valtils from . import slide_tools from . import warp_tools -from . import valtils +logger = logging.getLogger(__name__) pyvips.cache_set_max(0) @@ -52,9 +50,6 @@ MAX_TILE_SIZE = 2**10 """int: maximum tile used to read or write images""" -BF_RDR = "bioformats" -"""str: Name of Bioformats reader.""" - VIPS_RDR = "libvips" """str: Name of pyvips reader""" @@ -67,10 +62,21 @@ PIXEL_UNIT = "pixel" """str: Physical unit when the unit can't be found in the metadata""" -MICRON_UNIT = u'\u00B5m' +MICRON_UNIT = "\u00b5m" """str: Phyiscal unit for micron/micrometers""" -ALL_OPENSLIDE_READABLE_FORMATS = [".svs", ".tif", ".vms", ".vmu", ".ndpi", ".scn", ".mrxs", ".tiff", ".svslide", ".bif"] +ALL_OPENSLIDE_READABLE_FORMATS = [ + ".svs", + ".tif", + ".vms", + ".vmu", + ".ndpi", + ".scn", + ".mrxs", + ".tiff", + ".svslide", + ".bif", +] """list: File extensions that OpenSlide can read""" @@ -83,354 +89,28 @@ # ".nii", ".nii.gz", # ".dzi" ".xml", ".dcm", ".ome.tiff", ".ome.tif"] -VIPS_READABLE_FORMATS = [*pyvips.get_suffixes(), *ALL_OPENSLIDE_READABLE_FORMATS, ".ome.tiff", ".ome.tif"] +VIPS_READABLE_FORMATS = [ + *pyvips.get_suffixes(), + *ALL_OPENSLIDE_READABLE_FORMATS, + ".ome.tiff", + ".ome.tif", +] """list: File extensions that libvips can read. See https://github.com/libvips/libvips """ -VIPS_RGB_FORMATS = [x.lower() for x in dir(pyvips.enums.Interpretation) if re.search("rgb", x.lower()) is not None] +VIPS_RGB_FORMATS = [ + x.lower() + for x in dir(pyvips.enums.Interpretation) + if re.search("rgb", x.lower()) is not None +] """list: List of libvips rgb formats """ -BF_FORMAT_F = os.path.join(os.path.split(__file__)[0], "data/bf_formats.txt") - - -def read_bf_formats(): - bf_formats = open(BF_FORMAT_F, "r").read().split("\n") - - return bf_formats - - -if os.path.exists(BF_FORMAT_F): - BF_READABLE_FORMATS = read_bf_formats() -else: - BF_READABLE_FORMATS = None -"""list: File extensions that Bioformats can read. - Filled in after initializing JVM""" - -ALL_READABLE_FORMATS = list(set([*VIPS_READABLE_FORMATS, *BF_READABLE_FORMATS])) -OPENSLIDE_ONLY = None -"""list: File extensions that OpenSlide can read but Bioformats can't. - Filled in after initializingJVM""" - -FormatTools = None -"""Bioformats FormatTools. - Created after initializing JVM""" - -BF_UNIT = None -"""Bioformats UNITS. - Created after initializing JVM.""" - -BF_MICROMETER = None -"""Bioformats Unit mircometer object. - Created after initializing JVM.""" - -ome = None -"""Bioformats ome from bioforamts_jar. - Created after initializing JVM.""" - -loci = None -"""Bioformats loci from bioforamts_jar. - Created after initializing JVM.""" +ALL_READABLE_FORMATS = VIPS_READABLE_FORMATS OME_TYPES_PARSER = "lxml" -""" -NOTE: Commented out block is how to use boformats with javabrdige. -However, on conda, javabridge isn't available for python 3.9. -If using, remember to put bftools/bioformats_package.jar in the -source directory. - -Keeping the code just in case need to use javabridge again. -""" -# Bioformats + Javabridge # -#---------------------------------------# -# -# try: -# bf_jar = os.path.join(pathlib.Path(__file__).parent, "bftools/bioformats_package.jar") -# except Exception: -# # Running interactively -# bf_jar = os.path.join(os.getcwd(), "bftools/bioformats_package.jar") -# -# -# def init_jvm_javabridge(): -# """Initialize JVM for BioFormats -# """ -# -# if javabridge.get_env() is None: -# -# all_jars = javabridge.JARS + [bf_jar] -# javabridge.start_vm(class_path=all_jars, max_heap_size="10G", run_headless=True) -# -# myloglevel = "ERROR" -# rootLoggerName = javabridge.get_static_field("org/slf4j/Logger", "ROOT_LOGGER_NAME", "Ljava/lang/String;") -# rootLogger = javabridge.static_call("org/slf4j/LoggerFactory", "getLogger", -# "(Ljava/lang/String;)Lorg/slf4j/Logger;", rootLoggerName) -# logLevel = javabridge.get_static_field("ch/qos/logback/classic/Level", myloglevel, "Lch/qos/logback/classic/Level;") -# javabridge.call(rootLogger, "setLevel", "(Lch/qos/logback/classic/Level;)V", logLevel) -# -# msg = "JVM has been initialize. Be sure to call valis.kill_jvm() or slide_io.kill_jvm() at the end of your script" -# valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) -# -# # Fill in global variables that can only be created after initializing the JVM -# -# global FormatTools -# global BF_UNIT -# global BF_MICROMETER -# global BF_READABLE_FORMATS -# global OPENSLIDE_ONLY -# -# FormatTools = javabridge.JClassWrapper("loci.formats.FormatTools") -# BF_UNIT = javabridge.JClassWrapper("ome.units.UNITS") -# BF_MICROMETER = BF_UNIT.MICROMETER -# BF_READABLE_FORMATS = get_bf_readable_formats_javabridge() -# OPENSLIDE_ONLY = list(set(ALL_OPENSLIDE_READABLE_FORMATS).difference(set(BF_READABLE_FORMATS))) -# -# -# def kill_jvm_javabridge(): -# """Kill JVM for BioFormats -# """ -# javabridge.kill_vm() -# msg = "JVM has been killed. If this was due to an error, then a new Python session will need to be started" -# valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) -# -# -# def get_bf_readable_formats_javabridge(): -# """Get extensions of formats that BioFormats can read -# """ -# if javabridge.get_env() is None: -# init_jvm_javabridge() -# -# env = javabridge.get_env() -# base_reader = javabridge.make_instance('loci/formats/ImageReader', '()V') -# readers = javabridge.jutil.call(base_reader, 'getReaders', -# '()[Lloci/formats/IFormatReader;') -# all_readers = env.get_object_array_elements(readers) -# readable_formats = [] -# f_append = readable_formats.append -# for format_reader in all_readers: -# j_suffixes = javabridge.get_env().get_object_array_elements( -# javabridge.jutil.call( -# format_reader, 'getSuffixes', -# '()[Ljava/lang/String;')) -# -# for js in j_suffixes: -# suffix = javabridge.to_string(js) -# if len(suffix) > 0: -# f_append("." + suffix) -# -# javabridge.jutil.call(base_reader, 'close', '()V') -# -# return readable_formats -#---------------------------------------# - -# Bioformats/scyjava + Jpype # -#--------------------# - - -def init_jvm(jar=None, mem_gb=10): - """Initialize JVM for BioFormats - - Parameters - ---------- - mem_gb : int - Amount of memory, in GB, for JVM - """ - import jpype - if not jpype.isJVMStarted(): - global FormatTools - global BF_MICROMETER - global OPENSLIDE_ONLY - global BF_READABLE_FORMATS - global ome - global loci - - if jar is None: - - # Check if jar is bundled with source code, like in a Docker image - # Can use instead of using maven to download, which requires an unblocked connection - parent_dir = pathlib.Path(__file__).parent.resolve() - local_bf_jar = os.path.join(parent_dir, "bioformats_package.jar") - if os.path.exists(local_bf_jar): - jar = local_bf_jar - - if jar is not None: - jpype.addClassPath(jar) - jpype.startJVM(f"-Djava.awt.headless=true -Xmx{mem_gb}G", classpath=jar) - - else: - scyjava.config.endpoints.extend(['ome:formats-gpl', 'ome:jxrlib-all']) - scyjava.start_jvm([f"-Xmx{mem_gb}G"]) - - loci = jpype.JPackage("loci") - ome = jpype.JPackage("ome") - loci.common.DebugTools.setRootLevel("ERROR") - - FormatTools = loci.formats.FormatTools - BF_MICROMETER = ome.units.UNITS.MICROMETER - BF_READABLE_FORMATS = get_bf_readable_formats() - OPENSLIDE_ONLY = list(set(ALL_OPENSLIDE_READABLE_FORMATS).difference(set(BF_READABLE_FORMATS))) - - # Save formats in case using a different version of Bioformats - bf_data_parent_dir = os.path.split(BF_FORMAT_F)[0] - pathlib.Path(bf_data_parent_dir).mkdir(exist_ok=True, parents=True) - with open(BF_FORMAT_F, "w") as f: - for line in BF_READABLE_FORMATS: - f.write(f"{line}\n") - - msg = (f"JVM has been initialized. " - f"Be sure to call registration.kill_jvm() " - f"or slide_io.kill_jvm() at the end of your script.") - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - -def kill_jvm(): - """Kill JVM for BioFormats - """ - try: - scyjava.shutdown_jvm() - msg = "JVM has been killed. If this was due to an error, then a new Python session will need to be started" - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - except NameError: - pass - - -def get_bioformats_version(): - v = loci.formats.FormatTools.VERSION - - return v - - -def get_bf_readable_formats(): - """Get extensions of formats that BioFormats can read - - Returns - ------- - readable_formats : list of str - List of formats that can be read by Bioformats - - """ - - if not jpype.isJVMStarted(): - init_jvm() - - baseReader = loci.formats.ImageReader() - readers = baseReader.getReaders() - read_range = range(1, readers.length) - readable_formats = ["." + str(f) for l in [list(readers[i].getSuffixes()) for i in read_range] for f in l if len(f) > 0] - baseReader.close() - - return readable_formats - - -def bf_to_numpy_dtype(bf_pixel_type, little_endian): - """Get numpy equivalent of the bioformats pixel type - - Adapted from the python-bioformats package - - Parameters - ---------- - bf_pixel_type : int - Integer indicating the Bioformats pixel type - - little_endian : bool - Whether or not the image is little endian - - Returns - ------- - dtype : numpy.dtype - Numpy dtype - - scale : int - Maximum value of `dtype` - - """ - - if bf_pixel_type == FormatTools.INT8: - # FormatTools.INT8 = 0 - dtype = np.int8 - scale = 255 - - elif bf_pixel_type == FormatTools.UINT8: - # FormatTools.UINT8 = 1 - dtype = np.uint8 - scale = 255 - - elif bf_pixel_type == FormatTools.UINT16: - # FormatTools.UINT16 = 3 - dtype = 'u2' - scale = 65535 - - elif bf_pixel_type == FormatTools.INT16: - # FormatTools.INT16 = 2 - dtype = 'i2' - scale = 65535 - - elif bf_pixel_type == FormatTools.UINT32: - # FormatTools.UINT32 = 5 - dtype = 'u4' - scale = 2**32 - - elif bf_pixel_type == FormatTools.INT32: - # FormatTools.INT32 = 4 - dtype = 'i4' - scale = 2**32-1 - - elif bf_pixel_type == FormatTools.FLOAT: - # FormatTools.FLOAT = 6 - dtype = 'f4' - scale = 1 - - elif bf_pixel_type == FormatTools.DOUBLE: - # FormatTools.DOUBLE = 7 - dtype = 'f8' - scale = 1 - - return dtype, scale - - -def vips2bf_dtype(vips_format): - """Get bioformats equivalent of the pyvips pixel type - - Parameters - ---------- - vips_format : str - Format of the pyvips.Image - - Returns - ------- - bf_dtype : str - String format of Bioformats datatype - - """ - - np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_format] - bf_dtype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] - - return bf_dtype - - -def bf2vips_dtype(bf_dtype): - """Get bioformats equivalent of the pyvips pixel type - - Parameters - ---------- - bf_dtype : str - String format of Bioformats datatype - - Returns - ------- - vips_format : str - Format of the pyvips.Image - - """ - - np_type = slide_tools.BF_FORMAT_NUMPY_DTYPE[bf_dtype] - vips_format = slide_tools.NUMPY_FORMAT_VIPS_DTYPE[np_type] - - return vips_format - def check_czi_jpegxr(src_f): f_extension = slide_tools.get_slide_extension(src_f) @@ -474,7 +154,8 @@ def check_to_use_openslide(src_f): use_openslide = True except pyvips.error.Error as e: traceback_msg = traceback.format_exc() - valtils.print_warning(e, traceback_msg=traceback_msg) + logger.warning(e) + logger.warning(traceback_msg) return use_openslide @@ -511,26 +192,20 @@ def get_ome_obj(x): except Exception as e: # Sometimes the image description found by `ome_types.from_tiff` # does not contain the ome-xml. Seems to be the case for ImageJ exports, at least - if is_file: - try: - bf_rdr, bf_meta = get_bioformats_reader_and_meta(x) - meta_xml = bf_meta.dumpXML() - meta_xml = str(meta_xml) - ome_obj = ome_types.from_xml(meta_xml) - if len(ome_obj.images) == 0: - ome_obj = ome_types.from_xml(meta_xml, parser=OME_TYPES_PARSER) - - except Exception as e: - if ome_fxn == ome_types.from_tiff: - valtils.print_warning(f"Could not get OME-XML for image {x}, due to the following error: {e}") - else: - valtils.print_warning(f"Could not get OME-XML, due to the following error: {e}") + if ome_fxn == ome_types.from_tiff: + logger.warning( + f"Could not get OME-XML for image {x}, due to the following error: {e}" + ) + else: + logger.warning(f"Could not get OME-XML, due to the following error: {e}") return ome_obj def check_is_ome(src_f): - is_ome = re.search(".ome", src_f) is not None and re.search(".tif*", src_f) is not None + is_ome = ( + re.search(".ome", src_f) is not None and re.search(".tif*", src_f) is not None + ) if is_ome: # Verify that image is valid ome.tiff ome_obj = get_ome_obj(src_f) @@ -548,47 +223,14 @@ def check_to_use_vips(src_f): return can_use_pyvips -def check_to_use_bioformats(src_f, series=None): - """Check if bioformats can be used to read metadata and/or image - - """ - img_format = slide_tools.get_slide_extension(src_f) - use_bf = img_format in BF_READABLE_FORMATS - - can_get_metadata = use_bf - can_read_img = use_bf - if use_bf: - - err_msg = f"Error using Bioformats to read {os.path.split(src_f)[-1]}. Will try to use a different reader" - try: - # Try to get metadata - bf_reader = BioFormatsSlideReader(src_f, series=series) - except Exception as e: - valtils.print_warning(err_msg) - can_get_metadata = False - can_read_img = False - - if can_get_metadata: - try: - # Can get metadata, try reading small slice - test_read_level = len(bf_reader.metadata.slide_dimensions) - 1 - bf_reader.slide2vips(level=test_read_level, xywh=(0, 0, 5, 5)) - except Exception as e: - valtils.print_warning(err_msg) - can_read_img = False - - return can_get_metadata, can_read_img - - -def check_flattened_pyramid_tiff(src_f, check_with_bf=False): +def check_flattened_pyramid_tiff(src_f): """Determine if a tiff is a flattened pyramid Determines if a slide is pyramid where each page/plane is a channel in the pyramid. An example would be one where the plane dimensions are something like [(600, 600), (600, 600), (600, 600), (300, 300), (300, 300), (300, 300)] - for a 3 channel image with 2 pyramid levels. It seems that bioformats - does not recognize these as pyramid images. + for a 3 channel image with 2 pyramid levels. Parameters ---------- @@ -600,9 +242,6 @@ def check_flattened_pyramid_tiff(src_f, check_with_bf=False): is_flattended_pyramid : bool Whether or not the slide is a flattened pyramid - can_use_bf : bool - Whether or not Bioformats will read the slide in the same way - slide_dimensions : ndarray Dimensions (width, height) for each level in the pyramid @@ -619,9 +258,8 @@ def check_flattened_pyramid_tiff(src_f, check_with_bf=False): vips_fields = vips_img.get_fields() is_flattended_pyramid = False - can_use_bf = False - if 'n-pages' in vips_fields: + if "n-pages" in vips_fields: n_pages = vips_img.get("n-pages") all_areas = [] all_dims = [] @@ -632,13 +270,13 @@ def check_flattened_pyramid_tiff(src_f, check_with_bf=False): try: page = pyvips.Image.new_from_file(src_f, page=i) except pyvips.error.Error as e: - print(f"error at page {i}: {e}") + logger.error(f"error at page {i}: {e}") continue w = page.width h = page.height nc = page.bands - img_area = w*h*nc + img_area = w * h * nc all_areas.append(img_area) all_dims.append([w, h]) @@ -665,15 +303,18 @@ def check_flattened_pyramid_tiff(src_f, check_with_bf=False): if is_flattended_pyramid: nchannels_per_each_level = np.diff(level_starts) - last_level_channel_count = np.sum(all_n_channels[level_starts[-1]:]) - nchannels_per_each_level = np.hstack([nchannels_per_each_level, - last_level_channel_count]) + last_level_channel_count = np.sum(all_n_channels[level_starts[-1] :]) + nchannels_per_each_level = np.hstack( + [nchannels_per_each_level, last_level_channel_count] + ) if last_level_channel_count == 3 and nchannels_per_each_level[0] != 3: # last level is probably a thumbnail nchannels_per_each_level = nchannels_per_each_level[:-1] n_channels = mode(nchannels_per_each_level) - levels_start_idx = level_starts[np.where(nchannels_per_each_level==n_channels)[0]] + levels_start_idx = level_starts[ + np.where(nchannels_per_each_level == n_channels)[0] + ] slide_dimensions = np.array(all_dims)[levels_start_idx] else: @@ -682,20 +323,18 @@ def check_flattened_pyramid_tiff(src_f, check_with_bf=False): n_channels = most_common_channel_count else: - return is_flattended_pyramid, can_use_bf, None, None, None - # Now check if Bioformats reads it similarly # - if check_with_bf: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(src_f) - bf_levels = len(bf_reader.metadata.slide_dimensions) - bf_channels = bf_reader.metadata.n_channels - can_use_bf = bf_levels >= len(slide_dimensions) and bf_channels == n_channels + return is_flattended_pyramid, None, None, None - return is_flattended_pyramid, can_use_bf, slide_dimensions, levels_start_idx, n_channels + return ( + is_flattended_pyramid, + slide_dimensions, + levels_start_idx, + n_channels, + ) def check_xml_img_match(xml, vips_img, metadata, series=0): - """ Make sure that provided xml and image match. + """Make sure that provided xml and image match. If there is a mismatch (i.e. channel number), the values in the image take precedence """ ome_obj = get_ome_obj(xml) @@ -708,10 +347,12 @@ def check_xml_img_match(xml, vips_img, metadata, series=0): ome_size_y = ome_img.size_y ome_dtype = ome_img.type.name.lower() - total_pages = ome_nc*ome_nt*ome_nz + total_pages = ome_nc * ome_nt * ome_nz else: - msg = f"ome-xml for {metadata.name} does not contain any metadata for any images" - valtils.print_warning(msg) + msg = ( + f"ome-xml for {metadata.name} does not contain any metadata for any images" + ) + logger.warning(msg) ome_nc = None ome_nt = None ome_nz = None @@ -723,55 +364,31 @@ def check_xml_img_match(xml, vips_img, metadata, series=0): vips_nc = vips_img.bands vips_size_x = vips_img.width vips_size_y = vips_img.height - np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_img.format] - vips_bf_dtype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)].lower() if total_pages != vips_img.bands: - msg = (f"For {metadata.name}, the ome-xml states there should be {total_pages} pages, but there is/are {vips_img.bands} pages in the image", - f"Assuming that all pages are channels (not time points or Z-axes), so updating the metadata to have {total_pages} channels") - metadata.n_channels = vips_nc - metadata.n_t = 1 - metadata.n_z = 1 - if ome_nc is not None : - valtils.print_warning(msg) + msg = ( + f"For {metadata.name}, the ome-xml states there should be {total_pages} pages, but there is/are {vips_img.bands} pages in the image", + f"Assuming that all pages are channels (not time points or Z-axes), so updating the metadata to have {total_pages} channels", + ) + metadata.n_channels = vips_nc + metadata.n_t = 1 + metadata.n_z = 1 + if ome_nc is not None: + logger.warning(msg) if ome_size_x != vips_size_x: msg = f"For {metadata.name}, the ome-xml states the width should be {ome_size_x}, but the image has a width of {vips_size_x}" if ome_size_x is not None: - valtils.print_warning(msg) + logger.warning(msg) if ome_size_y != vips_size_y: msg = f"For {metadata.name}, the ome-xml states the height should be {ome_size_y}, but the image has a width of {vips_size_y}" if ome_size_y is not None: - valtils.print_warning(msg) - - if ome_dtype != vips_bf_dtype: - msg = f"For {metadata.name}, the ome-xml states the image type should be {ome_dtype}, but the image has type of {vips_bf_dtype}" - metadata.bf_datatype = vips_bf_dtype - metadata.bf_pixel_type = slide_tools.BF_DTYPE_PIXEL_TYPE[vips_bf_dtype] - - if ome_dtype is not None: - valtils.print_warning(msg) + logger.warning(msg) return metadata -def get_bioformats_reader_and_meta(src_f): - init_jvm() - rdr = loci.formats.ImageReader() - factory = loci.common.services.ServiceFactory() - OMEXMLService_class = loci.formats.services.OMEXMLService - - service = factory.getInstance(OMEXMLService_class) - ome_meta = service.createOMEXMLMetadata() - rdr.setMetadataStore(ome_meta) - rdr.setFlattenedResolutions(False) - rdr.setId(src_f) - meta = rdr.getMetadataStore() - - return rdr, meta - - def metadata_from_xml(xml, name, server, series=0, metadata=None): """ Use ome-types to extract metadata from xml. @@ -792,15 +409,23 @@ def metadata_from_xml(xml, name, server, series=0, metadata=None): samples_per_pixel = ome_img.pixels.channels[0].samples_per_pixel if samples_per_pixel is None: samples_per_pixel = ome_img.pixels.size_c - metadata.is_rgb = samples_per_pixel == 3 and \ - ome_img.pixels.type.value == 'uint8' and \ - len(ome_img.pixels.channels) == 1 + metadata.is_rgb = ( + samples_per_pixel == 3 + and ome_img.pixels.type.value == "uint8" + and len(ome_img.pixels.channels) == 1 + ) else: # No channel info, so guess based on image shape and datatype - metadata.is_rgb = ome_img.pixels.type.value == 'uint8' and ome_img.pixels.size_c == 3 + metadata.is_rgb = ( + ome_img.pixels.type.value == "uint8" and ome_img.pixels.size_c == 3 + ) if ome_img.pixels.physical_size_x is not None: - metadata.pixel_physical_size_xyu = (ome_img.pixels.physical_size_x, ome_img.pixels.physical_size_y, MICRON_UNIT) + metadata.pixel_physical_size_xyu = ( + ome_img.pixels.physical_size_x, + ome_img.pixels.physical_size_y, + MICRON_UNIT, + ) else: metadata.pixel_physical_size_xyu = (1, 1, PIXEL_UNIT) @@ -808,29 +433,33 @@ def metadata_from_xml(xml, name, server, series=0, metadata=None): metadata.n_z = ome_img.pixels.size_z metadata.n_t = ome_img.pixels.size_t metadata.original_xml = ome_info.to_xml() - metadata.bf_datatype = ome_img.pixels.type.value - metadata.bf_pixel_type = slide_tools.BF_DTYPE_PIXEL_TYPE[metadata.bf_datatype] if not metadata.is_rgb: if has_channel_info: - metadata.channel_names = [ome_img.pixels.channels[i].name for i in range(metadata.n_channels)] - metadata.channel_names = check_channel_names(metadata.channel_names, metadata.is_rgb, metadata.n_channels, src_f=name) + metadata.channel_names = [ + ome_img.pixels.channels[i].name for i in range(metadata.n_channels) + ] + metadata.channel_names = check_channel_names( + metadata.channel_names, metadata.is_rgb, metadata.n_channels, src_f=name + ) else: - metadata.channel_names = get_default_channel_names(metadata.n_channels, src_f=name) + metadata.channel_names = get_default_channel_names( + metadata.n_channels, src_f=name + ) return metadata def openslide_desc_2_omexml(vips_img): - """Get basic metatad using openslide and convert to ome-xml - - """ - assert "openslide.vendor" in vips_img.get_fields(), "image does not appear to be openslide metadata" + """Get basic metatad using openslide and convert to ome-xml""" + assert ( + "openslide.vendor" in vips_img.get_fields() + ), "image does not appear to be openslide metadata" img_shape_wh = warp_tools.get_shape(vips_img)[0:2][::-1] x, y, z, c, t = get_shape_xyzct(shape_wh=img_shape_wh, n_channels=vips_img.bands) np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_img.format] - bf_datatype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] + ome_datatype = slide_tools.NUMPY_FORMAT_OME_DTYPE[str(np_dtype().dtype)] new_img = ome_types.model.Image( id=f"Image:0", @@ -841,20 +470,20 @@ def openslide_desc_2_omexml(vips_img): size_z=z, size_c=c, size_t=t, - type=bf_datatype, - dimension_order='XYZCT', - physical_size_x = eval(vips_img.get('openslide.mpp-x')), - physical_size_x_unit = MICRON_UNIT, - physical_size_y = eval(vips_img.get('openslide.mpp-y')), - physical_size_y_unit = MICRON_UNIT, - metadata_only=True - ) + type=ome_datatype, + dimension_order="XYZCT", + physical_size_x=eval(vips_img.get("openslide.mpp-x")), + physical_size_x_unit=MICRON_UNIT, + physical_size_y=eval(vips_img.get("openslide.mpp-y")), + physical_size_y_unit=MICRON_UNIT, + metadata_only=True, + ), ) # Should always be rgb, but checking anyway is_rgb = vips_img.interpretation in VIPS_RGB_FORMATS if is_rgb: - rgb_channel = ome_types.model.Channel(id='Channel:0:0', samples_per_pixel=3) + rgb_channel = ome_types.model.Channel(id="Channel:0:0", samples_per_pixel=3) new_img.pixels.channels = [rgb_channel] new_ome = ome_types.OME() @@ -865,7 +494,6 @@ def openslide_desc_2_omexml(vips_img): return img_xml - # Read slides # class MetaData(object): """Store slide metadata @@ -902,9 +530,6 @@ class MetaData(object): original_xml : str Xml string created by bio-formats - bf_datatype : str - String indicating bioformats image datatype - optimal_tile_wh : int Tile width and height used to open and/or save image @@ -937,8 +562,6 @@ def __init__(self, name, server, series=0): self.n_z = 1 self.n_t = 1 self.original_xml = None - self.bf_datatype = None - self.bf_pixel_type = None self.is_little_endian = None self.optimal_tile_wh = 1024 @@ -1052,17 +675,19 @@ def scale_physical_size(self, level): level_0_shape = self.metadata.slide_dimensions[0] level_shape = self.metadata.slide_dimensions[level] - scale_x = level_0_shape[0]/level_shape[0] - scale_y = level_0_shape[1]/level_shape[1] + scale_x = level_0_shape[0] / level_shape[0] + scale_y = level_0_shape[1] / level_shape[1] - level_xy_per_px = (scale_x * self.metadata.pixel_physical_size_xyu[0], - scale_y * self.metadata.pixel_physical_size_xyu[1], - self.metadata.pixel_physical_size_xyu[2]) + level_xy_per_px = ( + scale_x * self.metadata.pixel_physical_size_xyu[0], + scale_y * self.metadata.pixel_physical_size_xyu[1], + self.metadata.pixel_physical_size_xyu[2], + ) return level_xy_per_px def create_metadata(self): - """ Create and fill in a MetaData object + """Create and fill in a MetaData object Returns ------- @@ -1083,15 +708,17 @@ def get_channel_index(self, channel): matching_channel_idx = cnames.index(best_match) if best_match.upper() != channel.upper(): msg = f"Cannot find exact match to channel '{channel}' in {valtils.get_name(self.src_f)}. Using channel {best_match}" - valtils.print_warning(msg) + logger.warning(msg) except Exception as e: traceback_msg = traceback.format_exc() matching_channel_idx = 0 - msg = (f"Cannot find channel '{channel}' in {valtils.get_name(self.src_f)}." - f" Available channels are {self.metadata.channel_names}." - f" Using channel number {matching_channel_idx}, which has name {self.metadata.channel_names[matching_channel_idx]}") + msg = ( + f"Cannot find channel '{channel}' in {valtils.get_name(self.src_f)}." + f" Available channels are {self.metadata.channel_names}." + f" Using channel number {matching_channel_idx}, which has name {self.metadata.channel_names[matching_channel_idx]}" + ) - valtils.print_warning(msg) + logger.warning(msg) else: matching_channel_idx = 0 @@ -1163,465 +790,16 @@ def _get_pixel_physical_size(self, *args, **kwargs): Returns ------- res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' + Physical size per pixel and the unit, e.g. u'\u00b5m' Notes ----- - If physical unit is micron, it must be u'\u00B5m', + If physical unit is micron, it must be u'\u00b5m', not mu (u'\u03bcm') or u. """ -class BioFormatsSlideReader(SlideReader): - """Read slides using BioFormats - - Uses the packages jpype and bioformats-jar - - """ - def __init__(self, src_f, series=None, *args, **kwargs): - """ - Parameters - ----------- - src_f : str - Path to slide - - series : int - The series to be read. If `series` is None, the the `series` - will be set to the series associated with the largest image. - - """ - - init_jvm() - - self.meta_list = [None] - super().__init__(src_f=src_f, *args, **kwargs) - - try: - self.meta_list = self.create_metadata() - except Exception as e: - traceback_msg = traceback.format_exc() - valtils.print_warning(e, traceback_msg=traceback_msg) - kill_jvm() - - self.n_series = len(self.meta_list) - if series is None: - img_areas = [np.multiply(*meta.slide_dimensions[0]) for meta in self.meta_list] - series = np.argmax(img_areas) - if len(img_areas) > 1: - msg = (f"No series provided. " - f"Selecting series with largest image, " - f"which is series {series}") - - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) - - self._series = series - self.series = series - - def _set_series(self, series): - self._series = series - self.metadata = self.meta_list[series] - - def _get_series(self): - return self._series - - series = property(fget=_get_series, - fset=_set_series, - doc="Slide series") - - def get_tiles_parallel(self, level, tile_bbox_list, pixel_type, series=0, z=0, t=0): - """Get tiles to slice from the slide - - """ - - n_tiles = len(tile_bbox_list) - tile_array = [None] * n_tiles - - def tile2vips_threaded(idx): - xywh = tile_bbox_list[idx] - # javabridge.attach() - # jpype.attachThreadToJVM() - jpype.java.lang.Thread.attach() - try: - tile = self.slide2image(level, series, xywh=tuple(xywh), z=z, t=t) - except Exception as e: - traceback_msg = traceback.format_exc() - valtils.print_warning(e, traceback_msg=traceback_msg, rgb=Fore.RED) - pass - # javabridge.detach() - # jpype.detachThreadFromJVM() - jpype.java.lang.Thread.detach() - - tile_array[idx] = slide_tools.numpy2vips(tile, self.metadata.pyvips_interpretation) - - n_cpu = valtils.get_ncpus_available() - 1 - res = pqdm(range(n_tiles), tile2vips_threaded, n_jobs=n_cpu, unit="tiles", leave=None) - - return tile_array - - def slide2vips(self, level, series=None, xywh=None, tile_wh=None, z=0, t=0, *args, **kwargs): - """Convert slide to pyvips.Image - - This method uses Bioformats to slice tiles from the slides, and then - stitch them together using pyvips. - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 0 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - tile_wh : int, optional - Size of tiles used to contstruct `vips_slide` - - Returns - ------- - vips_slide : pyvips.Image - An of the slide or the region defined by xywh - - """ - - if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}") - level = max(0, level) - - if series is None: - series = self.series - - else: - self.series = series - - rdr, meta = self._get_bf_objects() - pixel_type, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - slide_shape_wh = self.metadata.slide_dimensions[level] - - if tile_wh is None: - tile_wh = rdr.getOptimalTileWidth() - rdr.close() - - tile_wh = min(tile_wh, MAX_TILE_SIZE) - if np.any(slide_shape_wh < tile_wh): - tile_wh = min(slide_shape_wh) - - tile_bbox = warp_tools.get_grid_bboxes(slide_shape_wh[::-1], - tile_wh, tile_wh, inclusive=True) - - n_across = len(np.unique(tile_bbox[:, 0])) - - print(f"Converting slide to pyvips image") - vips_slide = pyvips.Image.arrayjoin( - self.get_tiles_parallel(level, tile_bbox_list=tile_bbox, pixel_type=pixel_type, series=series, z=z, t=t), - across=n_across).crop(0, 0, *slide_shape_wh) - if xywh is not None: - vips_slide = vips_slide.extract_area(*xywh) - - return vips_slide - - def slide2image(self, level, series=None, xywh=None, z=0, t=0, *args, **kwargs): - """Convert slide to image - - Parameters - ----------- - level : int - Pyramid level - - series : int, optional - Series number. Defaults to 1 - - xywh : tuple of int, optional - The region to be sliced from the slide. If None, - then the entire slide will be converted. Otherwise - xywh is the (top left x, top left y, width, height) of - the region to be sliced. - - Returns - ------- - img : ndarray - An image of the slide or the region defined by xywh - - """ - if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") - level = max(0, level) - - if series is None: - series = self.series - - else: - self.series = series - - rdr, meta = self._get_bf_objects() - - rdr.setSeries(series) - rdr.setResolution(level) - if xywh is None: - x = 0 - y = 0 - w = rdr.getSizeX() - h = rdr.getSizeY() - xywh = (x, y, w, h) - - if rdr.isRGB(): - img = self._read_rgb(rdr=rdr, xywh=xywh, z=z, t=t) - - else: - img = self._read_multichannel(rdr=rdr, xywh=xywh, z=z, t=t) - - rdr.close() - - return img - - def create_metadata(self): - rdr, meta = self._get_bf_objects() - meta_xml = meta.dumpXML() - try: - n_series = rdr.getSeriesCount() - i0 = rdr.getSeries() - slide_format = f"{BF_RDR}_{rdr.getFormat()}" - meta_list = [None] * n_series - for i in range(n_series): - rdr.setSeries(i) - series_name = str(meta.getImageName(i)) - temp_name = f"{os.path.split(self.src_f)[1]}_{series_name}".strip("_") - full_name = f"{temp_name}_Series_{i}" - full_name = full_name.replace(" ", "_") - - series_meta = MetaData(full_name, slide_format, series=i) - - series_meta.is_rgb = self._check_rgb(rdr) - series_meta.channel_names = self._get_channel_names(rdr, meta) - series_meta.n_channels = int(rdr.getSizeC()) - series_meta.n_z = rdr.getSizeZ() - series_meta.n_t = rdr.getSizeT() - series_meta.slide_dimensions = self._get_slide_dimensions(rdr) - if series_meta.is_rgb: - series_meta.pyvips_interpretation = 'srgb' - elif series_meta.n_channels == 1: - series_meta.pyvips_interpretation = 'b-w' - else: - series_meta.pyvips_interpretation = 'multiband' - - series_meta.pixel_physical_size_xyu = self._get_pixel_physical_size(rdr, meta) - series_meta.bf_pixel_type = str(rdr.getPixelType()) - series_meta.is_little_endian = rdr.isLittleEndian() - series_meta.original_xml = str(meta_xml) - series_meta.bf_datatype = str(FormatTools.getPixelTypeString(rdr.getPixelType())) - series_meta.optimal_tile_wh = int(rdr.getOptimalTileWidth()) - - meta_list[i] = series_meta - - i0 = rdr.setSeries(i0) - rdr.close() - - except Exception as e: - traceback_msg = traceback.format_exc() - valtils.print_warning(e, traceback_msg=traceback_msg) - rdr.close() - - return meta_list - - def _read_rgb(self, rdr, xywh, z=0, t=0): - - np_dtype, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - buffer = rdr.openBytes(0, *xywh) - img = np.frombuffer(bytes(buffer), np_dtype) - nrgb = rdr.getRGBChannelCount() - _, _, w, h = xywh - - if rdr.isInterleaved(): - img = img.reshape(h, w, nrgb) - else: - img = img.reshape(nrgb, h, w) - img = np.transpose(img, (1, 2, 0)) - - if img.shape[2] > 3: - img = img[0:3] - - return img - - def _read_multichannel(self, rdr, xywh, z=0, t=0): - _, _, w, h = xywh - n_channels = rdr.getSizeC() - np_dtype, drange = bf_to_numpy_dtype(rdr.getPixelType(), - rdr.isLittleEndian()) - - if n_channels > 1: - img = np.zeros((h, w, n_channels), dtype=np_dtype) - else: - img = None - - for i in range(n_channels): - idx = rdr.getIndex(z, i, t) # ZCT - buffer = rdr.openBytes(idx, *xywh) - if img is None: - img = np.frombuffer(bytes(buffer), np_dtype).reshape((h, w)) - else: - img[..., i] = np.frombuffer(bytes(buffer), np_dtype).reshape((h, w)) - - return img - - def _get_bf_objects(self): - """Get Bioformat objects - - Returns - ------- - - rdr : IFormatReader - IFormatReader object that is a property of a bioformats.ImageReader. - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata - - Notes - ----- - Be sure to close rdr with rdr.close() when it's no longer needed - - """ - # Javabridge # - #------------# - # env = javabridge.jutil.get_env() - # rdr = javabridge.JWrapper(javabridge.make_instance( - # 'loci/formats/ImageReader', '()V') - # ) - - # factory = javabridge.JWrapper(javabridge.make_instance( - # 'loci/common/services/ServiceFactory', '()V') - # ) - - # OMEXMLService_class = \ - # env.find_class('loci/formats/services/OMEXMLService').as_class_object() - - # Jpype # - #-------# - - rdr, meta = get_bioformats_reader_and_meta(self.src_f) - - return rdr, meta - - def _check_rgb(self, rdr): - """Determine if image is RGB - - Returns - ------- - is_rgb : bool - Whether or not the image is RGB - - """ - - return rdr.isRGB() - - def _get_slide_dimensions(self, rdr): - """Get dimensions of slide at all pyramid levels - - Parameters - ---------- - rdr : IFormatReader - IFormatReader object - - Returns - ------- - slide_dims : ndarray - Dimensions of all images in the pyramid (width, height). - - Notes - ----- - Using javabridge and python-bioformmats, this can be accessed as follows - ` - bf_slide = bioformats.ImageReader(slide_f) - bf_img_reader = javabridge.JWrapper(bf_slide.rdr.o) - - Or - with bioformats.ImageReader(slide_f) as bf_slide: - bf_img_reader = javabridge.JWrapper(bf_slide.rdr.o) - - """ - - r0 = rdr.getResolution() - n_res = rdr.getResolutionCount() - slide_dims = [None] * n_res - for j in range(n_res): - rdr.setResolution(j) - slide_dims[j] = [rdr.getSizeX(), rdr.getSizeY()] - - slide_dims = np.array(slide_dims) - - rdr.setResolution(r0) - - return slide_dims - - def _get_pixel_physical_size(self, rdr, meta): - """Get resolutions for each series - - Parameters - ---------- - rdr : IFormatReader - IFormatReader object. - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata - - Returns - ------- - res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' - - """ - current_series = rdr.getSeries() - temp_x_res = meta.getPixelsPhysicalSizeX(current_series) - if temp_x_res is not None: - x_res = float(temp_x_res.value(BF_MICROMETER).doubleValue()) - y_res = float(meta.getPixelsPhysicalSizeY(current_series).value(BF_MICROMETER).doubleValue()) - phys_unit = str(BF_MICROMETER.getSymbol()) - else: - x_res = 1 - y_res = 1 - phys_unit = PIXEL_UNIT - - res_xyu = (x_res, y_res, phys_unit) - - return res_xyu - - def _get_channel_names(self, rdr, meta): - """Get channel names of image - Parameters - ---------- - rdr : IFormatReader - IFormatReader object - - meta : loci.formats.ome.OMEPyramidStore - Used to read metadata. - - Returns - ------- - channel_names : list - List of channel names. - - """ - - nc = rdr.getSizeC() - current_series = rdr.getSeries() - if rdr.isRGB(): - channel_names = None - else: - channel_names = [""] * nc - for i in range(nc): - channel_names[i] = str(meta.getChannelName(current_series, i)) - - return channel_names - - class VipsSlideReader(SlideReader): """Read slides using pyvips Pyvips includes OpenSlide and so can read those formats as well. @@ -1643,6 +821,7 @@ class VipsSlideReader(SlideReader): when installing from source. """ + def __init__(self, src_f, *args, **kwargs): super().__init__(src_f=src_f, *args, **kwargs) self.use_openslide = check_to_use_openslide(self.src_f) @@ -1651,19 +830,9 @@ def __init__(self, src_f, *args, **kwargs): self.verify_xml() def create_metadata(self): - vips_img = pyvips.Image.new_from_file(self.src_f) - if "image-description" in vips_img.get_fields(): - is_image_J = re.search("imagej", vips_img.get("image-description").lower()) is not None - if is_image_J: - slide_meta = self._get_metadata_bf() - return slide_meta - - if self.use_openslide: - server = OPENSLIDE_RDR - else: - server = VIPS_RDR + server = VIPS_RDR meta_name = f"{os.path.split(self.src_f)[1]}_Series(0)".strip("_") slide_meta = MetaData(meta_name, server) @@ -1679,10 +848,12 @@ def create_metadata(self): img_xml = self._get_xml(vips_img) if img_xml is not None: try: - slide_meta = metadata_from_xml(xml=img_xml, - name=slide_meta.name, - server=server, - metadata=slide_meta) + slide_meta = metadata_from_xml( + xml=img_xml, + name=slide_meta.name, + server=server, + metadata=slide_meta, + ) except Exception as e: slide_meta = self._get_metadata_vips(slide_meta, vips_img) @@ -1693,46 +864,43 @@ def create_metadata(self): slide_meta.channel_names = None if self.is_ome: - toilet_roll = pyvips.Image.new_from_file(self.src_f, n=-1, subifd=-1) - page = pyvips.Image.new_from_file(self.src_f, n=1, subifd=-1, access='random') - n_pages = toilet_roll.height/page.height + with TiffFile(self.src_f) as tif: + n_pages = len(tif.pages) if n_pages > 1: slide_meta.n_channels = int(n_pages) + else: + slide_meta.n_channels = 1 return slide_meta def verify_xml(self): img_xml = self.metadata.original_xml - if img_xml is not None and not self.use_openslide: + if img_xml is not None and self.is_ome and not self.use_openslide: # Don't check openslide images, as metadata counts alpha channel - try: - ome_info = get_ome_obj(img_xml) - assert len(ome_info.images) > 0 - except: - return None + ome_info = get_ome_obj(img_xml) + assert len(ome_info.images) > 0 read_img = self.slide2vips(0) - self.metadata = check_xml_img_match(xml=img_xml, vips_img=read_img, metadata=self.metadata, series=self.series) + self.metadata = check_xml_img_match( + xml=img_xml, + vips_img=read_img, + metadata=self.metadata, + series=self.series, + ) def _get_metadata_vips(self, slide_meta, vips_img): slide_meta.n_channels = vips_img.bands - slide_meta.channel_names = self._get_channel_names(vips_img, n_channels=slide_meta.n_channels) + slide_meta.channel_names = self._get_channel_names( + vips_img, n_channels=slide_meta.n_channels + ) slide_meta.pixel_physical_size_xyu = self._get_pixel_physical_size(vips_img) - np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_img.format] - slide_meta.bf_datatype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] - slide_meta.bf_pixel_type = slide_tools.BF_DTYPE_PIXEL_TYPE[slide_meta.bf_datatype] slide_meta.is_little_endian = sys.byteorder.startswith("l") slide_meta.original_xml = self._get_xml(vips_img) - slide_meta.optimal_tile_wh = get_tile_wh(self, 0, warp_tools.get_shape(vips_img)[0:2][::-1]) + slide_meta.optimal_tile_wh = get_tile_wh( + self, 0, warp_tools.get_shape(vips_img)[0:2][::-1] + ) return slide_meta - def _get_metadata_bf(self): - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - - return bf_reader.metadata - def _slide2vips_ome_one_series(self, level, *args, **kwargs): """Use pyvips to read an ome.tiff image that has only 1 series @@ -1758,14 +926,16 @@ def _slide2vips_ome_one_series(self, level, *args, **kwargs): """ - toilet_roll = pyvips.Image.new_from_file(self.src_f, n=-1, subifd=level-1) - page = pyvips.Image.new_from_file(self.src_f, n=1, subifd=level-1, access='random') + page = pyvips.Image.new_from_file( + self.src_f, n=1, subifd=level - 1, access="random" + ) if page.interpretation in VIPS_RGB_FORMATS: vips_slide = page else: - page_height = page.height - pages = [toilet_roll.crop(0, y, toilet_roll.width, page_height) for - y in range(0, toilet_roll.height, page_height)] + pages = [ + pyvips.Image.new_from_file(self.src_f, page=p - 1) + for p in range(self.metadata.n_channels) + ] vips_slide = pages[0].bandjoin(pages[1:]) if vips_slide.bands == 1: @@ -1796,26 +966,28 @@ def slide2vips(self, level, xywh=None, *args, **kwargs): """ if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}") + logger.warning(f"level is negative {level} for {self.src_f}") level = max(0, level) if self.use_openslide: # Keep rgb=False returns rgba. Makes it possible to avoid having black pixels for background. Can remove alpha channel after - vips_slide = pyvips.Image.new_from_file(self.src_f, level=level, autocrop=True, rgb=False, access='random')[0:3] - - elif self.is_ome: - vips_slide = self._slide2vips_ome_one_series(level=level, *args, **kwargs) - + vips_slide = pyvips.Image.new_from_file( + self.src_f, level=level, autocrop=True, rgb=False, access="random" + )[0:3] else: try: - vips_slide = pyvips.Image.new_from_file(self.src_f, subifd=level-1, access='random') + vips_slide = pyvips.Image.new_from_file( + self.src_f, subifd=level - 1, access="random" + ) except Exception as e: if level > 0 and len(self.metadata.slide_dimensions) > 1: # Pyramid image but each level is a page, not a SubIFD - vips_slide = pyvips.Image.new_from_file(self.src_f, page=level, access='random') + vips_slide = pyvips.Image.new_from_file( + self.src_f, page=level, access="random" + ) else: # Regular images like png or jpeg don't have SubIFD or pages - vips_slide = pyvips.Image.new_from_file(self.src_f, access='random') + vips_slide = pyvips.Image.new_from_file(self.src_f, access="random") if self.metadata.is_rgb and vips_slide.hasalpha() >= 1: # Remove alpha channel @@ -1847,7 +1019,9 @@ def slide2image(self, level, xywh=None, *args, **kwargs): """ if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") + logger.warning( + f"level is negative {level} for {self.src_f}. Should be >= 0" + ) level = max(0, level) vips_slide = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) @@ -1911,7 +1085,7 @@ def _get_channel_names(self, vips_img, *args, **kwargs): vips_fields = vips_img.get_fields() channel_names = None - if 'n-pages' in vips_fields and "image-description" in vips_fields: + if "n-pages" in vips_fields and "image-description" in vips_fields: n_pages = vips_img.get("n-pages") channel_names = [] for i in range(n_pages): @@ -1933,8 +1107,7 @@ def _get_channel_names(self, vips_img, *args, **kwargs): is_vips_rgb = vips_img.interpretation in VIPS_RGB_FORMATS if (channel_names is None or len(channel_names) == 0) and not is_vips_rgb: - channel_names = get_default_channel_names(vips_img.bands, - src_f=self.src_f) + channel_names = get_default_channel_names(vips_img.bands, src_f=self.src_f) return channel_names def _get_slide_dimensions(self, vips_img): @@ -1963,12 +1136,6 @@ def _get_slide_dimensions(self, vips_img): return slide_dimensions - def _get_slide_dimensions_ometiff_bf(self, *args): - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - return np.array(bf_reader.metadata.slide_dimensions) - def _get_slide_dimensions_ometiff(self, vips_img, *args): if "n-subifds" not in vips_img.get_fields(): @@ -1982,7 +1149,7 @@ def _get_slide_dimensions_ometiff(self, vips_img, *args): n_levels = vips_img.get("n-subifds") + 1 slide_dims_wh = [None] * n_levels for i in range(0, n_levels): - page = pyvips.Image.new_from_file(self.src_f, n=1, subifd=i-1) + page = pyvips.Image.new_from_file(self.src_f, n=1, subifd=i - 1) slide_dims_wh[i] = np.array([page.width, page.height]) slide_dims_wh = np.array(slide_dims_wh) @@ -2004,8 +1171,17 @@ def _get_slide_dimensions_openslide(self, vips_img): """ - n_levels = eval(vips_img.get('openslide.level-count')) - slide_dims = np.array([warp_tools.get_shape(pyvips.Image.new_from_file(self.src_f, level=i, autocrop=True, rgb=True))[0:2][::-1] for i in range(n_levels)]) + n_levels = eval(vips_img.get("openslide.level-count")) + slide_dims = np.array( + [ + warp_tools.get_shape( + pyvips.Image.new_from_file( + self.src_f, level=i, autocrop=True, rgb=True + ) + )[0:2][::-1] + for i in range(n_levels) + ] + ) return slide_dims @@ -2025,7 +1201,7 @@ def _get_slide_dimensions_vips(self, vips_img): """ vips_fields = vips_img.get_fields() - if 'n-pages' in vips_fields: + if "n-pages" in vips_fields: n_pages = vips_img.get("n-pages") all_dims = [] all_channels = [] @@ -2033,7 +1209,7 @@ def _get_slide_dimensions_vips(self, vips_img): try: page = pyvips.Image.new_from_file(self.src_f, page=i) except pyvips.error.Error as e: - print(f"error at page {i}: {e}") + logger.error(f"error at page {i}: {e}") w = page.width h = page.height @@ -2043,7 +1219,9 @@ def _get_slide_dimensions_vips(self, vips_img): all_channels.append(c) try: - most_common_channel_count = stats.mode(all_channels, keepdims=True)[0][0] + most_common_channel_count = stats.mode(all_channels, keepdims=True)[0][ + 0 + ] except: most_common_channel_count = stats.mode(all_channels)[0][0] @@ -2067,20 +1245,20 @@ def _get_pixel_physical_size(self, vips_img): Returns ------- res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' + Physical size per pixel and the unit, e.g. u'\u00b5m' Notes ----- - If physical unit is micron, it must be u'\u00B5m', + If physical unit is micron, it must be u'\u00b5m', not mu (u'\u03bcm') or u. """ res_xyu = None if self.use_openslide: - x_res = eval(vips_img.get('openslide.mpp-x')) - y_res = eval(vips_img.get('openslide.mpp-y')) - vips_img.get('slide-associated-images') + x_res = eval(vips_img.get("openslide.mpp-x")) + y_res = eval(vips_img.get("openslide.mpp-y")) + vips_img.get("slide-associated-images") phys_unit = MICRON_UNIT else: x_res = vips_img.get("xres") @@ -2089,8 +1267,8 @@ def _get_pixel_physical_size(self, vips_img): if x_res != 0 and y_res != 0 and has_units: # in vips, x_res and y_res are px/mm (https://www.libvips.org/API/current/VipsImage.html#VipsImage--xres) # Need to convert to um/px - x_res = (1/x_res)*(10**3) - y_res = (1/y_res)*(10**3) + x_res = (1 / x_res) * (10**3) + y_res = (1 / y_res) * (10**3) phys_unit = MICRON_UNIT else: # Default value is 0, so not provided @@ -2109,25 +1287,25 @@ class FlattenedPyramidReader(VipsSlideReader): An example would be one where the plane dimensions are something like [(600, 600), (600, 600), (600, 600), (300, 300), (300, 300), (300, 300)] - for a 3 channel image with 2 pyramid levels. It seems that bioformats - does not recognize these as pyramid images. + for a 3 channel image with 2 pyramid levels. """ def __init__(self, src_f, *args, **kwargs): super().__init__(src_f, *args, **kwargs) - # BF Datatype may not match min/max values in the image - # e.g. datatype is uint32, but min and max are floats self.metadata.img_dtype = None - self.metadata.img_dtype = self._get_dtype() - def create_metadata(self): - is_flattended_pyramid, bf_reads_flat, slide_dimensions,\ - levels_start_idx, n_channels = \ - check_flattened_pyramid_tiff(self.src_f) + ( + is_flattended_pyramid, + slide_dimensions, + levels_start_idx, + n_channels, + ) = check_flattened_pyramid_tiff(self.src_f) - assert is_flattended_pyramid and not bf_reads_flat, "Trying to use FlattenedPyramidReader but slide is not a flattened pyramid" + assert ( + is_flattended_pyramid + ), "Trying to use FlattenedPyramidReader but slide is not a flattened pyramid" meta_name = f"{os.path.split(self.src_f)[1]}_Series(0)".strip("_") server = VIPS_RDR @@ -2144,11 +1322,13 @@ def create_metadata(self): can_read_xml = False if img_xml is not None: try: - slide_meta = metadata_from_xml(xml=img_xml, - name=slide_meta.name, - server=server, - metadata=slide_meta) - can_read_xml = True + slide_meta = metadata_from_xml( + xml=img_xml, + name=slide_meta.name, + server=server, + metadata=slide_meta, + ) + can_read_xml = True except Exception as e: slide_meta = self._get_metadata_vips(slide_meta, vips_img) @@ -2161,7 +1341,9 @@ def create_metadata(self): if can_read_xml: # Verify basic info of read image matches xml read_img = self.slide2vips(0) - slide_meta = check_xml_img_match(img_xml, read_img, slide_meta, series=self.series) + slide_meta = check_xml_img_match( + img_xml, read_img, slide_meta, series=self.series + ) return slide_meta @@ -2186,14 +1368,16 @@ def slide2vips(self, level, xywh=None, *args, **kwargs): """ if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") + logger.warning( + f"level is negative {level} for {self.src_f}. Should be >= 0" + ) level = max(0, level) level_start = self.metadata.levels_start_idx[level] vips_slide = None level_shape = self.metadata.slide_dimensions[level] for i in range(level_start, self.metadata.n_pages): - page = pyvips.Image.new_from_file(self.src_f, page=i, access='random') + page = pyvips.Image.new_from_file(self.src_f, page=i, access="random") page_shape = np.array([page.width, page.height]) if not np.all(page_shape == level_shape): continue @@ -2206,20 +1390,9 @@ def slide2vips(self, level, xywh=None, *args, **kwargs): if xywh is not None: vips_slide = vips_slide.extract_area(*xywh) - if self.metadata.bf_datatype != self.metadata.img_dtype and self.metadata.img_dtype is not None: - # Min/max/response datatypes in xml don't match values image. - msg = (f"Bio-formats datatype is {self.metadata.bf_datatype}, " - f"but min/max/response values in xml are {self.metadata.img_dtype}. " - f"Converting to {self.metadata.img_dtype}" - ) - valtils.print_warning(msg) - vips_dtype = bf2vips_dtype(self.metadata.img_dtype) - vips_slide = vips_slide.copy(format=vips_dtype) - self.bf_datatype = self.metadata.img_dtype - if vips_slide.bands == 1: vips_slide = vips_slide.copy(interpretation="b-w") - elif vips_slide.bands == 3 and vips_slide.format == 'uchar': + elif vips_slide.bands == 3 and vips_slide.format == "uchar": vips_slide = vips_slide.copy(interpretation="srgb") else: vips_slide = vips_slide.copy(interpretation="multiband") @@ -2228,7 +1401,9 @@ def slide2vips(self, level, xywh=None, *args, **kwargs): def slide2image(self, level, xywh=None, *args, **kwargs): if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") + logger.warning( + f"level is negative {level} for {self.src_f}. Should be >= 0" + ) level = max(0, level) vips_slide = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) @@ -2238,16 +1413,20 @@ def slide2image(self, level, xywh=None, *args, **kwargs): # Big hack for when get the error "tiff2vips: out of order read" even with random access out_shape_wh = self.metadata.slide_dimensions[level] msg1 = f"pyvips.error.Error: {e} when converting pvips.Image to numpy array" - msg2 = f"Will try to resize level 0 to have shape {out_shape_wh} and convert" - valtils.print_warning(msg1) - valtils.print_warning(msg2, None) + msg2 = ( + f"Will try to resize level 0 to have shape {out_shape_wh} and convert" + ) + logger.warning(msg1) + logger.info(msg2) - s = np.mean(out_shape_wh/self.metadata.slide_dimensions[0]) + s = np.mean(out_shape_wh / self.metadata.slide_dimensions[0]) l0_slide = self.slide2vips(level=0, xywh=xywh, *args, **kwargs) resized = l0_slide.resize(s) vips_img = slide_tools.vips2numpy(resized) if not np.all(vips_img.shape[0:2][::-1] == out_shape_wh): - vips_img = transform.resize(vips_img, output_shape=out_shape_wh[::-1], preserve_range=True) + vips_img = transform.resize( + vips_img, output_shape=out_shape_wh[::-1], preserve_range=True + ) return vips_img @@ -2292,8 +1471,9 @@ def cname_from_tag_channel(soup): return names - default_channel_names = get_default_channel_names(vips_img.bands, - src_f=self.src_f) + default_channel_names = get_default_channel_names( + vips_img.bands, src_f=self.src_f + ) vips_fields = vips_img.get_fields() if "image-description" in vips_fields: @@ -2314,82 +1494,13 @@ def cname_from_tag_channel(soup): def _get_page_count(self, vips_img): vips_fields = vips_img.get_fields() - if 'n-pages' in vips_fields: + if "n-pages" in vips_fields: n_pages = vips_img.get("n-pages") else: n_pages = 0 return n_pages - def _get_dtype(self): - """Get Bio-Formats datatype from values in metadata. - - For example, BF metadata may have image datatype as - uint32, but in the image descriiption, min/max/resppnse, - are floats. This will determine if the slide should be cast - to a different dataatype to match values in metadata. - - """ - smallest_level = len(self.metadata.slide_dimensions) - 1 - vips_img = self.slide2vips(smallest_level) - vips_fields = vips_img.get_fields() - current_bf_dtype = vips2bf_dtype(vips_img.format) - bf_type = current_bf_dtype - if 'n-pages' in vips_fields: - page = pyvips.Image.new_from_file(self.src_f, page=0) - page_metadata = page.get("image-description") - - page_soup = BeautifulSoup(page_metadata, features="lxml") - # channels = page_soup.findAll("channel") # deprecated - # response = page_soup.findAll("response") # deprecated - channels = page_soup.find_all("channel") - response = page_soup.find_all("response") - if len(channels) > 0: - # Indica Labs tiff - dtypes = [None] * len(channels) - for i, chnl in enumerate(channels): - if chnl.has_attr("max"): - max_v = eval(chnl["max"]) - dtypes[i] = max_v.__class__.__name__ - - elif len(response) > 0: - # PerkinElmer-QPI tiff - dtypes = [None] * len(response) - for i, r in enumerate(response): - v = eval(r.getText("response")) - dtypes[i] = np.array([v]).dtype - dtypes[i] = v.__class__.__name__ - else: - return current_bf_dtype - - unique_dtypes = set(dtypes) - if len(unique_dtypes) > 1: - msg = "More than 1 datatype. Will not try to scale values" - valtils.print_warning(msg) - img_dtype = None - else: - img_dtype = dtypes[0] - - vals_are_floats = re.search("float", img_dtype) is not None - img_is_int = re.search("int", current_bf_dtype) is not None - if vals_are_floats and img_is_int: - max_v = vips_img.max() - - bf_px_num_type = FormatTools.pixelTypeFromString(self.metadata.bf_datatype) - temp_np_type, max_v_for_type = bf_to_numpy_dtype(bf_px_num_type, self.metadata.is_little_endian) - if temp_np_type.endswith('4'): - np_type = "float32" - elif temp_np_type.endswith('8'): - np_type = "float64" - - bf_type = slide_tools.NUMPY_FORMAT_BF_DTYPE[np_type] - else: - bf_type = current_bf_dtype - else: - bf_type = current_bf_dtype - - return bf_type - class CziJpgxrReader(SlideReader): """Read slides and get metadata @@ -2406,6 +1517,7 @@ class CziJpgxrReader(SlideReader): Image series """ + def __init__(self, src_f, series=None, *args, **kwargs): """ Parameters @@ -2423,7 +1535,8 @@ def __init__(self, src_f, series=None, *args, **kwargs): except Exception as e: traceback_msg = traceback.format_exc() msg = "Please install aicspylibczi" - valtils.print_warning(msg, traceback_msg=traceback_msg) + logger.warning(msg) + logger.warning(traceback_msg) czi_reader = CziFile(src_f) self.original_meta_dict = valtils.etree_to_dict(czi_reader.meta) @@ -2437,18 +1550,23 @@ def __init__(self, src_f, series=None, *args, **kwargs): self.meta_list = self.create_metadata() except Exception as e: traceback_msg = traceback.format_exc() - valtils.print_warning(e, traceback_msg=traceback_msg) + logger.warning(e) + logger.warning(traceback_msg) self.n_series = len(self.meta_list) if series is None: - img_areas = [np.multiply(*meta.slide_dimensions[0]) for meta in self.meta_list] + img_areas = [ + np.multiply(*meta.slide_dimensions[0]) for meta in self.meta_list + ] series = np.argmax(img_areas) if len(img_areas) > 1: - msg = (f"No series provided. " - f"Selecting series with largest image, " - f"which is series {series}") + msg = ( + f"No series provided. " + f"Selecting series with largest image, " + f"which is series {series}" + ) - valtils.print_warning(msg, warning_type=None, rgb=valtils.Fore.GREEN) + logger.info(msg) self._series = series self.series = series @@ -2460,9 +1578,7 @@ def _set_series(self, series): def _get_series(self): return self._series - series = property(fget=_get_series, - fset=_set_series, - doc="Slide scene") + series = property(fget=_get_series, fset=_set_series, doc="Slide scene") def _read_whole_img(self, level=0, xywh=None, *args, **kwargs): """ @@ -2494,7 +1610,7 @@ def _read_mosaic(self, level=0, xywh=None, *args, **kwargs): tile_bboxes = czi_reader.get_all_mosaic_tile_bounding_boxes(C=0) vips_img = pyvips.Image.black(*out_shape_wh, bands=self.metadata.n_channels) - print(f"Building CZI mosaic for {valtils.get_name(self.src_f)}") + logger.info(f"Building CZI mosaic for {valtils.get_name(self.src_f)}") for tile_info, tile_bbox in tqdm(tile_bboxes.items()): m = tile_info.m_index x = tile_bbox.x @@ -2515,17 +1631,19 @@ def _read_mosaic(self, level=0, xywh=None, *args, **kwargs): def slide2vips(self, level=0, xywh=None, *args, **kwargs): if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") + logger.warning( + f"level is negative {level} for {self.src_f}. Should be >= 0" + ) level = max(0, level) try: # Image is mosaic - vips_img = self._read_mosaic(level=level, xywh=xywh,*args, **kwargs) + vips_img = self._read_mosaic(level=level, xywh=xywh, *args, **kwargs) except Exception as e: - print(e) - print("Reading whole image") - vips_img = self._read_whole_img(level=level, xywh=xywh,*args, **kwargs) + logger.info(e) + logger.info("Reading whole image") + vips_img = self._read_whole_img(level=level, xywh=xywh, *args, **kwargs) czi_reader = CziFile(self.src_f) if xywh is not None: @@ -2537,7 +1655,7 @@ def slide2vips(self, level=0, xywh=None, *args, **kwargs): if self.is_bgr: vips_img = vips_img.copy(interpretation="srgb") - np_type = slide_tools.CZI_FORMAT_TO_BF_FORMAT[czi_reader.pixel_type] + np_type = slide_tools.CZI_FORMAT_TO_OME_FORMAT[czi_reader.pixel_type] vips_type = slide_tools.NUMPY_FORMAT_VIPS_DTYPE[np_type] vips_img = vips_img.cast(vips_type) @@ -2564,7 +1682,9 @@ def slide2image(self, level, xywh=None, *args, **kwargs): """ if level < 0: - valtils.print_warning(f"level is negative {level} for {self.src_f}. Should be >= 0") + logger.warning( + f"level is negative {level} for {self.src_f}. Should be >= 0" + ) level = max(0, level) vips_img = self.slide2vips(level=level, xywh=xywh, *args, **kwargs) @@ -2573,7 +1693,7 @@ def slide2image(self, level, xywh=None, *args, **kwargs): return np_img def create_metadata(self): - """ Create and fill in a MetaData object + """Create and fill in a MetaData object Returns ------- @@ -2588,10 +1708,7 @@ def create_metadata(self): n_scenes = len(dims_dict) meta_list = [None] * n_scenes phys_size = self._get_pixel_physical_size() - - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - original_xml = bf_reader.metadata.original_xml + original_xml = None for i in range(n_scenes): @@ -2604,17 +1721,16 @@ def create_metadata(self): if series_meta.is_rgb: n_channels = dims_dict[i]["A"][1] - series_meta.pyvips_interpretation = 'srgb' + series_meta.pyvips_interpretation = "srgb" else: n_channels = dims_dict[i]["C"][1] if n_channels == 1: - series_meta.pyvips_interpretation = 'b-w' + series_meta.pyvips_interpretation = "b-w" else: - series_meta.pyvips_interpretation = 'multiband' + series_meta.pyvips_interpretation = "multiband" series_meta.n_channels = n_channels series_meta.slide_dimensions = self._get_slide_dimensions(i) - series_meta.bf_datatype = slide_tools.CZI_FORMAT_TO_BF_FORMAT[czi_reader.pixel_type] series_meta.channel_names = self._get_channel_names(meta=series_meta) series_meta.pixel_physical_size_xyu = phys_size @@ -2623,11 +1739,12 @@ def create_metadata(self): meta_list[i] = series_meta - return meta_list def _get_img_meta_dict(self): - return self.original_meta_dict["ImageDocument"]["Metadata"]["Information"]["Image"] + return self.original_meta_dict["ImageDocument"]["Metadata"]["Information"][ + "Image" + ] def _check_rgb(self, *args, **kwargs): """Determine if image is RGB @@ -2641,7 +1758,7 @@ def _check_rgb(self, *args, **kwargs): czi_reader = CziFile(self.src_f) self.is_bgr = czi_reader.pixel_type.startswith("bgr") _is_rgb = czi_reader.pixel_type.startswith("rgb") - is_rgb =_is_rgb or self.is_bgr + is_rgb = _is_rgb or self.is_bgr return is_rgb @@ -2676,7 +1793,7 @@ def _get_channel_names_aics(self, meta, *args, **kwargs): try: all_channel_ids = [x["@Id"].split(":") for x in channels] all_channel_ids = [eval(x["@Id"].split(":")[1]) for x in channels] - max_c = max([eval(img_dict["SizeC"]), max(all_channel_ids)+1]) + max_c = max([eval(img_dict["SizeC"]), max(all_channel_ids) + 1]) channel_names = [None] * max_c except: channel_names = [None] * eval(img_dict["SizeC"]) @@ -2690,30 +1807,6 @@ def _get_channel_names_aics(self, meta, *args, **kwargs): return channel_names - - def _get_channel_names_bf(self, meta, *args, **kwargs): - """Get names of each channel - - Get list of channel names - - Returns - ------- - channel_names : list - List of channel names - - """ - if meta.is_rgb: - return None - - - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - rdr, bf_meta = bf_reader._get_bf_objects() - channel_names = bf_reader._get_channel_names(rdr, bf_meta) - - return channel_names - def _get_channel_names(self, meta, *args, **kwargs): channel_names = self._get_channel_names_aics(meta) return channel_names @@ -2732,7 +1825,9 @@ def _get_slide_dimensions(self, scene=0, *args, **kwargs): czi_reader = CziFile(self.src_f) scene_bbox = czi_reader.get_all_scene_bounding_boxes()[scene] scence_l0_wh = np.array([scene_bbox.w, scene_bbox.h]) - slide_dimensions = np.round(scence_l0_wh*zoom_levels[..., np.newaxis]).astype(int) + slide_dimensions = np.round(scence_l0_wh * zoom_levels[..., np.newaxis]).astype( + int + ) return slide_dimensions @@ -2751,7 +1846,7 @@ def _get_zoom_levels(self, scene=0): n_levels = eval(pyramid_info["PyramidLayersCount"]) downsampling = eval(pyramid_info["MinificationFactor"]) - zoom_levels = (1/downsampling)**(np.arange(0, n_levels)) + zoom_levels = (1 / downsampling) ** (np.arange(0, n_levels)) return zoom_levels @@ -2761,22 +1856,24 @@ def _get_pixel_physical_size(self, *args, **kwargs): Returns ------- res_xyu : tuple - Physical size per pixel and the unit, e.g. u'\u00B5m' + Physical size per pixel and the unit, e.g. u'\u00b5m' Notes ----- - If physical unit is micron, it must be u'\u00B5m', + If physical unit is micron, it must be u'\u00b5m', not mu (u'\u03bcm') or u. """ - physical_sizes = self.original_meta_dict["ImageDocument"]["Metadata"]["Scaling"]["Items"]["Distance"] + physical_sizes = self.original_meta_dict["ImageDocument"]["Metadata"][ + "Scaling" + ]["Items"]["Distance"] physical_size_xyu = [None] * 3 physical_unit = physical_sizes[0]["DefaultUnitFormat"] physical_size_xyu[2] = physical_unit - if physical_unit == u'\u00B5m': + if physical_unit == "\u00b5m": physical_scaling = 10**6 elif physical_unit == "mm": physical_scaling = 10**3 @@ -2787,17 +1884,15 @@ def _get_pixel_physical_size(self, *args, **kwargs): for ps in physical_sizes: if ps["@Id"] == "X": - physical_size_xyu[0] = eval(ps["Value"])*physical_scaling + physical_size_xyu[0] = eval(ps["Value"]) * physical_scaling elif ps["@Id"] == "Y": - physical_size_xyu[1] = eval(ps["Value"])*physical_scaling + physical_size_xyu[1] = eval(ps["Value"]) * physical_scaling return tuple(physical_size_xyu) class ImageReader(SlideReader): - """Read image using scikit-image - - """ + """Read image using scikit-image""" def __init__(self, src_f, *args, **kwargs): super().__init__(src_f, *args, **kwargs) @@ -2815,13 +1910,6 @@ def create_metadata(self): slide_meta.pixel_physical_size_xyu = [1, 1, PIXEL_UNIT] slide_meta.slide_dimensions = self._get_slide_dimensions(pil_img) - f_extension = slide_tools.get_slide_extension(self.src_f) - if f_extension in BF_READABLE_FORMATS: - with valtils.HiddenPrints(): - bf_reader = BioFormatsSlideReader(self.src_f) - - slide_meta.original_xml = bf_reader.metadata.original_xml - slide_meta.bf_datatype = bf_reader.metadata.bf_datatype pil_img.close() return slide_meta @@ -2844,8 +1932,7 @@ def slide2image(self, xywh=None, *args, **kwargs): return img def _get_slide_dimensions(self, pil_img, *args, **kwargs): - """ - """ + """ """ img_dims = np.array([[pil_img.width, pil_img.height]]) return img_dims @@ -2858,12 +1945,12 @@ def _get_n_channels(self, pil_img, *args, **kwargs): def _check_rgb(self, pil_img, *args, **kwargs): - is_rgb = pil_img.mode == 'RGB' + is_rgb = pil_img.mode == "RGB" return is_rgb def _get_channel_names(self, pil_img, *args, **kwargs): - is_rgb = pil_img.mode == 'RGB' + is_rgb = pil_img.mode == "RGB" if is_rgb: channel_names = None else: @@ -2874,8 +1961,7 @@ def _get_channel_names(self, pil_img, *args, **kwargs): def get_slide_reader(src_f, series=None): """Get appropriate SlideReader - If a slide can be read by openslide and bioformats, VipsSlideReader will be used - because it can be opened as a pyvips.Image. + VipsSlideReader will be used for slides that can be read by openslide or pyvips. Parameters ---------- @@ -2902,12 +1988,11 @@ def get_slide_reader(src_f, series=None): """ - src_f = str(src_f) f_extension = slide_tools.get_slide_extension(src_f) if f_extension is None: msg = f"Unable to find reader to open {os.path.split(src_f)[-1]}" - valtils.print_warning(msg, rgb=Fore.RED) + logger.warning(msg) return None is_ome_tiff = check_is_ome(src_f) @@ -2915,9 +2000,8 @@ def get_slide_reader(src_f, series=None): is_czi = f_extension == ".czi" is_flattened_tiff = False - bf_reads_flat = False if is_tiff: - is_flattened_tiff, _ = check_flattened_pyramid_tiff(src_f, check_with_bf=False)[0:2] + is_flattened_tiff, _ = check_flattened_pyramid_tiff(src_f)[0:2] one_series = True if is_ome_tiff: @@ -2925,54 +2009,23 @@ def get_slide_reader(src_f, series=None): one_series = len(ome_obj.images) <= 1 can_use_vips = check_to_use_vips(src_f) - can_use_openslide = check_to_use_openslide(src_f) # Checks openslide is installed + can_use_openslide = check_to_use_openslide(src_f) # Checks openslide is installed # Give preference to vips/openslide since it will be fastest - if (can_use_vips or can_use_openslide) and one_series and series in [0, None] and not is_flattened_tiff: + if (can_use_vips or can_use_openslide) and not is_flattened_tiff: return VipsSlideReader - if is_czi: - is_jpegxr = check_czi_jpegxr(src_f) - is_m1_mac = valtils.check_m1_mac() - if is_m1_mac and is_jpegxr: - msg = "Will likely be errors using Bioformats to read a JPEGXR compressed CZI on this Apple M1 machine. Will use CziJpgxrReader instead." - return CziJpgxrReader - - # Check to see if Bio-formats will work - init_jvm() - can_read_meta_bf, can_read_img_bf = check_to_use_bioformats(src_f, series=series) - can_use_bf = can_read_meta_bf and can_read_img_bf if is_flattened_tiff: - _, bf_reads_flat = check_flattened_pyramid_tiff(src_f, check_with_bf=True)[0:2] - # Give preference to BioFormatsSlideReader since it will be faster - if bf_reads_flat and can_read_img_bf: - return BioFormatsSlideReader - else: - return FlattenedPyramidReader + return FlattenedPyramidReader if is_czi: - if can_read_img_bf: - # Bio-formats should be able to read CZI - return BioFormatsSlideReader + is_jpegxr = check_czi_jpegxr(src_f) + if is_jpegxr: + return CziJpgxrReader else: - # Bio-formats unable to read CZI. Check if it is due to jpgxr compression - czi = CziFile(src_f) - comp_tree = czi.meta.findall(".//OriginalCompressionMethod") - if len(comp_tree) > 0: - is_czi_jpgxr = comp_tree[0].text.lower() == "jpgxr" - else: - is_czi_jpgxr = False - - if is_czi_jpgxr: - return CziJpgxrReader - else: - msg = f"Unable to find reader to open {os.path.split(src_f)[-1]}" - valtils.print_warning(msg, rgb=Fore.RED) - - return None - - if can_use_bf: - return BioFormatsSlideReader + msg = f"Unable to find reader to open {os.path.split(src_f)[-1]}" + logger.warning(msg) + return None # Try using scikit-image. Not ideal if image is large try: @@ -2981,14 +2034,12 @@ def get_slide_reader(src_f, series=None): except: pass - msg = f"Unable to find reader to open {os.path.split(src_f)[-1]}" - valtils.print_warning(msg, rgb=Fore.RED) - - return None + raise RuntimeError(f"Unable to find reader to open {os.path.split(src_f)[-1]}") # Write slides to ome.tiff # + def remove_control_chars(s): """Remove control characters @@ -3007,9 +2058,11 @@ def remove_control_chars(s): """ - control_chars = ''.join(map(chr, itertools.chain(range(0x00,0x20), range(0x7f,0xa0)))) - control_char_re = re.compile('[%s]' % re.escape(control_chars)) - control_char_removed = control_char_re.sub('', s) + control_chars = "".join( + map(chr, itertools.chain(range(0x00, 0x20), range(0x7F, 0xA0))) + ) + control_char_re = re.compile("[%s]" % re.escape(control_chars)) + control_char_removed = control_char_re.sub("", s) return control_char_removed @@ -3058,8 +2111,8 @@ def create_channel(channel_id, name=None, color=None, samples_per_pixel=1): """ if name is not None: - unicode_name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore') - decoded_name = unicode_name.decode('unicode_escape') + unicode_name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore") + decoded_name = unicode_name.decode("unicode_escape") decoded_name = remove_control_chars(decoded_name) else: @@ -3098,14 +2151,16 @@ def get_colormap(channel_names, is_rgb, series=0, original_xml=None): og_ome = get_ome_obj(original_xml) ome_img = og_ome.images[series] - colormap = {c.name: c.color.as_rgb_tuple() for c in ome_img.pixels.channels} + colormap = { + c.name: c.color.as_rgb_tuple() for c in ome_img.pixels.channels + } all_rgb = set(list(colormap.values())) nc = len(ome_img.pixels.channels) if len(all_rgb) < nc: # Channels do not have unique colors colormap = default_colormap except Exception as e: - print(e) + logger.warning(e) # Can't get original colors colormap = default_colormap else: @@ -3133,13 +2188,19 @@ def check_colormap(colormap, channel_names): if isinstance(colormap, str) and colormap == CMAP_AUTO: updated_colormap = get_colormap(channel_names, is_rgb=False) - elif isinstance(colormap, list) or isinstance(colormap, np.ndarray) or isinstance(colormap, tuple): + elif ( + isinstance(colormap, list) + or isinstance(colormap, np.ndarray) + or isinstance(colormap, tuple) + ): if np.array(colormap).ndim == 1 and len(channel_names) == 1: # colormap is an array for a single channel updated_colormap = np.array([updated_colormap]) if len(updated_colormap) < len(channel_names): msg = f"Not enough colors in colormap. Only {len(updated_colormap)} colors provided, but there are {len(channel_names)} channels" - updated_colormap = {channel_names[i]: updated_colormap[i] for i in range(len(channel_names))} + updated_colormap = { + channel_names[i]: updated_colormap[i] for i in range(len(channel_names)) + } elif isinstance(colormap, dict): @@ -3149,15 +2210,17 @@ def check_colormap(colormap, channel_names): msg = f"Missing colors in colormap for the following channels: {missing_channels}" elif colormap is not None: - msg = (f"Colormap must be {CMAP_AUTO}, " - f"a list of colors with the same length as `channel_names`, ", - f"a dictionary (key=channel name, value=rgb color), ", - f"or `None`") + msg = ( + f"Colormap must be {CMAP_AUTO}, " + f"a list of colors with the same length as `channel_names`, ", + f"a dictionary (key=channel name, value=rgb color), ", + f"or `None`", + ) if msg is not None: msg += ". Will not try to add channel colors" updated_colormap = None - valtils.print_warning(msg) + logger.warning(msg) return updated_colormap @@ -3185,19 +2248,31 @@ def check_channel_names(channel_names, is_rgb, nc, src_f=None): if len(channel_names) == 0 and nc > 0: updated_channel_names = default_channel_names else: - updated_channel_names = [channel_names[i] if - (channel_names[i] is not None and channel_names[i] != "None") - else default_channel_names[i] for i in range(nc)] + updated_channel_names = [ + ( + channel_names[i] + if (channel_names[i] is not None and channel_names[i] != "None") + else default_channel_names[i] + ) + for i in range(nc) + ] renamed_channels = set(updated_channel_names) - set(channel_names) if len(renamed_channels) > 0: msg = f"some non-RGB channel names were `None` or not provided. Channels that got renamed are: {sorted(list(renamed_channels))}" - print(msg) + logger.info(msg) return updated_channel_names -def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, channel_names=None, colormap=CMAP_AUTO): +def create_ome_xml( + shape_xyzct, + ome_dtype, + is_rgb, + pixel_physical_size_xyu=None, + channel_names=None, + colormap=CMAP_AUTO, +): """Create new ome-xmml object Parameters @@ -3205,8 +2280,8 @@ def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, shape_xyzct : tuple of int XYZCT shape of image - bf_dtype : str - String format of Bioformats datatype + ome_dtype : str + String format of OME datatype is_rgb : bool Whether or not the image is RGB @@ -3241,10 +2316,10 @@ def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, size_z=z, size_c=c, size_t=t, - type=bf_dtype, - dimension_order='XYZCT', - metadata_only=True - ) + type=ome_dtype, + dimension_order="XYZCT", + metadata_only=True, + ), ) if pixel_physical_size_xyu is not None: @@ -3255,7 +2330,7 @@ def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, new_img.pixels.physical_size_y_unit = phys_u if is_rgb: - rgb_channel = ome_types.model.Channel(id='Channel:0:0', samples_per_pixel=3) + rgb_channel = ome_types.model.Channel(id="Channel:0:0", samples_per_pixel=3) new_img.pixels.channels = [rgb_channel] else: @@ -3267,19 +2342,39 @@ def create_ome_xml(shape_xyzct, bf_dtype, is_rgb, pixel_physical_size_xyu=None, colormap = check_colormap(colormap, updated_channel_names) try: if isinstance(colormap, dict): - channels = [create_channel(i, name=updated_channel_names[i], color=colormap[updated_channel_names[i]]) for i in range(c)] - elif isinstance(colormap, np.ndarray) or isinstance(colormap, list) or isinstance(colormap, tuple): - channels = [create_channel(i, name=updated_channel_names[i], color=colormap[i]) for i in range(c)] + channels = [ + create_channel( + i, + name=updated_channel_names[i], + color=colormap[updated_channel_names[i]], + ) + for i in range(c) + ] + elif ( + isinstance(colormap, np.ndarray) + or isinstance(colormap, list) + or isinstance(colormap, tuple) + ): + channels = [ + create_channel( + i, name=updated_channel_names[i], color=colormap[i] + ) + for i in range(c) + ] except KeyError as e: msg = f"Mismatch between channel names and keys in colormap. Cannot find channel name {e} in colormap" if colormap is not None: msg += f", which has keys: {list(colormap.keys())}" msg += ". Saving without colormap. To avoid this error, please provide valid colormap, or set colormap=None." - valtils.print_warning(msg) - channels = [create_channel(i, name=updated_channel_names[i]) for i in range(c)] - #Mismatch between channel names and keys in colormap + logger.warning(msg) + channels = [ + create_channel(i, name=updated_channel_names[i]) for i in range(c) + ] + # Mismatch between channel names and keys in colormap else: - channels = [create_channel(i, name=updated_channel_names[i]) for i in range(c)] + channels = [ + create_channel(i, name=updated_channel_names[i]) for i in range(c) + ] new_img.pixels.channels = channels @@ -3305,8 +2400,10 @@ def get_tile_wh(reader, level, out_shape_wh, default_wh=512): tile_wh = slide_meta.optimal_tile_wh if level != 0: - down_sampling = np.mean(slide_meta.slide_dimensions[level]/slide_meta.slide_dimensions[0]) - tile_wh = int(np.round(tile_wh*down_sampling)) + down_sampling = np.mean( + slide_meta.slide_dimensions[level] / slide_meta.slide_dimensions[0] + ) + tile_wh = int(np.round(tile_wh * down_sampling)) tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 if tile_wh < 16: tile_wh = 16 @@ -3322,15 +2419,27 @@ def get_tile_wh(reader, level, out_shape_wh, default_wh=512): if max_tile_exp <= np.log2(min_wh): min_wh = 16 - possible_wh = 2**np.arange(np.log2(min_wh), max_tile_exp+1) - overhangs = np.array([np.max(np.ceil(out_shape_wh/wh)*wh - out_shape_wh) for wh in possible_wh]) + possible_wh = 2 ** np.arange(np.log2(min_wh), max_tile_exp + 1) + overhangs = np.array( + [ + np.max(np.ceil(out_shape_wh / wh) * wh - out_shape_wh) + for wh in possible_wh + ] + ) min_overhang = np.min(overhangs) tile_wh = int(np.max(possible_wh[overhangs == min_overhang])) return tile_wh -def update_xml_for_new_img(img, reader, level=0, channel_names=None, colormap=CMAP_AUTO, pixel_physical_size_xyu=None): +def update_xml_for_new_img( + img, + reader, + level=0, + channel_names=None, + colormap=CMAP_AUTO, + pixel_physical_size_xyu=None, +): """Update dimensions ome-xml metadata Used to create a new ome-xmlthat reflects changes in an image, such as its shape @@ -3365,15 +2474,18 @@ def update_xml_for_new_img(img, reader, level=0, channel_names=None, colormap=CM img_h, img_w, _ = warp_tools.get_shape(img) nc = slide_meta.n_channels - new_xyzct = get_shape_xyzct((img_w, img_h), nc, nt=slide_meta.n_t, nz=slide_meta.n_z) + new_xyzct = get_shape_xyzct( + (img_w, img_h), nc, nt=slide_meta.n_t, nz=slide_meta.n_z + ) current_ome_xml_str = slide_meta.original_xml is_rgb = slide_meta.is_rgb series = slide_meta.series if isinstance(img, pyvips.Image): - bf_dtype = vips2bf_dtype(img.format) + np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[img.format] + ome_dtype = slide_tools.NUMPY_FORMAT_OME_DTYPE[str(np_dtype().dtype)] else: - bf_dtype = slide_tools.NUMPY_FORMAT_BF_DTYPE[str(img.dtype)] + ome_dtype = slide_tools.NUMPY_FORMAT_OME_DTYPE[str(img.dtype)] if channel_names is None: channel_names = slide_meta.channel_names @@ -3387,7 +2499,12 @@ def update_xml_for_new_img(img, reader, level=0, channel_names=None, colormap=CM if not is_rgb: if isinstance(colormap, str) and colormap == CMAP_AUTO: - colormap = get_colormap(updated_channel_names, is_rgb=is_rgb, series=series, original_xml=current_ome_xml_str) + colormap = get_colormap( + updated_channel_names, + is_rgb=is_rgb, + series=series, + original_xml=current_ome_xml_str, + ) colormap = check_colormap(colormap, channel_names=updated_channel_names) @@ -3400,15 +2517,21 @@ def update_xml_for_new_img(img, reader, level=0, channel_names=None, colormap=CM except elementTree.ParseError as e: traceback_msg = traceback.format_exc() msg = "xml in original file is invalid or missing. Will create one" - valtils.print_warning(msg, traceback_msg=traceback_msg) + logger.warning(msg) + logger.warning(traceback_msg) og_valid_xml = False else: og_valid_xml = False - temp_new_ome = create_ome_xml(shape_xyzct=new_xyzct, bf_dtype=bf_dtype, is_rgb=is_rgb, - pixel_physical_size_xyu=pixel_physical_size_xyu, - channel_names=updated_channel_names, colormap=colormap) + temp_new_ome = create_ome_xml( + shape_xyzct=new_xyzct, + ome_dtype=ome_dtype, + is_rgb=is_rgb, + pixel_physical_size_xyu=pixel_physical_size_xyu, + channel_names=updated_channel_names, + colormap=colormap, + ) if og_valid_xml and og_ome is not None: new_ome = og_ome.copy() @@ -3420,13 +2543,28 @@ def update_xml_for_new_img(img, reader, level=0, channel_names=None, colormap=CM @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def warp_and_save_slide(src_f, dst_f, transformation_src_shape_rc, transformation_dst_shape_rc, - aligned_slide_shape_rc, M=None, dxdy=None, - level=0, series=None, interp_method="bicubic", - bbox_xywh=None, bg_color=None, colormap=None, channel_names=None, - tile_wh=None, compression=DEFAULT_COMPRESSION, Q=100, pyramid=True, reader=None): - - """ Warp and save a slide +def warp_and_save_slide( + src_f, + dst_f, + transformation_src_shape_rc, + transformation_dst_shape_rc, + aligned_slide_shape_rc, + M=None, + dxdy=None, + level=0, + series=None, + interp_method="bicubic", + bbox_xywh=None, + bg_color=None, + colormap=None, + channel_names=None, + tile_wh=None, + compression=DEFAULT_COMPRESSION, + Q=100, + pyramid=True, + reader=None, +): + """Warp and save a slide Warp slide according to `M` and/or `dxdy`, then save as an ome.tiff image. @@ -3499,42 +2637,59 @@ def warp_and_save_slide(src_f, dst_f, transformation_src_shape_rc, transformatio `get_slide_reader` will be used to find the appropriate reader. """ - warped_slide = slide_tools.warp_slide(src_f=src_f, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - aligned_slide_shape_rc=aligned_slide_shape_rc, - M=M, - dxdy=dxdy, - level=level, - series=series, - interp_method=interp_method, - bbox_xywh=bbox_xywh, - bg_color=bg_color, - reader=reader) + warped_slide = slide_tools.warp_slide( + src_f=src_f, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + aligned_slide_shape_rc=aligned_slide_shape_rc, + M=M, + dxdy=dxdy, + level=level, + series=series, + interp_method=interp_method, + bbox_xywh=bbox_xywh, + bg_color=bg_color, + reader=reader, + ) # Get OMEXML and update with new dimensions if reader is None: - reader_cls = get_slide_reader(src_f, series=series) # Get slide reader class - reader = reader_cls(src_f, series=series) # Get reader - - ome_xml_obj = update_xml_for_new_img(img=warped_slide, - reader=reader, - level=level, - channel_names=channel_names, - colormap=colormap) + reader_cls = get_slide_reader(src_f, series=series) # Get slide reader class + reader = reader_cls(src_f, series=series) # Get reader + + ome_xml_obj = update_xml_for_new_img( + img=warped_slide, + reader=reader, + level=level, + channel_names=channel_names, + colormap=colormap, + ) ome_xml = ome_xml_obj.to_xml() out_shape_wh = warp_tools.get_shape(warped_slide)[0:2][::-1] - tile_wh = get_tile_wh(reader=reader, - level=level, - out_shape_wh=out_shape_wh) - - save_ome_tiff(warped_slide, dst_f=dst_f, ome_xml=ome_xml, - tile_wh=tile_wh, compression=compression, Q=Q, pyramid=pyramid) + tile_wh = get_tile_wh(reader=reader, level=level, out_shape_wh=out_shape_wh) + + save_ome_tiff( + warped_slide, + dst_f=dst_f, + ome_xml=ome_xml, + tile_wh=tile_wh, + compression=compression, + Q=Q, + pyramid=pyramid, + ) -def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=512, compression=DEFAULT_COMPRESSION, Q=100, pyramid=True): +def save_ome_tiff( + img, + dst_f, + ome_xml=None, + tile_wh=512, + compression=DEFAULT_COMPRESSION, + Q=100, + pyramid=True, +): """Save an image in the ome.tiff format using pyvips Parameters @@ -3568,13 +2723,16 @@ def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=512, compression=DEFAULT_COM if not isinstance(img, pyvips.vimage.Image): img = slide_tools.numpy2vips(img) - if img.format in ["float", "double"] and compression in [pyvips.enums.ForeignTiffCompression.JP2K, pyvips.enums.ForeignTiffCompression.JPEG]: + if img.format in ["float", "double"] and compression in [ + pyvips.enums.ForeignTiffCompression.JP2K, + pyvips.enums.ForeignTiffCompression.JPEG, + ]: msg = f"Image has type {img.format} but compression method {compression} will convert image to uint8. To avoid this, change compression 'lzw', 'deflate', or 'none' " - valtils.print_warning(msg) + logger.warning(msg) if compression == "jp2k": compression = "jpeg" msg = f"Float images can't be saved using {compression} compression. Please change to another method, such as 'lzw', 'deflate', or 'none' " - valtils.print_warning(msg) + logger.warning(msg) return None @@ -3584,7 +2742,7 @@ def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=512, compression=DEFAULT_COM new_out_f = out_f.split(dst_f_extension)[0] + ".ome.tiff" new_dst_f = os.path.join(dst_dir, new_out_f) msg = f"{out_f} is not an ome.tiff. Changing dst_f to {new_dst_f}" - valtils.print_warning(msg) + logger.warning(msg) dst_f = new_dst_f # Get ome-xml metadata # @@ -3593,26 +2751,32 @@ def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=512, compression=DEFAULT_COM og_interpretation = img.interpretation is_rgb = og_interpretation in VIPS_RGB_FORMATS - bf_dtype = vips2bf_dtype(img.format) + np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[img.format] + ome_dtype = slide_tools.NUMPY_FORMAT_OME_DTYPE[str(np_dtype().dtype)] if ome_xml is None: # Create minimal ome-xml - ome_xml_obj = create_ome_xml(shape_xyzct=xyzct, bf_dtype=bf_dtype, is_rgb=is_rgb) + ome_xml_obj = create_ome_xml( + shape_xyzct=xyzct, ome_dtype=ome_dtype, is_rgb=is_rgb + ) else: # Verify that vips image and ome-xml match ome_xml_obj = get_ome_obj(ome_xml) ome_img = ome_xml_obj.images[0].pixels - total_pages = ome_img.size_c*ome_img.size_z*ome_img.size_t + total_pages = ome_img.size_c * ome_img.size_z * ome_img.size_t - match_dict = {"same_x": ome_img.size_x == img.width, - "same_y": ome_img.size_y == img.height, - "total_pages": total_pages == img.bands, - "same_type": ome_img.type.name.lower() == bf_dtype - } + match_dict = { + "same_x": ome_img.size_x == img.width, + "same_y": ome_img.size_y == img.height, + "total_pages": total_pages == img.bands, + "same_type": ome_img.type.name.lower() == ome_dtype, + } if not all(list(match_dict.values())): - msg = f"mismatch in ome-xml and image: {str(match_dict)}. Will create ome-xml" - valtils.print_warning(msg) - ome_xml_obj = create_ome_xml(xyzct, bf_dtype, is_rgb) + msg = ( + f"mismatch in ome-xml and image: {str(match_dict)}. Will create ome-xml" + ) + logger.warning(msg) + ome_xml_obj = create_ome_xml(xyzct, ome_dtype, is_rgb) ome_xml_obj.creator = f"pyvips version {pyvips.__version__}" ome_metadata = ome_xml_obj.to_xml() @@ -3634,23 +2798,27 @@ def save_ome_tiff(img, dst_f, ome_xml=None, tile_wh=512, compression=DEFAULT_COM if is_rgb: total = 100 else: - total = 100*image_bands + total = 100 * image_bands tic = time.time() save_ome_tiff.n_complete = -1 save_ome_tiff.current_im = None + def eval_handler(im, progress): if save_ome_tiff.current_im != progress.im: save_ome_tiff.n_complete += 1 save_ome_tiff.current_im = progress.im - count = save_ome_tiff.n_complete*100 + progress.percent + count = save_ome_tiff.n_complete * 100 + progress.percent filled_len = int(round(bar_len * count / float(total))) percents = round(100.0 * count / float(total), 1) - bar = '=' * filled_len + '-' * (bar_len - filled_len) + bar = "=" * filled_len + "-" * (bar_len - filled_len) toc = time.time() - processing_time_h = round((toc - tic)/(60), 3) + processing_time_h = round((toc - tic) / (60), 3) - sys.stdout.write('[%s] %s%s %s %s %s\r' % (bar, percents, '%', 'in', processing_time_h, "minutes")) + sys.stdout.write( + "[%s] %s%s %s %s %s\r" + % (bar, percents, "%", "in", processing_time_h, "minutes") + ) sys.stdout.flush() try: @@ -3658,9 +2826,11 @@ def eval_handler(im, progress): img.signal_connect("eval", eval_handler) except pyvips.error.Error: msg = "Unable to create progress bar for pyvips. May need to update libvips to >= 8.11" - valtils.print_warning(msg) + logger.warning(msg) - print(f"saving {dst_f} ({img.width} x {image_height} and {image_bands} channels)") + logger.info( + f"saving {dst_f} ({img.width} x {image_height} and {image_bands} channels)" + ) # Write image # tile_wh = tile_wh - (tile_wh % 16) # Tile shape must be multiple of 16 @@ -3678,31 +2848,58 @@ def eval_handler(im, progress): if tile_wh < 16: tile_wh = 16 - print("") + logger.info("") lossless = Q == 100 rgbjpeg = compression in ["jp2k", "jpeg"] and img.interpretation in VIPS_RGB_FORMATS - subifd = pyramid # a pyramid will still be created if subifd = True and pyramid = False - img.tiffsave(dst_f, compression=compression, tile=tile, - tile_width=tile_wh, tile_height=tile_wh, - pyramid=pyramid, subifd=subifd, bigtiff=True, - lossless=lossless, Q=Q, rgbjpeg=rgbjpeg) + subifd = ( + pyramid # a pyramid will still be created if subifd = True and pyramid = False + ) + img.tiffsave( + dst_f, + compression=compression, + tile=tile, + tile_width=tile_wh, + tile_height=tile_wh, + pyramid=pyramid, + subifd=subifd, + bigtiff=True, + lossless=lossless, + Q=Q, + rgbjpeg=rgbjpeg, + ) # Print total time to completion # toc = time.time() - processing_time_seconds = toc-tic - processing_time, processing_time_unit = valtils.get_elapsed_time_string(processing_time_seconds) + processing_time_seconds = toc - tic + processing_time, processing_time_unit = valtils.get_elapsed_time_string( + processing_time_seconds + ) - bar = '=' * bar_len - sys.stdout.write('[%s] %s%s %s %s %s\r' % (bar, 100.0, '%', 'in', processing_time, processing_time_unit)) + bar = "=" * bar_len + sys.stdout.write( + "[%s] %s%s %s %s %s\r" + % (bar, 100.0, "%", "in", processing_time, processing_time_unit) + ) sys.stdout.flush() - sys.stdout.write('\nComplete\n') - print("") + sys.stdout.write("\nComplete\n") + logger.info("") @valtils.deprecated_args(perceputally_uniform_channel_colors="colormap") -def convert_to_ome_tiff(src_f, dst_f, level, series=None, xywh=None, - colormap=CMAP_AUTO, tile_wh=None, compression=DEFAULT_COMPRESSION, Q=100, pyramid=True, reader=None): +def convert_to_ome_tiff( + src_f, + dst_f, + level, + series=None, + xywh=None, + colormap=CMAP_AUTO, + tile_wh=None, + compression=DEFAULT_COMPRESSION, + Q=100, + pyramid=True, + reader=None, +): """Convert an image to an ome.tiff image Saves a new copy of the image as a tiled pyramid ome.tiff with valid ome-xml. @@ -3760,11 +2957,13 @@ def convert_to_ome_tiff(src_f, dst_f, level, series=None, xywh=None, vips_img = reader.slide2vips(level=level, series=series, xywh=xywh) - ome_obj = update_xml_for_new_img(img=vips_img, - reader=reader, - level=level, - channel_names=slide_meta.channel_names, - colormap=colormap) + ome_obj = update_xml_for_new_img( + img=vips_img, + reader=reader, + level=level, + channel_names=slide_meta.channel_names, + colormap=colormap, + ) ome_obj.creator = f"pyvips version {pyvips.__version__}" ome_xml_str = ome_obj.to_xml() @@ -3774,4 +2973,12 @@ def convert_to_ome_tiff(src_f, dst_f, level, series=None, xywh=None, if tile_wh > MAX_TILE_SIZE: tile_wh = MAX_TILE_SIZE - save_ome_tiff(img=vips_img, dst_f=dst_f, ome_xml=ome_xml_str, tile_wh=tile_wh, compression=compression, Q=Q, pyramid=pyramid) + save_ome_tiff( + img=vips_img, + dst_f=dst_f, + ome_xml=ome_xml_str, + tile_wh=tile_wh, + compression=compression, + Q=Q, + pyramid=pyramid, + ) diff --git a/valis/slide_tools.py b/src/valis/slide_tools.py similarity index 70% rename from valis/slide_tools.py rename to src/valis/slide_tools.py index 792f3d73..9f34f4b7 100644 --- a/valis/slide_tools.py +++ b/src/valis/slide_tools.py @@ -2,9 +2,11 @@ Methods to work with slides, after being opened using slide_io """ + import torch import kornia +import logging import os import pyvips import numpy as np @@ -20,6 +22,8 @@ from . import viz from . import preprocessing +logger = logging.getLogger(__name__) + IHC_NAME = "brightfield" IF_NAME = "fluorescence" MULTI_MODAL_NAME = "multi" @@ -28,52 +32,43 @@ BG_AUTO_FILL_STR = "auto" NUMPY_FORMAT_VIPS_DTYPE = { - 'uint8': 'uchar', - 'int8': 'char', - 'uint16': 'ushort', - 'int16': 'short', - 'uint32': 'uint', - 'int32': 'int', - 'float32': 'float', - 'float64': 'double', - 'complex64': 'complex', - 'complex128': 'dpcomplex', - } + "uint8": "uchar", + "int8": "char", + "uint16": "ushort", + "int16": "short", + "uint32": "uint", + "int32": "int", + "float32": "float", + "float64": "double", + "complex64": "complex", + "complex128": "dpcomplex", +} VIPS_FORMAT_NUMPY_DTYPE = { - 'uchar': np.uint8, - 'char': np.int8, - 'ushort': np.uint16, - 'short': np.int16, - 'uint': np.uint32, - 'int': np.int32, - 'float': np.float32, - 'double': np.float64, - 'complex': np.complex64, - 'dpcomplex': np.complex128, + "uchar": np.uint8, + "char": np.int8, + "ushort": np.uint16, + "short": np.int16, + "uint": np.uint32, + "int": np.int32, + "float": np.float32, + "double": np.float64, + "complex": np.complex64, + "dpcomplex": np.complex128, } -NUMPY_FORMAT_BF_DTYPE = {'uint8': 'uint8', - 'int8': 'int8', - 'uint16': 'uint16', - 'int16': 'int16', - 'uint32': 'uint32', - 'int32': 'int32', - 'float32': 'float', - 'float64': 'double'} - -# See slide_io.bf_to_numpy_dtype -BF_DTYPE_PIXEL_TYPE = {'uint8':1, - 'int8': 0, - 'uint16': 3, - 'int16': 2, - 'uint32': 5, - 'int32': 4, - 'float': 6, - 'double': 7 - } +NUMPY_FORMAT_OME_DTYPE = { + "uint8": "uint8", + "int8": "int8", + "uint16": "uint16", + "int16": "int16", + "uint32": "uint32", + "int32": "int32", + "float32": "float", + "float64": "double", +} CZI_FORMAT_NUMPY_DTYPE = { "gray8": "uint8", @@ -85,9 +80,11 @@ "invalid": "uint8", } -CZI_FORMAT_TO_BF_FORMAT = {k:NUMPY_FORMAT_BF_DTYPE[v] for k,v in CZI_FORMAT_NUMPY_DTYPE.items()} +CZI_FORMAT_TO_OME_FORMAT = { + k: NUMPY_FORMAT_OME_DTYPE[v] for k, v in CZI_FORMAT_NUMPY_DTYPE.items() +} -BF_FORMAT_NUMPY_DTYPE = {v:k for k, v in NUMPY_FORMAT_BF_DTYPE.items()} +OME_FORMAT_NUMPY_DTYPE = {v: k for k, v in NUMPY_FORMAT_OME_DTYPE.items()} def vips2numpy(vi): @@ -98,9 +95,11 @@ def vips2numpy(vi): try: img = vi.numpy() except: - img = np.ndarray(buffer=vi.write_to_memory(), - dtype=VIPS_FORMAT_NUMPY_DTYPE[vi.format], - shape=[vi.height, vi.width, vi.bands]) + img = np.ndarray( + buffer=vi.write_to_memory(), + dtype=VIPS_FORMAT_NUMPY_DTYPE[vi.format], + shape=[vi.height, vi.width, vi.bands], + ) if vi.bands == 1: img = img[..., 0] @@ -108,9 +107,7 @@ def vips2numpy(vi): def numpy2vips(a, pyvips_interpretation=None): - """ - - """ + """ """ try: vi = pyvips.Image.new_from_array(a) @@ -126,8 +123,9 @@ def numpy2vips(a, pyvips_interpretation=None): # vips seems to expect the array to be little endian, but `a` is big endian linear = linear.byteswap(inplace=False) - vi = pyvips.Image.new_from_memory(linear.data, width, height, bands, - NUMPY_FORMAT_VIPS_DTYPE[a.dtype.name]) + vi = pyvips.Image.new_from_memory( + linear.data, width, height, bands, NUMPY_FORMAT_VIPS_DTYPE[a.dtype.name] + ) if pyvips_interpretation is not None: vi = vi.copy(interpretation=pyvips_interpretation) @@ -143,18 +141,22 @@ def get_slide_extension(src_f): return None src_f = str(src_f).lower() - possible_formats = [fmt for fmt in slide_io.ALL_READABLE_FORMATS if src_f.endswith(fmt.lower())] + possible_formats = [ + fmt for fmt in slide_io.ALL_READABLE_FORMATS if src_f.endswith(fmt.lower()) + ] if len(possible_formats) == 0: msg = f"Do not recognize format of {src_f}" - valtils.print_warning(msg) + logger.warning(msg) return None elif len(possible_formats) == 1: img_extension = possible_formats[0] else: - match_lengths = [np.diff(re.search(fmt, src_f).span())[0] for fmt in possible_formats] + match_lengths = [ + np.diff(re.search(fmt, src_f).span())[0] for fmt in possible_formats + ] best_match_idx = np.argmax(match_lengths) img_extension = possible_formats[best_match_idx] @@ -202,7 +204,7 @@ def get_img_type(img_f): if f_extension is None: return None - if f_extension.lower() == '.ds_store': + if f_extension.lower() == ".ds_store": return kind is_ome_tiff = slide_io.check_is_ome(str(img_f)) @@ -225,13 +227,6 @@ def get_img_type(img_f): if can_use_openslide or is_ome_tiff or is_czi: return TYPE_SLIDE_NAME - # Finally, see if Bioformats can read slide. - if slide_io.BF_READABLE_FORMATS is None: - slide_io.init_jvm() - can_use_bf = f_extension in slide_io.BF_READABLE_FORMATS - if can_use_bf: - return TYPE_SLIDE_NAME - return kind @@ -265,14 +260,21 @@ def determine_if_staining_round(src_dir): master_img_f = None else: - f_list = [os.path.join(src_dir, f) for f in os.listdir(src_dir) if get_img_type(os.path.join(src_dir, f)) is not None and not f.startswith(".")] + f_list = [ + os.path.join(src_dir, f) + for f in os.listdir(src_dir) + if get_img_type(os.path.join(src_dir, f)) is not None + and not f.startswith(".") + ] extensions = [get_slide_extension(f) for f in f_list] format_counts = Counter(extensions) format_count_values = list(format_counts.values()) n_formats = len(format_count_values) if n_formats > 1 and min(format_count_values) == 1: multifile_img = True - master_img_format = list(format_counts.keys())[np.argmin(format_count_values)] + master_img_format = list(format_counts.keys())[ + np.argmin(format_count_values) + ] master_img_file_idx = extensions.index(master_img_format) master_img_f = f_list[master_img_file_idx] else: @@ -283,16 +285,25 @@ def determine_if_staining_round(src_dir): def um_to_px(um, um_per_px): - """Conver mircon to pixel - """ - return um * 1/um_per_px - - -def warp_slide(src_f, transformation_src_shape_rc, transformation_dst_shape_rc, - aligned_slide_shape_rc, M=None, dxdy=None, - level=0, series=None, interp_method="bicubic", - bbox_xywh=None, bg_color=None, reader=None): - """ Warp a slide + """Conver mircon to pixel""" + return um * 1 / um_per_px + + +def warp_slide( + src_f, + transformation_src_shape_rc, + transformation_dst_shape_rc, + aligned_slide_shape_rc, + M=None, + dxdy=None, + level=0, + series=None, + interp_method="bicubic", + bbox_xywh=None, + bg_color=None, + reader=None, +): + """Warp a slide Warp slide according to `M` and/or `non_rigid_dxdy` @@ -350,18 +361,24 @@ def warp_slide(src_f, transformation_src_shape_rc, transformation_dst_shape_rc, if M is None and dxdy is None: return vips_slide - vips_warped = warp_tools.warp_img(img=vips_slide, M=M, bk_dxdy=dxdy, - transformation_dst_shape_rc=transformation_dst_shape_rc, - out_shape_rc=aligned_slide_shape_rc, - transformation_src_shape_rc=transformation_src_shape_rc, - bbox_xywh=bbox_xywh, - bg_color=bg_color, - interp_method=interp_method) + vips_warped = warp_tools.warp_img( + img=vips_slide, + M=M, + bk_dxdy=dxdy, + transformation_dst_shape_rc=transformation_dst_shape_rc, + out_shape_rc=aligned_slide_shape_rc, + transformation_src_shape_rc=transformation_src_shape_rc, + bbox_xywh=bbox_xywh, + bg_color=bg_color, + interp_method=interp_method, + ) return vips_warped -def get_matplotlib_channel_colors(n_colors, name="gist_rainbow", min_lum=0.5, min_c=0.2): +def get_matplotlib_channel_colors( + n_colors, name="gist_rainbow", min_lum=0.5, min_c=0.2 +): """Get channel colors using matplotlib colormaps Parameters @@ -393,7 +410,7 @@ def get_matplotlib_channel_colors(n_colors, name="gist_rainbow", min_lum=0.5, mi jch = preprocessing.rgb2jch(all_colors) all_colors = all_colors[(jch[..., 0] >= min_lum) & (jch[..., 1] >= min_c)] channel_colors = viz.get_n_colors(all_colors, n_colors) - channel_colors = (255*channel_colors).astype(np.uint8) + channel_colors = (255 * channel_colors).astype(np.uint8) return channel_colors @@ -419,14 +436,14 @@ def turbo_channel_colors(n_colors): turbo = viz.turbo_cmap()[40:-40] with colour.utilities.suppress_warnings(colour_usage_warnings=True): - cam16 = colour.convert(turbo, 'sRGB', "CAM16UCS") + cam16 = colour.convert(turbo, "sRGB", "CAM16UCS") cam16[..., 0] *= 1.1 - brighter_turbo = colour.convert(cam16, "CAM16UCS", 'sRGB') + brighter_turbo = colour.convert(cam16, "CAM16UCS", "sRGB") brighter_turbo = np.clip(brighter_turbo, 0, 1) channel_colors = viz.get_n_colors(brighter_turbo, n_colors) - channel_colors = (255*channel_colors).astype(np.uint8) + channel_colors = (255 * channel_colors).astype(np.uint8) return channel_colors @@ -452,6 +469,6 @@ def perceptually_uniform_channel_colors(n_colors): """ cmap = viz.jzazbz_cmap() channel_colors = viz.get_n_colors(cmap, n_colors) - channel_colors = (channel_colors*255).astype(np.uint8) + channel_colors = (channel_colors * 255).astype(np.uint8) return channel_colors diff --git a/src/valis/superglue_models/__init__.py b/src/valis/superglue_models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/valis/superglue_models/matching.py b/src/valis/superglue_models/matching.py similarity index 81% rename from valis/superglue_models/matching.py rename to src/valis/superglue_models/matching.py index 5d174208..066feb55 100644 --- a/valis/superglue_models/matching.py +++ b/src/valis/superglue_models/matching.py @@ -40,21 +40,25 @@ # --------------------------------------------------------------------*/ # %BANNER_END% +import logging import torch from .superpoint import SuperPoint from .superglue import SuperGlue +logger = logging.getLogger(__name__) + class Matching(torch.nn.Module): - """ Image Matching Frontend (SuperPoint + SuperGlue) """ + """Image Matching Frontend (SuperPoint + SuperGlue)""" + def __init__(self, config={}): super().__init__() - self.superpoint = SuperPoint(config.get('superpoint', {})) - self.superglue = SuperGlue(config.get('superglue', {})) + self.superpoint = SuperPoint(config.get("superpoint", {})) + self.superglue = SuperGlue(config.get("superglue", {})) def forward(self, data): - """ Run SuperPoint (optionally) and SuperGlue + """Run SuperPoint (optionally) and SuperGlue SuperPoint is skipped if ['keypoints0', 'keypoints1'] exist in input Args: data: dictionary with minimal keys: ['image0', 'image1'] @@ -62,12 +66,12 @@ def forward(self, data): pred = {} # Extract SuperPoint (keypoints, scores, descriptors) if not provided - if 'keypoints0' not in data: - pred0 = self.superpoint({'image': data['image0']}) - pred = {**pred, **{k+'0': v for k, v in pred0.items()}} - if 'keypoints1' not in data: - pred1 = self.superpoint({'image': data['image1']}) - pred = {**pred, **{k+'1': v for k, v in pred1.items()}} + if "keypoints0" not in data: + pred0 = self.superpoint({"image": data["image0"]}) + pred = {**pred, **{k + "0": v for k, v in pred0.items()}} + if "keypoints1" not in data: + pred1 = self.superpoint({"image": data["image1"]}) + pred = {**pred, **{k + "1": v for k, v in pred1.items()}} # Batch all features # We should either have i) one image per batch, or diff --git a/valis/superglue_models/superglue.py b/src/valis/superglue_models/superglue.py similarity index 66% rename from valis/superglue_models/superglue.py rename to src/valis/superglue_models/superglue.py index 5a89b034..7923fc27 100644 --- a/valis/superglue_models/superglue.py +++ b/src/valis/superglue_models/superglue.py @@ -41,21 +41,23 @@ # %BANNER_END% from copy import deepcopy +import logging from pathlib import Path from typing import List, Tuple import torch from torch import nn +logger = logging.getLogger(__name__) + def MLP(channels: List[int], do_bn: bool = True) -> nn.Module: - """ Multi-layer perceptron """ + """Multi-layer perceptron""" n = len(channels) layers = [] for i in range(1, n): - layers.append( - nn.Conv1d(channels[i - 1], channels[i], kernel_size=1, bias=True)) - if i < (n-1): + layers.append(nn.Conv1d(channels[i - 1], channels[i], kernel_size=1, bias=True)) + if i < (n - 1): if do_bn: layers.append(nn.BatchNorm1d(channels[i])) layers.append(nn.ReLU()) @@ -63,17 +65,18 @@ def MLP(channels: List[int], do_bn: bool = True) -> nn.Module: def normalize_keypoints(kpts, image_shape): - """ Normalize keypoints locations based on image image_shape""" + """Normalize keypoints locations based on image image_shape""" _, _, height, width = image_shape one = kpts.new_tensor(1) - size = torch.stack([one*width, one*height])[None] + size = torch.stack([one * width, one * height])[None] center = size / 2 scaling = size.max(1, keepdim=True).values * 0.7 return (kpts - center[:, None, :]) / scaling[:, None, :] class KeypointEncoder(nn.Module): - """ Joint encoding of visual appearance and location using MLPs""" + """Joint encoding of visual appearance and location using MLPs""" + def __init__(self, feature_dim: int, layers: List[int]) -> None: super().__init__() self.encoder = MLP([3] + layers + [feature_dim]) @@ -84,15 +87,18 @@ def forward(self, kpts, scores): return self.encoder(torch.cat(inputs, dim=1)) -def attention(query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> Tuple[torch.Tensor,torch.Tensor]: +def attention( + query: torch.Tensor, key: torch.Tensor, value: torch.Tensor +) -> Tuple[torch.Tensor, torch.Tensor]: dim = query.shape[1] - scores = torch.einsum('bdhn,bdhm->bhnm', query, key) / dim**.5 + scores = torch.einsum("bdhn,bdhm->bhnm", query, key) / dim**0.5 prob = torch.nn.functional.softmax(scores, dim=-1) - return torch.einsum('bhnm,bdhm->bdhn', prob, value), prob + return torch.einsum("bhnm,bdhm->bdhn", prob, value), prob class MultiHeadedAttention(nn.Module): - """ Multi-head attention to increase model expressivitiy """ + """Multi-head attention to increase model expressivitiy""" + def __init__(self, num_heads: int, d_model: int): super().__init__() assert d_model % num_heads == 0 @@ -101,19 +107,23 @@ def __init__(self, num_heads: int, d_model: int): self.merge = nn.Conv1d(d_model, d_model, kernel_size=1) self.proj = nn.ModuleList([deepcopy(self.merge) for _ in range(3)]) - def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor) -> torch.Tensor: + def forward( + self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor + ) -> torch.Tensor: batch_dim = query.size(0) - query, key, value = [l(x).view(batch_dim, self.dim, self.num_heads, -1) - for l, x in zip(self.proj, (query, key, value))] + query, key, value = [ + l(x).view(batch_dim, self.dim, self.num_heads, -1) + for l, x in zip(self.proj, (query, key, value)) + ] x, _ = attention(query, key, value) - return self.merge(x.contiguous().view(batch_dim, self.dim*self.num_heads, -1)) + return self.merge(x.contiguous().view(batch_dim, self.dim * self.num_heads, -1)) class AttentionalPropagation(nn.Module): def __init__(self, feature_dim: int, num_heads: int): super().__init__() self.attn = MultiHeadedAttention(num_heads, feature_dim) - self.mlp = MLP([feature_dim*2, feature_dim*2, feature_dim]) + self.mlp = MLP([feature_dim * 2, feature_dim * 2, feature_dim]) nn.init.constant_(self.mlp[-1].bias, 0.0) def forward(self, x: torch.Tensor, source: torch.Tensor) -> torch.Tensor: @@ -124,14 +134,16 @@ def forward(self, x: torch.Tensor, source: torch.Tensor) -> torch.Tensor: class AttentionalGNN(nn.Module): def __init__(self, feature_dim: int, layer_names: List[str]) -> None: super().__init__() - self.layers = nn.ModuleList([ - AttentionalPropagation(feature_dim, 4) - for _ in range(len(layer_names))]) + self.layers = nn.ModuleList( + [AttentionalPropagation(feature_dim, 4) for _ in range(len(layer_names))] + ) self.names = layer_names - def forward(self, desc0: torch.Tensor, desc1: torch.Tensor) -> Tuple[torch.Tensor,torch.Tensor]: + def forward( + self, desc0: torch.Tensor, desc1: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: for layer, name in zip(self.layers, self.names): - if name == 'cross': + if name == "cross": src0, src1 = desc1, desc0 else: # if name == 'self': src0, src1 = desc0, desc1 @@ -140,8 +152,10 @@ def forward(self, desc0: torch.Tensor, desc1: torch.Tensor) -> Tuple[torch.Tenso return desc0, desc1 -def log_sinkhorn_iterations(Z: torch.Tensor, log_mu: torch.Tensor, log_nu: torch.Tensor, iters: int) -> torch.Tensor: - """ Perform Sinkhorn Normalization in Log-space for stability""" +def log_sinkhorn_iterations( + Z: torch.Tensor, log_mu: torch.Tensor, log_nu: torch.Tensor, iters: int +) -> torch.Tensor: + """Perform Sinkhorn Normalization in Log-space for stability""" u, v = torch.zeros_like(log_mu), torch.zeros_like(log_nu) for _ in range(iters): u = log_mu - torch.logsumexp(Z + v.unsqueeze(1), dim=2) @@ -149,20 +163,23 @@ def log_sinkhorn_iterations(Z: torch.Tensor, log_mu: torch.Tensor, log_nu: torch return Z + u.unsqueeze(2) + v.unsqueeze(1) -def log_optimal_transport(scores: torch.Tensor, alpha: torch.Tensor, iters: int) -> torch.Tensor: - """ Perform Differentiable Optimal Transport in Log-space for stability""" +def log_optimal_transport( + scores: torch.Tensor, alpha: torch.Tensor, iters: int +) -> torch.Tensor: + """Perform Differentiable Optimal Transport in Log-space for stability""" b, m, n = scores.shape one = scores.new_tensor(1) - ms, ns = (m*one).to(scores), (n*one).to(scores) + ms, ns = (m * one).to(scores), (n * one).to(scores) bins0 = alpha.expand(b, m, 1) bins1 = alpha.expand(b, 1, n) alpha = alpha.expand(b, 1, 1) - couplings = torch.cat([torch.cat([scores, bins0], -1), - torch.cat([bins1, alpha], -1)], 1) + couplings = torch.cat( + [torch.cat([scores, bins0], -1), torch.cat([bins1, alpha], -1)], 1 + ) - norm = - (ms + ns).log() + norm = -(ms + ns).log() log_mu = torch.cat([norm.expand(m), ns.log()[None] + norm]) log_nu = torch.cat([norm.expand(n), ms.log()[None] + norm]) log_mu, log_nu = log_mu[None].expand(b, -1), log_nu[None].expand(b, -1) @@ -194,13 +211,14 @@ class SuperGlue(nn.Module): Networks. In CVPR, 2020. https://arxiv.org/abs/1911.11763 """ + default_config = { - 'descriptor_dim': 256, - 'weights': 'indoor', - 'keypoint_encoder': [32, 64, 128, 256], - 'GNN_layers': ['self', 'cross'] * 9, - 'sinkhorn_iterations': 100, - 'match_threshold': 0.2, + "descriptor_dim": 256, + "weights": "indoor", + "keypoint_encoder": [32, 64, 128, 256], + "GNN_layers": ["self", "cross"] * 9, + "sinkhorn_iterations": 100, + "match_threshold": 0.2, } def __init__(self, config): @@ -208,46 +226,51 @@ def __init__(self, config): self.config = {**self.default_config, **config} self.kenc = KeypointEncoder( - self.config['descriptor_dim'], self.config['keypoint_encoder']) + self.config["descriptor_dim"], self.config["keypoint_encoder"] + ) self.gnn = AttentionalGNN( - feature_dim=self.config['descriptor_dim'], layer_names=self.config['GNN_layers']) + feature_dim=self.config["descriptor_dim"], + layer_names=self.config["GNN_layers"], + ) self.final_proj = nn.Conv1d( - self.config['descriptor_dim'], self.config['descriptor_dim'], - kernel_size=1, bias=True) + self.config["descriptor_dim"], + self.config["descriptor_dim"], + kernel_size=1, + bias=True, + ) - bin_score = torch.nn.Parameter(torch.tensor(1.)) - self.register_parameter('bin_score', bin_score) + bin_score = torch.nn.Parameter(torch.tensor(1.0)) + self.register_parameter("bin_score", bin_score) - assert self.config['weights'] in ['indoor', 'outdoor'] + assert self.config["weights"] in ["indoor", "outdoor"] path = Path(__file__).parent - path = path / 'weights/superglue_{}.pth'.format(self.config['weights']) + path = path / "weights/superglue_{}.pth".format(self.config["weights"]) self.load_state_dict(torch.load(str(path))) - print('Loaded SuperGlue model (\"{}\" weights)'.format( - self.config['weights'])) + logger.info('Loaded SuperGlue model ("%s" weights)', self.config["weights"]) def forward(self, data): """Run SuperGlue on a pair of keypoints and descriptors""" - desc0, desc1 = data['descriptors0'], data['descriptors1'] - kpts0, kpts1 = data['keypoints0'], data['keypoints1'] + desc0, desc1 = data["descriptors0"], data["descriptors1"] + kpts0, kpts1 = data["keypoints0"], data["keypoints1"] if kpts0.shape[1] == 0 or kpts1.shape[1] == 0: # no keypoints shape0, shape1 = kpts0.shape[:-1], kpts1.shape[:-1] return { - 'matches0': kpts0.new_full(shape0, -1, dtype=torch.int), - 'matches1': kpts1.new_full(shape1, -1, dtype=torch.int), - 'matching_scores0': kpts0.new_zeros(shape0), - 'matching_scores1': kpts1.new_zeros(shape1), + "matches0": kpts0.new_full(shape0, -1, dtype=torch.int), + "matches1": kpts1.new_full(shape1, -1, dtype=torch.int), + "matching_scores0": kpts0.new_zeros(shape0), + "matching_scores1": kpts1.new_zeros(shape1), } # Keypoint normalization. - kpts0 = normalize_keypoints(kpts0, data['image0'].shape) - kpts1 = normalize_keypoints(kpts1, data['image1'].shape) + kpts0 = normalize_keypoints(kpts0, data["image0"].shape) + kpts1 = normalize_keypoints(kpts1, data["image1"].shape) # Keypoint MLP encoder. - desc0 = desc0 + self.kenc(kpts0, data['scores0']) - desc1 = desc1 + self.kenc(kpts1, data['scores1']) + desc0 = desc0 + self.kenc(kpts0, data["scores0"]) + desc1 = desc1 + self.kenc(kpts1, data["scores1"]) # Multi-layer Transformer network. desc0, desc1 = self.gnn(desc0, desc1) @@ -256,13 +279,13 @@ def forward(self, data): mdesc0, mdesc1 = self.final_proj(desc0), self.final_proj(desc1) # Compute matching descriptor distance. - scores = torch.einsum('bdn,bdm->bnm', mdesc0, mdesc1) - scores = scores / self.config['descriptor_dim']**.5 + scores = torch.einsum("bdn,bdm->bnm", mdesc0, mdesc1) + scores = scores / self.config["descriptor_dim"] ** 0.5 # Run the optimal transport. scores = log_optimal_transport( - scores, self.bin_score, - iters=self.config['sinkhorn_iterations']) + scores, self.bin_score, iters=self.config["sinkhorn_iterations"] + ) # Get the matches with score above "match_threshold". max0, max1 = scores[:, :-1, :-1].max(2), scores[:, :-1, :-1].max(1) @@ -272,14 +295,14 @@ def forward(self, data): zero = scores.new_tensor(0) mscores0 = torch.where(mutual0, max0.values.exp(), zero) mscores1 = torch.where(mutual1, mscores0.gather(1, indices1), zero) - valid0 = mutual0 & (mscores0 > self.config['match_threshold']) + valid0 = mutual0 & (mscores0 > self.config["match_threshold"]) valid1 = mutual1 & valid0.gather(1, indices1) indices0 = torch.where(valid0, indices0, indices0.new_tensor(-1)) indices1 = torch.where(valid1, indices1, indices1.new_tensor(-1)) return { - 'matches0': indices0, # use -1 for invalid match - 'matches1': indices1, # use -1 for invalid match - 'matching_scores0': mscores0, - 'matching_scores1': mscores1, + "matches0": indices0, # use -1 for invalid match + "matches1": indices1, # use -1 for invalid match + "matching_scores0": mscores0, + "matching_scores1": mscores1, } diff --git a/valis/superglue_models/superpoint.py b/src/valis/superglue_models/superpoint.py similarity index 72% rename from valis/superglue_models/superpoint.py rename to src/valis/superglue_models/superpoint.py index 7d22443d..508c64db 100644 --- a/valis/superglue_models/superpoint.py +++ b/src/valis/superglue_models/superpoint.py @@ -40,17 +40,22 @@ # --------------------------------------------------------------------*/ # %BANNER_END% +import logging import torch from torch import nn from pathlib import Path +logger = logging.getLogger(__name__) + + def simple_nms(scores, nms_radius: int): - """ Fast Non-maximum suppression to remove nearby points """ - assert(nms_radius >= 0) + """Fast Non-maximum suppression to remove nearby points""" + assert nms_radius >= 0 def max_pool(x): return torch.nn.functional.max_pool2d( - x, kernel_size=nms_radius*2+1, stride=1, padding=nms_radius) + x, kernel_size=nms_radius * 2 + 1, stride=1, padding=nms_radius + ) zeros = torch.zeros_like(scores) max_mask = scores == max_pool(scores) @@ -63,7 +68,7 @@ def max_pool(x): def remove_borders(keypoints, scores, border: int, height: int, width: int): - """ Removes keypoints too close to the border """ + """Removes keypoints too close to the border""" mask_h = (keypoints[:, 0] >= border) & (keypoints[:, 0] < (height - border)) mask_w = (keypoints[:, 1] >= border) & (keypoints[:, 1] < (width - border)) mask = mask_h & mask_w @@ -78,17 +83,22 @@ def top_k_keypoints(keypoints, scores, k: int): def sample_descriptors(keypoints, descriptors, s: int = 8): - """ Interpolate descriptors at keypoint locations """ + """Interpolate descriptors at keypoint locations""" b, c, h, w = descriptors.shape keypoints = keypoints - s / 2 + 0.5 - keypoints /= torch.tensor([(w*s - s/2 - 0.5), (h*s - s/2 - 0.5)], - ).to(keypoints)[None] - keypoints = keypoints*2 - 1 # normalize to (-1, 1) - args = {'align_corners': True} if torch.__version__ >= '1.3' else {} + keypoints /= torch.tensor( + [(w * s - s / 2 - 0.5), (h * s - s / 2 - 0.5)], + ).to( + keypoints + )[None] + keypoints = keypoints * 2 - 1 # normalize to (-1, 1) + args = {"align_corners": True} if torch.__version__ >= "1.3" else {} descriptors = torch.nn.functional.grid_sample( - descriptors, keypoints.view(b, 1, -1, 2), mode='bilinear', **args) + descriptors, keypoints.view(b, 1, -1, 2), mode="bilinear", **args + ) descriptors = torch.nn.functional.normalize( - descriptors.reshape(b, c, -1), p=2, dim=1) + descriptors.reshape(b, c, -1), p=2, dim=1 + ) return descriptors @@ -100,12 +110,13 @@ class SuperPoint(nn.Module): Rabinovich. In CVPRW, 2019. https://arxiv.org/abs/1712.07629 """ + default_config = { - 'descriptor_dim': 256, - 'nms_radius': 4, - 'keypoint_threshold': 0.005, - 'max_keypoints': -1, - 'remove_borders': 4, + "descriptor_dim": 256, + "nms_radius": 4, + "keypoint_threshold": 0.005, + "max_keypoints": -1, + "remove_borders": 4, } def __init__(self, config): @@ -129,12 +140,12 @@ def __init__(self, config): self.convPb = nn.Conv2d(c5, 65, kernel_size=1, stride=1, padding=0) self.convDa = nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1) self.convDb = nn.Conv2d( - c5, self.config['descriptor_dim'], - kernel_size=1, stride=1, padding=0) + c5, self.config["descriptor_dim"], kernel_size=1, stride=1, padding=0 + ) # print("set convDb") # import os - path = Path(__file__).parent / 'weights/superpoint_v1.pth' + path = Path(__file__).parent / "weights/superpoint_v1.pth" device = self.config["device"] # self.load_state_dict(torch.load(str(path), map_location=device)) @@ -142,17 +153,17 @@ def __init__(self, config): # print("loaded weights") self.load_state_dict(weights) - mk = self.config['max_keypoints'] - # print(f"max kp = {mk}") + mk = self.config["max_keypoints"] + # logger.debug("max kp = %s", mk) if mk == 0 or mk < -1: - raise ValueError('\"max_keypoints\" must be positive or \"-1\"') + raise ValueError('"max_keypoints" must be positive or "-1"') - print('Loaded SuperPoint model') + logger.info("Loaded SuperPoint model") def forward(self, data): - """ Compute keypoints, scores, descriptors for image """ + """Compute keypoints, scores, descriptors for image""" # Shared Encoder - x = self.relu(self.conv1a(data['image'])) + x = self.relu(self.conv1a(data["image"])) x = self.relu(self.conv1b(x)) x = self.pool(x) x = self.relu(self.conv2a(x)) @@ -170,25 +181,35 @@ def forward(self, data): scores = torch.nn.functional.softmax(scores, 1)[:, :-1] b, _, h, w = scores.shape scores = scores.permute(0, 2, 3, 1).reshape(b, h, w, 8, 8) - scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h*8, w*8) - scores = simple_nms(scores, self.config['nms_radius']) + scores = scores.permute(0, 1, 3, 2, 4).reshape(b, h * 8, w * 8) + scores = simple_nms(scores, self.config["nms_radius"]) # Extract keypoints keypoints = [ - torch.nonzero(s > self.config['keypoint_threshold']) - for s in scores] + torch.nonzero(s > self.config["keypoint_threshold"]) for s in scores + ] scores = [s[tuple(k.t())] for s, k in zip(scores, keypoints)] # Discard keypoints near the image borders - keypoints, scores = list(zip(*[ - remove_borders(k, s, self.config['remove_borders'], h*8, w*8) - for k, s in zip(keypoints, scores)])) + keypoints, scores = list( + zip( + *[ + remove_borders(k, s, self.config["remove_borders"], h * 8, w * 8) + for k, s in zip(keypoints, scores) + ] + ) + ) # Keep the k keypoints with highest score - if self.config['max_keypoints'] >= 0: - keypoints, scores = list(zip(*[ - top_k_keypoints(k, s, self.config['max_keypoints']) - for k, s in zip(keypoints, scores)])) + if self.config["max_keypoints"] >= 0: + keypoints, scores = list( + zip( + *[ + top_k_keypoints(k, s, self.config["max_keypoints"]) + for k, s in zip(keypoints, scores) + ] + ) + ) # Convert (h, w) to (x, y) keypoints = [torch.flip(k, [1]).float() for k in keypoints] @@ -199,11 +220,13 @@ def forward(self, data): descriptors = torch.nn.functional.normalize(descriptors, p=2, dim=1) # Extract descriptors - descriptors = [sample_descriptors(k[None], d[None], 8)[0] - for k, d in zip(keypoints, descriptors)] + descriptors = [ + sample_descriptors(k[None], d[None], 8)[0] + for k, d in zip(keypoints, descriptors) + ] return { - 'keypoints': keypoints, - 'scores': scores, - 'descriptors': descriptors, + "keypoints": keypoints, + "scores": scores, + "descriptors": descriptors, } diff --git a/valis/superglue_models/utils.py b/src/valis/superglue_models/utils.py similarity index 63% rename from valis/superglue_models/utils.py rename to src/valis/superglue_models/utils.py index 65c64d10..49fe3647 100644 --- a/valis/superglue_models/utils.py +++ b/src/valis/superglue_models/utils.py @@ -46,16 +46,22 @@ import time from collections import OrderedDict from threading import Thread +import logging +import matplotlib +import matplotlib.pyplot as plt import numpy as np import cv2 import torch + +logger = logging.getLogger(__name__) + # import matplotlib.pyplot as plt # import matplotlib # matplotlib.use('Agg') class AverageTimer: - """ Class to help manage printing simple timing of code execution. """ + """Class to help manage printing simple timing of code execution.""" def __init__(self, smoothing=0.3, newline=False): self.smoothing = smoothing @@ -71,7 +77,7 @@ def reset(self): for name in self.will_print: self.will_print[name] = False - def update(self, name='default'): + def update(self, name="default"): now = time.time() dt = now - self.last_time if name in self.times: @@ -80,29 +86,27 @@ def update(self, name='default'): self.will_print[name] = True self.last_time = now - def print(self, text='Timer'): - total = 0. - print('[{}]'.format(text), end=' ') + def print(self, text="Timer"): + total = 0.0 + parts = ["[{}]".format(text)] for key in self.times: val = self.times[key] if self.will_print[key]: - print('%s=%.3f' % (key, val), end=' ') + parts.append("%s=%.3f" % (key, val)) total += val - print('total=%.3f sec {%.1f FPS}' % (total, 1./total), end=' ') - if self.newline: - print(flush=True) - else: - print(end='\r', flush=True) + parts.append("total=%.3f sec {%.1f FPS}" % (total, 1.0 / total)) + logger.debug(" ".join(parts)) self.reset() class VideoStreamer: - """ Class to help process image streams. Four types of possible inputs:" - 1.) USB Webcam. - 2.) An IP camera - 3.) A directory of images (files in directory matching 'image_glob'). - 4.) A video file, such as an .mp4 or .avi file. + """Class to help process image streams. Four types of possible inputs:" + 1.) USB Webcam. + 2.) An IP camera + 3.) A directory of images (files in directory matching 'image_glob'). + 4.) A video file, such as an .mp4 or .avi file. """ + def __init__(self, basedir, resize, skip, image_glob, max_length=1000000): self._ip_grabbed = False self._ip_running = False @@ -119,45 +123,45 @@ def __init__(self, basedir, resize, skip, image_glob, max_length=1000000): self.skip = skip self.max_length = max_length if isinstance(basedir, int) or basedir.isdigit(): - print('==> Processing USB webcam input: {}'.format(basedir)) + logger.info("==> Processing USB webcam input: %s", basedir) self.cap = cv2.VideoCapture(int(basedir)) self.listing = range(0, self.max_length) - elif basedir.startswith(('http', 'rtsp')): - print('==> Processing IP camera input: {}'.format(basedir)) + elif basedir.startswith(("http", "rtsp")): + logger.info("==> Processing IP camera input: %s", basedir) self.cap = cv2.VideoCapture(basedir) self.start_ip_camera_thread() self._ip_camera = True self.listing = range(0, self.max_length) elif Path(basedir).is_dir(): - print('==> Processing image directory input: {}'.format(basedir)) + logger.info("==> Processing image directory input: %s", basedir) self.listing = list(Path(basedir).glob(image_glob[0])) for j in range(1, len(image_glob)): image_path = list(Path(basedir).glob(image_glob[j])) self.listing = self.listing + image_path self.listing.sort() - self.listing = self.listing[::self.skip] + self.listing = self.listing[:: self.skip] self.max_length = np.min([self.max_length, len(self.listing)]) if self.max_length == 0: - raise IOError('No images found (maybe bad \'image_glob\' ?)') - self.listing = self.listing[:self.max_length] + raise IOError("No images found (maybe bad 'image_glob' ?)") + self.listing = self.listing[: self.max_length] self.camera = False elif Path(basedir).exists(): - print('==> Processing video input: {}'.format(basedir)) + logger.info("==> Processing video input: %s", basedir) self.cap = cv2.VideoCapture(basedir) self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) num_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.listing = range(0, num_frames) - self.listing = self.listing[::self.skip] + self.listing = self.listing[:: self.skip] self.video_file = True self.max_length = np.min([self.max_length, len(self.listing)]) - self.listing = self.listing[:self.max_length] + self.listing = self.listing[: self.max_length] else: - raise ValueError('VideoStreamer input \"{}\" not recognized.'.format(basedir)) + raise ValueError('VideoStreamer input "{}" not recognized.'.format(basedir)) if self.camera and not self.cap.isOpened(): - raise IOError('Could not read camera') + raise IOError("Could not read camera") def load_image(self, impath): - """ Read image as grayscale and resize to img_size. + """Read image as grayscale and resize to img_size. Inputs impath: Path to input image. Returns @@ -165,15 +169,14 @@ def load_image(self, impath): """ grayim = cv2.imread(impath, 0) if grayim is None: - raise Exception('Error reading image %s' % impath) + raise Exception("Error reading image %s" % impath) w, h = grayim.shape[1], grayim.shape[0] w_new, h_new = process_resize(w, h, self.resize) - grayim = cv2.resize( - grayim, (w_new, h_new), interpolation=self.interp) + grayim = cv2.resize(grayim, (w_new, h_new), interpolation=self.interp) return grayim def next_frame(self): - """ Return the next frame, and increment internal counter. + """Return the next frame, and increment internal counter. Returns image: Next H x W image. status: True or False depending whether image was loaded. @@ -184,9 +187,9 @@ def next_frame(self): if self.camera: if self._ip_camera: - #Wait for first image, making sure we haven't exited + # Wait for first image, making sure we haven't exited while self._ip_grabbed is False and self._ip_exited is False: - time.sleep(.001) + time.sleep(0.001) ret, image = self._ip_grabbed, self._ip_image.copy() if ret is False: @@ -194,15 +197,14 @@ def next_frame(self): else: ret, image = self.cap.read() if ret is False: - print('VideoStreamer: Cannot get image from camera') + logger.warning("VideoStreamer: Cannot get image from camera") return (None, False) w, h = image.shape[1], image.shape[0] if self.video_file: self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.listing[self.i]) w_new, h_new = process_resize(w, h, self.resize) - image = cv2.resize(image, (w_new, h_new), - interpolation=self.interp) + image = cv2.resize(image, (w_new, h_new), interpolation=self.interp) image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) else: image_file = str(self.listing[self.i]) @@ -229,19 +231,20 @@ def update_ip_camera(self): self._ip_image = img self._ip_grabbed = ret self._ip_index += 1 - #print('IPCAMERA THREAD got frame {}'.format(self._ip_index)) - + # logger.debug('IPCAMERA THREAD got frame %s', self._ip_index) def cleanup(self): self._ip_running = False + # --- PREPROCESSING --- + def process_resize(w, h, resize): - assert(len(resize) > 0 and len(resize) <= 2) + assert len(resize) > 0 and len(resize) <= 2 if len(resize) == 1 and resize[0] > -1: scale = resize[0] / max(h, w) - w_new, h_new = int(round(w*scale)), int(round(h*scale)) + w_new, h_new = int(round(w * scale)), int(round(h * scale)) elif len(resize) == 1 and resize[0] == -1: w_new, h_new = w, h else: # len(resize) == 2: @@ -249,15 +252,15 @@ def process_resize(w, h, resize): # Issue warning if resolution is too small or too large. if max(w_new, h_new) < 160: - print('Warning: input resolution is very small, results may vary') + logger.warning("Warning: input resolution is very small, results may vary") elif max(w_new, h_new) > 2000: - print('Warning: input resolution is very large, results may vary') + logger.warning("Warning: input resolution is very large, results may vary") return w_new, h_new def frame2tensor(frame, device): - return torch.from_numpy(frame/255.).float()[None, None].to(device) + return torch.from_numpy(frame / 255.0).float()[None, None].to(device) def read_image(path, device, resize, rotation, resize_float): @@ -269,9 +272,9 @@ def read_image(path, device, resize, rotation, resize_float): scales = (float(w) / float(w_new), float(h) / float(h_new)) if resize_float: - image = cv2.resize(image.astype('float32'), (w_new, h_new)) + image = cv2.resize(image.astype("float32"), (w_new, h_new)) else: - image = cv2.resize(image, (w_new, h_new)).astype('float32') + image = cv2.resize(image, (w_new, h_new)).astype("float32") if rotation != 0: image = np.rot90(image, k=rotation) @@ -296,16 +299,15 @@ def estimate_pose(kpts0, kpts1, K0, K1, thresh, conf=0.99999): kpts1 = (kpts1 - K1[[0, 1], [2, 2]][None]) / K1[[0, 1], [0, 1]][None] E, mask = cv2.findEssentialMat( - kpts0, kpts1, np.eye(3), threshold=norm_thresh, prob=conf, - method=cv2.RANSAC) + kpts0, kpts1, np.eye(3), threshold=norm_thresh, prob=conf, method=cv2.RANSAC + ) assert E is not None best_num_inliers = 0 ret = None for _E in np.split(E, len(E) / 3): - n, R, t, _ = cv2.recoverPose( - _E, kpts0, kpts1, np.eye(3), 1e9, mask=mask) + n, R, t, _ = cv2.recoverPose(_E, kpts0, kpts1, np.eye(3), 1e9, mask=mask) if n > best_num_inliers: best_num_inliers = n ret = (R, t[:, 0], mask.ravel() > 0) @@ -315,36 +317,42 @@ def estimate_pose(kpts0, kpts1, K0, K1, thresh, conf=0.99999): def rotate_intrinsics(K, image_shape, rot): """image_shape is the shape of the image after rotation""" assert rot <= 3 - h, w = image_shape[:2][::-1 if (rot % 2) else 1] + h, w = image_shape[:2][:: -1 if (rot % 2) else 1] fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] rot = rot % 4 if rot == 1: - return np.array([[fy, 0., cy], - [0., fx, w-1-cx], - [0., 0., 1.]], dtype=K.dtype) + return np.array( + [[fy, 0.0, cy], [0.0, fx, w - 1 - cx], [0.0, 0.0, 1.0]], dtype=K.dtype + ) elif rot == 2: - return np.array([[fx, 0., w-1-cx], - [0., fy, h-1-cy], - [0., 0., 1.]], dtype=K.dtype) + return np.array( + [[fx, 0.0, w - 1 - cx], [0.0, fy, h - 1 - cy], [0.0, 0.0, 1.0]], + dtype=K.dtype, + ) else: # if rot == 3: - return np.array([[fy, 0., h-1-cy], - [0., fx, cx], - [0., 0., 1.]], dtype=K.dtype) + return np.array( + [[fy, 0.0, h - 1 - cy], [0.0, fx, cx], [0.0, 0.0, 1.0]], dtype=K.dtype + ) def rotate_pose_inplane(i_T_w, rot): rotation_matrices = [ - np.array([[np.cos(r), -np.sin(r), 0., 0.], - [np.sin(r), np.cos(r), 0., 0.], - [0., 0., 1., 0.], - [0., 0., 0., 1.]], dtype=np.float32) + np.array( + [ + [np.cos(r), -np.sin(r), 0.0, 0.0], + [np.sin(r), np.cos(r), 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=np.float32, + ) for r in [np.deg2rad(d) for d in (0, 270, 180, 90)] ] return np.dot(rotation_matrices[rot], i_T_w) def scale_intrinsics(K, scales): - scales = np.diag([1./scales[0], 1./scales[1], 1.]) + scales = np.diag([1.0 / scales[0], 1.0 / scales[1], 1.0]) return np.dot(scales, K) @@ -359,24 +367,22 @@ def compute_epipolar_error(kpts0, kpts1, T_0to1, K0, K1): kpts1 = to_homogeneous(kpts1) t0, t1, t2 = T_0to1[:3, 3] - t_skew = np.array([ - [0, -t2, t1], - [t2, 0, -t0], - [-t1, t0, 0] - ]) + t_skew = np.array([[0, -t2, t1], [t2, 0, -t0], [-t1, t0, 0]]) E = t_skew @ T_0to1[:3, :3] Ep0 = kpts0 @ E.T # N x 3 p1Ep0 = np.sum(kpts1 * Ep0, -1) # N Etp1 = kpts1 @ E # N x 3 - d = p1Ep0**2 * (1.0 / (Ep0[:, 0]**2 + Ep0[:, 1]**2) - + 1.0 / (Etp1[:, 0]**2 + Etp1[:, 1]**2)) + d = p1Ep0**2 * ( + 1.0 / (Ep0[:, 0] ** 2 + Ep0[:, 1] ** 2) + + 1.0 / (Etp1[:, 0] ** 2 + Etp1[:, 1] ** 2) + ) return d def angle_error_mat(R1, R2): cos = (np.trace(np.dot(R1.T, R2)) - 1) / 2 - cos = np.clip(cos, -1., 1.) # numercial errors can make it out of bounds + cos = np.clip(cos, -1.0, 1.0) # numercial errors can make it out of bounds return np.rad2deg(np.abs(np.arccos(cos))) @@ -398,27 +404,27 @@ def pose_auc(errors, thresholds): sort_idx = np.argsort(errors) errors = np.array(errors.copy())[sort_idx] recall = (np.arange(len(errors)) + 1) / len(errors) - errors = np.r_[0., errors] - recall = np.r_[0., recall] + errors = np.r_[0.0, errors] + recall = np.r_[0.0, recall] aucs = [] for t in thresholds: last_index = np.searchsorted(errors, t) - r = np.r_[recall[:last_index], recall[last_index-1]] + r = np.r_[recall[:last_index], recall[last_index - 1]] e = np.r_[errors[:last_index], t] - aucs.append(np.trapz(r, x=e)/t) + aucs.append(np.trapz(r, x=e) / t) return aucs # --- VISUALIZATION --- -def plot_image_pair(imgs, dpi=100, size=6, pad=.5): +def plot_image_pair(imgs, dpi=100, size=6, pad=0.5): n = len(imgs) - assert n == 2, 'number of images must be two' - figsize = (size*n, size*3/4) if size is not None else None + assert n == 2, "number of images must be two" + figsize = (size * n, size * 3 / 4) if size is not None else None _, ax = plt.subplots(1, n, figsize=figsize, dpi=dpi) for i in range(n): - ax[i].imshow(imgs[i], cmap=plt.get_cmap('gray'), vmin=0, vmax=255) + ax[i].imshow(imgs[i], cmap=plt.get_cmap("gray"), vmin=0, vmax=255) ax[i].get_yaxis().set_ticks([]) ax[i].get_xaxis().set_ticks([]) for spine in ax[i].spines.values(): # remove frame @@ -426,7 +432,7 @@ def plot_image_pair(imgs, dpi=100, size=6, pad=.5): plt.tight_layout(pad=pad) -def plot_keypoints(kpts0, kpts1, color='w', ps=2): +def plot_keypoints(kpts0, kpts1, color="w", ps=2): ax = plt.gcf().axes ax[0].scatter(kpts0[:, 0], kpts0[:, 1], c=color, s=ps) ax[1].scatter(kpts1[:, 0], kpts1[:, 1], c=color, s=ps) @@ -441,59 +447,116 @@ def plot_matches(kpts0, kpts1, color, lw=1.5, ps=4): fkpts0 = transFigure.transform(ax[0].transData.transform(kpts0)) fkpts1 = transFigure.transform(ax[1].transData.transform(kpts1)) - fig.lines = [matplotlib.lines.Line2D( - (fkpts0[i, 0], fkpts1[i, 0]), (fkpts0[i, 1], fkpts1[i, 1]), zorder=1, - transform=fig.transFigure, c=color[i], linewidth=lw) - for i in range(len(kpts0))] + fig.lines = [ + matplotlib.lines.Line2D( + (fkpts0[i, 0], fkpts1[i, 0]), + (fkpts0[i, 1], fkpts1[i, 1]), + zorder=1, + transform=fig.transFigure, + c=color[i], + linewidth=lw, + ) + for i in range(len(kpts0)) + ] ax[0].scatter(kpts0[:, 0], kpts0[:, 1], c=color, s=ps) ax[1].scatter(kpts1[:, 0], kpts1[:, 1], c=color, s=ps) -def make_matching_plot(image0, image1, kpts0, kpts1, mkpts0, mkpts1, - color, text, path, show_keypoints=False, - fast_viz=False, opencv_display=False, - opencv_title='matches', small_text=[]): +def make_matching_plot( + image0, + image1, + kpts0, + kpts1, + mkpts0, + mkpts1, + color, + text, + path, + show_keypoints=False, + fast_viz=False, + opencv_display=False, + opencv_title="matches", + small_text=[], +): if fast_viz: - make_matching_plot_fast(image0, image1, kpts0, kpts1, mkpts0, mkpts1, - color, text, path, show_keypoints, 10, - opencv_display, opencv_title, small_text) + make_matching_plot_fast( + image0, + image1, + kpts0, + kpts1, + mkpts0, + mkpts1, + color, + text, + path, + show_keypoints, + 10, + opencv_display, + opencv_title, + small_text, + ) return plot_image_pair([image0, image1]) if show_keypoints: - plot_keypoints(kpts0, kpts1, color='k', ps=4) - plot_keypoints(kpts0, kpts1, color='w', ps=2) + plot_keypoints(kpts0, kpts1, color="k", ps=4) + plot_keypoints(kpts0, kpts1, color="w", ps=2) plot_matches(mkpts0, mkpts1, color) fig = plt.gcf() - txt_color = 'k' if image0[:100, :150].mean() > 200 else 'w' + txt_color = "k" if image0[:100, :150].mean() > 200 else "w" fig.text( - 0.01, 0.99, '\n'.join(text), transform=fig.axes[0].transAxes, - fontsize=15, va='top', ha='left', color=txt_color) - - txt_color = 'k' if image0[-100:, :150].mean() > 200 else 'w' + 0.01, + 0.99, + "\n".join(text), + transform=fig.axes[0].transAxes, + fontsize=15, + va="top", + ha="left", + color=txt_color, + ) + + txt_color = "k" if image0[-100:, :150].mean() > 200 else "w" fig.text( - 0.01, 0.01, '\n'.join(small_text), transform=fig.axes[0].transAxes, - fontsize=5, va='bottom', ha='left', color=txt_color) - - plt.savefig(str(path), bbox_inches='tight', pad_inches=0) + 0.01, + 0.01, + "\n".join(small_text), + transform=fig.axes[0].transAxes, + fontsize=5, + va="bottom", + ha="left", + color=txt_color, + ) + + plt.savefig(str(path), bbox_inches="tight", pad_inches=0) plt.close() -def make_matching_plot_fast(image0, image1, kpts0, kpts1, mkpts0, - mkpts1, color, text, path=None, - show_keypoints=False, margin=10, - opencv_display=False, opencv_title='', - small_text=[]): +def make_matching_plot_fast( + image0, + image1, + kpts0, + kpts1, + mkpts0, + mkpts1, + color, + text, + path=None, + show_keypoints=False, + margin=10, + opencv_display=False, + opencv_title="", + small_text=[], +): H0, W0 = image0.shape H1, W1 = image1.shape H, W = max(H0, H1), W0 + W1 + margin - out = 255*np.ones((H, W), np.uint8) + out = 255 * np.ones((H, W), np.uint8) out[:H0, :W0] = image0 - out[:H1, W0+margin:] = image1 - out = np.stack([out]*3, -1) + out[:H1, W0 + margin :] = image1 + out = np.stack([out] * 3, -1) if show_keypoints: kpts0, kpts1 = np.round(kpts0).astype(int), np.round(kpts1).astype(int) @@ -503,42 +566,77 @@ def make_matching_plot_fast(image0, image1, kpts0, kpts1, mkpts0, cv2.circle(out, (x, y), 2, black, -1, lineType=cv2.LINE_AA) cv2.circle(out, (x, y), 1, white, -1, lineType=cv2.LINE_AA) for x, y in kpts1: - cv2.circle(out, (x + margin + W0, y), 2, black, -1, - lineType=cv2.LINE_AA) - cv2.circle(out, (x + margin + W0, y), 1, white, -1, - lineType=cv2.LINE_AA) + cv2.circle(out, (x + margin + W0, y), 2, black, -1, lineType=cv2.LINE_AA) + cv2.circle(out, (x + margin + W0, y), 1, white, -1, lineType=cv2.LINE_AA) mkpts0, mkpts1 = np.round(mkpts0).astype(int), np.round(mkpts1).astype(int) - color = (np.array(color[:, :3])*255).astype(int)[:, ::-1] + color = (np.array(color[:, :3]) * 255).astype(int)[:, ::-1] for (x0, y0), (x1, y1), c in zip(mkpts0, mkpts1, color): c = c.tolist() - cv2.line(out, (x0, y0), (x1 + margin + W0, y1), - color=c, thickness=1, lineType=cv2.LINE_AA) + cv2.line( + out, + (x0, y0), + (x1 + margin + W0, y1), + color=c, + thickness=1, + lineType=cv2.LINE_AA, + ) # display line end-points as circles cv2.circle(out, (x0, y0), 2, c, -1, lineType=cv2.LINE_AA) - cv2.circle(out, (x1 + margin + W0, y1), 2, c, -1, - lineType=cv2.LINE_AA) + cv2.circle(out, (x1 + margin + W0, y1), 2, c, -1, lineType=cv2.LINE_AA) # Scale factor for consistent visualization across scales. - sc = min(H / 640., 2.0) + sc = min(H / 640.0, 2.0) # Big text. Ht = int(30 * sc) # text height txt_color_fg = (255, 255, 255) txt_color_bg = (0, 0, 0) for i, t in enumerate(text): - cv2.putText(out, t, (int(8*sc), Ht*(i+1)), cv2.FONT_HERSHEY_DUPLEX, - 1.0*sc, txt_color_bg, 2, cv2.LINE_AA) - cv2.putText(out, t, (int(8*sc), Ht*(i+1)), cv2.FONT_HERSHEY_DUPLEX, - 1.0*sc, txt_color_fg, 1, cv2.LINE_AA) + cv2.putText( + out, + t, + (int(8 * sc), Ht * (i + 1)), + cv2.FONT_HERSHEY_DUPLEX, + 1.0 * sc, + txt_color_bg, + 2, + cv2.LINE_AA, + ) + cv2.putText( + out, + t, + (int(8 * sc), Ht * (i + 1)), + cv2.FONT_HERSHEY_DUPLEX, + 1.0 * sc, + txt_color_fg, + 1, + cv2.LINE_AA, + ) # Small text. Ht = int(18 * sc) # text height for i, t in enumerate(reversed(small_text)): - cv2.putText(out, t, (int(8*sc), int(H-Ht*(i+.6))), cv2.FONT_HERSHEY_DUPLEX, - 0.5*sc, txt_color_bg, 2, cv2.LINE_AA) - cv2.putText(out, t, (int(8*sc), int(H-Ht*(i+.6))), cv2.FONT_HERSHEY_DUPLEX, - 0.5*sc, txt_color_fg, 1, cv2.LINE_AA) + cv2.putText( + out, + t, + (int(8 * sc), int(H - Ht * (i + 0.6))), + cv2.FONT_HERSHEY_DUPLEX, + 0.5 * sc, + txt_color_bg, + 2, + cv2.LINE_AA, + ) + cv2.putText( + out, + t, + (int(8 * sc), int(H - Ht * (i + 0.6))), + cv2.FONT_HERSHEY_DUPLEX, + 0.5 * sc, + txt_color_fg, + 1, + cv2.LINE_AA, + ) if path is not None: cv2.imwrite(str(path), out) @@ -552,4 +650,5 @@ def make_matching_plot_fast(image0, image1, kpts0, kpts1, mkpts0, def error_colormap(x): return np.clip( - np.stack([2-x*2, x*2, np.zeros_like(x), np.ones_like(x)], -1), 0, 1) + np.stack([2 - x * 2, x * 2, np.zeros_like(x), np.ones_like(x)], -1), 0, 1 + ) diff --git a/valis/superglue_models/weights/superglue_indoor.pth b/src/valis/superglue_models/weights/superglue_indoor.pth similarity index 100% rename from valis/superglue_models/weights/superglue_indoor.pth rename to src/valis/superglue_models/weights/superglue_indoor.pth diff --git a/valis/superglue_models/weights/superglue_outdoor.pth b/src/valis/superglue_models/weights/superglue_outdoor.pth similarity index 100% rename from valis/superglue_models/weights/superglue_outdoor.pth rename to src/valis/superglue_models/weights/superglue_outdoor.pth diff --git a/valis/superglue_models/weights/superpoint_v1.pth b/src/valis/superglue_models/weights/superpoint_v1.pth similarity index 100% rename from valis/superglue_models/weights/superpoint_v1.pth rename to src/valis/superglue_models/weights/superpoint_v1.pth diff --git a/valis/valtils.py b/src/valis/valtils.py similarity index 68% rename from valis/valtils.py rename to src/valis/valtils.py index 2c3be7f9..4970cd81 100644 --- a/valis/valtils.py +++ b/src/valis/valtils.py @@ -4,8 +4,6 @@ import re import os import multiprocessing -from colorama import init as color_init -from colorama import Fore, Style import functools import pyvips import warnings @@ -13,24 +11,23 @@ from collections import defaultdict import platform import subprocess +import logging +logger = logging.getLogger(__name__) -color_init() - -def print_warning(msg, warning_type=UserWarning, rgb=Fore.YELLOW, traceback_msg=None): - """Print warning message with color - """ - warning_msg = f"{rgb}{msg}{Style.RESET_ALL}" - if warning_type is None: - print(warning_msg) +def print_warning(msg, warning_type=UserWarning, rgb=None, traceback_msg=None): + """Deprecated: Use logging instead. This function is kept for backwards compatibility.""" + if warning_type == DeprecationWarning: + logger.warning(msg) + elif warning_type is None: + logger.info(msg) else: - warnings.simplefilter('always', warning_type) - warnings.warn(warning_msg, warning_type) + logger.warning(msg) if traceback_msg is not None: - traceback_msg_rgb = f"{rgb}{traceback_msg}{Style.RESET_ALL}" - print(traceback_msg_rgb) + logger.warning(traceback_msg) + def deprecated_args(**aliases): def deco(f): @@ -43,22 +40,24 @@ def wrapper(*args, **kwargs): return deco + def rename_kwargs(func_name, kwargs, aliases): for alias, new in aliases.items(): if alias in kwargs: if new in kwargs: - raise TypeError('{} received both {} and {}'.format( - func_name, alias, new)) + raise TypeError( + "{} received both {} and {}".format(func_name, alias, new) + ) - msg = f'{alias} is deprecated; use {new} instead' - print_warning(msg, DeprecationWarning) + msg = f"{alias} is deprecated; use {new} instead" + logger.warning(msg) kwargs[new] = kwargs.pop(alias) @contextlib.contextmanager def HiddenPrints(): - with contextlib.redirect_stdout(open(os.devnull, 'w')): + with contextlib.redirect_stdout(open(os.devnull, "w")): yield @@ -77,17 +76,22 @@ def pad_strings(string_list, side="r"): return padded_strings + def check_m1_mac(): is_mac_m1 = False if platform.system() == "Darwin": - cpu_kind = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]).decode('utf-8') + cpu_kind = subprocess.check_output( + ["sysctl", "-n", "machdep.cpu.brand_string"] + ).decode("utf-8") if cpu_kind.startswith("Apple M1"): is_mac_m1 = True return is_mac_m1 -from . import slide_tools # Put import here to avoid circular imports +from . import slide_tools # Put import here to avoid circular imports + + def get_name(f): fonly = os.path.split(f)[1] @@ -122,9 +126,9 @@ def levenshtein_d(str1, str2): else: # Choose the minimum cost operation curr_row[j] = 1 + min( - curr_row[j - 1], # Insert - prev_row[j], # Remove - prev_row[j - 1] # Replace + curr_row[j - 1], # Insert + prev_row[j], # Remove + prev_row[j - 1], # Replace ) # Update the previous row with the current row @@ -135,9 +139,10 @@ def levenshtein_d(str1, str2): def sort_nicely(l): - """Sort the given list in the way that humans expect. - """ - l.sort(key=lambda s: [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)]) + """Sort the given list in the way that humans expect.""" + l.sort( + key=lambda s: [int(c) if c.isdigit() else c for c in re.split("([0-9]+)", s)] + ) def get_elapsed_time_string(elapsed_time, rounding=3): @@ -165,12 +170,12 @@ def get_elapsed_time_string(elapsed_time, rounding=3): scaled_time = elapsed_time time_unit = "seconds" - elif 60 <= elapsed_time < 60 ** 2: + elif 60 <= elapsed_time < 60**2: scaled_time = elapsed_time / 60 time_unit = "minutes" else: - scaled_time = elapsed_time / (60 ** 2) + scaled_time = elapsed_time / (60**2) time_unit = "hours" scaled_time = round(scaled_time, rounding) @@ -195,34 +200,33 @@ def etree_to_dict(t): for dc in map(etree_to_dict, children): for k, v in dc.items(): dd[k].append(v) - d = {t.tag: {k: v[0] if len(v) == 1 else v - for k, v in dd.items()}} + d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} if t.attrib: - d[t.tag].update(('@' + k, v) - for k, v in t.attrib.items()) + d[t.tag].update(("@" + k, v) for k, v in t.attrib.items()) if t.text: text = t.text.strip() if children or t.attrib: if text: - d[t.tag]['#text'] = text + d[t.tag]["#text"] = text else: d[t.tag] = text return d def hex_to_rgb(value): - value = value.lstrip('#') + value = value.lstrip("#") lv = len(value) - return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) + return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) def get_ncpus_available(): - ncpus = 2 # returning 2 by default as code assumes ncpus > 1 (or that packages gracefully handle scheduling 0 jobs/threads) - + n_cpus = 1 if hasattr(os, "sched_getaffinity"): - ncpus = len(os.sched_getaffinity(0)) + n_cpus = len(os.sched_getaffinity(0)) elif hasattr(multiprocessing, "cpu_count"): - ncpus = multiprocessing.cpu_count() + n_cpus = multiprocessing.cpu_count() - return int(ncpus) + if n_cpus == 1: + raise RuntimeError("Need multiple cpus") + return n_cpus - 1 diff --git a/valis/viz.py b/src/valis/viz.py similarity index 51% rename from valis/viz.py rename to src/valis/viz.py index 1c193ed3..86cba09a 100644 --- a/valis/viz.py +++ b/src/valis/viz.py @@ -1,6 +1,6 @@ -"""Various functions used to visualize registration results +"""Various functions used to visualize registration results""" -""" +import logging import colour import matplotlib.pyplot as plt from skimage import draw, color, exposure, transform @@ -14,12 +14,15 @@ from . import warp_tools from . import slide_io + +logger = logging.getLogger(__name__) + # JzAzBz # DXDY_CSPACE = "JzAzBz" DXDY_CRANGE = (0, 0.025) DXDY_LRANGE = (0.004, 0.015) -if platform.system() == 'Windows' or platform.system() == 'Darwin': +if platform.system() == "Windows" or platform.system() == "Darwin": uniTupleDtype = np.int32 else: uniTupleDtype = np.int64 @@ -62,19 +65,21 @@ def draw_outline(img, mask, clr=(100, 240, 39), thickness=2): outline_img = outline_img.astype(np.uint8) - detection_mask = 255*(mask != 0).astype(np.uint8) - contours, _ = cv2.findContours(detection_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + detection_mask = 255 * (mask != 0).astype(np.uint8) + contours, _ = cv2.findContours( + detection_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) int_color = [int(i) for i in clr] for cnt in contours: - outline_img = cv2.drawContours(outline_img, [cnt], 0, color=int_color, thickness=thickness) + outline_img = cv2.drawContours( + outline_img, [cnt], 0, color=int_color, thickness=thickness + ) return outline_img def draw_features(kp_xy, image, n_features=500): - """Draw keypoints on a image - - """ + """Draw keypoints on a image""" image = exposure.rescale_intensity(image, out_range=(0, 255)) if image.ndim == 2: @@ -95,7 +100,7 @@ def draw_features(kp_xy, image, n_features=500): return feature_img -def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal'): +def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment="horizontal"): """Draw feature matches between two images Parameters @@ -132,10 +137,10 @@ def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal' padded_dst, dst_T = warp_tools.pad_img(dst_img, out_shape) if padded_src.ndim == 2: - padded_src = np.dstack([padded_src]*3) + padded_src = np.dstack([padded_src] * 3) if padded_dst.ndim == 2: - padded_dst = np.dstack([padded_dst]*3) + padded_dst = np.dstack([padded_dst] * 3) if not padded_src.dtype == np.uint8: padded_src = exposure.rescale_intensity(padded_src, out_range=np.uint8) @@ -155,7 +160,7 @@ def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal' src_xy_in_feature_img = warp_tools.warp_xy(kp1_xy, M=src_T) n_pt = np.min([kp1_xy.shape[0], kp2_xy.shape[0]]) - cmap = (255*jzazbz_cmap()).astype(np.uint8) + cmap = (255 * jzazbz_cmap()).astype(np.uint8) all_color_idx = np.arange(0, cmap.shape[0]) colors = cmap[np.random.choice(all_color_idx, n_pt), :] for i in range(n_pt): @@ -166,11 +171,17 @@ def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal' circ_rc_1 = draw.ellipse(*xy1[::-1], rad, rad, shape=feature_img.shape) circ_rc_2 = draw.ellipse(*xy2[::-1], rad, rad, shape=feature_img.shape) - line_rc = np.array(draw.line_aa(*np.round(xy1[::-1]).astype(int), *np.round(xy2[::-1]).astype(int))) - line_rc[0] = np.clip(line_rc[0], 0, feature_img.shape[0]-1).astype(int) - line_rc[1] = np.clip(line_rc[1], 0, feature_img.shape[1]-1).astype(int) - - feature_img[line_rc[0].astype(int), line_rc[1].astype(int)] = pt_color*line_rc[2][..., np.newaxis] + line_rc = np.array( + draw.line_aa( + *np.round(xy1[::-1]).astype(int), *np.round(xy2[::-1]).astype(int) + ) + ) + line_rc[0] = np.clip(line_rc[0], 0, feature_img.shape[0] - 1).astype(int) + line_rc[1] = np.clip(line_rc[1], 0, feature_img.shape[1] - 1).astype(int) + + feature_img[line_rc[0].astype(int), line_rc[1].astype(int)] = ( + pt_color * line_rc[2][..., np.newaxis] + ) feature_img[circ_rc_1] = pt_color feature_img[circ_rc_2] = pt_color @@ -178,27 +189,25 @@ def draw_matches(src_img, kp1_xy, dst_img, kp2_xy, rad=3, alignment='horizontal' def draw_clusterd_D(D, optimal_Z): - """Draw clustered distance matrix with dendrograms along the axes - - """ + """Draw clustered distance matrix with dendrograms along the axes""" fig = plt.figure() axdendro = fig.add_axes([0.013, 0.05, 0.1, 0.798]) - Z = dendrogram(optimal_Z, orientation='left', link_color_func=lambda k: "black") + Z = dendrogram(optimal_Z, orientation="left", link_color_func=lambda k: "black") axdendro.set_xticks([]) axdendro.set_yticks([]) - axdendro.axis('off') + axdendro.axis("off") axdendro.invert_yaxis() axdendro_top = fig.add_axes([0.115, 0.85, 0.6, 0.14]) - Z_top = dendrogram(optimal_Z, orientation='top', link_color_func=lambda k: "black") + Z_top = dendrogram(optimal_Z, orientation="top", link_color_func=lambda k: "black") axdendro_top.set_xticks([]) axdendro_top.set_yticks([]) - axdendro_top.axis('off') + axdendro_top.axis("off") axmatrix = fig.add_axes([0.115, 0.05, 0.6, 0.798]) - im = axmatrix.matshow(D, aspect='auto', cmap="plasma_r") + im = axmatrix.matshow(D, aspect="auto", cmap="plasma_r") axmatrix.set_xticks([]) axmatrix.set_yticks([]) @@ -232,11 +241,13 @@ def get_grid(shape, grid_spacing, thickness=1): all_cols = [] row_add_idx = 0 for k in range(thickness): - for i in np.arange(grid_spacing - thickness, shape[0] + thickness, grid_spacing): + for i in np.arange( + grid_spacing - thickness, shape[0] + thickness, grid_spacing + ): for j in np.arange(0, shape[1]): - if k%2 == 0: + if k % 2 == 0: r = i + row_add_idx - elif k%2 != 0: + elif k % 2 != 0: r = i - row_add_idx if r >= 0 and r < shape[0]: @@ -250,9 +261,9 @@ def get_grid(shape, grid_spacing, thickness=1): for k in range(thickness): for j in np.arange(grid_spacing - thickness, shape[1], grid_spacing): for i in np.arange(0, shape[0]): - if k%2 == 0: + if k % 2 == 0: c = j + col_add_idx - elif k%2 != 0: + elif k % 2 != 0: c = j - col_add_idx if c >= 0 and c < shape[1]: @@ -262,7 +273,9 @@ def get_grid(shape, grid_spacing, thickness=1): if k % 2 == 0: col_add_idx += 1 - return np.array(all_rows, dtype=uniTupleDtype), np.array(all_cols, dtype=uniTupleDtype) + return np.array(all_rows, dtype=uniTupleDtype), np.array( + all_cols, dtype=uniTupleDtype + ) def jzazbz_cmap(luminosity=0.012, colorfulness=0.02, max_h=260): @@ -287,7 +300,7 @@ def jzazbz_cmap(luminosity=0.012, colorfulness=0.02, max_h=260): jzazbz = np.dstack([j, a, b]) with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(jzazbz, 'JzAzBz', 'sRGB') + rgb = colour.convert(jzazbz, "JzAzBz", "sRGB") rgb = np.clip(rgb, 0, 1)[0] if max_h != 360: @@ -316,9 +329,9 @@ def cam16ucs_cmap(luminosity=0.8, colorfulness=0.5, max_h=300): j = np.repeat(luminosity, len(h)) eps = np.finfo("float").eps - cam = np.dstack([j, a+eps, b+eps]) + cam = np.dstack([j, a + eps, b + eps]) with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(cam, 'CAM16UCS', 'sRGB') + rgb = colour.convert(cam, "CAM16UCS", "sRGB") rgb = np.clip(rgb, 0, 1)[0] if max_h != 360: @@ -336,19 +349,31 @@ def make_cbar(rgb, bar_height=30): def rgb_triangle_cmap(): total_n = 360 - n = total_n//3 + n = total_n // 3 max_v = 0.9 min_v = 1 - max_v - tri_edges_x = np.hstack([np.linspace(min_v, max_v, n*2), np.linspace(max_v - 1/n, min_v + 1/n, n)]) - tri_edges_y = np.hstack([np.linspace(min_v, max_v, n), np.linspace(max_v - 1/n, min_v, n), np.repeat(min_v, n)]) + tri_edges_x = np.hstack( + [np.linspace(min_v, max_v, n * 2), np.linspace(max_v - 1 / n, min_v + 1 / n, n)] + ) + tri_edges_y = np.hstack( + [ + np.linspace(min_v, max_v, n), + np.linspace(max_v - 1 / n, min_v, n), + np.repeat(min_v, n), + ] + ) tri_edges_xy = np.dstack([tri_edges_x, tri_edges_y])[0] - T = np.array([[-1., -0.5], - [0., 1.]]) + T = np.array([[-1.0, -0.5], [0.0, 1.0]]) - bary_xy = np.array([np.linalg.inv(T) @ (tri_edges_xy[i] - np.array([ 1., 0.])) for i in range(len(tri_edges_xy))]) + bary_xy = np.array( + [ + np.linalg.inv(T) @ (tri_edges_xy[i] - np.array([1.0, 0.0])) + for i in range(len(tri_edges_xy)) + ] + ) bary_z = 1 - np.sum(bary_xy, axis=1) rgb = np.array([bary_xy[:, 0], bary_xy[:, 1], bary_z]).T @@ -367,92 +392,266 @@ def turbo_cmap(): # If you have 16-bit or 32-bit integer values, convert them to floating point values on the [0,1] range and then use interpolate(). Doing the interpolation in floating point will reduce banding. # If some of your values may lie outside the [0,1] range, use interpolate_or_clip() to highlight them. - turbo_colormap_data = np.array([[0.18995, 0.07176, 0.23217], [0.19483, 0.08339, 0.26149], [0.19956, 0.09498, 0.29024], - [0.20415, 0.10652, 0.31844], [0.20860, 0.11802, 0.34607], [0.21291, 0.12947, 0.37314], - [0.21708, 0.14087, 0.39964], [0.22111, 0.15223, 0.42558], [0.22500, 0.16354, 0.45096], - [0.22875, 0.17481, 0.47578], [0.23236, 0.18603, 0.50004], [0.23582, 0.19720, 0.52373], - [0.23915, 0.20833, 0.54686], [0.24234, 0.21941, 0.56942], [0.24539, 0.23044, 0.59142], - [0.24830, 0.24143, 0.61286], [0.25107, 0.25237, 0.63374], [0.25369, 0.26327, 0.65406], - [0.25618, 0.27412, 0.67381], [0.25853, 0.28492, 0.69300], [0.26074, 0.29568, 0.71162], - [0.26280, 0.30639, 0.72968], [0.26473, 0.31706, 0.74718], [0.26652, 0.32768, 0.76412], - [0.26816, 0.33825, 0.78050], [0.26967, 0.34878, 0.79631], [0.27103, 0.35926, 0.81156], - [0.27226, 0.36970, 0.82624], [0.27334, 0.38008, 0.84037], [0.27429, 0.39043, 0.85393], - [0.27509, 0.40072, 0.86692], [0.27576, 0.41097, 0.87936], [0.27628, 0.42118, 0.89123], - [0.27667, 0.43134, 0.90254], [0.27691, 0.44145, 0.91328], [0.27701, 0.45152, 0.92347], - [0.27698, 0.46153, 0.93309], [0.27680, 0.47151, 0.94214], [0.27648, 0.48144, 0.95064], - [0.27603, 0.49132, 0.95857], [0.27543, 0.50115, 0.96594], [0.27469, 0.51094, 0.97275], - [0.27381, 0.52069, 0.97899], [0.27273, 0.53040, 0.98461], [0.27106, 0.54015, 0.98930], - [0.26878, 0.54995, 0.99303], [0.26592, 0.55979, 0.99583], [0.26252, 0.56967, 0.99773], - [0.25862, 0.57958, 0.99876], [0.25425, 0.58950, 0.99896], [0.24946, 0.59943, 0.99835], - [0.24427, 0.60937, 0.99697], [0.23874, 0.61931, 0.99485], [0.23288, 0.62923, 0.99202], - [0.22676, 0.63913, 0.98851], [0.22039, 0.64901, 0.98436], [0.21382, 0.65886, 0.97959], - [0.20708, 0.66866, 0.97423], [0.20021, 0.67842, 0.96833], [0.19326, 0.68812, 0.96190], - [0.18625, 0.69775, 0.95498], [0.17923, 0.70732, 0.94761], [0.17223, 0.71680, 0.93981], - [0.16529, 0.72620, 0.93161], [0.15844, 0.73551, 0.92305], [0.15173, 0.74472, 0.91416], - [0.14519, 0.75381, 0.90496], [0.13886, 0.76279, 0.89550], [0.13278, 0.77165, 0.88580], - [0.12698, 0.78037, 0.87590], [0.12151, 0.78896, 0.86581], [0.11639, 0.79740, 0.85559], - [0.11167, 0.80569, 0.84525], [0.10738, 0.81381, 0.83484], [0.10357, 0.82177, 0.82437], - [0.10026, 0.82955, 0.81389], [0.09750, 0.83714, 0.80342], [0.09532, 0.84455, 0.79299], - [0.09377, 0.85175, 0.78264], [0.09287, 0.85875, 0.77240], [0.09267, 0.86554, 0.76230], - [0.09320, 0.87211, 0.75237], [0.09451, 0.87844, 0.74265], [0.09662, 0.88454, 0.73316], - [0.09958, 0.89040, 0.72393], [0.10342, 0.89600, 0.71500], [0.10815, 0.90142, 0.70599], - [0.11374, 0.90673, 0.69651], [0.12014, 0.91193, 0.68660], [0.12733, 0.91701, 0.67627], - [0.13526, 0.92197, 0.66556], [0.14391, 0.92680, 0.65448], [0.15323, 0.93151, 0.64308], - [0.16319, 0.93609, 0.63137], [0.17377, 0.94053, 0.61938], [0.18491, 0.94484, 0.60713], - [0.19659, 0.94901, 0.59466], [0.20877, 0.95304, 0.58199], [0.22142, 0.95692, 0.56914], - [0.23449, 0.96065, 0.55614], [0.24797, 0.96423, 0.54303], [0.26180, 0.96765, 0.52981], - [0.27597, 0.97092, 0.51653], [0.29042, 0.97403, 0.50321], [0.30513, 0.97697, 0.48987], - [0.32006, 0.97974, 0.47654], [0.33517, 0.98234, 0.46325], [0.35043, 0.98477, 0.45002], - [0.36581, 0.98702, 0.43688], [0.38127, 0.98909, 0.42386], [0.39678, 0.99098, 0.41098], - [0.41229, 0.99268, 0.39826], [0.42778, 0.99419, 0.38575], [0.44321, 0.99551, 0.37345], - [0.45854, 0.99663, 0.36140], [0.47375, 0.99755, 0.34963], [0.48879, 0.99828, 0.33816], - [0.50362, 0.99879, 0.32701], [0.51822, 0.99910, 0.31622], [0.53255, 0.99919, 0.30581], - [0.54658, 0.99907, 0.29581], [0.56026, 0.99873, 0.28623], [0.57357, 0.99817, 0.27712], - [0.58646, 0.99739, 0.26849], [0.59891, 0.99638, 0.26038], [0.61088, 0.99514, 0.25280], - [0.62233, 0.99366, 0.24579], [0.63323, 0.99195, 0.23937], [0.64362, 0.98999, 0.23356], - [0.65394, 0.98775, 0.22835], [0.66428, 0.98524, 0.22370], [0.67462, 0.98246, 0.21960], - [0.68494, 0.97941, 0.21602], [0.69525, 0.97610, 0.21294], [0.70553, 0.97255, 0.21032], - [0.71577, 0.96875, 0.20815], [0.72596, 0.96470, 0.20640], [0.73610, 0.96043, 0.20504], - [0.74617, 0.95593, 0.20406], [0.75617, 0.95121, 0.20343], [0.76608, 0.94627, 0.20311], - [0.77591, 0.94113, 0.20310], [0.78563, 0.93579, 0.20336], [0.79524, 0.93025, 0.20386], - [0.80473, 0.92452, 0.20459], [0.81410, 0.91861, 0.20552], [0.82333, 0.91253, 0.20663], - [0.83241, 0.90627, 0.20788], [0.84133, 0.89986, 0.20926], [0.85010, 0.89328, 0.21074], - [0.85868, 0.88655, 0.21230], [0.86709, 0.87968, 0.21391], [0.87530, 0.87267, 0.21555], - [0.88331, 0.86553, 0.21719], [0.89112, 0.85826, 0.21880], [0.89870, 0.85087, 0.22038], - [0.90605, 0.84337, 0.22188], [0.91317, 0.83576, 0.22328], [0.92004, 0.82806, 0.22456], - [0.92666, 0.82025, 0.22570], [0.93301, 0.81236, 0.22667], [0.93909, 0.80439, 0.22744], - [0.94489, 0.79634, 0.22800], [0.95039, 0.78823, 0.22831], [0.95560, 0.78005, 0.22836], - [0.96049, 0.77181, 0.22811], [0.96507, 0.76352, 0.22754], [0.96931, 0.75519, 0.22663], - [0.97323, 0.74682, 0.22536], [0.97679, 0.73842, 0.22369], [0.98000, 0.73000, 0.22161], - [0.98289, 0.72140, 0.21918], [0.98549, 0.71250, 0.21650], [0.98781, 0.70330, 0.21358], - [0.98986, 0.69382, 0.21043], [0.99163, 0.68408, 0.20706], [0.99314, 0.67408, 0.20348], - [0.99438, 0.66386, 0.19971], [0.99535, 0.65341, 0.19577], [0.99607, 0.64277, 0.19165], - [0.99654, 0.63193, 0.18738], [0.99675, 0.62093, 0.18297], [0.99672, 0.60977, 0.17842], - [0.99644, 0.59846, 0.17376], [0.99593, 0.58703, 0.16899], [0.99517, 0.57549, 0.16412], - [0.99419, 0.56386, 0.15918], [0.99297, 0.55214, 0.15417], [0.99153, 0.54036, 0.14910], - [0.98987, 0.52854, 0.14398], [0.98799, 0.51667, 0.13883], [0.98590, 0.50479, 0.13367], - [0.98360, 0.49291, 0.12849], [0.98108, 0.48104, 0.12332], [0.97837, 0.46920, 0.11817], - [0.97545, 0.45740, 0.11305], [0.97234, 0.44565, 0.10797], [0.96904, 0.43399, 0.10294], - [0.96555, 0.42241, 0.09798], [0.96187, 0.41093, 0.09310], [0.95801, 0.39958, 0.08831], - [0.95398, 0.38836, 0.08362], [0.94977, 0.37729, 0.07905], [0.94538, 0.36638, 0.07461], - [0.94084, 0.35566, 0.07031], [0.93612, 0.34513, 0.06616], [0.93125, 0.33482, 0.06218], - [0.92623, 0.32473, 0.05837], [0.92105, 0.31489, 0.05475], [0.91572, 0.30530, 0.05134], - [0.91024, 0.29599, 0.04814], [0.90463, 0.28696, 0.04516], [0.89888, 0.27824, 0.04243], - [0.89298, 0.26981, 0.03993], [0.88691, 0.26152, 0.03753], [0.88066, 0.25334, 0.03521], - [0.87422, 0.24526, 0.03297], [0.86760, 0.23730, 0.03082], [0.86079, 0.22945, 0.02875], - [0.85380, 0.22170, 0.02677], [0.84662, 0.21407, 0.02487], [0.83926, 0.20654, 0.02305], - [0.83172, 0.19912, 0.02131], [0.82399, 0.19182, 0.01966], [0.81608, 0.18462, 0.01809], - [0.80799, 0.17753, 0.01660], [0.79971, 0.17055, 0.01520], [0.79125, 0.16368, 0.01387], - [0.78260, 0.15693, 0.01264], [0.77377, 0.15028, 0.01148], [0.76476, 0.14374, 0.01041], - [0.75556, 0.13731, 0.00942], [0.74617, 0.13098, 0.00851], [0.73661, 0.12477, 0.00769], - [0.72686, 0.11867, 0.00695], [0.71692, 0.11268, 0.00629], [0.70680, 0.10680, 0.00571], - [0.69650, 0.10102, 0.00522], [0.68602, 0.09536, 0.00481], [0.67535, 0.08980, 0.00449], - [0.66449, 0.08436, 0.00424], [0.65345, 0.07902, 0.00408], [0.64223, 0.07380, 0.00401], - [0.63082, 0.06868, 0.00401], [0.61923, 0.06367, 0.00410], [0.60746, 0.05878, 0.00427], - [0.59550, 0.05399, 0.00453], [0.58336, 0.04931, 0.00486], [0.57103, 0.04474, 0.00529], - [0.55852, 0.04028, 0.00579], [0.54583, 0.03593, 0.00638], [0.53295, 0.03169, 0.00705], - [0.51989, 0.02756, 0.00780], [0.50664, 0.02354, 0.00863], [0.49321, 0.01963, 0.00955], - [0.47960, 0.01583, 0.01055]]) + turbo_colormap_data = np.array( + [ + [0.18995, 0.07176, 0.23217], + [0.19483, 0.08339, 0.26149], + [0.19956, 0.09498, 0.29024], + [0.20415, 0.10652, 0.31844], + [0.20860, 0.11802, 0.34607], + [0.21291, 0.12947, 0.37314], + [0.21708, 0.14087, 0.39964], + [0.22111, 0.15223, 0.42558], + [0.22500, 0.16354, 0.45096], + [0.22875, 0.17481, 0.47578], + [0.23236, 0.18603, 0.50004], + [0.23582, 0.19720, 0.52373], + [0.23915, 0.20833, 0.54686], + [0.24234, 0.21941, 0.56942], + [0.24539, 0.23044, 0.59142], + [0.24830, 0.24143, 0.61286], + [0.25107, 0.25237, 0.63374], + [0.25369, 0.26327, 0.65406], + [0.25618, 0.27412, 0.67381], + [0.25853, 0.28492, 0.69300], + [0.26074, 0.29568, 0.71162], + [0.26280, 0.30639, 0.72968], + [0.26473, 0.31706, 0.74718], + [0.26652, 0.32768, 0.76412], + [0.26816, 0.33825, 0.78050], + [0.26967, 0.34878, 0.79631], + [0.27103, 0.35926, 0.81156], + [0.27226, 0.36970, 0.82624], + [0.27334, 0.38008, 0.84037], + [0.27429, 0.39043, 0.85393], + [0.27509, 0.40072, 0.86692], + [0.27576, 0.41097, 0.87936], + [0.27628, 0.42118, 0.89123], + [0.27667, 0.43134, 0.90254], + [0.27691, 0.44145, 0.91328], + [0.27701, 0.45152, 0.92347], + [0.27698, 0.46153, 0.93309], + [0.27680, 0.47151, 0.94214], + [0.27648, 0.48144, 0.95064], + [0.27603, 0.49132, 0.95857], + [0.27543, 0.50115, 0.96594], + [0.27469, 0.51094, 0.97275], + [0.27381, 0.52069, 0.97899], + [0.27273, 0.53040, 0.98461], + [0.27106, 0.54015, 0.98930], + [0.26878, 0.54995, 0.99303], + [0.26592, 0.55979, 0.99583], + [0.26252, 0.56967, 0.99773], + [0.25862, 0.57958, 0.99876], + [0.25425, 0.58950, 0.99896], + [0.24946, 0.59943, 0.99835], + [0.24427, 0.60937, 0.99697], + [0.23874, 0.61931, 0.99485], + [0.23288, 0.62923, 0.99202], + [0.22676, 0.63913, 0.98851], + [0.22039, 0.64901, 0.98436], + [0.21382, 0.65886, 0.97959], + [0.20708, 0.66866, 0.97423], + [0.20021, 0.67842, 0.96833], + [0.19326, 0.68812, 0.96190], + [0.18625, 0.69775, 0.95498], + [0.17923, 0.70732, 0.94761], + [0.17223, 0.71680, 0.93981], + [0.16529, 0.72620, 0.93161], + [0.15844, 0.73551, 0.92305], + [0.15173, 0.74472, 0.91416], + [0.14519, 0.75381, 0.90496], + [0.13886, 0.76279, 0.89550], + [0.13278, 0.77165, 0.88580], + [0.12698, 0.78037, 0.87590], + [0.12151, 0.78896, 0.86581], + [0.11639, 0.79740, 0.85559], + [0.11167, 0.80569, 0.84525], + [0.10738, 0.81381, 0.83484], + [0.10357, 0.82177, 0.82437], + [0.10026, 0.82955, 0.81389], + [0.09750, 0.83714, 0.80342], + [0.09532, 0.84455, 0.79299], + [0.09377, 0.85175, 0.78264], + [0.09287, 0.85875, 0.77240], + [0.09267, 0.86554, 0.76230], + [0.09320, 0.87211, 0.75237], + [0.09451, 0.87844, 0.74265], + [0.09662, 0.88454, 0.73316], + [0.09958, 0.89040, 0.72393], + [0.10342, 0.89600, 0.71500], + [0.10815, 0.90142, 0.70599], + [0.11374, 0.90673, 0.69651], + [0.12014, 0.91193, 0.68660], + [0.12733, 0.91701, 0.67627], + [0.13526, 0.92197, 0.66556], + [0.14391, 0.92680, 0.65448], + [0.15323, 0.93151, 0.64308], + [0.16319, 0.93609, 0.63137], + [0.17377, 0.94053, 0.61938], + [0.18491, 0.94484, 0.60713], + [0.19659, 0.94901, 0.59466], + [0.20877, 0.95304, 0.58199], + [0.22142, 0.95692, 0.56914], + [0.23449, 0.96065, 0.55614], + [0.24797, 0.96423, 0.54303], + [0.26180, 0.96765, 0.52981], + [0.27597, 0.97092, 0.51653], + [0.29042, 0.97403, 0.50321], + [0.30513, 0.97697, 0.48987], + [0.32006, 0.97974, 0.47654], + [0.33517, 0.98234, 0.46325], + [0.35043, 0.98477, 0.45002], + [0.36581, 0.98702, 0.43688], + [0.38127, 0.98909, 0.42386], + [0.39678, 0.99098, 0.41098], + [0.41229, 0.99268, 0.39826], + [0.42778, 0.99419, 0.38575], + [0.44321, 0.99551, 0.37345], + [0.45854, 0.99663, 0.36140], + [0.47375, 0.99755, 0.34963], + [0.48879, 0.99828, 0.33816], + [0.50362, 0.99879, 0.32701], + [0.51822, 0.99910, 0.31622], + [0.53255, 0.99919, 0.30581], + [0.54658, 0.99907, 0.29581], + [0.56026, 0.99873, 0.28623], + [0.57357, 0.99817, 0.27712], + [0.58646, 0.99739, 0.26849], + [0.59891, 0.99638, 0.26038], + [0.61088, 0.99514, 0.25280], + [0.62233, 0.99366, 0.24579], + [0.63323, 0.99195, 0.23937], + [0.64362, 0.98999, 0.23356], + [0.65394, 0.98775, 0.22835], + [0.66428, 0.98524, 0.22370], + [0.67462, 0.98246, 0.21960], + [0.68494, 0.97941, 0.21602], + [0.69525, 0.97610, 0.21294], + [0.70553, 0.97255, 0.21032], + [0.71577, 0.96875, 0.20815], + [0.72596, 0.96470, 0.20640], + [0.73610, 0.96043, 0.20504], + [0.74617, 0.95593, 0.20406], + [0.75617, 0.95121, 0.20343], + [0.76608, 0.94627, 0.20311], + [0.77591, 0.94113, 0.20310], + [0.78563, 0.93579, 0.20336], + [0.79524, 0.93025, 0.20386], + [0.80473, 0.92452, 0.20459], + [0.81410, 0.91861, 0.20552], + [0.82333, 0.91253, 0.20663], + [0.83241, 0.90627, 0.20788], + [0.84133, 0.89986, 0.20926], + [0.85010, 0.89328, 0.21074], + [0.85868, 0.88655, 0.21230], + [0.86709, 0.87968, 0.21391], + [0.87530, 0.87267, 0.21555], + [0.88331, 0.86553, 0.21719], + [0.89112, 0.85826, 0.21880], + [0.89870, 0.85087, 0.22038], + [0.90605, 0.84337, 0.22188], + [0.91317, 0.83576, 0.22328], + [0.92004, 0.82806, 0.22456], + [0.92666, 0.82025, 0.22570], + [0.93301, 0.81236, 0.22667], + [0.93909, 0.80439, 0.22744], + [0.94489, 0.79634, 0.22800], + [0.95039, 0.78823, 0.22831], + [0.95560, 0.78005, 0.22836], + [0.96049, 0.77181, 0.22811], + [0.96507, 0.76352, 0.22754], + [0.96931, 0.75519, 0.22663], + [0.97323, 0.74682, 0.22536], + [0.97679, 0.73842, 0.22369], + [0.98000, 0.73000, 0.22161], + [0.98289, 0.72140, 0.21918], + [0.98549, 0.71250, 0.21650], + [0.98781, 0.70330, 0.21358], + [0.98986, 0.69382, 0.21043], + [0.99163, 0.68408, 0.20706], + [0.99314, 0.67408, 0.20348], + [0.99438, 0.66386, 0.19971], + [0.99535, 0.65341, 0.19577], + [0.99607, 0.64277, 0.19165], + [0.99654, 0.63193, 0.18738], + [0.99675, 0.62093, 0.18297], + [0.99672, 0.60977, 0.17842], + [0.99644, 0.59846, 0.17376], + [0.99593, 0.58703, 0.16899], + [0.99517, 0.57549, 0.16412], + [0.99419, 0.56386, 0.15918], + [0.99297, 0.55214, 0.15417], + [0.99153, 0.54036, 0.14910], + [0.98987, 0.52854, 0.14398], + [0.98799, 0.51667, 0.13883], + [0.98590, 0.50479, 0.13367], + [0.98360, 0.49291, 0.12849], + [0.98108, 0.48104, 0.12332], + [0.97837, 0.46920, 0.11817], + [0.97545, 0.45740, 0.11305], + [0.97234, 0.44565, 0.10797], + [0.96904, 0.43399, 0.10294], + [0.96555, 0.42241, 0.09798], + [0.96187, 0.41093, 0.09310], + [0.95801, 0.39958, 0.08831], + [0.95398, 0.38836, 0.08362], + [0.94977, 0.37729, 0.07905], + [0.94538, 0.36638, 0.07461], + [0.94084, 0.35566, 0.07031], + [0.93612, 0.34513, 0.06616], + [0.93125, 0.33482, 0.06218], + [0.92623, 0.32473, 0.05837], + [0.92105, 0.31489, 0.05475], + [0.91572, 0.30530, 0.05134], + [0.91024, 0.29599, 0.04814], + [0.90463, 0.28696, 0.04516], + [0.89888, 0.27824, 0.04243], + [0.89298, 0.26981, 0.03993], + [0.88691, 0.26152, 0.03753], + [0.88066, 0.25334, 0.03521], + [0.87422, 0.24526, 0.03297], + [0.86760, 0.23730, 0.03082], + [0.86079, 0.22945, 0.02875], + [0.85380, 0.22170, 0.02677], + [0.84662, 0.21407, 0.02487], + [0.83926, 0.20654, 0.02305], + [0.83172, 0.19912, 0.02131], + [0.82399, 0.19182, 0.01966], + [0.81608, 0.18462, 0.01809], + [0.80799, 0.17753, 0.01660], + [0.79971, 0.17055, 0.01520], + [0.79125, 0.16368, 0.01387], + [0.78260, 0.15693, 0.01264], + [0.77377, 0.15028, 0.01148], + [0.76476, 0.14374, 0.01041], + [0.75556, 0.13731, 0.00942], + [0.74617, 0.13098, 0.00851], + [0.73661, 0.12477, 0.00769], + [0.72686, 0.11867, 0.00695], + [0.71692, 0.11268, 0.00629], + [0.70680, 0.10680, 0.00571], + [0.69650, 0.10102, 0.00522], + [0.68602, 0.09536, 0.00481], + [0.67535, 0.08980, 0.00449], + [0.66449, 0.08436, 0.00424], + [0.65345, 0.07902, 0.00408], + [0.64223, 0.07380, 0.00401], + [0.63082, 0.06868, 0.00401], + [0.61923, 0.06367, 0.00410], + [0.60746, 0.05878, 0.00427], + [0.59550, 0.05399, 0.00453], + [0.58336, 0.04931, 0.00486], + [0.57103, 0.04474, 0.00529], + [0.55852, 0.04028, 0.00579], + [0.54583, 0.03593, 0.00638], + [0.53295, 0.03169, 0.00705], + [0.51989, 0.02756, 0.00780], + [0.50664, 0.02354, 0.00863], + [0.49321, 0.01963, 0.00955], + [0.47960, 0.01583, 0.01055], + ] + ) return turbo_colormap_data @@ -467,7 +666,7 @@ def get_n_colors(rgb, n): n_full_rep = n // n_clrs n_extra = n % n_clrs - all_colors = np.vstack([*[rgb]*n_full_rep, rgb[0:n_extra]]) + all_colors = np.vstack([*[rgb] * n_full_rep, rgb[0:n_extra]]) assert all_colors.shape[0] == n np.random.shuffle(all_colors) @@ -476,9 +675,9 @@ def get_n_colors(rgb, n): with colour.utilities.suppress_warnings(colour_usage_warnings=True): if 1 < rgb.max() <= 255 and np.issubdtype(rgb.dtype, np.integer): - cam = colour.convert(rgb/255, 'sRGB', 'CAM16UCS') + cam = colour.convert(rgb / 255, "sRGB", "CAM16UCS") else: - cam = colour.convert(rgb, 'sRGB', 'CAM16UCS') + cam = colour.convert(rgb, "sRGB", "CAM16UCS") sq_D = distance.cdist(cam, cam) max_D = sq_D.max() @@ -505,7 +704,7 @@ def tint_grey(grey_img, tint_rgb, outspace="srgb"): outspace : "srgb" or "lab" """ if max(tint_rgb) <= 1: - tint_rgb_255 = (255*tint_rgb).astype(np.uint8) + tint_rgb_255 = (255 * tint_rgb).astype(np.uint8) else: tint_rgb_255 = tint_rgb @@ -518,7 +717,7 @@ def tint_grey(grey_img, tint_rgb, outspace="srgb"): else: g = grey_img - l = g/g.max() + l = g / g.max() l *= tint_lab[0] a = pyvips.Image.black(g.width, g.height, bands=1) + tint_lab[1] @@ -553,7 +752,11 @@ def create_overlap_img(img_list, cmap=jzazbz_cmap(), blending="weighted"): is_np = isinstance(img_list[0], np.ndarray) if is_np: - is_rgb = img_list[0].dtype == np.uint8 and img_list[0].ndim==3 and img_list[0].shape[2] == 3 + is_rgb = ( + img_list[0].dtype == np.uint8 + and img_list[0].ndim == 3 + and img_list[0].shape[2] == 3 + ) if is_rgb: grey_img_list = [color.rgb2gray(x) if x.ndim == 3 else x for x in img_list] elif img_list[0].ndim > 2: @@ -563,7 +766,10 @@ def create_overlap_img(img_list, cmap=jzazbz_cmap(), blending="weighted"): else: is_rgb = img_list[0].interpretation == pyvips.enums.Interpretation.SRGB if is_rgb: - grey_img_list = [x.colourspace(pyvips.enums.Interpretation.B_W) if x.bands == 3 else x for x in img_list] + grey_img_list = [ + x.colourspace(pyvips.enums.Interpretation.B_W) if x.bands == 3 else x + for x in img_list + ] elif img_list[0].bands > 2: grey_img_list = [x[0] for x in img_list] else: @@ -571,13 +777,22 @@ def create_overlap_img(img_list, cmap=jzazbz_cmap(), blending="weighted"): if blending == "light": if is_np: - tinted_img_list = [tint_grey(warp_tools.numpy2vips(grey_img_list[i]), color_list[i]) for i in range(n_imgs)] + tinted_img_list = [ + tint_grey(warp_tools.numpy2vips(grey_img_list[i]), color_list[i]) + for i in range(n_imgs) + ] else: - tinted_img_list = [tint_grey(grey_img_list[i], color_list[i]) for i in range(n_imgs)] + tinted_img_list = [ + tint_grey(grey_img_list[i], color_list[i]) for i in range(n_imgs) + ] base = tinted_img_list[0] - blended_vips_rgb = base.composite(other=tinted_img_list[1:], mode=pyvips.enums.BlendMode.LIGHTEN, compositing_space=pyvips.enums.Interpretation.SRGB) - blended_vips_rgb = blended_vips_rgb[0:3] # remove alpha channel + blended_vips_rgb = base.composite( + other=tinted_img_list[1:], + mode=pyvips.enums.BlendMode.LIGHTEN, + compositing_space=pyvips.enums.Interpretation.SRGB, + ) + blended_vips_rgb = blended_vips_rgb[0:3] # remove alpha channel else: eps = np.finfo("float").eps @@ -585,32 +800,48 @@ def create_overlap_img(img_list, cmap=jzazbz_cmap(), blending="weighted"): sum_img = np.full(warp_tools.get_shape(grey_img_list[0])[0:2], eps) blended_img = np.zeros((*sum_img.shape, 3)) else: - sum_img = pyvips.Image.black(grey_img_list[0].width, grey_img_list[0].height) + eps - blended_img = pyvips.Image.black(grey_img_list[0].width, grey_img_list[0].height, bands=3) + sum_img = ( + pyvips.Image.black(grey_img_list[0].width, grey_img_list[0].height) + + eps + ) + blended_img = pyvips.Image.black( + grey_img_list[0].width, grey_img_list[0].height, bands=3 + ) max_v = 0 for i in range(len(grey_img_list)): sum_img += grey_img_list[i] max_v = max(max_v, grey_img_list[i].max()) - vips_lab_color_list = [warp_tools.numpy2vips(np.array([[255*clr]])).colourspace(pyvips.enums.Interpretation.LAB) for clr in color_list] + vips_lab_color_list = [ + warp_tools.numpy2vips(np.array([[255 * clr]])).colourspace( + pyvips.enums.Interpretation.LAB + ) + for clr in color_list + ] for i in range(len(grey_img_list)): - weight = grey_img_list[i]/sum_img + weight = grey_img_list[i] / sum_img if is_np: lab_clr = warp_tools.vips2numpy(vips_lab_color_list[i]) - blended_img += lab_clr * np.dstack([grey_img_list[i]/max_v * weight]*3) + blended_img += lab_clr * np.dstack( + [grey_img_list[i] / max_v * weight] * 3 + ) else: lab_clr = vips_lab_color_list[i] - blended_img += lab_clr*weight*grey_img_list[i]/max_v + blended_img += lab_clr * weight * grey_img_list[i] / max_v if is_np: blended_vips = warp_tools.numpy2vips(blended_img) else: blended_vips = blended_img - blended_vips_lab = blended_vips.copy(interpretation=pyvips.enums.Interpretation.LAB) - blended_vips_rgb = blended_vips_lab.colourspace(pyvips.enums.Interpretation.SRGB)[0:3] + blended_vips_lab = blended_vips.copy( + interpretation=pyvips.enums.Interpretation.LAB + ) + blended_vips_rgb = blended_vips_lab.colourspace( + pyvips.enums.Interpretation.SRGB + )[0:3] if is_np: blended = warp_tools.vips2numpy(blended_vips_rgb) @@ -621,7 +852,13 @@ def create_overlap_img(img_list, cmap=jzazbz_cmap(), blending="weighted"): def save_overlap_full_rez(registrar, dst_f): - warped_slide_list = [registrar.get_slide(f).warp_slide(level=0).colourspace(pyvips.enums.Interpretation.B_W).invert() for f in registrar.get_sorted_img_f_list()] + warped_slide_list = [ + registrar.get_slide(f) + .warp_slide(level=0) + .colourspace(pyvips.enums.Interpretation.B_W) + .invert() + for f in registrar.get_sorted_img_f_list() + ] full_rez_overlap = create_overlap_img(warped_slide_list, blending="light") ref_slide = registrar.get_ref_slide() pixel_physical_size_xyu = ref_slide.reader.metadata.pixel_physical_size_xyu @@ -629,14 +866,21 @@ def save_overlap_full_rez(registrar, dst_f): shape_xyzct = slide_io.get_shape_xyzct((img_w, img_h), img_c) bf_dtype = slide_io.vips2bf_dtype(full_rez_overlap.format) - ome_obj = slide_io.create_ome_xml(shape_xyzct, bf_dtype, is_rgb=True, pixel_physical_size_xyu=pixel_physical_size_xyu) + ome_obj = slide_io.create_ome_xml( + shape_xyzct, + bf_dtype, + is_rgb=True, + pixel_physical_size_xyu=pixel_physical_size_xyu, + ) ome_xml = ome_obj.to_xml() - slide_io.save_ome_tiff(full_rez_overlap, dst_f=dst_f, ome_xml=ome_xml, compression="jpeg") + slide_io.save_ome_tiff( + full_rez_overlap, dst_f=dst_f, ome_xml=ome_xml, compression="jpeg" + ) def blend_colors(img, colors, scale_by): - """ Color an image by blending + """Color an image by blending Parameters ---------- @@ -658,7 +902,6 @@ def blend_colors(img, colors, scale_by): """ - if len(colors) > 1: n_channel_colors = colors.shape[1] else: @@ -682,10 +925,10 @@ def blend_colors(img, colors, scale_by): channel_max = img[..., i].max() relative_img = img[..., i] / channel_max else: - relative_img = img[..., i]/img_max + relative_img = img[..., i] / img_max # blending is how to weight the mix of colors, similar to an alpha channel - blending = img[..., i]/sum_img + blending = img[..., i] / sum_img for j in range(colors.shape[1]): channel_color = colors[i, j] blended_img[..., j] += channel_color * relative_img * blending @@ -693,7 +936,13 @@ def blend_colors(img, colors, scale_by): return blended_img -def color_multichannel(multichannel_img, marker_colors, rescale_channels=False, normalize_by="image", cspace="Hunter Lab"): +def color_multichannel( + multichannel_img, + marker_colors, + rescale_channels=False, + normalize_by="image", + cspace="Hunter Lab", +): """Color a multichannel image to view as RGB Parameters @@ -734,12 +983,23 @@ def color_multichannel(multichannel_img, marker_colors, rescale_channels=False, """ if rescale_channels: - multichannel_img = np.dstack([exposure.rescale_intensity(multichannel_img[..., i].astype(float), in_range="image", out_range=(0, 1)) for i in range(multichannel_img.shape[2])]) + multichannel_img = np.dstack( + [ + exposure.rescale_intensity( + multichannel_img[..., i].astype(float), + in_range="image", + out_range=(0, 1), + ) + for i in range(multichannel_img.shape[2]) + ] + ) is_srgb = cspace.lower() == "srgb" is_srgb_01 = True - if 1 < marker_colors.max() <= 255 and np.issubdtype(marker_colors.dtype, np.integer): - srgb_01 = marker_colors/255 + if 1 < marker_colors.max() <= 255 and np.issubdtype( + marker_colors.dtype, np.integer + ): + srgb_01 = marker_colors / 255 is_srgb_01 = False else: @@ -747,14 +1007,14 @@ def color_multichannel(multichannel_img, marker_colors, rescale_channels=False, eps = np.finfo("float").eps if not is_srgb: with colour.utilities.suppress_warnings(colour_usage_warnings=True): - cspace_colors = colour.convert(srgb_01 + eps, 'sRGB', cspace) + cspace_colors = colour.convert(srgb_01 + eps, "sRGB", cspace) else: cspace_colors = srgb_01 blended_img = blend_colors(multichannel_img, cspace_colors, normalize_by) if not is_srgb: with colour.utilities.suppress_warnings(colour_usage_warnings=True): - srgb_blended = colour.convert(blended_img + eps, cspace, 'sRGB') - 2*eps + srgb_blended = colour.convert(blended_img + eps, cspace, "sRGB") - 2 * eps else: srgb_blended = blended_img @@ -767,30 +1027,30 @@ def color_multichannel(multichannel_img, marker_colors, rescale_channels=False, def color_dxdy(dx, dy, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, cspace=DXDY_CSPACE): """ - Color displacement, where larger displacements are more colorful, - and, if scale_l=True, brighter. + Color displacement, where larger displacements are more colorful, + and, if scale_l=True, brighter. - Parameters - ---------- - dx: array - 1D Array containing the displacement in the X (column) direction + Parameters + ---------- + dx: array + 1D Array containing the displacement in the X (column) direction - dy: array - 1D Array containing the displacement in the Y (row) direction + dy: array + 1D Array containing the displacement in the Y (row) direction - c_range: (float, float) - Minimum and maximum colorfulness in JzAzBz colorspace + c_range: (float, float) + Minimum and maximum colorfulness in JzAzBz colorspace - l_range: (float, float) - Minimum and maximum luminosity in JzAzBz colorspace + l_range: (float, float) + Minimum and maximum luminosity in JzAzBz colorspace - scale_l: boolean - Scale the luminosity based on magnitude of displacement + scale_l: boolean + Scale the luminosity based on magnitude of displacement - Returns - ------- - displacement_rgb : array - RGB (0, 255) color for each displacement, with the same shape as dx and dy + Returns + ------- + displacement_rgb : array + RGB (0, 255) color for each displacement, with the same shape as dx and dy """ @@ -798,25 +1058,31 @@ def color_dxdy(dx, dy, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, cspace=DXDY_CSP dx = dx.reshape(-1) dy = dy.reshape(-1) - if np.all(dx==0) and np.all(dy==0): + if np.all(dx == 0) and np.all(dy == 0): # No displacements. Return grey image with colour.utilities.suppress_warnings(colour_usage_warnings=True): - bg_rgb = colour.convert(np.dstack([l_range[0], 0, 0]), cspace, 'sRGB')*255 + bg_rgb = colour.convert(np.dstack([l_range[0], 0, 0]), cspace, "sRGB") * 255 displacement_rgb = np.full((*initial_shape, 3), bg_rgb).astype(np.uint8) return displacement_rgb eps = np.finfo("float").eps - magnitude = np.sqrt(dx ** 2 + dy ** 2 + eps) - C = exposure.rescale_intensity(magnitude, in_range=(0, magnitude.max()), out_range=tuple(c_range)) + magnitude = np.sqrt(dx**2 + dy**2 + eps) + C = exposure.rescale_intensity( + magnitude, in_range=(0, magnitude.max()), out_range=tuple(c_range) + ) H = np.arctan2(dy.T, dx.T) A, B = C * np.cos(H), C * np.sin(H) - J = exposure.rescale_intensity(magnitude, in_range=(0, magnitude.max()), out_range=tuple(l_range)) + J = exposure.rescale_intensity( + magnitude, in_range=(0, magnitude.max()), out_range=tuple(l_range) + ) with colour.utilities.suppress_warnings(colour_usage_warnings=True): - rgb = colour.convert(np.dstack([J, A+eps, B+eps]), cspace, 'sRGB') + rgb = colour.convert(np.dstack([J, A + eps, B + eps]), cspace, "sRGB") - displacement_rgb = (255*np.clip(rgb, 0, 1)).astype(np.uint8).reshape((*initial_shape, 3)) + displacement_rgb = ( + (255 * np.clip(rgb, 0, 1)).astype(np.uint8).reshape((*initial_shape, 3)) + ) return displacement_rgb @@ -827,17 +1093,19 @@ def displacement_legend(): Y = np.linspace(-1, 1, 100) X, Y = np.meshgrid(X, Y) - R = np.sqrt(X ** 2 + Y ** 2) + R = np.sqrt(X**2 + Y**2) C = np.sin(R) C = exposure.rescale_intensity(C, out_range=(0, 1)) grad = np.linspace(-1, 1, X.shape[0]) grad = np.resize(grad, X.shape) - dx = grad*C + dx = grad * C dy = grad.T * C - displacement_legend = color_dxdy(dx, dy, DXDY_CRANGE, DXDY_LRANGE, cspace=DXDY_CSPACE) + displacement_legend = color_dxdy( + dx, dy, DXDY_CRANGE, DXDY_LRANGE, cspace=DXDY_CSPACE + ) return displacement_legend @@ -849,25 +1117,38 @@ def draw_displacement_legend(): ax.imshow(leg) ax.set_xticklabels(["", "--", "", "", "", "", "0", "", "", "", "+"]) ax.set_yticklabels(["", "+", "", "", "", "", "0", "", "", "", "--"]) - ax.set_xlabel('dx') - ax.set_ylabel('dy') - - -def color_displacement_grid(bk_dx, bk_dy, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, thickness=None, grid_spacing_ratio=0.02, cspace=DXDY_CSPACE): - """Color a displacement grid - """ - - grid_spacing = np.max(np.array(bk_dx.shape)*grid_spacing_ratio).astype(int) + ax.set_xlabel("dx") + ax.set_ylabel("dy") + + +def color_displacement_grid( + bk_dx, + bk_dy, + c_range=DXDY_CRANGE, + l_range=DXDY_LRANGE, + thickness=None, + grid_spacing_ratio=0.02, + cspace=DXDY_CSPACE, +): + """Color a displacement grid""" + + grid_spacing = np.max(np.array(bk_dx.shape) * grid_spacing_ratio).astype(int) min_dim = np.min(bk_dx.shape) if thickness is None: - thickness = int(np.ceil((grid_spacing/min_dim)*15)) + thickness = int(np.ceil((grid_spacing / min_dim) * 15)) if thickness < 1: thickness = 1 grid_r, grid_c = get_grid(bk_dx.shape, grid_spacing, thickness) - grid_colors = color_dxdy(bk_dx[grid_r, grid_c], bk_dy[grid_r, grid_c], c_range=c_range, l_range=l_range, cspace=cspace) + grid_colors = color_dxdy( + bk_dx[grid_r, grid_c], + bk_dy[grid_r, grid_c], + c_range=c_range, + l_range=l_range, + cspace=cspace, + ) # Warp image of grid grid_img = np.zeros((*bk_dx.shape, 3)) @@ -882,7 +1163,9 @@ def color_displacement_grid(bk_dx, bk_dy, c_range=DXDY_CRANGE, l_range=DXDY_LRAN warped_hcl = [None] * 3 for i in range(3): - warped_hcl[i] = transform.warp(grid_img[..., i], np.array([img_warp_r, img_warp_c])) + warped_hcl[i] = transform.warp( + grid_img[..., i], np.array([img_warp_r, img_warp_c]) + ) grid_img = np.dstack(warped_hcl).astype(np.uint8) @@ -890,26 +1173,33 @@ def color_displacement_grid(bk_dx, bk_dy, c_range=DXDY_CRANGE, l_range=DXDY_LRAN def draw_trimesh(shape_rc, tri_verts, tri_faces, thickness=2): - """Draw a triangular mesh - """ + """Draw a triangular mesh""" tri_img = np.zeros(shape_rc) for face in tri_faces: verts = tri_verts[face] # make sure points are clockwise cx, cy = np.mean(verts, axis=0) - pt_order = np.argsort([np.rad2deg(np.arctan2(xy[1]-cy, xy[0]-cx)) - for xy in verts]) + pt_order = np.argsort( + [np.rad2deg(np.arctan2(xy[1] - cy, xy[0] - cx)) for xy in verts] + ) # draw points pts = verts[pt_order, :].reshape(-1, 1, 2).astype(int) - tri_img = cv2.polylines(tri_img, [pts], True, 1, thickness, - lineType=cv2.LINE_AA) + tri_img = cv2.polylines( + tri_img, [pts], True, 1, thickness, lineType=cv2.LINE_AA + ) return tri_img.astype(float) -def draw_displacement_vector_field(img, dxdy, spacing=25, brightness=0.7, cspace = "OKLAB"): - start_r, start_c = np.meshgrid(np.arange(0, img.shape[0], spacing), np.arange(0, img.shape[1], spacing), indexing="ij") +def draw_displacement_vector_field( + img, dxdy, spacing=25, brightness=0.7, cspace="OKLAB" +): + start_r, start_c = np.meshgrid( + np.arange(0, img.shape[0], spacing), + np.arange(0, img.shape[1], spacing), + indexing="ij", + ) end_r = start_r + dxdy[1][start_r, start_c] end_c = start_c + dxdy[0][start_r, start_c] @@ -925,28 +1215,47 @@ def draw_displacement_vector_field(img, dxdy, spacing=25, brightness=0.7, cspace ab = colour.algebra.polar_to_cartesian(np.dstack([mag, angles])) jab = np.dstack([np.full_like(mag, brightness), ab]) - rgb = (255*np.clip(colour.convert(jab, cspace, 'sRGB'), 0, 1)).astype(np.uint8).reshape((-1, 3)) + rgb = ( + (255 * np.clip(colour.convert(jab, cspace, "sRGB"), 0, 1)) + .astype(np.uint8) + .reshape((-1, 3)) + ) vector_img = img.copy() if vector_img.ndim == 2: - vector_img = np.dstack(3*[vector_img]) + vector_img = np.dstack(3 * [vector_img]) for i in range(end_xy.shape[0]): arrow_start = start_xy[i] arrow_end = end_xy[i] arrow_rgb = rgb[i].tolist() - vector_img = cv2.arrowedLine(img=vector_img, pt1=arrow_start, pt2=arrow_end, color=arrow_rgb, thickness=1, line_type=cv2.LINE_AA) + vector_img = cv2.arrowedLine( + img=vector_img, + pt1=arrow_start, + pt2=arrow_end, + color=arrow_rgb, + thickness=1, + line_type=cv2.LINE_AA, + ) return vector_img -def color_displacement_tri_grid(bk_dx, bk_dy, img=None, n_grid_pts=25, c_range=DXDY_CRANGE, l_range=DXDY_LRANGE, thickness=None, cspace=DXDY_CSPACE): - """View how a displacement warps a triangular mesh. - """ +def color_displacement_tri_grid( + bk_dx, + bk_dy, + img=None, + n_grid_pts=25, + c_range=DXDY_CRANGE, + l_range=DXDY_LRANGE, + thickness=None, + cspace=DXDY_CSPACE, +): + """View how a displacement warps a triangular mesh.""" shape = np.array(bk_dx.shape) - grid_spacing = int(np.min(np.round(shape/n_grid_pts))) + grid_spacing = int(np.min(np.round(shape / n_grid_pts))) new_r = shape[0] - shape[0] % grid_spacing + grid_spacing sample_y = np.arange(0, new_r + grid_spacing, grid_spacing) @@ -954,16 +1263,20 @@ def color_displacement_tri_grid(bk_dx, bk_dy, img=None, n_grid_pts=25, c_range=D new_c = shape[1] - shape[1] % grid_spacing + grid_spacing sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) - padded_shape = np.array([new_r+1, new_c+1]) + padded_shape = np.array([new_r + 1, new_c + 1]) padding_T = warp_tools.get_padding_matrix(shape, padded_shape) - padded_dx = transform.warp(bk_dx, padding_T, output_shape=padded_shape, preserve_range=True) - padded_dy = transform.warp(bk_dy, padding_T, output_shape=padded_shape, preserve_range=True) + padded_dx = transform.warp( + bk_dx, padding_T, output_shape=padded_shape, preserve_range=True + ) + padded_dy = transform.warp( + bk_dy, padding_T, output_shape=padded_shape, preserve_range=True + ) min_dim = np.min(padded_dy.shape) if thickness is None: - thickness = int(np.ceil((grid_spacing/min_dim)*15)) + thickness = int(np.ceil((grid_spacing / min_dim) * 15)) if thickness < 1: thickness = 1 @@ -972,11 +1285,21 @@ def color_displacement_tri_grid(bk_dx, bk_dy, img=None, n_grid_pts=25, c_range=D inv_T = np.linalg.inv(padding_T) trimesh_img = draw_trimesh(padded_shape, warped_xy, tri_faces, thickness=thickness) - trimesh_img = transform.warp(trimesh_img, inv_T, output_shape=shape, preserve_range=True) - colored_displacement = color_dxdy(bk_dx, bk_dy, c_range=c_range, l_range=l_range, cspace=cspace) + trimesh_img = transform.warp( + trimesh_img, inv_T, output_shape=shape, preserve_range=True + ) + colored_displacement = color_dxdy( + bk_dx, bk_dy, c_range=c_range, l_range=l_range, cspace=cspace + ) if img is not None: - assert img.shape[0:2] == trimesh_img.shape[0:2], print(f"mismatch in shape between `img` {img.shape[0:2]} and displacement fields {trimesh_img.shape[0:2]}") + if img.shape[0:2] != trimesh_img.shape[0:2]: + logger.error( + f"mismatch in shape between `img` {img.shape[0:2]} and displacement fields {trimesh_img.shape[0:2]}" + ) + assert ( + img.shape[0:2] == trimesh_img.shape[0:2] + ), f"mismatch in shape between `img` {img.shape[0:2]} and displacement fields {trimesh_img.shape[0:2]}" mesh_pos = trimesh_img > 0 out_img = img.copy() out_img[mesh_pos] = colored_displacement[mesh_pos] @@ -984,4 +1307,3 @@ def color_displacement_tri_grid(bk_dx, bk_dy, img=None, n_grid_pts=25, c_range=D out_img = trimesh_img[..., np.newaxis] * colored_displacement return out_img - diff --git a/valis/warp_tools.py b/src/valis/warp_tools.py similarity index 64% rename from valis/warp_tools.py rename to src/valis/warp_tools.py index a5038707..4676e2cc 100644 --- a/valis/warp_tools.py +++ b/src/valis/warp_tools.py @@ -1,3 +1,4 @@ +import logging import multiprocessing from scipy.optimize import fmin_l_bfgs_b from scipy import ndimage, spatial @@ -12,7 +13,6 @@ import tqdm import cv2 from PIL import Image, ImageDraw -import numpy as np import weightedstats import warnings import pyvips @@ -21,17 +21,28 @@ from colorama import Fore import os import re - +from packaging import version from copy import deepcopy from . import valtils +logger = logging.getLogger(__name__) + pyvips.cache_set_max(0) +def rc_to_wh(shape_rc: tuple[int, int]) -> tuple[int, int]: + """Convert a (row, col) shape to (width, height) — i.e. reverse the axes.""" + return shape_rc[1], shape_rc[0] + + +def wh_to_rc(wh: tuple[int, int]) -> tuple[int, int]: + """Convert a (width, height) size to (row, col) shape — i.e. reverse the axes.""" + return wh[1], wh[0] + + def is_pyvips_22(): - pvips_ver = pyvips.__version__.split(".") - pyvips_22 = eval(pvips_ver[0]) >= 2 and eval(pvips_ver[1]) >= 2 - return pyvips_22 + return version.parse(pyvips.__version__) >= version.parse("2.2.0") + def get_ref_img_idx(img_f_list, ref_img_name=None): """Get index of reference image @@ -70,16 +81,20 @@ def get_ref_img_idx(img_f_list, ref_img_name=None): ref_img_idx = img_names.index(ref_name_lower) except ValueError: # Get closest match - string_d = [valtils.levenshtein_d(ref_name_lower, img_names[i]) for i in range(len(img_names))] + string_d = [ + valtils.levenshtein_d(ref_name_lower, img_names[i]) + for i in range(len(img_names)) + ] ref_img_idx = np.argmin(string_d) - warning_msg = (f"No files in `img_f_list` match exactly match {ref_img_name}. " - f"Returning closest match, which is {valtils.get_name(img_f_list[ref_img_idx])}") - valtils.print_warning(warning_msg) + warning_msg = ( + f"No files in `img_f_list` match exactly match {ref_img_name}. " + f"Returning closest match, which is {valtils.get_name(img_f_list[ref_img_idx])}" + ) + logger.warning(warning_msg) return ref_img_idx - def get_alignment_indices(n_imgs, ref_img_idx=None): """Get indices to align in stack. @@ -109,7 +124,7 @@ def get_alignment_indices(n_imgs, ref_img_idx=None): """ if ref_img_idx is None: - ref_img_idx = n_imgs//2 + ref_img_idx = n_imgs // 2 matching_indices = [None] * (n_imgs - 1) idx = 0 @@ -119,7 +134,7 @@ def get_alignment_indices(n_imgs, ref_img_idx=None): matching_indices[idx] = (current_idx, next_idx) idx += 1 - for i in range(ref_img_idx, n_imgs-1): + for i in range(ref_img_idx, n_imgs - 1): current_idx = i + 1 next_idx = i matching_indices[idx] = (current_idx, next_idx) @@ -129,22 +144,23 @@ def get_alignment_indices(n_imgs, ref_img_idx=None): def calc_memory_size_gb(shape, nchannels, np_dtype): - """Estimate amount of space an image will take up, in Gb - """ + """Estimate amount of space an image will take up, in Gb""" - bitdepth = "".join(re.findall(r'\d+', np_dtype)) + bitdepth = "".join(re.findall(r"\d+", np_dtype)) if len(bitdepth) > 0: bitdepth = eval(bitdepth) else: bitdepth = 1 - n_px = nchannels*np.multiply(*shape) - gb = ((n_px*8)/bitdepth)/(2**30) + n_px = nchannels * np.multiply(*shape) + gb = ((n_px * 8) / bitdepth) / (2**30) return gb -def remove_invasive_displacements(bk_dxdy, M, src_shape_rc, out_shape_rc, inpaint_holes=False): +def remove_invasive_displacements( + bk_dxdy, M, src_shape_rc, out_shape_rc, inpaint_holes=False +): """Remove displacements that would distort the image edges Finds areas where areas outside of the image get brought inside. Can happen if displacements are combined. @@ -170,21 +186,28 @@ def remove_invasive_displacements(bk_dxdy, M, src_shape_rc, out_shape_rc, inpain new_dx = bk_dxdy[0].copy() new_dy = bk_dxdy[1].copy() if M is not None: - affine_mask = warp_img(np.full(src_shape_rc, 255, dtype=np.uint8), M, out_shape_rc=out_shape_rc, interp_method="nearest") + affine_mask = warp_img( + np.full(src_shape_rc, 255, dtype=np.uint8), + M, + out_shape_rc=out_shape_rc, + interp_method="nearest", + ) if not np.all(out_shape_rc == bk_dxdy[0].shape): - affine_mask = resize_img(affine_mask, bk_dxdy[0].shape, interp_method="nearest") + affine_mask = resize_img( + affine_mask, bk_dxdy[0].shape, interp_method="nearest" + ) new_dx[affine_mask == 0] = 0 new_dy[affine_mask == 0] = 0 else: affine_mask = np.full(out_shape_rc, 255, dtype=np.uint8) - inv_mask = 255*(affine_mask == 0).astype(np.uint8) + inv_mask = 255 * (affine_mask == 0).astype(np.uint8) inv_nr = warp_img(inv_mask, bk_dxdy=bk_dxdy) - out_to_in = ((inv_nr > 0) & (affine_mask > 0)) + out_to_in = (inv_nr > 0) & (affine_mask > 0) selem = morphology.disk(3) - out_to_in = morphology.binary_dilation(out_to_in, selem) + out_to_in = morphology.binary_dilation(out_to_in, selem) new_dy = bk_dxdy[1].copy() new_dx = bk_dxdy[0].copy() @@ -194,12 +217,16 @@ def remove_invasive_displacements(bk_dxdy, M, src_shape_rc, out_shape_rc, inpain nr_img = np.round(warp_img(affine_mask, bk_dxdy=[new_dx, new_dy])).astype(np.uint8) - holes_mask = ((nr_img == 0) & (affine_mask > 0)) - holes_mask = 255*(morphology.binary_dilation(holes_mask, selem)).astype(np.uint8) + holes_mask = (nr_img == 0) & (affine_mask > 0) + holes_mask = 255 * (morphology.binary_dilation(holes_mask, selem)).astype(np.uint8) if inpaint_holes and holes_mask.max() > 0: - new_dx = cv2.inpaint(new_dx.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA) - new_dy = cv2.inpaint(new_dy.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA) + new_dx = cv2.inpaint( + new_dx.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA + ) + new_dy = cv2.inpaint( + new_dy.astype(np.float32), holes_mask, 3, cv2.INPAINT_TELEA + ) else: new_dx[holes_mask > 0] = 0 new_dy[holes_mask > 0] = 0 @@ -232,15 +259,13 @@ def resize_img(img, out_shape_rc, interp_method="bicubic"): out_h, out_w = out_shape_rc src_shape_rc = np.array([img.height, img.width]) - sy, sx = (np.array(out_shape_rc)/src_shape_rc) + sy, sx = np.array(out_shape_rc) / src_shape_rc S = [sx, 0, 0, sy] interpolator = pyvips.Interpolate.new(interp_method) - resized = img.affine(S, - oarea=[0, 0, out_w, out_h], - interpolate=interpolator, - premultiplied=True - ) + resized = img.affine( + S, oarea=[0, 0, out_w, out_h], interpolate=interpolator, premultiplied=True + ) if is_array: resized = vips2numpy(resized) @@ -254,16 +279,18 @@ def scale_dxdy(dxdy, out_shape_rc): else: vips_dxdy = dxdy - sxy = (np.array(out_shape_rc)/np.array([vips_dxdy.height, vips_dxdy.width]))[::-1] - scaled_dx = float(sxy[0])*vips_dxdy[0] - scaled_dy = float(sxy[1])*vips_dxdy[1] + sxy = (np.array(out_shape_rc) / np.array([vips_dxdy.height, vips_dxdy.width]))[::-1] + scaled_dx = float(sxy[0]) * vips_dxdy[0] + scaled_dy = float(sxy[1]) * vips_dxdy[1] scaled_dxdy = scaled_dx.bandjoin(scaled_dy) scaled_dxdy = resize_img(scaled_dxdy, out_shape_rc) return scaled_dxdy -def get_src_img_shape_and_M(M, transformation_src_shape_rc, transformation_dst_shape_rc, dst_shape_rc): +def get_src_img_shape_and_M( + M, transformation_src_shape_rc, transformation_dst_shape_rc, dst_shape_rc +): """Determine the size of an image that, when warped, will have the same relative position as in the original transformation dst image. @@ -301,13 +328,15 @@ def get_src_img_shape_and_M(M, transformation_src_shape_rc, transformation_dst_s """ img_corners_xy = get_corners_of_image(transformation_src_shape_rc)[:, ::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc - ) + warped_corners = warp_xy( + img_corners_xy, + M=M, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + ) - dst_sxy = (np.array(dst_shape_rc)/np.array(transformation_dst_shape_rc))[::-1] - scaled_warped_corners = dst_sxy*warped_corners + dst_sxy = (np.array(dst_shape_rc) / np.array(transformation_dst_shape_rc))[::-1] + scaled_warped_corners = dst_sxy * warped_corners scaled_M = scale_M(M, *dst_sxy) scaled_unwarped_corners = warp_xy(scaled_warped_corners, M=np.linalg.inv(scaled_M)) @@ -339,7 +368,7 @@ def save_img(dst_f, img, thumbnail_size=None): if thumbnail_size is not None: vips_wh = np.array([vips_img.width, vips_img.height]) - s = np.min(thumbnail_size/vips_wh) + s = np.min(thumbnail_size / vips_wh) if s < 1: out_img = vips_img.resize(s) else: @@ -353,7 +382,9 @@ def save_img(dst_f, img, thumbnail_size=None): def get_pts_in_bbox(xy, xywh): x0, y0 = xywh[0:2] x1, y1 = xywh[0:2] + xywh[2:] - in_bbox_idx = np.where((xy[:, 0] >= x0) & (xy[:, 0] < x1) & (xy[:, 1] >= y0) & (xy[:, 1] < y1)==True)[0] + in_bbox_idx = np.where( + (xy[:, 0] >= x0) & (xy[:, 0] < x1) & (xy[:, 1] >= y0) & (xy[:, 1] < y1) == True + )[0] xy_in_bbox = xy[in_bbox_idx] return xy_in_bbox, in_bbox_idx @@ -378,7 +409,7 @@ def get_img_dimensions(img_f): def get_shape(img): - """ Get shape of image (row, col, nchannels) + """Get shape of image (row, col, nchannels) Parameters ---------- @@ -410,9 +441,7 @@ def get_shape(img): def apply_mask(img, mask): - """Mask an image - - """ + """Mask an image""" mask_is_vips = isinstance(mask, pyvips.Image) if not mask_is_vips: vips_mask = numpy2vips(mask) @@ -480,12 +509,16 @@ def get_grid_bboxes(shape_rc, bbox_w, bbox_h, inclusive=False): temp_y = np.hstack([temp_y, shape_rc[0]]) tl_y, tl_x = np.meshgrid(temp_y, temp_x, indexing="ij") - bbox_list = [[tl_x[i, j], - tl_y[i, j], - tl_x[i+1, j+1] - tl_x[i, j], - tl_y[i+1, j+1] - tl_y[i, j]] - for i in range(len(temp_y)-1) - for j in range(len(temp_x)-1)] + bbox_list = [ + [ + tl_x[i, j], + tl_y[i, j], + tl_x[i + 1, j + 1] - tl_x[i, j], + tl_y[i + 1, j + 1] - tl_y[i, j], + ] + for i in range(len(temp_y) - 1) + for j in range(len(temp_x) - 1) + ] return np.array(bbox_list) @@ -495,7 +528,7 @@ def expand_bbox(bbox_xywh, expand, shape_rc=None): new_xy[new_xy < 0] = 0 new_x, new_y = new_xy - new_w, new_h = bbox_xywh[2:] + 2*expand + new_w, new_h = bbox_xywh[2:] + 2 * expand if shape_rc is not None: h, w = shape_rc @@ -537,7 +570,9 @@ def stitch_tiles(tile_list, tile_bboxes, nrow, ncol, overlap): offset = x_offset - row_mosaic.width right_tile = col_tiles[j] - row_mosaic = row_mosaic.merge(right_tile, "horizontal", offset, 0, mblend=overlap) + row_mosaic = row_mosaic.merge( + right_tile, "horizontal", offset, 0, mblend=overlap + ) row_mosaics[i] = row_mosaic stitched = row_mosaics[0] @@ -560,7 +595,7 @@ def stitch_tiles(tile_list, tile_bboxes, nrow, ncol, overlap): def index2d_to_1d(row, col, ncol): - idx = (ncol*row) + col + idx = (ncol * row) + col return idx @@ -594,12 +629,16 @@ def get_triangular_mesh(x_pos, y_pos): """ tl_y, tl_x = np.meshgrid(y_pos, x_pos, indexing="ij") - grid_boxes_wh = [[tl_x[i, j], - tl_y[i, j], - tl_x[i+1, j+1] - tl_x[i, j], - tl_y[i+1, j+1] - tl_y[i, j]] - for i in range(len(y_pos)-1) - for j in range(len(x_pos)-1)] + grid_boxes_wh = [ + [ + tl_x[i, j], + tl_y[i, j], + tl_x[i + 1, j + 1] - tl_x[i, j], + tl_y[i + 1, j + 1] - tl_y[i, j], + ] + for i in range(len(y_pos) - 1) + for j in range(len(x_pos) - 1) + ] grid_boxes_xy = [bbox2xy(wh) for wh in grid_boxes_wh] vert_dict = {} @@ -607,7 +646,7 @@ def get_triangular_mesh(x_pos, y_pos): current_max_vert_id = 0 for bbox_xy in grid_boxes_xy: bbox = xy2bbox(bbox_xy) - bbox_center_xy = tuple(bbox[0:2] + bbox[2:]/2) + bbox_center_xy = tuple(bbox[0:2] + bbox[2:] / 2) bbox_tuples = [tuple(xy) for xy in bbox_xy] for vert in bbox_tuples: if not vert in vert_dict: @@ -619,25 +658,32 @@ def get_triangular_mesh(x_pos, y_pos): # 4 triangles in bbox. Bbbox : 0=TL, 1=TR, 2=BR, 3=BL # # Each sorted clockwise, with A= being most top left - left_face = [vert_dict[bbox_tuples[0]], - vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[3]]] - - top_face = [vert_dict[bbox_tuples[0]], - vert_dict[bbox_tuples[1]], - vert_dict[bbox_center_xy]] - - right_face = [vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[1]], - vert_dict[bbox_tuples[2]]] - - btm_face = [vert_dict[bbox_center_xy], - vert_dict[bbox_tuples[2]], - vert_dict[bbox_tuples[3]]] + left_face = [ + vert_dict[bbox_tuples[0]], + vert_dict[bbox_center_xy], + vert_dict[bbox_tuples[3]], + ] + + top_face = [ + vert_dict[bbox_tuples[0]], + vert_dict[bbox_tuples[1]], + vert_dict[bbox_center_xy], + ] + + right_face = [ + vert_dict[bbox_center_xy], + vert_dict[bbox_tuples[1]], + vert_dict[bbox_tuples[2]], + ] + + btm_face = [ + vert_dict[bbox_center_xy], + vert_dict[bbox_tuples[2]], + vert_dict[bbox_tuples[3]], + ] tri_faces.extend([left_face, top_face, right_face, btm_face]) - temp_tri_verts = list(vert_dict.keys()) tri_verts = np.array([temp_tri_verts[i] for i in vert_dict.values()]) tri_faces = np.array(tri_faces) @@ -645,7 +691,7 @@ def get_triangular_mesh(x_pos, y_pos): return tri_verts, tri_faces -def mattes_mi(img1, img2, nbins=50, mask=None): +def mattes_mi(img1, img2, nbins=50, mask=None): """Measure Mattes mutual information between 2 images. Parameters @@ -688,7 +734,7 @@ def mattes_mi(img1, img2, nbins=50, mask=None): mmi = reg.MetricEvaluate(sitk.GetImageFromArray(img1), sitk.GetImageFromArray(img2)) - return -1*mmi + return -1 * mmi def calc_rotated_shape(w, h, degree): @@ -698,7 +744,6 @@ def calc_rotated_shape(w, h, degree): new_w = np.abs(w * np.cos(rad)) + np.abs(h * np.sin(rad)) new_h = np.abs(w * np.sin(rad)) + np.abs(h * np.cos(rad)) - return new_w, new_h @@ -708,7 +753,7 @@ def rotate( resize=True, center=None, order=None, - mode='constant', + mode="constant", cval=0, clip=True, preserve_range=True, @@ -834,7 +879,6 @@ def rotate( return warped, tform - def order_points(pts_xy): """ Order points in clockwise order (TL, TR, BR, BL) @@ -866,14 +910,14 @@ def order_points(pts_xy): # y-coordinates so we can grab the top-left and bottom-left # points, respectively leftMost = leftMost[np.argsort(leftMost[:, 1]), :] - (tl, bl) = leftMost + tl, bl = leftMost # now that we have the top-left coordinate, use it as an # anchor to calculate the Euclidean distance between the # top-left and right-most points; by the Pythagorean # theorem, the point with the largest distance will be # our bottom-right point D = spatial.distance.cdist(tl[np.newaxis], rightMost, "euclidean")[0] - (br, tr) = rightMost[np.argsort(D)[::-1], :] + br, tr = rightMost[np.argsort(D)[::-1], :] # return the coordinates in top-left, top-right, # bottom-right, and bottom-left order @@ -886,7 +930,7 @@ def get_resize_M(in_shape_rc, out_shape_rc): in_corners = get_corners_of_image(in_shape_rc) out_corners = get_corners_of_image(out_shape_rc) - sy, sx = out_corners[2]/in_corners[2] + sy, sx = out_corners[2] / in_corners[2] resize_M = np.identity(3) resize_M[0, 0] = sx @@ -930,16 +974,16 @@ def _numpy2vips_pre_22(a): """ dtype_to_format = { - 'uint8': 'uchar', - 'int8': 'char', - 'uint16': 'ushort', - 'int16': 'short', - 'uint32': 'uint', - 'int32': 'int', - 'float32': 'float', - 'float64': 'double', - 'complex64': 'complex', - 'complex128': 'dpcomplex', + "uint8": "uchar", + "int8": "char", + "uint16": "ushort", + "int16": "short", + "uint32": "uint", + "int32": "int", + "float32": "float", + "float64": "double", + "complex64": "complex", + "complex128": "dpcomplex", } if a.ndim > 2: @@ -949,8 +993,9 @@ def _numpy2vips_pre_22(a): bands = 1 linear = a.reshape(width * height * bands) - vi = pyvips.Image.new_from_memory(linear.data, width, height, bands, - dtype_to_format[str(a.dtype)]) + vi = pyvips.Image.new_from_memory( + linear.data, width, height, bands, dtype_to_format[str(a.dtype)] + ) return vi @@ -960,21 +1005,23 @@ def _vips2numpy_pre_22(vi): """ format_to_dtype = { - 'uchar': np.uint8, - 'char': np.int8, - 'ushort': np.uint16, - 'short': np.int16, - 'uint': np.uint32, - 'int': np.int32, - 'float': np.float32, - 'double': np.float64, - 'complex': np.complex64, - 'dpcomplex': np.complex128, + "uchar": np.uint8, + "char": np.int8, + "ushort": np.uint16, + "short": np.int16, + "uint": np.uint32, + "int": np.int32, + "float": np.float32, + "double": np.float64, + "complex": np.complex64, + "dpcomplex": np.complex128, } - img = np.ndarray(buffer=vi.write_to_memory(), - dtype=format_to_dtype[vi.format], - shape=[vi.height, vi.width, vi.bands]) + img = np.ndarray( + buffer=vi.write_to_memory(), + dtype=format_to_dtype[vi.format], + shape=[vi.height, vi.width, vi.bands], + ) if vi.bands == 1: img = img[..., 0] @@ -1002,6 +1049,7 @@ def numpy2vips(a): return vi + def vips2numpy(vi): if is_pyvips_22(): a = _vips2numpy_22(vi) @@ -1013,17 +1061,24 @@ def vips2numpy(vi): def pad_img(img, padded_shape, interp_method="bicubic"): padding_T = get_padding_matrix(get_shape(img)[0:2], padded_shape) - padded_img = warp_img(img, padding_T, out_shape_rc=padded_shape, interp_method=interp_method) + padded_img = warp_img( + img, padding_T, out_shape_rc=padded_shape, interp_method=interp_method + ) return padded_img, padding_T -def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, - transformation_src_shape_rc=None, - transformation_dst_shape_rc=None, - bbox_xywh=None, - bg_color=None, - interp_method="bicubic"): +def warp_img( + img, + M=None, + bk_dxdy=None, + out_shape_rc=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + bbox_xywh=None, + bg_color=None, + interp_method="bicubic", +): """Warp an image using rigid and/or non-rigid transformations Warp an image using the trasformations defined by `M` and the optional @@ -1094,9 +1149,15 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, elif out_shape_rc is not None: transformation_dst_shape_rc = out_shape_rc else: - transformation_src_corners_rc = get_corners_of_image(transformation_src_shape_rc) - warped_transformation_src_corners_xy = warp_xy(transformation_src_corners_rc[:, ::-1], M) - transformation_dst_shape_rc = np.ceil(np.max(warped_transformation_src_corners_xy[:, ::-1], axis=0)).astype(int) + transformation_src_corners_rc = get_corners_of_image( + transformation_src_shape_rc + ) + warped_transformation_src_corners_xy = warp_xy( + transformation_src_corners_rc[:, ::-1], M + ) + transformation_dst_shape_rc = np.ceil( + np.max(warped_transformation_src_corners_xy[:, ::-1], axis=0) + ).astype(int) # Determine shape of scaled output if out_shape_rc is None: @@ -1107,11 +1168,15 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, out_shape_rc = np.array(out_shape_rc) transformation_dst_shape_rc = np.array(transformation_dst_shape_rc) - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors( - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=out_shape_rc, - bk_dxdy=bk_dxdy) + src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = ( + get_warp_scaling_factors( + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=out_shape_rc, + bk_dxdy=bk_dxdy, + ) + ) if bbox_xywh is not None: do_crop = True # Taking ceiling can prevent shifting down & right by 1 pixel when warping images much larger than the one used to find M @@ -1148,11 +1213,14 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, if do_rigid: if not np.all(src_sxy == 1): img_corners_xy = get_corners_of_image(src_shape_rc)[:, ::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, - dst_shape_rc=out_shape_rc) + warped_corners = warp_xy( + img_corners_xy, + M=M, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=out_shape_rc, + ) M_tform = transform.ProjectiveTransform() M_tform.estimate(warped_corners, img_corners_xy) warp_M = M_tform.params @@ -1164,15 +1232,16 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, warp_M = np.linalg.inv(warp_M) vips_M = warp_M[:2, :2].reshape(-1).tolist() - affine_warped = img.affine(vips_M, + affine_warped = img.affine( + vips_M, oarea=[0, 0, out_shape_rc[1], out_shape_rc[0]], interpolate=interpolator, idx=-tx, idy=-ty, premultiplied=True, background=bg_color, - extend=bg_extender - ) + extend=bg_extender, + ) else: affine_warped = img @@ -1196,22 +1265,25 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, else: S = [1.0, 0.0, 0.0, 1.0] - - warp_dxdy = vips_dxdy.affine(S, - oarea=[0, 0, out_shape_rc[1], out_shape_rc[0]], - interpolate=interpolator, - premultiplied=True) + warp_dxdy = vips_dxdy.affine( + S, + oarea=[0, 0, out_shape_rc[1], out_shape_rc[0]], + interpolate=interpolator, + premultiplied=True, + ) index = pyvips.Image.xyz(affine_warped.width, affine_warped.height) warp_index = (index[0] + warp_dxdy[0]).bandjoin(index[1] + warp_dxdy[1]) try: - #Option to set backround color in mapim added in libvips 8.13 - warped = affine_warped.mapim(warp_index, + # Option to set backround color in mapim added in libvips 8.13 + warped = affine_warped.mapim( + warp_index, premultiplied=True, background=bg_color, extend=bg_extender, - interpolate=interpolator) + interpolate=interpolator, + ) except pyvips.error.Error: warped = affine_warped.mapim(warp_index, interpolate=interpolator) @@ -1230,7 +1302,17 @@ def warp_img(img, M=None, bk_dxdy=None, out_shape_rc=None, return warped -def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, bk_dxdy=None, bg_color=None, interp_method="bicubic"): +def warp_img_inv( + img, + M=None, + fwd_dxdy=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + bk_dxdy=None, + bg_color=None, + interp_method="bicubic", +): """Unwarp an image using rigid and/or non-rigid transformations Unwarp an image using the trasformations defined by `M` and the optional @@ -1291,10 +1373,16 @@ def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, t if transformation_dst_shape_rc is None: transformation_dst_shape_rc = warped_src_shape_rc - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=warped_src_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) + src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = ( + get_warp_scaling_factors( + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=warped_src_shape_rc, + bk_dxdy=bk_dxdy, + fwd_dxdy=fwd_dxdy, + ) + ) # Do transformations if bg_color is None: @@ -1327,21 +1415,25 @@ def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, t else: S = [1.0, 0.0, 0.0, 1.0] - warp_dxdy = vips_dxdy.affine(S, - oarea=[0, 0, img.width, img.height], - interpolate=interpolator, - premultiplied=True) + warp_dxdy = vips_dxdy.affine( + S, + oarea=[0, 0, img.width, img.height], + interpolate=interpolator, + premultiplied=True, + ) index = pyvips.Image.xyz(img.width, img.height) warp_index = (index[0] + warp_dxdy[0]).bandjoin(index[1] + warp_dxdy[1]) try: # Option to set backround color in mapim added in libvips 8.13 - nr_warped = img.mapim(warp_index, + nr_warped = img.mapim( + warp_index, premultiplied=True, background=bg_color, extend=bg_extender, - interpolate=interpolator) + interpolate=interpolator, + ) except pyvips.error.Error: nr_warped = img.mapim(warp_index, interpolate=interpolator) @@ -1354,11 +1446,14 @@ def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, t if do_rigid: img_corners_xy = get_corners_of_image(src_shape_rc)[:, ::-1] - warped_corners = warp_xy(img_corners_xy, M=M, - transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, - dst_shape_rc=warped_src_shape_rc) + warped_corners = warp_xy( + img_corners_xy, + M=M, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=warped_src_shape_rc, + ) M_tform = transform.ProjectiveTransform() M_tform.estimate(img_corners_xy, warped_corners) warp_M = M_tform.params @@ -1366,32 +1461,42 @@ def warp_img_inv(img, M=None, fwd_dxdy=None, transformation_src_shape_rc=None, t tx, ty = warp_M[:2, 2] warp_M = np.linalg.inv(warp_M) vips_M = warp_M[:2, :2].reshape(-1).tolist() - warped = nr_warped.affine(vips_M, - oarea=[0, 0, src_shape_rc[1], src_shape_rc[0]], - interpolate=interpolator, - idx=-tx, - idy=-ty, - premultiplied=True, - background=bg_color, - extend=bg_extender - ) + warped = nr_warped.affine( + vips_M, + oarea=[0, 0, src_shape_rc[1], src_shape_rc[0]], + interpolate=interpolator, + idx=-tx, + idy=-ty, + premultiplied=True, + background=bg_color, + extend=bg_extender, + ) else: warped = nr_warped - if is_array: warped = vips2numpy(warped) return warped -def warp_img_from_to(img, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, - from_dst_shape_rc=None, from_bk_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_bk_dxdy=None, to_fwd_dxdy=None, bg_color=None, interp_method="bicubic"): +def warp_img_from_to( + img, + from_M=None, + from_transformation_src_shape_rc=None, + from_transformation_dst_shape_rc=None, + from_dst_shape_rc=None, + from_bk_dxdy=None, + to_M=None, + to_transformation_src_shape_rc=None, + to_transformation_dst_shape_rc=None, + to_src_shape_rc=None, + to_bk_dxdy=None, + to_fwd_dxdy=None, + bg_color=None, + interp_method="bicubic", +): """Warp image onto another Warps `img` to registered coordinates using the "from" parameters, and then uses @@ -1463,27 +1568,28 @@ def warp_img_from_to(img, from_M=None, from_transformation_src_shape_rc=None, """ + in_reg_space = warp_img( + img, + M=from_M, + bk_dxdy=from_bk_dxdy, + out_shape_rc=from_dst_shape_rc, + transformation_src_shape_rc=from_transformation_src_shape_rc, + transformation_dst_shape_rc=from_transformation_dst_shape_rc, + bg_color=bg_color, + interp_method=interp_method, + ) - in_reg_space = warp_img(img, - M=from_M, - bk_dxdy=from_bk_dxdy, - out_shape_rc=from_dst_shape_rc, - transformation_src_shape_rc=from_transformation_src_shape_rc, - transformation_dst_shape_rc=from_transformation_dst_shape_rc, - bg_color=bg_color, - interp_method=interp_method - ) - - in_target_space = warp_img_inv(img=in_reg_space, - M=to_M, - fwd_dxdy=to_fwd_dxdy, - transformation_src_shape_rc=to_transformation_src_shape_rc, - transformation_dst_shape_rc=to_transformation_dst_shape_rc, - src_shape_rc=to_src_shape_rc, - bk_dxdy=to_bk_dxdy, - bg_color=bg_color, - interp_method=interp_method - ) + in_target_space = warp_img_inv( + img=in_reg_space, + M=to_M, + fwd_dxdy=to_fwd_dxdy, + transformation_src_shape_rc=to_transformation_src_shape_rc, + transformation_dst_shape_rc=to_transformation_dst_shape_rc, + src_shape_rc=to_src_shape_rc, + bk_dxdy=to_bk_dxdy, + bg_color=bg_color, + interp_method=interp_method, + ) return in_target_space @@ -1502,9 +1608,15 @@ def crop_img(img, xywh): return cropped -def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, - dst_shape_rc=None, transformation_src_shape_rc=None, - src_shape_rc=None, return_xy=False): +def get_warp_map( + M=None, + dxdy=None, + transformation_dst_shape_rc=None, + dst_shape_rc=None, + transformation_src_shape_rc=None, + src_shape_rc=None, + return_xy=False, +): """Get map to warp an image Get a coordinate map that will perform the warp defined by M and the optional displacement field, dxdy Map can be scaled so that it can be applied to an image with shape unwarped_out_shape_rc @@ -1543,7 +1655,6 @@ def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, """ - if M is None and dxdy is None: warnings.warn("Please provide `M` and/or `dxdy`") return None @@ -1561,7 +1672,6 @@ def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, if src_shape_rc is None: src_shape_rc = transformation_src_shape_rc - if np.all(transformation_dst_shape_rc == dst_shape_rc): grid_r, grid_c = np.indices(transformation_dst_shape_rc) @@ -1570,10 +1680,12 @@ def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, scaled_x = np.linspace(0, dst_shape_rc[1], num=transformation_dst_shape_rc[1]) grid_y, grid_x = np.meshgrid(scaled_y, scaled_x, indexing="ij") scaled_xy = np.dstack([grid_x.reshape(-1), grid_y.reshape(-1)])[0] - sy, sx = np.array(dst_shape_rc)/np.array(transformation_dst_shape_rc) + sy, sx = np.array(dst_shape_rc) / np.array(transformation_dst_shape_rc) S = transform.SimilarityTransform(scale=(sx, sy)) src_xy_pos = S.inverse(scaled_xy) - grid_r, grid_c = src_xy_pos[:, 1].reshape(transformation_dst_shape_rc), src_xy_pos[:, 0].reshape(transformation_dst_shape_rc) + grid_r, grid_c = src_xy_pos[:, 1].reshape( + transformation_dst_shape_rc + ), src_xy_pos[:, 0].reshape(transformation_dst_shape_rc) if dxdy is None: r_in_src = grid_r @@ -1584,17 +1696,29 @@ def get_warp_map(M=None, dxdy=None, transformation_dst_shape_rc=None, if M is not None: tformer = transform.ProjectiveTransform(matrix=M) - xy_pos_in_src = tformer(np.dstack([c_in_src.reshape(-1), r_in_src.reshape(-1)])[0]) - xy_pos_in_src = [xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc)] + xy_pos_in_src = tformer( + np.dstack([c_in_src.reshape(-1), r_in_src.reshape(-1)])[0] + ) + xy_pos_in_src = [ + xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), + xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc), + ] else: xy_pos_in_src = [c_in_src, r_in_src] if np.any(transformation_src_shape_rc != src_shape_rc): - in_scale_y, in_scale_x = np.array(src_shape_rc)/np.array(transformation_src_shape_rc) + in_scale_y, in_scale_x = np.array(src_shape_rc) / np.array( + transformation_src_shape_rc + ) in_S = transform.SimilarityTransform(scale=(in_scale_x, in_scale_y)) - xy_pos_in_src = in_S(np.dstack([xy_pos_in_src[0].reshape(-1), xy_pos_in_src[1].reshape(-1)])[0]) - xy_pos_in_src = [xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc)] + xy_pos_in_src = in_S( + np.dstack([xy_pos_in_src[0].reshape(-1), xy_pos_in_src[1].reshape(-1)])[0] + ) + xy_pos_in_src = [ + xy_pos_in_src[:, 0].reshape(transformation_dst_shape_rc), + xy_pos_in_src[:, 1].reshape(transformation_dst_shape_rc), + ] if return_xy: c1, c2 = 0, 1 @@ -1610,11 +1734,11 @@ def get_padding_matrix(img_shape_rc, out_shape_rc): img_h, img_w = img_shape_rc out_h, out_w = out_shape_rc - d_h = (out_h - img_h) - d_w = (out_w - img_w) + d_h = out_h - img_h + d_w = out_w - img_w - h_pad = d_h/2 - w_pad = d_w/2 + h_pad = d_h / 2 + w_pad = d_w / 2 T = np.identity(3).astype(np.float64) T[0, 2] = -w_pad T[1, 2] = -h_pad @@ -1665,7 +1789,10 @@ def get_img_area(img_shape_rc, M=None): prev_img_corners = warp_xy(prev_img_corners, M) prev_img_corners = order_points(prev_img_corners) - prev_area = 0.5*np.abs(np.dot(prev_img_corners[:, 0],np.roll(prev_img_corners[:, 1],1))-np.dot(prev_img_corners[:, 1],np.roll(prev_img_corners[:, 0],1))) + prev_area = 0.5 * np.abs( + np.dot(prev_img_corners[:, 0], np.roll(prev_img_corners[:, 1], 1)) + - np.dot(prev_img_corners[:, 1], np.roll(prev_img_corners[:, 0], 1)) + ) return prev_area @@ -1679,7 +1806,7 @@ def get_overlap_mask(img1, img2): def center_and_get_translation_matrix(img_shape_rc, x, y, w, h): - ''' + """ x, y, w, h attributes or :param img_shape_rc: :param x: @@ -1687,13 +1814,12 @@ def center_and_get_translation_matrix(img_shape_rc, x, y, w, h): :param w: :param h: :return: - ''' + """ # Center smaller image inside larger image # img_center_w = int(img_shape_rc[1] / 2) img_center_h = int(img_shape_rc[0] / 2) - out_center_w = int(w / 2) + x out_center_h = int(h / 2) + y @@ -1781,7 +1907,7 @@ def get_rotate_around_center_M(img_shape, rotation_rad): rows, cols = img_shape[0:2] # rotation around center - center = np.array((cols, rows)) / 2. - 0.5 + center = np.array((cols, rows)) / 2.0 - 0.5 tform1 = transform.SimilarityTransform(translation=center) tform2 = transform.SimilarityTransform(rotation=rotation_rad) tform3 = transform.SimilarityTransform(translation=-center) @@ -1807,7 +1933,7 @@ def calc_d(pt1, pt2): distnace between correspoing points in pt1 and pt2 """ - d = np.sqrt(np.sum((pt1 - pt2)**2, axis=1)) + d = np.sqrt(np.sum((pt1 - pt2) ** 2, axis=1)) return d @@ -1848,17 +1974,16 @@ def get_mesh(shape, grid_spacing, bbox_rc_wh=None, inclusive=False): c_grid_pts = np.arange(min_c, max_c, grid_spacing) if inclusive: - if max(r_grid_pts) != shape[0]-1: - r_grid_pts = np.hstack([r_grid_pts, shape[0]-1]) + if max(r_grid_pts) != shape[0] - 1: + r_grid_pts = np.hstack([r_grid_pts, shape[0] - 1]) - if max(c_grid_pts) != shape[1]-1: - c_grid_pts = np.hstack([c_grid_pts, shape[1]-1]) + if max(c_grid_pts) != shape[1] - 1: + c_grid_pts = np.hstack([c_grid_pts, shape[1] - 1]) return np.meshgrid(r_grid_pts, c_grid_pts, indexing="ij") -def smooth_dxdy(dxdy, grid_spacing_ratio=0.015, sigma_ratio=0.005, - method="gauss"): +def smooth_dxdy(dxdy, grid_spacing_ratio=0.015, sigma_ratio=0.005, method="gauss"): """Smooth displacement fields Use cubic interpolation to smooth displacement field @@ -1896,8 +2021,8 @@ def smooth_dxdy(dxdy, grid_spacing_ratio=0.015, sigma_ratio=0.005, dx, dy = dxdy if method.lower().startswith("c"): - grid_spacing_x = dx.shape[1]*grid_spacing_ratio - grid_spacing_y = dx.shape[0]*grid_spacing_ratio + grid_spacing_x = dx.shape[1] * grid_spacing_ratio + grid_spacing_y = dx.shape[0] * grid_spacing_ratio grid_spacing = int(np.mean([grid_spacing_x, grid_spacing_y])) subgrid_r, subgrid_c = get_mesh(dx.shape, grid_spacing, inclusive=True) @@ -1911,14 +2036,22 @@ def smooth_dxdy(dxdy, grid_spacing_ratio=0.015, sigma_ratio=0.005, subgrid_r_flat = subgrid_r.reshape(-1) subgrid_c_flat = subgrid_c.reshape(-1) - smooth_dx_interp = SmoothBivariateSpline(subgrid_r_flat, subgrid_c_flat, sub_dx.reshape(-1)) - smooth_dx = smooth_dx_interp(grid_xy[:, 1], grid_xy[:, 0], grid=False).reshape(dx.shape) + smooth_dx_interp = SmoothBivariateSpline( + subgrid_r_flat, subgrid_c_flat, sub_dx.reshape(-1) + ) + smooth_dx = smooth_dx_interp(grid_xy[:, 1], grid_xy[:, 0], grid=False).reshape( + dx.shape + ) - smooth_dy_interp = SmoothBivariateSpline(subgrid_r_flat, subgrid_c_flat, sub_dy.reshape(-1)) - smooth_dy = smooth_dx_interp(grid_xy[:, 1], grid_xy[:, 0], grid=False).reshape(dy.shape) + smooth_dy_interp = SmoothBivariateSpline( + subgrid_r_flat, subgrid_c_flat, sub_dy.reshape(-1) + ) + smooth_dy = smooth_dx_interp(grid_xy[:, 1], grid_xy[:, 0], grid=False).reshape( + dy.shape + ) elif method.lower().startswith("g"): - sigma = sigma_ratio*np.max(dx.shape) + sigma = sigma_ratio * np.max(dx.shape) smooth_dx = filters.gaussian(dx, sigma=sigma) smooth_dy = filters.gaussian(dy, sigma=sigma) @@ -1930,8 +2063,10 @@ def get_inverse_field(backwards_xy_deltas, n_inter=10): Invert transform """ - sitk_bk_dxdy = sitk.GetImageFromArray(np.dstack(backwards_xy_deltas), isVector=True) - sitk_fw_dxdy = sitk.IterativeInverseDisplacementField(sitk_bk_dxdy, numberOfIterations=n_inter) + sitk_bk_dxdy = sitk.GetImageFromArray(np.dstack(backwards_xy_deltas), isVector=True) + sitk_fw_dxdy = sitk.IterativeInverseDisplacementField( + sitk_bk_dxdy, numberOfIterations=n_inter + ) fwd_dxdy = sitk.GetArrayFromImage(sitk_fw_dxdy) fwd_dxdy = [fwd_dxdy[..., 0], fwd_dxdy[..., 1]] @@ -1939,7 +2074,7 @@ def get_inverse_field(backwards_xy_deltas, n_inter=10): def warp_xy_rigid(xy, inv_matrix): - """ Warp points + """Warp points Warp xy given an inverse transformation matrix found using one of scikit-image's transform objects Inverse matrix should have been found using tform(dst, src) @@ -1952,8 +2087,8 @@ def warp_xy_rigid(xy, inv_matrix): src_pts = np.vstack((x, y, np.ones_like(x))) try: dst_pts = src_pts.T @ np.linalg.inv(inv_matrix).T - except np.linalg.LinAlgError : - print("Singular matrix") + except np.linalg.LinAlgError: + logger.error("Singular matrix") dst_pts = src_pts.T @ np.linalg.pinv(inv_matrix).T # below, we will divide by the last dimension of the homogeneous @@ -1992,7 +2127,14 @@ def warp_xy_non_rigid(xy, dxdy, displacement_shape_rc=None): return nr_xy -def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, dst_shape_rc=None, bk_dxdy=None, fwd_dxdy=None): +def get_warp_scaling_factors( + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + bk_dxdy=None, + fwd_dxdy=None, +): """Get scaling factors needed to warp points If a returned value is None, it means there is no need to scale the image @@ -2042,7 +2184,7 @@ def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_ds if np.all(transformation_src_shape_rc == src_shape_rc): src_sxy = None else: - src_sxy = (src_shape_rc/transformation_src_shape_rc)[::-1] + src_sxy = (src_shape_rc / transformation_src_shape_rc)[::-1] else: src_sxy = None @@ -2069,7 +2211,7 @@ def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_ds displacement_shape_rc = np.array([fwd_dxdy.height, fwd_dxdy.width]) if transformation_dst_shape_rc is None and do_non_rigid: - transformation_dst_shape_rc = displacement_shape_rc + transformation_dst_shape_rc = displacement_shape_rc if dst_shape_rc is None and transformation_dst_shape_rc is not None: dst_shape_rc = transformation_dst_shape_rc @@ -2078,11 +2220,13 @@ def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_ds if do_non_rigid: if not np.all(transformation_dst_shape_rc == displacement_shape_rc): # non-rigid found on scaled image - displacement_sxy = (displacement_shape_rc/transformation_dst_shape_rc)[::-1] - dst_sxy = (dst_shape_rc/displacement_shape_rc)[::-1] + displacement_sxy = (displacement_shape_rc / transformation_dst_shape_rc)[ + ::-1 + ] + dst_sxy = (dst_shape_rc / displacement_shape_rc)[::-1] else: displacement_sxy = None - dst_sxy = (dst_shape_rc/transformation_dst_shape_rc)[::-1] + dst_sxy = (dst_shape_rc / transformation_dst_shape_rc)[::-1] if np.all(dst_sxy == 1): dst_sxy = None @@ -2093,21 +2237,28 @@ def get_warp_scaling_factors(transformation_src_shape_rc=None, transformation_ds displacement_sxy = None if transformation_dst_shape_rc is not None and dst_shape_rc is not None: if not np.all(dst_shape_rc == transformation_dst_shape_rc): - dst_sxy = (dst_shape_rc/transformation_dst_shape_rc)[::-1] + dst_sxy = (dst_shape_rc / transformation_dst_shape_rc)[::-1] return src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc - -def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=None, dst_sxy=None, displacement_sxy=None, displacement_shape_rc=None, pt_buffer=100): - """Warp single point when the displacement fields are pyvips.Image objects - - """ +def _warp_pt_vips( + xy, + M=None, + vips_bk_dxdy=None, + vips_fwd_dxdy=None, + src_sxy=None, + dst_sxy=None, + displacement_sxy=None, + displacement_shape_rc=None, + pt_buffer=100, +): + """Warp single point when the displacement fields are pyvips.Image objects""" do_non_rigid = vips_bk_dxdy is not None or vips_fwd_dxdy is not None if src_sxy is not None: - in_src_xy = xy/src_sxy + in_src_xy = xy / src_sxy else: in_src_xy = xy @@ -2116,7 +2267,7 @@ def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=Non rigid_xy = warp_xy_rigid(in_src_xy, M).astype(float)[0] if not do_non_rigid: if dst_sxy is not None: - return rigid_xy*dst_sxy + return rigid_xy * dst_sxy else: return rigid_xy else: @@ -2127,10 +2278,14 @@ def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=Non # So move points into new displacement field rigid_xy *= displacement_sxy - bbox_xy_tl = (rigid_xy - pt_buffer//2).astype(int) - bbox_xy_br = np.ceil(rigid_xy + pt_buffer//2).astype(int) - bbox_x01 = np.clip(np.array([bbox_xy_tl[0], bbox_xy_br[0]]), 0, displacement_shape_rc[1]) - bbox_y01 = np.clip(np.array([bbox_xy_tl[1], bbox_xy_br[1]]), 0, displacement_shape_rc[0]) + bbox_xy_tl = (rigid_xy - pt_buffer // 2).astype(int) + bbox_xy_br = np.ceil(rigid_xy + pt_buffer // 2).astype(int) + bbox_x01 = np.clip( + np.array([bbox_xy_tl[0], bbox_xy_br[0]]), 0, displacement_shape_rc[1] + ) + bbox_y01 = np.clip( + np.array([bbox_xy_tl[1], bbox_xy_br[1]]), 0, displacement_shape_rc[0] + ) bbox_w = -int(np.subtract(*bbox_x01)) bbox_h = -int(np.subtract(*bbox_y01)) @@ -2146,9 +2301,15 @@ def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=Non elif vips_bk_dxdy is not None and vips_fwd_dxdy is None: vips_region_bk_dxdy = vips_bk_dxdy.extract_area(*region_bbox_xywh) region_bk_dxdy = vips2numpy(vips_region_bk_dxdy) - region_dxdy = np.dstack(get_inverse_field(region_bk_dxdy[..., 0], region_bk_dxdy[..., 1])) - - nonrigid_xy = warp_xy_non_rigid(xy=rigid_xy_in_tile, dxdy=[region_dxdy[..., 0], region_dxdy[..., 1]], displacement_shape_rc=[bbox_h, bbox_w]) + region_dxdy = np.dstack( + get_inverse_field(region_bk_dxdy[..., 0], region_bk_dxdy[..., 1]) + ) + + nonrigid_xy = warp_xy_non_rigid( + xy=rigid_xy_in_tile, + dxdy=[region_dxdy[..., 0], region_dxdy[..., 1]], + displacement_shape_rc=[bbox_h, bbox_w], + ) nonrigid_xy += region_bbox_xywh[0:2] if dst_sxy is not None: @@ -2157,8 +2318,17 @@ def _warp_pt_vips(xy, M=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, src_sxy=Non return nonrigid_xy -def _warp_xy_vips(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, vips_bk_dxdy=None, vips_fwd_dxdy=None, pt_buffer=100): +def _warp_xy_vips( + xy, + M=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + vips_bk_dxdy=None, + vips_fwd_dxdy=None, + pt_buffer=100, +): """ Warp xy points using M and/or bk_dxdy/fwd_dxdy. Used when `vips_bk_dxdy` or `vips_fwd_dxdy` is a pyvips.Image @@ -2207,19 +2377,47 @@ def _warp_xy_vips(xy, M=None, transformation_src_shape_rc=None, transformation_d Array of warped xy coordinates for P points """ - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=vips_bk_dxdy, fwd_dxdy=vips_fwd_dxdy) - + src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = ( + get_warp_scaling_factors( + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=dst_shape_rc, + bk_dxdy=vips_bk_dxdy, + fwd_dxdy=vips_fwd_dxdy, + ) + ) - warped_xy = np.vstack([_warp_pt_vips(pt, M, vips_bk_dxdy=vips_bk_dxdy, vips_fwd_dxdy=vips_fwd_dxdy, src_sxy=src_sxy, dst_sxy=dst_sxy, displacement_sxy=displacement_sxy, displacement_shape_rc=displacement_shape_rc, pt_buffer=pt_buffer) for pt in xy]) + warped_xy = np.vstack( + [ + _warp_pt_vips( + pt, + M, + vips_bk_dxdy=vips_bk_dxdy, + vips_fwd_dxdy=vips_fwd_dxdy, + src_sxy=src_sxy, + dst_sxy=dst_sxy, + displacement_sxy=displacement_sxy, + displacement_shape_rc=displacement_shape_rc, + pt_buffer=pt_buffer, + ) + for pt in xy + ] + ) return warped_xy -def _warp_xy_numpy(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, bk_dxdy=None, fwd_dxdy=None): +def _warp_xy_numpy( + xy, + M=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + bk_dxdy=None, + fwd_dxdy=None, +): """ Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy @@ -2269,12 +2467,18 @@ def _warp_xy_numpy(xy, M=None, transformation_src_shape_rc=None, transformation_ if M is None and not do_non_rigid: return xy - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) + src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = ( + get_warp_scaling_factors( + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=dst_shape_rc, + bk_dxdy=bk_dxdy, + fwd_dxdy=fwd_dxdy, + ) + ) if src_sxy is not None: - in_src_xy = xy/src_sxy + in_src_xy = xy / src_sxy else: in_src_xy = xy @@ -2282,7 +2486,7 @@ def _warp_xy_numpy(xy, M=None, transformation_src_shape_rc=None, transformation_ rigid_xy = warp_xy_rigid(in_src_xy, M).astype(float) if not do_non_rigid: if dst_sxy is not None: - return rigid_xy*dst_sxy + return rigid_xy * dst_sxy else: return rigid_xy else: @@ -2296,16 +2500,27 @@ def _warp_xy_numpy(xy, M=None, transformation_src_shape_rc=None, transformation_ if bk_dxdy is not None and fwd_dxdy is None: fwd_dxdy = get_inverse_field(bk_dxdy) - nonrigid_xy = warp_xy_non_rigid(rigid_xy, dxdy=fwd_dxdy, displacement_shape_rc=displacement_shape_rc) + nonrigid_xy = warp_xy_non_rigid( + rigid_xy, dxdy=fwd_dxdy, displacement_shape_rc=displacement_shape_rc + ) if dst_sxy is not None: nonrigid_xy *= dst_sxy return nonrigid_xy -def warp_xy(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, - bk_dxdy=None, fwd_dxdy=None, pt_buffer=100): + +def warp_xy( + xy, + M=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + bk_dxdy=None, + fwd_dxdy=None, + pt_buffer=100, +): """ Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy @@ -2362,19 +2577,41 @@ def warp_xy(xy, M=None, transformation_src_shape_rc=None, transformation_dst_sha return xy if isinstance(bk_dxdy, pyvips.Image) or isinstance(fwd_dxdy, pyvips.Image): - warped_xy = _warp_xy_vips(xy, M, transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - vips_bk_dxdy=bk_dxdy, vips_fwd_dxdy=fwd_dxdy, pt_buffer=pt_buffer) + warped_xy = _warp_xy_vips( + xy, + M, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=dst_shape_rc, + vips_bk_dxdy=bk_dxdy, + vips_fwd_dxdy=fwd_dxdy, + pt_buffer=pt_buffer, + ) else: - warped_xy = _warp_xy_numpy(xy, M, transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) + warped_xy = _warp_xy_numpy( + xy, + M, + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=dst_shape_rc, + bk_dxdy=bk_dxdy, + fwd_dxdy=fwd_dxdy, + ) return warped_xy -def warp_xy_inv(xy, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, src_shape_rc=None, dst_shape_rc=None, bk_dxdy=None, fwd_dxdy=None): +def warp_xy_inv( + xy, + M=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + bk_dxdy=None, + fwd_dxdy=None, +): """Warp points from registered coordinates to original coordinates Parameters @@ -2417,13 +2654,19 @@ def warp_xy_inv(xy, M=None, transformation_src_shape_rc=None, transformation_dst if M is None and not do_non_rigid: return xy - src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = get_warp_scaling_factors(transformation_src_shape_rc=transformation_src_shape_rc, - transformation_dst_shape_rc=transformation_dst_shape_rc, - src_shape_rc=src_shape_rc, dst_shape_rc=dst_shape_rc, - bk_dxdy=bk_dxdy, fwd_dxdy=fwd_dxdy) + src_sxy, dst_sxy, displacement_sxy, displacement_shape_rc = ( + get_warp_scaling_factors( + transformation_src_shape_rc=transformation_src_shape_rc, + transformation_dst_shape_rc=transformation_dst_shape_rc, + src_shape_rc=src_shape_rc, + dst_shape_rc=dst_shape_rc, + bk_dxdy=bk_dxdy, + fwd_dxdy=fwd_dxdy, + ) + ) if dst_sxy is not None: - xy_in_reg_img = xy/dst_sxy + xy_in_reg_img = xy / dst_sxy else: xy_in_reg_img = xy @@ -2439,7 +2682,7 @@ def warp_xy_inv(xy, M=None, transformation_src_shape_rc=None, transformation_dst xy_in_rigid = xy_in_reg_img if M is not None: - xy_inv = warp_xy(xy_in_rigid, M=np.linalg.inv(M)) + xy_inv = warp_xy(xy_in_rigid, M=np.linalg.inv(M)) else: xy_inv = xy_in_rigid @@ -2449,12 +2692,23 @@ def warp_xy_inv(xy, M=None, transformation_src_shape_rc=None, transformation_dst return xy_inv -def warp_xy_from_to(xy, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, from_src_shape_rc=None, - from_dst_shape_rc=None,from_bk_dxdy=None, from_fwd_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_dst_shape_rc=None, to_bk_dxdy=None, to_fwd_dxdy=None): +def warp_xy_from_to( + xy, + from_M=None, + from_transformation_src_shape_rc=None, + from_transformation_dst_shape_rc=None, + from_src_shape_rc=None, + from_dst_shape_rc=None, + from_bk_dxdy=None, + from_fwd_dxdy=None, + to_M=None, + to_transformation_src_shape_rc=None, + to_transformation_dst_shape_rc=None, + to_src_shape_rc=None, + to_dst_shape_rc=None, + to_bk_dxdy=None, + to_fwd_dxdy=None, +): """Warp points in one image to their position in another unregistered image Takes a set of points found in the unwarped "from" image, and warps them to their @@ -2521,32 +2775,34 @@ def warp_xy_from_to(xy, from_M=None, from_transformation_src_shape_rc=None, """ - xy_in_reg_space = warp_xy(xy, M=from_M, - transformation_src_shape_rc=from_transformation_src_shape_rc, - transformation_dst_shape_rc=from_transformation_dst_shape_rc, - src_shape_rc=from_src_shape_rc, - dst_shape_rc=from_dst_shape_rc, - bk_dxdy=from_bk_dxdy, - fwd_dxdy=from_fwd_dxdy - ) - - xy_in_to_space = warp_xy_inv(xy_in_reg_space, M=to_M, - transformation_src_shape_rc=to_transformation_src_shape_rc, - transformation_dst_shape_rc=to_transformation_dst_shape_rc, - src_shape_rc=to_src_shape_rc, - dst_shape_rc=to_dst_shape_rc, - bk_dxdy=to_bk_dxdy, - fwd_dxdy=to_fwd_dxdy - ) + xy_in_reg_space = warp_xy( + xy, + M=from_M, + transformation_src_shape_rc=from_transformation_src_shape_rc, + transformation_dst_shape_rc=from_transformation_dst_shape_rc, + src_shape_rc=from_src_shape_rc, + dst_shape_rc=from_dst_shape_rc, + bk_dxdy=from_bk_dxdy, + fwd_dxdy=from_fwd_dxdy, + ) + + xy_in_to_space = warp_xy_inv( + xy_in_reg_space, + M=to_M, + transformation_src_shape_rc=to_transformation_src_shape_rc, + transformation_dst_shape_rc=to_transformation_dst_shape_rc, + src_shape_rc=to_src_shape_rc, + dst_shape_rc=to_dst_shape_rc, + bk_dxdy=to_bk_dxdy, + fwd_dxdy=to_fwd_dxdy, + ) return xy_in_to_space def clip_xy(xy, shape_rc): - """Clip xy coordintaes to be within image - - """ - clipped_x = np.clip(xy[:, 0], 0, shape_rc[1]) - clipped_y = np.clip(xy[:, 1], 0, shape_rc[0]) + """Clip xy coordintaes to be within image""" + clipped_x = np.clip(xy[:, 0], 0, shape_rc[1]) + clipped_y = np.clip(xy[:, 1], 0, shape_rc[0]) clipped_xy = np.dstack([clipped_x, clipped_y])[0] return clipped_xy @@ -2562,7 +2818,7 @@ def _warp_shapely(geom, warp_fxn, warp_kwargs, shift_xy=None): elif "to_dst_shape_rc" in warp_kwargs: dst_shape_rc = warp_kwargs["to_dst_shape_rc"] else: - dst_shape_rc = None + dst_shape_rc = None if geom.is_empty: return type(geom)([]) @@ -2598,14 +2854,27 @@ def _warp_shapely(geom, warp_fxn, warp_kwargs, shift_xy=None): return type(geom)(shell, holes) elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection": - return type(geom)([_warp_shapely(part, warp_fxn, warp_kwargs) for part in geom.geoms]) + return type(geom)( + [_warp_shapely(part, warp_fxn, warp_kwargs) for part in geom.geoms] + ) else: - raise shapely.errors.GeometryTypeError(f"Type {geom.geom_type!r} not recognized") - - -def warp_shapely_geom(geom, M=None, transformation_src_shape_rc=None, transformation_dst_shape_rc=None, - src_shape_rc=None, dst_shape_rc=None, - bk_dxdy=None, fwd_dxdy=None, pt_buffer=100, shift_xy=None): + raise shapely.errors.GeometryTypeError( + f"Type {geom.geom_type!r} not recognized" + ) + + +def warp_shapely_geom( + geom, + M=None, + transformation_src_shape_rc=None, + transformation_dst_shape_rc=None, + src_shape_rc=None, + dst_shape_rc=None, + bk_dxdy=None, + fwd_dxdy=None, + pt_buffer=100, + shift_xy=None, +): """ Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy @@ -2658,14 +2927,16 @@ def warp_shapely_geom(geom, M=None, transformation_src_shape_rc=None, transforma """ - warp_kwargs = {"M":M, - "transformation_src_shape_rc": transformation_src_shape_rc, - "transformation_dst_shape_rc": transformation_dst_shape_rc, - "src_shape_rc": src_shape_rc, - "dst_shape_rc": dst_shape_rc, - 'bk_dxdy': bk_dxdy, - "fwd_dxdy": fwd_dxdy, - "pt_buffer": pt_buffer} + warp_kwargs = { + "M": M, + "transformation_src_shape_rc": transformation_src_shape_rc, + "transformation_dst_shape_rc": transformation_dst_shape_rc, + "src_shape_rc": src_shape_rc, + "dst_shape_rc": dst_shape_rc, + "bk_dxdy": bk_dxdy, + "fwd_dxdy": fwd_dxdy, + "pt_buffer": pt_buffer, + } if shift_xy is not None: shift_xy = np.array(shift_xy) @@ -2675,13 +2946,23 @@ def warp_shapely_geom(geom, M=None, transformation_src_shape_rc=None, transforma return warped_geom - -def warp_shapely_geom_from_to(geom, from_M=None, from_transformation_src_shape_rc=None, - from_transformation_dst_shape_rc=None, from_src_shape_rc=None, - from_dst_shape_rc=None,from_bk_dxdy=None, from_fwd_dxdy=None, - to_M=None, to_transformation_src_shape_rc=None, - to_transformation_dst_shape_rc=None, to_src_shape_rc=None, - to_dst_shape_rc=None, to_bk_dxdy=None, to_fwd_dxdy=None): +def warp_shapely_geom_from_to( + geom, + from_M=None, + from_transformation_src_shape_rc=None, + from_transformation_dst_shape_rc=None, + from_src_shape_rc=None, + from_dst_shape_rc=None, + from_bk_dxdy=None, + from_fwd_dxdy=None, + to_M=None, + to_transformation_src_shape_rc=None, + to_transformation_dst_shape_rc=None, + to_src_shape_rc=None, + to_dst_shape_rc=None, + to_bk_dxdy=None, + to_fwd_dxdy=None, +): """ Warp xy points using M and/or bk_dxdy/fwd_dxdy. If bk_dxdy is provided, it will be inverted to create fwd_dxdy @@ -2732,19 +3013,22 @@ def warp_shapely_geom_from_to(geom, from_M=None, from_transformation_src_shape_r """ - warp_kwargs = {"from_M": from_M, - "from_transformation_src_shape_rc": from_transformation_src_shape_rc, - "from_transformation_dst_shape_rc": from_transformation_dst_shape_rc, - "from_src_shape_rc": from_src_shape_rc, - "from_dst_shape_rc":from_dst_shape_rc, - "from_bk_dxdy":from_bk_dxdy, - "from_fwd_dxdy":from_fwd_dxdy, - "to_M":to_M, - "to_transformation_src_shape_rc": to_transformation_src_shape_rc, - "to_transformation_dst_shape_rc": to_transformation_dst_shape_rc, - "to_src_shape_rc": to_src_shape_rc, - "to_dst_shape_rc": to_dst_shape_rc, "to_bk_dxdy": to_bk_dxdy, - "to_fwd_dxdy":to_fwd_dxdy} + warp_kwargs = { + "from_M": from_M, + "from_transformation_src_shape_rc": from_transformation_src_shape_rc, + "from_transformation_dst_shape_rc": from_transformation_dst_shape_rc, + "from_src_shape_rc": from_src_shape_rc, + "from_dst_shape_rc": from_dst_shape_rc, + "from_bk_dxdy": from_bk_dxdy, + "from_fwd_dxdy": from_fwd_dxdy, + "to_M": to_M, + "to_transformation_src_shape_rc": to_transformation_src_shape_rc, + "to_transformation_dst_shape_rc": to_transformation_dst_shape_rc, + "to_src_shape_rc": to_src_shape_rc, + "to_dst_shape_rc": to_dst_shape_rc, + "to_bk_dxdy": to_bk_dxdy, + "to_fwd_dxdy": to_fwd_dxdy, + } warped_geom = _warp_shapely(geom, warp_xy_from_to, warp_kwargs) @@ -2771,13 +3055,14 @@ def get_inside_mask_idx(xy, mask): (Q) array containing the indices of points inside the mask. """ - mask_cnt, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE) + mask_cnt, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - inside_mask = np.array([cv2.pointPolygonTest(mask_cnt[0], - tuple(xy[i]), - False) - for i in range(xy.shape[0])]) + inside_mask = np.array( + [ + cv2.pointPolygonTest(mask_cnt[0], tuple(xy[i]), False) + for i in range(xy.shape[0]) + ] + ) inside_mask_idx = np.where(inside_mask == 1.0)[0] @@ -2803,19 +3088,14 @@ def mask2xy(mask): min_y = 0 max_y = mask.shape[0] - 1 - bbox = np.array([ - [min_x, min_y], - [max_x, min_y], - [max_x, max_y], - [min_x, max_y] - ]) + bbox = np.array([[min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y]]) return bbox def bbox2mask(x, y, w, h, shape): mask = np.zeros(shape, dtype=np.uint8) - mask[y:y+h+1, x:x+w+1] = 255 + mask[y : y + h + 1, x : x + w + 1] = 255 return mask @@ -2828,7 +3108,7 @@ def xy2bbox(xy): w = abs(max_x - min_x) h = abs(max_y - min_y) - return(np.array([min_x, min_y, w, h])) + return np.array([min_x, min_y, w, h]) def bbox2xy(xywh): @@ -2880,7 +3160,9 @@ def get_xy_inside_mask(xy, mask): """ mask_cnt, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - mask_polys = [shapely.geometry.Polygon(np.squeeze(cnt)) for cnt in mask_cnt if len(cnt) > 2] + mask_polys = [ + shapely.geometry.Polygon(np.squeeze(cnt)) for cnt in mask_cnt if len(cnt) > 2 + ] in_mask = np.zeros(xy.shape[0]) for i, pt_xy in enumerate(xy): pt = shapely.geometry.Point(pt_xy) @@ -2939,8 +3221,8 @@ def measure_error(src_xy, dst_xy, shape, feature_similarity=None): Median Euclidean distance between src_xy and dst_xy, optinally weighted by feature similarity """ - d = np.sqrt((src_xy[:, 0]-dst_xy[:, 0])**2 + (src_xy[:, 1]-dst_xy[:, 1])**2) - rtre = d/np.sqrt(np.sum(np.power(shape, 2))) + d = np.sqrt((src_xy[:, 0] - dst_xy[:, 0]) ** 2 + (src_xy[:, 1] - dst_xy[:, 1]) ** 2) + rtre = d / np.sqrt(np.sum(np.power(shape, 2))) med_tre = np.median(rtre) if feature_similarity is not None: @@ -2948,7 +3230,6 @@ def measure_error(src_xy, dst_xy, shape, feature_similarity=None): else: med_d = np.median(d) - return med_tre, med_d @@ -2990,7 +3271,9 @@ def get_overlapping_poly(mesh_poly_coords): """ buffer_v = 0.01 - poly_l = [Polygon(verts).buffer(-buffer_v) for verts in np.round(mesh_poly_coords, 2)] + poly_l = [ + Polygon(verts).buffer(-buffer_v) for verts in np.round(mesh_poly_coords, 2) + ] s = STRtree(poly_l) n_poly = len(poly_l) overlapping_poly_list = [] @@ -3015,552 +3298,7 @@ def clip_poly(i): else: poly_diffs.append(diff.buffer(buffer_v)) - n_cpu = valtils.get_ncpus_available() - 1 + n_cpu = valtils.get_ncpus_available() res = pqdm(range(n_poly), clip_poly, n_jobs=n_cpu, unit="image", leave=None) return overlapping_poly_list, poly_diffs - - -# def untangle(dxdy, n_grid_pts=50, penalty=10e-6, mask=None): -# """Remove tangles caused by 2D displacement -# Based on method described in -# "Foldover-free maps in 50 lines of code" Garanzha et al. 2021. - -# Parameters -# ---------- -# dxdy : ndarray -# 2xMxN array of displacement fields - -# n_grid_pts : int, optional -# Number of grid points to sample, in each dimension - -# penalty : float -# How much to penalize tangles - -# mask : ndarray -# Mask indicating which areas should be untangled - -# Returns -# ------- -# untangled_dxdy : ndarray -# Copy of `dxdy`, but with displacements adjusted so that they -# won't introduce tangles. - -# """ - -# qut = QuadUntangler(dxdy, n_grid_pts=n_grid_pts, fold_penalty=penalty) -# mesh = qut.mesh -# if mask is not None: -# frozen_mask = mask.copy() -# if np.any(mask.shape != mesh.padded_shape): -# padding_T = get_padding_matrix(mask.shape, mesh.padded_shape) -# frozen_mask = transform.warp(frozen_mask, padding_T, output_shape=mesh.padded_shape, preserve_range=True) -# # Freeze regions that aren't folded -# frozen_mask[0:frozen_mask.shape[0]-1, 0:mesh.c_offset] = 0 # left -# frozen_mask[0:frozen_mask.shape[0]-1, frozen_mask.shape[1]-mesh.c_offset : frozen_mask.shape[1]-1] = 0 # right -# frozen_mask[0:mesh.r_offset, 0:frozen_mask.shape[1]-1] = 0 # top -# frozen_mask[frozen_mask.shape[0]-mesh.r_offset : frozen_mask.shape[0] - 1, 0:frozen_mask.shape[1]-1] = 0 # bottom -# frozen_point = frozen_mask[mesh.sample_pos_xy[:, 1].astype(int), -# mesh.sample_pos_xy[:, 0].astype(int)].reshape(-1) == 0 - -# qut.mesh.boundary = frozen_point - - -# untangled_mesh = qut.untangle() -# qut.mesh.x = untangled_mesh -# untangled_coords = np.dstack([untangled_mesh[:mesh.nverts], untangled_mesh[mesh.nverts:]])[0] -# untangled_coords *= mesh.scaling -# untangled_dx = (mesh.sample_pos_xy[:, 0] - untangled_coords[:, 0]).reshape((mesh.nr, mesh.nc)) -# untangled_dy = (mesh.sample_pos_xy[:, 1] - untangled_coords[:, 1]).reshape((mesh.nr, mesh.nc)) - -# padded_shape = mesh.padded_shape -# grid = UCGrid((0.0, float(padded_shape[1]), int(mesh.nc)), -# (0.0, float(padded_shape[0]), int(mesh.nr))) - -# dx_cubic_coeffs = filter_cubic(grid, untangled_dx).T -# dy_cubic_coeffs = filter_cubic(grid, untangled_dy).T - -# img_y, img_x = np.indices(padded_shape) -# img_xy = np.dstack([img_x.reshape(-1), img_y.reshape(-1)]).astype(float)[0] -# untangled_dx = eval_cubic(grid, dx_cubic_coeffs, img_xy).reshape(padded_shape) -# untangled_dy = eval_cubic(grid, dy_cubic_coeffs, img_xy).reshape(padded_shape) - -# inv_T = np.linalg.inv(mesh.padding_T) -# untangled_dx = transform.warp(untangled_dx, inv_T, output_shape=mesh.shape_rc, preserve_range=True) -# untangled_dy = transform.warp(untangled_dy, inv_T, output_shape=mesh.shape_rc, preserve_range=True) -# untangled_dxdy = np.array([untangled_dx, untangled_dy]) - -# return untangled_dxdy - - -# def remove_folds_in_dxdy(dxdy, n_grid_pts=50, method="inpaint", paint_size=5000, fold_penalty=1e-6): -# """Remove folds in displacement fields - -# Find and remove folds in displacement fields - -# Parameters -# --------- -# method : str, optional -# "inpaint" will use inpainting to fill in areas idenetified -# as containing folds. "regularize" will unfold those regions -# using the mehod described in "Foldover-free maps in 50 lines of code" -# Garanzha et al. 2021. - -# n_grid_pts : int -# Number of gridpoints used to detect folds. Also the number -# of gridpoints to use when regularizing he mesh when -# `method` = "regularize". - -# paint_size : int -# Used to determine how much to resize the image to have efficient inpainting. -# Larger values = longer processing time. Only used if `method` = "inpaint". - -# fold_penalty : float -# How much to penalize folding/stretching. Larger values will make -# the deformation field more uniform. Only used if `method` = "regularize" - -# Returns -# ------- -# no_folds_dxdy : ndarray -# An array containing the x-axis (column) displacement, and y-axis (row) -# displacement after removing folds. - -# """ - -# # Use triangular mesh to find regions with folds -# # TriangleMesh will warp triangle points using dxdy to determine location vertices in warped image -# # It is assumed dxdy is a backwards transform found by registering images. -# # Because TriMesh is warping points, the inverse of dxdy is used. -# # Any image create from these points can be warped to their original position using dxdy - -# valtils.print_warning("Looking for folds", None, rgb=Fore.YELLOW) -# tri_mesh = TriangleMesh(dxdy, n_grid_pts) -# padded_shape = tri_mesh.padded_shape - -# tri_verts_xy = np.dstack([tri_mesh.x[:tri_mesh.nverts], tri_mesh.x[tri_mesh.nverts:]])[0]*tri_mesh.scaling -# tri_xy = np.array([tri_verts_xy[t, :] for t in tri_mesh.tri]) - -# overlapping_poly_list, poly_diff_list = get_overlapping_poly(tri_xy) -# poly_overlap_mask = np.zeros(padded_shape, dtype=np.uint8) -# for poly in overlapping_poly_list: -# poly_r, poly_c = draw.polygon(*poly.exterior.xy[::-1], shape=padded_shape) -# poly_overlap_mask[poly_r, poly_c] = 255 - -# # Warp mask back to original image. Should isolaate regions that will cause folding -# warp_map = get_warp_map(dxdy=tri_mesh.padded_dxdy) -# src_folds_mask = transform.warp(poly_overlap_mask, warp_map, preserve_range=True) -# src_folds_mask[src_folds_mask != 0] = 255 -# src_folds_mask = ndimage.binary_fill_holes(src_folds_mask).astype(np.uint8)*255 - -# folded_area = len(np.where(src_folds_mask > 0)[0]) -# if folded_area == 0: -# return dxdy - -# if method == 'regularize': -# valtils.print_warning("Removing folds using regularizaation", None, rgb=Fore.YELLOW) -# # Untanlge folded regions using regularization -# qut = QuadUntangler(dxdy, n_grid_pts=n_grid_pts, fold_penalty=fold_penalty) -# mesh = qut.mesh -# frozen_mask = src_folds_mask.copy() -# # Freeze regions that aren't folded -# frozen_mask[0:frozen_mask.shape[0]-1, 0:mesh.c_offset] = 0 # left -# frozen_mask[0:frozen_mask.shape[0]-1, frozen_mask.shape[1]-mesh.c_offset : frozen_mask.shape[1]-1] = 0 # right -# frozen_mask[0:mesh.r_offset, 0:frozen_mask.shape[1]-1] = 0 # top -# frozen_mask[frozen_mask.shape[0]-mesh.r_offset : frozen_mask.shape[0] - 1, 0:frozen_mask.shape[1]-1] = 0 # bottom -# frozen_point = frozen_mask[mesh.sample_pos_xy[:, 1].astype(int), -# mesh.sample_pos_xy[:, 0].astype(int)].reshape(-1) == 0 - -# qut.mesh.boundary = frozen_point - -# # Untangle and interpolate -# untangled_mesh = qut.untangle() -# qut.mesh.x = untangled_mesh -# untangled_coords = np.dstack([untangled_mesh[:mesh.nverts], untangled_mesh[mesh.nverts:]])[0] -# untangled_coords *= mesh.scaling -# untangled_dx = (mesh.sample_pos_xy[:, 0] - untangled_coords[:, 0]).reshape((mesh.nr, mesh.nc)) -# untangled_dy = (mesh.sample_pos_xy[:, 1] - untangled_coords[:, 1]).reshape((mesh.nr, mesh.nc)) - -# grid = UCGrid((0.0, float(padded_shape[1]), int(mesh.nc)), -# (0.0, float(padded_shape[0]), int(mesh.nr))) - -# dx_cubic_coeffs = filter_cubic(grid, untangled_dx).T -# dy_cubic_coeffs = filter_cubic(grid, untangled_dy).T - -# img_y, img_x = np.indices(padded_shape) -# img_xy = np.dstack([img_x.reshape(-1), img_y.reshape(-1)]).astype(float)[0] -# no_folds_dx = eval_cubic(grid, dx_cubic_coeffs, img_xy).reshape(padded_shape) -# no_folds_dy = eval_cubic(grid, dy_cubic_coeffs, img_xy).reshape(padded_shape) - -# else: - -# s = np.sqrt(paint_size)/np.sqrt(folded_area) -# if s > 1: -# s = 1 - -# inpaint_mask = transform.rescale(src_folds_mask, s, preserve_range=True) - -# to_paint_dx = transform.rescale(tri_mesh.padded_dxdy[0], s, preserve_range=True) -# painted_dx = restoration.inpaint_biharmonic(to_paint_dx, inpaint_mask) -# smooth_dx = transform.resize(painted_dx, tri_mesh.padded_shape, preserve_range=True) - -# to_paint_dy = transform.rescale(tri_mesh.padded_dxdy[1], s, preserve_range=True) -# painted_dy = restoration.inpaint_biharmonic(to_paint_dy, inpaint_mask) -# smooth_dy = transform.resize(painted_dy, tri_mesh.padded_shape, preserve_range=True) - -# blending_mask = filters.gaussian(src_folds_mask, 1) -# no_folds_dx = blending_mask*smooth_dx + (1-blending_mask)*tri_mesh.padded_dxdy[0] -# no_folds_dy = blending_mask*smooth_dy + (1-blending_mask)*tri_mesh.padded_dxdy[1] - -# # Crop to original shape # -# no_folds_dx = transform.warp(no_folds_dx, inv_T, output_shape=tri_mesh.shape_rc, preserve_range=True) -# no_folds_dy = transform.warp(no_folds_dy, inv_T, output_shape=tri_mesh.shape_rc, preserve_range=True) -# no_folds_dxdy = np.array([no_folds_dx, no_folds_dy]) - -# return no_folds_dxdy - - -# class QuadMesh(object): - -# def __init__(self, dxdy, n_grid_pts=50): -# shape = np.array(dxdy[0].shape) -# self.shape_rc = shape -# grid_spacing = int(np.min(np.round(shape/n_grid_pts))) - -# new_r = shape[0] - shape[0] % grid_spacing + grid_spacing -# self.r_padding = new_r - shape[0] -# sample_y = np.floor(np.arange(0, new_r + grid_spacing, grid_spacing)) - -# new_c = shape[1] - shape[1] % grid_spacing + grid_spacing -# sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) -# self.c_padding = new_c - shape[1] - -# nr = len(sample_y) -# nc = len(sample_x) -# padded_shape = np.array([new_r+1, new_c+1]) -# self.padded_shape = padded_shape -# y_center, x_center = padded_shape/2 -# self.nverts = nr*nc -# self.nr = nr -# self.nc = nc - -# self.r_offset, self.c_offset = (padded_shape - shape)//2 - -# # Pad displacement # -# self.padding_T = get_padding_matrix(shape, padded_shape) - -# padded_dx = transform.warp(dxdy[0], self.padding_T, output_shape=padded_shape, preserve_range=True) -# padded_dy = transform.warp(dxdy[1], self.padding_T, output_shape=padded_shape, preserve_range=True) - -# self.padded_dxdy = np.array([padded_dx, padded_dy]) -# # Flattend indices for each pixel in a quadrat -# quads = [[r*nc + c, r*nc + c + 1, (r+1)*nc + c + 1, (r+1)*nc + c] for r in range(nr-1) for c in range(nc-1)] -# self.quads = quads -# self.boundary = [None] * self.nverts - -# for i in range(self.nverts): -# r_idx = i // nc -# c_idx = i % nc -# r = sample_y[r_idx] -# c = sample_x[c_idx] -# if r <= y_center or r >= new_r - y_center or c <= x_center or c >= new_c - x_center: -# self.boundary[i] = True - -# else: -# self.boundary[i] = False - -# sample_pos_y, sample_pos_x = np.meshgrid(sample_y, sample_x, indexing="ij") -# unwarped_xy = np.dstack([sample_pos_x.reshape(-1), sample_pos_y.reshape(-1)])[0].astype(float) -# self.sample_pos_xy = unwarped_xy -# sample_xy = warp_xy(unwarped_xy, M=None, bk_dxdy=[padded_dx, padded_dy]) -# self.warped_xy = sample_xy -# scaled_coords = self.scale_coords(sample_xy) -# self.x = np.hstack([scaled_coords[:, 0], scaled_coords[:, 1]]) - -# def scale_coords(self, xy): -# max_side = np.max(self.padded_shape) -# scaled_coords = xy/max_side -# self.scaling = max_side - -# return scaled_coords - - -# def __str__(self): -# ret = "" -# for v in range(self.nverts): -# ret = ret + ("v %f %f 0\n" % (self.x[v], self.x[v+self.nverts])) -# for f in self.quads: -# ret = ret + ("f %d %d %d %d\n" % (f[0]+1, f[1]+1, f[2]+1, f[3]+1)) -# return ret - -# def show(self): -# res = 1000 -# off = 100 -# image = Image.new(mode='L', size=(res, res), color=255) -# draw = ImageDraw.Draw(image) - -# for quad in self.quads: -# for e in range(4): -# i = quad[e] -# j = quad[(e+1)%4] - -# line = ((off+self.x[i]*res/2, off+self.x[i+self.nverts]*res/2), (off+self.x[j]*res/2, off+self.x[j+self.nverts]*res/2)) -# draw.line(line, fill=128) -# del draw -# image.show() - - -# class TriangleMesh(object): -# def __init__(self, dxdy, n_grid_pts=50): -# shape = np.array(dxdy[0].shape) -# self.shape_rc = shape -# grid_spacing = int(np.min(np.round(shape/n_grid_pts))) - -# new_r = shape[0] - shape[0] % grid_spacing + grid_spacing -# self.r_padding = new_r - shape[0] -# sample_y = np.floor(np.arange(0, new_r + grid_spacing, grid_spacing)) - -# new_c = shape[1] - shape[1] % grid_spacing + grid_spacing -# sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) -# self.c_padding = new_c - shape[1] - -# nr = len(sample_y) -# nc = len(sample_x) -# padded_shape = np.array([new_r+1, new_c+1]) -# self.padded_shape = padded_shape -# self.r_offset, self.c_offset = (padded_shape - shape)//2 - -# self.nverts = nr*nc -# self.nr = nr -# self.nc = nc -# y_center, x_center = padded_shape/2 - -# self.padding_T = get_padding_matrix(shape, padded_shape) - -# padded_dx = transform.warp(dxdy[0], self.padding_T, output_shape=padded_shape, preserve_range=True) -# padded_dy = transform.warp(dxdy[1], self.padding_T, output_shape=padded_shape, preserve_range=True) - -# self.padded_dxdy = np.array([padded_dx, padded_dy]) - -# # Get triangle vertices -# sample_x = np.arange(0, new_c + grid_spacing, grid_spacing) -# sample_y = np.arange(0, new_r + grid_spacing, grid_spacing) - -# tri_verts, tri_faces = get_triangular_mesh(sample_x, sample_y) -# self.nverts = tri_verts.shape[0] -# self.tri_verts = tri_verts -# self.boundary = [None] * self.nverts -# for i in range(self.nverts): -# c, r = tri_verts[i] - -# if r <= y_center or r >= new_r - y_center or c <= x_center or c >= new_c - x_center: -# self.boundary[i] = True -# else: -# self.boundary[i] = False - -# sample_xy = warp_xy(tri_verts, M=None, bk_dxdy=[padded_dx, padded_dy]) -# self.warped_xy = sample_xy - -# self.tri = tri_faces -# self.nfacets = len(self.tri) -# self.vert = self.scale_coords(sample_xy) -# self.x = np.hstack([self.vert[:, 0], self.vert[:, 1]]) - -# def scale_coords(self, xy): - -# max_side = np.max(self.padded_shape) -# scaled_coords = xy/max_side -# self.scaling = max_side - -# return scaled_coords - - -# class QuadUntangler(object): -# def __init__(self, dxdy, fold_penalty=1e-6, n_grid_pts=50): -# self.shape = np.array(dxdy[0].shape) -# self.mesh = QuadMesh(dxdy, n_grid_pts) -# self.mesh_type = self.mesh.__class__.__name__ -# self.n_grid_pts = n_grid_pts -# self.n = self.mesh.nverts -# self.fold_penalty = fold_penalty - -# def untangle(self): -# n = self.n -# mesh = self.mesh -# Q = [np.matrix('-1,-1;1,0;0,0;0,1'), np.matrix('-1,0;1,-1;0,1;0,0'), # quadratures for -# np.matrix('0,0;0,-1;1,1;-1,0'), np.matrix('0,-1;0,0;1,0;-1,1') ] # every quad corner - -# def jacobian(U, qc, quad): -# return np.matrix([[U[quad[0] ], U[quad[1] ], U[quad[2] ], U[quad[3] ]], -# [U[quad[0]+n], U[quad[1]+n], U[quad[2]+n], U[quad[3]+n]]]) * Q[qc] - -# mindet = min([np.linalg.det( jacobian(mesh.x, qc, quad) ) for quad in mesh.quads for qc in range(4)]) -# eps = np.sqrt(1e-6**2 + min(mindet, 0)**2) # the regularization parameter e -# eps *= 1/self.fold_penalty - -# def energy(U): # compute the energy and its gradient for the map u -# F,G = 0, np.zeros(2*n) -# for quad in mesh.quads: # sum over all quads -# for qc in range(4): # evaluate the Jacobian matrix for every quad corner -# J = jacobian(U, qc, quad) -# det = np.linalg.det(J) -# chi = det/2 + np.sqrt(eps**2 + det**2)/2 # the penalty function -# chip = .5 + det/(2*np.sqrt(eps**2 + det**2)) # its derivative - -# f = np.trace(np.transpose(J)*J)/chi # quad corner shape quality -# F += f -# dfdj = (2*J - np.matrix([[J[1,1],-J[1,0]],[-J[0,1],J[0,0]]])*f*chip)/chi -# dfdu = Q[qc] * np.transpose(dfdj) # chain rule for the actual variables -# for i,v in enumerate(quad): -# if (mesh.boundary[v]): continue # the boundary verts are locked -# G[v ] += dfdu[i,0] -# G[v+n] += dfdu[i,1] -# return F,G - -# # factr are: 1e12 for low accuracy; 1e7 for moderate accuracy; 10.0 for extremely high accuracy. -# factr = 1e7 -# untangled = fmin_l_bfgs_b(energy, mesh.x, factr=factr)[0] # inner L-BFGS loop - -# return untangled - - -# class _TriUntangler(object): -# def __init__(self, dxdy, n_grid_pts=50): -# self.shape = np.array(dxdy[0].shape) - -# # self.mesh = QuadMesh(dxdy, n_grid_pts) -# self.mesh = TriangleMesh(dxdy, n_grid_pts) -# self.mesh_type = self.mesh.__class__.__name__ -# self.n_grid_pts = n_grid_pts -# self.n = self.mesh.nverts -# self.n_tri = len(self.mesh.tri) - -# def triangle_area2d(self, a, b, c): -# x = 0 -# y = 1 -# tri_area = .5*((b[y]-a[y])*(b[x]+a[x]) + (c[y]-b[y])*(c[x]+b[x]) + (a[y]-c[y])*(a[x]+c[x])) -# return tri_area - -# def triangle_aspect_ratio_2d(self, a, b, c): - -# l1 = np.linalg.norm(b-a) -# l2 = np.linalg.norm(c-b) -# l3 = np.linalg.norm(a-c) -# lmax = max([l1, l2, l3]) - -# return lmax*(l1+l2+l3)/(4.*np.sqrt(3.)*self.triangle_area2d(a, b, c)) - - -# def setup(self): -# area = [None] * self.n_tri -# ref_tri = [None] * self.n_tri -# for t, faces in enumerate(self.mesh.tri): - - -# ax, bx, cx = self.mesh.x[self.mesh.tri[t]] -# ay, by, cy = self.mesh.x[self.mesh.tri[t]+ self.mesh.nverts] - -# A = np.array([ax, ay]) -# B = np.array([bx, by]) -# C = np.array([cx, cy]) - - -# area[t] = self.triangle_area2d(A, B, C) - -# ar = self.triangle_aspect_ratio_2d(A, B, C) -# if ar > 10: -# #if the aspect ratio is bad, assign an equilateral reference triangle -# l1 = np.linalg.norm(B-A) -# l2 = np.linalg.norm(C-B) -# l3 = np.linalg.norm(A-C) -# a = (l1 + l2 + l3)/3 # edge length is the average of the original triangle -# area[t] = np.sqrt(3.)/4.*a*a -# A = np.array([0., 0.]) -# B = np.array([a, 0.]) -# C = np.array([a/2., np.sqrt(3.)/2.*a]) - -# ST = np.matrix([B-A, C-A]) -# ST_invert_transpose = np.linalg.inv(ST).T -# ref_tri[t] = np.array([[-1, -1], [1, 0], [0, 1] ]) @ ST_invert_transpose - -# self.area = area -# self.ref_tri = ref_tri - - -# def untangle(self): -# self.setup() -# def evaluate_jacobian(X, t): -# J = np.matrix(np.zeros((2, 2))) -# for i in range(3): -# for d in range(2): -# J[d] += self.ref_tri[t][i, d] + X[self.mesh.tri[t][i] + self.n*d] - -# K = np.array([[J[1, 1], -J[1, 0]], -# [-J[0, 1], J[0, 0]] -# ]) - -# det = np.linalg.det(J) - -# return J, K, det - -# mindet = np.inf -# for t in range(self.n_tri): -# _, _, det = evaluate_jacobian(self.mesh.x, t) -# mindet = np.min([mindet, det]) - -# eps = np.sqrt(1e-6**2 + min(mindet, 0)**2) # the regularization parameter e -# theta = 1./128 - -# def chi(eps, det): -# if det < 0: -# return (det + np.sqrt(eps*eps + det*det) + 10**-6)*.5 -# else: -# return .5*eps*eps / (np.sqrt(eps*eps + det*det) - det + 10**-6) - -# def chi_deriv(eps, det): -# return .5+det/(2.*np.sqrt(eps*eps + det*det + 10**-6)) - - -# def energy(U): - - -# F,G = 0, np.zeros(2*self.n) - -# for t in range(self.n_tri): -# J, K, det = evaluate_jacobian(U, t) - -# c1 = chi(eps, det) -# c2 = chi_deriv(eps, det) - -# f = np.trace(np.transpose(J)*J)/c1 # corner shape quality -# g = (1+det*det)/c1 - -# F += ((1-theta)*f + theta*g)*self.area[t] - -# for dim in range(2): - -# a = J[dim] # tangent basis -# b = K[dim] # dual basis -# dfda = (a*2. - b*f*c2)/c1 -# dgda = b*(2*det-g*c2)/c1 -# for i in range(3): -# v = self.mesh.tri[t][i] -# if self.mesh.boundary[v]: continue # the boundary verts are locked -# # og_pos = G[v+ dim*self.n] -# G[v+ dim*self.n] += (self.ref_tri[t][i] @ np.transpose(dfda*(1.-theta) + dgda*theta))*self.area[t] -# # new_pos = G[v+ dim*self.n] -# # print(new_pos - og_pos) -# return F, G - -# n_iter = 3 - -# for i in range(n_iter): - -# self.mesh.x = fmin_l_bfgs_b(energy, self.mesh.x, factr=1e12)[0] # inner L-BFGS loop -# # updated_xy = self.mesh.x.reshape((self.n, 2)) -# updated_xy = np.dstack([self.mesh.x[self.n:], self.mesh.x[:self.n]])[0] -# # plt.triplot(updated_xy[:, 0], -updated_xy[:, 1], self.mesh.tri, linewidth=0.5) -# plt.triplot(updated_xy[:, 1], -updated_xy[:, 0], self.mesh.tri, linewidth=0.5) -# plt.axis("equal") -# plt.savefig(f"{i}_smooth_mesh.png") -# plt.close() - diff --git a/tests/conftest.py b/tests/conftest.py index 244d100e..326a21c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,7 @@ import sys import platform + print(platform.python_version()) from valis import slide_io -def pytest_sessionstart(session): - """ - Called after the Session object has been created and - before performing collection and entering the run test loop. - """ - slide_io.init_jvm() - - -def pytest_sessionfinish(session, exitstatus): - """ - Called after whole test run finished, right before - returning the exit status to the system. - """ - slide_io.kill_jvm() diff --git a/tests/test_align_two.py b/tests/test_align_two.py new file mode 100644 index 00000000..d474cd5f --- /dev/null +++ b/tests/test_align_two.py @@ -0,0 +1,70 @@ +"""Smoketest: align two images using the same code path as examples/align_two_images.py. + +Downloads the two smallest example images from the upstream repo on first run +and caches them in tests/example_datasets/ so subsequent runs are offline. + +Visual artifacts (overlap thumbnails, deformation fields, etc.) are written to +tests/test_output/ so you can inspect alignment quality after the run. +""" + +import os +import urllib.request +import pytest + +TESTS_DIR = os.path.dirname(__file__) +DATASETS_DIR = os.path.join(TESTS_DIR, "example_datasets", "cycif") +OUTPUT_DIR = os.path.join(TESTS_DIR, "test_output") + +# Two smallest cycif files from upstream +IMAGES = { + "CD4 CD68 CD3.ome.tiff": "https://raw.githubusercontent.com/MathOnco/valis/main/examples/example_datasets/cycif/CD4%20CD68%20CD3.ome.tiff", + "CD20 FOXP3 CD3.ome.tiff": "https://raw.githubusercontent.com/MathOnco/valis/main/examples/example_datasets/cycif/CD20%20FOXP3%20CD3.ome.tiff", +} + + +def _ensure_datasets(): + os.makedirs(DATASETS_DIR, exist_ok=True) + for name, url in IMAGES.items(): + path = os.path.join(DATASETS_DIR, name) + if not os.path.exists(path): + print(f"Downloading {name} ...") + urllib.request.urlretrieve(url, path) + return [os.path.join(DATASETS_DIR, name) for name in IMAGES] + + +def test_align_two_images(): + """Register two images and verify alignment completes with low error.""" + from valis import registration + + img_list = _ensure_datasets() + reference = img_list[ + 1 + ] # CD20 FOXP3 CD3 as reference (matches example script pattern) + + registrar = registration.Valis( + src_dir=DATASETS_DIR, + dst_dir=OUTPUT_DIR, + name="smoketest", + img_list=img_list, + reference_img_f=reference, + align_to_reference=True, + check_for_reflections=False, + ) + rigid_registrar, non_rigid_registrar, error_df = registrar.register() + + assert error_df is not None, "register() returned no error_df" + assert len(error_df) > 0, "error_df is empty" + + max_error = error_df["mean_non_rigid_D"].max() + assert ( + max_error < 50 + ), f"Alignment error too high: {max_error:.1f}px (threshold: 50px)" + + # Verify the reference slide has no warping applied + ref_slide = registrar.get_ref_slide() + import numpy as np + + dxdy = np.dstack(ref_slide.bk_dxdy) + assert ( + dxdy.min() == 0 and dxdy.max() == 0 + ), "Reference slide should have zero displacement field" diff --git a/tests/test_examples.py b/tests/test_examples.py index 227b054f..565991a9 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,4 +1,4 @@ -""" Registration of whole slide images (WSI) +"""Registration of whole slide images (WSI) This example shows how to register, warp, and save a collection of whole slide images (WSI) using the default parameters. @@ -45,8 +45,6 @@ are, then one can warp and save all of the slides. """ - - import torch import kornia @@ -59,44 +57,55 @@ from skimage import transform import shutil import sys -import os import pandas as pd from valis import registration, valtils, slide_io, micro_rigid_registrar from valis.feature_matcher import * + def check_for_no_transforms_in_ref(ref_slide, reference_img_fname): - assert ref_slide.name == valtils.get_name(reference_img_fname), "Reference image is not the same as specified image" + assert ref_slide.name == valtils.get_name( + reference_img_fname + ), "Reference image is not the same as specified image" og_crop = ref_slide.crop ref_slide.crop = registration.CROP_REF # M may have translations for cropping to overlap, but they are ignored when cropping to reference (verified below) try: tformer = transform.AffineTransform(ref_slide.M) - assert np.all(tformer.scale == [1.0, 1.0]), "Reference image has unexpected scaling" + assert np.all( + tformer.scale == [1.0, 1.0] + ), "Reference image has unexpected scaling" assert tformer.rotation == 0, "Reference image has unexpected rotation" assert tformer.shear == 0, "Reference image has unexpected shearing" # Check that translations only crop padded image to the reference image's origin out_shape_rc = ref_slide.slide_dimensions_wh[0][::-1] - sxy = (out_shape_rc/ref_slide.processed_img_shape_rc)[::-1] - scaled_txy = sxy*ref_slide.M[:2, 2] + sxy = (out_shape_rc / ref_slide.processed_img_shape_rc)[::-1] + scaled_txy = sxy * ref_slide.M[:2, 2] - crop_bbox, _ = ref_slide.get_crop_xywh(ref_slide.crop, out_shape_rc=out_shape_rc) + crop_bbox, _ = ref_slide.get_crop_xywh( + ref_slide.crop, out_shape_rc=out_shape_rc + ) cropping_to_origin = np.all(np.abs(crop_bbox[0:2] + scaled_txy) < 1) assert cropping_to_origin, "translations don't move reference to the origin" # Check for non-rigid transforms - assert np.dstack(ref_slide.bk_dxdy).min() == 0 and np.dstack(ref_slide.bk_dxdy).max() == 0, "Found unexpected non-rigid transforms" + assert ( + np.dstack(ref_slide.bk_dxdy).min() == 0 + and np.dstack(ref_slide.bk_dxdy).max() == 0 + ), "Found unexpected non-rigid transforms" # Check that points are warped correctly too xy = np.array([[0, 0]]) warped_origin = np.round(ref_slide.warp_xy(xy)) - assert np.all(warped_origin == 0), "reference image not warping points correctly" + assert np.all( + warped_origin == 0 + ), "reference image not warping points correctly" # Compare raw values unwarped_ref_img = ref_slide.slide2vips(level=0) warped_ref_img = ref_slide.warp_slide(level=0) - eq_img = (unwarped_ref_img == warped_ref_img) + eq_img = unwarped_ref_img == warped_ref_img min_eq = eq_img.min() assert min_eq == 255, "warped and original images do not have the same values" @@ -106,9 +115,10 @@ def check_for_no_transforms_in_ref(ref_slide, reference_img_fname): ref_slide.crop = og_crop - def get_dirs(): - dst_parent_folder = f"{sys.platform}_{sys.version_info.major}{sys.version_info.minor}" + dst_parent_folder = ( + f"{sys.platform}_{sys.version_info.major}{sys.version_info.minor}" + ) try: current_file_path = os.path.abspath(__file__) current_directory = os.path.dirname(current_file_path) @@ -119,9 +129,13 @@ def get_dirs(): except: cwd = os.getcwd() dir_split = cwd.split(os.sep) - split_idx = [i for i in range(len(dir_split)) if dir_split[i] == "valis_project"][0] - parent_dir = os.path.join(os.sep.join(dir_split[:split_idx+1]), "valis") - results_dst_dir = os.path.join(parent_dir, f"tests/{sys.version_info.major}{sys.version_info.minor}") + split_idx = [ + i for i in range(len(dir_split)) if dir_split[i] == "valis_project" + ][0] + parent_dir = os.path.join(os.sep.join(dir_split[: split_idx + 1]), "valis") + results_dst_dir = os.path.join( + parent_dir, f"tests/{sys.version_info.major}{sys.version_info.minor}" + ) return parent_dir, results_dst_dir @@ -143,22 +157,35 @@ def cnames_from_filename(src_f): def register_hi_rez(src_dir): high_rez_dst_dir = os.path.join(results_dst_dir, "high_rez") - micro_reg_fraction = 0.25 # Fraction full resolution used for non-rigid registration + micro_reg_fraction = ( + 0.25 # Fraction full resolution used for non-rigid registration + ) # Perform high resolution rigid registration using the MicroRigidRegistrar start = time.time() - registrar = registration.Valis(src_dir, high_rez_dst_dir, micro_rigid_registrar_cls=micro_rigid_registrar.MicroRigidRegistrar) + registrar = registration.Valis( + src_dir, + high_rez_dst_dir, + micro_rigid_registrar_cls=micro_rigid_registrar.MicroRigidRegistrar, + ) rigid_registrar, non_rigid_registrar, error_df = registrar.register() # Calculate what `max_non_rigid_registration_dim_px` needs to be to do non-rigid registration on an image that is 25% full resolution. - img_dims = np.array([slide_obj.slide_dimensions_wh[0] for slide_obj in registrar.slide_dict.values()]) + img_dims = np.array( + [ + slide_obj.slide_dimensions_wh[0] + for slide_obj in registrar.slide_dict.values() + ] + ) min_max_size = np.min([np.max(d) for d in img_dims]) img_areas = [np.multiply(*d) for d in img_dims] max_img_w, max_img_h = tuple(img_dims[np.argmax(img_areas)]) - micro_reg_size = np.floor(min_max_size*micro_reg_fraction).astype(int) + micro_reg_size = np.floor(min_max_size * micro_reg_fraction).astype(int) # Perform high resolution non-rigid registration - micro_reg, micro_error = registrar.register_micro(max_non_rigid_registration_dim_px=micro_reg_size) + micro_reg, micro_error = registrar.register_micro( + max_non_rigid_registration_dim_px=micro_reg_size + ) ref_slide = registrar.get_ref_slide() ref_slide_src_f = ref_slide.src_f @@ -174,9 +201,7 @@ def register_hi_rez(src_dir): def test_register_ihc(max_error=70): - """Tests registration and lossy jpeg compression - - """ + """Tests registration and lossy jpeg compression""" ihc_src_dir = os.path.join(datasets_src_dir, "ihc") # ihc_src_dir = "/Users/gatenbcd/Dropbox/Documents/Andriy/Bina_alignments/slides/NSG_from_Marusyk/NSG 48 hours_374" try: @@ -191,8 +216,12 @@ def test_register_ihc(max_error=70): # shutil.rmtree(ihc_dst_dir, ignore_errors=True) assert False, f"error was {avg_error} but should be below {max_error}" - registered_slide_dst_dir = os.path.join(registrar.dst_dir, "registered_slides", registrar.name) - registrar.warp_and_save_slides(dst_dir=registered_slide_dst_dir, Q=90, compression="jpeg") + registered_slide_dst_dir = os.path.join( + registrar.dst_dir, "registered_slides", registrar.name + ) + registrar.warp_and_save_slides( + dst_dir=registered_slide_dst_dir, Q=90, compression="jpeg" + ) ref_slide = registrar.get_ref_slide() ref_slide_src_f = ref_slide.src_f @@ -223,7 +252,13 @@ def test_register_cycif(max_error=3): img_list = np.roll(img_list, 1) ref_img_f = str(img_list[0]) - registrar = registration.Valis(cycif_src_dir, results_dst_dir, img_list=img_list, imgs_ordered=True, reference_img_f=ref_img_f) + registrar = registration.Valis( + cycif_src_dir, + results_dst_dir, + img_list=img_list, + imgs_ordered=True, + reference_img_f=ref_img_f, + ) rigid_registrar, non_rigid_registrar, error_df = registrar.register() avg_error = np.max(error_df["mean_non_rigid_D"]) @@ -232,11 +267,16 @@ def test_register_cycif(max_error=3): channel_name_dict = {str(f): cnames_from_filename(f) for f in img_list} - dst_f = os.path.join(registrar.dst_dir, "registered_slides", f"{registrar.name}.ome.tiff") - merged_img, channel_names, ome_xml = registrar.warp_and_merge_slides(dst_f, - channel_name_dict=channel_name_dict, - drop_duplicates=drop_duplicates, - Q=90, compression="jp2k") + dst_f = os.path.join( + registrar.dst_dir, "registered_slides", f"{registrar.name}.ome.tiff" + ) + merged_img, channel_names, ome_xml = registrar.warp_and_merge_slides( + dst_f, + channel_name_dict=channel_name_dict, + drop_duplicates=drop_duplicates, + Q=90, + compression="jp2k", + ) # Check that specified reference image is not warped ref_slide = registrar.get_ref_slide() @@ -245,21 +285,31 @@ def test_register_cycif(max_error=3): # Check merged image has channel names in expected order - assert [slide_obj.stack_idx for slide_obj in registrar.slide_dict.values()] == list(range(registrar.size)), "Slides got sorted when `imgs_ordered=True`" + assert [ + slide_obj.stack_idx for slide_obj in registrar.slide_dict.values() + ] == list(range(registrar.size)), "Slides got sorted when `imgs_ordered=True`" - sorted_img_list = registrar.get_sorted_img_f_list() # Get images in the same order used for registration (may be different than order in directory) - expected_channel_order = list(chain.from_iterable([channel_name_dict[str(f)] for f in sorted_img_list])) + sorted_img_list = ( + registrar.get_sorted_img_f_list() + ) # Get images in the same order used for registration (may be different than order in directory) + expected_channel_order = list( + chain.from_iterable([channel_name_dict[str(f)] for f in sorted_img_list]) + ) if drop_duplicates: - cnames_df = pd.DataFrame(expected_channel_order, columns=['cname']) - expected_channel_order = list(cnames_df.drop_duplicates(keep="first")['cname']) + cnames_df = pd.DataFrame(expected_channel_order, columns=["cname"]) + expected_channel_order = list( + cnames_df.drop_duplicates(keep="first")["cname"] + ) saved_ome_xml = ome_types.from_xml(ome_xml) saved_channel_names = [x.name for x in saved_ome_xml.images[0].pixels.channels] - assert np.all(expected_channel_order == saved_channel_names), (f"Channels not saved in correct order.\n" - f"Expected: {expected_channel_order}.\n" - f"Got: {saved_channel_names}" - f"Img list: {[os.path.split(x)[1] for x in img_list]}") + assert np.all(expected_channel_order == saved_channel_names), ( + f"Channels not saved in correct order.\n" + f"Expected: {expected_channel_order}.\n" + f"Got: {saved_channel_names}" + f"Img list: {[os.path.split(x)[1] for x in img_list]}" + ) assert True diff --git a/tests/test_unit.py b/tests/test_unit.py new file mode 100644 index 00000000..e640587b --- /dev/null +++ b/tests/test_unit.py @@ -0,0 +1,290 @@ +"""Unit tests for valis utilities. + +These tests do not require external datasets and run purely on synthetic data. +""" + +import importlib +import pathlib +import pkgutil +import tempfile + +import numpy as np +import pytest + +# --------------------------------------------------------------------------- +# Import smoke test — guards against missing-import / NameError-at-runtime bugs +# in module-level code by walking every submodule under ``valis``. +# --------------------------------------------------------------------------- + + +def _iter_valis_modules(): + import valis + + for info in pkgutil.walk_packages(valis.__path__, prefix="valis."): + # superglue_models pulls in heavy DL deps that are an optional install. + if info.name.startswith("valis.superglue_models"): + continue + yield info.name + + +@pytest.mark.parametrize("module_name", list(_iter_valis_modules())) +def test_module_imports(module_name): + importlib.import_module(module_name) + + +# --------------------------------------------------------------------------- +# Coordinate-convention utilities (warp_tools.rc_to_wh / wh_to_rc) +# --------------------------------------------------------------------------- + + +class TestCoordConversions: + def test_rc_to_wh_basic(self): + from valis.warp_tools import rc_to_wh + + assert rc_to_wh((100, 200)) == (200, 100) + + def test_wh_to_rc_basic(self): + from valis.warp_tools import wh_to_rc + + assert wh_to_rc((200, 100)) == (100, 200) + + def test_roundtrip_rc_wh(self): + from valis.warp_tools import rc_to_wh, wh_to_rc + + shape = (480, 640) + assert wh_to_rc(rc_to_wh(shape)) == shape + + def test_roundtrip_wh_rc(self): + from valis.warp_tools import rc_to_wh, wh_to_rc + + size = (1920, 1080) + assert rc_to_wh(wh_to_rc(size)) == size + + def test_square_image(self): + from valis.warp_tools import rc_to_wh, wh_to_rc + + assert rc_to_wh((256, 256)) == (256, 256) + assert wh_to_rc((256, 256)) == (256, 256) + + +# --------------------------------------------------------------------------- +# CropMode enum +# --------------------------------------------------------------------------- + + +class TestCropMode: + def test_values(self): + from valis.registration import CropMode + + assert CropMode.OVERLAP == "overlap" + assert CropMode.REFERENCE == "reference" + assert CropMode.NONE == "all" + + def test_string_equality(self): + from valis.registration import CropMode + + # StrEnum: enum members compare equal to their string value + assert CropMode.OVERLAP == "overlap" + assert "overlap" == CropMode.OVERLAP + + def test_backward_compat_constants(self): + from valis.registration import CROP_OVERLAP, CROP_REF, CROP_NONE, CropMode + + assert CROP_OVERLAP == CropMode.OVERLAP + assert CROP_REF == CropMode.REFERENCE + assert CROP_NONE == CropMode.NONE + + def test_all_members(self): + from valis.registration import CropMode + + members = {m.value for m in CropMode} + assert members == {"overlap", "reference", "all"} + + +# --------------------------------------------------------------------------- +# Valis construction validation (Issue 13) +# --------------------------------------------------------------------------- + + +class TestValisConstructionValidation: + def test_nonexistent_src_dir(self): + from valis.registration import Valis + + with pytest.raises(FileNotFoundError): + Valis("/this/path/does/not/exist", "/tmp/dst") + + def test_src_dir_is_file(self, tmp_path): + from valis.registration import Valis + + f = tmp_path / "not_a_dir.txt" + f.write_text("hello") + with pytest.raises(NotADirectoryError): + Valis(str(f), str(tmp_path / "dst")) + + def test_empty_src_dir(self, tmp_path): + from valis.registration import Valis + + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + with pytest.raises(ValueError, match="No supported images"): + Valis(str(empty_dir), str(tmp_path / "dst")) + + def test_single_image_src_dir(self, tmp_path): + """Need at least 2 images for registration.""" + import tifffile + from valis.registration import Valis + + src = tmp_path / "src" + src.mkdir() + # Write a minimal single-channel TIFF + img = np.zeros((32, 32), dtype=np.uint8) + tifffile.imwrite(str(src / "img1.tif"), img) + + with pytest.raises(ValueError, match="At least 2 images"): + Valis(str(src), str(tmp_path / "dst")) + + +# --------------------------------------------------------------------------- +# warp_tools.get_alignment_indices — edge cases +# --------------------------------------------------------------------------- + + +class TestGetAlignmentIndices: + def test_two_images(self): + from valis.warp_tools import get_alignment_indices + + indices = get_alignment_indices(2, ref_img_idx=0) + assert len(indices) == 1 + + def test_five_images_length(self): + from valis.warp_tools import get_alignment_indices + + indices = get_alignment_indices(5, ref_img_idx=2) + assert len(indices) == 4 + + def test_reference_not_in_indices(self): + from valis.warp_tools import get_alignment_indices + + ref = 2 + indices = get_alignment_indices(5, ref_img_idx=ref) + for from_idx, to_idx in indices: + assert from_idx != ref + + def test_all_images_covered(self): + from valis.warp_tools import get_alignment_indices + + n = 5 + indices = get_alignment_indices(n, ref_img_idx=2) + from_idxs = {i for i, _ in indices} + # Every non-reference image must appear exactly once as "from" + assert from_idxs == {0, 1, 3, 4} + + +# --------------------------------------------------------------------------- +# RegistrationConfig dataclass (Issue 3) +# --------------------------------------------------------------------------- + + +class TestRegistrationConfig: + def test_defaults_match_valis_defaults(self): + from valis.registration import RegistrationConfig, DEFAULT_SIMILARITY_METRIC + from valis.registration import ( + DEFAULT_MAX_IMG_DIM, + DEFAULT_MAX_PROCESSED_IMG_SIZE, + ) + + cfg = RegistrationConfig() + assert cfg.similarity_metric == DEFAULT_SIMILARITY_METRIC + assert cfg.max_image_dim_px == DEFAULT_MAX_IMG_DIM + assert cfg.max_processed_image_dim_px == DEFAULT_MAX_PROCESSED_IMG_SIZE + + def test_for_ihc_preset(self): + from valis.registration import RegistrationConfig, CropMode + + cfg = RegistrationConfig.for_ihc() + assert cfg.non_rigid_registrar_cls is None + assert cfg.crop == CropMode.REFERENCE + assert cfg.max_processed_image_dim_px == 1024 + + def test_for_cycif_preset(self): + from valis.registration import RegistrationConfig, CropMode + + cfg = RegistrationConfig.for_cycif() + assert cfg.non_rigid_registrar_cls is not None + assert cfg.crop == CropMode.OVERLAP + + def test_config_applied_to_valis(self, tmp_path): + """RegistrationConfig values should appear on the Valis object.""" + import tifffile + from valis.registration import Valis, RegistrationConfig, CropMode + + src = tmp_path / "src" + src.mkdir() + for i in range(2): + tifffile.imwrite( + str(src / f"img{i}.tif"), np.zeros((32, 32), dtype=np.uint8) + ) + + cfg = RegistrationConfig( + max_processed_image_dim_px=256, + crop=CropMode.NONE, + ) + registrar = Valis(str(src), str(tmp_path / "dst"), config=cfg) + assert registrar.max_processed_image_dim_px == 256 + assert registrar.crop == CropMode.NONE + + def test_explicit_kwarg_overrides_config(self, tmp_path): + """Explicit keyword argument must take precedence over config value.""" + import tifffile + from valis.registration import Valis, RegistrationConfig, CropMode + + src = tmp_path / "src" + src.mkdir() + for i in range(2): + tifffile.imwrite( + str(src / f"img{i}.tif"), np.zeros((32, 32), dtype=np.uint8) + ) + + cfg = RegistrationConfig(crop=CropMode.OVERLAP) + registrar = Valis(str(src), str(tmp_path / "dst"), config=cfg, crop="reference") + assert registrar.crop == "reference" + + +# --------------------------------------------------------------------------- +# DisplacementField (Issue 6) +# --------------------------------------------------------------------------- + + +class TestDisplacementField: + def test_empty_by_default(self): + from valis.registration import DisplacementField + + df = DisplacementField() + assert df.is_empty + assert not df.is_on_disk + + def test_set_and_retrieve_array(self): + from valis.registration import DisplacementField + + arr = np.zeros((2, 4, 4), dtype=np.float32) + df = DisplacementField(array=arr) + assert not df.is_empty + result = df.as_numpy() + assert np.array_equal(result, arr) + + def test_is_on_disk_after_set_path(self, tmp_path): + from valis.registration import DisplacementField + + p = tmp_path / "dxdy.tiff" + df = DisplacementField() + df.set_path(p) + assert df.is_on_disk + + def test_set_array_rejects_pyvips(self): + import pyvips + from valis.registration import DisplacementField + + vips_img = pyvips.Image.black(4, 4) + df = DisplacementField() + with pytest.raises(TypeError): + df.set_array(vips_img) diff --git a/tma_alignment_pipeline/README.md b/tma_alignment_pipeline/README.md new file mode 100644 index 00000000..a0bf8ae1 --- /dev/null +++ b/tma_alignment_pipeline/README.md @@ -0,0 +1,180 @@ +# TMA alignment pipeline + +A small [Nextflow](https://www.nextflow.io/) pipeline that aligns an H&E +whole-slide image to a reference morphology (e.g. Xenium / IF) whole-slide +image, **one TMA core at a time**, then composes the per-core warped H&E +results back onto a single morphology-sized canvas. + +The per-core alignment itself is delegated to +[Valis](https://github.com/MathOnco/valis) via the `align_two_images.py` +helper script. + +## How it works + +The core problem this pipeline solves: **whole-slide registration between +an H&E scan and a multiplex/IF "morphology" image of a TMA fails when run +on the full slides**. The two modalities are at different physical scales, +have different rotations, contain large blank regions between cores, and +each core is small relative to the slide — feature-based registration +gets distracted by global structure and misaligns individual cores. + +The trick is to **divide the slide into individual TMA cores up front**, +register each one independently, then paste the per-core results back onto +a single canvas in the morphology's coordinate system. Each per-core +registration is a tractable problem (one piece of tissue, roughly +centered, similar in scale), and the pieces are embarrassingly parallel. + +This requires solving two sub-problems before alignment can run: + +1. **Where is each TMA core on each slide?** For the morphology image, + we trust the polygons in the TMA-boundaries GeoParquet — they were + drawn against this exact coordinate system. The polygon bounds (in + microns) divide by `um_per_px` to get morphology pixel bboxes directly. +2. **Which H&E core corresponds to which morphology TMA?** The H&E image + has no a priori bbox annotations, and a naive global affine fit + between morphology and H&E centroids accumulates error toward the + slide's bottom rows (the original `step2b` failure mode). Instead, we + detect H&E core centroids with Otsu thresholding + contour analysis on + a thumbnail, then sort **both** sets of centroids into the same grid + order using a gap-based row-detection heuristic: sort by `y`, find the + `n_rows - 1` largest gaps in the sorted `y` values, use those gaps as + row boundaries, then sort each row by `x`. Matching is then by rank + within that grid — TMA `i` in the morphology grid maps to detected + core `i` in the H&E grid. This sidesteps the bad-affine problem + entirely: we never compute a global transform, just two independent + topological sorts. + +Once both sets of bboxes exist, the rest is straightforward: + +- Crop both modalities to each core's bbox (one pair per TMA). +- Hand each pair to Valis `align_two_images.py`, which does the actual + per-core feature-based registration and warping (morphology = reference, + H&E = moving). +- Allocate a blank canvas the size of the morphology image and `insert` + each warped H&E crop at its morphology bbox position. + +A side-effect of this design is that **everything from step 3 onward is +per-TMA and parallelizable** — Nextflow fans out `VERIFY_CROP_MATCHING` +and `CROP_AND_ALIGN` to one task per core, and `-resume` lets you re-run +only the cores that failed without re-doing the ones that already +succeeded. + +### Why a separate verify step + +The grid-rank matching can fail silently if the H&E thumbnail mask misses +a core or merges two adjacent ones — the counts will mismatch (we hard-fail +there) but more subtly, an off-by-one in any row will scramble every +subsequent assignment. `VERIFY_CROP_MATCHING` writes a side-by-side +thumbnail of `morphology_crop` vs. `hne_crop` for each TMA, labeled with +the TMA ID. Flipping through `results/verify_crops/*.png` is the cheap +way to confirm the matching is correct before letting Valis spend hours +on 30+ alignments. + +## Pipeline steps + +1. **`EXTRACT_MORPHOLOGY_BOXES`** — reads the TMA boundaries geoparquet, + converts the polygon bounds from microns to morphology pixel coordinates, + pads them, and writes `morphology_boxes.json`. +2. **`MATCH_CORES`** — thumbnails the H&E image, Otsu-thresholds it to a + tissue mask, extracts contour centroids + radii for every core, and + matches them by grid-rank (largest y-gaps define rows) to the morphology + centroids from the parquet. Writes `hne_tma_boxes.json`. +3. **`VERIFY_CROP_MATCHING`** — runs once per TMA in parallel. Produces a + side-by-side thumbnail of that core's morphology crop vs. its H&E crop + so you can eyeball the matching before / during the heavy alignment step. +4. **`CROP_AND_ALIGN`** — runs **once per TMA in parallel**. Crops both + modalities to that core's bbox and runs Valis pairwise alignment + (morphology = reference, H&E = moving). Emits `aligned_tma_.ome.tif`. +5. **`COMPOSE`** — pastes every `aligned_tma_.ome.tif` onto a blank + canvas the size of the morphology slide at its `morphology_boxes.json` + position. Output: `aligned_to_morphology.ome.tif` (tiled, pyramidal). + +## Inputs + +| Param | Description | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `--parquet` | TMA-boundaries **GeoParquet** (`tma_boundaries_with_metadata.geo.parquet`). One row per TMA, polygon geometry in **microns**, column `tma_id`. | +| `--morphology` | Reference morphology **OME-TIFF** (e.g. inverted-fluorescence focus image). Coordinate system used for the final composed output. Width/height are read from the file header. | +| `--he` | Moving **H&E OME-TIFF** to be aligned to the morphology image. | +| `--outdir` | Output directory (default: `results`). | + +### Optional tuning parameters + +| Param | Default | Description | +| ---------------------- | ---------------------------------- | ------------------------------------------------------------------------ | +| `--um_per_px` | `0.2125` | Microns per pixel for the morphology image (used to convert parquet µm → px). | +| `--pad` | `200` | Padding (px) around each morphology bbox. | +| `--he_pad` | `150` | Padding (px) added to the detected H&E core circle radius. | +| `--n_rows` | `10` | Expected number of TMA rows on the slide (used for grid-rank matching). | +| `--thumb_height` | `3000` | Thumbnail height (px) used for H&E core detection. | +| `--min_area` | `1500` | Minimum thumbnail-pixel contour area to count as a core. | +| `--verify_thumb_height`| `400` | Per-panel height (px) of the side-by-side verify thumbnails. | +| `--valis_python` | conda env path on `tanseyw` | Python interpreter that has Valis installed. The alignment driver (`bin/align_two_images.py`) is bundled in this directory. | +| `--max_processed_dim` | `1024` | `--max-processed-image-dim-px` passed to Valis. | +| `--reference_stain` | `inverted-fluorescence` | `--reference-stain` passed to Valis. | +| `--image_stain` | `he-hematoxylin-sparse` | `--image-stain` passed to Valis. | + +## Outputs + +Published into `--outdir`: + +``` +results/ +├── morphology_boxes.json # morphology-coord bboxes, keyed by tma_id +├── hne_tma_boxes.json # H&E-coord bboxes, keyed by tma_id +├── he_mask.png # debug: Otsu tissue mask of the H&E thumbnail +├── verify_crops/ +│ └── tma_.png # side-by-side morphology vs. H&E crop, per core +├── aligned/ +│ └── aligned_tma_.ome.tif # per-TMA warped H&E (one per core) +├── logs/ +│ └── align_tma_.log # Valis stdout/stderr per core +├── aligned_to_morphology.ome.tif # final composed image, in morphology coords +├── nextflow_report.html +└── nextflow_trace.txt +``` + +## Running it + +The `bin/` scripts assume `pyvips`, `geopandas`, `opencv-python`, and `numpy` +are importable. The simplest setup is to activate the Valis conda env first, +since it already has everything: + +```bash +conda activate /data1/tanseyw/quinnj2/conda_environments/valis +``` + +Then: + +```bash +nextflow run main.nf \ + --parquet tma_boundaries_with_metadata.geo.parquet \ + --morphology morphology_focus_0000_8bit_inverted.tif \ + --he ES-3990_R1-S1_cropped_rgb.tif \ + --outdir results +``` + +To run on LSF instead of the local machine: + +```bash +nextflow run main.nf -profile lsf [...] +``` + +To resume after a failed/partial run (Nextflow's killer feature — already +finished cores will not be re-aligned): + +```bash +nextflow run main.nf -resume [...] +``` + +## Notes / caveats + +- **Core count must match.** `MATCH_CORES` fails if the number of detected + H&E cores differs from the number of morphology TMAs. If that happens, + inspect `results/he_mask.png` and tune `--min_area` or `--thumb_height`. +- **Grid-rank matching** assumes the TMAs are arranged in a regular grid + and that both modalities have the same number of rows. The original + ES-3990 case had 10 rows; set `--n_rows` accordingly. +- **Stain flags** are forwarded verbatim to Valis. The defaults are + tuned for inverted-fluorescence reference vs. sparse H&E hematoxylin + moving — change them if you align other modality pairs. diff --git a/tma_alignment_pipeline/bin/align_two_images.py b/tma_alignment_pipeline/bin/align_two_images.py new file mode 100755 index 00000000..4ae57e9a --- /dev/null +++ b/tma_alignment_pipeline/bin/align_two_images.py @@ -0,0 +1,1020 @@ +import argparse +import os +import shutil +import sys +import time +from valis import registration, feature_matcher, feature_detectors, preprocessing +from valis.serial_rigid import TooFewMatchesError +import numpy as np +import pyvips + +from pathlib import Path +from typing import List + +from tqdm import tqdm + +from ome_types import OME +from ome_types._autogenerated.ome_2016_06 import ( + Channel, + TiffData, + Plane, + Pixels, + UnitsLength, + Image, +) +from ome_types.model.simple_types import ChannelID, PixelsID, ImageID +from valis import slide_tools +from valis import orientation_check +import logging + +NUMPY_FORMAT_BF_DTYPE = { + "uint8": "uint8", + "int8": "int8", + "uint16": "uint16", + "int16": "int16", + "uint32": "uint32", + "int32": "int32", + "float32": "float", + "float64": "double", +} + +logger = logging.getLogger(__name__) + + +def get_parser(): + parser = argparse.ArgumentParser(description="Align two WSIs.") + parser.add_argument( + "--reference", + type=str, + help="Path to WSI in .ome.tif format. Will be used as the reference image.", + ) + parser.add_argument( + "--image", + type=str, + help="Path to WSI in .ome.tif format. Will be used as the warped image.", + ) + parser.add_argument( + "--output-dir", + type=str, + help="Output directory.", + ) + parser.add_argument( + "--max-processed-image-dim-px", + type=int, + default=2048, + help="Max side length used for feature detection / non-rigid registration. " + "Higher = better matches but more memory; 4096 OOMs on a 32GB laptop.", + ) + stain_choices = ( + "auto", + "he-hematoxylin", + "he-hematoxylin-raw", + "he-hematoxylin-sparse", + "inverted-fluorescence", + "od", + "colorful-standardizer", + "luminosity", + ) + parser.add_argument( + "--image-stain", + choices=stain_choices, + default="auto", + help="Preprocessor for --image. 'he-hematoxylin' deconvolves H&E and " + "keeps the hematoxylin (nuclei) channel, automatically falling back " + "to the sparse-dots variant if the matcher gets too few matches. " + "'he-hematoxylin-sparse' forces the sparse path. 'inverted-" + "fluorescence' un-inverts a DAPI-style image so nuclei are bright. " + "'auto' lets the script decide.", + ) + parser.add_argument( + "--reference-stain", + choices=stain_choices, + default="auto", + help="Same as --image-stain but for --reference.", + ) + parser.add_argument( + "--orientation-margin", + type=float, + default=0.0, + help="Minimum NCC margin (best - identity) required to apply a D4 " + "pre-rotation. Below this, the script falls back to identity. " + "Default 0 trusts the winning transform; raise it (e.g. 0.05) only " + "when the script's heuristic is producing wrong flips.", + ) + parser.add_argument( + "--no-script-orientation", + action="store_true", + help="Skip the script's own D4 pre-rotation entirely and let valis " + "handle reflections (via check_for_reflections=True). Useful when the " + "script's NCC-based orientation check is too noisy on weak/ambiguous " + "stain pairs.", + ) + parser.add_argument( + "--min-rigid-matches", + type=int, + default=30, + help="Minimum number of initial keypoint matches required before " + "valis's rematch step. Below this, the script aborts with a clear " + "error instead of letting valis warp the image with a degenerate " + "transform (which OOMs the rematch's feature detection).", + ) + return parser + + +class HematoxylinExtractor(preprocessing.ImageProcesser): + """Extract the hematoxylin (nuclei) channel from an H&E image. + + Pipeline: Macenko-style normalization against a standard H&E + reference (so faded slides come back up to consistent intensity), + then deconvolve using ``skimage.color.rgb2hed`` with the fixed + Ruifrok-Johnston stain matrix and keep only the H channel. + + Macenko's ``normalize_he`` picks H vs E by an angle heuristic on the + candidate stain vectors' red component. On eosin-dominant or + hematoxylin-faded slides that ordering can flip, sending eosin into + the "hematoxylin" row — yielding an output where stroma lights up + brighter than nuclei. We detect that here by checking which row + correlates with bluish pixels in the original RGB (true hematoxylin) + and swap if needed. + + Pass ``use_macenko=False`` to skip normalization entirely and just + run ``rgb2hed`` on raw RGB — useful as a fallback when Macenko + misbehaves on an atypical slide. + """ + + def create_mask(self): + from valis.preprocessing import create_tissue_mask_from_rgb + + _, tissue_mask = create_tissue_mask_from_rgb(self.image) + return tissue_mask + + def process_image( + self, + *args, + use_macenko: bool = True, + sparse: bool = False, + sparse_pct: float = 90.0, + sparse_blur_sigma: float = 1.5, + **kwargs, + ): + """Extract hematoxylin channel. + + ``sparse=True`` thresholds the H channel at the ``sparse_pct`` + percentile and zeros everything below — yielding a punctate + "nuclei dots" image that resembles a fluorescence reference. + Use this on eosin-dominant / hematoxylin-faded slides where the + smooth percentile stretch produces stromal texture instead of + isolated nuclei. + """ + from skimage.color import rgb2hed + + img = self.image + if img.ndim != 3 or img.shape[2] < 3: + raise ValueError("HematoxylinExtractor requires an RGB image") + rgb = img[..., :3] + if rgb.dtype != np.uint8: + rgb = np.clip(rgb, 0, 255).astype(np.uint8) + + rgb_for_unmix = rgb + if use_macenko: + # Macenko-style normalization to a standard H&E reference. + # Brings faded slides up to consistent stain intensity. + try: + normalized = preprocessing.normalize_he(rgb, Io=240, alpha=1, beta=0.15) + normalized = _fix_he_swap(normalized, rgb) + # Reproject normalized concentrations through the + # canonical Ruifrok-Johnston H/E vectors so rgb2hed below + # sees a canonical H&E image. + ref_stain = np.array( + [[0.5626, 0.7201, 0.4062], [0.2159, 0.8012, 0.5581]] + ) + recon_od = ref_stain.T @ normalized + recon = np.clip(240.0 * np.exp(-recon_od), 0, 255).T.reshape(rgb.shape) + rgb_for_unmix = recon.astype(np.uint8) + except Exception: + # Macenko can fail on degenerate (very faded / very dark) + # tissue. Fall back to raw RGB rather than aborting. + rgb_for_unmix = rgb + + hed = rgb2hed(rgb_for_unmix) + h = hed[..., 0] # hematoxylin channel (positive where stain is dense) + + if sparse: + # Restrict the percentile to tissue pixels so a large dark + # background doesn't pull the threshold down. Then zero + # everything below the cutoff and stretch the survivors. + from valis.preprocessing import create_tissue_mask_from_rgb + + try: + _, tissue_mask = create_tissue_mask_from_rgb(rgb) + tissue = tissue_mask > 0 + except Exception: + tissue = np.ones(h.shape, dtype=bool) + if tissue.sum() < 1000: + tissue = np.ones(h.shape, dtype=bool) + cutoff = np.percentile(h[tissue], sparse_pct) + top = np.percentile(h[tissue], 99.9) + denom = max(top - cutoff, 1e-6) + out = np.clip((h - cutoff) / denom, 0.0, 1.0) + out[~tissue] = 0 + if sparse_blur_sigma > 0: + # Smooth so each surviving spot becomes a small Gaussian + # blob with a real local maximum — gives keypoint matchers + # something with scale to lock onto. + from scipy.ndimage import gaussian_filter + + out = gaussian_filter(out.astype(np.float32), sparse_blur_sigma) + m = out.max() + if m > 0: + out = out / m + return (out * 255).astype(np.uint8) + + lo, hi = np.percentile(h, (1, 99)) + if hi <= lo: + hi = lo + 1e-6 + h = np.clip((h - lo) / (hi - lo), 0.0, 1.0) + return (h * 255).astype(np.uint8) + + +def _fix_he_swap(normalized: np.ndarray, rgb: np.ndarray) -> np.ndarray: + """Detect and correct H/E row swaps from Macenko's angle heuristic. + + Hematoxylin stains pixels blue-purple, eosin pink-red. So the true + hematoxylin concentration row should correlate positively with + (B - R) across the image, while eosin should anti-correlate. If the + rows are swapped, swap them back. + """ + flat = rgb.reshape(-1, 3).astype(np.float32) + blueness = flat[:, 2] - flat[:, 0] # B - R + # Mask to tissue pixels (anything sufficiently darker than 240 bg) + od_proxy = 255.0 - flat.mean(axis=1) + tissue = od_proxy > 15.0 + if tissue.sum() < 1000: + return normalized + b = blueness[tissue] + if b.std() < 1e-3: + return normalized + c0 = np.corrcoef(normalized[0, tissue], b)[0, 1] + c1 = np.corrcoef(normalized[1, tissue], b)[0, 1] + if np.isnan(c0) or np.isnan(c1): + return normalized + if c1 > c0: + return normalized[::-1].copy() + return normalized + + +class InvertedFluorescence(preprocessing.ImageProcesser): + """Reverse the inversion on an 'inverted DAPI' (or similar) greyscale image + so that nuclei come out bright — matching the convention of hematoxylin + deconvolution output. + """ + + def create_mask(self): + img = self.image + if img.ndim == 3: + img = img.mean(axis=-1) + img = img.astype(np.float32) + # In an inverted-fluorescence image, tissue is dark on a bright bg. + thresh = np.percentile(img, 90) + mask = (img < thresh).astype(np.uint8) * 255 + return mask + + def process_image(self, *args, **kwargs): + img = self.image + if img.ndim == 3: + img = img.mean(axis=-1) + img = img.astype(np.float32) + lo, hi = np.percentile(img, (1, 99)) + if hi <= lo: + hi = lo + 1.0 + img = np.clip((img - lo) / (hi - lo), 0.0, 1.0) + inverted = 1.0 - img + return (inverted * 255).astype(np.uint8) + + +def setup_valis_logging(): + """Configure detailed stream logging for valis logger""" + # Get the valis logger + valis_logger = logging.getLogger("valis") + valis_logger.setLevel(logging.DEBUG) + valis_logger.setLevel(logging.DEBUG) + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + + # Create detailed formatter with line number, function name, etc. + formatter = logging.Formatter( + fmt="%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s() - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + console_handler.setFormatter(formatter) + + # Add handler to logger + valis_logger.addHandler(console_handler) + valis_logger.addHandler(console_handler) + + return valis_logger + + +def pyvips_to_thumbnail_array(img: pyvips.Image, size: int) -> "np.ndarray": + """Render a pyvips image to a small numpy array for orientation scoring. + + Uses pyvips' resize so we never materialize the full slide in memory. + """ + import numpy as np + + scale = size / max(img.width, img.height) + small = img.resize(scale) + if small.bands > 1: + small = small.colourspace("b-w") + if small.format == "ushort": + small = (small >> 8).cast("uchar") + elif small.format != "uchar": + small = small.cast("uchar") + mem = small.write_to_memory() + return np.frombuffer(mem, dtype=np.uint8).reshape(small.height, small.width) + + +def pyvips_to_thumbnail_rgb_array(img: pyvips.Image, size: int) -> "np.ndarray": + """Render a pyvips image to a small RGB numpy array. Used to feed + color-aware preprocessors (HEDeconvolution, HematoxylinFixed, OD, ...) + on a thumbnail prior to the orientation check. + """ + scale = size / max(img.width, img.height) + small = img.resize(scale) + if small.format == "ushort": + small = (small >> 8).cast("uchar") + elif small.format != "uchar": + small = small.cast("uchar") + if small.bands == 1: + small = small.bandjoin([small, small]) + elif small.bands > 3: + small = small[0].bandjoin([small[1], small[2]]) + mem = small.write_to_memory() + return np.frombuffer(mem, dtype=np.uint8).reshape(small.height, small.width, 3) + + +def run_processor_on_thumbnail( + processor_spec, thumb_array: "np.ndarray", src_f: str +) -> "np.ndarray": + """Instantiate a valis ``ImageProcesser`` subclass on a thumbnail and + return its ``process_image`` output. Used so the orientation check + sees the same nuclei-bright signal that valis will register on, + rather than raw luminance of two visually dissimilar stains. + """ + cls, kwargs = processor_spec + proc = cls(image=thumb_array, src_f=src_f, level=0, series=0) + return proc.process_image(**(kwargs or {})) + + +def check_and_correct_orientation( + moving_img: pyvips.Image, + reference_img: pyvips.Image, + downsample_size: int = 2048, + margin_threshold: float = 0.05, + moving_processor=None, + reference_processor=None, + moving_src_f: str = "", + reference_src_f: str = "", +) -> tuple[pyvips.Image, "orientation_check.OrientationMatch"]: + """Find the best D4 orientation of ``moving_img`` against ``reference_img`` + and return the corrected pyvips image plus the match info. + + ``downsample_size`` is the requested thumbnail side. If either input is + smaller than that on its short side, the effective size is clamped down + so we never upsample. + + If ``moving_processor`` / ``reference_processor`` are provided, the + corresponding side is preprocessed with that valis ``ImageProcesser`` + before the NCC scoring. This is what makes the check usable across + stains: comparing raw luminance of (e.g.) H&E vs. inverted DAPI is + essentially noise, but comparing extracted hematoxylin vs. un-inverted + DAPI gives a strong, anatomically-aligned signal. + """ + max_useful = min( + reference_img.width, + reference_img.height, + moving_img.width, + moving_img.height, + ) + effective_size = min(downsample_size, max_useful) + if effective_size != downsample_size: + print( + f"[orientation] requested size {downsample_size} clamped to " + f"{effective_size} (smaller input side)" + ) + + if reference_processor is not None: + rgb = pyvips_to_thumbnail_rgb_array(reference_img, effective_size) + ref_thumb = run_processor_on_thumbnail( + reference_processor, rgb, reference_src_f + ) + else: + ref_thumb = pyvips_to_thumbnail_array(reference_img, effective_size) + if moving_processor is not None: + rgb = pyvips_to_thumbnail_rgb_array(moving_img, effective_size) + mov_thumb = run_processor_on_thumbnail(moving_processor, rgb, moving_src_f) + else: + mov_thumb = pyvips_to_thumbnail_array(moving_img, effective_size) + + match = orientation_check.find_best_orientation( + ref_thumb, mov_thumb, downsample_size=effective_size, use_gradient=True + ) + + print("\n=== Orientation check ===") + print(f" thumbnail size : {effective_size}x{effective_size}") + print(f" best transform : {match.name}") + print(f" correction needed : {orientation_check.describe(match)}") + print(f" best NCC score : {match.score:+.4f}") + identity_score = match.scores["identity"] + margin = match.score - identity_score + print(f" identity NCC score : {identity_score:+.4f} (margin: {margin:+.4f})") + print(" all scores :") + for name, score in sorted(match.scores.items(), key=lambda kv: -kv[1]): + marker = " <-- chosen" if name == match.name else "" + print(f" {name:<14s} {score:+.4f}{marker}") + inconclusive = ( + margin_threshold > 0 + and (match.k != 0 or match.mirror) + and margin < margin_threshold + ) + if inconclusive: + print( + f" >> margin {margin:+.4f} below threshold {margin_threshold}; " + "treating orientation check as inconclusive and using identity." + ) + match = orientation_check.OrientationMatch( + name="identity", + k=0, + mirror=False, + score=identity_score, + scores=match.scores, + ) + elif match.k == 0 and not match.mirror: + print(" >> images already correctly oriented; no pre-rotation applied.") + else: + print( + f" >> applying {orientation_check.describe(match)} to moving image " + "before registration." + ) + print("=========================\n") + + corrected = orientation_check.apply_d4_pyvips(moving_img, match.k, match.mirror) + return corrected, match + + +def convert_16to8_bit(image: pyvips.Image, clamp=None): + if clamp: + image = (image > clamp).ifthenelse(clamp, image) + else: + clamp = 2**16 + tile_processed = ((image / clamp) * 255).cast("uchar") + return tile_processed.colourspace("b-w") + + +def warp_new_slide( + new_img: pyvips.Image, + registrar: registration.Valis, + source_img: str, + dst_img: str, + bbox_xywh=None, +): + src_slide_obj = registrar.get_slide(source_img) + dst_slide_obj = registrar.get_slide(dst_img) + + warped_img = src_slide_obj.warp_img_from_to(new_img, to_slide_obj=dst_slide_obj) + + return warped_img + + +def create_progress_callback(): + """Create a progress callback for writing the output file.""" + pbar = tqdm(total=100, desc="Writing OME-TIFF", unit="%") + last_update = time.time() + + def eval_callback(image, progress): + if (time.time() - last_update) > 0.25: + # Set the progress bar's current iteration count directly + pbar.n = progress.percent + + # Refresh the display to show the updated value + pbar.refresh() + + eval_callback.close = lambda: pbar.close() + return eval_callback + + +def vips2bf_dtype(vips_format): + np_dtype = slide_tools.VIPS_FORMAT_NUMPY_DTYPE[vips_format] + bf_dtype = NUMPY_FORMAT_BF_DTYPE[str(np_dtype().dtype)] + + return bf_dtype + + +def create_ome_metadata( + width: int, + height: int, + protein_names: List[str], + protein_ids: List[str], + um_per_px: float, + dtype: str = "uint16", +) -> str: + """ + Create OME-XML metadata for single or multi-channel protein images. + + Parameters: + ----------- + width : int + Image width in pixels + height : int + Image height in pixels + protein_names : list of str + Human-readable protein name(s), one per channel + protein_ids : list of str + Protein identifier(s), one per channel + um_per_px : float + Micrometers per pixel + dtype : str + Data type (default: 'uint16') + + Returns: + -------- + str : OME-XML metadata as string + """ + + # Validate inputs + num_channels = len(protein_names) + if len(protein_ids) != num_channels: + raise ValueError( + f"Length of protein_names ({num_channels}) and protein_ids " + f"({len(protein_ids)}) must match" + ) + + if num_channels == 0: + raise ValueError("Must provide at least one protein name and ID") + + # Create the OME structure + ome = OME() + + # Create Channels + channels = [] + for c in range(num_channels): + channel = Channel( + id=ChannelID(f"Channel:0:{c}"), + name=protein_names[c], + samples_per_pixel=1, + ) + channels.append(channel) + + # Create TiffData blocks + tiff_data_blocks = [] + for c in range(num_channels): + tiff_data = TiffData( + first_z=0, + first_c=c, + first_t=0, + ifd=c, + plane_count=1, + ) + tiff_data_blocks.append(tiff_data) + + # Create Planes + planes = [] + for c in range(num_channels): + plane = Plane( + the_z=0, + the_c=c, + the_t=0, + ) + planes.append(plane) + + # Create Pixels + pixels = Pixels( + id=PixelsID("Pixels:0"), + dimension_order="XYZCT", + size_x=width, + size_y=height, + size_z=1, + size_c=num_channels, + size_t=1, + type=dtype, + big_endian=False, + physical_size_x=um_per_px, + physical_size_y=um_per_px, + physical_size_x_unit=UnitsLength.MICROMETER, + physical_size_y_unit=UnitsLength.MICROMETER, + channels=channels, + tiff_data_blocks=tiff_data_blocks, + planes=planes, + ) + + # Create Image name from protein info + if num_channels == 1: + image_name = f"{protein_names[0]} ({protein_ids[0]})" + else: + # For multichannel, create a combined name + image_name = f"Multichannel ({', '.join(protein_ids)})" + + # Create Image with pixels + image = Image( + id=ImageID("Image:0"), + name=image_name, + pixels=pixels, + ) + + # Assemble the structure + ome.images.append(image) + + # Convert to XML string + return ome.to_xml() + + +def write_ome_tiff( + images: list[pyvips.Image], + names: list[str], + output_path, + um_per_px: float, + scale: float = 1.0, +): + first_height = images[0].height + first_width = images[0].width + first_format = images[0].format + for img in images: + if img.width != first_width or img.height != first_height: + raise ValueError( + f"mismatched image size:" + f" {first_width}x{first_height} vs {img.width}x{img.height}" + ) + if img.format != first_format: + raise ValueError(f"mismatched format: {first_format} vs {img.format}") + if scale < 1.0: + images = [im.resize(scale) for im in images] + + # Only create OME metadata if ome_tiff is True + ome_xml = create_ome_metadata( + width=images[0].width, + height=images[0].height, + protein_names=names, + protein_ids=names, + um_per_px=um_per_px / scale, + dtype=vips2bf_dtype(images[0].format), + ) + + stacked = pyvips.Image.arrayjoin(images, across=1) + stacked.set_type(pyvips.GValue.gstr_type, "image-description", ome_xml) + stacked.set_type(pyvips.GValue.gint_type, "page-height", images[0].height) + + stacked.set_progress(True) + stacked.signal_connect("eval", create_progress_callback()) + + # Append remaining images + stacked.tiffsave( + output_path, + pyramid=True, + subifd=True, + tile=True, + compression="jpeg", + tile_height=256, + tile_width=256, + Q=100, + bigtiff=True, + ) + + print(f"\nSuccessfully wrote: {output_path}") + + +def main(): + # Setup logging first + setup_valis_logging() + args = get_parser().parse_args() + Path(args.output_dir).mkdir(parents=True, exist_ok=True) + + print("=== align_two_images invocation ===", flush=True) + print(f" argv : {' '.join(sys.argv)}", flush=True) + print(f" cwd : {os.getcwd()}", flush=True) + for k, v in sorted(vars(args).items()): + print(f" {k:<24}: {v}", flush=True) + print("===================================", flush=True) + + img_out_path = os.path.join(args.output_dir, os.path.basename(args.image)) + ref_out_path = os.path.join(args.output_dir, os.path.basename(args.reference)) + + # Save 8-bit copy of --reference so valis learns registration from uint8 + if not os.path.exists(ref_out_path): + ref_img = pyvips.Image.new_from_file(args.reference, page=0) + if ref_img.format == "ushort": + ref_img = convert_16to8_bit(ref_img) + ref_img.set_progress(True) + cb = create_progress_callback() + ref_img.signal_connect("eval", cb) + ref_img.tiffsave(ref_out_path) + cb.close() + + # Build a per-image processor dict so we can force nuclei-extraction on + # both sides — H&E -> hematoxylin channel, inverted DAPI -> un-inverted + # greyscale. Both end up "nuclei = bright", which gives the matcher + # something common to lock onto across modalities. We also feed these + # processors into the orientation check below so it scores nuclei-vs- + # nuclei rather than raw luminance of two visually different stains. + stain_to_processor = { + "he-hematoxylin": [HematoxylinExtractor, {}], + "he-hematoxylin-raw": [HematoxylinExtractor, {"use_macenko": False}], + "he-hematoxylin-sparse": [HematoxylinExtractor, {"sparse": True}], + "inverted-fluorescence": [InvertedFluorescence, {}], + "od": [preprocessing.OD, {}], + "colorful-standardizer": [preprocessing.ColorfulStandardizer, {}], + "luminosity": [preprocessing.Luminosity, {}], + } + + # `auto` resolves *here*, not inside valis. Valis's default for a 1-band + # uchar (assumed-fluorescence) input is to leave polarity untouched — + # which silently breaks cross-modal matching when the input is *inverted* + # DAPI (dark nuclei on bright bg). We pick a real processor based on + # bands + mean intensity so 'nuclei = bright' on both sides, which is + # what the matcher actually needs. + def _resolve_auto_stain(path: str) -> str: + peek = pyvips.Image.new_from_file(path, page=0) + thumb = pyvips_to_thumbnail_array(peek, 256) + mean = float(thumb.mean()) + if peek.bands >= 3: + return "he-hematoxylin" + # single-band: high mean => bright bg (inverted DAPI), un-invert + return "inverted-fluorescence" if mean > 127 else "luminosity" + + if args.image_stain == "auto": + args.image_stain = _resolve_auto_stain(args.image) + print(f"[auto-stain] --image -> {args.image_stain}") + if args.reference_stain == "auto": + args.reference_stain = _resolve_auto_stain(args.reference) + print(f"[auto-stain] --reference -> {args.reference_stain}") + + reference_path = os.path.join(args.output_dir, os.path.basename(args.reference)) + + # Cheap pre-alignment orientation check. If the moving image is rotated or + # mirrored relative to the reference, bake the correction into the copy + # that gets handed to Valis so registration only deals with residuals. + if args.no_script_orientation: + print( + "[orientation] script orientation check disabled (--no-script-orientation)" + ) + orient_match = orientation_check.OrientationMatch( + name="identity", k=0, mirror=False, score=0.0, scores={} + ) + else: + ref_for_check = pyvips.Image.new_from_file(ref_out_path, page=0) + moving_for_check = pyvips.Image.new_from_file(args.image, page=0) + _, orient_match = check_and_correct_orientation( + moving_img=moving_for_check, + reference_img=ref_for_check, + downsample_size=2048, + margin_threshold=args.orientation_margin, + moving_processor=stain_to_processor.get(args.image_stain), + reference_processor=stain_to_processor.get(args.reference_stain), + moving_src_f=args.image, + reference_src_f=args.reference, + ) + + needs_correction = orient_match.k != 0 or orient_match.mirror + if needs_correction: + # Write the D4-corrected moving image to a distinct filename so we + # don't collide with — and silently skip — an already-existing copy + # at ``img_out_path`` (which happens whenever ``args.output_dir`` + # is the same directory as the input). + stem, ext = os.path.splitext(os.path.basename(args.image)) + suffix = f"_k{orient_match.k}_m{int(orient_match.mirror)}" + img_out_path = os.path.join(args.output_dir, f"{stem}{suffix}{ext}") + if not os.path.exists(img_out_path): + if needs_correction: + print( + f"[orientation] writing D4-corrected copy to {img_out_path}", + flush=True, + ) + moving_full = pyvips.Image.new_from_file(args.image, page=0) + corrected = orientation_check.apply_d4_pyvips( + moving_full, orient_match.k, orient_match.mirror + ) + corrected.set_progress(True) + cb = create_progress_callback() + corrected.signal_connect("eval", cb) + corrected.tiffsave(img_out_path, bigtiff=True) + cb.close() + else: + os.symlink(os.path.abspath(args.image), img_out_path) + + # Build the processor_dict against the *final* registered file paths, + # which differ from the raw inputs when we wrote a D4-corrected copy. + image_path = img_out_path + + def _build_processor_dict( + image_stain: str, + reference_stain: str, + sparse_kwargs: dict | None = None, + ) -> dict: + pd = {} + if image_stain != "auto": + cls, kw = stain_to_processor[image_stain] + if sparse_kwargs and image_stain in ( + "he-hematoxylin", + "he-hematoxylin-sparse", + ): + kw = {**kw, **sparse_kwargs} + pd[image_path] = [cls, kw] + if reference_stain != "auto": + cls, kw = stain_to_processor[reference_stain] + if sparse_kwargs and reference_stain in ( + "he-hematoxylin", + "he-hematoxylin-sparse", + ): + kw = {**kw, **sparse_kwargs} + pd[reference_path] = [cls, kw] + return pd + + matches_dir = os.path.join(args.output_dir, "registration", "matches") + + def _dump_failed_matches(registrar): + # Try to dump whatever matches valis did find before bailing — the + # serial-rigid registrar holds the initial-pass match_dict at this + # point, which is exactly what the user needs to diagnose the + # failure. Pipeline.draw_matches isn't usable yet (relies on + # post-rigid attributes), so build the viz from match_dict directly. + try: + from valis import viz as _viz, warp_tools as _wt + + os.makedirs(matches_dir, exist_ok=True) + srr = registrar.rigid_registrar + for moving_idx, fixed_idx in srr.iter_order: + moving = srr.img_obj_list[moving_idx] + fixed = srr.img_obj_list[fixed_idx] + m = fixed.match_dict.get(moving) + if m is None: + continue + img1 = _wt.resize_img(moving.image, moving.image.shape[:2]) + img2 = _wt.resize_img(fixed.image, fixed.image.shape[:2]) + viz_img = _viz.draw_matches( + src_img=img1, + kp1_xy=m.matched_kp2_xy, + dst_img=img2, + kp2_xy=m.matched_kp1_xy, + rad=3, + alignment="horizontal", + ) + out_f = os.path.join( + matches_dir, + f"failed_{moving.name}_to_{fixed.name}_matches.png", + ) + _wt.save_img(out_f, viz_img) + print(f"Saved pre-failure match viz: {out_f}") + except Exception as draw_err: + print(f"(could not draw matches: {draw_err})") + + # Holder so the caller can grab the registrar from a failed attempt + # (so we can dump pre-failure match visualizations). + last_registrar = {"obj": None} + + def _attempt_register(processor_dict): + # Wipe any prior registration output so Valis starts fresh + # (otherwise it short-circuits on cached processed images and + # we'd re-use the previous attempt's processor outputs). + reg_dir = os.path.join(args.output_dir, "registration") + if os.path.isdir(reg_dir): + shutil.rmtree(reg_dir) + registrar = registration.Valis( + src_dir=args.output_dir, + dst_dir=args.output_dir, + name="registration", + img_list=[image_path, reference_path], + reference_img_f=reference_path, + thumbnail_size=4096, + max_processed_image_dim_px=args.max_processed_image_dim_px, + max_image_dim_px=args.max_processed_image_dim_px, + min_rigid_matches=args.min_rigid_matches, + check_for_reflections=False, + similarity_metric="euclidean", + align_to_reference=True, + ) + last_registrar["obj"] = registrar + registrar.register( + processor_dict=processor_dict if processor_dict else None, + ) + return registrar + + # Primary attempt with the user-selected stains. If `he-hematoxylin` + # fails to produce enough matches, retry once with the sparse variant + # (small bright dots resembling fluorescence nuclei) — same output is + # available explicitly via `he-hematoxylin-sparse`. + primary_pd = _build_processor_dict(args.image_stain, args.reference_stain) + try: + registrar = _attempt_register(primary_pd) + except TooFewMatchesError as e: + fb_image_stain = ( + "he-hematoxylin-sparse" + if args.image_stain == "he-hematoxylin" + else args.image_stain + ) + fb_reference_stain = ( + "he-hematoxylin-sparse" + if args.reference_stain == "he-hematoxylin" + else args.reference_stain + ) + has_fallback = ( + fb_image_stain != args.image_stain + or fb_reference_stain != args.reference_stain + ) + if not has_fallback: + if last_registrar["obj"] is not None: + _dump_failed_matches(last_registrar["obj"]) + print(f"\nALIGNMENT ABORTED: {e}", flush=True) + raise SystemExit(2) + + # Sweep sparse-extractor params: each (pct, sigma) gives a + # different density / blob-size tradeoff. We try several + # combinations before giving up so a single percentile that + # happens to be poorly tuned for this slide doesn't doom the + # whole alignment. Order goes default-first, then progressively + # denser/sparser variants. + sparse_sweep = [ + {"sparse_pct": 90.0, "sparse_blur_sigma": 1.5}, + {"sparse_pct": 85.0, "sparse_blur_sigma": 2.0}, + {"sparse_pct": 95.0, "sparse_blur_sigma": 1.0}, + {"sparse_pct": 80.0, "sparse_blur_sigma": 2.5}, + {"sparse_pct": 97.0, "sparse_blur_sigma": 1.5}, + ] + registrar = None + last_err: TooFewMatchesError | None = None + for i, sparse_kwargs in enumerate(sparse_sweep, start=1): + label = ( + f"pct={sparse_kwargs['sparse_pct']}, " + f"sigma={sparse_kwargs['sparse_blur_sigma']}" + ) + print( + f"[fallback {i}/{len(sparse_sweep)}] retrying with sparse " + f"hematoxylin extractor (image={fb_image_stain}, " + f"reference={fb_reference_stain}, {label})", + flush=True, + ) + fb_pd = _build_processor_dict( + fb_image_stain, fb_reference_stain, sparse_kwargs=sparse_kwargs + ) + try: + registrar = _attempt_register(fb_pd) + break + except TooFewMatchesError as e2: + last_err = e2 + continue + if registrar is None: + if last_registrar["obj"] is not None: + _dump_failed_matches(last_registrar["obj"]) + print( + f"\nALIGNMENT ABORTED: exhausted sparse sweep " + f"({len(sparse_sweep)} attempts). Last error: {last_err}", + flush=True, + ) + raise SystemExit(2) + + registrar.draw_matches(matches_dir) + print(f"Saved feature-match visualization to {matches_dir}") + + if not os.path.exists(os.path.join(args.output_dir, "aligned.ome.tif")): + warped_slides = [] + names = [] + + img = pyvips.Image.new_from_file(args.image, page=0) + if img.format == "ushort": + img = convert_16to8_bit(img) + if needs_correction: + img = orientation_check.apply_d4_pyvips( + img, orient_match.k, orient_match.mirror + ) + warped_img = warp_new_slide( + img, + registrar, + source_img=image_path, + dst_img=reference_path, + ) + warped_slides.append(warped_img) + names.append(os.path.basename(args.reference)) + + img = pyvips.Image.new_from_file(args.reference, page=0) + if img.format == "ushort": + img = convert_16to8_bit(img) + warped_img = warp_new_slide( + img, + registrar, + source_img=reference_path, + dst_img=reference_path, + ) + warped_slides.append(warped_img) + names.append(os.path.basename(args.reference)) + + write_ome_tiff( + images=warped_slides, + names=names, + output_path=os.path.join(args.output_dir, "aligned.ome.tif"), + um_per_px=0.2, + scale=1, + ) + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/bin/compose.py b/tma_alignment_pipeline/bin/compose.py new file mode 100755 index 00000000..70cc270e --- /dev/null +++ b/tma_alignment_pipeline/bin/compose.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +"""Compose aligned TMA crops back onto a blank morphology-sized canvas.""" + +import argparse +import glob +import json +import os +import re +import pyvips + + +def main(): + p = argparse.ArgumentParser() + p.add_argument( + "--morphology", + required=True, + help="reference morphology OME-TIFF; canvas size taken from its header", + ) + p.add_argument("--morphology-boxes", required=True) + p.add_argument( + "--aligned-dir", + required=True, + help="dir containing aligned_tma_.ome.tif files", + ) + p.add_argument("--out", required=True) + args = p.parse_args() + + with open(args.morphology_boxes) as f: + boxes = json.load(f) + + morph = pyvips.Image.new_from_file(args.morphology) + img_w, img_h = morph.width, morph.height + print(f"Canvas: {img_w}x{img_h}") + + canvas = pyvips.Image.black(img_w, img_h, bands=3).cast("uchar") + + pattern = re.compile(r"aligned_tma_(\d+)\.ome\.tif$") + aligned = {} + for fp in glob.glob(os.path.join(args.aligned_dir, "aligned_tma_*.ome.tif")): + m = pattern.search(os.path.basename(fp)) + if m: + aligned[m.group(1)] = fp + + for tma_id in sorted(boxes.keys(), key=int): + bbox = boxes[tma_id] + fp = aligned.get(tma_id) + if not fp or not os.path.exists(fp): + print(f" TMA {tma_id}: aligned file missing, skipping") + continue + warped = pyvips.Image.new_from_file(fp, page=0) + if warped.bands == 1: + warped = warped.bandjoin([warped, warped]) + elif warped.bands > 3: + warped = warped.extract_band(0, n=3) + x, y = bbox["x"], bbox["y"] + w = min(warped.width, img_w - x) + h = min(warped.height, img_h - y) + if w != warped.width or h != warped.height: + warped = warped.crop(0, 0, w, h) + canvas = canvas.insert(warped, x, y) + print(f" TMA {tma_id}: inserted at ({x},{y})") + + canvas.tiffsave( + args.out, + bigtiff=True, + tile=True, + pyramid=True, + tile_width=256, + tile_height=256, + compression="lzw", + ) + print(f"Wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/bin/crop_tma.py b/tma_alignment_pipeline/bin/crop_tma.py new file mode 100755 index 00000000..26884856 --- /dev/null +++ b/tma_alignment_pipeline/bin/crop_tma.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +"""Crop one TMA core out of a whole-slide image given a bbox JSON entry.""" + +import argparse +import json +import pyvips + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--image", required=True) + p.add_argument("--boxes", required=True) + p.add_argument("--tma-id", required=True) + p.add_argument("--out", required=True) + args = p.parse_args() + + with open(args.boxes) as f: + boxes = json.load(f) + b = boxes[str(args.tma_id)] + img = pyvips.Image.new_from_file(args.image) + crop = img.crop(b["x"], b["y"], b["w"], b["h"]) + crop.tiffsave(args.out, compression="none", bigtiff=True, strip=True) + print(f"Wrote {args.out} ({b['w']}x{b['h']})") + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/bin/extract_morphology_boxes.py b/tma_alignment_pipeline/bin/extract_morphology_boxes.py new file mode 100755 index 00000000..1bd81375 --- /dev/null +++ b/tma_alignment_pipeline/bin/extract_morphology_boxes.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +"""Extract morphology TMA bounding boxes from the geoparquet file.""" + +import argparse +import json +import geopandas as gpd +import pyvips + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--parquet", required=True) + p.add_argument( + "--morphology", + required=True, + help="morphology OME-TIFF; width/height read from its header", + ) + p.add_argument("--um-per-px", type=float, default=0.2125) + p.add_argument("--pad", type=int, default=200) + p.add_argument("--out", required=True) + args = p.parse_args() + + morph = pyvips.Image.new_from_file(args.morphology) + img_w, img_h = morph.width, morph.height + print(f"Morphology image: {img_w}x{img_h}") + + gdf = gpd.read_parquet(args.parquet) + boxes = {} + for _, row in gdf.iterrows(): + tma_id = int(row["tma_id"]) + minx, miny, maxx, maxy = row.geometry.bounds + x_orig = int(minx / args.um_per_px) - args.pad + y_orig = int(miny / args.um_per_px) - args.pad + x1 = int(maxx / args.um_per_px) + args.pad + y1 = int(maxy / args.um_per_px) + args.pad + x = max(0, x_orig) + y = max(0, y_orig) + x1 = min(img_w, x1) + y1 = min(img_h, y1) + boxes[tma_id] = { + "x": x, + "y": y, + "w": x1 - x, + "h": y1 - y, + "x_orig": x_orig, + "y_orig": y_orig, + } + + with open(args.out, "w") as f: + json.dump(boxes, f, indent=2) + print(f"Wrote {len(boxes)} bboxes to {args.out}") + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/bin/match_cores.py b/tma_alignment_pipeline/bin/match_cores.py new file mode 100755 index 00000000..38710475 --- /dev/null +++ b/tma_alignment_pipeline/bin/match_cores.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +""" +Detect tissue core centroids in the H&E whole-slide image and match them +by grid rank to the morphology TMA centroids from the geoparquet, then +emit an hne_tma_boxes.json sized to each core's detected circle radius. +""" + +import argparse +import json +import os + +import cv2 +import geopandas as gpd +import numpy as np +import pyvips + + +def gap_grid_sort(pts, n_rows): + sorted_items = sorted(pts.items(), key=lambda kv: kv[1][1]) + ids = [item[0] for item in sorted_items] + y_vals = np.array([item[1][1] for item in sorted_items]) + gaps = np.diff(y_vals) + split_positions = sorted(np.argsort(gaps)[::-1][: n_rows - 1] + 1) + rows, prev = [], 0 + for pos in split_positions: + rows.append(ids[prev:pos]) + prev = pos + rows.append(ids[prev:]) + order = [] + for row in rows: + order.extend(sorted(row, key=lambda i: pts[i][0])) + return order + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--parquet", required=True) + p.add_argument("--he-image", required=True) + p.add_argument("--morphology-boxes", required=True) + p.add_argument("--um-per-px", type=float, default=0.2125) + p.add_argument( + "--n-rows", + type=int, + default=10, + help="number of TMA rows expected on the slide", + ) + p.add_argument("--target-thumb-height", type=int, default=3000) + p.add_argument("--pad", type=int, default=150) + p.add_argument("--min-area", type=int, default=1500) + p.add_argument("--out", required=True) + p.add_argument("--mask-debug", default=None) + args = p.parse_args() + + gdf = gpd.read_parquet(args.parquet) + morph_pts = {} + for _, row in gdf.iterrows(): + tid = int(row["tma_id"]) + morph_pts[tid] = ( + row.geometry.centroid.x / args.um_per_px, + row.geometry.centroid.y / args.um_per_px, + ) + print(f"Morphology: {len(morph_pts)} centroids") + + he = pyvips.Image.new_from_file(args.he_image) + he_w, he_h = he.width, he.height + scale = args.target_thumb_height / he_h + thumb = he.resize(scale) + arr = thumb.numpy() + if arr.ndim == 3: + gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY) + else: + gray = arr + + _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) + mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k) + mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, k) + if args.mask_debug: + cv2.imwrite(args.mask_debug, mask) + + cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + es_spots = [] + for cnt in cnts: + area = cv2.contourArea(cnt) + if area < args.min_area: + continue + M = cv2.moments(cnt) + if M["m00"] == 0: + continue + cx_full = (M["m10"] / M["m00"]) / scale + cy_full = (M["m01"] / M["m00"]) / scale + _, radius_thumb = cv2.minEnclosingCircle(cnt) + es_spots.append((cx_full, cy_full, radius_thumb / scale)) + print(f"H&E: {len(es_spots)} spots (need {len(morph_pts)})") + + if len(es_spots) != len(morph_pts): + raise SystemExit( + f"core count mismatch — adjust --min-area or --target-thumb-height " + f"(detected {len(es_spots)}, expected {len(morph_pts)})" + ) + + es_pts = {i: (s[0], s[1]) for i, s in enumerate(es_spots)} + es_radii = {i: s[2] for i, s in enumerate(es_spots)} + + morph_order = gap_grid_sort(morph_pts, n_rows=args.n_rows) + es_order = gap_grid_sort(es_pts, n_rows=args.n_rows) + tma_to_es = {morph_order[i]: es_order[i] for i in range(len(morph_order))} + + with open(args.morphology_boxes) as f: + morph_boxes = json.load(f) + + hne_boxes = {} + for tma_id_str in morph_boxes: + tid = int(tma_id_str) + es_idx = tma_to_es[tid] + ex, ey = es_pts[es_idx] + r = es_radii[es_idx] + args.pad + x = max(0, int(ex - r)) + y = max(0, int(ey - r)) + x1 = min(he_w, int(ex + r)) + y1 = min(he_h, int(ey + r)) + hne_boxes[tma_id_str] = {"x": x, "y": y, "w": x1 - x, "h": y1 - y} + + with open(args.out, "w") as f: + json.dump(hne_boxes, f, indent=2) + print(f"Wrote {len(hne_boxes)} boxes to {args.out}") + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/bin/verify_crops.py b/tma_alignment_pipeline/bin/verify_crops.py new file mode 100755 index 00000000..9380e6e8 --- /dev/null +++ b/tma_alignment_pipeline/bin/verify_crops.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +"""Side-by-side thumbnail of one TMA's morphology crop vs. its H&E crop.""" + +import argparse +import json +import os + +import cv2 +import numpy as np +import pyvips + + +def crop_thumb(img, box, thumb_h): + crop = img.crop(box["x"], box["y"], box["w"], box["h"]) + scale = thumb_h / box["h"] + arr = crop.resize(scale).numpy() + if arr.ndim == 2 or arr.shape[2] == 1: + arr = arr.squeeze() + lo = np.percentile(arr[arr > 0], 1) if arr.max() > 0 else 0 + hi = np.percentile(arr[arr > 0], 99.9) if arr.max() > 0 else 255 + arr = np.clip( + (arr.astype(np.float32) - lo) / max(hi - lo, 1) * 255, 0, 255 + ).astype(np.uint8) + arr = cv2.cvtColor(arr, cv2.COLOR_GRAY2BGR) + else: + arr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR) + return arr + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--morphology", required=True) + p.add_argument("--he", required=True) + p.add_argument("--morphology-boxes", required=True) + p.add_argument("--he-boxes", required=True) + p.add_argument("--tma-id", required=True) + p.add_argument("--thumb-height", type=int, default=400) + p.add_argument("--out", required=True) + args = p.parse_args() + + with open(args.morphology_boxes) as f: + mb = json.load(f)[str(args.tma_id)] + with open(args.he_boxes) as f: + eb = json.load(f)[str(args.tma_id)] + + morph_img = pyvips.Image.new_from_file(args.morphology) + he_img = pyvips.Image.new_from_file(args.he) + + m = crop_thumb(morph_img, mb, args.thumb_height) + e = crop_thumb(he_img, eb, args.thumb_height) + + max_h = max(m.shape[0], e.shape[0]) + if m.shape[0] < max_h: + m = np.pad(m, ((0, max_h - m.shape[0]), (0, 0), (0, 0))) + if e.shape[0] < max_h: + e = np.pad(e, ((0, max_h - e.shape[0]), (0, 0), (0, 0))) + + cv2.putText( + m, + f"TMA {args.tma_id} morphology", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1.0, + (0, 0, 255), + 2, + cv2.LINE_AA, + ) + cv2.putText( + e, + f"TMA {args.tma_id} H&E", + (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1.0, + (0, 0, 255), + 2, + cv2.LINE_AA, + ) + + div = np.full((max_h, 4, 3), 255, dtype=np.uint8) + panel = np.hstack([m, div, e]) + cv2.imwrite(args.out, panel) + print(f"Wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/tma_alignment_pipeline/main.nf b/tma_alignment_pipeline/main.nf new file mode 100644 index 00000000..7b0f45b9 --- /dev/null +++ b/tma_alignment_pipeline/main.nf @@ -0,0 +1,210 @@ +#!/usr/bin/env nextflow + +nextflow.enable.dsl = 2 + +/* + * TMA alignment pipeline: + * 1. extract morphology bboxes from a TMA-boundaries geoparquet + * 2. detect H&E core centroids and match them by grid rank to morphology TMAs + * 3. per-TMA: crop both modalities and run Valis pairwise alignment + * 4. compose all aligned crops back onto a morphology-sized canvas + */ + +params.parquet = null +params.morphology = null // morphology OME-TIFF (reference) +params.he = null // H&E OME-TIFF (moving) +params.outdir = "results" + +params.um_per_px = 0.2125 +params.pad = 200 +params.he_pad = 150 +params.n_rows = 10 +params.thumb_height = 3000 +params.min_area = 1500 +params.verify_thumb_height = 400 + +params.valis_python = "/data1/tanseyw/quinnj2/conda_environments/valis/bin/python" +params.max_processed_dim = 1024 +params.reference_stain = "inverted-fluorescence" +params.image_stain = "he-hematoxylin-sparse" + + +process EXTRACT_MORPHOLOGY_BOXES { + publishDir "${params.outdir}", mode: 'copy' + + input: + path parquet + path morphology + + output: + path "morphology_boxes.json", emit: boxes + + script: + """ + extract_morphology_boxes.py \\ + --parquet ${parquet} \\ + --morphology ${morphology} \\ + --um-per-px ${params.um_per_px} \\ + --pad ${params.pad} \\ + --out morphology_boxes.json + """ +} + + +process MATCH_CORES { + publishDir "${params.outdir}", mode: 'copy' + + input: + path parquet + path he + path morphology_boxes + + output: + path "hne_tma_boxes.json", emit: boxes + path "he_mask.png", optional: true + + script: + """ + match_cores.py \\ + --parquet ${parquet} \\ + --he-image ${he} \\ + --morphology-boxes ${morphology_boxes} \\ + --um-per-px ${params.um_per_px} \\ + --n-rows ${params.n_rows} \\ + --target-thumb-height ${params.thumb_height} \\ + --pad ${params.he_pad} \\ + --min-area ${params.min_area} \\ + --out hne_tma_boxes.json \\ + --mask-debug he_mask.png + """ +} + + +process VERIFY_CROP_MATCHING { + tag "tma_${tma_id}" + publishDir "${params.outdir}/verify_crops", mode: 'copy' + + input: + tuple val(tma_id), + path(morphology), + path(he), + path(morphology_boxes), + path(hne_tma_boxes) + + output: + path "tma_${tma_id}.png" + + script: + """ + verify_crops.py \\ + --morphology ${morphology} \\ + --he ${he} \\ + --morphology-boxes ${morphology_boxes} \\ + --he-boxes ${hne_tma_boxes} \\ + --tma-id ${tma_id} \\ + --thumb-height ${params.verify_thumb_height} \\ + --out tma_${tma_id}.png + """ +} + + +process CROP_AND_ALIGN { + tag "tma_${tma_id}" + publishDir "${params.outdir}/aligned", mode: 'copy', pattern: "aligned_tma_*.ome.tif" + publishDir "${params.outdir}/logs", mode: 'copy', pattern: "align_tma_*.log" + + input: + tuple val(tma_id), + path(morphology), + path(he), + path(morphology_boxes), + path(hne_tma_boxes) + + output: + path "aligned_tma_${tma_id}.ome.tif", emit: aligned, optional: true + path "align_tma_${tma_id}.log", emit: log + + script: + """ + set -o pipefail + + crop_tma.py \\ + --image ${morphology} \\ + --boxes ${morphology_boxes} \\ + --tma-id ${tma_id} \\ + --out morph_tma_${tma_id}.tif + + crop_tma.py \\ + --image ${he} \\ + --boxes ${hne_tma_boxes} \\ + --tma-id ${tma_id} \\ + --out he_tma_${tma_id}.tif + + mkdir -p aligned_${tma_id} + ${params.valis_python} \$(which align_two_images.py) \\ + --reference morph_tma_${tma_id}.tif \\ + --image he_tma_${tma_id}.tif \\ + --output-dir aligned_${tma_id} \\ + --max-processed-image-dim-px ${params.max_processed_dim} \\ + --reference-stain ${params.reference_stain} \\ + --image-stain ${params.image_stain} \\ + --no-script-orientation \\ + > align_tma_${tma_id}.log 2>&1 + + if [ -f aligned_${tma_id}/aligned.ome.tif ]; then + mv aligned_${tma_id}/aligned.ome.tif aligned_tma_${tma_id}.ome.tif + fi + """ +} + + +process COMPOSE { + publishDir "${params.outdir}", mode: 'copy' + + input: + path morphology + path morphology_boxes + path aligned_tifs + + output: + path "aligned_to_morphology.ome.tif" + + script: + """ + mkdir -p aligned_in + for f in ${aligned_tifs}; do ln -sf \$PWD/\$f aligned_in/; done + + compose.py \\ + --morphology ${morphology} \\ + --morphology-boxes ${morphology_boxes} \\ + --aligned-dir aligned_in \\ + --out aligned_to_morphology.ome.tif + """ +} + + +workflow { + if (!params.parquet || !params.morphology || !params.he) { + error "Required: --parquet, --morphology, --he" + } + + parquet_ch = Channel.fromPath(params.parquet, checkIfExists: true) + morphology_ch = Channel.fromPath(params.morphology, checkIfExists: true) + he_ch = Channel.fromPath(params.he, checkIfExists: true) + + morph_boxes = EXTRACT_MORPHOLOGY_BOXES(parquet_ch, morphology_ch).boxes + hne_boxes = MATCH_CORES(parquet_ch, he_ch, morph_boxes).boxes + + tma_ids = morph_boxes + .map { f -> new groovy.json.JsonSlurper().parse(f).keySet().collect { it as String } } + .flatten() + + per_tma = tma_ids.combine(morphology_ch).combine(he_ch) + .combine(morph_boxes).combine(hne_boxes) + + VERIFY_CROP_MATCHING(per_tma) + + aligned = CROP_AND_ALIGN(per_tma).aligned + + COMPOSE(morphology_ch, morph_boxes, aligned.collect()) +} diff --git a/tma_alignment_pipeline/nextflow.config b/tma_alignment_pipeline/nextflow.config new file mode 100644 index 00000000..d8474b43 --- /dev/null +++ b/tma_alignment_pipeline/nextflow.config @@ -0,0 +1,49 @@ +// Nextflow config for the TMA alignment pipeline. +// `bin/` scripts are placed on PATH automatically by Nextflow. + +params { + publish_mode = 'copy' +} + +process { + // Default: use the local environment (assumes pyvips, geopandas, opencv, numpy on PATH). + // Override per-process with conda / container if desired. + executor = 'local' + cpus = 2 + memory = '8 GB' + + withName: CROP_AND_ALIGN { + cpus = 4 + memory = '32 GB' + // Valis alignment is the heavy step — limit parallelism if needed: + // maxForks = 4 + } + + withName: COMPOSE { + cpus = 2 + memory = '16 GB' + } +} + +profiles { + standard { + process.executor = 'local' + } + + lsf { + process.executor = 'lsf' + process.queue = 'cpuqueue' + } +} + +report { + enabled = true + file = "${params.outdir}/nextflow_report.html" + overwrite = true +} + +trace { + enabled = true + file = "${params.outdir}/nextflow_trace.txt" + overwrite = true +} diff --git a/uv.lock b/uv.lock index e40c1b0a..711e4c4d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,19 +1,19 @@ version = 1 -revision = 1 -requires-python = ">=3.9" +revision = 3 +requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -23,799 +23,669 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/14/8b158b16168e3158220d942cf3024011e0de111eb58ef18a68ce20e093c4/aicspylibczi-3.3.1.tar.gz", hash = "sha256:e3d18daf92c4de6e91d37a33a43b83611d3268cadf8a610c2f3eae7f54408ba3", size = 7928980 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/87/56823eb22ede8f5f40b0503776fa1daab30d241f28807e71af71863f1eb1/aicspylibczi-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:34b47e53cdcb06ba7147059807d36078f238c3cba02b6bb2859c029f0214a0d5", size = 1398430 }, - { url = "https://files.pythonhosted.org/packages/4d/0a/b7861e131126db90a44e6c25047cf3301e472737530f6170da67920c2750/aicspylibczi-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49e5735a19f3797b7f0fc138cccd4fd7b7e5a063cf0f557e2c50714ed9f74040", size = 761394 }, - { url = "https://files.pythonhosted.org/packages/a8/81/8a390514f978fe669997401f5ff5279c0700591a981fcdfe6d18495bf72b/aicspylibczi-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:512101699cd5ac35a089c9abc41e6119e755b6382fc4310355a414d93d041308", size = 661751 }, - { url = "https://files.pythonhosted.org/packages/46/5c/638a78b5f97a509eb771b92e7347b4bfb12c05af72d94263db3d81a7e695/aicspylibczi-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f0914218ad0e3b977373189417001b9aadcdabb1921576afae39c4eb2caab83", size = 1110714 }, - { url = "https://files.pythonhosted.org/packages/8d/22/7a4f5d07ada4e7a5be2f9e2a69d5866ee05e95a9edc7f3f2351523eb8e4a/aicspylibczi-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ee13d25a07accef9b30dcef319cd4f87f221e737283095f9f8f55a90bfaaa13", size = 557790 }, - { url = "https://files.pythonhosted.org/packages/96/d8/a838093e7ba25caf85830bbb9e8b0bfd4d9f84b86e8ce7f871f8b15883c2/aicspylibczi-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:72fbfb14438e90baac7f76059804da60af254d790f3f0f9670d692e3cabbb97e", size = 1401014 }, - { url = "https://files.pythonhosted.org/packages/a2/35/4d72c6d88b7f0bd1a50fbfaa5eb805deda616b186402e76c6e80c4556d4c/aicspylibczi-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca4ae922a906f81ff981d1ec74093354f38d5d93bd16350a1bb3f742ac786ca8", size = 762464 }, - { url = "https://files.pythonhosted.org/packages/4b/46/9f3ef3c84022d8aaa13576e05ca3c1b50554dcfc3bb3d9c08922beaeda9f/aicspylibczi-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e11b04c05d479ebeb3946ba11b725ed013a6b9e2edcf1f7f94d9e84ec103a0c3", size = 663132 }, - { url = "https://files.pythonhosted.org/packages/62/7a/470f73b8fde2d520adc0a2ed51191383a968d0b1067fa602101b676890ba/aicspylibczi-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbbe4915763cefc4e386316ed68b6a006c50ba8e18cfbd4d0252a6e3f745220", size = 1112364 }, - { url = "https://files.pythonhosted.org/packages/9b/8a/320ffccd5662a93e7f90bcab04ab21e033760012f57688d2e9d47fb5086e/aicspylibczi-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b94f2ccb9d19b2ba98875094af0e877910150f923d5d223c43234e133a0003f2", size = 558988 }, - { url = "https://files.pythonhosted.org/packages/9e/8c/a02e1ba30b72d81ba760f9895d5a81c7cda8d82bea2b125bd7ae3e89c467/aicspylibczi-3.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e11476656b50d6f3cc2887bb1ea74dba7c605296bd04dba207c4e9c134ca554", size = 1401705 }, - { url = "https://files.pythonhosted.org/packages/18/89/e173dbf1fad9b6905c49821db449dcf9e3256cb2c85a4a59d1b7343ee216/aicspylibczi-3.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2697bc3ece509169842b0ccb2fff98c35f3896005085dd183dfc1535202f9ca", size = 762806 }, - { url = "https://files.pythonhosted.org/packages/7b/d0/34c3ccd12bdef62f6933fa0455633dcf1381a354fe835fd3e99c7498b449/aicspylibczi-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8622e052261d6d33c8988b0d3d6f996123a98e66410ca2bd7e1a50cbce8a194b", size = 663296 }, - { url = "https://files.pythonhosted.org/packages/f8/8e/6441991722b9bb6b5bd591da7889a8f518413276332bb765dfe8e484b224/aicspylibczi-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a67e01d308eae32d76dc1826dff4d933d19aaeb2533cea9f5a4d7d286e2e9d", size = 1111428 }, - { url = "https://files.pythonhosted.org/packages/91/3d/0bff6bd768c517a3535a2a595e02f54f9c3f9662a40ce52e9ca8ce476e46/aicspylibczi-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:d67053d147cfe7da684d052a8ccae3d22b6264870f0cbed95ac2bb82c30ef07d", size = 559479 }, - { url = "https://files.pythonhosted.org/packages/2f/6e/8ab7acd26abb660b81c592e1aa7787757c9422f95b3ee54aca34357ab332/aicspylibczi-3.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:baf0ee951283a7e70d706eb97338d9756711d392f3bede6db9700401f6f7f02d", size = 1401905 }, - { url = "https://files.pythonhosted.org/packages/dc/79/bf8113c52c75cbea0f01ba8e0a3f1dfd55ccbbcdc7b80ae065528adf7c71/aicspylibczi-3.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03c5b0375e6cbccbff15c8fe7a00e65fbded3140bb6ad0c15538d1a9344112d5", size = 762841 }, - { url = "https://files.pythonhosted.org/packages/65/4a/3cb65f83b43dd8f5212a375e968089c2570d1aacff8cdda784e820ded94a/aicspylibczi-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bea539f6023a0f7293a036fc78711272f90a43d9f529afef0a44b68046f5ae54", size = 663315 }, - { url = "https://files.pythonhosted.org/packages/42/19/ec14b688e0e3bbd5152f24fc8ea064b12d8c0252d4ce498b948a5c50e8f7/aicspylibczi-3.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aa611540f0b3ce463aa4f8194217fdc5ba12d807cdd408fd10637695fd50dfe", size = 1112132 }, - { url = "https://files.pythonhosted.org/packages/56/9b/661854e4f86be0c851552fe2805655236590c846f53143ec8e53d3f11156/aicspylibczi-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:b53991e2d993962593f2cc9ad64d235d86a4531dae23b9467e4e02002bdc3ea1", size = 559454 }, - { url = "https://files.pythonhosted.org/packages/50/02/1d112701e5626af0a6de926201d8e7ae493c7c9425127733853e1fdba6ef/aicspylibczi-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae84316ffff39ad5f36f67ec974ec2c2db00127e636c5fdefb11e375562765", size = 1398566 }, - { url = "https://files.pythonhosted.org/packages/17/20/21a99185fb5cb1740b5c241fa6cfff1364134c1a80c320548e83499ad1d3/aicspylibczi-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4aa4eb2478daf3eced4fc21789d384ea026530c788b01e2e74a9bee37e8a819", size = 761442 }, - { url = "https://files.pythonhosted.org/packages/2e/b5/83d0a03428b6b15346ccd61126f97ccf06c89fc802e81a19c6d3ca6e77bb/aicspylibczi-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:36d262640e7670b7be46479b30166ee502b15329a203748a0282c5ddb74b426a", size = 661871 }, - { url = "https://files.pythonhosted.org/packages/e8/5d/076b46f307179fb0db6875e38107c3bddff983401d1f089cc27932f3a5bd/aicspylibczi-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7cf364dbcb7a1a4ca5b99d97a5eeb1f6dc9264fb963756e112eeed4bd1fd798", size = 1111094 }, - { url = "https://files.pythonhosted.org/packages/8a/6d/e490d170db9c27b5959998cec869e11cdf22ee677dbe51e6c2572f26d478/aicspylibczi-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:0ff77eb88967622b5c92f9b9768ab5579a370ef69f7cf417076a25d09954ac95", size = 558007 }, -] - -[[package]] -name = "alabaster" -version = "0.7.16" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/14/8b158b16168e3158220d942cf3024011e0de111eb58ef18a68ce20e093c4/aicspylibczi-3.3.1.tar.gz", hash = "sha256:e3d18daf92c4de6e91d37a33a43b83611d3268cadf8a610c2f3eae7f54408ba3", size = 7928980, upload-time = "2025-04-14T15:59:12.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, + { url = "https://files.pythonhosted.org/packages/96/d8/a838093e7ba25caf85830bbb9e8b0bfd4d9f84b86e8ce7f871f8b15883c2/aicspylibczi-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:72fbfb14438e90baac7f76059804da60af254d790f3f0f9670d692e3cabbb97e", size = 1401014, upload-time = "2025-04-14T15:58:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/35/4d72c6d88b7f0bd1a50fbfaa5eb805deda616b186402e76c6e80c4556d4c/aicspylibczi-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca4ae922a906f81ff981d1ec74093354f38d5d93bd16350a1bb3f742ac786ca8", size = 762464, upload-time = "2025-04-14T15:58:42.017Z" }, + { url = "https://files.pythonhosted.org/packages/4b/46/9f3ef3c84022d8aaa13576e05ca3c1b50554dcfc3bb3d9c08922beaeda9f/aicspylibczi-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e11b04c05d479ebeb3946ba11b725ed013a6b9e2edcf1f7f94d9e84ec103a0c3", size = 663132, upload-time = "2025-04-14T15:58:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/7a/470f73b8fde2d520adc0a2ed51191383a968d0b1067fa602101b676890ba/aicspylibczi-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbbe4915763cefc4e386316ed68b6a006c50ba8e18cfbd4d0252a6e3f745220", size = 1112364, upload-time = "2025-04-14T15:58:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/320ffccd5662a93e7f90bcab04ab21e033760012f57688d2e9d47fb5086e/aicspylibczi-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b94f2ccb9d19b2ba98875094af0e877910150f923d5d223c43234e133a0003f2", size = 558988, upload-time = "2025-04-14T15:58:46.142Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a02e1ba30b72d81ba760f9895d5a81c7cda8d82bea2b125bd7ae3e89c467/aicspylibczi-3.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e11476656b50d6f3cc2887bb1ea74dba7c605296bd04dba207c4e9c134ca554", size = 1401705, upload-time = "2025-04-14T15:58:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/18/89/e173dbf1fad9b6905c49821db449dcf9e3256cb2c85a4a59d1b7343ee216/aicspylibczi-3.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2697bc3ece509169842b0ccb2fff98c35f3896005085dd183dfc1535202f9ca", size = 762806, upload-time = "2025-04-14T15:58:52.041Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d0/34c3ccd12bdef62f6933fa0455633dcf1381a354fe835fd3e99c7498b449/aicspylibczi-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8622e052261d6d33c8988b0d3d6f996123a98e66410ca2bd7e1a50cbce8a194b", size = 663296, upload-time = "2025-04-14T15:58:53.793Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/6441991722b9bb6b5bd591da7889a8f518413276332bb765dfe8e484b224/aicspylibczi-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a67e01d308eae32d76dc1826dff4d933d19aaeb2533cea9f5a4d7d286e2e9d", size = 1111428, upload-time = "2025-04-14T15:58:55.183Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/0bff6bd768c517a3535a2a595e02f54f9c3f9662a40ce52e9ca8ce476e46/aicspylibczi-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:d67053d147cfe7da684d052a8ccae3d22b6264870f0cbed95ac2bb82c30ef07d", size = 559479, upload-time = "2025-04-14T15:58:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6e/8ab7acd26abb660b81c592e1aa7787757c9422f95b3ee54aca34357ab332/aicspylibczi-3.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:baf0ee951283a7e70d706eb97338d9756711d392f3bede6db9700401f6f7f02d", size = 1401905, upload-time = "2025-04-14T15:58:57.862Z" }, + { url = "https://files.pythonhosted.org/packages/dc/79/bf8113c52c75cbea0f01ba8e0a3f1dfd55ccbbcdc7b80ae065528adf7c71/aicspylibczi-3.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03c5b0375e6cbccbff15c8fe7a00e65fbded3140bb6ad0c15538d1a9344112d5", size = 762841, upload-time = "2025-04-14T15:58:59.214Z" }, + { url = "https://files.pythonhosted.org/packages/65/4a/3cb65f83b43dd8f5212a375e968089c2570d1aacff8cdda784e820ded94a/aicspylibczi-3.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bea539f6023a0f7293a036fc78711272f90a43d9f529afef0a44b68046f5ae54", size = 663315, upload-time = "2025-04-14T15:59:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/42/19/ec14b688e0e3bbd5152f24fc8ea064b12d8c0252d4ce498b948a5c50e8f7/aicspylibczi-3.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aa611540f0b3ce463aa4f8194217fdc5ba12d807cdd408fd10637695fd50dfe", size = 1112132, upload-time = "2025-04-14T15:59:02.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/9b/661854e4f86be0c851552fe2805655236590c846f53143ec8e53d3f11156/aicspylibczi-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:b53991e2d993962593f2cc9ad64d235d86a4531dae23b9467e4e02002bdc3ea1", size = 559454, upload-time = "2025-04-14T15:59:04.153Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "beautifulsoup4" -version = "4.13.4" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] name = "bounded-pool-executor" version = "0.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f1/e34501c1228415e9fbcac8cb9c81098900e78331b30eeee1816176324bab/bounded_pool_executor-0.0.3.tar.gz", hash = "sha256:e092221bc38ade555e1064831f9ed800580fa34a4b6d8e9dd3cd961549627f6e", size = 2238 } +sdist = { url = "https://files.pythonhosted.org/packages/23/f1/e34501c1228415e9fbcac8cb9c81098900e78331b30eeee1816176324bab/bounded_pool_executor-0.0.3.tar.gz", hash = "sha256:e092221bc38ade555e1064831f9ed800580fa34a4b6d8e9dd3cd961549627f6e", size = 2238, upload-time = "2019-06-04T19:29:06.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/23/72ecfe284a1da711257ff310b29c6667d0187a608322d58bf1c7a927c7b2/bounded_pool_executor-0.0.3-py3-none-any.whl", hash = "sha256:6f164d64919db1e6a5c187cce281f62bc559a5fed4ce064942e650c227aef190", size = 3371 }, + { url = "https://files.pythonhosted.org/packages/bc/23/72ecfe284a1da711257ff310b29c6667d0187a608322d58bf1c7a927c7b2/bounded_pool_executor-0.0.3-py3-none-any.whl", hash = "sha256:6f164d64919db1e6a5c187cce281f62bc559a5fed4ce064942e650c227aef190", size = 3371, upload-time = "2019-06-04T19:29:05.152Z" }, ] [[package]] name = "certifi" -version = "2025.4.26" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, -] - -[[package]] -name = "cjdk" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "progressbar2" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/bd/175d20937665964799cf16651def9d279d7eba6403c6f5f9e5268db567fd/cjdk-0.4.1.tar.gz", hash = "sha256:908c93f305c14e9e9aca52512b8ab20f78c764928201fb2960c4d68dd9ad8735", size = 38437 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9d/d8090552e532684f3881e2843f9defb1c664ecd905034f1d0e15695fa052/cjdk-0.4.1-py3-none-any.whl", hash = "sha256:51fb2456c39cf7016791382f7adeb2b722ff48973d18c17fd4e6166f4f14b961", size = 22462 }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "colour-science" -version = "0.4.4" +version = "0.4.7" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "imageio", marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/5e/0580628abe6011ff0784a998fc947af50eb12f905bea604d61f1910af922/colour_science-0.4.4.tar.gz", hash = "sha256:a3cb3b8e8a51db82b62524173d65ae70396bfa943636e111e50fb7cc125857ad", size = 1878054 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/66/6e2216ffae6f0008118c99d6f425f7591e688671ab99f5636f7f65021395/colour_science-0.4.7.tar.gz", hash = "sha256:b34773dc4dd3f9ba99cca5297fa10e9f532134d994b3a2308dacbe970dfd5079", size = 13283171, upload-time = "2025-12-06T08:15:08.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a7/b8fc90dfcfa3968a4ce9d74e185e60dcd7884310f59c7161deb5a08c0c21/colour_science-0.4.4-py3-none-any.whl", hash = "sha256:07d8ae6da6a6f745a83a32df2686faace8a9f7baf7da61dbb6ca9a61f7671bdb", size = 2283236 }, + { url = "https://files.pythonhosted.org/packages/6d/9e/ce54e33f383d7e1432be9fb39557477779ccb155c60033c491609936dacf/colour_science-0.4.7-py3-none-any.whl", hash = "sha256:fe2383d86e507fc11a4b5206ff31a7399031e0dce527e22ec06eeb491d45d9a7", size = 9065613, upload-time = "2025-12-06T08:15:14.908Z" }, ] [[package]] -name = "colour-science" -version = "0.4.6" +name = "contourpy" +version = "1.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/29/4ea0082b8ad8c5e18b9ac7cf7ad5f21b70e10976ed53b877773ead3c268d/colour_science-0.4.6.tar.gz", hash = "sha256:be98c2c9b2a5caf0c443431f402599ca9e1cc7d944bb804156803bcc97af4cf0", size = 2228183 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/3e/7a39e00d11a58ab6aa75985433ad4b8001eabf965c20280ed22ed1512887/colour_science-0.4.6-py3-none-any.whl", hash = "sha256:4cd90e6d500c16f3c24225da57031e1944de52fec6b484f5bb3d4ea7d0cfee08", size = 2480689 }, +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] -name = "contourpy" -version = "1.3.0" +name = "cryptography" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, - { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879 }, - { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573 }, - { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184 }, - { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262 }, - { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806 }, - { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710 }, - { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107 }, - { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458 }, - { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643 }, - { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301 }, - { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972 }, - { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375 }, - { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188 }, - { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644 }, - { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141 }, - { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469 }, - { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894 }, - { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829 }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518 }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350 }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167 }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279 }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519 }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922 }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017 }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773 }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353 }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817 }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894 }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099 }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, ] [[package]] -name = "contourpy" -version = "1.3.2" +name = "cuda-bindings" +version = "12.9.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, ] [[package]] -name = "cryptography" -version = "45.0.4" +name = "cuda-pathfinder" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335 }, - { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487 }, - { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922 }, - { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433 }, - { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163 }, - { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687 }, - { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623 }, - { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447 }, - { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830 }, - { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746 }, - { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456 }, - { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495 }, - { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540 }, - { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052 }, - { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024 }, - { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442 }, - { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038 }, - { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964 }, - { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732 }, - { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424 }, - { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438 }, - { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899 }, - { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900 }, - { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422 }, - { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475 }, +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/d6/ac63065d33dd700fee7ebd7d287332401b54e31b9346e142f871e1f0b116/cuda_pathfinder-1.5.3-py3-none-any.whl", hash = "sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d", size = 49991, upload-time = "2026-04-14T20:09:27.037Z" }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "docutils" -version = "0.21.2" +version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "einops" -version = "0.8.1" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359 }, + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, ] [[package]] name = "et-xmlfile" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] [[package]] name = "fastcluster" -version = "1.2.6" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/b8/f143d907d93bd4a3dd51d07c4e79b37bedbfc2177f4949bfa0d6ba0af647/fastcluster-1.2.6.tar.gz", hash = "sha256:aab886efa7b6bba7ac124f4498153d053e5a08b822d2254926b7206cdf5a8aa6", size = 173773 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/ec/7b1632cdebb48118a3c90bec2e180542718aab251f5fb6c48e639a45e12c/fastcluster-1.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d0e8faef0437a25fd083df70fb86cc65ce3c9c9780d4ae377cbe6521e7746ce0", size = 67631 }, - { url = "https://files.pythonhosted.org/packages/6a/63/4b415e0f176ff01b9435a86dfd8a057133c2a7a4f35d63d358c3430f342b/fastcluster-1.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8be01f97bc2bf11a9188537864f8e520e1103cdc6007aa2c5d7979b1363b121", size = 40128 }, - { url = "https://files.pythonhosted.org/packages/95/2a/f1c116cdd302f9df7d39a5ca0aa353cc19b7344b3b0e044e68219aff60b0/fastcluster-1.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:855ab2b7e6fa9b05f19c4f3023dedfb1a35a88d831933d65d0d9e10a070a9e85", size = 37568 }, - { url = "https://files.pythonhosted.org/packages/be/88/ee6041c7a8380c70d4a2fe9f9c2787fe042ece85352d683aaffe476706e4/fastcluster-1.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72503e727887a61a15f9aaa13178798d3994dfec58aa7a943e42dcfda07c0149", size = 184415 }, - { url = "https://files.pythonhosted.org/packages/fa/4c/b72c421b4c2962f2f8a794be40349cd8706e9ede6c3c742bce0ea158d6dd/fastcluster-1.2.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcb0973ca0e6978e3242046338c350cbed1493108929231fae9bd35ad05a6d6", size = 189769 }, - { url = "https://files.pythonhosted.org/packages/6f/e6/610c672d6d893a0822aa6c45d47ceae8f3eb7fcbe47907037dcb1a07a769/fastcluster-1.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9020899b67fe492d0ed87a3e993ec9962c5a0b51ea2df71d86b1766f065f1cef", size = 194022 }, - { url = "https://files.pythonhosted.org/packages/e5/b1/8be97880ef43606afffc779a00741fd21dee958a2764dc1e821a483f45b6/fastcluster-1.2.6-cp310-cp310-win32.whl", hash = "sha256:6cf156d4203708348522393c523c2e61c81f5a6a500e0411dcba2b064551ea2f", size = 33221 }, - { url = "https://files.pythonhosted.org/packages/84/ea/a3639f8aa11e66968ff01c8c7631cd8f15261b33e6f134eaca4f50784eeb/fastcluster-1.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:1801c9daa9aa5bbbb0830efe8bd3034b4b7a417e4b8dd353683999be29797df2", size = 36407 }, - { url = "https://files.pythonhosted.org/packages/7d/51/9a75a15df26f594112968f1de267762ccd3ac7da3492f2bb4df74ba9575e/fastcluster-1.2.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce70c743490f6778b463524d1767a9ecccd31c8bd2dbb5739bb2174168c15d39", size = 69299 }, - { url = "https://files.pythonhosted.org/packages/27/47/ab525237529c7e6e39eb704700fd91db58bebeb6d5ca0076ef2fbaac5428/fastcluster-1.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac1b84d4b28456a379a71451d13995eb3242143452ce9c861f8913360de842a3", size = 38560 }, - { url = "https://files.pythonhosted.org/packages/5b/45/1c1a84efb8b6089b2318b4cbbf5a425a6a84b3d68549a4b5dd6d0eebd303/fastcluster-1.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b49f6033c45a28f93540847b495ed0f718b5c3f4fef446cf77e3726662e1d5", size = 40827 }, - { url = "https://files.pythonhosted.org/packages/69/e0/67a87022793dedfa4b38e9c6ad0ce2ee72668800d31544ea6eae11fa9b76/fastcluster-1.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1c776a4ec7594f47cd2e1e2da73a30134f1d402d7c93a81e3cb7c3d8e191173", size = 185205 }, - { url = "https://files.pythonhosted.org/packages/91/9f/4ed758b3607b6ce26d84b00c85fa59764a0f08199d9b18efaf9a58342cac/fastcluster-1.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca61d16435bb7aea3901939d7d7d7e36aff9bb538123e649166a3014b280054", size = 190720 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/52fb1915461ee37498f84cf646ddbed92b2bf3e6f542cfccac1f6a813133/fastcluster-1.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04ea4a68e0675072ca761bad33322a0e998cb43693fd41165bc420d7db40429a", size = 195001 }, - { url = "https://files.pythonhosted.org/packages/37/6e/96a4cee1ff1452edab1f8f5bf02a291d047761211be4b1c67296f6b01a4b/fastcluster-1.2.6-cp311-cp311-win32.whl", hash = "sha256:773043d5db2790e1ff2a4e1eae0b6a60afb2a93ad2c74897a56c80bc800db04f", size = 33169 }, - { url = "https://files.pythonhosted.org/packages/c5/2e/1406301a131605cd27cbdca56c81e59475433d5145fd05759f06cff5717c/fastcluster-1.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:841d128daa6597d13781793eb482b0b566bbd58d2a9d1e2cf1b58838773beb14", size = 36305 }, - { url = "https://files.pythonhosted.org/packages/0c/06/0d8a1eb095e8d1777a7ab975729d4c6071e5e9370ae8359454f3cd630c16/fastcluster-1.2.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a085e7e13f1afc517358981b2b7ed774dc9abf95f2be0da9a495d9e6b58c4409", size = 67627 }, - { url = "https://files.pythonhosted.org/packages/04/82/ce9970726a4107d326e6de65094017a260aee3282995222ddf7bfeb2f8fd/fastcluster-1.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a7c7f51a6d2f5ab58b1d85e9d0af2af9600ec13bb43bc6aafc9085d2c4ccd93", size = 40115 }, - { url = "https://files.pythonhosted.org/packages/28/bc/a6f3c7d76c8d83e0258fd5f0ac6d7eaf95b9925e004a90383cfe6ef2b665/fastcluster-1.2.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8bac5cf64691060cf86b0752dd385ef1eccff6d24bdb8b60691cf8cbf0e4f9ef", size = 37560 }, - { url = "https://files.pythonhosted.org/packages/d7/4f/0a71ce787dabe3a845b1b19d2ee01987f8d638b1e78d0b67db352239c5d6/fastcluster-1.2.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060c1cb3c84942d8d3618385e2c25998ba690c46ec8c73d64477f808abfac3f2", size = 184110 }, - { url = "https://files.pythonhosted.org/packages/91/23/b98b7c8c51141e45de9c64345f8837ba874812e8f1d323727daa7614e5a1/fastcluster-1.2.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03a228e018457842eb81de85be7af0b5fe8065d666dd093193e3bdcf1f13d2e", size = 189526 }, - { url = "https://files.pythonhosted.org/packages/87/15/af8c4a2a9329c9f117e49efd16c0e13ee1b3e4ef16be27d35005fbc36b6f/fastcluster-1.2.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6f8da329c0032f2acaf4beaef958a2db0dae43d3f946f592dad5c29aa82c832", size = 193741 }, - { url = "https://files.pythonhosted.org/packages/7f/61/b6d0e0723129ff34c214c90fd3bdcccc4b5594351eee0d949fd14a0a8a5d/fastcluster-1.2.6-cp39-cp39-win32.whl", hash = "sha256:eb3f98791427d5d5d02d023b66bcef61e48954edfadae6527ef72d70cf32ec86", size = 33219 }, - { url = "https://files.pythonhosted.org/packages/d6/7c/9a5d7fd8ca2f8ede3fd32ce8550f7487ead6d4d6f46c4f0d9016dede7eb4/fastcluster-1.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:4b9cfd426966b8037bec2fc03a0d7a9c87313482c699b36ffa1432b49f84ed2e", size = 36410 }, +sdist = { url = "https://files.pythonhosted.org/packages/41/1e/417892546cb92e71f5bcaeffc8d89b47716fd811805a8ae559b91f754015/fastcluster-1.3.0.tar.gz", hash = "sha256:d5233aeba5c3faa949c7fa6a39345a09f716ccebbd748541e5735c866696df02", size = 173065, upload-time = "2025-05-06T17:45:30.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/fa/427569f391a9951fe483222d3df6bb18a690d963810473fb0e305150cf86/fastcluster-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee25c5c36d2b68036aa0d9ff37ba316434824087e6b9884005f1eb09ceaf9ad", size = 62915, upload-time = "2025-05-06T17:45:07.894Z" }, + { url = "https://files.pythonhosted.org/packages/34/ee/99e3b9968e5130940539ef34008e6eded2c6ea8b0bab01e5f83241bb03e7/fastcluster-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a65bed904a67a966a9496f438d28d1d4cc7b57da5ff9525fbdc047874de60cf", size = 38998, upload-time = "2025-05-06T17:45:08.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/16/c83a748debf8b0bb8d23b630b012a8a03a13f855b283df1da89e1c76f647/fastcluster-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41dc92126d6195d38f011ae8f4f33d0e4b0edc842da73749f6608b529b6c6d34", size = 34774, upload-time = "2025-05-06T17:45:09.974Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/3d7de0cb4669848fef3f9ec90adee4a4afc03544bc85b5d9fe284cbd909f/fastcluster-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e7037a010199cde6028c3d42327d2f84caae673fc708afed084b5f6c043650d", size = 184808, upload-time = "2025-05-06T17:45:10.992Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9b/700f7d7d4f39e909f8c098a283797d61afc694934c87eb041dd87ba8f63f/fastcluster-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7467e6da746dba310235546d0b6eef90a1652994dfd2c3022f7052effd142dee", size = 194527, upload-time = "2025-05-06T17:45:12.546Z" }, + { url = "https://files.pythonhosted.org/packages/4c/60/ef55d3aa66914e0d1fe751df85b222fe9e78c8782c8f930ad3240d52d92f/fastcluster-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:5ae60778ac7ed46108c1175478ff09af0a19a2dbb02090a1c13bf9c5bffbf230", size = 37328, upload-time = "2025-05-06T17:45:14.086Z" }, + { url = "https://files.pythonhosted.org/packages/41/dc/b43081c5f4c1441b46e847adee464cea22dbb106891437b4a2d41a81f59a/fastcluster-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b20785852abb0ba5af62316327b654cea0fd736f819cd48792de0875ffb485f0", size = 62799, upload-time = "2025-05-06T17:45:15.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/77/d1cf1f6e6c83c11ebcf4d378a5ea566d30b50e240477f695e33a9b88698b/fastcluster-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6be2e33529917df1398f5d85ea55856ebddd81041b0fbe2dfc6badcb0c3b2054", size = 38879, upload-time = "2025-05-06T17:45:16.649Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/0bf77416d2fba60d773039eb236c6fcf64384236c58e63b5a2120e803af3/fastcluster-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ddd6df989ee9ced20c4ecd7cef8df421a10b5410913385bb29d9183d21cc5ee", size = 34778, upload-time = "2025-05-06T17:45:17.646Z" }, + { url = "https://files.pythonhosted.org/packages/db/36/bc720b34d27bcb40024d63692e1f30a4e9402670881121755c5a1fb5e5c8/fastcluster-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d22a99d2fef1d7a314650e0f5fc78d4f91d4b233e5aa81b31da45506d25f21", size = 184385, upload-time = "2025-05-06T17:45:18.697Z" }, + { url = "https://files.pythonhosted.org/packages/27/eb/df607b9e505fc105539977c7da68af06a448d6dfb86355ff2b839c775fbe/fastcluster-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:428126288895fcb6316a239635bafaa19e4677240afee2723a952e488929091d", size = 194095, upload-time = "2025-05-06T17:45:20.378Z" }, + { url = "https://files.pythonhosted.org/packages/d0/63/e6ffa0b2cc9d708f9ab6eb4dd22fc843d64002e7cf9b2bc1ca6ec6df0dd7/fastcluster-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:317db2531895cdf178a3009d3a8b13dfa83a5ed4ab14943b33377174cf9420cf", size = 37350, upload-time = "2025-05-06T17:45:22.018Z" }, + { url = "https://files.pythonhosted.org/packages/36/87/07ff592bcf14e6fcace16f639f623fd4298ddd3538bbd4f0c0ee697f2edd/fastcluster-1.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:07b7cb3a831958f3039064be193a7c125657861e57b257965245c755d41ac031", size = 62797, upload-time = "2025-05-06T17:45:23.13Z" }, + { url = "https://files.pythonhosted.org/packages/89/b5/d72b995d254457e015057ca7ef0bbb34e85d45afb71f88d03f847879d711/fastcluster-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:34f0be433cfe0f5c5f3f71052e1850a99fe712f625889adb08d8d1ae851fe7ec", size = 38879, upload-time = "2025-05-06T17:45:24.208Z" }, + { url = "https://files.pythonhosted.org/packages/40/2a/69c313ffc9521d5bd6a32250ac0222289ef6192b4ce524bb8d42796967ef/fastcluster-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:400213bdc7d5135c4282fdb7ad6b0579d5a041c16e67ef69bd9d6749335256fd", size = 34785, upload-time = "2025-05-06T17:45:25.236Z" }, + { url = "https://files.pythonhosted.org/packages/ea/48/5d043913451576248ac026e9659f4fffb5356f7e2556e115fe3941f7fe03/fastcluster-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c71b7236288b9067d329359639789060e030285e8da2724fcdacd55ae751947", size = 184257, upload-time = "2025-05-06T17:45:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/59/f3/f71552b94a39509b62e72c4a26b6e4440bb9ce6decacf90af2916829e69e/fastcluster-1.3.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dce31ace6f8e08c5400d6e19492fe09aba2b050f78a7aa6943ba2ae50dcd1b0", size = 193944, upload-time = "2025-05-06T17:45:27.866Z" }, + { url = "https://files.pythonhosted.org/packages/b1/64/78709a98a17f33feb960c6547c33e7e4838e49d4998b3a318f255757fe68/fastcluster-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:e7c78f0e9e05c6f380d3d018ef5106d4526461abfd764fe7c0a7b53911b36c7d", size = 37354, upload-time = "2025-05-06T17:45:29.056Z" }, ] [[package]] name = "filelock" -version = "3.18.0" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] name = "fonttools" -version = "4.58.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/7a/30c581aeaa86d94e7a29344bccefd2408870bf5b0e7640b6f4ffede61bd0/fonttools-4.58.1.tar.gz", hash = "sha256:cbc8868e0a29c3e22628dfa1432adf7a104d86d1bc661cecc3e9173070b6ab2d", size = 3519505 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ed/94a7310e6ee87f6164d7cf273335445fb12b70625582df137b3692ec495b/fonttools-4.58.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ebd423034ac4f74196c1ae29f8ed3b862f820345acbf35600af8596ebf62573", size = 2734333 }, - { url = "https://files.pythonhosted.org/packages/09/d9/7f16d4aea0494dc02a284cb497ddd37a5b88d0d3da4ea41f7298ce96ca1a/fonttools-4.58.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9dc36f4b4044d95e6fb358da4c3e6a5c07c9b6f4c1e8c396e89bee3b65dae902", size = 2306563 }, - { url = "https://files.pythonhosted.org/packages/cf/16/abdecf240d4fcc8badf6dbe3941500b64acd1401288bd9515e936ab2d27f/fonttools-4.58.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4b74d7bb84189fe264d56a544ac5c818f8f1e8141856746768691fe185b229", size = 4717603 }, - { url = "https://files.pythonhosted.org/packages/9c/3c/ad9bc6cfb4c4260689808b083c1d1a0c15b11d7c87bf7f6e61f77d4c106c/fonttools-4.58.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa4fa41e9cb43f78881a5896d6e41b6a0ec54e9d68e7eaaff6d7a1769b17017", size = 4750798 }, - { url = "https://files.pythonhosted.org/packages/63/e7/d32080afcd754b78c7bedfa8475b6887792fca81a95ff7c634a59dc8eb4c/fonttools-4.58.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91335202f19c9edc04f2f6a7d9bb269b0a435d7de771e3f33c3ea9f87f19c8d4", size = 4800201 }, - { url = "https://files.pythonhosted.org/packages/46/21/68f5285ba7c59c9df8fdc045b55a149c10af865b2615ea426daa47bcf287/fonttools-4.58.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6b0ec2171e811a0d9e467225dc06b0fac39a84b4704f263c2d538c3c67b99b2", size = 4908504 }, - { url = "https://files.pythonhosted.org/packages/66/77/abf1739cee99672b9bc3701bc3a51b01d325c4e117d7efd7e69315c28ce5/fonttools-4.58.1-cp310-cp310-win32.whl", hash = "sha256:a788983d522d02a9b457cc98aa60fc631dabae352fb3b30a56200890cd338ca0", size = 2190748 }, - { url = "https://files.pythonhosted.org/packages/5e/18/e5a239f913f51e48a2d620be07a8f942fb8018850e0fbfeee2c11dd72723/fonttools-4.58.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8c848a2d5961d277b85ac339480cecea90599059f72a42047ced25431e8b72a", size = 2235207 }, - { url = "https://files.pythonhosted.org/packages/50/3f/9fecd69149b0eec5ca46ec58de83b2fd34d07204fe2c12c209255082507a/fonttools-4.58.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9966e14729669bcfbb56f83b747a2397c4d97c6d4798cb2e2adc28f9388fa008", size = 2754713 }, - { url = "https://files.pythonhosted.org/packages/c8/19/d04ea5f3ab2afa7799f2b1ebe1d57ff71b479f99f29b82bddc7197d50220/fonttools-4.58.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64cc1647bbe83dea57f5496ec878ad19ccdba7185b0dd34955d3e6f03dc789e6", size = 2316637 }, - { url = "https://files.pythonhosted.org/packages/5c/3f/375f59d756b17318336c050363849011e03ac82904538f39ebe8189835bc/fonttools-4.58.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464f790ce681d08d1583df0735776aa9cb1999594bf336ddd0bf962c17b629ac", size = 4915730 }, - { url = "https://files.pythonhosted.org/packages/2f/90/069f859d6f6480503574cda21b84ceee98bf5f5fd1764f26674e828a2600/fonttools-4.58.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c53c6a720ee70cc25746d511ba88c45c95ec510fd258026ed209b0b9e3ba92f", size = 4936194 }, - { url = "https://files.pythonhosted.org/packages/01/11/339973e588e1c27f20c578f845bdcf84376c5e42bd35fca05419fd8d1648/fonttools-4.58.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6823a633bbce29cf3033508ebb54a433c473fb9833eff7f936bfdc5204fd98d", size = 4978982 }, - { url = "https://files.pythonhosted.org/packages/a7/aa/1c627532a69715f54b8d96ab3a7bc8628f6e89989e9275dfc067dc2d6d56/fonttools-4.58.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5701fe66a1408c1974d2f78c00f964f8aad17cccbc32bc041e1b81421f31f448", size = 5090087 }, - { url = "https://files.pythonhosted.org/packages/77/ce/cf7b624db35bce589ac1f2c98329ea91b28f0283d3b7e9e6126dfaeb5abd/fonttools-4.58.1-cp311-cp311-win32.whl", hash = "sha256:4cad2c74adf9ee31ae43be6b0b376fdb386d4d50c60979790e32c3548efec051", size = 2188923 }, - { url = "https://files.pythonhosted.org/packages/b9/22/c4f1f76eeb1b9353e9cc81451d0ae08acc3d3aa31b9ab8f3791a18af1f89/fonttools-4.58.1-cp311-cp311-win_amd64.whl", hash = "sha256:7ade12485abccb0f6b6a6e2a88c50e587ff0e201e48e0153dd9b2e0ed67a2f38", size = 2236853 }, - { url = "https://files.pythonhosted.org/packages/32/97/ed1078b1e138fbc0b4ee75878000d549a70c02d83bb4e557e416efc34140/fonttools-4.58.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f56085a65769dc0100822c814069327541db9c3c4f21e599c6138f9dbda75e96", size = 2740473 }, - { url = "https://files.pythonhosted.org/packages/28/35/53d49fb7d6b30128153d11628b976fda3ce8ae44234b5a81c4edb3023798/fonttools-4.58.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19c65a88e522c9f1be0c05d73541de20feada99d23d06e9b5354023cc3e517b0", size = 2309936 }, - { url = "https://files.pythonhosted.org/packages/0c/db/8b63c1d673b2bf0cfed77500d47769dc4aa85453b5f0ef525db2cf952895/fonttools-4.58.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01bb37006e97703300bfde7a73d1c7038574dd1df9d8d92ca99af151becf2ca", size = 4814671 }, - { url = "https://files.pythonhosted.org/packages/a6/13/0b96eeb148b77c521b8e94628c59d15e4fb0e76191c41f5616a656d6adb9/fonttools-4.58.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d629dea240f0fc826d8bb14566e95c663214eece21b5932c9228d3e8907f55aa", size = 4881493 }, - { url = "https://files.pythonhosted.org/packages/ac/b0/9f8aa60e8e5be91aba8dfaa3fa6b33fd950511686921cf27e97bf4154e3d/fonttools-4.58.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef0b33ff35421a04a638e736823c2dee9d200cdd275cfdb43e875ca745150aae", size = 4874960 }, - { url = "https://files.pythonhosted.org/packages/b6/7e/83b409659eb4818f1283a8319f3570497718d6d3b70f4fca2ddf962e948e/fonttools-4.58.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4db9399ee633855c718fe8bea5eecbdc5bf3fdbed2648e50f67f8946b943ed1c", size = 5026677 }, - { url = "https://files.pythonhosted.org/packages/34/52/1eb69802d3b54e569158c97810195f317d350f56390b83c43e1c999551d8/fonttools-4.58.1-cp312-cp312-win32.whl", hash = "sha256:5cf04c4f73d36b30ea1cff091a7a9e65f8d5b08345b950f82679034e9f7573f4", size = 2176201 }, - { url = "https://files.pythonhosted.org/packages/6f/25/8dcfeb771de8d9cdffab2b957a05af4395d41ec9a198ec139d2326366a07/fonttools-4.58.1-cp312-cp312-win_amd64.whl", hash = "sha256:4a3841b59c67fa1f739542b05211609c453cec5d11d21f863dd2652d5a81ec9b", size = 2225519 }, - { url = "https://files.pythonhosted.org/packages/83/7a/7ed2e4e381f9b1f5122d33b7e626a40f646cacc1ef72d8806aacece9e580/fonttools-4.58.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68379d1599fc59569956a97eb7b07e0413f76142ac8513fa24c9f2c03970543a", size = 2731231 }, - { url = "https://files.pythonhosted.org/packages/e7/28/74864dc9248e917cbe07c903e0ce1517c89d42e2fab6b0ce218387ef0e24/fonttools-4.58.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8631905657de4f9a7ae1e12186c1ed20ba4d6168c2d593b9e0bd2908061d341b", size = 2305224 }, - { url = "https://files.pythonhosted.org/packages/e7/f1/ced758896188c1632c5b034a0741457f305e087eb4fa762d86aa3c1ae422/fonttools-4.58.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2ecea7289061c2c71468723409a8dd6e70d1ecfce6bc7686e5a74b9ce9154fe", size = 4793934 }, - { url = "https://files.pythonhosted.org/packages/c1/46/8b46469c6edac393de1c380c7ec61922d5440f25605dfca7849e5ffff295/fonttools-4.58.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b8860f8cd48b345bd1df1d7be650f600f69ee971ffe338c5bd5bcb6bdb3b92c", size = 4863415 }, - { url = "https://files.pythonhosted.org/packages/12/1b/82aa678bb96af6663fe163d51493ffb8622948f4908c886cba6b67fbf6c5/fonttools-4.58.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7c9a0acdefcb8d7ccd7c59202056166c400e797047009ecb299b75ab950c2a9c", size = 4865025 }, - { url = "https://files.pythonhosted.org/packages/7d/26/b66ab2f2dc34b962caecd6fa72a036395b1bc9fb849f52856b1e1144cd63/fonttools-4.58.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1fac0be6be3e4309058e156948cb73196e5fd994268b89b5e3f5a26ee2b582", size = 5002698 }, - { url = "https://files.pythonhosted.org/packages/7b/56/cdddc63333ed77e810df56e5e7fb93659022d535a670335d8792be6d59fd/fonttools-4.58.1-cp313-cp313-win32.whl", hash = "sha256:aed7f93a9a072f0ce6fb46aad9474824ac6dd9c7c38a72f8295dd14f2215950f", size = 2174515 }, - { url = "https://files.pythonhosted.org/packages/ba/81/c7f395718e44cebe1010fcd7f1b91957d65d512d5f03114d2d6d00cae1c4/fonttools-4.58.1-cp313-cp313-win_amd64.whl", hash = "sha256:b27d69c97c20c9bca807f7ae7fc7df459eb62994859ff6a2a489e420634deac3", size = 2225290 }, - { url = "https://files.pythonhosted.org/packages/0a/b6/eaa8b2f38ad5339bc51ff75bf6a9c29e4b619453d8378ae9a374535e954d/fonttools-4.58.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:927762f9fe39ea0a4d9116353251f409389a6b58fab58717d3c3377acfc23452", size = 2740399 }, - { url = "https://files.pythonhosted.org/packages/94/a1/6b56d0a5e20be9586c7669189cdcfcabced90bf676030f46397162d56926/fonttools-4.58.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:761ac80efcb7333c71760458c23f728d6fe2dff253b649faf52471fd7aebe584", size = 2309460 }, - { url = "https://files.pythonhosted.org/packages/23/3c/bebd50b085d78d64ee518fb9c95fd08b90f9b715ca08c0b43fd53a963560/fonttools-4.58.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef910226f788a4e72aa0fc1c1657fb43fa62a4200b883edffdb1392b03fe86", size = 4701742 }, - { url = "https://files.pythonhosted.org/packages/89/4a/dbc6f9efac98718feba2735ceb72237e8965a4878529c0af6d33f32e7403/fonttools-4.58.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2859ca2319454df8c26af6693269b21f2e9c0e46df126be916a4f6d85fc75", size = 4730821 }, - { url = "https://files.pythonhosted.org/packages/63/ed/1a64f06747d05a8bb4d6b2bf7de59e960533d5303f254cf366cc4d827e7d/fonttools-4.58.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:418927e888e1bcc976b4e190a562f110dc27b0b5cac18033286f805dc137fc66", size = 4787238 }, - { url = "https://files.pythonhosted.org/packages/86/82/ecb3e23507cca2548902cb1f1c09c7d919b9ad1bf83e464fd2c7c924adf6/fonttools-4.58.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a907007a8b341e8e129d3994d34d1cc85bc8bf38b3a0be65eb14e4668f634a21", size = 4895738 }, - { url = "https://files.pythonhosted.org/packages/dc/44/73c560fbcdee65ffcf2dc9069afc21d5afab1cbdf318284d56649e937b30/fonttools-4.58.1-cp39-cp39-win32.whl", hash = "sha256:455cb6adc9f3419273925fadc51a6207046e147ce503797b29895ba6bdf85762", size = 1468967 }, - { url = "https://files.pythonhosted.org/packages/70/fe/df31c80575567b7239d225760a820b3abfe307e2830a9119bd4a6eb6bb8f/fonttools-4.58.1-cp39-cp39-win_amd64.whl", hash = "sha256:2e64931258866df187bd597b4e9fff488f059a0bc230fbae434f0f112de3ce46", size = 1513516 }, - { url = "https://files.pythonhosted.org/packages/21/ff/995277586691c0cc314c28b24b4ec30610440fd7bf580072aed1409f95b0/fonttools-4.58.1-py3-none-any.whl", hash = "sha256:db88365d0962cd6f5bce54b190a4669aeed9c9941aa7bd60a5af084d8d9173d6", size = 1113429 }, +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] [[package]] name = "fsspec" -version = "2025.5.1" +version = "2026.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052 }, + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] name = "id" -version = "1.5.0" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611 }, + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] name = "imageio" -version = "2.37.0" +version = "2.37.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796 }, + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, ] [[package]] name = "imagesize" -version = "1.4.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, -] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -825,54 +695,42 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, ] [[package]] name = "jaraco-functools" -version = "4.1.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, -] - -[[package]] -name = "jgo" -version = "1.0.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "psutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/4f/2a9485b9e3d68b343c5b590b19d503f6cc4bebb2fcd827e3467c9d3ab92a/jgo-1.0.6.tar.gz", hash = "sha256:31824d329dc5824954cc150a9d934c543a227818dc1c0ea6ea20741c2e462b24", size = 23284 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/0c/c29fcd6a59f50d953fd296d10fa5d455a239c84fa74ed8db4913f30d13cc/jgo-1.0.6-py3-none-any.whl", hash = "sha256:b59b8da5ab13f84cae2eae295bafa54592204433cef1e3fdede9dae946c0b61d", size = 15355 }, + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] @@ -882,62 +740,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "joblib" -version = "1.5.1" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, -] - -[[package]] -name = "jpype1" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bd/68/47fa634cbd0418cbca86355e9421425f5892ee994f7338106327e49f9117/jpype1-1.5.2.tar.gz", hash = "sha256:74a42eccf21d30394c1832aec3985a14965fa5320da087b65029d172c0cec43b", size = 1011421 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/f2/b2efcad1ea5a541f125218e4eb1529ebb8ca18941264c879f3e89a36dc35/jpype1-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7b2da98c142812ca40a18a735b33e47c6511b03debf1e979630f4cf473b68a87", size = 584511 }, - { url = "https://files.pythonhosted.org/packages/c0/c6/63538d160c17e837f62d29ba4163bc444cef08c29cd3f3b8090691c1869c/jpype1-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcfc5c1d45d6b108800d172ea817bda585db7f1646d6a98d14da9aca66e0eb44", size = 467488 }, - { url = "https://files.pythonhosted.org/packages/91/69/655d73a64ae6731706eab73d3f63fd2d48e36704da09710fbc0155c14402/jpype1-1.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:156f469a976cc61f695a2455db6de7334774388e7d53c3da8a0a23d9c062d1b2", size = 510064 }, - { url = "https://files.pythonhosted.org/packages/97/0a/cbe03759331c640aa5862f974028122a862b08935a0b11b8fa6f6e46c26b/jpype1-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca93cc74f8db1f604d2ea6adb764dec4dec68528f1ee68308fa3d524095739", size = 494097 }, - { url = "https://files.pythonhosted.org/packages/22/18/0a51845ca890ffdc72f4d71a0c2be334b887c5bb6812207efe5ad45afcb3/jpype1-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:924b0a0cf93d3dddb3f79286fbe40f8c901c78ed61216edbe108666234df43e0", size = 356655 }, - { url = "https://files.pythonhosted.org/packages/35/a0/638186a75026a02286041e4a0449b1dff799a3914dc1c0716ef9b9367b73/jpype1-1.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9f6ab8dd284c16e2617a697d54c3d0304b08020a37386ed96103a129391a2d9", size = 584474 }, - { url = "https://files.pythonhosted.org/packages/0e/78/95db2eb3c8a7311ee08a2c237cea24828859db6a6cb5e901971d3f5e49da/jpype1-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a54a771ee56260f98e5b9a77455084e4a48061967de13dabf628bdba9c8122e0", size = 467508 }, - { url = "https://files.pythonhosted.org/packages/84/07/f4ed08bf1f65526d59a07efa86a79a2cc37263fea9a91efeb7dfd2b81bcb/jpype1-1.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05dc6d2759111483a9c808a50e67f556efe494e999585c7d7e7d6d8a8ee58525", size = 510256 }, - { url = "https://files.pythonhosted.org/packages/0b/7d/9fdbbc1a574be43f9820735ca8df0caf8b159856201d9b21fd73932342bc/jpype1-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b900e154826a076118d074166596f1d817e113e07084bf0c9c43d8064a86ab77", size = 494076 }, - { url = "https://files.pythonhosted.org/packages/0e/b9/4dfb38a7f4efb21f71df7344944a8d9a23e30d0503574e455af6ce4f1a56/jpype1-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:0a0d18d4384b3df2e55282545737dfcf18c604504f1382ad14f880bef960f265", size = 356587 }, - { url = "https://files.pythonhosted.org/packages/8d/e4/0c27352e8222dcc0e3ce44b298015072d2057d08dd353541c980a31d26c9/jpype1-1.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1e1db9ac909ad2ae0e40b04c2aa88cb14250d5245d69715561507681f2b08b2f", size = 583445 }, - { url = "https://files.pythonhosted.org/packages/fa/4c/e0200a6e3fed5cda79e926c2a8a610676f04948f89d7e38d93c7d4b21be9/jpype1-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994fb7b319b453f77ad4b6aff01e0dd4180ea74a6fe5a031e4e9db92dbe95376", size = 466485 }, - { url = "https://files.pythonhosted.org/packages/c3/ad/d85926b2f0104ded953fa53ff95fe71c96935cd742fdadb888f1145ffe79/jpype1-1.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec2f1009d7221fb3443decfb8f326039febc93578aadedfb3e052dab0afbf5a", size = 509372 }, - { url = "https://files.pythonhosted.org/packages/74/f3/1cd4332076ed0421e703412f47f15f43af170809435c57ba3162edc80d4b/jpype1-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b1fb2b430a50f081ea0ee24d19232ae0d03dbfe3dd076ec5f8ae42b30a656f", size = 493520 }, - { url = "https://files.pythonhosted.org/packages/74/dd/7408d4beae755de6fcd07c76b2f0bacabc0461b43fba83811c1f7c22440e/jpype1-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b1c2d76d211cab60be16505d32a6b3c9fffc51ce79c68e81a3d48e5effff2d", size = 356149 }, - { url = "https://files.pythonhosted.org/packages/76/be/b37005bec457b94eaaf637a663073b7c5df70113fd4ae4865f6e386c612f/jpype1-1.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4acb098cb1698b14b6e5c79e275f4c70dcc01b0fb93425f206d0a5e380e43c66", size = 583600 }, - { url = "https://files.pythonhosted.org/packages/20/a3/00a265d424f7d47e0dc547df2320225ce0143fec671faf710def41404b8c/jpype1-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c08480c7d18125664a12bf0a244b96b49c05105306b65937dbefeb05ab4b2847", size = 466426 }, - { url = "https://files.pythonhosted.org/packages/0e/cd/890d9ed43d7e1366e151c0ed7046e59f6c1cb7ecfc8ecfefe9e5e79f8420/jpype1-1.5.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6819f231c651ef876ffb23158083ea498ff80b57c46da537148412aa22235a13", size = 509513 }, - { url = "https://files.pythonhosted.org/packages/6d/d0/191db2e9ab6ae7029368a488c9d88235966843b185aba7925e54aa0c0013/jpype1-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42fe8db66ad4e5c66f637f5c4de82fca880ba696104e1f4a7e575885923dead8", size = 493474 }, - { url = "https://files.pythonhosted.org/packages/e3/b7/e1787633b41d609320b41d0dd87fe3118598210609e4e3f6cef93cfcef40/jpype1-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2b96365f1302df2fb3c6ad73117d6fe450a55b7550fd7fecadac3cec5bc7117c", size = 356150 }, - { url = "https://files.pythonhosted.org/packages/a8/c8/76541ffefa6fc4ee7a3a8c7e6d9e376f5ac8980a47ff31a8c330ffbb5dcf/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ea5001f6a7a42be6f5f500dcb20dad5738fef6a7d19c86dbcf482b803f01cab", size = 468412 }, - { url = "https://files.pythonhosted.org/packages/be/6f/827ca43aaa5ea6b773aa90b405acec22bd152d1284aa40f7c0c02bc1657b/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220d33305998ff7a7cbc9c0b5eea54f2691fdc21e60d52ad56276ea13fd5bd4c", size = 511322 }, - { url = "https://files.pythonhosted.org/packages/76/37/f1396d7b66f9b6867a279db510e625ba51c8a4b4397f59a6c2b20fb55548/jpype1-1.5.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f1ed152fdd5200067dc0cdcd4683530b87a6be3987b98596cdf5a7d3ac4c679", size = 494714 }, - { url = "https://files.pythonhosted.org/packages/05/71/590b2a91b43763aa27eac2c63803542a2878a4d8c600b81aa694d3fde919/jpype1-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b75d33e93a3bc6543ddf97c24ee0adb5a86a69fb67f0e4f4fa1c8c3970bbf98", size = 385026 }, - { url = "https://files.pythonhosted.org/packages/77/6b/130fb6d0c43976b4e129c6bc19daf0e25c42fc38c5096ed92c4105bfd2c4/jpype1-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea21bca4cece752cd3ee88fcd62ce8f444feac8dc7244475fdb9c0e8712e07ea", size = 467423 }, - { url = "https://files.pythonhosted.org/packages/d4/a9/ec047a94e85c7cadf07f33f871e72d20faa6935c911dc5884388ca08a8bf/jpype1-1.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670daf6b13f32653438ebde81aa30f6f48149b95dad4d80a40b3ebea47622164", size = 510117 }, - { url = "https://files.pythonhosted.org/packages/77/91/f08a719461a390b48d9096b50f1f4a49ee281007ec192e51073090d3d8b7/jpype1-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54744265ef36665d110d139a4b81d10532694c6077b23ef60f3609feadc22d30", size = 494053 }, - { url = "https://files.pythonhosted.org/packages/e5/cf/344e1f81f1e8c651ec23dfa9fe4b91f6e1d699b36f610a547ba85ee7fb16/jpype1-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:68e1d118200fc46f4ea4bf20900081587ec04de484037c997b0a3b7c5eb71fe3", size = 356716 }, + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "keyring" -version = "25.6.0" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, @@ -948,873 +767,710 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750 } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085 }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, - { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, - { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, - { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, - { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, - { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, - { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, - { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, - { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, - { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, - { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, - { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, - { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, - { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, - { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, - { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449 }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757 }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312 }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966 }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044 }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879 }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751 }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122 }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126 }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313 }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784 }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988 }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980 }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847 }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494 }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666 }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088 }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321 }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776 }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984 }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] name = "kiwisolver" -version = "1.4.8" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] [[package]] name = "kornia" -version = "0.8.1" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "kornia-rs" }, { name = "packaging" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/49/3c3aa9c68262b87551d50cdeefff027394316abfe11937a729371a39f50a/kornia-0.8.1.tar.gz", hash = "sha256:9ce5a54a11df661794934a293f89f8b8d49e83dd09b0b9419f6082ab07afe433", size = 652542 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/e6/45e757d4924176e4d4e111e10effaab7db382313243e0188a06805010073/kornia-0.8.2.tar.gz", hash = "sha256:5411b2ce0dd909d1608016308cd68faeef90f88c47f47e8ecd40553fd4d8b937", size = 667151, upload-time = "2025-11-08T12:10:03.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/95/a2e359cd08ea24498be59dcd5980e290cdf658ce534bcb7ae13b8ef694e6/kornia-0.8.1-py2.py3-none-any.whl", hash = "sha256:5dcb00faa795dfb45a3630d771387290bc4f40473451352ca250e5bcc81af3d1", size = 1078646 }, + { url = "https://files.pythonhosted.org/packages/79/d4/e9bd12b7b4cbd23b4dfb47e744ee1fa54d6d9c3c9bc406ec86c1be8c8307/kornia-0.8.2-py2.py3-none-any.whl", hash = "sha256:32dfe77c9c74a87a2de49395aa3c2c376a1b63c27611a298b394d02d13905819", size = 1095012, upload-time = "2025-11-08T12:10:01.226Z" }, ] [[package]] name = "kornia-rs" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/28/6e0cc55dfe9967228cfacbeee79d2a31d8ed5bd6bcc478ec0a0f790f1e71/kornia_rs-0.1.9.tar.gz", hash = "sha256:c7e45e84eb3c2454055326f86329e48a68743507460fb7e39315397fa6eeb9a0", size = 108371 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/f2/b0c668be833f628e9586c85a63424271e76c0b44cde187efd409846694e5/kornia_rs-0.1.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba3bbb9da2c14803b6b7e5faeef87d3e95d0728e722d0a9e7e32e59b3a22ed42", size = 2569136 }, - { url = "https://files.pythonhosted.org/packages/eb/af/9dcc146e3693aec8ad52505ee3303c495d6e8faaa8c17e20082109e1c3cd/kornia_rs-0.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bb14f89bd999d5cd5bf5c187d2f25f63b3134676da146eb579023303e9ea2a3", size = 2304661 }, - { url = "https://files.pythonhosted.org/packages/e6/49/e8c3865c856c3e95d7b97d8070e14c685fc2eaea9d22787da3a94e6f6d35/kornia_rs-0.1.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d545c3f323d37745228681a71b577e88468d54b965e2194c0e346bb0a969c5f3", size = 2487276 }, - { url = "https://files.pythonhosted.org/packages/e9/af/459207ad974d4b0540ff949d0fe2f6197eca905dc4db15b58a230ad5c902/kornia_rs-0.1.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba36918487a6c9f3b8914562fa94011b82fc4fa822b626763babaf3669aaece1", size = 2759368 }, - { url = "https://files.pythonhosted.org/packages/77/81/53c882f3b93474a025302aead72b361128a0620dd629a17c86408fadfe31/kornia_rs-0.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:511d0487c853792f816c3a8bc8edbd550fe9160c6de0a22423c2161fac29d814", size = 2276544 }, - { url = "https://files.pythonhosted.org/packages/b6/60/7f514359f49514c3f798702425877ac64a278c65775067d6530766b2deda/kornia_rs-0.1.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a0f45987702e816f34bc0fcac253c504932d8ca877c9ab644d8445e7de737aec", size = 2562384 }, - { url = "https://files.pythonhosted.org/packages/d6/4d/21d75004dfdef346292b460119a3fab3668c4df38df6a5ffb2058283d16f/kornia_rs-0.1.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9bb863f3ff42919749b7db4a56f2adb076a10d3c57907305898c72e643fa3d5d", size = 2304921 }, - { url = "https://files.pythonhosted.org/packages/b0/ea/e0b14d6d1c6dcefaeacde37d1c2ef5098486e0d5de455e5e18e1063bf72d/kornia_rs-0.1.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54604bc8eb7d4d703eac19963378b4d6a72432a3f0da765edb1b0396d10def01", size = 2487791 }, - { url = "https://files.pythonhosted.org/packages/1c/5a/5969a793c1b2b0d9027b2f813ecadb568be3d319abe299df4f8e47ec7707/kornia_rs-0.1.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cdda9133297c4cff2c2c54be44d5c39bce715306d0bccb8ab1fae7c0dc7cf63", size = 2759632 }, - { url = "https://files.pythonhosted.org/packages/e2/59/5cad0e8562417ed54799fc72081c3ee6c63adc3e0d56b2c6b1fd2a1acb13/kornia_rs-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:689929d8dab80928feedbcdc2375060a3fe02b32fbf0dba12ae3fefb605fd089", size = 2276410 }, - { url = "https://files.pythonhosted.org/packages/ed/50/2f467cd49f47d6036f1199edc6145ba3f0a6729916c310451e1257baa13c/kornia_rs-0.1.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:889b4121f830d8827260f0246554f8bd722137d98db0bf3c2b0f5b1812cf5c63", size = 2554172 }, - { url = "https://files.pythonhosted.org/packages/e7/35/2eafb7923b4d5582f1c79ac0237195e030f9c460651b48d39dea96b5a627/kornia_rs-0.1.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a0834f6aa44a35b486fe9802516dde24ec5d8b3b51563f18b488098062074671", size = 2299529 }, - { url = "https://files.pythonhosted.org/packages/7c/d0/549ccabae5fe59256a4c749e78f844d4bdc17fda87d3fbe813fc8f430055/kornia_rs-0.1.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e52ead233c9d0b924eee4b906f26f32fde1e236a30723525dfb2b1a610bd48b", size = 2486013 }, - { url = "https://files.pythonhosted.org/packages/37/43/fd431c4278101dd25c44ae4c04166905671280f3d6b50d28dc062df544dc/kornia_rs-0.1.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e74a8901843e8e5e47f495927462288c7b7b8dc8a253495051c72a5fbda5f664", size = 2760478 }, - { url = "https://files.pythonhosted.org/packages/2f/f1/be747ef1b553950cdce6e63a2031e9b55ba3a793d2a8dd506cfcc9a83d10/kornia_rs-0.1.9-cp312-cp312-win_amd64.whl", hash = "sha256:fdfe0baa04800e541425730d03f3b3d217a1a6f0303926889b443b4562c0fda5", size = 2276689 }, - { url = "https://files.pythonhosted.org/packages/ad/12/83ebd43c8fb42d5ad1e66ce66af9c80e9b99e95758f7b0b1ceb8be8eebde/kornia_rs-0.1.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e82f5d90ad5b3d02d28535d19bf612e25ca559003d56fad1d12cd173be5d8418", size = 2554277 }, - { url = "https://files.pythonhosted.org/packages/6b/b3/d61868697048f92df3b1ca0fbee791b92c305ba7fae3755521d44dd9ffe1/kornia_rs-0.1.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:876f0805ed8d05bd94d6b9a6c43161cdc74bc5c4508f5b737d6975d1dcf9016d", size = 2299896 }, - { url = "https://files.pythonhosted.org/packages/98/3a/5f37388ed4fae3f34702e1af25398cdd7c071b4bb250f780e7a2a6378e70/kornia_rs-0.1.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00021bb1941766e1e9e8c2cdbebcf33a08f8de3586e6efc791b9580a7e52b5ed", size = 2486072 }, - { url = "https://files.pythonhosted.org/packages/c3/d8/a286db51c1269f632b3e4fdb08cb7319476dbe576796be19fdfbdfd4df1c/kornia_rs-0.1.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feaa6d410bc4d0609f3c1f2c0c010743706c2f5eed6f5ded10f2b8fec32acdf", size = 2760634 }, - { url = "https://files.pythonhosted.org/packages/9a/6f/c3d7da7a06b71a1eb1eb017a1944bcdc9504ef5cdbb0e3345aceabd24b7a/kornia_rs-0.1.9-cp313-cp313-win_amd64.whl", hash = "sha256:4d063e9d74d2b198f080940cd1679cfb66004cd59bb7cc20e0bcf5874ce3d200", size = 2276574 }, - { url = "https://files.pythonhosted.org/packages/46/0d/09cb9deeac45e1ac89aacfa1a9417eba3a27015282ca841f4df197e727b2/kornia_rs-0.1.9-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f8d03da3dba89fd290f4df012f49262cde128e3682632b5c498b34909d875190", size = 2555142 }, - { url = "https://files.pythonhosted.org/packages/db/88/679db8feae7e2ad19317aaef84d6ff2129a37c601b47012ff67041daf7e3/kornia_rs-0.1.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cef15e7db4cd05ebedc25cd62616c38346f71f7e8a2fb751feacc8726e7e417e", size = 2301310 }, - { url = "https://files.pythonhosted.org/packages/19/58/1ba746ac8a8a25d30bf3205c59b7a008dbcbe95533d21a33ad9097afe56d/kornia_rs-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d6ea40aa2027f62e90c590112fc4be4a787092359ed041479d2b015c0483c97", size = 2487761 }, - { url = "https://files.pythonhosted.org/packages/0c/53/84462d5aaf0ad52cb30791d38e144ce8a79519c201ff204f903d54f1dd46/kornia_rs-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9282a70c9e81710f04fdb23617c73191324ccc070c7f96d62efb5bf66dc242a", size = 2758585 }, - { url = "https://files.pythonhosted.org/packages/5e/51/755dda17b63604816a34ee3b9336d1c6ccb54f3e9bcbc8aafd98412fcb8f/kornia_rs-0.1.9-cp313-cp313t-win_amd64.whl", hash = "sha256:cd6b9860ca2fd6103340f4f977f74fa96562d89563e36c00059f2d96c7cea464", size = 2275851 }, - { url = "https://files.pythonhosted.org/packages/a6/20/fb2bbf28667591d359577c8e372577d71bb269952b62bbd2fb0be9529118/kornia_rs-0.1.9-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:118272a83749e5739b7f04bbb229d1eb5c746fca1506c281a0b5d284f089af2e", size = 2569747 }, - { url = "https://files.pythonhosted.org/packages/b0/d2/677bb3d0de161ed210ffbf10d703a1a1f559e159c4e3a277259649485431/kornia_rs-0.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:58fab6c78f773ec4d6861c2d64a960d4f1b4dfce342c296da29209b35e579c69", size = 2305505 }, - { url = "https://files.pythonhosted.org/packages/d1/b3/faea7624d563b2f4f0b8b661d685c843222e37b060b1d570075c16e7057b/kornia_rs-0.1.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ff95fdbd291e1e9c43f0f1b2a4de1499a7c9f19b49215f736ff8f599089ec7b", size = 2489269 }, - { url = "https://files.pythonhosted.org/packages/68/1c/9eb323b6332aa1ca7ed52fc7882d390fc06ce49817e616aca3a6242f8c03/kornia_rs-0.1.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba1353d4883c72334c29fa171b1dd36bc5f647a48056979bd234db64b93acc8", size = 2761237 }, - { url = "https://files.pythonhosted.org/packages/c9/8b/57dee872a0a954da0f405fb7c50b531c34f3aaf3d393d2a8df7b7ae5edb6/kornia_rs-0.1.9-cp39-cp39-win_amd64.whl", hash = "sha256:c80afaafb9bb19847da23b4ec7d9e874fd85d3c9f863a47c49fe6140c9296a0f", size = 2277486 }, +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/17/8b3518ece01512a575b18f86b346879793d3dea264b314796bbd44d42e11/kornia_rs-0.1.10.tar.gz", hash = "sha256:5fd3fbc65240fa751975f5870b079f98e7fdcaa2885ea577b3da324d8bf01d81", size = 145610, upload-time = "2025-11-08T11:29:32.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/25/ab91a87cefd8d92a10749fa5d923366dfd2a2d240d9e57260e4218e9a5af/kornia_rs-0.1.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6757940733f13c52c4f142b9b11e3e9bd12ef9d209e333300602e86e21f5ae2f", size = 2811949, upload-time = "2025-11-08T11:30:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/61/6125a970249e04dd31cf3edf3fb0ceb98ea65269bc416ba48fd70f9a8f5e/kornia_rs-0.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68e90101a34ba2bbce920332b25fd4d25c8c546d9a241b2606a6d886df2dd1ed", size = 2078639, upload-time = "2025-11-08T11:30:06.363Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e4/c3484e5921a08e6368f0565c30646741fd12b46cb45c962d519cac3d12ad/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b0adb81858a8963455f2f0da01fcd6ea3296147b918306488edeeaf6bc2a979", size = 2204722, upload-time = "2025-11-08T11:29:33.566Z" }, + { url = "https://files.pythonhosted.org/packages/93/a4/2e6e33da900f19ae6411bfad41d317e56f1ae4f204bd73e61f0881bd5418/kornia_rs-0.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3e237a8428524ad9f86599c0c47b355bc3007669fe297ea3fbd59cd64bc2f7", size = 3042890, upload-time = "2025-11-08T11:29:50.15Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/5e171c98b742139bebd1bd593d768e3c045f824bf0ae14190b63f0ac0acc/kornia_rs-0.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:1d300ea6d4666e47302fba6cc438556d91e37ce41caf291a9a04a8f74c231d0b", size = 2544572, upload-time = "2025-11-08T11:30:32.32Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6c/8248f08c90a10d6b8ca2e74783da8df7fa509f46b64a3b4fbb7dd0ac4e9c/kornia_rs-0.1.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0809277e51156d59be3c39605ba9659e94f7a4cf3b0b6c035ec2f06f6067881", size = 2811606, upload-time = "2025-11-08T11:30:21.346Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/29e5710cbc5d01c155ee1fd7621db48b94378a7ae394741bb34a6bfb36d9/kornia_rs-0.1.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ecf2ba0291cc1bb178073d56e46b16296a8864a20272b63af02ee88771cb574", size = 2076141, upload-time = "2025-11-08T11:30:07.527Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/0b3e90b9d0a25e6211c7ac9fa1dfed4db1306a812c359ee49678390a1bdc/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d874ca12dd58871f9849672d9bf9fa998398470a88b52d61223ce2133b196662", size = 2205562, upload-time = "2025-11-08T11:29:35.353Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/315f358b2a2c29d9af3a73f3d1973c2fd8e0cdeb65a57af98643e66fa7c8/kornia_rs-0.1.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f332a2a034cc791006f25c2d85e342a060887145e9236e8e43562badcadededf", size = 3042197, upload-time = "2025-11-08T11:29:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b8/0ddbdf1d35fec3ef24f5b8cc29eb633ce5ce16c94c9fb090408c1280abe9/kornia_rs-0.1.10-cp312-cp312-win_amd64.whl", hash = "sha256:34111ce1c8abe930079b4b0aeb8d372f876c621a867ed03f77181de685e71a8f", size = 2539656, upload-time = "2025-11-08T11:30:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/90/01/1d658b11635431f8c31f416c90ca99befdc1f4fdd20e91a05b480b9c0ea8/kornia_rs-0.1.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:950a943f91c2cff94d80282886b0d48bbc15ef4a7cc4b15ac819724dfdb2f414", size = 2811810, upload-time = "2025-11-08T11:30:22.497Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/bd970ded1d819557cc33055d982b1847eb385151ea5b0c915c16ed74f5c0/kornia_rs-0.1.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:63b802aaf95590276d3426edc6d23ff11caf269d2bc2ec37cb6c679b7b2a8ee0", size = 2076195, upload-time = "2025-11-08T11:30:08.726Z" }, + { url = "https://files.pythonhosted.org/packages/c1/10/afd700455105fdba5b043d724f3a65ca36259b89c736a3b71d5a03103808/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38087da7cdf2bffe10530c0d53335dd1fc107fae6521f2dd4797c6522b6d11b3", size = 2205781, upload-time = "2025-11-08T11:29:36.8Z" }, + { url = "https://files.pythonhosted.org/packages/25/16/ec8dc3ce1d79660ddd6a186a77037e0c3bf61648e6c72250280b648fb291/kornia_rs-0.1.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa3464de8f9920d87415721c36840ceea23e054dcb54dd9f69189ba9eabce0c7", size = 3042272, upload-time = "2025-11-08T11:29:52.936Z" }, + { url = "https://files.pythonhosted.org/packages/f7/75/62785aba777d35a562a97a987d65840306fab7a8ecd2d928dd8ac779e29b/kornia_rs-0.1.10-cp313-cp313-win_amd64.whl", hash = "sha256:c57d157bebe64c22e2e44c72455b1c7365eee4d767e0c187dc28f22d072ebaf7", size = 2539802, upload-time = "2025-11-08T11:30:35.753Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d5/32b23d110109eb77b2dc952be75411f7e495da9105058e2cb08924a9cc90/kornia_rs-0.1.10-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:0b375f02422ef5986caed612799b4ddcc91f57f303906868b0a8c397a17e7607", size = 2810244, upload-time = "2025-11-08T11:30:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/5f/5ecde42b7c18e7df26c413848a98744427c3d370f5eed725b65f0bc356fb/kornia_rs-0.1.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2bcfa438d6b5dbe07d573afc980f2871f6639b2eac5148b8c0bba4f82357b9a", size = 2074220, upload-time = "2025-11-08T11:30:09.972Z" }, + { url = "https://files.pythonhosted.org/packages/18/6c/6fc86eb855bcc723924c3b91de98dc6c0f381987ce582e080b8eade3bc88/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:021b0a02b2356b12b3954a298f369ed4fe2dd522dcf8b6d72f91bf3bd8eea201", size = 2204672, upload-time = "2025-11-08T11:29:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/19/26/3ac706d1b36761c0f7a36934327079adcb42d761c8c219865123d49fc1b2/kornia_rs-0.1.10-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b07e2ae79e423b3248d94afd092e324c5ddfe3157fafc047531cc8bffa6a3", size = 3042797, upload-time = "2025-11-08T11:29:54.719Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/d62728d86bc67f5516249b154ff0bdfcf38a854dae284ff0ce62da87af99/kornia_rs-0.1.10-cp313-cp313t-win_amd64.whl", hash = "sha256:b80a037e34d63cb021bcd5fc571e41aff804a2981311f66e883768c6b8e5f8de", size = 2543855, upload-time = "2025-11-08T11:30:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/8ed1288a51d2ad71a6c01152ceccdd2d92f21692dfd2304b1ae9383496fa/kornia_rs-0.1.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:119eb434d1384257cae6c1ee9444e1aa1b0fda617f6d5a79fef3f145fdac70ac", size = 2809873, upload-time = "2025-11-08T11:30:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/fd5f919723aaa69ec5c1e60b10b7904a9126be5b9d6ccc0267fa42ca77e0/kornia_rs-0.1.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60bca692911e5969e51d256299ecc6e90d32b9a2c5bf7bd1c7eb8f096cb9234b", size = 2074360, upload-time = "2025-11-08T11:30:11.327Z" }, + { url = "https://files.pythonhosted.org/packages/43/ec/7987aa5fb7d188180866bd8dafa5bb5b1f00a74ba738bb4e2abe63c589ac/kornia_rs-0.1.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61f126644f49ff9947d9402126edacfeeb4b47c0999a7af487d27ce4fc4cbc2a", size = 2206111, upload-time = "2025-11-08T11:29:40.608Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/cb73b7e87a07b2af1146988d159d48722f0a28f550f920397c8964ab7c19/kornia_rs-0.1.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:614aeffdb1d39c4041ace0e0fc318b36eb211f6c133912984e946174e67dbb42", size = 3041436, upload-time = "2025-11-08T11:29:55.984Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/9f50fce2d8e9edd6b2d09908b6d5613f9ead992bf2e80060e080f2e7d64d/kornia_rs-0.1.10-cp314-cp314-win_amd64.whl", hash = "sha256:6de4e73b1c90cc76b7486491866eb9e61e5cf34d3a4016957d4563ac7d3ee39a", size = 2544067, upload-time = "2025-11-08T11:30:38.638Z" }, + { url = "https://files.pythonhosted.org/packages/d1/8f/45895818f3c7a5009737119b075db6b88bbf00938275611bc5d2cfbd0b2a/kornia_rs-0.1.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f0db8b41ae03a746bb0dcb970d5ff2fd66213adb4a3b4de1186fe86205698e89", size = 2806089, upload-time = "2025-11-08T11:30:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/38/af/831e79b45702f8b6102438b1ff9b44a912669890cdf209cd275257f6d655/kornia_rs-0.1.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b63ee175125892ef18027bd3a43b447fd53f9bf42cea4d6f699ab4e69cf3f16", size = 2064116, upload-time = "2025-11-08T11:30:13.481Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/e92606e0fa9a1b52ecf57faf322dcc076ae35315b4e1870d380fd64926d7/kornia_rs-0.1.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68eb25ba4639fa5e1cd94a10fb6410c8840c9f0162e5912d834c4a8c7c174493", size = 2197890, upload-time = "2025-11-08T11:29:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fa/a2adce992b5eb65ef8adfc6f4465989948bfa8b875638e17c214541af25a/kornia_rs-0.1.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc18ba839f5c10ceb4757342ee7530cef8a0ecdd20486b8bbe14a56f72fa7037", size = 3040852, upload-time = "2025-11-08T11:29:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/40a3e3a235c370f5f61a8f9a7bdedf47d1bdd8f7d7e145e551545babff6b/kornia_rs-0.1.10-cp314-cp314t-win_amd64.whl", hash = "sha256:257eb0a780f990c0c44ac47acb77504dd95b8df0c592fd31354da1228df6678d", size = 2543609, upload-time = "2025-11-08T11:30:40.1Z" }, ] [[package]] name = "lazy-loader" -version = "0.4" +version = "0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431 } +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097 }, + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, ] [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838 }, - { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827 }, - { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098 }, - { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261 }, - { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621 }, - { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231 }, - { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279 }, - { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405 }, - { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169 }, - { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691 }, - { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503 }, - { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346 }, - { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139 }, - { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609 }, - { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285 }, - { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507 }, - { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104 }, - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086 }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613 }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008 }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915 }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890 }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817 }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916 }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274 }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757 }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028 }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487 }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688 }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043 }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569 }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270 }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606 }, - { url = "https://files.pythonhosted.org/packages/1e/04/acd238222ea25683e43ac7113facc380b3aaf77c53e7d88c4f544cef02ca/lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e", size = 8082189 }, - { url = "https://files.pythonhosted.org/packages/d6/4e/cc7fe9ccb9999cc648492ce970b63c657606aefc7d0fba46b17aa2ba93fb/lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40", size = 4384950 }, - { url = "https://files.pythonhosted.org/packages/56/bf/acd219c489346d0243a30769b9d446b71e5608581db49a18c8d91a669e19/lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729", size = 5209823 }, - { url = "https://files.pythonhosted.org/packages/57/51/ec31cd33175c09aa7b93d101f56eed43d89e15504455d884d021df7166a7/lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87", size = 4931808 }, - { url = "https://files.pythonhosted.org/packages/e5/68/865d229f191514da1777125598d028dc88a5ea300d68c30e1f120bfd01bd/lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd", size = 5086067 }, - { url = "https://files.pythonhosted.org/packages/82/01/4c958c5848b4e263cd9e83dff6b49f975a5a0854feb1070dfe0bdcdf70a0/lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433", size = 4929026 }, - { url = "https://files.pythonhosted.org/packages/55/31/5327d8af74d7f35e645b40ae6658761e1fee59ebecaa6a8d295e495c2ca9/lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140", size = 5134245 }, - { url = "https://files.pythonhosted.org/packages/6f/c9/204eba2400beb0016dacc2c5335ecb1e37f397796683ffdb7f471e86bddb/lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5", size = 5001020 }, - { url = "https://files.pythonhosted.org/packages/07/53/979165f50a853dab1cf3b9e53105032d55f85c5993f94afc4d9a61a22877/lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142", size = 5192346 }, - { url = "https://files.pythonhosted.org/packages/17/2b/f37b5ae28949143f863ba3066b30eede6107fc9a503bd0d01677d4e2a1e0/lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6", size = 3478275 }, - { url = "https://files.pythonhosted.org/packages/9a/d5/b795a183680126147665a8eeda8e802c180f2f7661aa9a550bba5bcdae63/lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1", size = 3806275 }, - { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319 }, - { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614 }, - { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273 }, - { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552 }, - { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091 }, - { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862 }, - { url = "https://files.pythonhosted.org/packages/ad/fb/d19b67e4bb63adc20574ba3476cf763b3514df1a37551084b890254e4b15/lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530", size = 3891034 }, - { url = "https://files.pythonhosted.org/packages/c9/5d/6e1033ee0cdb2f9bc93164f9df14e42cb5bbf1bbed3bf67f687de2763104/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6", size = 4207420 }, - { url = "https://files.pythonhosted.org/packages/f3/4b/23ac79efc32d913259d66672c5f93daac7750a3d97cdc1c1a9a5d1c1b46c/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877", size = 4305106 }, - { url = "https://files.pythonhosted.org/packages/a4/7a/fe558bee63a62f7a75a52111c0a94556c1c1bdcf558cd7d52861de558759/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8", size = 4205587 }, - { url = "https://files.pythonhosted.org/packages/ed/5b/3207e6bd8d67c952acfec6bac9d1fa0ee353202e7c40b335ebe00879ab7d/lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d", size = 4329077 }, - { url = "https://files.pythonhosted.org/packages/a1/25/d381abcfd00102d3304aa191caab62f6e3bcbac93ee248771db6be153dfd/lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987", size = 3486416 }, +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, + { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, + { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, + { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, + { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, ] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "2.1.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, - { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, - { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, - { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, - { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, - { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, - { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, - { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, - { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, - { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, - { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, - { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, - { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, - { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, - { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, - { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, - { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, - { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, - { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, - { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, - { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, - { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, - { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, - { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, - { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, - { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, - { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, - { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, - { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, - { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, - { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, - { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, - { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, - { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, - { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, - { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, - { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, - { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, - { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, ] [[package]] name = "matplotlib" -version = "3.9.4" +version = "3.10.9" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089 }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600 }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138 }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711 }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622 }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211 }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430 }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045 }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906 }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873 }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566 }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065 }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131 }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365 }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707 }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761 }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284 }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160 }, - { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499 }, - { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802 }, - { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802 }, - { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880 }, - { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637 }, - { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311 }, - { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989 }, - { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417 }, - { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258 }, - { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849 }, - { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152 }, - { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987 }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919 }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486 }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838 }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492 }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500 }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962 }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995 }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300 }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423 }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624 }, -] - -[[package]] -name = "matplotlib" -version = "3.10.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862 }, - { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149 }, - { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719 }, - { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801 }, - { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111 }, - { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213 }, - { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, - { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689 }, - { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466 }, - { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252 }, - { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321 }, - { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972 }, - { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954 }, - { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318 }, - { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132 }, - { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633 }, - { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031 }, - { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988 }, - { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034 }, - { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223 }, - { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985 }, - { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109 }, - { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082 }, - { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699 }, - { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044 }, - { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896 }, - { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702 }, - { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "more-itertools" -version = "10.7.0" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] -name = "networkx" -version = "3.2.1" +name = "mypy-extensions" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "networkx" -version = "3.4.2" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, -] - -[[package]] -name = "networkx" -version = "3.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "nh3" -version = "0.2.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678 }, - { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774 }, - { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012 }, - { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619 }, - { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384 }, - { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908 }, - { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180 }, - { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747 }, - { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908 }, - { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133 }, - { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328 }, - { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020 }, - { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878 }, - { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460 }, - { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369 }, - { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036 }, - { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559 }, - { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591 }, - { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670 }, - { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093 }, - { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623 }, - { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283 }, +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/5f/1d19bdc7d27238e37f3672cdc02cb77c56a4a86d140cd4f4f23c90df6e16/nh3-0.3.5.tar.gz", hash = "sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8", size = 20743, upload-time = "2026-04-25T10:44:16.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b0/8587ac42a9627ab88e7e221601f1dfccbf4db80b2a29222ea63266dc9abc/nh3-0.3.5-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a", size = 1420126, upload-time = "2026-04-25T10:43:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/1dbc4d0c43f12e8c1784ede17eaee6f061d4fbe5505757c65c49b2ceab95/nh3-0.3.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263", size = 793943, upload-time = "2026-04-25T10:43:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/47/9f/d6758d7a14ee964bf439cc35ae4fa24a763a93399c8ef6f22bd11d532d29/nh3-0.3.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9", size = 841150, upload-time = "2026-04-25T10:43:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/d5d1ae8374612c98f390e1ea7c610fa6c9716259a03bbf4d15b269f40073/nh3-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588", size = 1008415, upload-time = "2026-04-25T10:43:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/d13a9c3fd2d9c131a2a281737380e9379eb0f8c33fea24c2b923aaafbb15/nh3-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd", size = 1092706, upload-time = "2026-04-25T10:43:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/2f3add7f8680fcc896afa6a675cb2bab09982853ee8af40bad621f6b61c4/nh3-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17", size = 1048346, upload-time = "2026-04-25T10:43:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c3/2f9e4ffa82863074d1361bfe949bc46393d91b3411579dfbbd090b24cac5/nh3-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29", size = 1029038, upload-time = "2026-04-25T10:43:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/e8/10/2804deb3f3315184c9cae41702e293c87524b5a21f766b07d7fe3ffbcfbb/nh3-0.3.5-cp314-cp314t-win32.whl", hash = "sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88", size = 603263, upload-time = "2026-04-25T10:43:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/f6685248b49f7548fc9a8c335ab3a52f68610b72e8a61576447151e4e2e6/nh3-0.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f", size = 616866, upload-time = "2026-04-25T10:43:51.005Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/d8c9018635d4acfefde6b68470daa510eed715a350cbaa2f928ba0609f81/nh3-0.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79", size = 602566, upload-time = "2026-04-25T10:43:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/d162e99746a2fb1d98bb0ef23af3e201b156cf09f7de867c7390c8fe1c06/nh3-0.3.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101", size = 1442393, upload-time = "2026-04-25T10:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/25/8c/072120d506978ab053e1732d0efa7c86cb478fee0ee098fda0ac0d31cb34/nh3-0.3.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878", size = 837722, upload-time = "2026-04-25T10:43:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/d4e06e28c5ad1c4b065f89737d02631bd49f1660b6ebcf17a87ffcd201da/nh3-0.3.5-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93", size = 822872, upload-time = "2026-04-25T10:43:56.581Z" }, + { url = "https://files.pythonhosted.org/packages/0a/62/50659255213f241ec5797ae7427464c969397373e83b3659372b341ae869/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3", size = 1100031, upload-time = "2026-04-25T10:43:58.098Z" }, + { url = "https://files.pythonhosted.org/packages/00/7a/a12ae77593b2fcf3be25df7bc1c01967d0de448bdb4b6c7ec80fe4f5a74f/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4", size = 1057669, upload-time = "2026-04-25T10:43:59.328Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/5647dc04c0233192a3956fc91708822b21403a06508cacf78083c68e7bf0/nh3-0.3.5-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b", size = 914795, upload-time = "2026-04-25T10:44:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0e/bf298920729f216adcb002acf7ea01b90842603d2e4e2ce9b900d9ee8fab/nh3-0.3.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1", size = 806976, upload-time = "2026-04-25T10:44:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/85/01/26761e1dc2b848e65a62c19e5d39ad446283287cd4afddc89f364ab86bc9/nh3-0.3.5-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc", size = 834904, upload-time = "2026-04-25T10:44:03.454Z" }, + { url = "https://files.pythonhosted.org/packages/33/53/0766113e679540ac1edc1b82b1295aecd321eeb75d6fead70109a838b6ee/nh3-0.3.5-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b", size = 857159, upload-time = "2026-04-25T10:44:05.003Z" }, + { url = "https://files.pythonhosted.org/packages/58/36/734d353dfaf292fed574b8b3092f0ef79dc6404f3879f7faaa61a4701fad/nh3-0.3.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4", size = 1018600, upload-time = "2026-04-25T10:44:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/d9c59c1b49669fcb7bababa55df82385f029ad5c2651f583c3a1141cfdd1/nh3-0.3.5-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d", size = 1103530, upload-time = "2026-04-25T10:44:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/cdd210bfb8d9d43fb02fc3c868336b9955934d8e15e66eb1d15a147b8af0/nh3-0.3.5-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad", size = 1061754, upload-time = "2026-04-25T10:44:09.362Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/7a39e72e668c8445bdd95e494b3e21cfdddc68329be8ea3522c8befb46c4/nh3-0.3.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9", size = 1040938, upload-time = "2026-04-25T10:44:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/fc2f9ed208a3801a319f59b5fea03cdc20cf3bd8af14be930d3a8de01224/nh3-0.3.5-cp38-abi3-win32.whl", hash = "sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f", size = 611445, upload-time = "2026-04-25T10:44:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/db/1a/e4c9b5e2ae13e6092c9ec16d8ca30646cb01fcdea245f36c5b08fd21fbd5/nh3-0.3.5-cp38-abi3-win_amd64.whl", hash = "sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30", size = 626502, upload-time = "2026-04-25T10:44:13.682Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/19cd0671d1ba2762fb388fc149697d20d0568ccfeef833b11280a619e526/nh3-0.3.5-cp38-abi3-win_arm64.whl", hash = "sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b", size = 611069, upload-time = "2026-04-25T10:44:14.934Z" }, ] [[package]] name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, - { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301 }, - { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216 }, - { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281 }, - { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516 }, - { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132 }, - { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181 }, - { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360 }, - { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633 }, - { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961 }, - { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071 }, - { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730 }, +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] name = "nvidia-cublas-cu12" -version = "12.6.4.1" +version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322 }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, ] [[package]] name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980 }, - { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972 }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, ] [[package]] name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" +version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380 }, + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, ] [[package]] name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690 }, - { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, ] [[package]] name = "nvidia-cudnn-cu12" -version = "9.5.1.17" +version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386 }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, ] [[package]] name = "nvidia-cufft-cu12" -version = "11.3.0.4" +version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632 }, - { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622 }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, ] [[package]] name = "nvidia-cufile-cu12" -version = "1.11.1.6" +version = "1.13.1.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103 }, + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, ] [[package]] name = "nvidia-curand-cu12" -version = "10.3.7.77" +version = "10.3.9.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010 }, - { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000 }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, ] [[package]] name = "nvidia-cusolver-cu12" -version = "11.7.1.2" +version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790 }, - { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780 }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, ] [[package]] name = "nvidia-cusparse-cu12" -version = "12.5.4.2" +version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367 }, - { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357 }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, ] [[package]] name = "nvidia-cusparselt-cu12" -version = "0.6.3" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796 }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, ] [[package]] name = "nvidia-nccl-cu12" -version = "2.26.2" +version = "2.27.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755 }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, ] [[package]] name = "nvidia-nvjitlink-cu12" -version = "12.6.85" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971 }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, ] [[package]] name = "nvidia-nvtx-cu12" -version = "12.6.77" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276 }, - { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265 }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, ] [[package]] name = "ome-types" -version = "0.6.1" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, + { name = "pydantic-core", marker = "python_full_version >= '3.13'" }, { name = "pydantic-extra-types" }, { name = "xsdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/49/f9d313e03eefb4a7f81d1edba8493a4a40ef622bb48bab2f03600ee1ad86/ome_types-0.6.1.tar.gz", hash = "sha256:49e7a79ac90f8e4c33392f285491cb2577d41f2583bb879307c4f5ba9436e721", size = 121623 } +sdist = { url = "https://files.pythonhosted.org/packages/48/4c/d252c1619c733eec9b4d2d21fe369fd21a2594954b396bf4352edea1e272/ome_types-0.6.3.tar.gz", hash = "sha256:eef4138cda5edfdcb2a44cfb90b714a59ead1b69e4c5ce5f9892ad397ccaaa68", size = 121784, upload-time = "2025-11-26T00:28:24.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/c7/5d63372b13bfe9eaff26d540e7a1d2da5f0b4d82a5cbe634b638f1df61a3/ome_types-0.6.1-py3-none-any.whl", hash = "sha256:128f9bd5f52212cf37dad087ee4a7df939ccbf40a328d986e0bf9ac564abd4e9", size = 245142 }, + { url = "https://files.pythonhosted.org/packages/fc/6a/1000cad1700ab0af4d1b1d0a9c23c34badddb4f547c008bde2a6c61968f1/ome_types-0.6.3-py3-none-any.whl", hash = "sha256:ce9753ff351bbc534ee5c5038d3cf60b1e4c13d69ad2e6b5a5b75de2a52521a5", size = 245802, upload-time = "2025-11-26T00:28:22.853Z" }, ] [[package]] name = "opencv-contrib-python-headless" -version = "4.9.0.80" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/70/72ee1fb68f197755b90550b3d13006506d5e4a1b0a0ec6e1bbbb6000884c/opencv-contrib-python-headless-4.9.0.80.tar.gz", hash = "sha256:d027e598d4aca81ca20538dd0a95cf71b81df6cbacf088997c9ea819b1bb30c6", size = 152670129 } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/dc/597b2e31dab4c1fb909d06b8f041da01ef6d05140004ee7b2bcec81d65f0/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:83da3b09777d3aa4a35288040159fd94a6c56ff0c430a6f07b2bcaf05841a6de", size = 65421084 }, - { url = "https://files.pythonhosted.org/packages/49/c8/e0d0d8b184da98a36f02579547515383e2fb45b8976ebcd24a290442eca3/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b97b7ccb5ee9ad6a0dd7a764d7662716568a93171dcebd97de4d38aa2317d0e7", size = 44166990 }, - { url = "https://files.pythonhosted.org/packages/39/77/5d1c4a7986338ea12ce291ac16bdd18fa6ff6f2d254b1aedd20b33a12ac1/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0a061babe3bb86c0095c3057742fa23fe94dabbaf6a3ceb13151dd91d46bbae", size = 34581021 }, - { url = "https://files.pythonhosted.org/packages/b0/f8/f5072f03c6c5a5239e24ddb91e8449a4c0eff0b6c637da4709b67922c94e/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69052c3635be4fd245f0670b9330427596d71783d28c153824aef3d607b2a907", size = 55701910 }, - { url = "https://files.pythonhosted.org/packages/ba/1a/6e6eb54d1f954ef0971cc31b0569a2cd3884e3d0fa57bb427d14917d3ebf/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:140b48d876836abc143a933ebc6342e0ce457ae4c9a66b71578f92a044ee3801", size = 34234791 }, - { url = "https://files.pythonhosted.org/packages/fe/b9/b3e6bf31785cd495c51a80804638c62be0f9b84346ef8134a608006b2ed0/opencv_contrib_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:ddbbabc28d5d6053b6a3ccf607358d32c9952094847fc15a5382d93df1f5a688", size = 45207864 }, + { url = "https://files.pythonhosted.org/packages/70/b5/9af5b81d9279e9982e21dad52f8a6aec10f7c891ae1e3d3d1b3ce111f8e7/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:b0467988c2d56c283b00fb808e0b57f5db2e3ca7743164a3b3efc733bfa03d3a", size = 52041681, upload-time = "2026-02-05T07:01:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/f0aef27baf1f376007b018b00f6c304c42c20d31aa8491633c53b18912cb/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:79e503b77880d806a1b106ff8182c6f898347ccfd1db58ffc9a6369acc236c4c", size = 38830456, upload-time = "2026-02-05T07:01:56.47Z" }, + { url = "https://files.pythonhosted.org/packages/14/84/e6b3568f9147b4f114e881fb0e733fd97bdca15452feba78b510351584d1/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:449c1f00a685a3a7dff8d6fa93a70fbfe0de5537c24358ea03a1d996d12b33e8", size = 39355323, upload-time = "2026-02-05T10:17:31.671Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/89714580c617cf6e9f66eed9137759fc017ab6ab093c2a03227e8ee19578/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c9b028adc04f6579f37227eb1d648bead14fd6fefc58da86df37c8320351f7bd", size = 62147375, upload-time = "2026-02-05T10:20:03.076Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/abdd2ff2f8f07e9aa37c70edc9987b8aa63730ae70957c378f6f2e9d72d2/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdd974b34801f24735d18b1057cfaab1698d5cb02c9bba01dab7dc47201f2ef6", size = 40840722, upload-time = "2026-02-05T10:21:27.877Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/3194fdf035ef5123bd8cc3e3ad1a96c1ddeeedd0fdd12aaa0d2cfeb1649a/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9e26469baed9069f627ea56fa46819690c4545580362071dd09f1dcf47a40f2f", size = 66610130, upload-time = "2026-02-05T10:23:32.427Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/afe9b43c02b86b675a3d3ac6fc220473a88016e1acb487f5138efd2d2630/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:696a6dd84d309a499efc63644e375f035447b1da777faa2954f2dea7626cc0e7", size = 36708602, upload-time = "2026-02-05T07:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/9fdc70520eb915b46d816f9cc5415458b1bd114a65d7a7e657cbd9b863e5/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:dcbb12d04ae74f5dcd782e3b166e1894c6fbdfaaf30866588746205d2a0cde5a", size = 46345416, upload-time = "2026-02-05T07:02:33.446Z" }, ] [[package]] @@ -1824,170 +1480,192 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "et-xmlfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] [[package]] name = "packaging" -version = "25.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "pandas" -version = "2.3.0" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/2d/df6b98c736ba51b8eaa71229e8fcd91233a831ec00ab520e1e23090cc072/pandas-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:625466edd01d43b75b1883a64d859168e4556261a5035b32f9d743b67ef44634", size = 11527531 }, - { url = "https://files.pythonhosted.org/packages/77/1c/3f8c331d223f86ba1d0ed7d3ed7fcf1501c6f250882489cc820d2567ddbf/pandas-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675", size = 10774764 }, - { url = "https://files.pythonhosted.org/packages/1b/45/d2599400fad7fe06b849bd40b52c65684bc88fbe5f0a474d0513d057a377/pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2", size = 11711963 }, - { url = "https://files.pythonhosted.org/packages/66/f8/5508bc45e994e698dbc93607ee6b9b6eb67df978dc10ee2b09df80103d9e/pandas-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e", size = 12349446 }, - { url = "https://files.pythonhosted.org/packages/f7/fc/17851e1b1ea0c8456ba90a2f514c35134dd56d981cf30ccdc501a0adeac4/pandas-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23c2b2dc5213810208ca0b80b8666670eb4660bbfd9d45f58592cc4ddcfd62e1", size = 12920002 }, - { url = "https://files.pythonhosted.org/packages/a1/9b/8743be105989c81fa33f8e2a4e9822ac0ad4aaf812c00fee6bb09fc814f9/pandas-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6", size = 13651218 }, - { url = "https://files.pythonhosted.org/packages/26/fa/8eeb2353f6d40974a6a9fd4081ad1700e2386cf4264a8f28542fd10b3e38/pandas-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2", size = 11082485 }, - { url = "https://files.pythonhosted.org/packages/96/1e/ba313812a699fe37bf62e6194265a4621be11833f5fce46d9eae22acb5d7/pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca", size = 11551836 }, - { url = "https://files.pythonhosted.org/packages/1b/cc/0af9c07f8d714ea563b12383a7e5bde9479cf32413ee2f346a9c5a801f22/pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef", size = 10807977 }, - { url = "https://files.pythonhosted.org/packages/ee/3e/8c0fb7e2cf4a55198466ced1ca6a9054ae3b7e7630df7757031df10001fd/pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d", size = 11788230 }, - { url = "https://files.pythonhosted.org/packages/14/22/b493ec614582307faf3f94989be0f7f0a71932ed6f56c9a80c0bb4a3b51e/pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46", size = 12370423 }, - { url = "https://files.pythonhosted.org/packages/9f/74/b012addb34cda5ce855218a37b258c4e056a0b9b334d116e518d72638737/pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33", size = 12990594 }, - { url = "https://files.pythonhosted.org/packages/95/81/b310e60d033ab64b08e66c635b94076488f0b6ce6a674379dd5b224fc51c/pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c", size = 13745952 }, - { url = "https://files.pythonhosted.org/packages/25/ac/f6ee5250a8881b55bd3aecde9b8cfddea2f2b43e3588bca68a4e9aaf46c8/pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a", size = 11094534 }, - { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865 }, - { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154 }, - { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180 }, - { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493 }, - { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733 }, - { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406 }, - { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199 }, - { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913 }, - { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249 }, - { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359 }, - { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789 }, - { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734 }, - { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381 }, - { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135 }, - { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356 }, - { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674 }, - { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876 }, - { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182 }, - { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686 }, - { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847 }, - { url = "https://files.pythonhosted.org/packages/38/86/d786690bd1d666d3369355a173b32a4ab7a83053cbb2d6a24ceeedb31262/pandas-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9efc0acbbffb5236fbdf0409c04edce96bec4bdaa649d49985427bd1ec73e085", size = 11552206 }, - { url = "https://files.pythonhosted.org/packages/9c/2f/99f581c1c5b013fcfcbf00a48f5464fb0105da99ea5839af955e045ae3ab/pandas-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75651c14fde635e680496148a8526b328e09fe0572d9ae9b638648c46a544ba3", size = 10796831 }, - { url = "https://files.pythonhosted.org/packages/5c/be/3ee7f424367e0f9e2daee93a3145a18b703fbf733ba56e1cf914af4b40d1/pandas-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5be867a0541a9fb47a4be0c5790a4bccd5b77b92f0a59eeec9375fafc2aa14", size = 11736943 }, - { url = "https://files.pythonhosted.org/packages/83/95/81c7bb8f1aefecd948f80464177a7d9a1c5e205c5a1e279984fdacbac9de/pandas-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84141f722d45d0c2a89544dd29d35b3abfc13d2250ed7e68394eda7564bd6324", size = 12366679 }, - { url = "https://files.pythonhosted.org/packages/d5/7a/54cf52fb454408317136d683a736bb597864db74977efee05e63af0a7d38/pandas-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f95a2aef32614ed86216d3c450ab12a4e82084e8102e355707a1d96e33d51c34", size = 12924072 }, - { url = "https://files.pythonhosted.org/packages/0a/bf/25018e431257f8a42c173080f9da7c592508269def54af4a76ccd1c14420/pandas-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e0f51973ba93a9f97185049326d75b942b9aeb472bec616a129806facb129ebb", size = 13696374 }, - { url = "https://files.pythonhosted.org/packages/db/84/5ffd2c447c02db56326f5c19a235a747fae727e4842cc20e1ddd28f990f6/pandas-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b198687ca9c8529662213538a9bb1e60fa0bf0f6af89292eb68fea28743fcd5a", size = 11104735 }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "pillow" -version = "11.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442 }, - { url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553 }, - { url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503 }, - { url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648 }, - { url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937 }, - { url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802 }, - { url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717 }, - { url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874 }, - { url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717 }, - { url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204 }, - { url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767 }, - { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, - { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, - { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, - { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, - { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, - { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, - { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, - { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, - { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, - { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, - { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, - { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, - { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, - { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, - { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, - { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, - { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, - { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, - { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, - { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, - { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, - { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, - { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, - { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, - { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, - { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, - { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, - { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, - { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, - { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, - { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, - { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, - { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, - { url = "https://files.pythonhosted.org/packages/21/3a/c1835d1c7cf83559e95b4f4ed07ab0bb7acc689712adfce406b3f456e9fd/pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8", size = 3198391 }, - { url = "https://files.pythonhosted.org/packages/b6/4d/dcb7a9af3fc1e8653267c38ed622605d9d1793349274b3ef7af06457e257/pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909", size = 3030573 }, - { url = "https://files.pythonhosted.org/packages/9d/29/530ca098c1a1eb31d4e163d317d0e24e6d2ead907991c69ca5b663de1bc5/pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928", size = 4398677 }, - { url = "https://files.pythonhosted.org/packages/8b/ee/0e5e51db34de1690264e5f30dcd25328c540aa11d50a3bc0b540e2a445b6/pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79", size = 4484986 }, - { url = "https://files.pythonhosted.org/packages/93/7d/bc723b41ce3d2c28532c47678ec988974f731b5c6fadd5b3a4fba9015e4f/pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35", size = 4501897 }, - { url = "https://files.pythonhosted.org/packages/be/0b/532e31abc7389617ddff12551af625a9b03cd61d2989fa595e43c470ec67/pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb", size = 4592618 }, - { url = "https://files.pythonhosted.org/packages/4c/f0/21ed6499a6216fef753e2e2254a19d08bff3747108ba042422383f3e9faa/pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a", size = 4570493 }, - { url = "https://files.pythonhosted.org/packages/68/de/17004ddb8ab855573fe1127ab0168d11378cdfe4a7ee2a792a70ff2e9ba7/pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36", size = 4647748 }, - { url = "https://files.pythonhosted.org/packages/c7/23/82ecb486384bb3578115c509d4a00bb52f463ee700a5ca1be53da3c88c19/pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67", size = 2331731 }, - { url = "https://files.pythonhosted.org/packages/58/bb/87efd58b3689537a623d44dbb2550ef0bb5ff6a62769707a0fe8b1a7bdeb/pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1", size = 2676346 }, - { url = "https://files.pythonhosted.org/packages/80/08/dc268475b22887b816e5dcfae31bce897f524b4646bab130c2142c9b2400/pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e", size = 2414623 }, - { url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727 }, - { url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833 }, - { url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472 }, - { url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976 }, - { url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133 }, - { url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555 }, - { url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713 }, - { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, - { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, - { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, - { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, - { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, - { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, - { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -1999,50 +1677,23 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/dd/1b2ae6551a32bf8ae26b90c6e191a889bee5050bf23c88021761fbca03d1/pqdm-0.2.0.tar.gz", hash = "sha256:d99d01fe498d327b440ebfe08c14c84e0dc9ecce6172ef9a31f96bb1aaf4e9e3", size = 13899 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/b7/720988acdc9b5805cd1ef311aa75d6fd1c5438b87f4add1ec8d11f78d63b/pqdm-0.2.0-py2.py3-none-any.whl", hash = "sha256:0da33a22ebee349a047abf8ef7fd00d85403638101d5e374b421a74188231b62", size = 6765 }, -] - -[[package]] -name = "progressbar2" -version = "4.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/24/3587e795fc590611434e4bcb9fbe0c3dddb5754ce1a20edfd86c587c0004/progressbar2-4.5.0.tar.gz", hash = "sha256:6662cb624886ed31eb94daf61e27583b5144ebc7383a17bae076f8f4f59088fb", size = 101449 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/94/448f037fb0ffd0e8a63b625cf9f5b13494b88d15573a987be8aaa735579d/progressbar2-4.5.0-py3-none-any.whl", hash = "sha256:625c94a54e63915b3959355e6d4aacd63a00219e5f3e2b12181b76867bf6f628", size = 57132 }, -] - -[[package]] -name = "psutil" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/dd/1b2ae6551a32bf8ae26b90c6e191a889bee5050bf23c88021761fbca03d1/pqdm-0.2.0.tar.gz", hash = "sha256:d99d01fe498d327b440ebfe08c14c84e0dc9ecce6172ef9a31f96bb1aaf4e9e3", size = 13899, upload-time = "2022-02-14T10:16:20.675Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, + { url = "https://files.pythonhosted.org/packages/9e/b7/720988acdc9b5805cd1ef311aa75d6fd1c5438b87f4add1ec8d11f78d63b/pqdm-0.2.0-py2.py3-none-any.whl", hash = "sha256:0da33a22ebee349a047abf8ef7fd00d85403638101d5e374b421a74188231b62", size = 6765, upload-time = "2022-02-14T10:16:18.824Z" }, ] [[package]] name = "pycparser" -version = "2.22" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" -version = "2.11.5" +version = "2.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2050,167 +1701,158 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 }, + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, - { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677 }, - { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735 }, - { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467 }, - { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041 }, - { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503 }, - { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079 }, - { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508 }, - { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693 }, - { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224 }, - { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403 }, - { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331 }, - { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571 }, - { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504 }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, - { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034 }, - { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578 }, - { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858 }, - { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498 }, - { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428 }, - { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854 }, - { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859 }, - { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059 }, - { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661 }, +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, ] [[package]] name = "pydantic-extra-types" -version = "2.10.5" +version = "2.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ba/4178111ec4116c54e1dc7ecd2a1ff8f54256cdbd250e576882911e8f710a/pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8", size = 138429 } +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/1a/5f4fd9e7285f10c44095a4f9fe17d0f358d1702a7c74a9278c794e8a7537/pydantic_extra_types-2.10.5-py3-none-any.whl", hash = "sha256:b60c4e23d573a69a4f1a16dd92888ecc0ef34fb0e655b4f305530377fa70e7a8", size = 38315 }, + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyparsing" -version = "3.2.3" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pytest" -version = "8.4.0" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -2220,48 +1862,61 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-utils" -version = "3.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/4c/ef8b7b1046d65c1f18ca31e5235c7d6627ca2b3f389ab1d44a74d22f5cc9/python_utils-3.9.1.tar.gz", hash = "sha256:eb574b4292415eb230f094cbf50ab5ef36e3579b8f09e9f2ba74af70891449a0", size = 35403 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl", hash = "sha256:0273d7363c7ad4b70999b2791d5ba6b55333d6f7a4e4c8b6b39fb82b5fab4613", size = 32078 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] -name = "pytz" -version = "2025.2" +name = "pytokens" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] name = "pyvips" -version = "3.0.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/a2/d8ecd2f7ffa084870ba071a584aac44800a89f3c77b305999be7dc8b7bb3/pyvips-3.0.0.tar.gz", hash = "sha256:79459975e4a16089b0eaafed26eb1400ae66ebc16d3ff3a7d2241abcf19dc9e8", size = 56806 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6a/282936de9faac6addf6bc8792c18e006489d0023ffd8856b8643f54d0558/pyvips-3.1.1.tar.gz", hash = "sha256:84fe744d023b1084ac2516bb17064cacd41c7f8aabf8e524dd383534941b9301", size = 56951, upload-time = "2025-12-09T18:38:06.355Z" } [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] @@ -2273,14 +1928,14 @@ dependencies = [ { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2288,9 +1943,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -2300,680 +1955,487 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] name = "rfc3986" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, ] [[package]] name = "rich" -version = "14.0.0" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] -name = "roman-numerals-py" -version = "3.1.0" +name = "roman-numerals" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] -name = "scikit-image" -version = "0.24.0" +name = "ruff" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "imageio", marker = "python_full_version < '3.10'" }, - { name = "lazy-loader", marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tifffile", version = "2024.8.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/c5/bcd66bf5aae5587d3b4b69c74bee30889c46c9778e858942ce93a030e1f3/scikit_image-0.24.0.tar.gz", hash = "sha256:5d16efe95da8edbeb363e0c4157b99becbd650a60b77f6e3af5768b66cf007ab", size = 22693928 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/82/d4eaa6e441f28a783762093a3c74bcc4a67f1c65bf011414ad4ea85187d8/scikit_image-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3bc0264b6ab30b43c4179ee6156bc18b4861e78bb329dd8d16537b7bbf827a", size = 14051470 }, - { url = "https://files.pythonhosted.org/packages/65/15/1879307aaa2c771aa8ef8f00a171a85033bffc6b2553cfd2657426881452/scikit_image-0.24.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9c7a52e20cdd760738da38564ba1fed7942b623c0317489af1a598a8dedf088b", size = 13385822 }, - { url = "https://files.pythonhosted.org/packages/b6/b8/2d52864714b82122f4a36f47933f61f1cd2a6df34987873837f8064d4fdf/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93f46e6ce42e5409f4d09ce1b0c7f80dd7e4373bcec635b6348b63e3c886eac8", size = 14216787 }, - { url = "https://files.pythonhosted.org/packages/40/2e/8b39cd2c347490dbe10adf21fd50bbddb1dada5bb0512c3a39371285eb62/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ee0af13435c57351a3397eb379e72164ff85161923eec0c38849fecf1b4764", size = 14866533 }, - { url = "https://files.pythonhosted.org/packages/99/89/3fcd68d034db5d29c974e964d03deec9d0fbf9410ff0a0b95efff70947f6/scikit_image-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ac7913b028b8aa780ffae85922894a69e33d1c0bf270ea1774f382fe8bf95e7", size = 12864601 }, - { url = "https://files.pythonhosted.org/packages/90/e3/564beb0c78bf83018a146dfcdc959c99c10a0d136480b932a350c852adbc/scikit_image-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:272909e02a59cea3ed4aa03739bb88df2625daa809f633f40b5053cf09241831", size = 14020429 }, - { url = "https://files.pythonhosted.org/packages/3c/f6/be8b16d8ab6ebf19057877c2aec905cbd438dd92ca64b8efe9e9af008fa3/scikit_image-0.24.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:190ebde80b4470fe8838764b9b15f232a964f1a20391663e31008d76f0c696f7", size = 13371950 }, - { url = "https://files.pythonhosted.org/packages/b8/2e/3a949995f8fc2a65b15a4964373e26c5601cb2ea68f36b115571663e7a38/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c98cc695005faf2b79904e4663796c977af22586ddf1b12d6af2fa22842dc2", size = 14197889 }, - { url = "https://files.pythonhosted.org/packages/ad/96/138484302b8ec9a69cdf65e8d4ab47a640a3b1a8ea3c437e1da3e1a5a6b8/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa27b3a0dbad807b966b8db2d78da734cb812ca4787f7fbb143764800ce2fa9c", size = 14861425 }, - { url = "https://files.pythonhosted.org/packages/50/b2/d5e97115733e2dc657e99868ae0237705b79d0c81f6ced21b8f0799a30d1/scikit_image-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:dacf591ac0c272a111181afad4b788a27fe70d213cfddd631d151cbc34f8ca2c", size = 12843506 }, - { url = "https://files.pythonhosted.org/packages/16/19/45ad3b8b8ab8d275a48a9d1016c4beb1c2801a7a13e384268861d01145c1/scikit_image-0.24.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6fccceb54c9574590abcddc8caf6cefa57c13b5b8b4260ab3ff88ad8f3c252b3", size = 14101823 }, - { url = "https://files.pythonhosted.org/packages/6e/75/db10ee1bc7936b411d285809b5fe62224bbb1b324a03dd703582132ce5ee/scikit_image-0.24.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ccc01e4760d655aab7601c1ba7aa4ddd8b46f494ac46ec9c268df6f33ccddf4c", size = 13420758 }, - { url = "https://files.pythonhosted.org/packages/87/fd/07a7396962abfe22a285a922a63d18e4d5ec48eb5dbb1c06e96fb8fb6528/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18836a18d3a7b6aca5376a2d805f0045826bc6c9fc85331659c33b4813e0b563", size = 14256813 }, - { url = "https://files.pythonhosted.org/packages/2c/24/4bcd94046b409ac4d63e2f92e46481f95f5006a43e68f6ab2b24f5d70ab4/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8579bda9c3f78cb3b3ed8b9425213c53a25fa7e994b7ac01f2440b395babf660", size = 15013039 }, - { url = "https://files.pythonhosted.org/packages/d9/17/b561823143eb931de0f82fed03ae128ef954a9641309602ea0901c357f95/scikit_image-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:82ab903afa60b2da1da2e6f0c8c65e7c8868c60a869464c41971da929b3e82bc", size = 12949363 }, - { url = "https://files.pythonhosted.org/packages/93/8e/b6e50d8a6572daf12e27acbf9a1722fdb5e6bfc64f04a5fefa2a71fea0c3/scikit_image-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef04360eda372ee5cd60aebe9be91258639c86ae2ea24093fb9182118008d009", size = 14083010 }, - { url = "https://files.pythonhosted.org/packages/d6/6c/f528c6b80b4e9d38444d89f0d1160797d20c640b7a8cabd8b614ac600b79/scikit_image-0.24.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e9aadb442360a7e76f0c5c9d105f79a83d6df0e01e431bd1d5757e2c5871a1f3", size = 13414235 }, - { url = "https://files.pythonhosted.org/packages/52/03/59c52aa59b952aafcf19163e5d7e924e6156c3d9e9c86ea3372ad31d90f8/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e37de6f4c1abcf794e13c258dc9b7d385d5be868441de11c180363824192ff7", size = 14238540 }, - { url = "https://files.pythonhosted.org/packages/f0/cc/1a58efefb9b17c60d15626b33416728003028d5d51f0521482151a222560/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4688c18bd7ec33c08d7bf0fd19549be246d90d5f2c1d795a89986629af0a1e83", size = 14883801 }, - { url = "https://files.pythonhosted.org/packages/9d/63/233300aa76c65a442a301f9d2416a9b06c91631287bd6dd3d6b620040096/scikit_image-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:56dab751d20b25d5d3985e95c9b4e975f55573554bd76b0aedf5875217c93e69", size = 12891952 }, +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.10'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "tifffile", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922 }, - { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698 }, - { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634 }, - { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545 }, - { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908 }, - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057 }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335 }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783 }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376 }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698 }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000 }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893 }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389 }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435 }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474 }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841 }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862 }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785 }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119 }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116 }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801 }, -] - -[[package]] -name = "scikit-learn" -version = "1.6.1" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702 }, - { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765 }, - { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991 }, - { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182 }, - { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517 }, - { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620 }, - { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234 }, - { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155 }, - { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069 }, - { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809 }, - { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 }, - { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 }, - { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 }, - { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 }, - { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 }, - { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 }, - { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 }, - { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 }, - { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 }, - { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 }, - { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 }, - { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 }, - { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 }, - { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 }, - { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868 }, - { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062 }, - { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173 }, - { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605 }, - { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078 }, + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "tifffile", version = "2026.4.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, ] [[package]] name = "scikit-learn" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/3b/29fa87e76b1d7b3b77cc1fcbe82e6e6b8cd704410705b008822de530277c/scikit_learn-1.7.0.tar.gz", hash = "sha256:c01e869b15aec88e2cdb73d27f15bdbe03bce8e2fb43afbe77c45d399e73a5a3", size = 7178217 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/70/e725b1da11e7e833f558eb4d3ea8b7ed7100edda26101df074f1ae778235/scikit_learn-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9fe7f51435f49d97bd41d724bb3e11eeb939882af9c29c931a8002c357e8cdd5", size = 11728006 }, - { url = "https://files.pythonhosted.org/packages/32/aa/43874d372e9dc51eb361f5c2f0a4462915c9454563b3abb0d9457c66b7e9/scikit_learn-1.7.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0c93294e1e1acbee2d029b1f2a064f26bd928b284938d51d412c22e0c977eb3", size = 10726255 }, - { url = "https://files.pythonhosted.org/packages/f5/1a/da73cc18e00f0b9ae89f7e4463a02fb6e0569778120aeab138d9554ecef0/scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3755f25f145186ad8c403312f74fb90df82a4dfa1af19dc96ef35f57237a94", size = 12205657 }, - { url = "https://files.pythonhosted.org/packages/fb/f6/800cb3243dd0137ca6d98df8c9d539eb567ba0a0a39ecd245c33fab93510/scikit_learn-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2726c8787933add436fb66fb63ad18e8ef342dfb39bbbd19dc1e83e8f828a85a", size = 12877290 }, - { url = "https://files.pythonhosted.org/packages/4c/bd/99c3ccb49946bd06318fe194a1c54fb7d57ac4fe1c2f4660d86b3a2adf64/scikit_learn-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2539bb58886a531b6e86a510c0348afaadd25005604ad35966a85c2ec378800", size = 10713211 }, - { url = "https://files.pythonhosted.org/packages/5a/42/c6b41711c2bee01c4800ad8da2862c0b6d2956a399d23ce4d77f2ca7f0c7/scikit_learn-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ef09b1615e1ad04dc0d0054ad50634514818a8eb3ee3dee99af3bffc0ef5007", size = 11719657 }, - { url = "https://files.pythonhosted.org/packages/a3/24/44acca76449e391b6b2522e67a63c0454b7c1f060531bdc6d0118fb40851/scikit_learn-1.7.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7d7240c7b19edf6ed93403f43b0fcb0fe95b53bc0b17821f8fb88edab97085ef", size = 10712636 }, - { url = "https://files.pythonhosted.org/packages/9f/1b/fcad1ccb29bdc9b96bcaa2ed8345d56afb77b16c0c47bafe392cc5d1d213/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80bd3bd4e95381efc47073a720d4cbab485fc483966f1709f1fd559afac57ab8", size = 12242817 }, - { url = "https://files.pythonhosted.org/packages/c6/38/48b75c3d8d268a3f19837cb8a89155ead6e97c6892bb64837183ea41db2b/scikit_learn-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dbe48d69aa38ecfc5a6cda6c5df5abef0c0ebdb2468e92437e2053f84abb8bc", size = 12873961 }, - { url = "https://files.pythonhosted.org/packages/f4/5a/ba91b8c57aa37dbd80d5ff958576a9a8c14317b04b671ae7f0d09b00993a/scikit_learn-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8fa979313b2ffdfa049ed07252dc94038def3ecd49ea2a814db5401c07f1ecfa", size = 10717277 }, - { url = "https://files.pythonhosted.org/packages/70/3a/bffab14e974a665a3ee2d79766e7389572ffcaad941a246931c824afcdb2/scikit_learn-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2c7243d34aaede0efca7a5a96d67fddaebb4ad7e14a70991b9abee9dc5c0379", size = 11646758 }, - { url = "https://files.pythonhosted.org/packages/58/d8/f3249232fa79a70cb40595282813e61453c1e76da3e1a44b77a63dd8d0cb/scikit_learn-1.7.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f39f6a811bf3f15177b66c82cbe0d7b1ebad9f190737dcdef77cfca1ea3c19c", size = 10673971 }, - { url = "https://files.pythonhosted.org/packages/67/93/eb14c50533bea2f77758abe7d60a10057e5f2e2cdcf0a75a14c6bc19c734/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63017a5f9a74963d24aac7590287149a8d0f1a0799bbe7173c0d8ba1523293c0", size = 11818428 }, - { url = "https://files.pythonhosted.org/packages/08/17/804cc13b22a8663564bb0b55fb89e661a577e4e88a61a39740d58b909efe/scikit_learn-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f8a0b1e73e9a08b7cc498bb2aeab36cdc1f571f8ab2b35c6e5d1c7115d97d", size = 12505887 }, - { url = "https://files.pythonhosted.org/packages/68/c7/4e956281a077f4835458c3f9656c666300282d5199039f26d9de1dabd9be/scikit_learn-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:34cc8d9d010d29fb2b7cbcd5ccc24ffdd80515f65fe9f1e4894ace36b267ce19", size = 10668129 }, - { url = "https://files.pythonhosted.org/packages/9a/c3/a85dcccdaf1e807e6f067fa95788a6485b0491d9ea44fd4c812050d04f45/scikit_learn-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5b7974f1f32bc586c90145df51130e02267e4b7e77cab76165c76cf43faca0d9", size = 11559841 }, - { url = "https://files.pythonhosted.org/packages/d8/57/eea0de1562cc52d3196eae51a68c5736a31949a465f0b6bb3579b2d80282/scikit_learn-1.7.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:014e07a23fe02e65f9392898143c542a50b6001dbe89cb867e19688e468d049b", size = 10616463 }, - { url = "https://files.pythonhosted.org/packages/10/a4/39717ca669296dfc3a62928393168da88ac9d8cbec88b6321ffa62c6776f/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e7ced20582d3a5516fb6f405fd1d254e1f5ce712bfef2589f51326af6346e8", size = 11766512 }, - { url = "https://files.pythonhosted.org/packages/d5/cd/a19722241d5f7b51e08351e1e82453e0057aeb7621b17805f31fcb57bb6c/scikit_learn-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1babf2511e6ffd695da7a983b4e4d6de45dce39577b26b721610711081850906", size = 12461075 }, - { url = "https://files.pythonhosted.org/packages/f3/bc/282514272815c827a9acacbe5b99f4f1a4bc5961053719d319480aee0812/scikit_learn-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:5abd2acff939d5bd4701283f009b01496832d50ddafa83c90125a4e41c33e314", size = 10652517 }, - { url = "https://files.pythonhosted.org/packages/ea/78/7357d12b2e4c6674175f9a09a3ba10498cde8340e622715bcc71e532981d/scikit_learn-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e39d95a929b112047c25b775035c8c234c5ca67e681ce60d12413afb501129f7", size = 12111822 }, - { url = "https://files.pythonhosted.org/packages/d0/0c/9c3715393343f04232f9d81fe540eb3831d0b4ec351135a145855295110f/scikit_learn-1.7.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:0521cb460426c56fee7e07f9365b0f45ec8ca7b2d696534ac98bfb85e7ae4775", size = 11325286 }, - { url = "https://files.pythonhosted.org/packages/64/e0/42282ad3dd70b7c1a5f65c412ac3841f6543502a8d6263cae7b466612dc9/scikit_learn-1.7.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317ca9f83acbde2883bd6bb27116a741bfcb371369706b4f9973cf30e9a03b0d", size = 12380865 }, - { url = "https://files.pythonhosted.org/packages/4e/d0/3ef4ab2c6be4aa910445cd09c5ef0b44512e3de2cfb2112a88bb647d2cf7/scikit_learn-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:126c09740a6f016e815ab985b21e3a0656835414521c81fc1a8da78b679bdb75", size = 11549609 }, -] - -[[package]] -name = "scipy" -version = "1.13.1" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076 }, - { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232 }, - { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202 }, - { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335 }, - { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728 }, - { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588 }, - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805 }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687 }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638 }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931 }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145 }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227 }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301 }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348 }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062 }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311 }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493 }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 }, - { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927 }, - { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538 }, - { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190 }, - { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244 }, - { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637 }, - { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440 }, + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, ] [[package]] name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, -] - -[[package]] -name = "scyjava" -version = "1.12.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cjdk" }, - { name = "jgo" }, - { name = "jpype1" }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/36/4f9ad8594b8da8e33097f773d3156da79013c3a76fa3926ba0748daa95db/scyjava-1.12.0.tar.gz", hash = "sha256:45fb7d69146244a9f417fed4d828551e222a1e14340b4329dbee273bb13e77b6", size = 57086 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/b8/cd5c3ecb0aa69e7238b306b6c2876cd779a9882f5cebce5c2e97f1bece47/scyjava-1.12.0-py3-none-any.whl", hash = "sha256:b927ea594a937c7de5d344006fb5f5af3ab2b5cbf8dae856fa129984342a4468", size = 39370 }, +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] [[package]] name = "secretstorage" -version = "3.3.3" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, - { name = "jeepney", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, + { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "setuptools" -version = "69.5.1" +version = "82.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/4f/b10f707e14ef7de524fe1f8988a294fb262a29c9b5b12275c7e188864aed/setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", size = 2291314 } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/29/13965af254e3373bceae8fb9a0e6ea0d0e571171b80d6646932131d6439b/setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32", size = 894566 }, + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] name = "shapely" -version = "2.0.7" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/c0/a911d1fd765d07a2b6769ce155219a281bfbe311584ebe97340d75c5bdb1/shapely-2.0.7.tar.gz", hash = "sha256:28fe2997aab9a9dc026dc6a355d04e85841546b2a5d232ed953e3321ab958ee5", size = 283413 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/2e/02c694d6ddacd4f13b625722d313d2838f23c5b988cbc680132983f73ce3/shapely-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:33fb10e50b16113714ae40adccf7670379e9ccf5b7a41d0002046ba2b8f0f691", size = 1478310 }, - { url = "https://files.pythonhosted.org/packages/87/69/b54a08bcd25e561bdd5183c008ace4424c25e80506e80674032504800efd/shapely-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44eda8bd7a4bccb0f281264b34bf3518d8c4c9a8ffe69a1a05dabf6e8461147", size = 1336082 }, - { url = "https://files.pythonhosted.org/packages/b3/f9/40473fcb5b66ff849e563ca523d2a26dafd6957d52dd876ffd0eded39f1c/shapely-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6c50cd879831955ac47af9c907ce0310245f9d162e298703f82e1785e38c98", size = 2371047 }, - { url = "https://files.pythonhosted.org/packages/d6/f3/c9cc07a7a03b5f5e83bd059f9adf3e21cf086b0e41d7f95e6464b151e798/shapely-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a65d882456e13c8b417562c36324c0cd1e5915f3c18ad516bb32ee3f5fc895", size = 2469112 }, - { url = "https://files.pythonhosted.org/packages/5d/b9/fc63d6b0b25063a3ff806857a5dc88851d54d1c278288f18cef1b322b449/shapely-2.0.7-cp310-cp310-win32.whl", hash = "sha256:7e97104d28e60b69f9b6a957c4d3a2a893b27525bc1fc96b47b3ccef46726bf2", size = 1296057 }, - { url = "https://files.pythonhosted.org/packages/fe/d1/8df43f94cf4cda0edbab4545f7cdd67d3f1d02910eaff152f9f45c6d00d8/shapely-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:35524cc8d40ee4752520819f9894b9f28ba339a42d4922e92c99b148bed3be39", size = 1441787 }, - { url = "https://files.pythonhosted.org/packages/1d/ad/21798c2fec013e289f8ab91d42d4d3299c315b8c4460c08c75fef0901713/shapely-2.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5cf23400cb25deccf48c56a7cdda8197ae66c0e9097fcdd122ac2007e320bc34", size = 1473091 }, - { url = "https://files.pythonhosted.org/packages/15/63/eef4f180f1b5859c70e7f91d2f2570643e5c61e7d7c40743d15f8c6cbc42/shapely-2.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f1da01c04527f7da59ee3755d8ee112cd8967c15fab9e43bba936b81e2a013", size = 1332921 }, - { url = "https://files.pythonhosted.org/packages/fe/67/77851dd17738bbe7762a0ef1acf7bc499d756f68600dd68a987d78229412/shapely-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f623b64bb219d62014781120f47499a7adc30cf7787e24b659e56651ceebcb0", size = 2427949 }, - { url = "https://files.pythonhosted.org/packages/0b/a5/2c8dbb0f383519771df19164e3bf3a8895d195d2edeab4b6040f176ee28e/shapely-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6d95703efaa64aaabf278ced641b888fc23d9c6dd71f8215091afd8a26a66e3", size = 2529282 }, - { url = "https://files.pythonhosted.org/packages/dc/4e/e1d608773c7fe4cde36d48903c0d6298e3233dc69412403783ac03fa5205/shapely-2.0.7-cp311-cp311-win32.whl", hash = "sha256:2f6e4759cf680a0f00a54234902415f2fa5fe02f6b05546c662654001f0793a2", size = 1295751 }, - { url = "https://files.pythonhosted.org/packages/27/57/8ec7c62012bed06731f7ee979da7f207bbc4b27feed5f36680b6a70df54f/shapely-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b52f3ab845d32dfd20afba86675c91919a622f4627182daec64974db9b0b4608", size = 1442684 }, - { url = "https://files.pythonhosted.org/packages/4f/3e/ea100eec5811bafd0175eb21828a3be5b0960f65250f4474391868be7c0f/shapely-2.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c2b9859424facbafa54f4a19b625a752ff958ab49e01bc695f254f7db1835fa", size = 1482451 }, - { url = "https://files.pythonhosted.org/packages/ce/53/c6a3487716fd32e1f813d2a9608ba7b72a8a52a6966e31c6443480a1d016/shapely-2.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5aed1c6764f51011d69a679fdf6b57e691371ae49ebe28c3edb5486537ffbd51", size = 1345765 }, - { url = "https://files.pythonhosted.org/packages/fd/dd/b35d7891d25cc11066a70fb8d8169a6a7fca0735dd9b4d563a84684969a3/shapely-2.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c9ae8cf443187d784d57202199bf9fd2d4bb7d5521fe8926ba40db1bc33e8e", size = 2421540 }, - { url = "https://files.pythonhosted.org/packages/62/de/8dbd7df60eb23cb983bb698aac982944b3d602ef0ce877a940c269eae34e/shapely-2.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73", size = 2525741 }, - { url = "https://files.pythonhosted.org/packages/96/64/faf0413ebc7a84fe7a0790bf39ec0b02b40132b68e57aba985c0b6e4e7b6/shapely-2.0.7-cp312-cp312-win32.whl", hash = "sha256:6bca5095e86be9d4ef3cb52d56bdd66df63ff111d580855cb8546f06c3c907cd", size = 1296552 }, - { url = "https://files.pythonhosted.org/packages/63/05/8a1c279c226d6ad7604d9e237713dd21788eab96db97bf4ce0ea565e5596/shapely-2.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f86e2c0259fe598c4532acfcf638c1f520fa77c1275912bbc958faecbf00b108", size = 1443464 }, - { url = "https://files.pythonhosted.org/packages/c6/21/abea43effbfe11f792e44409ee9ad7635aa93ef1c8ada0ef59b3c1c3abad/shapely-2.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a0c09e3e02f948631c7763b4fd3dd175bc45303a0ae04b000856dedebefe13cb", size = 1481618 }, - { url = "https://files.pythonhosted.org/packages/d9/71/af688798da36fe355a6e6ffe1d4628449cb5fa131d57fc169bcb614aeee7/shapely-2.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06ff6020949b44baa8fc2e5e57e0f3d09486cd5c33b47d669f847c54136e7027", size = 1345159 }, - { url = "https://files.pythonhosted.org/packages/67/47/f934fe2b70d31bb9774ad4376e34f81666deed6b811306ff574faa3d115e/shapely-2.0.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6dbf096f961ca6bec5640e22e65ccdec11e676344e8157fe7d636e7904fd36", size = 2410267 }, - { url = "https://files.pythonhosted.org/packages/f5/8a/2545cc2a30afc63fc6176c1da3b76af28ef9c7358ed4f68f7c6a9d86cf5b/shapely-2.0.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adeddfb1e22c20548e840403e5e0b3d9dc3daf66f05fa59f1fcf5b5f664f0e98", size = 2514128 }, - { url = "https://files.pythonhosted.org/packages/87/54/2344ce7da39676adec94e84fbaba92a8f1664e4ae2d33bd404dafcbe607f/shapely-2.0.7-cp313-cp313-win32.whl", hash = "sha256:a7f04691ce1c7ed974c2f8b34a1fe4c3c5dfe33128eae886aa32d730f1ec1913", size = 1295783 }, - { url = "https://files.pythonhosted.org/packages/d7/1e/6461e5cfc8e73ae165b8cff6eb26a4d65274fad0e1435137c5ba34fe4e88/shapely-2.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:aaaf5f7e6cc234c1793f2a2760da464b604584fb58c6b6d7d94144fd2692d67e", size = 1442300 }, - { url = "https://files.pythonhosted.org/packages/ad/de/dc856cf99a981b83aa041d1a240a65b36618657d5145d1c0c7ffb4263d5b/shapely-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4abeb44b3b946236e4e1a1b3d2a0987fb4d8a63bfb3fdefb8a19d142b72001e5", size = 1478794 }, - { url = "https://files.pythonhosted.org/packages/53/ea/70fec89a9f6fa84a8bf6bd2807111a9175cee22a3df24470965acdd5fb74/shapely-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0e75d9124b73e06a42bf1615ad3d7d805f66871aa94538c3a9b7871d620013", size = 1336402 }, - { url = "https://files.pythonhosted.org/packages/e5/22/f6b074b08748d6f6afedd79f707d7eb88b79fa0121369246c25bbc721776/shapely-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7977d8a39c4cf0e06247cd2dca695ad4e020b81981d4c82152c996346cf1094b", size = 2376673 }, - { url = "https://files.pythonhosted.org/packages/ab/f0/befc440a6c90c577300f5f84361bad80919e7c7ac381ae4960ce3195cedc/shapely-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0145387565fcf8f7c028b073c802956431308da933ef41d08b1693de49990d27", size = 2474380 }, - { url = "https://files.pythonhosted.org/packages/13/b8/edaf33dfb97e281d9de3871810de131b01e4f33d38d8f613515abc89d91e/shapely-2.0.7-cp39-cp39-win32.whl", hash = "sha256:98697c842d5c221408ba8aa573d4f49caef4831e9bc6b6e785ce38aca42d1999", size = 1297939 }, - { url = "https://files.pythonhosted.org/packages/7b/95/4d164c2fcb19c51e50537aafb99ecfda82f62356bfdb6f4ca620a3932bad/shapely-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:a3fb7fbae257e1b042f440289ee7235d03f433ea880e73e687f108d044b24db5", size = 1443665 }, -] - -[[package]] -name = "shapely" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + { name = "numpy" }, ] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fa/f18025c95b86116dd8f1ec58cab078bd59ab51456b448136ca27463be533/shapely-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8ccc872a632acb7bdcb69e5e78df27213f7efd195882668ffba5405497337c6", size = 1825117 }, - { url = "https://files.pythonhosted.org/packages/c7/65/46b519555ee9fb851234288be7c78be11e6260995281071d13abf2c313d0/shapely-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f24f2ecda1e6c091da64bcbef8dd121380948074875bd1b247b3d17e99407099", size = 1628541 }, - { url = "https://files.pythonhosted.org/packages/29/51/0b158a261df94e33505eadfe737db9531f346dfa60850945ad25fd4162f1/shapely-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45112a5be0b745b49e50f8829ce490eb67fefb0cea8d4f8ac5764bfedaa83d2d", size = 2948453 }, - { url = "https://files.pythonhosted.org/packages/a9/4f/6c9bb4bd7b1a14d7051641b9b479ad2a643d5cbc382bcf5bd52fd0896974/shapely-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c10ce6f11904d65e9bbb3e41e774903c944e20b3f0b282559885302f52f224a", size = 3057029 }, - { url = "https://files.pythonhosted.org/packages/89/0b/ad1b0af491d753a83ea93138eee12a4597f763ae12727968d05934fe7c78/shapely-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:61168010dfe4e45f956ffbbaf080c88afce199ea81eb1f0ac43230065df320bd", size = 3894342 }, - { url = "https://files.pythonhosted.org/packages/7d/96/73232c5de0b9fdf0ec7ddfc95c43aaf928740e87d9f168bff0e928d78c6d/shapely-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cacf067cdff741cd5c56a21c52f54ece4e4dad9d311130493a791997da4a886b", size = 4056766 }, - { url = "https://files.pythonhosted.org/packages/43/cc/eec3c01f754f5b3e0c47574b198f9deb70465579ad0dad0e1cef2ce9e103/shapely-2.1.1-cp310-cp310-win32.whl", hash = "sha256:23b8772c3b815e7790fb2eab75a0b3951f435bc0fce7bb146cb064f17d35ab4f", size = 1523744 }, - { url = "https://files.pythonhosted.org/packages/50/fc/a7187e6dadb10b91e66a9e715d28105cde6489e1017cce476876185a43da/shapely-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c7b2b6143abf4fa77851cef8ef690e03feade9a0d48acd6dc41d9e0e78d7ca6", size = 1703061 }, - { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368 }, - { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362 }, - { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005 }, - { url = "https://files.pythonhosted.org/packages/a2/17/e09357274699c6e012bbb5a8ea14765a4d5860bb658df1931c9f90d53bd3/shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753", size = 3108489 }, - { url = "https://files.pythonhosted.org/packages/17/5d/93a6c37c4b4e9955ad40834f42b17260ca74ecf36df2e81bb14d12221b90/shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647", size = 3945727 }, - { url = "https://files.pythonhosted.org/packages/a3/1a/ad696648f16fd82dd6bfcca0b3b8fbafa7aacc13431c7fc4c9b49e481681/shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0", size = 4109311 }, - { url = "https://files.pythonhosted.org/packages/d4/38/150dd245beab179ec0d4472bf6799bf18f21b1efbef59ac87de3377dbf1c/shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab", size = 1522982 }, - { url = "https://files.pythonhosted.org/packages/93/5b/842022c00fbb051083c1c85430f3bb55565b7fd2d775f4f398c0ba8052ce/shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93", size = 1703872 }, - { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021 }, - { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018 }, - { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417 }, - { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224 }, - { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982 }, - { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122 }, - { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437 }, - { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479 }, - { url = "https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3004a644d9e89e26c20286d5fdc10f41b1744c48ce910bd1867fdff963fe6c48", size = 1832107 }, - { url = "https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1415146fa12d80a47d13cfad5310b3c8b9c2aa8c14a0c845c9d3d75e77cb54f6", size = 1642355 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/6d28b43d53fea56de69c744e34c2b999ed4042f7a811dc1bceb876071c95/shapely-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fcab88b7520820ec16d09d6bea68652ca13993c84dffc6129dc3607c95594c", size = 2968871 }, - { url = "https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ce6a5cc52c974b291237a96c08c5592e50f066871704fb5b12be2639d9026a", size = 3080830 }, - { url = "https://files.pythonhosted.org/packages/1d/fe/f4a03d81abd96a6ce31c49cd8aaba970eaaa98e191bd1e4d43041e57ae5a/shapely-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:04e4c12a45a1d70aeb266618d8cf81a2de9c4df511b63e105b90bfdfb52146de", size = 3908961 }, - { url = "https://files.pythonhosted.org/packages/ef/59/7605289a95a6844056a2017ab36d9b0cb9d6a3c3b5317c1f968c193031c9/shapely-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ca74d851ca5264aae16c2b47e96735579686cb69fa93c4078070a0ec845b8d8", size = 4079623 }, - { url = "https://files.pythonhosted.org/packages/bc/4d/9fea036eff2ef4059d30247128b2d67aaa5f0b25e9fc27e1d15cc1b84704/shapely-2.1.1-cp313-cp313-win32.whl", hash = "sha256:fd9130501bf42ffb7e0695b9ea17a27ae8ce68d50b56b6941c7f9b3d3453bc52", size = 1521916 }, - { url = "https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ab8d878687b438a2f4c138ed1a80941c6ab0029e0f4c785ecfe114413b498a97", size = 1702746 }, - { url = "https://files.pythonhosted.org/packages/60/36/b1452e3e7f35f5f6454d96f3be6e2bb87082720ff6c9437ecc215fa79be0/shapely-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c062384316a47f776305ed2fa22182717508ffdeb4a56d0ff4087a77b2a0f6d", size = 1833482 }, - { url = "https://files.pythonhosted.org/packages/ce/ca/8e6f59be0718893eb3e478141285796a923636dc8f086f83e5b0ec0036d0/shapely-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4ecf6c196b896e8f1360cc219ed4eee1c1e5f5883e505d449f263bd053fb8c05", size = 1642256 }, - { url = "https://files.pythonhosted.org/packages/ab/78/0053aea449bb1d4503999525fec6232f049abcdc8df60d290416110de943/shapely-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb00070b4c4860f6743c600285109c273cca5241e970ad56bb87bef0be1ea3a0", size = 3016614 }, - { url = "https://files.pythonhosted.org/packages/ee/53/36f1b1de1dfafd1b457dcbafa785b298ce1b8a3e7026b79619e708a245d5/shapely-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14a9afa5fa980fbe7bf63706fdfb8ff588f638f145a1d9dbc18374b5b7de913", size = 3093542 }, - { url = "https://files.pythonhosted.org/packages/b9/bf/0619f37ceec6b924d84427c88835b61f27f43560239936ff88915c37da19/shapely-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b640e390dabde790e3fb947198b466e63223e0a9ccd787da5f07bcb14756c28d", size = 3945961 }, - { url = "https://files.pythonhosted.org/packages/93/c9/20ca4afeb572763b07a7997f00854cb9499df6af85929e93012b189d8917/shapely-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:69e08bf9697c1b73ec6aa70437db922bafcea7baca131c90c26d59491a9760f9", size = 4089514 }, - { url = "https://files.pythonhosted.org/packages/33/6a/27036a5a560b80012a544366bceafd491e8abb94a8db14047b5346b5a749/shapely-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:ef2d09d5a964cc90c2c18b03566cf918a61c248596998a0301d5b632beadb9db", size = 1540607 }, - { url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061 }, +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, ] [[package]] name = "simpleitk" -version = "2.5.0" +version = "2.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/65/67069b823abce4ed0c6e1378d57634eb10b4050987d926bd4d8c070db2cf/simpleitk-2.5.0.tar.gz", hash = "sha256:222546fc8ddef6a9e2cfae95253bfc4b77f7b574dfd292194f221685bd5a2ae0", size = 2104838 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/38/4b5f8f839626247d44189b106db69a7ff186acbfc500d20fd8b343b75e1f/simpleitk-2.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e53bdc12949b34edf6be1526bc63d6a0b81930ffb7430d77cff427052df04e8", size = 44587628 }, - { url = "https://files.pythonhosted.org/packages/bb/a4/9293f73324b37e81d354231cfe41d1d52e754e631ecd945e3a74b92f5f0a/simpleitk-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d34ada59c6944a53e581a7bebe97980c1401f3a5ad86cf67dd96d26e70d152a2", size = 38486498 }, - { url = "https://files.pythonhosted.org/packages/29/22/5492b15cb11d9261265896f9e33dce719a7e747f63a8da7024952ba891ad/simpleitk-2.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e854b76ddd4d77b3b57cf5d8d5d5ddcbe49f147a0108cd22391766d108b3203f", size = 47964609 }, - { url = "https://files.pythonhosted.org/packages/e1/28/31eddbff54285b095d0c86e40691436db4ba9480c5f66232bd382bf25409/simpleitk-2.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f090a4ece132d6b83613c91b6631a1dc7d7530232092231cac5dec2f3e52b35d", size = 52628238 }, - { url = "https://files.pythonhosted.org/packages/54/2b/e2fa9e0f7c403cd5c25af04bfec781cd6a51c67aa832f7350552bed0ca4b/simpleitk-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:093020866c2486ae208663ed057f7535616c97e9478e04a7ce53924fa7613195", size = 18766330 }, - { url = "https://files.pythonhosted.org/packages/d0/d6/d4b325f4d382d8d299885b90ea32062ebbdada572adc96878fd151adbdbb/simpleitk-2.5.0-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c4039e136fa7aee3367c0c3380721ae423ed99995ee7d1a16cbba8ca1f741589", size = 44619461 }, - { url = "https://files.pythonhosted.org/packages/27/ba/3881b5a09e131de47fc4ddf8878dba162aaf53d334c428a5b24c6fd730db/simpleitk-2.5.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:9039e1bc7988594ef73cfa911925eee1a647d1ced81613565607bc56b16a7fbc", size = 38512619 }, - { url = "https://files.pythonhosted.org/packages/9d/62/99035b48c5fdfd3be1eda78e397c35311d45cfc839aeb9d87c1427405e3e/simpleitk-2.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a96296e33661a8911d4f73d14a8d26eed387beb681327706bbe7e4a53893d48", size = 47944744 }, - { url = "https://files.pythonhosted.org/packages/68/e8/2c095851d76de64c2e0ec6f7ed0eed162001cf0481586354ae8d156a7f89/simpleitk-2.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5bb0e50e9fa5e3102d8bd7f243376743592bc25ecdd5aa738f63e6bfb11dd76", size = 52625824 }, - { url = "https://files.pythonhosted.org/packages/27/25/d8a5b042cd66881b40fd680239bfbca5f9199aa509687471cf713c1ab8bb/simpleitk-2.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:348ee9ae6b81ea9173701f726308ac2dac2ff22dcae7afdbdf67d037c0bd431f", size = 18810175 }, - { url = "https://files.pythonhosted.org/packages/f0/a7/35050122ed4c54f79fb97d39506507dec0615dce218d1d1435331c04ea30/simpleitk-2.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db6d28785de2a97f3bc71fdecb6ef65894f7378c63c64e1791085b30d127a222", size = 44587606 }, - { url = "https://files.pythonhosted.org/packages/42/8e/29d56657e7b0c1e4fdf54df51d97bc4230b444bf4675bf10961573c86780/simpleitk-2.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e1660d054d01d2b422f05b36b0d1c8c3d458ee5c86d7833169c73b791951b1c5", size = 38486355 }, - { url = "https://files.pythonhosted.org/packages/22/6e/61299f53bc16406ff1b6aa6cf22404e876281fc1bd400ab82b2e3f15d748/simpleitk-2.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73dbd1786042ad2667004bc3e24e847cdec11e23ee0635f438cafcb98dc0e5a", size = 47966024 }, - { url = "https://files.pythonhosted.org/packages/ee/e3/6a6ca88e72cb48e2a5c2c8a6ac8cda107f71409fb915880f68908eb3bb93/simpleitk-2.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80ac24ced63a6e66fc14b988f74a31a1472918aca2aa8901b285bd6f4788003d", size = 52613608 }, - { url = "https://files.pythonhosted.org/packages/26/a3/1297b09ca41a075bd274e11af2fa725a654fb7469cf6b926c3a9a8c9d436/simpleitk-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:0883784ea6a01985adec39847a73a2acee0fc985ab0300ae4d4b85ea753b06ef", size = 18765675 }, + { url = "https://files.pythonhosted.org/packages/c1/cd/8bb3ccc3f8428f99530d85dc39aabcdd6feb66d805cb7b33a0d643532df0/simpleitk-2.5.4-cp311-abi3-macosx_10_9_x86_64.whl", hash = "sha256:58f5018711adcefae9fa9aca784323b8125b01fa715e1dd1e565c5587bc73079", size = 42594233, upload-time = "2026-04-24T15:27:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/1d85ed1373a9f6043eae33406f214a561f6b1724c4d293b56550a5243db3/simpleitk-2.5.4-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:38773eaba333c2f5ee8bf1073229c12c7af8391381a69667891d427e78bbc9d1", size = 38646455, upload-time = "2026-04-24T15:27:55.936Z" }, + { url = "https://files.pythonhosted.org/packages/0b/de/056c1fa2850418ff6f815e3ef1b006eda6a8f3461b3c68f046b1c0f5699c/simpleitk-2.5.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab2af59c93ee6304d6a85bbd8f33025b041db8b3c512553f6fad30461bf33c2a", size = 48114580, upload-time = "2026-04-24T15:27:58.766Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5d/5e017f59bbc06af5b9c78331036478b2ddb371d7ff810c559c36e5389904/simpleitk-2.5.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4cbb85a6125c0248c774b6e202ca86128b82d5c60bec2a90d59f8a0e94df518c", size = 52844636, upload-time = "2026-04-24T15:28:01.979Z" }, + { url = "https://files.pythonhosted.org/packages/70/65/61dfcfc8d47f0a8849477a468b64dd2307042296f473bfd9d317eab76b02/simpleitk-2.5.4-cp311-abi3-win_amd64.whl", hash = "sha256:1fa8636047a4a5402cde792c30fe2629bd270b798f4d0ebaf999248543ca9262", size = 18944858, upload-time = "2026-04-24T15:28:04.918Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575 } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274 }, + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "soupsieve" -version = "2.7" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sphinx" -version = "7.4.7" +version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "babel", marker = "python_full_version < '3.10'" }, - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, - { name = "imagesize", marker = "python_full_version < '3.10'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "requests", marker = "python_full_version < '3.10'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, -] - -[[package]] -name = "sphinx" -version = "8.1.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, ] -dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "babel", marker = "python_full_version == '3.10.*'" }, - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, - { name = "imagesize", marker = "python_full_version == '3.10.*'" }, - { name = "jinja2", marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "requests", marker = "python_full_version == '3.10.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] [[package]] name = "sphinx" -version = "8.2.3" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741 }, + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinx-rtd-theme" -version = "3.0.2" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jquery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463 } +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561 }, + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] @@ -2981,40 +2443,39 @@ name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331 } +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104 }, + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] @@ -3024,124 +2485,70 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "threadpoolctl" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] [[package]] name = "tifffile" -version = "2024.8.30" +version = "2026.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/30/7017e5560154c100cad3a801c02adb48879cd8e8cb862b82696d84187184/tifffile-2024.8.30.tar.gz", hash = "sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4", size = 365714 } +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4f/73714b1c1d339b1545cac28764e39f88c69468b5e10e51f327f9aa9d55b9/tifffile-2024.8.30-py3-none-any.whl", hash = "sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad", size = 227262 }, + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, ] [[package]] name = "tifffile" -version = "2025.5.10" +version = "2026.4.11" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4a/e687f5957fead200faad58dbf9c9431a2bbb118040e96f5fb8a55f7ebc50/tifffile-2026.4.11.tar.gz", hash = "sha256:17758ff0c0d4db385792a083ad3ca51fcb0f4d942642f4d8f8bc1287fdcf17bc", size = 394956, upload-time = "2026-04-12T01:57:28.793Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533 }, -] - -[[package]] -name = "tifffile" -version = "2025.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/cc/deed7dd69d4029adba8e95214f8bf65fca8bc6b8426e27d056e1de624206/tifffile-2025.6.1.tar.gz", hash = "sha256:63cff7cf7305c26e3f3451c0b05fd95a09252beef4f1663227d4b70cb75c5fdb", size = 369769 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/77/7f7dfcf2d847c1c1c63a2d4157c480eb4c74e4aa56e844008795ff01f86d/tifffile-2025.6.1-py3-none-any.whl", hash = "sha256:ff7163f1aaea519b769a2ac77c43be69e7d83e5b5d5d6a676497399de50535e5", size = 230624 }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, + { url = "https://files.pythonhosted.org/packages/3f/9f/74f110b4271ded519c7add4341cbabc824de26817ff1c345b3109df9e99c/tifffile-2026.4.11-py3-none-any.whl", hash = "sha256:9b94ffeddb39e97601af646345e8808f885773de01b299e480ed6d3a41509ec9", size = 248227, upload-time = "2026-04-12T01:57:26.969Z" }, ] [[package]] name = "torch" -version = "2.7.1" +version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -3155,6 +2562,7 @@ dependencies = [ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "setuptools", marker = "python_full_version >= '3.12'" }, { name = "sympy" }, @@ -3162,35 +2570,44 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/27/2e06cb52adf89fe6e020963529d17ed51532fc73c1e6d1b18420ef03338c/torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f", size = 99089441 }, - { url = "https://files.pythonhosted.org/packages/0a/7c/0a5b3aee977596459ec45be2220370fde8e017f651fecc40522fd478cb1e/torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d", size = 821154516 }, - { url = "https://files.pythonhosted.org/packages/f9/91/3d709cfc5e15995fb3fe7a6b564ce42280d3a55676dad672205e94f34ac9/torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162", size = 216093147 }, - { url = "https://files.pythonhosted.org/packages/92/f6/5da3918414e07da9866ecb9330fe6ffdebe15cb9a4c5ada7d4b6e0a6654d/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c", size = 68630914 }, - { url = "https://files.pythonhosted.org/packages/11/56/2eae3494e3d375533034a8e8cf0ba163363e996d85f0629441fa9d9843fe/torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2", size = 99093039 }, - { url = "https://files.pythonhosted.org/packages/e5/94/34b80bd172d0072c9979708ccd279c2da2f55c3ef318eceec276ab9544a4/torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1", size = 821174704 }, - { url = "https://files.pythonhosted.org/packages/50/9e/acf04ff375b0b49a45511c55d188bcea5c942da2aaf293096676110086d1/torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52", size = 216095937 }, - { url = "https://files.pythonhosted.org/packages/5b/2b/d36d57c66ff031f93b4fa432e86802f84991477e522adcdffd314454326b/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730", size = 68640034 }, - { url = "https://files.pythonhosted.org/packages/87/93/fb505a5022a2e908d81fe9a5e0aa84c86c0d5f408173be71c6018836f34e/torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa", size = 98948276 }, - { url = "https://files.pythonhosted.org/packages/56/7e/67c3fe2b8c33f40af06326a3d6ae7776b3e3a01daa8f71d125d78594d874/torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc", size = 821025792 }, - { url = "https://files.pythonhosted.org/packages/a1/37/a37495502bc7a23bf34f89584fa5a78e25bae7b8da513bc1b8f97afb7009/torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b", size = 216050349 }, - { url = "https://files.pythonhosted.org/packages/3a/60/04b77281c730bb13460628e518c52721257814ac6c298acd25757f6a175c/torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:787687087412c4bd68d315e39bc1223f08aae1d16a9e9771d95eabbb04ae98fb", size = 68645146 }, - { url = "https://files.pythonhosted.org/packages/66/81/e48c9edb655ee8eb8c2a6026abdb6f8d2146abd1f150979ede807bb75dcb/torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28", size = 98946649 }, - { url = "https://files.pythonhosted.org/packages/3a/24/efe2f520d75274fc06b695c616415a1e8a1021d87a13c68ff9dce733d088/torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412", size = 821033192 }, - { url = "https://files.pythonhosted.org/packages/dd/d9/9c24d230333ff4e9b6807274f6f8d52a864210b52ec794c5def7925f4495/torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38", size = 216055668 }, - { url = "https://files.pythonhosted.org/packages/95/bf/e086ee36ddcef9299f6e708d3b6c8487c1651787bb9ee2939eb2a7f74911/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585", size = 68925988 }, - { url = "https://files.pythonhosted.org/packages/69/6a/67090dcfe1cf9048448b31555af6efb149f7afa0a310a366adbdada32105/torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934", size = 99028857 }, - { url = "https://files.pythonhosted.org/packages/90/1c/48b988870823d1cc381f15ec4e70ed3d65e043f43f919329b0045ae83529/torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8", size = 821098066 }, - { url = "https://files.pythonhosted.org/packages/7b/eb/10050d61c9d5140c5dc04a89ed3257ef1a6b93e49dd91b95363d757071e0/torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e", size = 216336310 }, - { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708 }, - { url = "https://files.pythonhosted.org/packages/71/8a/7db5ed2696e9d67dbc7f8df02d0bc1680b68a0552a3c07ea2d1795fb3f19/torch-2.7.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:e0d81e9a12764b6f3879a866607c8ae93113cbcad57ce01ebde63eb48a576369", size = 99140587 }, - { url = "https://files.pythonhosted.org/packages/95/7b/62bedf718e6100c6d1d53fbdb7e56cb7ad80912a57f2bc7f4f1f289988f1/torch-2.7.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8394833c44484547ed4a47162318337b88c97acdb3273d85ea06e03ffff44998", size = 821146689 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/80230d0eec3a4dd1b5d2b423e663026452ac8ffb64aeac1619febc1b4ac7/torch-2.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:df41989d9300e6e3c19ec9f56f856187a6ef060c3662fe54f4b6baf1fc90bd19", size = 215987480 }, - { url = "https://files.pythonhosted.org/packages/62/77/6391214d084a85aeb099d520420d39f405928b6a5f27a3f1a453c27c5173/torch-2.7.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:a737b5edd1c44a5c1ece2e9f3d00df9d1b3fb9541138bee56d83d38293fb6c9d", size = 68630146 }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, ] [[package]] name = "torchvision" -version = "0.22.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -3198,67 +2615,63 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/15/2c/7b67117b14c6cc84ae3126ca6981abfa3af2ac54eb5252b80d9475fb40df/torchvision-0.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b47d8369ee568c067795c0da0b4078f39a9dfea6f3bc1f3ac87530dfda1dd56", size = 1947825 }, - { url = "https://files.pythonhosted.org/packages/6c/9f/c4dcf1d232b75e28bc37e21209ab2458d6d60235e16163544ed693de54cb/torchvision-0.22.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:990de4d657a41ed71680cd8be2e98ebcab55371f30993dc9bd2e676441f7180e", size = 2512611 }, - { url = "https://files.pythonhosted.org/packages/e2/99/db71d62d12628111d59147095527a0ab492bdfecfba718d174c04ae6c505/torchvision-0.22.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3347f690c2eed6d02aa0edfb9b01d321e7f7cf1051992d96d8d196c39b881d49", size = 7485668 }, - { url = "https://files.pythonhosted.org/packages/32/ff/4a93a4623c3e5f97e8552af0f9f81d289dcf7f2ac71f1493f1c93a6b973d/torchvision-0.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:86ad938f5a6ca645f0d5fb19484b1762492c2188c0ffb05c602e9e9945b7b371", size = 1707961 }, - { url = "https://files.pythonhosted.org/packages/f6/00/bdab236ef19da050290abc2b5203ff9945c84a1f2c7aab73e8e9c8c85669/torchvision-0.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4addf626e2b57fc22fd6d329cf1346d474497672e6af8383b7b5b636fba94a53", size = 1947827 }, - { url = "https://files.pythonhosted.org/packages/ac/d0/18f951b2be3cfe48c0027b349dcc6fde950e3dc95dd83e037e86f284f6fd/torchvision-0.22.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8b4a53a6067d63adba0c52f2b8dd2290db649d642021674ee43c0c922f0c6a69", size = 2514021 }, - { url = "https://files.pythonhosted.org/packages/c3/1a/63eb241598b36d37a0221e10af357da34bd33402ccf5c0765e389642218a/torchvision-0.22.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7866a3b326413e67724ac46f1ee594996735e10521ba9e6cdbe0fa3cd98c2f2", size = 7487300 }, - { url = "https://files.pythonhosted.org/packages/e5/73/1b009b42fe4a7774ba19c23c26bb0f020d68525c417a348b166f1c56044f/torchvision-0.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:bb3f6df6f8fd415ce38ec4fd338376ad40c62e86052d7fc706a0dd51efac1718", size = 1707989 }, - { url = "https://files.pythonhosted.org/packages/02/90/f4e99a5112dc221cf68a485e853cc3d9f3f1787cb950b895f3ea26d1ea98/torchvision-0.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:153f1790e505bd6da123e21eee6e83e2e155df05c0fe7d56347303067d8543c5", size = 1947827 }, - { url = "https://files.pythonhosted.org/packages/25/f6/53e65384cdbbe732cc2106bb04f7fb908487e4fb02ae4a1613ce6904a122/torchvision-0.22.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:964414eef19459d55a10e886e2fca50677550e243586d1678f65e3f6f6bac47a", size = 2514576 }, - { url = "https://files.pythonhosted.org/packages/17/8b/155f99042f9319bd7759536779b2a5b67cbd4f89c380854670850f89a2f4/torchvision-0.22.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:699c2d70d33951187f6ed910ea05720b9b4aaac1dcc1135f53162ce7d42481d3", size = 7485962 }, - { url = "https://files.pythonhosted.org/packages/05/17/e45d5cd3627efdb47587a0634179a3533593436219de3f20c743672d2a79/torchvision-0.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:75e0897da7a8e43d78632f66f2bdc4f6e26da8d3f021a7c0fa83746073c2597b", size = 1707992 }, - { url = "https://files.pythonhosted.org/packages/7a/30/fecdd09fb973e963da68207fe9f3d03ec6f39a935516dc2a98397bf495c6/torchvision-0.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c3ae3319624c43cc8127020f46c14aa878406781f0899bb6283ae474afeafbf", size = 1947818 }, - { url = "https://files.pythonhosted.org/packages/55/f4/b45f6cd92fa0acfac5e31b8e9258232f25bcdb0709a604e8b8a39d76e411/torchvision-0.22.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:4a614a6a408d2ed74208d0ea6c28a2fbb68290e9a7df206c5fef3f0b6865d307", size = 2471597 }, - { url = "https://files.pythonhosted.org/packages/8d/b0/3cffd6a285b5ffee3fe4a31caff49e350c98c5963854474d1c4f7a51dea5/torchvision-0.22.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7ee682be589bb1a002b7704f06b8ec0b89e4b9068f48e79307d2c6e937a9fdf4", size = 7485894 }, - { url = "https://files.pythonhosted.org/packages/fd/1d/0ede596fedc2080d18108149921278b59f220fbb398f29619495337b0f86/torchvision-0.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:2566cafcfa47ecfdbeed04bab8cef1307c8d4ef75046f7624b9e55f384880dfe", size = 1708020 }, - { url = "https://files.pythonhosted.org/packages/0f/ca/e9a06bd61ee8e04fb4962a3fb524fe6ee4051662db07840b702a9f339b24/torchvision-0.22.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:043d9e35ed69c2e586aff6eb9e2887382e7863707115668ac9d140da58f42cba", size = 2137623 }, - { url = "https://files.pythonhosted.org/packages/ab/c8/2ebe90f18e7ffa2120f5c3eab62aa86923185f78d2d051a455ea91461608/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:27142bcc8a984227a6dcf560985e83f52b82a7d3f5fe9051af586a2ccc46ef26", size = 2476561 }, - { url = "https://files.pythonhosted.org/packages/94/8b/04c6b15f8c29b39f0679589753091cec8b192ab296d4fdaf9055544c4ec9/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef46e065502f7300ad6abc98554131c35dc4c837b978d91306658f1a65c00baa", size = 7658543 }, - { url = "https://files.pythonhosted.org/packages/ab/c0/131628e6d42682b0502c63fd7f647b8b5ca4bd94088f6c85ca7225db8ac4/torchvision-0.22.1-cp313-cp313t-win_amd64.whl", hash = "sha256:7414eeacfb941fa21acddcd725f1617da5630ec822e498660a4b864d7d998075", size = 1629892 }, - { url = "https://files.pythonhosted.org/packages/1f/91/cfd4dfab7893acebb7cea9b60cf9624a0a107681249c68b1b41fb10b2286/torchvision-0.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8be941b4d35c0aba819be70fdbbbed8ceb60401ce6996b8cfaaba1300ce62263", size = 1947875 }, - { url = "https://files.pythonhosted.org/packages/bd/e9/2c13d5aba26be09bcbb799e54955b5526eb75f630957bc2c24133e9e350e/torchvision-0.22.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:154a2bdc37a16122c2024f2f77e65f5986020b40c013515c694b5d357fac99a1", size = 2512672 }, - { url = "https://files.pythonhosted.org/packages/be/b0/ac3158206bff9e3ceadace60a753e4e21ce499daf0e6716184e9265a2855/torchvision-0.22.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ef7dee376f42900c0e7b0e34624f391d9ece70ab90ee74b42de0c1fffe371284", size = 7487053 }, - { url = "https://files.pythonhosted.org/packages/2e/ba/aa10c0771588420a81fa1ea3666801856d1fb57abc186f16d64a7c86c105/torchvision-0.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:e01631046fda25a1eca2f58d5fdc9a152b93740eb82435cdb27c5151b8d20c02", size = 1707934 }, + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "triton" -version = "3.3.1" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, -] wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/a9/549e51e9b1b2c9b854fd761a1d23df0ba2fbc60bd0c13b489ffa518cfcb7/triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e", size = 155600257 }, - { url = "https://files.pythonhosted.org/packages/21/2f/3e56ea7b58f80ff68899b1dbe810ff257c9d177d288c6b0f55bf2fe4eb50/triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b", size = 155689937 }, - { url = "https://files.pythonhosted.org/packages/24/5f/950fb373bf9c01ad4eb5a8cd5eaf32cdf9e238c02f9293557a2129b9c4ac/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43", size = 155669138 }, - { url = "https://files.pythonhosted.org/packages/74/1f/dfb531f90a2d367d914adfee771babbd3f1a5b26c3f5fbc458dee21daa78/triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240", size = 155673035 }, - { url = "https://files.pythonhosted.org/packages/28/71/bd20ffcb7a64c753dc2463489a61bf69d531f308e390ad06390268c4ea04/triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42", size = 155735832 }, - { url = "https://files.pythonhosted.org/packages/6d/81/ac4d50af22f594c4cb7c84fd2ad5ba1e0c03e2a83fe3483ddd79edcd7ec7/triton-3.3.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6139aeb04a146b0b8e0fbbd89ad1e65861c57cfed881f21d62d3cb94a36bab7", size = 155596799 }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, ] [[package]] name = "twine" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "id" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, { name = "packaging" }, { name = "readme-renderer" }, @@ -3268,68 +2681,63 @@ dependencies = [ { name = "rich" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791 }, + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, ] [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" -version = "2025.2" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "urllib3" -version = "2.4.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] -name = "valis-wsi" +name = "valis" version = "1.2.0" source = { editable = "." } dependencies = [ { name = "aicspylibczi" }, { name = "beautifulsoup4" }, { name = "colorama" }, - { name = "colour-science", version = "0.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "colour-science", version = "0.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "einops" }, + { name = "colour-science" }, { name = "fastcluster" }, - { name = "jpype1" }, - { name = "kornia" }, { name = "lxml" }, { name = "markupsafe" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "numpy" }, { name = "ome-types" }, { name = "opencv-contrib-python-headless" }, @@ -3338,29 +2746,37 @@ dependencies = [ { name = "pillow" }, { name = "pqdm" }, { name = "pyvips" }, - { name = "scikit-image", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "scyjava" }, + { name = "scikit-image" }, + { name = "scikit-learn" }, + { name = "scipy" }, { name = "setuptools" }, - { name = "shapely", version = "2.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "shapely", version = "2.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "shapely" }, { name = "simpleitk" }, - { name = "torch" }, - { name = "torchvision" }, { name = "tqdm" }, { name = "weightedstats" }, ] +[package.optional-dependencies] +dl = [ + { name = "einops" }, + { name = "kornia" }, + { name = "torch" }, + { name = "torchvision" }, +] +full = [ + { name = "einops" }, + { name = "kornia" }, + { name = "torch" }, + { name = "torchvision" }, +] + [package.dev-dependencies] dev = [ + { name = "black" }, { name = "pytest" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ruff" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-rtd-theme" }, { name = "twine" }, ] @@ -3371,16 +2787,15 @@ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.11.1,<5.0.0" }, { name = "colorama", specifier = ">=0.4.6,<1.0.0" }, { name = "colour-science", specifier = ">0.4.2" }, - { name = "einops", specifier = ">=0.8.0,<1.0.0" }, + { name = "einops", marker = "extra == 'dl'", specifier = ">=0.8.0,<1.0.0" }, { name = "fastcluster", specifier = ">=1.2.6,<2.0.0" }, - { name = "jpype1", specifier = ">=1.4.1,<2.0.0" }, - { name = "kornia", specifier = ">=0.7.3,<1.0.0" }, + { name = "kornia", marker = "extra == 'dl'", specifier = ">=0.7.3,<1.0.0" }, { name = "lxml", specifier = ">=5.4.0" }, { name = "markupsafe", specifier = ">=2.0.0,<3.0.0" }, { name = "matplotlib", specifier = ">=3.6.3,<4.0.0" }, - { name = "numpy", specifier = ">1.23,<2.0.0" }, + { name = "numpy" }, { name = "ome-types", specifier = ">=0.3.2,<1.0.0" }, - { name = "opencv-contrib-python-headless", specifier = "==4.9.0.80" }, + { name = "opencv-contrib-python-headless", specifier = ">=4.12" }, { name = "openpyxl", specifier = ">=3.1.5,<4.0.0" }, { name = "pandas", specifier = ">=2.0.0" }, { name = "pillow", specifier = ">=10.3.0" }, @@ -3389,19 +2804,22 @@ requires-dist = [ { name = "scikit-image", specifier = ">=0.24.0,<1.0.0" }, { name = "scikit-learn", specifier = ">=1.2.0,<2.0.0" }, { name = "scipy", specifier = ">=1.10.0,<2.0.0" }, - { name = "scyjava", specifier = ">=1.8.1,<2.0.0" }, { name = "setuptools", specifier = ">=69.0.3" }, { name = "shapely", specifier = ">=2.0.0,<3.0.0" }, { name = "simpleitk", specifier = ">=2.4.1,<3.0.0" }, - { name = "torch", specifier = ">=2.7.1" }, - { name = "torchvision", specifier = ">=0.20.1,<1.0.0" }, + { name = "torch", marker = "extra == 'dl'", specifier = ">=2.7.1" }, + { name = "torchvision", marker = "extra == 'dl'", specifier = ">=0.20.1,<1.0.0" }, { name = "tqdm", specifier = ">=4.64.1,<5.0.0" }, + { name = "valis", extras = ["dl"], marker = "extra == 'full'" }, { name = "weightedstats", specifier = ">=0.4.1,<1.0.0" }, ] +provides-extras = ["dl", "full"] [package.metadata.requires-dev] dev = [ + { name = "black", specifier = ">=24.0.0" }, { name = "pytest", specifier = ">=8.4.0" }, + { name = "ruff", specifier = ">=0.6.0" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, { name = "twine", specifier = ">=6.1.0" }, @@ -3411,28 +2829,28 @@ dev = [ name = "weightedstats" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/a5/f5c0e601a610e4618316be3155febbbec98994788fcc0e9d8080369266ec/weightedstats-0.4.1.tar.gz", hash = "sha256:beb488a3f46aa06dbc8491578ec7e408847ca682edc7ec90846f6df9e36cab50", size = 4327 } +sdist = { url = "https://files.pythonhosted.org/packages/da/a5/f5c0e601a610e4618316be3155febbbec98994788fcc0e9d8080369266ec/weightedstats-0.4.1.tar.gz", hash = "sha256:beb488a3f46aa06dbc8491578ec7e408847ca682edc7ec90846f6df9e36cab50", size = 4327, upload-time = "2020-02-04T22:46:32.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/73/24ecd3d2230edb304d8c2febe61711ae75c11fc792acc8fd3b056b4eb6cc/weightedstats-0.4.1-py3-none-any.whl", hash = "sha256:6ead0c27df10b0598d7e3a1c2bc201b925f5ac47099df0dafccce91932a5d155", size = 3812 }, + { url = "https://files.pythonhosted.org/packages/8d/73/24ecd3d2230edb304d8c2febe61711ae75c11fc792acc8fd3b056b4eb6cc/weightedstats-0.4.1-py3-none-any.whl", hash = "sha256:6ead0c27df10b0598d7e3a1c2bc201b925f5ac47099df0dafccce91932a5d155", size = 3812, upload-time = "2020-02-04T22:46:30.401Z" }, ] [[package]] name = "xsdata" -version = "24.3.1" +version = "26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/da/bce6f30a2b85bb258bd3c282258c3cec294ef6c3214397ca22349bcb515b/xsdata-24.3.1.tar.gz", hash = "sha256:cf6c6895616260cbe2a4eff6f8f906cd16f9c1dba8fd8561e500b91269b86616", size = 330034 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/71e9e8eac669091fd434ed494d806c8cc37614aecb34ce4c62c283f99abf/xsdata-26.2.tar.gz", hash = "sha256:c631af71aaa75734f8ce92a08fcf8389d905dee2aab0b5032c9032e9071009a6", size = 349690, upload-time = "2026-02-15T16:13:31.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/ac/27a6fcf61c64549a17b9eaa087ae7876417b5ed9efb6efdec8b52289cae4/xsdata-24.3.1-py3-none-any.whl", hash = "sha256:2f4a00ec33fb6ff41fca95f3fac826b83fc34d8644e2f3da830ad393defb5705", size = 224141 }, + { url = "https://files.pythonhosted.org/packages/d3/92/f0edcbc2f895ecea14a68e492b24c157625e251279a94b172a6b263290e7/xsdata-26.2-py3-none-any.whl", hash = "sha256:85a591a4405d903416afbd4a917e8dda8ea44641a3e66d72134bc2a31b3c16b0", size = 235561, upload-time = "2026-02-15T16:13:29.614Z" }, ] [[package]] name = "zipp" -version = "3.22.0" +version = "3.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257 } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796 }, + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ] diff --git a/valis/__init__.py b/valis/__init__.py deleted file mode 100644 index b849eaf5..00000000 --- a/valis/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -__version__ = "1.2.0" - -from . import affine_optimizer -from . import feature_detectors -from . import feature_matcher -from . import non_rigid_registrars -from . import preprocessing -from . import registration -from . import serial_non_rigid -from . import serial_rigid -from . import slide_io -from . import slide_tools -from . import valtils -from . import viz -from . import warp_tools -from . import micro_rigid_registrar - -__all__ = ["affine_optimizer", - "feature_detectors", - "feature_matcher", - "non_rigid_registrars", - "preprocessing", - "registration", - "serial_non_rigid", - "serial_rigid", - "slide_io", - "slide_tools", - "valtils", - "viz", - "warp_tools", - "micro_rigid_registrar" - ] \ No newline at end of file diff --git a/valis/data/bf_formats.txt b/valis/data/bf_formats.txt deleted file mode 100644 index 17fb022b..00000000 --- a/valis/data/bf_formats.txt +++ /dev/null @@ -1,324 +0,0 @@ -.zip -.png -.jpg -.jpeg -.jpe -.sldy -.pbm -.pgm -.ppm -.fits -.fts -.pcx -.gif -.bmp -.ipl -.ipm -.rcpnl -.dv -.r3d -.r3d_d3d -.dv.log -.r3d.log -.mrc -.st -.ali -.map -.rec -.mrcs -.dm3 -.dm4 -.dm2 -.ims -.raw -.ome -.ome.xml -.lif -.avi -.pict -.pct -.sdt -.spc -.set -.eps -.epsi -.ps -.sld -.spl -.al3d -.mng -.xv -.xys -.html -.lim -.psd -.xdce -.xml -.tiff -.tif -.xlog -.l2d -.scn -.tif -.img -.naf -.mnc -.mov -.mrw -.vws -.pst -.inf -.arf -.c01 -.dib -.fli -.tga -.top -.dti -.his -.wat -.xqd -.xqf -.tfr -.ffr -.zfr -.zfp -.2fl -.pr3 -.afm -.1sc -.sm2 -.sm3 -.stp -.pnl -.htd -.log -.htd -.tif -.v -.fdf -.aim -.frm -.spi -.mvd2 -.aisf -.aiix -.dat -.atsf -.hed -.img -.vms -.vsi -.ets -.inr -.bip -.acff -.czi -.sif -.ndpis -.df3 -.mod -.fake -.afi -.msr -.scn -.lms -.bin -.cif -.im3 -.i2i -.spe -.oir -.klb -.vff -.lof -.xlef -.omp2info -.dcimg -.dat -.img -.par -.nii -.img -.hdr -.nii.gz -.img -.hdr -.apl -.tnb -.mtb -.tif -.nrrd -.nhdr -.ics -.ids -.ano -.cfg -.csv -.htm -.rec -.tim -.zpo -.tif -.am -.amiramesh -.grey -.hx -.labels -.dat -.xml -.tif -.exp -.tif -.hdr -.dat -.hdr -.img -.img -.inf -.tif -.tiff -.xml -.hdr -.tif -.xml -.xml -.wpi -.pic -.xml -.raw -.oib -.oif -.pty -.lut -.zvi -.ipw -.jp2 -.j2k -.jpf -.jpx -.nd2 -.jp2 -.cxd -.ims -.ch5 -.hdf -.db -.lsm -.mdb -.seq -.ips -.gel -.ims -.flex -.mea -.res -.svs -.fff -.sxm -.tif -.tiff -.jpk -.ndpi -.pcoraw -.rec -.bif -.ome.tiff -.ome.tif -.ome.tf2 -.ome.tf8 -.ome.btf -.companion.ome -.tif -.tiff -.tif -.tiff -.txt -.tif -.tiff -.xml -.lei -.tif -.tiff -.raw -.nef -.tif -.tiff -.tif -.tiff -.tif -.tiff -.cfg -.env -.xml -.stk -.nd -.scan -.tif -.tiff -.tif -.tiff -.txt -.xml -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tiff -.tif -.tif -.tiff -.cr2 -.crw -.jpg -.thm -.wav -.tif -.tiff -.tif -.xml -.scn -.tiff -.tif -.qptiff -.tif -.tiff -.tif, tiff -.dic -.dcm -.dicom -.jp2 -.j2ki -.j2kr -.raw -.ima -.txt -.tif -.tiff -.tf2 -.tf8 -.btf -.txt -.csv -.img -.liff -.cr2 -.crw -.jpg -.thm -.wav -.obf -.msr -.xml -.h5