diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fb09f59 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +USER= +PASS= +DATA_DIR= \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ed7ddc9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: build +on: [ push, pull_request ] +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.10", "3.11" ] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Set up cache + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('poetry.lock') }} + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install + - name: Run style checks + run: | + make check-codestyle + - name: Run unit tests + run: | + make test-unit + - name: Run safety checks + run: | + make check-safety diff --git a/.gitignore b/.gitignore index 68bc17f..065fe4e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,107 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Created by https://www.toptal.com/developers/gitignore/api/intellij+iml +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+iml + +### Intellij+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +.idea/modules.xml + +# End of https://www.toptal.com/developers/gitignore/api/intellij+iml + +# Data +data/** + +*.env + +# External +external/youssef-nader-first-letters/*.ckpt +external/youssef-nader-first-letters/labels/*.png +external/youssef_nader_first_letters/labels.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6b27889 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +default_language_version: + python: python3.10 + +default_stages: [commit, push] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-ast + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: check-added-large-files + args: ["--maxkb=8000"] + - id: end-of-file-fixer + exclude: LICENSE + + - repo: local + hooks: + - id: pyupgrade + name: pyupgrade + entry: poetry run pyupgrade --py310-plus + types: [ python ] + language: system + + - repo: local + hooks: + - id: isort + name: isort + entry: poetry run isort --settings-path pyproject.toml + types: [python] + language: system + + - repo: local + hooks: + - id: black + name: black + entry: poetry run black --config pyproject.toml + types: [ python ] + language: system diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b4f975 --- /dev/null +++ b/Makefile @@ -0,0 +1,102 @@ +#* Variables +SHELL := /usr/bin/env bash +PYTHON := python + +# Determine OS. +ifeq ($(OS),Windows_NT) + OS := windows +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + OS := linux + endif + ifeq ($(UNAME_S),Darwin) + OS := macos + endif +endif + +#* Poetry +.PHONY: poetry-download +poetry-download: + curl -sSL https://install.python-poetry.org | python3 - + +#* Installation +.PHONY: install +install: + poetry lock -n && poetry export --without-hashes > requirements.txt + poetry install -n + -poetry run mypy --install-types --non-interactive ./ + +.PHONY: pre-commit-install +pre-commit-install: + poetry run pre-commit install + +.PHONY: rclone-install +rclone-install: +ifeq ($(OS),windows) + @echo "This command is not supported on Windows. Please download rclone from https://rclone.org/downloads/" +else + sudo -v ; curl https://rclone.org/install.sh | sudo bash +endif + +.PHONY: download-all-fragments +download-all-fragments: + ./scripts/download-fragments.sh 1 2 3 + +.PHONY: download-all-scrolls +download-all-scrolls: + ./scripts/download-scroll-surface-vols.sh 1 2 PHerc1667 PHerc0332 + +.PHONY: download-monster-segment +download-monster-segment: + ./scripts/download-monster-segment-surface-vols.sh recto verso + +#* Formatters +.PHONY: codestyle +codestyle: + poetry run isort --settings-path pyproject.toml ./ + poetry run black --config pyproject.toml ./ + +.PHONY: formatting +formatting: codestyle + +#* Linting +.PHONY: test +test: + poetry run pytest -c pyproject.toml tests/ --cov-report=html --cov=vesuvius_challenge_rnd + +test-unit: + poetry run pytest -m "not fragment_data and not scroll_data" -c pyproject.toml tests/ --cov-report=html --cov=vesuvius_challenge_rnd + +.PHONY: check-codestyle +check-codestyle: + poetry run isort --diff --check-only --settings-path pyproject.toml ./ + poetry run black --diff --check --config pyproject.toml ./ + +.PHONY: mypy +mypy: + poetry run mypy --config-file pyproject.toml ./ + +.PHONY: check-safety +check-safety: + poetry check + +.PHONY: lint +lint: test-unit check-codestyle check-safety + +#* Docker +.PHONY: frag-ink-det-gpu-build +frag-ink-det-gpu-build: + docker build -t frag-ink-det-gpu -f docker/fragment-ink-detection-gpu/Dockerfile . + +.PHONY: frag-ink-det-gpu-run +frag-ink-det-gpu-run: + docker run -it --rm --gpus all -e WANDB_DOCKER=frag-ink-det-gpu frag-ink-det-gpu + +.PHONY: scroll-ink-det-gpu-build +scroll-ink-det-gpu-build: + docker build -t scroll-ink-det-gpu -f docker/scroll-ink-detection-gpu/Dockerfile . + +.PHONY: scroll-ink-det-gpu-run +scroll-ink-det-gpu-run: + docker run -it --rm --gpus all -e WANDB_DOCKER=scroll-ink-det-gpu scroll-ink-det-gpu diff --git a/README.md b/README.md index 36b51c2..05f2329 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ # vesuvius-grand-prize-submission Vesuvius challenge grand prize submission + +## About + +We approached the ink detection task as a 3D-to-2D binary semantic segmentation problem using surface volumes from +scroll 1 (PHerc Paris 3). We followed a human-assisted pseudo-label-based self-training approach using the crackle signal as a surrogate +to the ink signal. + +For a summary of the methods used, please see [docs/methods.md](docs/methods.md). + +## Getting started + +For instructions on how to train and run inference, please see [docs/submission_reproduction_instructions.md](docs/submission_reproduction_instructions.md). + +A pretrained checkpoint is available [here](https://drive.google.com/file/d/1bY14CjSfY8VbqlKmjv1MW-bzhScLZOoV/view?usp=sharing) +(associated with [val_3336_C3.yaml](vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3.yaml)). + +## Authors +Louis Schlessinger, Arefeh Sherafati + +## License + +[MIT](https://choosealicense.com/licenses/mit/) + +## Credits +- [EduceLab-Scrolls: Verifiable Recovery of Text from Herculaneum Papyri using X-ray CT](https://arxiv.org/abs/2304.02084) +- [Introducing Hann windows for reducing edge-effects in patch-based image segmentation](https://arxiv.org/abs/1910.07831) +- [1st place Kaggle Vesuvius Challenge - Ink Detection](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/discussion/417496) +- [4th place Kaggle Vesuvius Challenge - Ink Detection](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/discussion/417779) +- [First Ink Vesuvius Challenge](https://caseyhandmer.wordpress.com/2023/08/05/reading-ancient-scrolls/) +- [2nd place Vesuvius Challenge First Letters](https://github.com/younader/Vesuvius-First-Letters) \ No newline at end of file diff --git a/docker/fragment-ink-detection-gpu/Dockerfile b/docker/fragment-ink-detection-gpu/Dockerfile new file mode 100644 index 0000000..f6f5803 --- /dev/null +++ b/docker/fragment-ink-detection-gpu/Dockerfile @@ -0,0 +1,40 @@ +# Base CUDA devel image. +FROM nvidia/cuda:11.8.0-devel-ubuntu22.04 + +ARG DEBIAN_FRONTEND=noninteractive + +WORKDIR /workspace + +# Apt-get installs. +RUN apt update +RUN apt install -y python3 python3-pip libmagickwand-dev +RUN python3 -m pip install --no-cache-dir --upgrade pip + +# Install poetry. +ENV \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 +ENV \ + POETRY_VERSION='1.5.1' \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 + +RUN pip3 install --no-cache-dir "poetry==$POETRY_VERSION" +ENV PATH="$POETRY_HOME/bin:$PATH" +RUN poetry --version + +# Install rclone. +RUN apt install rclone -y +RUN rclone version + +# Install htop. +RUN apt install htop -y + +# Install requirements. +COPY pyproject.toml poetry.lock ./ +RUN poetry export --without-hashes --with fragment-ink-det,torch_gpu -o requirements.txt +RUN pip3 install --no-cache-dir -r requirements.txt + +COPY vesuvius_challenge_rnd ./vesuvius_challenge_rnd +COPY scripts ./scripts diff --git a/docker/scroll-ink-detection-gpu/Dockerfile b/docker/scroll-ink-detection-gpu/Dockerfile new file mode 100644 index 0000000..7dcd487 --- /dev/null +++ b/docker/scroll-ink-detection-gpu/Dockerfile @@ -0,0 +1,47 @@ +# Base CUDA devel image. +FROM nvidia/cuda:12.3.1-devel-ubuntu22.04 +ARG DEBIAN_FRONTEND=noninteractive + +WORKDIR /workspace + +# Apt-get installs. +RUN apt update +RUN apt install -y python3 python3-pip libmagickwand-dev +RUN python3 -m pip install --no-cache-dir --upgrade pip + +# Install poetry. +ENV \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 +ENV \ + POETRY_VERSION='1.7.1' \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 + +RUN pip3 install --no-cache-dir "poetry==$POETRY_VERSION" +ENV PATH="$POETRY_HOME/bin:$PATH" +RUN poetry --version + +# Optional apt-get installs. +RUN apt-get update && apt-get install -y \ + curl \ + ca-certificates \ + sudo \ + git \ + rclone \ + htop \ + && rm -rf /var/lib/apt/lists/* + +# Install requirements. +COPY pyproject.toml poetry.lock ./ +RUN poetry export --without-hashes --with fragment-ink-det,scroll-ink-det,torch_gpu,analysis -o requirements.txt +RUN pip3 install --no-cache-dir -r requirements.txt +RUN pip3 install transformers==4.36.2 + +COPY README.md README.md +COPY vesuvius_challenge_rnd ./vesuvius_challenge_rnd +RUN pip3 install -e . + +COPY scripts ./scripts +COPY tools ./tools diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 0000000..4fe7a5f --- /dev/null +++ b/docs/methods.md @@ -0,0 +1,76 @@ +# Methods Summary + +## Preprocessing + +1. load image stack as raw uint16 +2. rescale and convert to uint8 +3. (optional) flip the depth dimension based on the segment +4. (optional) clip intensities + +## Labels + +We used two types of masks: ink labels and non-ink labels. The former corresponds to papyrus with ink on the +*target sheet* (the sheet centered around slice 32) and the latter to papyrus without ink on the target sheet. + +## Dataset generation + +### Patch sampling + +We generate patches (of a fixed size) using a sliding window approach, discarding any patch having a label containing +less than some pre-specified fraction of unlabeled pixels. In practice this was set to near 100%, thus requiring almost +every patch to be labeled. In our case, the labeled area is the union of the ink labels and non-ink labels. The +unlabeled area is its complement. We also discard any patch that overlaps with the non-papyrus region of the segment mask. + +### Dataset split + +We use leave-k-segments-out cross-validation. While we didn't find any signs of significant overfitting, we tried to minimize training on segments having overlap with the +validation segment where obvious. + +### Sub-segments + +We sometimes split the multi-column segments into sub-segments containing a single column of text. These sub-segments +are created by slicing along a particular axis and saving the output sub-segment mask and TIFFs. + +## Model + +We use a 3D-to-2D encoder-decoder model based on a 3D U-net encoder and a 2D Segformer decoder, similar to that used by +the winning team of the Vesuvius fragment ink detection Kaggle competition. + +- Encoder: Residual 3D U-Net with the addition of concurrent spatial and channel squeeze & excitation blocks +- Decoder: Segformer-b1 + +The PyTorch Lightning model we used is called `UNet3dSegformerPLModel` and the underlying PyTorch model is called +`UNet3DSegformer`. We used a feature size of 16 for the 3D U-Net, a depth of 5, and 32 output channels for +this model. These 3D features are max-pooled along the depth dimension. The Segformer is pretrained from the +*nvidia/mit-b1* checkpoint. Finally, the outputs are upscaled to the input resolution. + +## Training + +Here’s an overview of our training setup: + +- Optimizer: AdamW +- Loss function: dice + softBCE +- Learning rate scheduler: gradual warmup +- Performance measure: We select the model checkpoint that maximizes the mean of the binary average precision and the $F_{0.5}$ score in the validation set. +- XY patch size: 64 x 64 +- Z slices: 15 - 47 +- DDP training with 1 node and 8 devices +- Batch size: 32 +- FP16 mixed precision training + +## Data augmentation + +- horizontal and vertical flips, random brightness contrast, shift-scale-rotate, gaussian noise, gaussian blur, motion blur, coarse dropout, and cut-and-paste slices + +## Inference + +We used sliding window inference with the following settings: + +- Stride: 8 +- XY window size: 64 x 64 +- Z-min: 15 +- Z-max: 47 + +We also reverse the depth dimension for some segments. + +To reconstruct the segment prediction from the overlapping patch predictions, we weight the patches using a Hann window. diff --git a/docs/submission_reproduction_instructions.md b/docs/submission_reproduction_instructions.md new file mode 100644 index 0000000..1368628 --- /dev/null +++ b/docs/submission_reproduction_instructions.md @@ -0,0 +1,265 @@ +# Reproduction instructions + +## Overview + +Here, you'll find instructions on how to prepare the environment, train, and run inference. + +## Getting started + +### Prerequisites + +#### System requirements +Training: +- Disk space: ~1Tb +- RAM: 200Gb +- GPUs: 1 node, 8x H100 80Gb HBM3 + +Inference: +- Disk space: ~50Gb +- RAM: 80Gb +- GPUs: 1 node, 1 V100 (16Gb VRAM) + +You can probably train using a single GPU such as an A100 SXM4 40 GB, but the training time will be significantly +longer and parameters such as batch size, learning rate, and devices may have to be adjusted. + +You may need a bit more RAM for inference depending on segment size. + +### Installation +#### Docker build +- Image size: 13.4 Gb +- Docker version `24.0.7` + +You can build the image by running: + +```bash +docker build -t scroll-ink-det-gpu -f docker/scroll-ink-detection-gpu/Dockerfile . +``` + +#### Docker run + +You can run the following to create a new container with an interactive shell: +```bash +docker run -it --rm --gpus all -v /$(pwd)/data:/workspace/data scroll-ink-det-gpu +``` + +Feel free to change `$(pwd)` to wherever you want to the data downloaded on the host. + +> Note: Depending on your system, you may need to increase the shared memory size for this container for +training. + +We had to use `--shm-size=10g`, making the full docker run command: + +```bash +docker run -it --rm --gpus all -v /$(pwd)/data:/workspace/data --shm-size=10g scroll-ink-det-gpu +``` + +#### Download and prepare the data + +1. Preparing the labels + +Unzip the `labels.zip` file and place them in `data/labels` (the mounted volume). + +2. Downloading the surface volumes + +You can download the data after you have registered for the +[Vesuvius Challenge](https://scrollprize.org/) (see [data agreement here](https://docs.google.com/forms/d/e/1FAIpQLSf2lCOCwnO1xo0bc1QdlL0a034Uoe7zyjYBY2k33ZHslHE38Q/viewform)). +This will give you the username and password that you'll need for the next step. You must set the environment variables +`USER` and `PASS`. You can create an `.env` file (see `.env.example` for an example; run `cp .env.example .env` to +create this file) and fill it out. You'd then have to set the environment variables, which can be done as such in a Linux shell: + +```bash +export $(grep -v '^#' .env | tr -d '\r' | xargs) +``` + +From within the container's new shell session, you should download the surface volumes associated with the labels: +```bash +export LABEL_DIR=data/labels +./scripts/download-scroll-surface-vols-by-segment.sh 1 $(python3 tools/print_segment_ids_from_label_dir.py $LABEL_DIR) +``` + +3. Generate sub-segments. +Now we're ready to create sub-segments (cropped versions of the original segments). You can create them by running: +```bash +python3 vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_all_subsegments.py --load_in_memory +``` +You should now be able to find these sub-segments in the `data/scrolls` directory with `_C` postfixes, where `i` +indicates the `i`th column. + +## Usage + +### Training + +To train a model, you can execute the following: +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config path/to/config.yaml +``` + +You'll have to pass a `config.yaml` file for each run. + +#### Fast development run (optional) + +To check that everything is working, you can run the following fast development run config: +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run_docker.yaml +``` + +#### Full training +To train on the full dataset, you can train the following models (one run per validation segment). +It will prompt you to sign in to [Weights & Biases](https://wandb.ai/). Feel free to skip this. You can find all the submission +run configs under `vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission`. + +Run 1 (*3336_C3 as validation): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3.yaml +``` + +Run 2 (*5753 as validation): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_5753.yaml +``` + +Run 3 (*4422_C2 as validation): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C2.yaml +``` + +Run 4 (*4422_C3 as validation): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C3.yaml +``` + +Run 5 (*1321 as validation): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_1321_no0901c3_no5351.yaml +``` + +You can find the saved model checkpoints under `/workspace/lightning_logs/version_/checkpoints/`. There should only be +one checkpoint per training run. Note that `v_num` is an automatically generated experiment version ID and can be seen +in the progress bar. For each run, note the associated `v_num` so that it can be used later on for inference. + +To reproduce the run using *3336_C3 with a smaller dataset (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection fit --config vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3_reduced.yaml +``` + +### Inference + +With the model checkpoints, you can generate predictions by running: +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference path/to/checkpoint.ckpt --infer_z_reversal +``` + +By default, it will write the output images to `/workspace/outputs`, but you can change that using `-o path/to/new/output/dir`. + +Assuming you ran the experiments in the order above and have the checkpoint paths you can run the following to reproduce the predictions: + +_20231005123336_C3_ predictions (run 1): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231005123336_C3 --infer_z_reversal -o outputs/20231005123336_C3 +``` + +_20230519215753_ predictions (run 2): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20230519215753 --infer_z_reversal -o outputs/20230519215753 +``` + +_20231012184422_superseded_C2_ predictions (run 3): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231012184422_superseded_C2 --infer_z_reversal -o outputs/20231012184422_superseded_C2 +``` + +_20231012184422_superseded_C3_ predictions (run 4): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231012184422_superseded_C3 --infer_z_reversal -o outputs/20231012184422_superseded_C3 +``` + +_20231210121321_ predictions (run 5): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231210121321 --infer_z_reversal -o outputs/20231210121321 +``` + +_20231022170901_C3_ predictions (run 5): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231022170901_C3 --infer_z_reversal -o outputs/20231022170901_C3 +``` + +_20231106155351_ predictions (run 5): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231106155351 --infer_z_reversal -o outputs/20231106155351 +``` + +These will all be saved in `outputs/`. + +#### Run 6 predictions (optional) +To run predictions from run 6 (*3336 as validation with reduced dataset size), you'll need to first ensure any segment that was not included in training or validation +is downloaded as such: +```bash +./scripts/download-scroll-surface-vols-by-segment.sh 1 20231221180251 20231007101619 +``` + +You might need to generate sub-segments for *1619: + +```bash +python3 vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_all_subsegments.py --load_in_memory --skip_if_exists +``` + +Now, you are ready to run inference on the additional segments (saved to the same output directory). + +_20231221180251_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231221180251 --infer_z_reversal -o outputs/20231221180251 +``` + +_20231031143852_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231031143852 --infer_z_reversal -o outputs/20231031143852 +``` + +_20231005123336_C2_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231005123336_C2 --infer_z_reversal -o outputs/20231005123336_C2 +``` + +_20231022170901_C2_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231022170901_C2 --infer_z_reversal -o outputs/20231022170901_C2 +``` + +_20231022170901_C3_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231022170901_C3 --infer_z_reversal -o outputs/20231022170901_C3 +``` + +_20231022170901_C4_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231022170901_C4 --infer_z_reversal -o outputs/20231022170901_C4 +``` + +_20230929220926_C2_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20230929220926_C2 --infer_z_reversal -o outputs/20230929220926_C2 +``` + +_20230929220926_C3_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20230929220926_C3 --infer_z_reversal -o outputs/20230929220926_C3 +``` + +_20231007101619_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231007101619 --infer_z_reversal -o outputs/20231007101619 +``` + +You can also run *1619 predictions as sub-segments if needed: +_20231007101619_C1_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231007101619_C1 --infer_z_reversal -o outputs/20231007101619_C1 +``` +_20231007101619_C2_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231007101619_C2 --infer_z_reversal -o outputs/20231007101619_C2 +``` +_20231007101619_C3_ predictions (run 6): +```bash +python3 -m vesuvius_challenge_rnd.scroll_ink_detection.evaluation.inference /workspace/lightning_logs//checkpoints/.ckpt 20231007101619_C3 --infer_z_reversal -o outputs/20231007101619_C3 +``` diff --git a/labels.zip b/labels.zip new file mode 100644 index 0000000..a8ded2d Binary files /dev/null and b/labels.zip differ diff --git a/notebooks/00-scroll-and-fragment-basic-eda.ipynb b/notebooks/00-scroll-and-fragment-basic-eda.ipynb new file mode 100644 index 0000000..e6100d9 --- /dev/null +++ b/notebooks/00-scroll-and-fragment-basic-eda.ipynb @@ -0,0 +1,453 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from vesuvius_challenge_rnd.data import Fragment, Scroll, MonsterSegmentRecto, MonsterSegmentVerso" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Fragments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Load data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fragments = [Fragment(i + 1) for i in range(3)]\n", + "fragments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Visualize fragments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(len(fragments), 4, figsize=(12, 10), sharex=\"row\", sharey=\"row\")\n", + "for i, axis in enumerate(ax):\n", + " ax1, ax2, ax3, ax4 = axis\n", + "\n", + " # Set title for first row only.\n", + " if i == 0:\n", + " ax1.set_title(\"IR image\")\n", + " ax2.set_title(\"Papyrus mask\")\n", + "\n", + " ax3.set_title(\"Ink labels\")\n", + " ax4.set_title(\"Slice 32 micro-CT\")\n", + "\n", + " ir_img = fragments[i].load_ir_img()\n", + " ax1.imshow(ir_img, cmap=\"gray\")\n", + "\n", + " mask = fragments[i].load_mask()\n", + " ax2.imshow(mask, cmap=\"binary\")\n", + "\n", + " ink_labels = fragments[i].load_ink_labels()\n", + " ax3.imshow(ink_labels, cmap=\"binary\")\n", + "\n", + " subvolume = fragments[i].load_volume_as_memmap(31, 32)\n", + " ax4.imshow(subvolume[0], cmap=\"gray\")\n", + "\n", + "plt.tight_layout()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "## Scrolls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Load data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "scrolls = [Scroll(i + 1) for i in range(2)]\n", + "scrolls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "Let's see how many missing segments there are." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "for scroll in scrolls:\n", + " print(f\"Scroll {scroll.scroll_id} num missing segments: {scroll.n_missing_segments}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### View shape distribution\n", + "Let's look at the shape distribution. First of all, do they all have 65 slices?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "unique_num_slices = set()\n", + "for scroll in scrolls:\n", + " for segment in scroll.segments:\n", + " unique_num_slices.add(segment.n_slices)\n", + "print(f\"Unique number of slices: {unique_num_slices}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "So they all have 65 slices. How about the surface shapes?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "surface_shapes = []\n", + "labels = []\n", + "for scroll in scrolls:\n", + " for segment in scroll.segments:\n", + " surface_shapes.append(segment.surface_shape)\n", + " labels.append(scroll.scroll_id)\n", + "\n", + "surface_shape_df = pd.DataFrame({\"Surface shape\": surface_shapes, \"Scroll\": labels})\n", + "surface_shape_df[[\"Num rows\", \"Num columns\"]] = surface_shape_df[\"Surface shape\"].apply(pd.Series)\n", + "surface_shape_df = surface_shape_df.drop(\"Surface shape\", axis=1)\n", + "\n", + "joint_plot = sns.jointplot(\n", + " data=surface_shape_df, x=\"Num rows\", y=\"Num columns\", hue=\"Scroll\", kind=\"scatter\"\n", + ")\n", + "joint_plot.fig.suptitle(\"Surface shape distribution by scroll\", y=1.02)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Area distribution\n", + "\n", + "Let's look at the scroll segment area distribution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "areas = []\n", + "labels = []\n", + "for scroll in scrolls:\n", + " for segment in scroll.segments:\n", + " try:\n", + " area = segment.area_cm2\n", + " except FileNotFoundError as e:\n", + " print(f\"Skipping segment {segment.segment_name}: {e}\")\n", + " continue\n", + " areas.append(area)\n", + " labels.append(scroll.scroll_id)\n", + "area_df = pd.DataFrame({\"Area\": areas, \"Scroll\": labels})\n", + "sns.histplot(data=area_df, x=\"Area\", hue=\"Scroll\")\n", + "\n", + "plt.title(\"Area distribution by scroll\")\n", + "plt.xlabel(r\"Segment area $(cm^2)$\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Author distribution\n", + "\n", + "Here, we look at the distribution of authors (annotators) for each scroll segment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "authors = []\n", + "labels = []\n", + "for scroll in scrolls:\n", + " for segment in scroll.segments:\n", + " try:\n", + " author = segment.author\n", + " except FileNotFoundError as e:\n", + " print(f\"Skipping segment {segment.segment_name}: {e}\")\n", + " continue\n", + " authors.append(segment.author)\n", + " labels.append(scroll.scroll_id)\n", + "\n", + "author_df = pd.DataFrame({\"Author\": authors, \"Scroll\": labels})\n", + "\n", + "sns.histplot(\n", + " data=author_df,\n", + " x=\"Author\",\n", + " hue=\"Scroll\",\n", + " element=\"step\",\n", + " stat=\"count\",\n", + " binwidth=0.5,\n", + " discrete=True,\n", + " alpha=0.5,\n", + ")\n", + "\n", + "plt.title(\"Author distribution by scroll\")\n", + "plt.xticks(rotation=30)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "### Visualize a scroll segment\n", + "\n", + "We look at about 10 slices from the first segment of the first scroll." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "z_min = 27\n", + "z_max = 37\n", + "n_slices_to_show = z_max - z_min\n", + "segment = scrolls[0][0]\n", + "print(segment)\n", + "fig, ax = plt.subplots(1, 1 + n_slices_to_show, figsize=(25, 25), sharex=\"row\", sharey=\"row\")\n", + "\n", + "# Show mask.\n", + "ax1 = ax[0]\n", + "mask = segment.load_mask()\n", + "ax1.imshow(mask, cmap=\"binary\")\n", + "ax1.set_title(\"Papyrus mask\")\n", + "\n", + "# Show subvolume.\n", + "subvolume = segment.load_volume_as_memmap(z_min, z_max)\n", + "for i, axis in enumerate(ax[1:]):\n", + " axis.imshow(subvolume[i], cmap=\"gray\")\n", + " axis.set_title(f\"Slice {i + z_min}\", fontsize=10)\n", + "\n", + "plt.tight_layout()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualize the monster segment\n", + "\n", + "These are already included in scroll 1, but we can also create them separately." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Recto" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_min = 31\n", + "z_max = 34\n", + "n_slices_to_show = z_max - z_min\n", + "segment = MonsterSegmentRecto()\n", + "print(f\"orientation: '{segment.orientation}'\")\n", + "print(segment)\n", + "fig, ax = plt.subplots(1, 1 + n_slices_to_show, figsize=(25, 25), sharex=\"row\", sharey=\"row\")\n", + "\n", + "# Show mask.\n", + "ax1 = ax[0]\n", + "mask = segment.load_mask()\n", + "ax1.imshow(mask, cmap=\"binary\")\n", + "ax1.set_title(\"Papyrus mask\")\n", + "\n", + "# Show subvolume.\n", + "subvolume = segment.load_volume_as_memmap(z_min, z_max)\n", + "for i, axis in enumerate(ax[1:]):\n", + " axis.imshow(subvolume[i], cmap=\"gray\")\n", + " axis.set_title(f\"Slice {i + z_min}\", fontsize=10)\n", + "\n", + "plt.tight_layout()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Verso" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_min = 31\n", + "z_max = 34\n", + "n_slices_to_show = z_max - z_min\n", + "segment = MonsterSegmentVerso()\n", + "print(f\"orientation: '{segment.orientation}'\")\n", + "print(segment)\n", + "fig, ax = plt.subplots(1, 1 + n_slices_to_show, figsize=(25, 25), sharex=\"row\", sharey=\"row\")\n", + "\n", + "# Show mask.\n", + "ax1 = ax[0]\n", + "mask = segment.load_mask()\n", + "ax1.imshow(mask, cmap=\"binary\")\n", + "ax1.set_title(\"Papyrus mask\")\n", + "\n", + "# Show subvolume.\n", + "subvolume = segment.load_volume_as_memmap(z_min, z_max)\n", + "for i, axis in enumerate(ax[1:]):\n", + " axis.imshow(subvolume[i], cmap=\"gray\")\n", + " axis.set_title(f\"Slice {i + z_min}\", fontsize=10)\n", + "\n", + "plt.tight_layout()\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..75f9933 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,5188 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.1" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"}, + {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"}, + {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"}, + {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"}, + {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"}, + {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"}, + {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"}, + {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"}, + {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"}, + {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"}, + {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"}, + {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "albumentations" +version = "1.3.1" +description = "Fast image augmentation library and easy to use wrapper around other libraries" +optional = false +python-versions = ">=3.7" +files = [ + {file = "albumentations-1.3.1-py3-none-any.whl", hash = "sha256:6b641d13733181d9ecdc29550e6ad580d1bfa9d25e2213a66940062f25e291bd"}, + {file = "albumentations-1.3.1.tar.gz", hash = "sha256:a6a38388fe546c568071e8c82f414498e86c9ed03c08b58e7a88b31cf7a244c6"}, +] + +[package.dependencies] +numpy = ">=1.11.1" +opencv-python-headless = ">=4.1.1" +PyYAML = "*" +qudida = ">=0.0.4" +scikit-image = ">=0.16.1" +scipy = ">=1.1.0" + +[package.extras] +develop = ["imgaug (>=0.4.0)", "pytest"] +imgaug = ["imgaug (>=0.4.0)"] +tests = ["pytest"] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +description = "ANTLR 4.9.3 runtime for Python 3.7" +optional = false +python-versions = "*" +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asciitree" +version = "0.3.3" +description = "Draws ASCII trees." +optional = false +python-versions = "*" +files = [ + {file = "asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e"}, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +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]] +name = "bitsandbytes" +version = "0.41.1" +description = "k-bit optimizers and matrix multiplication routines." +optional = false +python-versions = "*" +files = [ + {file = "bitsandbytes-0.41.1-py3-none-any.whl", hash = "sha256:b25228c27636367f222232ed4d1e1502eedd2064be215633734fb8ea0c1c65f4"}, + {file = "bitsandbytes-0.41.1.tar.gz", hash = "sha256:b3f8e7e1e5f88d4813d10ebd4072034ba6a18eca7f0e255376f8320e5499032c"}, +] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cmake" +version = "3.28.1" +description = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" +optional = false +python-versions = "*" +files = [ + {file = "cmake-3.28.1-py2.py3-none-macosx_10_10_universal2.macosx_10_10_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:9c77c47afef821c0044ba73d182c386ab02e92e6bda5296e553c12455a083f29"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2010_i686.manylinux_2_12_i686.whl", hash = "sha256:6a9549755d1178426502753d48949edae9bb0c66f15a07f09904783125beb0e3"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:d0978cdd08c0ebc76f4f8543aba1381a41580dcb9c3bcffb536c41337b75aea1"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96d506c417d63bbcff19b3e9eaa69fe546456a0ddeffe914bcbb23cceee6818e"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74c9878c504ccc6ddd5b0914cbe3b86417a36a2c2dfc486040bfdfe63fbbb1ac"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:64d4642c48486bb4320540781a2266c2060929d1e236d6eb2b2c96273e75e958"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:363bd0124d71d7e3d9b1ac9bd1dce1d80ba90f48b264c3bf9dbfcfda875cafc9"}, + {file = "cmake-3.28.1-py2.py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1be8f351271f8bcbe32288066e5add642d7c32f2f8fec3f135949c2cb13dfac2"}, + {file = "cmake-3.28.1-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3ed193134a4937bad8de2b4f62faebc8c1a4049cd37dad9767db7e7d91a08b52"}, + {file = "cmake-3.28.1-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:6ffb1fdb0b0f7f11271d82b5892c2edc109d561e186f882def095970403e2110"}, + {file = "cmake-3.28.1-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:9ea12ebe4b8266f04d6619ed64860bd6e687522f02caf3131515dd39d614ef00"}, + {file = "cmake-3.28.1-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:2ad22d897d2ed38544e5ef26ee21c4dccc38e938660cd07497fd6bdba0993ea6"}, + {file = "cmake-3.28.1-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:379a730b274f39e5858ef2107861b2727918493347b0ccdd5f62bcbb6a8450d9"}, + {file = "cmake-3.28.1-py2.py3-none-win32.whl", hash = "sha256:c82bc0eb1495cf518cb4f355b8a73e584e67d53453406c0498bacc454cf6c404"}, + {file = "cmake-3.28.1-py2.py3-none-win_amd64.whl", hash = "sha256:bb03ed4753185d0c70c0bc3212e5533e20eb2c17fa0ca1e7603b702c6d0db8cf"}, + {file = "cmake-3.28.1-py2.py3-none-win_arm64.whl", hash = "sha256:40f0671c05ef7eec27c4f53c63630b0b621e40f80ab38607d3a0e3a1f2c9242a"}, + {file = "cmake-3.28.1.tar.gz", hash = "sha256:0d4051d101d151d8387156c463aa45c8cd0e164f870e0ac0c8c91d3ff08528e1"}, +] + +[package.extras] +test = ["coverage (>=4.2)", "importlib-metadata (>=2.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.4.0)"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +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]] +name = "comm" +version = "0.2.0" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.0-py3-none-any.whl", hash = "sha256:2da8d9ebb8dd7bfc247adaff99f24dce705638a8042b85cb995066793e391001"}, + {file = "comm-0.2.0.tar.gz", hash = "sha256:a517ea2ca28931c7007a7a99c562a0fa5883cfb48963140cf642c41c948498be"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "contourpy" +version = "1.2.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, + {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, + {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, + {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, + {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, + {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, + {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, + {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, + {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, + {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, + {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, +] + +[package.dependencies] +numpy = ">=1.20,<2.0" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "coverage" +version = "7.3.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aff2bd3d585969cc4486bfc69655e862028b689404563e6b549e6a8244f226df"}, + {file = "coverage-7.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4353923f38d752ecfbd3f1f20bf7a3546993ae5ecd7c07fd2f25d40b4e54571"}, + {file = "coverage-7.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea473c37872f0159294f7073f3fa72f68b03a129799f3533b2bb44d5e9fa4f82"}, + {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5214362abf26e254d749fc0c18af4c57b532a4bfde1a057565616dd3b8d7cc94"}, + {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99b7d3f7a7adfa3d11e3a48d1a91bb65739555dd6a0d3fa68aa5852d962e5b1"}, + {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:74397a1263275bea9d736572d4cf338efaade2de9ff759f9c26bcdceb383bb49"}, + {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f154bd866318185ef5865ace5be3ac047b6d1cc0aeecf53bf83fe846f4384d5d"}, + {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e0d84099ea7cba9ff467f9c6f747e3fc3906e2aadac1ce7b41add72e8d0a3712"}, + {file = "coverage-7.3.4-cp310-cp310-win32.whl", hash = "sha256:3f477fb8a56e0c603587b8278d9dbd32e54bcc2922d62405f65574bd76eba78a"}, + {file = "coverage-7.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:c75738ce13d257efbb6633a049fb2ed8e87e2e6c2e906c52d1093a4d08d67c6b"}, + {file = "coverage-7.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:997aa14b3e014339d8101b9886063c5d06238848905d9ad6c6eabe533440a9a7"}, + {file = "coverage-7.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9c5bc5db3eb4cd55ecb8397d8e9b70247904f8eca718cc53c12dcc98e59fc8"}, + {file = "coverage-7.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27ee94f088397d1feea3cb524e4313ff0410ead7d968029ecc4bc5a7e1d34fbf"}, + {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ce03e25e18dd9bf44723e83bc202114817f3367789052dc9e5b5c79f40cf59d"}, + {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85072e99474d894e5df582faec04abe137b28972d5e466999bc64fc37f564a03"}, + {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a877810ef918d0d345b783fc569608804f3ed2507bf32f14f652e4eaf5d8f8d0"}, + {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9ac17b94ab4ca66cf803f2b22d47e392f0977f9da838bf71d1f0db6c32893cb9"}, + {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36d75ef2acab74dc948d0b537ef021306796da551e8ac8b467810911000af66a"}, + {file = "coverage-7.3.4-cp311-cp311-win32.whl", hash = "sha256:47ee56c2cd445ea35a8cc3ad5c8134cb9bece3a5cb50bb8265514208d0a65928"}, + {file = "coverage-7.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:11ab62d0ce5d9324915726f611f511a761efcca970bd49d876cf831b4de65be5"}, + {file = "coverage-7.3.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:33e63c578f4acce1b6cd292a66bc30164495010f1091d4b7529d014845cd9bee"}, + {file = "coverage-7.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:782693b817218169bfeb9b9ba7f4a9f242764e180ac9589b45112571f32a0ba6"}, + {file = "coverage-7.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c4277ddaad9293454da19121c59f2d850f16bcb27f71f89a5c4836906eb35ef"}, + {file = "coverage-7.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d892a19ae24b9801771a5a989fb3e850bd1ad2e2b6e83e949c65e8f37bc67a1"}, + {file = "coverage-7.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3024ec1b3a221bd10b5d87337d0373c2bcaf7afd86d42081afe39b3e1820323b"}, + {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1c3e9d2bbd6f3f79cfecd6f20854f4dc0c6e0ec317df2b265266d0dc06535f1"}, + {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e91029d7f151d8bf5ab7d8bfe2c3dbefd239759d642b211a677bc0709c9fdb96"}, + {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6879fe41c60080aa4bb59703a526c54e0412b77e649a0d06a61782ecf0853ee1"}, + {file = "coverage-7.3.4-cp312-cp312-win32.whl", hash = "sha256:fd2f8a641f8f193968afdc8fd1697e602e199931012b574194052d132a79be13"}, + {file = "coverage-7.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:d1d0ce6c6947a3a4aa5479bebceff2c807b9f3b529b637e2b33dea4468d75fc7"}, + {file = "coverage-7.3.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36797b3625d1da885b369bdaaa3b0d9fb8865caed3c2b8230afaa6005434aa2f"}, + {file = "coverage-7.3.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfed0ec4b419fbc807dec417c401499ea869436910e1ca524cfb4f81cf3f60e7"}, + {file = "coverage-7.3.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f97ff5a9fc2ca47f3383482858dd2cb8ddbf7514427eecf5aa5f7992d0571429"}, + {file = "coverage-7.3.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:607b6c6b35aa49defaebf4526729bd5238bc36fe3ef1a417d9839e1d96ee1e4c"}, + {file = "coverage-7.3.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8e258dcc335055ab59fe79f1dec217d9fb0cdace103d6b5c6df6b75915e7959"}, + {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a02ac7c51819702b384fea5ee033a7c202f732a2a2f1fe6c41e3d4019828c8d3"}, + {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b710869a15b8caf02e31d16487a931dbe78335462a122c8603bb9bd401ff6fb2"}, + {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6a23ae9348a7a92e7f750f9b7e828448e428e99c24616dec93a0720342f241d"}, + {file = "coverage-7.3.4-cp38-cp38-win32.whl", hash = "sha256:758ebaf74578b73f727acc4e8ab4b16ab6f22a5ffd7dd254e5946aba42a4ce76"}, + {file = "coverage-7.3.4-cp38-cp38-win_amd64.whl", hash = "sha256:309ed6a559bc942b7cc721f2976326efbfe81fc2b8f601c722bff927328507dc"}, + {file = "coverage-7.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aefbb29dc56317a4fcb2f3857d5bce9b881038ed7e5aa5d3bcab25bd23f57328"}, + {file = "coverage-7.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:183c16173a70caf92e2dfcfe7c7a576de6fa9edc4119b8e13f91db7ca33a7923"}, + {file = "coverage-7.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a4184dcbe4f98d86470273e758f1d24191ca095412e4335ff27b417291f5964"}, + {file = "coverage-7.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93698ac0995516ccdca55342599a1463ed2e2d8942316da31686d4d614597ef9"}, + {file = "coverage-7.3.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb220b3596358a86361139edce40d97da7458412d412e1e10c8e1970ee8c09ab"}, + {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5b14abde6f8d969e6b9dd8c7a013d9a2b52af1235fe7bebef25ad5c8f47fa18"}, + {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:610afaf929dc0e09a5eef6981edb6a57a46b7eceff151947b836d869d6d567c1"}, + {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed790728fb71e6b8247bd28e77e99d0c276dff952389b5388169b8ca7b1c28"}, + {file = "coverage-7.3.4-cp39-cp39-win32.whl", hash = "sha256:c15fdfb141fcf6a900e68bfa35689e1256a670db32b96e7a931cab4a0e1600e5"}, + {file = "coverage-7.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:38d0b307c4d99a7aca4e00cad4311b7c51b7ac38fb7dea2abe0d182dd4008e05"}, + {file = "coverage-7.3.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b1e0f25ae99cf247abfb3f0fac7ae25739e4cd96bf1afa3537827c576b4847e5"}, + {file = "coverage-7.3.4.tar.gz", hash = "sha256:020d56d2da5bc22a0e00a5b0d54597ee91ad72446fa4cf1b97c35022f6b6dbf0"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "debugpy" +version = "1.8.0" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7fb95ca78f7ac43393cd0e0f2b6deda438ec7c5e47fa5d38553340897d2fbdfb"}, + {file = "debugpy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada"}, + {file = "debugpy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:a8b7a2fd27cd9f3553ac112f356ad4ca93338feadd8910277aff71ab24d8775f"}, + {file = "debugpy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d9de202f5d42e62f932507ee8b21e30d49aae7e46d5b1dd5c908db1d7068637"}, + {file = "debugpy-1.8.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ef54404365fae8d45cf450d0544ee40cefbcb9cb85ea7afe89a963c27028261e"}, + {file = "debugpy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60009b132c91951354f54363f8ebdf7457aeb150e84abba5ae251b8e9f29a8a6"}, + {file = "debugpy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:8cd0197141eb9e8a4566794550cfdcdb8b3db0818bdf8c49a8e8f8053e56e38b"}, + {file = "debugpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a64093656c4c64dc6a438e11d59369875d200bd5abb8f9b26c1f5f723622e153"}, + {file = "debugpy-1.8.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b05a6b503ed520ad58c8dc682749113d2fd9f41ffd45daec16e558ca884008cd"}, + {file = "debugpy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6fb41c98ec51dd010d7ed650accfd07a87fe5e93eca9d5f584d0578f28f35f"}, + {file = "debugpy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:46ab6780159eeabb43c1495d9c84cf85d62975e48b6ec21ee10c95767c0590aa"}, + {file = "debugpy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:bdc5ef99d14b9c0fcb35351b4fbfc06ac0ee576aeab6b2511702e5a648a2e595"}, + {file = "debugpy-1.8.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:61eab4a4c8b6125d41a34bad4e5fe3d2cc145caecd63c3fe953be4cc53e65bf8"}, + {file = "debugpy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332"}, + {file = "debugpy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:57161629133113c97b387382045649a2b985a348f0c9366e22217c87b68b73c6"}, + {file = "debugpy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e3412f9faa9ade82aa64a50b602544efcba848c91384e9f93497a458767e6926"}, + {file = "debugpy-1.8.0-py2.py3-none-any.whl", hash = "sha256:9c9b0ac1ce2a42888199df1a1906e45e6f3c9555497643a85e0bf2406e3ffbc4"}, + {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "docker-pycreds" +version = "0.4.0" +description = "Python bindings for the docker credentials store API" +optional = false +python-versions = "*" +files = [ + {file = "docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4"}, + {file = "docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49"}, +] + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "docstring-parser" +version = "0.15" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "docstring_parser-0.15-py3-none-any.whl", hash = "sha256:d1679b86250d269d06a99670924d6bce45adc00b08069dae8c47d98e89b667a9"}, + {file = "docstring_parser-0.15.tar.gz", hash = "sha256:48ddc093e8b1865899956fcc03b03e66bb7240c310fac5af81814580c55bf682"}, +] + +[[package]] +name = "efficientnet-pytorch" +version = "0.7.1" +description = "EfficientNet implemented in PyTorch." +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "efficientnet_pytorch-0.7.1.tar.gz", hash = "sha256:00b9b261effce59d2d47aae2ad238c29a2a65175470f41ada7ecac439b7c1ee1"}, +] + +[package.dependencies] +torch = "*" + +[[package]] +name = "einops" +version = "0.7.0" +description = "A new flavour of deep learning operations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "einops-0.7.0-py3-none-any.whl", hash = "sha256:0f3096f26b914f465f6ff3c66f5478f9a5e380bb367ffc6493a68143fbbf1fd1"}, + {file = "einops-0.7.0.tar.gz", hash = "sha256:b2b04ad6081a3b227080c9bf5e3ace7160357ff03043cd66cc5b2319eb7031d1"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "fasteners" +version = "0.19" +description = "A python package that provides useful locks" +optional = false +python-versions = ">=3.6" +files = [ + {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, + {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, +] + +[[package]] +name = "fastjsonschema" +version = "2.19.0" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.19.0-py3-none-any.whl", hash = "sha256:b9fd1a2dd6971dbc7fee280a95bd199ae0dd9ce22beb91cc75e9c1c528a5170e"}, + {file = "fastjsonschema-2.19.0.tar.gz", hash = "sha256:e25df6647e1bc4a26070b700897b07b542ec898dd4f1f6ea013e7f6a88417225"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fonttools" +version = "4.47.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2404107626f97a221dc1a65b05396d2bb2ce38e435f64f26ed2369f68675d9"}, + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01f409be619a9a0f5590389e37ccb58b47264939f0e8d58bfa1f3ba07d22671"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d986b66ff722ef675b7ee22fbe5947a41f60a61a4da15579d5e276d897fbc7fa"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8acf6dd0434b211b3bd30d572d9e019831aae17a54016629fa8224783b22df8"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:495369c660e0c27233e3c572269cbe520f7f4978be675f990f4005937337d391"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59227d7ba5b232281c26ae04fac2c73a79ad0e236bca5c44aae904a18f14faf"}, + {file = "fonttools-4.47.0-cp310-cp310-win32.whl", hash = "sha256:59a6c8b71a245800e923cb684a2dc0eac19c56493e2f896218fcf2571ed28984"}, + {file = "fonttools-4.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:52c82df66201f3a90db438d9d7b337c7c98139de598d0728fb99dab9fd0495ca"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:854421e328d47d70aa5abceacbe8eef231961b162c71cbe7ff3f47e235e2e5c5"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:511482df31cfea9f697930f61520f6541185fa5eeba2fa760fe72e8eee5af88b"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0e2c88c8c985b7b9a7efcd06511fb0a1fe3ddd9a6cd2895ef1dbf9059719d7"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7a0a8848726956e9d9fb18c977a279013daadf0cbb6725d2015a6dd57527992"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e869da810ae35afb3019baa0d0306cdbab4760a54909c89ad8904fa629991812"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd23848f877c3754f53a4903fb7a593ed100924f9b4bff7d5a4e2e8a7001ae11"}, + {file = "fonttools-4.47.0-cp311-cp311-win32.whl", hash = "sha256:bf1810635c00f7c45d93085611c995fc130009cec5abdc35b327156aa191f982"}, + {file = "fonttools-4.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:61df4dee5d38ab65b26da8efd62d859a1eef7a34dcbc331299a28e24d04c59a7"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e3f4d61f3a8195eac784f1d0c16c0a3105382c1b9a74d99ac4ba421da39a8826"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:174995f7b057e799355b393e97f4f93ef1f2197cbfa945e988d49b2a09ecbce8"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea592e6a09b71cb7a7661dd93ac0b877a6228e2d677ebacbad0a4d118494c86d"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bdbe90b33897d9cc4a39f8e415b0fcdeae4c40a99374b8a4982f127ff5c767"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:843509ae9b93db5aaf1a6302085e30bddc1111d31e11d724584818f5b698f500"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9acfa1cdc479e0dde528b61423855913d949a7f7fe09e276228298fef4589540"}, + {file = "fonttools-4.47.0-cp312-cp312-win32.whl", hash = "sha256:66c92ec7f95fd9732550ebedefcd190a8d81beaa97e89d523a0d17198a8bda4d"}, + {file = "fonttools-4.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8fa20748de55d0021f83754b371432dca0439e02847962fc4c42a0e444c2d78"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c75e19971209fbbce891ebfd1b10c37320a5a28e8d438861c21d35305aedb81c"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e79f1a3970d25f692bbb8c8c2637e621a66c0d60c109ab48d4a160f50856deff"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:562681188c62c024fe2c611b32e08b8de2afa00c0c4e72bed47c47c318e16d5c"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a77a60315c33393b2bd29d538d1ef026060a63d3a49a9233b779261bad9c3f71"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4fabb8cc9422efae1a925160083fdcbab8fdc96a8483441eb7457235df625bd"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a78dba8c2a1e9d53a0fb5382979f024200dc86adc46a56cbb668a2249862fda"}, + {file = "fonttools-4.47.0-cp38-cp38-win32.whl", hash = "sha256:e6b968543fde4119231c12c2a953dcf83349590ca631ba8216a8edf9cd4d36a9"}, + {file = "fonttools-4.47.0-cp38-cp38-win_amd64.whl", hash = "sha256:4a9a51745c0439516d947480d4d884fa18bd1458e05b829e482b9269afa655bc"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:62d8ddb058b8e87018e5dc26f3258e2c30daad4c87262dfeb0e2617dd84750e6"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dde0eab40faaa5476133123f6a622a1cc3ac9b7af45d65690870620323308b4"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4da089f6dfdb822293bde576916492cd708c37c2501c3651adde39804630538"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:253bb46bab970e8aae254cebf2ae3db98a4ef6bd034707aa68a239027d2b198d"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1193fb090061efa2f9e2d8d743ae9850c77b66746a3b32792324cdce65784154"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:084511482dd265bce6dca24c509894062f0117e4e6869384d853f46c0e6d43be"}, + {file = "fonttools-4.47.0-cp39-cp39-win32.whl", hash = "sha256:97620c4af36e4c849e52661492e31dc36916df12571cb900d16960ab8e92a980"}, + {file = "fonttools-4.47.0-cp39-cp39-win_amd64.whl", hash = "sha256:e77bdf52185bdaf63d39f3e1ac3212e6cfa3ab07d509b94557a8902ce9c13c82"}, + {file = "fonttools-4.47.0-py3-none-any.whl", hash = "sha256:d6477ba902dd2d7adda7f0fd3bfaeb92885d45993c9e1928c9f28fc3961415f7"}, + {file = "fonttools-4.47.0.tar.gz", hash = "sha256:ec13a10715eef0e031858c1c23bfaee6cba02b97558e4a7bfa089dba4a8c2ebf"}, +] + +[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", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "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.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "fsspec" +version = "2023.12.2" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960"}, + {file = "fsspec-2023.12.2.tar.gz", hash = "sha256:8548d39e8810b59c38014934f6b31e57f40c1b20f911f4cc2b85389c7e9bf0cb"}, +] + +[package.dependencies] +aiohttp = {version = "<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1", optional = true, markers = "extra == \"http\""} +requests = {version = "*", optional = true, markers = "extra == \"http\""} + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "gdown" +version = "4.7.1" +description = "Google Drive direct download of big files." +optional = true +python-versions = "*" +files = [ + {file = "gdown-4.7.1-py3-none-any.whl", hash = "sha256:65d495699e7c2c61af0d0e9c32748fb4f79abaf80d747a87456c7be14aac2560"}, + {file = "gdown-4.7.1.tar.gz", hash = "sha256:347f23769679aaf7efa73e5655270fcda8ca56be65eb84a4a21d143989541045"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +filelock = "*" +requests = {version = "*", extras = ["socks"]} +six = "*" +tqdm = "*" + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.40" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, + {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] + +[[package]] +name = "huggingface-hub" +version = "0.20.1" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.20.1-py3-none-any.whl", hash = "sha256:ecfdea395a8bc68cd160106c5bd857f7e010768d95f9e1862a779010cc304831"}, + {file = "huggingface_hub-0.20.1.tar.gz", hash = "sha256:8c88c4c3c8853e22f2dfb4d84c3d493f4e1af52fb3856a90e1eeddcf191ddbb1"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "hydra-core" +version = "1.3.2" +description = "A framework for elegantly configuring complex applications" +optional = false +python-versions = "*" +files = [ + {file = "hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824"}, + {file = "hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b"}, +] + +[package.dependencies] +antlr4-python3-runtime = "==4.9.*" +omegaconf = ">=2.2,<2.4" +packaging = "*" + +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "imageio" +version = "2.33.1" +description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." +optional = false +python-versions = ">=3.8" +files = [ + {file = "imageio-2.33.1-py3-none-any.whl", hash = "sha256:c5094c48ccf6b2e6da8b4061cd95e1209380afafcbeae4a4e280938cce227e1d"}, + {file = "imageio-2.33.1.tar.gz", hash = "sha256:78722d40b137bd98f5ec7312119f8aea9ad2049f76f434748eb306b6937cc1ce"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=8.3.2" + +[package.extras] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "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", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] +gdal = ["gdal"] +itk = ["itk"] +linting = ["black", "flake8"] +pillow-heif = ["pillow-heif"] +pyav = ["av"] +test = ["fsspec[github]", "pytest", "pytest-cov"] +tifffile = ["tifffile"] + +[[package]] +name = "importlib-resources" +version = "6.1.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "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", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipykernel" +version = "6.27.1" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.27.1-py3-none-any.whl", hash = "sha256:dab88b47f112f9f7df62236511023c9bdeef67abc73af7c652e4ce4441601686"}, + {file = "ipykernel-6.27.1.tar.gz", hash = "sha256:7d5d594b6690654b4d299edba5e872dc17bb7396a8d0609c97cb7b8a1c605de6"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=20" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.19.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.19.0-py3-none-any.whl", hash = "sha256:2f55d59370f59d0d2b2212109fe0e6035cfea436b1c0e6150ad2244746272ec5"}, + {file = "ipython-8.19.0.tar.gz", hash = "sha256:ac4da4ecf0042fb4e0ce57c60430c2db3c719fa8bdf92f8631d6bd8a5785d1f0"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.23)", "pandas", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "ipywidgets" +version = "8.1.1" +description = "Jupyter interactive widgets" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ipywidgets-8.1.1-py3-none-any.whl", hash = "sha256:2b88d728656aea3bbfd05d32c747cfd0078f9d7e159cf982433b58ad717eed7f"}, + {file = "ipywidgets-8.1.1.tar.gz", hash = "sha256:40211efb556adec6fa450ccc2a77d59ca44a060f4f9f136833df59c9f538e6e8"}, +] + +[package.dependencies] +comm = ">=0.1.3" +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.9,<3.1.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=4.0.9,<4.1.0" + +[package.extras] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.6", optional = true, markers = "extra == \"colors\""} + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +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]] +name = "joblib" +version = "1.3.2" +description = "Lightweight pipelining with Python functions" +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]] +name = "json5" +version = "0.9.14" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = "*" +files = [ + {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"}, + {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"}, +] + +[package.extras] +dev = ["hypothesis"] + +[[package]] +name = "jsonargparse" +version = "4.27.1" +description = "Implement minimal boilerplate CLIs derived from type hints and parse from command line, config files and environment variables." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonargparse-4.27.1-py3-none-any.whl", hash = "sha256:de11caf52174357589ee4570b45e37bd3c3c12b4f4823e4897b266ebfa31f625"}, + {file = "jsonargparse-4.27.1.tar.gz", hash = "sha256:16c83eea4f4f4129e9ddb2d867c0425b4db028bcb6580547a393274a1b9a7c34"}, +] + +[package.dependencies] +docstring-parser = {version = ">=0.15", optional = true, markers = "extra == \"signatures\""} +PyYAML = ">=3.13" +typeshed-client = {version = ">=2.1.0", optional = true, markers = "extra == \"signatures\""} + +[package.extras] +all = ["jsonargparse[argcomplete]", "jsonargparse[fsspec]", "jsonargparse[jsonnet]", "jsonargparse[jsonschema]", "jsonargparse[omegaconf]", "jsonargparse[reconplogger]", "jsonargparse[ruyaml]", "jsonargparse[signatures]", "jsonargparse[typing-extensions]", "jsonargparse[urls]"] +argcomplete = ["argcomplete (>=2.0.0)"] +coverage = ["jsonargparse[test-no-urls]", "pytest-cov (>=4.0.0)"] +dev = ["build (>=0.10.0)", "jsonargparse[coverage]", "jsonargparse[doc]", "jsonargparse[mypy]", "jsonargparse[test]", "pre-commit (>=2.19.0)", "tox (>=3.25.0)"] +doc = ["Sphinx (>=1.7.9)", "autodocsumm (>=0.1.10)", "sphinx-autodoc-typehints (>=1.19.5)", "sphinx-rtd-theme (>=1.2.2)"] +fsspec = ["fsspec (>=0.8.4)"] +jsonnet = ["jsonnet (>=0.13.0)", "jsonnet-binary (>=0.17.0)"] +jsonschema = ["jsonschema (>=3.2.0)"] +maintainer = ["bump2version (>=0.5.11)", "twine (>=4.0.2)"] +omegaconf = ["omegaconf (>=2.1.1)"] +reconplogger = ["reconplogger (>=4.4.0)"] +ruyaml = ["ruyaml (>=0.20.0)"] +signatures = ["docstring-parser (>=0.15)", "jsonargparse[typing-extensions]", "typeshed-client (>=2.1.0)"] +test = ["attrs (>=22.2.0)", "jsonargparse[test-no-urls]", "pydantic (>=2.3.0)", "responses (>=0.12.0)", "types-PyYAML (>=6.0.11)", "types-requests (>=2.28.9)"] +test-no-urls = ["pytest (>=6.2.5)", "pytest-subtests (>=0.8.0)"] +typing-extensions = ["typing-extensions (>=3.10.0.0)"] +urls = ["requests (>=2.18.4)"] + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + +[[package]] +name = "jsonschema" +version = "4.20.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, + {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.11.2" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.11.2-py3-none-any.whl", hash = "sha256:e74ba7c0a65e8cb49dc26837d6cfe576557084a8b423ed16a420984228104f93"}, + {file = "jsonschema_specifications-2023.11.2.tar.gz", hash = "sha256:9472fc4fea474cd74bea4a2b190daeccb5a9e4db2ea80efcf7a1b582fc9a81b8"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter" +version = "1.0.0" +description = "Jupyter metapackage. Install all the Jupyter components in one go." +optional = false +python-versions = "*" +files = [ + {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, + {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"}, + {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, +] + +[package.dependencies] +ipykernel = "*" +ipywidgets = "*" +jupyter-console = "*" +nbconvert = "*" +notebook = "*" +qtconsole = "*" + +[[package]] +name = "jupyter-client" +version = "8.6.0" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.0-py3-none-any.whl", hash = "sha256:909c474dbe62582ae62b758bca86d6518c85234bdee2d908c778db6d72f39d99"}, + {file = "jupyter_client-8.6.0.tar.gz", hash = "sha256:0642244bb83b4764ae60d07e010e15f0e2d275ec4e918a8f7b80fbbef3ca60c7"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +description = "Jupyter terminal console" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, + {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, +] + +[package.dependencies] +ipykernel = ">=6.14" +ipython = "*" +jupyter-client = ">=7.0.0" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +prompt-toolkit = ">=3.0.30" +pygments = "*" +pyzmq = ">=17" +traitlets = ">=5.4" + +[package.extras] +test = ["flaky", "pexpect", "pytest"] + +[[package]] +name = "jupyter-core" +version = "5.5.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.5.1-py3-none-any.whl", hash = "sha256:220dfb00c45f0d780ce132bb7976b58263f81a3ada6e90a9b6823785a424f739"}, + {file = "jupyter_core-5.5.1.tar.gz", hash = "sha256:1553311a97ccd12936037f36b9ab4d6ae8ceea6ad2d5c90d94a909e752178e40"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.9.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.9.0-py3-none-any.whl", hash = "sha256:d853b3c10273ff9bc8bb8b30076d65e2c9685579db736873de6c2232dde148bf"}, + {file = "jupyter_events-0.9.0.tar.gz", hash = "sha256:81ad2e4bc710881ec274d31c6c50669d71bbaa5dd9d01e600b56faa85700d399"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.1" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.1.tar.gz", hash = "sha256:b17fab6d70fe83c8896b0cff59237640038247c196056b43684a0902b6a9e0fb"}, + {file = "jupyter_lsp-2.2.1-py3-none-any.whl", hash = "sha256:17a689910c5e4ae5e7d334b02f31d08ffbe98108f6f658fb05e4304b4345368b"}, +] + +[package.dependencies] +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.12.1" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.12.1-py3-none-any.whl", hash = "sha256:fd030dd7be1ca572e4598203f718df6630c12bd28a599d7f1791c4d7938e1010"}, + {file = "jupyter_server-2.12.1.tar.gz", hash = "sha256:dc77b7dcc5fc0547acba2b2844f01798008667201eea27c6319ff9257d700a6d"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = "*" +jinja2 = "*" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = "*" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = "*" +packaging = "*" +prometheus-client = "*" +pywinpty = {version = "*", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = "*" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.0" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.5.0-py3-none-any.whl", hash = "sha256:2fc0692c883bfd891f4fba0c4b4a684a37234b0ba472f2e97ed0a3888f46e1e4"}, + {file = "jupyter_server_terminals-0.5.0.tar.gz", hash = "sha256:ebcd68c9afbf98a480a533e6f3266354336e645536953b7abcc7bdeebc0154a3"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.0.9" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.0.9-py3-none-any.whl", hash = "sha256:9f6f8e36d543fdbcc3df961a1d6a3f524b4a4001be0327a398f68fa4e534107c"}, + {file = "jupyterlab-4.0.9.tar.gz", hash = "sha256:9ebada41d52651f623c0c9f069ddb8a21d6848e4c887d8e5ddc0613166ed5c0b"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +ipykernel = "*" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.19.0,<3" +notebook-shim = ">=0.2" +packaging = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["black[jupyter] (==23.10.1)", "build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.1.4)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-tornasync", "sphinx (>=1.8,<7.2.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.0.1)", "ipython (==8.14.0)", "ipywidgets (==8.0.6)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post0)", "matplotlib (==3.7.1)", "nbconvert (>=7.0.0)", "pandas (==2.0.2)", "scipy (==1.10.1)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.25.2" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.25.2-py3-none-any.whl", hash = "sha256:5b1798c9cc6a44f65c757de9f97fc06fc3d42535afbf47d2ace5e964ab447aaf"}, + {file = "jupyterlab_server-2.25.2.tar.gz", hash = "sha256:bd0ec7a99ebcedc8bcff939ef86e52c378e44c2707e053fcd81d046ce979ee63"}, +] + +[package.dependencies] +babel = ">=2.10" +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.9" +description = "Jupyter interactive widgets for JupyterLab" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_widgets-3.0.9-py3-none-any.whl", hash = "sha256:3cf5bdf5b897bf3bccf1c11873aa4afd776d7430200f765e0686bd352487b58d"}, + {file = "jupyterlab_widgets-3.0.9.tar.gz", hash = "sha256:6005a4e974c7beee84060fdfba341a3218495046de8ae3ec64888e5fe19fdb4c"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "lazy-loader" +version = "0.3" +description = "lazy_loader" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy_loader-0.3-py3-none-any.whl", hash = "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554"}, + {file = "lazy_loader-0.3.tar.gz", hash = "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37"}, +] + +[package.extras] +lint = ["pre-commit (>=3.3)"] +test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] + +[[package]] +name = "lightning-utilities" +version = "0.10.0" +description = "PyTorch Lightning Sample project." +optional = false +python-versions = ">=3.7" +files = [ + {file = "lightning-utilities-0.10.0.tar.gz", hash = "sha256:9e31617eccbbadc6b737a2432fd7076ff8e24957f9c63aeba2530b189e19319c"}, + {file = "lightning_utilities-0.10.0-py3-none-any.whl", hash = "sha256:84d09b11fe9bc16c803ae5e412874748239d73ad2f3d1b90862f99ce15a03aa0"}, +] + +[package.dependencies] +packaging = ">=17.1" +setuptools = "*" +typing-extensions = "*" + +[package.extras] +cli = ["fire"] +docs = ["requests (>=2.0.0)"] +typing = ["mypy (>=1.0.0)", "types-setuptools"] + +[[package]] +name = "lit" +version = "17.0.6" +description = "A Software Testing Tool" +optional = false +python-versions = "*" +files = [ + {file = "lit-17.0.6.tar.gz", hash = "sha256:dfa9af9b55fc4509a56be7bf2346f079d7f4a242d583b9f2e0b078fd0abae31b"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +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]] +name = "matplotlib" +version = "3.8.2" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7"}, + {file = "matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367"}, + {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18"}, + {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31"}, + {file = "matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a"}, + {file = "matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a"}, + {file = "matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63"}, + {file = "matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8"}, + {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6"}, + {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788"}, + {file = "matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0"}, + {file = "matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717"}, + {file = "matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627"}, + {file = "matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4"}, + {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d"}, + {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331"}, + {file = "matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213"}, + {file = "matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630"}, + {file = "matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f"}, + {file = "matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89"}, + {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917"}, + {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843"}, + {file = "matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8"}, + {file = "matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4"}, + {file = "matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b"}, + {file = "matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20"}, + {file = "matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa"}, + {file = "matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.21,<2" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, +] + +[[package]] +name = "monai" +version = "1.3.0" +description = "AI Toolkit for Healthcare Imaging" +optional = false +python-versions = ">=3.8" +files = [ + {file = "monai-1.3.0-202310121228-py3-none-any.whl", hash = "sha256:6ac93decd6aff4c8272eb095e01c8a734bede07246a952cec3c9e2df5b116267"}, +] + +[package.dependencies] +numpy = ">=1.20" +torch = ">=1.9" + +[package.extras] +all = ["clearml (>=1.10.0rc0)", "cucim (>=23.2.0)", "einops", "fire", "gdown (>=4.4.0)", "h5py", "imagecodecs", "itk (>=5.2)", "jsonschema", "lmdb", "lpips (==0.1.4)", "matplotlib", "mlflow (>=1.28.0)", "nibabel", "ninja", "nni", "nvidia-ml-py", "onnx (>=1.13.0)", "onnxruntime", "openslide-python (==1.1.2)", "optuna", "pandas", "pillow", "psutil", "pydicom", "pynrrd", "pytorch-ignite (==0.4.11)", "pyyaml", "scikit-image (>=0.14.2)", "scipy (>=1.7.1)", "tensorboard", "tensorboardX", "tifffile", "torchvision", "tqdm (>=4.47.0)", "transformers (<4.22)", "zarr"] +clearml = ["clearml"] +cucim = ["cucim (>=23.2.0)"] +einops = ["einops"] +fire = ["fire"] +gdown = ["gdown (>=4.4.0)"] +h5py = ["h5py"] +ignite = ["pytorch-ignite (==0.4.11)"] +imagecodecs = ["imagecodecs"] +itk = ["itk (>=5.2)"] +jsonschema = ["jsonschema"] +lmdb = ["lmdb"] +lpips = ["lpips (==0.1.4)"] +matplotlib = ["matplotlib"] +mlflow = ["mlflow"] +nibabel = ["nibabel"] +ninja = ["ninja"] +nni = ["nni"] +onnx = ["onnx (>=1.13.0)", "onnxruntime"] +openslide = ["openslide-python (==1.1.2)"] +optuna = ["optuna"] +pandas = ["pandas"] +pillow = ["pillow (!=8.3.0)"] +psutil = ["psutil"] +pydicom = ["pydicom"] +pynrrd = ["pynrrd"] +pynvml = ["nvidia-ml-py"] +pyyaml = ["pyyaml"] +scipy = ["scipy (>=1.7.1)"] +skimage = ["scikit-image (>=0.14.2)"] +tensorboard = ["tensorboard"] +tensorboardx = ["tensorboardX"] +tifffile = ["tifffile"] +torchvision = ["torchvision"] +tqdm = ["tqdm (>=4.47.0)"] +transformers = ["transformers (<4.22)"] +zarr = ["zarr"] + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +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]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "munch" +version = "4.0.0" +description = "A dot-accessible dictionary (a la JavaScript objects)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4"}, + {file = "munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235"}, +] + +[package.extras] +testing = ["astroid (>=2.0)", "coverage", "pylint (>=2.3.1,<2.4.0)", "pytest"] +yaml = ["PyYAML (>=5.1.0)"] + +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nbclient" +version = "0.9.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.9.0-py3-none-any.whl", hash = "sha256:a3a1ddfb34d4a9d17fc744d655962714a866639acd30130e9be84191cd97cd15"}, + {file = "nbclient-0.9.0.tar.gz", hash = "sha256:4b28c207877cf33ef3a9838cdc7a54c5ceff981194a82eac59d558f05487295e"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.13.1" +description = "Converting Jupyter Notebooks" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.13.1-py3-none-any.whl", hash = "sha256:3c50eb2d326478cc90b8759cf2ab9dde3d892c6537cd6a5bc0991db8ef734bcc"}, + {file = "nbconvert-7.13.1.tar.gz", hash = "sha256:2dc8267dbdfeedce2dcd34c9e3f1b51af18f43cb105549d1c5a18189ec23ba85"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["nbconvert[qtpng]"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.9.2" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"}, + {file = "nbformat-5.9.2.tar.gz", hash = "sha256:5f98b5ba1997dff175e77e0c17d5c10a96eaed2cbd1de3533d1fc35d5e111192"}, +] + +[package.dependencies] +fastjsonschema = "*" +jsonschema = ">=2.6" +jupyter-core = "*" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.5.8" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.5.8-py3-none-any.whl", hash = "sha256:accda7a339a70599cb08f9dd09a67e0c2ef8d8d6f4c07f96ab203f2ae254e48d"}, + {file = "nest_asyncio-1.5.8.tar.gz", hash = "sha256:25aa2ca0d2a5b5531956b9e273b45cf664cae2b145101d73b86b199978d48fdb"}, +] + +[[package]] +name = "networkx" +version = "3.2.1" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, +] + +[package.extras] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "notebook" +version = "7.0.6" +description = "Jupyter Notebook - A web-based notebook environment for interactive computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "notebook-7.0.6-py3-none-any.whl", hash = "sha256:0fe8f67102fea3744fedf652e4c15339390902ca70c5a31c4f547fa23da697cc"}, + {file = "notebook-7.0.6.tar.gz", hash = "sha256:ec6113b06529019f7f287819af06c97a2baf7a95ac21a8f6e32192898e9f9a58"}, +] + +[package.dependencies] +jupyter-server = ">=2.4.0,<3" +jupyterlab = ">=4.0.2,<5" +jupyterlab-server = ">=2.22.1,<3" +notebook-shim = ">=0.2,<0.3" +tornado = ">=6.2.0" + +[package.extras] +dev = ["hatch", "pre-commit"] +docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] + +[[package]] +name = "notebook-shim" +version = "0.2.3" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.3-py3-none-any.whl", hash = "sha256:a83496a43341c1674b093bfcebf0fe8e74cbe7eda5fd2bbc56f8e39e1486c0c7"}, + {file = "notebook_shim-0.2.3.tar.gz", hash = "sha256:f69388ac283ae008cd506dda10d0288b09a017d822d5e8c7129a152cbd3ce7e9"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + +[[package]] +name = "numcodecs" +version = "0.12.1" +description = "A Python package providing buffer compression and transformation codecs for use in data storage and communication applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "numcodecs-0.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d37f628fe92b3699e65831d5733feca74d2e33b50ef29118ffd41c13c677210e"}, + {file = "numcodecs-0.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:941b7446b68cf79f089bcfe92edaa3b154533dcbcd82474f994b28f2eedb1c60"}, + {file = "numcodecs-0.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e79bf9d1d37199ac00a60ff3adb64757523291d19d03116832e600cac391c51"}, + {file = "numcodecs-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:82d7107f80f9307235cb7e74719292d101c7ea1e393fe628817f0d635b7384f5"}, + {file = "numcodecs-0.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eeaf42768910f1c6eebf6c1bb00160728e62c9343df9e2e315dc9fe12e3f6071"}, + {file = "numcodecs-0.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:135b2d47563f7b9dc5ee6ce3d1b81b0f1397f69309e909f1a35bb0f7c553d45e"}, + {file = "numcodecs-0.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a191a8e347ecd016e5c357f2bf41fbcb026f6ffe78fff50c77ab12e96701d155"}, + {file = "numcodecs-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:21d8267bd4313f4d16f5b6287731d4c8ebdab236038f29ad1b0e93c9b2ca64ee"}, + {file = "numcodecs-0.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2f84df6b8693206365a5b37c005bfa9d1be486122bde683a7b6446af4b75d862"}, + {file = "numcodecs-0.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:760627780a8b6afdb7f942f2a0ddaf4e31d3d7eea1d8498cf0fd3204a33c4618"}, + {file = "numcodecs-0.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c258bd1d3dfa75a9b708540d23b2da43d63607f9df76dfa0309a7597d1de3b73"}, + {file = "numcodecs-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e04649ea504aff858dbe294631f098fbfd671baf58bfc04fc48d746554c05d67"}, + {file = "numcodecs-0.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:caf1a1e6678aab9c1e29d2109b299f7a467bd4d4c34235b1f0e082167846b88f"}, + {file = "numcodecs-0.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c17687b1fd1fef68af616bc83f896035d24e40e04e91e7e6dae56379eb59fe33"}, + {file = "numcodecs-0.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29dfb195f835a55c4d490fb097aac8c1bcb96c54cf1b037d9218492c95e9d8c5"}, + {file = "numcodecs-0.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:2f1ba2f4af3fd3ba65b1bcffb717fe65efe101a50a91c368f79f3101dbb1e243"}, + {file = "numcodecs-0.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fbb12a6a1abe95926f25c65e283762d63a9bf9e43c0de2c6a1a798347dfcb40"}, + {file = "numcodecs-0.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2207871868b2464dc11c513965fd99b958a9d7cde2629be7b2dc84fdaab013b"}, + {file = "numcodecs-0.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abff3554a6892a89aacf7b642a044e4535499edf07aeae2f2e6e8fc08c9ba07f"}, + {file = "numcodecs-0.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef964d4860d3e6b38df0633caf3e51dc850a6293fd8e93240473642681d95136"}, + {file = "numcodecs-0.12.1.tar.gz", hash = "sha256:05d91a433733e7eef268d7e80ec226a0232da244289614a8f3826901aec1098e"}, +] + +[package.dependencies] +numpy = ">=1.7" + +[package.extras] +docs = ["mock", "numpydoc", "sphinx (<7.0.0)", "sphinx-issues"] +msgpack = ["msgpack"] +test = ["coverage", "flake8", "pytest", "pytest-cov"] +test-extras = ["importlib-metadata"] +zfpy = ["zfpy (>=1.0.0)"] + +[[package]] +name = "numpy" +version = "1.26.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +description = "A flexible configuration library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, + {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, +] + +[package.dependencies] +antlr4-python3-runtime = "==4.9.*" +PyYAML = ">=5.1.0" + +[[package]] +name = "opencv-python-headless" +version = "4.8.1.78" +description = "Wrapper package for OpenCV python bindings." +optional = false +python-versions = ">=3.6" +files = [ + {file = "opencv-python-headless-4.8.1.78.tar.gz", hash = "sha256:bc7197b42352f6f865c302a49140b889ec7cd957dd697e2d7fc016ad0d3f28f1"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:f3a33f644249f9ce1c913eac580e4b3ef4ce7cab0a71900274708959c2feb5e3"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:2c7d45721df9801c4dcd34683a15caa0e30f38b185263fec04a6eb274bc720f0"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b6bd6e1132b6f5dcb3a5bfe30fc4d341a7bfb26134da349a06c9255288ded94"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58e70d2f0915fe23e02c6e405588276c9397844a47d38b9c87fac5f7f9ba2dcc"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:382f8c7a6a14f80091284eecedd52cee4812231ee0eff1118592197b538d9252"}, + {file = "opencv_python_headless-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:0a0f1e9f836f7d5bad1dd164694944c8761711cbdf4b36ebbd4815a8ef731079"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, +] + +[[package]] +name = "overrides" +version = "7.4.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, + {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandas" +version = "2.1.4" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, + {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, + {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, + {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, + {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, + {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, + {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, + {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, + {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, + {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, + {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + +[[package]] +name = "pandocfilters" +version = "1.5.0" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "patchify" +version = "0.2.3" +description = "A library that helps you split image into small, overlappable patches, and merge patches back into the original image." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "patchify-0.2.3-py3-none-any.whl", hash = "sha256:4bd4f80c83280b36c6968cb4d802bde28cd11446cc8ace94e0aa14f573fcf41b"}, + {file = "patchify-0.2.3.tar.gz", hash = "sha256:6cc409124f34ceee672f1931d818923f88f5116f323ac7bb9be7e6c5d0845502"}, +] + +[package.dependencies] +numpy = ">=1,<2" + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pathtools" +version = "0.1.2" +description = "File system general utilities" +optional = false +python-versions = "*" +files = [ + {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +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]] +name = "pint" +version = "0.22" +description = "Physical quantities module" +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]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "plotly" +version = "5.18.0" +description = "An open-source, interactive data visualization library for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "plotly-5.18.0-py3-none-any.whl", hash = "sha256:23aa8ea2f4fb364a20d34ad38235524bd9d691bf5299e800bca608c31e8db8de"}, + {file = "plotly-5.18.0.tar.gz", hash = "sha256:360a31e6fbb49d12b007036eb6929521343d6bee2236f8459915821baefa2cbb"}, +] + +[package.dependencies] +packaging = "*" +tenacity = ">=6.2.0" + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pretrainedmodels" +version = "0.7.4" +description = "Pretrained models for Pytorch" +optional = false +python-versions = "*" +files = [ + {file = "pretrainedmodels-0.7.4.tar.gz", hash = "sha256:7e77ead4619a3e11ab3c41982c8ad5b86edffe37c87fd2a37ec3c2cc6470b98a"}, +] + +[package.dependencies] +munch = "*" +torch = "*" +torchvision = "*" +tqdm = "*" + +[[package]] +name = "prometheus-client" +version = "0.19.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"}, + {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "protobuf" +version = "4.25.1" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, + {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, + {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, + {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, + {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, + {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, + {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, + {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, + {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, +] + +[[package]] +name = "psutil" +version = "5.9.7" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.7-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056"}, + {file = "psutil-5.9.7-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df"}, + {file = "psutil-5.9.7-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7"}, + {file = "psutil-5.9.7-cp27-none-win32.whl", hash = "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c"}, + {file = "psutil-5.9.7-cp27-none-win_amd64.whl", hash = "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6"}, + {file = "psutil-5.9.7-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284"}, + {file = "psutil-5.9.7-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe"}, + {file = "psutil-5.9.7-cp36-cp36m-win32.whl", hash = "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9"}, + {file = "psutil-5.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e"}, + {file = "psutil-5.9.7-cp37-abi3-win32.whl", hash = "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68"}, + {file = "psutil-5.9.7-cp37-abi3-win_amd64.whl", hash = "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414"}, + {file = "psutil-5.9.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340"}, + {file = "psutil-5.9.7.tar.gz", hash = "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +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]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +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]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + +[[package]] +name = "pytorch-lightning" +version = "2.1.3" +description = "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytorch-lightning-2.1.3.tar.gz", hash = "sha256:2500b002fa09cb37b0e12f879876bf30a2d260b0f04783d33264dab175f0c966"}, + {file = "pytorch_lightning-2.1.3-py3-none-any.whl", hash = "sha256:03ed186035a230b161130e0d8ecf1dd6657ff7e3f1520e9257b0db7650f9aeea"}, +] + +[package.dependencies] +bitsandbytes = {version = "<=0.41.1", optional = true, markers = "extra == \"extra\""} +fsspec = {version = ">=2022.5.0", extras = ["http"]} +hydra-core = {version = ">=1.0.5", optional = true, markers = "extra == \"extra\""} +jsonargparse = {version = ">=4.26.1", extras = ["signatures"], optional = true, markers = "extra == \"extra\""} +lightning-utilities = ">=0.8.0" +matplotlib = {version = ">3.1", optional = true, markers = "extra == \"extra\""} +numpy = ">=1.17.2" +omegaconf = {version = ">=2.0.5", optional = true, markers = "extra == \"extra\""} +packaging = ">=20.0" +PyYAML = ">=5.4" +rich = {version = ">=12.3.0", optional = true, markers = "extra == \"extra\""} +tensorboardX = {version = ">=2.2", optional = true, markers = "extra == \"extra\""} +torch = ">=1.12.0" +torchmetrics = ">=0.7.0" +tqdm = ">=4.57.0" +typing-extensions = ">=4.0.0" + +[package.extras] +all = ["bitsandbytes (<=0.41.1)", "deepspeed (>=0.8.2,<=0.9.3)", "gym[classic-control] (>=0.17.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.15.0)", "jsonargparse[signatures] (>=4.26.1)", "lightning-utilities (>=0.8.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=12.3.0)", "tensorboardX (>=2.2)", "torchmetrics (>=0.10.0)", "torchvision (>=0.13.0)"] +deepspeed = ["deepspeed (>=0.8.2,<=0.9.3)"] +dev = ["bitsandbytes (<=0.41.1)", "cloudpickle (>=1.3)", "coverage (==7.3.1)", "deepspeed (>=0.8.2,<=0.9.3)", "fastapi", "gym[classic-control] (>=0.17.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.15.0)", "jsonargparse[signatures] (>=4.26.1)", "lightning-utilities (>=0.8.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnx (>=0.14.0)", "onnxruntime (>=0.15.0)", "pandas (>1.0)", "psutil (<5.9.6)", "pytest (==7.4.0)", "pytest-cov (==4.1.0)", "pytest-random-order (==1.1.0)", "pytest-rerunfailures (==12.0)", "pytest-timeout (==2.1.0)", "rich (>=12.3.0)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "tensorboardX (>=2.2)", "torchmetrics (>=0.10.0)", "torchvision (>=0.13.0)", "uvicorn"] +examples = ["gym[classic-control] (>=0.17.0)", "ipython[all] (<8.15.0)", "lightning-utilities (>=0.8.0)", "torchmetrics (>=0.10.0)", "torchvision (>=0.13.0)"] +extra = ["bitsandbytes (<=0.41.1)", "hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.26.1)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=12.3.0)", "tensorboardX (>=2.2)"] +strategies = ["deepspeed (>=0.8.2,<=0.9.3)"] +test = ["cloudpickle (>=1.3)", "coverage (==7.3.1)", "fastapi", "onnx (>=0.14.0)", "onnxruntime (>=0.15.0)", "pandas (>1.0)", "psutil (<5.9.6)", "pytest (==7.4.0)", "pytest-cov (==4.1.0)", "pytest-random-order (==1.1.0)", "pytest-rerunfailures (==12.0)", "pytest-timeout (==2.1.0)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn"] + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyupgrade" +version = "3.15.0" +description = "A tool to automatically upgrade syntax for newer versions." +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "pyupgrade-3.15.0-py2.py3-none-any.whl", hash = "sha256:8dc8ebfaed43566e2c65994162795017c7db11f531558a74bc8aa077907bc305"}, + {file = "pyupgrade-3.15.0.tar.gz", hash = "sha256:a7fde381060d7c224f55aef7a30fae5ac93bbc428367d27e70a603bc2acd4f00"}, +] + +[package.dependencies] +tokenize-rt = ">=5.2.0" + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pywinpty" +version = "2.0.12" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"}, + {file = "pywinpty-2.0.12-cp311-none-win_amd64.whl", hash = "sha256:853985a8f48f4731a716653170cd735da36ffbdc79dcb4c7b7140bce11d8c722"}, + {file = "pywinpty-2.0.12-cp312-none-win_amd64.whl", hash = "sha256:1617b729999eb6713590e17665052b1a6ae0ad76ee31e60b444147c5b6a35dca"}, + {file = "pywinpty-2.0.12-cp38-none-win_amd64.whl", hash = "sha256:189380469ca143d06e19e19ff3fba0fcefe8b4a8cc942140a6b863aed7eebb2d"}, + {file = "pywinpty-2.0.12-cp39-none-win_amd64.whl", hash = "sha256:7520575b6546db23e693cbd865db2764097bd6d4ef5dc18c92555904cd62c3d4"}, + {file = "pywinpty-2.0.12.tar.gz", hash = "sha256:8197de460ae8ebb7f5d1701dfa1b5df45b157bb832e92acba316305e18ca00dd"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzmq" +version = "25.1.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, + {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, + {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, + {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, + {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, + {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, + {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, + {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, + {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, + {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, + {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, + {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, + {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "qtconsole" +version = "5.5.1" +description = "Jupyter Qt console" +optional = false +python-versions = ">= 3.8" +files = [ + {file = "qtconsole-5.5.1-py3-none-any.whl", hash = "sha256:8c75fa3e9b4ed884880ff7cea90a1b67451219279ec33deaee1d59e3df1a5d2b"}, + {file = "qtconsole-5.5.1.tar.gz", hash = "sha256:a0e806c6951db9490628e4df80caec9669b65149c7ba40f9bf033c025a5b56bc"}, +] + +[package.dependencies] +ipykernel = ">=4.1" +jupyter-client = ">=4.1" +jupyter-core = "*" +packaging = "*" +pygments = "*" +pyzmq = ">=17.1" +qtpy = ">=2.4.0" +traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" + +[package.extras] +doc = ["Sphinx (>=1.3)"] +test = ["flaky", "pytest", "pytest-qt"] + +[[package]] +name = "qtpy" +version = "2.4.1" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "QtPy-2.4.1-py3-none-any.whl", hash = "sha256:1c1d8c4fa2c884ae742b069151b0abe15b3f70491f3972698c683b8e38de839b"}, + {file = "QtPy-2.4.1.tar.gz", hash = "sha256:a5a15ffd519550a1361bdc56ffc07fda56a6af7292f17c7b395d4083af632987"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] + +[[package]] +name = "qudida" +version = "0.0.4" +description = "QUick and DIrty Domain Adaptation" +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "qudida-0.0.4-py3-none-any.whl", hash = "sha256:4519714c40cd0f2e6c51e1735edae8f8b19f4efe1f33be13e9d644ca5f736dd6"}, + {file = "qudida-0.0.4.tar.gz", hash = "sha256:db198e2887ab0c9aa0023e565afbff41dfb76b361f85fd5e13f780d75ba18cc8"}, +] + +[package.dependencies] +numpy = ">=0.18.0" +opencv-python-headless = ">=4.0.1" +scikit-learn = ">=0.19.1" +typing-extensions = "*" + +[[package]] +name = "referencing" +version = "0.32.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.32.0-py3-none-any.whl", hash = "sha256:bdcd3efb936f82ff86f993093f6da7435c7de69a3b3a5a06678a6050184bee99"}, + {file = "referencing-0.32.0.tar.gz", hash = "sha256:689e64fe121843dcfd57b71933318ef1f91188ffb45367332700a86ac8fd6161"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rich" +version = "13.7.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.15.2" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:337a8653fb11d2fbe7157c961cc78cb3c161d98cf44410ace9a3dc2db4fad882"}, + {file = "rpds_py-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:813a65f95bfcb7c8f2a70dd6add9b51e9accc3bdb3e03d0ff7a9e6a2d3e174bf"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:082e0e55d73690ffb4da4352d1b5bbe1b5c6034eb9dc8c91aa2a3ee15f70d3e2"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5595c80dd03d7e6c6afb73f3594bf3379a7d79fa57164b591d012d4b71d6ac4c"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb10bb720348fe1647a94eb605accb9ef6a9b1875d8845f9e763d9d71a706387"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53304cc14b1d94487d70086e1cb0cb4c29ec6da994d58ae84a4d7e78c6a6d04d"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d64a657de7aae8db2da60dc0c9e4638a0c3893b4d60101fd564a3362b2bfeb34"}, + {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee40206d1d6e95eaa2b7b919195e3689a5cf6ded730632de7f187f35a1b6052c"}, + {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1607cda6129f815493a3c184492acb5ae4aa6ed61d3a1b3663aa9824ed26f7ac"}, + {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3e6e2e502c4043c52a99316d89dc49f416acda5b0c6886e0dd8ea7bb35859e8"}, + {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:044f6f46d62444800402851afa3c3ae50141f12013060c1a3a0677e013310d6d"}, + {file = "rpds_py-0.15.2-cp310-none-win32.whl", hash = "sha256:c827a931c6b57f50f1bb5de400dcfb00bad8117e3753e80b96adb72d9d811514"}, + {file = "rpds_py-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3bbc89ce2a219662ea142f0abcf8d43f04a41d5b1880be17a794c39f0d609cb0"}, + {file = "rpds_py-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1fd0f0b1ccd7d537b858a56355a250108df692102e08aa2036e1a094fd78b2dc"}, + {file = "rpds_py-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b414ef79f1f06fb90b5165db8aef77512c1a5e3ed1b4807da8476b7e2c853283"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31272c674f725dfe0f343d73b0abe8c878c646967ec1c6106122faae1efc15b"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6945c2d61c42bb7e818677f43638675b8c1c43e858b67a96df3eb2426a86c9d"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02744236ac1895d7be837878e707a5c35fb8edc5137602f253b63623d7ad5c8c"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2181e86d4e1cdf49a7320cb72a36c45efcb7670d0a88f09fd2d3a7967c0540fd"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8ff8e809da81363bffca2b965cb6e4bf6056b495fc3f078467d1f8266fe27f"}, + {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97532802f14d383f37d603a56e226909f825a83ff298dc1b6697de00d2243999"}, + {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:13716e53627ad97babf72ac9e01cf9a7d4af2f75dd5ed7b323a7a9520e948282"}, + {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1f295a5c28cfa74a7d48c95acc1c8a7acd49d7d9072040d4b694fe11cd7166"}, + {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8ec464f20fe803ae00419bd1610934e3bda963aeba1e6181dfc9033dc7e8940c"}, + {file = "rpds_py-0.15.2-cp311-none-win32.whl", hash = "sha256:b61d5096e75fd71018b25da50b82dd70ec39b5e15bb2134daf7eb7bbbc103644"}, + {file = "rpds_py-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:9d41ebb471a6f064c0d1c873c4f7dded733d16ca5db7d551fb04ff3805d87802"}, + {file = "rpds_py-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:13ff62d3561a23c17341b4afc78e8fcfd799ab67c0b1ca32091d71383a98ba4b"}, + {file = "rpds_py-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b70b45a40ad0798b69748b34d508259ef2bdc84fb2aad4048bc7c9cafb68ddb3"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ecbba7efd82bd2a4bb88aab7f984eb5470991c1347bdd1f35fb34ea28dba6e"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d38494a8d21c246c535b41ecdb2d562c4b933cf3d68de03e8bc43a0d41be652"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13152dfe7d7c27c40df8b99ac6aab12b978b546716e99f67e8a67a1d441acbc3"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:164fcee32f15d04d61568c9cb0d919e37ff3195919cd604039ff3053ada0461b"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a5122b17a4faf5d7a6d91fa67b479736c0cacc7afe791ddebb7163a8550b799"}, + {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46b4f3d47d1033db569173be62365fbf7808c2bd3fb742314d251f130d90d44c"}, + {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c61e42b4ceb9759727045765e87d51c1bb9f89987aca1fcc8a040232138cad1c"}, + {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d2aa3ca9552f83b0b4fa6ca8c6ce08da6580f37e3e0ab7afac73a1cfdc230c0e"}, + {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec19e823b4ccd87bd69e990879acbce9e961fc7aebe150156b8f4418d4b27b7f"}, + {file = "rpds_py-0.15.2-cp312-none-win32.whl", hash = "sha256:afeabb382c1256a7477b739820bce7fe782bb807d82927102cee73e79b41b38b"}, + {file = "rpds_py-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:422b0901878a31ef167435c5ad46560362891816a76cc0d150683f3868a6f0d1"}, + {file = "rpds_py-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:baf744e5f9d5ee6531deea443be78b36ed1cd36c65a0b95ea4e8d69fa0102268"}, + {file = "rpds_py-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e072f5da38d6428ba1fc1115d3cc0dae895df671cb04c70c019985e8c7606be"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f138f550b83554f5b344d6be35d3ed59348510edc3cb96f75309db6e9bfe8210"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2a4cd924d0e2f4b1a68034abe4cadc73d69ad5f4cf02db6481c0d4d749f548f"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5eb05b654a41e0f81ab27a7c3e88b6590425eb3e934e1d533ecec5dc88a6ffff"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ee066a64f0d2ba45391cac15b3a70dcb549e968a117bd0500634754cfe0e5fc"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51a899792ee2c696072791e56b2020caff58b275abecbc9ae0cb71af0645c95"}, + {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac2ac84a4950d627d84b61f082eba61314373cfab4b3c264b62efab02ababe83"}, + {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:62b292fff4739c6be89e6a0240c02bda5a9066a339d90ab191cf66e9fdbdc193"}, + {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:98ee201a52a7f65608e5494518932e1473fd43535f12cade0a1b4ab32737fe28"}, + {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3d40fb3ca22e3d40f494d577441b263026a3bd8c97ae6ce89b2d3c4b39ac9581"}, + {file = "rpds_py-0.15.2-cp38-none-win32.whl", hash = "sha256:30479a9f1fce47df56b07460b520f49fa2115ec2926d3b1303c85c81f8401ed1"}, + {file = "rpds_py-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:2df3d07a16a3bef0917b28cd564778fbb31f3ffa5b5e33584470e2d1b0f248f0"}, + {file = "rpds_py-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:56b51ba29a18e5f5810224bcf00747ad931c0716e3c09a76b4a1edd3d4aba71f"}, + {file = "rpds_py-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c11bc5814554b018f6c5d6ae0969e43766f81e995000b53a5d8c8057055e886"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2faa97212b0dc465afeedf49045cdd077f97be1188285e646a9f689cb5dfff9e"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86c01299942b0f4b5b5f28c8701689181ad2eab852e65417172dbdd6c5b3ccc8"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd7d3608589072f63078b4063a6c536af832e76b0b3885f1bfe9e892abe6c207"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:938518a11780b39998179d07f31a4a468888123f9b00463842cd40f98191f4d3"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dccc623725d0b298f557d869a68496a2fd2a9e9c41107f234fa5f7a37d278ac"}, + {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d46ee458452727a147d7897bb33886981ae1235775e05decae5d5d07f537695a"}, + {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9d7ebcd11ea76ba0feaae98485cd8e31467c3d7985210fab46983278214736b"}, + {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8a5f574b92b3ee7d254e56d56e37ec0e1416acb1ae357c4956d76a1788dc58fb"}, + {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3db0c998c92b909d7c90b66c965590d4f3cd86157176a6cf14aa1f867b77b889"}, + {file = "rpds_py-0.15.2-cp39-none-win32.whl", hash = "sha256:bbc7421cbd28b4316d1d017db338039a7943f945c6f2bb15e1439b14b5682d28"}, + {file = "rpds_py-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:1c24e30d720c0009b6fb2e1905b025da56103c70a8b31b99138e4ed1c2a6c5b0"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e6fcd0a0f62f2997107f758bb372397b8d5fd5f39cc6dcb86f7cb98a2172d6c"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d800a8e2ac62db1b9ea5d6d1724f1a93c53907ca061de4d05ed94e8dfa79050c"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e09d017e3f4d9bd7d17a30d3f59e4d6d9ba2d2ced280eec2425e84112cf623f"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b88c3ab98556bc351b36d6208a6089de8c8db14a7f6e1f57f82a334bd2c18f0b"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f333bfe782a2d05a67cfaa0cc9cd68b36b39ee6acfe099f980541ed973a7093"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b629db53fe17e6ce478a969d30bd1d0e8b53238c46e3a9c9db39e8b65a9ef973"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485fbdd23becb822804ed05622907ee5c8e8a5f43f6f43894a45f463b2217045"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:893e38d0f4319dfa70c0f36381a37cc418985c87b11d9784365b1fff4fa6973b"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8ffdeb7dbd0160d4e391e1f857477e4762d00aa2199c294eb95dfb9451aa1d9f"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fc33267d58dfbb2361baed52668c5d8c15d24bc0372cecbb79fed77339b55e0d"}, + {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2e7e5633577b3bd56bf3af2ef6ae3778bbafb83743989d57f0e7edbf6c0980e4"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8b9650f92251fdef843e74fc252cdfd6e3c700157ad686eeb0c6d7fdb2d11652"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:07a2e1d78d382f7181789713cdf0c16edbad4fe14fe1d115526cb6f0eef0daa3"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f9c5875515820633bd7709a25c3e60c1ea9ad1c5d4030ce8a8c203309c36fd"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:580182fa5b269c2981e9ce9764367cb4edc81982ce289208d4607c203f44ffde"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa1e626c524d2c7972c0f3a8a575d654a3a9c008370dc2a97e46abd0eaa749b9"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae9d83a81b09ce3a817e2cbb23aabc07f86a3abc664c613cd283ce7a03541e95"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9235be95662559141934fced8197de6fee8c58870f36756b0584424b6d708393"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a72e00826a2b032dda3eb25aa3e3579c6d6773d22d8446089a57a123481cc46c"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ab095edf1d840a6a6a4307e1a5b907a299a94e7b90e75436ee770b8c35d22a25"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b79c63d29101cbaa53a517683557bb550462394fb91044cc5998dd2acff7340"}, + {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:911e600e798374c0d86235e7ef19109cf865d1336942d398ff313375a25a93ba"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3cd61e759c4075510052d1eca5cddbd297fe1164efec14ef1fce3f09b974dfe4"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d2ae79f31da5143e020a8d4fc74e1f0cbcb8011bdf97453c140aa616db51406"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e99d6510c8557510c220b865d966b105464740dcbebf9b79ecd4fbab30a13d9"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c43e1b89099279cc03eb1c725c5de12af6edcd2f78e2f8a022569efa639ada3"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7187bee72384b9cfedf09a29a3b2b6e8815cc64c095cdc8b5e6aec81e9fd5f"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3423007fc0661827e06f8a185a3792c73dda41f30f3421562f210cf0c9e49569"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2974e6dff38afafd5ccf8f41cb8fc94600b3f4fd9b0a98f6ece6e2219e3158d5"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93c18a1696a8e0388ed84b024fe1a188a26ba999b61d1d9a371318cb89885a8c"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd0841a586b7105513a7c8c3d5c276f3adc762a072d81ef7fae80632afad1e"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:709dc11af2f74ba89c68b1592368c6edcbccdb0a06ba77eb28c8fe08bb6997da"}, + {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fc066395e6332da1e7525d605b4c96055669f8336600bef8ac569d5226a7c76f"}, + {file = "rpds_py-0.15.2.tar.gz", hash = "sha256:373b76eeb79e8c14f6d82cb1d4d5293f9e4059baec6c1b16dca7ad13b6131b39"}, +] + +[[package]] +name = "safetensors" +version = "0.4.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "safetensors-0.4.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:cba01c6b76e01ec453933b3b3c0157c59b52881c83eaa0f7666244e71aa75fd1"}, + {file = "safetensors-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a8f6f679d97ea0135c7935c202feefbd042c149aa70ee759855e890c01c7814"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc2ce1f5ae5143a7fb72b71fa71db6a42b4f6cf912aa3acdc6b914084778e68"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d87d993eaefe6611a9c241a8bd364a5f1ffed5771c74840363a6c4ed8d868f6"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:097e9af2efa8778cd2f0cba451784253e62fa7cc9fc73c0744d27212f7294e25"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d10a9f7bae608ccfdc009351f01dc3d8535ff57f9488a58a4c38e45bf954fe93"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270b99885ec14abfd56c1d7f28ada81740a9220b4bae960c3de1c6fe84af9e4d"}, + {file = "safetensors-0.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:285b52a481e7ba93e29ad4ec5841ef2c4479ef0a6c633c4e2629e0508453577b"}, + {file = "safetensors-0.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c3c9f0ca510e0de95abd6424789dcbc879942a3a4e29b0dfa99d9427bf1da75c"}, + {file = "safetensors-0.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:88b4653059c903015284a9722f9a46838c654257173b279c8f6f46dbe80b612d"}, + {file = "safetensors-0.4.1-cp310-none-win32.whl", hash = "sha256:2fe6926110e3d425c4b684a4379b7796fdc26ad7d16922ea1696c8e6ea7e920f"}, + {file = "safetensors-0.4.1-cp310-none-win_amd64.whl", hash = "sha256:a79e16222106b2f5edbca1b8185661477d8971b659a3c814cc6f15181a9b34c8"}, + {file = "safetensors-0.4.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:d93321eea0dd7e81b283e47a1d20dee6069165cc158286316d0d06d340de8fe8"}, + {file = "safetensors-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ff8e41c8037db17de0ea2a23bc684f43eaf623be7d34906fe1ac10985b8365e"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39d36f1d88468a87c437a1bc27c502e71b6ca44c385a9117a9f9ba03a75cc9c6"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ef010e9afcb4057fb6be3d0a0cfa07aac04fe97ef73fe4a23138d8522ba7c17"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b287304f2b2220d51ccb51fd857761e78bcffbeabe7b0238f8dc36f2edfd9542"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e09000b2599e1836314430f81a3884c66a5cbabdff5d9f175b5d560d4de38d78"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c80ce0001efa16066358d2dd77993adc25f5a6c61850e4ad096a2232930bce"}, + {file = "safetensors-0.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:413e1f6ac248f7d1b755199a06635e70c3515493d3b41ba46063dec33aa2ebb7"}, + {file = "safetensors-0.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3ac139377cfe71ba04573f1cda66e663b7c3e95be850e9e6c2dd4b5984bd513"}, + {file = "safetensors-0.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:04157d008385bea66d12fe90844a80d4a76dc25ec5230b5bd9a630496d1b7c03"}, + {file = "safetensors-0.4.1-cp311-none-win32.whl", hash = "sha256:5f25297148ec665f0deb8bd67e9564634d8d6841041ab5393ccfe203379ea88b"}, + {file = "safetensors-0.4.1-cp311-none-win_amd64.whl", hash = "sha256:b2f8877990a72ff595507b80f4b69036a9a1986a641f8681adf3425d97d3d2a5"}, + {file = "safetensors-0.4.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:eb2c1da1cc39509d1a55620a5f4d14f8911c47a89c926a96e6f4876e864375a3"}, + {file = "safetensors-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:303d2c0415cf15a28f8d7f17379ea3c34c2b466119118a34edd9965983a1a8a6"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4cb3e37a9b961ddd68e873b29fe9ab4a081e3703412e34aedd2b7a8e9cafd9"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae5497adc68669db2fed7cb2dad81e6a6106e79c9a132da3efdb6af1db1014fa"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b30abd0cddfe959d1daedf92edcd1b445521ebf7ddefc20860ed01486b33c90"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d784a98c492c751f228a4a894c3b8a092ff08b24e73b5568938c28b8c0e8f8df"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57a5ab08b0ec7a7caf30d2ac79bb30c89168431aca4f8854464bb9461686925"}, + {file = "safetensors-0.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:edcf3121890b5f0616aa5a54683b1a5d2332037b970e507d6bb7841a3a596556"}, + {file = "safetensors-0.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fdb58dee173ef33634c3016c459d671ca12d11e6acf9db008261cbe58107e579"}, + {file = "safetensors-0.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:780dc21eb3fd32ddd0e8c904bdb0290f2454f4ac21ae71e94f9ce72db1900a5a"}, + {file = "safetensors-0.4.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:48901bd540f8a3c1791314bc5c8a170927bf7f6acddb75bf0a263d081a3637d4"}, + {file = "safetensors-0.4.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3b0b7b2d5976fbed8a05e2bbdce5816a59e6902e9e7c7e07dc723637ed539787"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f69903ff49cb30b9227fb5d029bea276ea20d04b06803877a420c5b1b74c689"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ddd050e01f3e843aa8c1c27bf68675b8a08e385d0045487af4d70418c3cb356"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a82bc2bd7a9a0e08239bdd6d7774d64121f136add93dfa344a2f1a6d7ef35fa"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ace9e66a40f98a216ad661245782483cf79cf56eb2b112650bb904b0baa9db5"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82cbb8f4d022f2e94498cbefca900698b8ded3d4f85212f47da614001ff06652"}, + {file = "safetensors-0.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:791edc10a3c359a2f5f52d5cddab0df8a45107d91027d86c3d44e57162e5d934"}, + {file = "safetensors-0.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:83c2cfbe8c6304f0891e7bb378d56f66d2148972eeb5f747cd8a2246886f0d8c"}, + {file = "safetensors-0.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:04dd14f53f5500eb4c4149674216ba1000670efbcf4b1b5c2643eb244e7882ea"}, + {file = "safetensors-0.4.1-cp37-none-win32.whl", hash = "sha256:d5b3defa74f3723a388bfde2f5d488742bc4879682bd93267c09a3bcdf8f869b"}, + {file = "safetensors-0.4.1-cp37-none-win_amd64.whl", hash = "sha256:25a043cbb59d4f75e9dd87fdf5c009dd8830105a2c57ace49b72167dd9808111"}, + {file = "safetensors-0.4.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:3f6a520af7f2717c5ecba112041f2c8af1ca6480b97bf957aba81ed9642e654c"}, + {file = "safetensors-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3807ac3b16288dffebb3474b555b56fe466baa677dfc16290dcd02dca1ab228"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b58ba13a9e82b4bc3fc221914f6ef237fe6c2adb13cede3ace64d1aacf49610"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dac4bb42f8679aadc59bd91a4c5a1784a758ad49d0912995945cd674089f628e"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911b48dc09e321a194def3a7431662ff4f03646832f3a8915bbf0f449b8a5fcb"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82571d20288c975c1b30b08deb9b1c3550f36b31191e1e81fae87669a92217d0"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da52ee0dc8ba03348ffceab767bd8230842fdf78f8a996e2a16445747143a778"}, + {file = "safetensors-0.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2536b11ce665834201072e9397404170f93f3be10cca9995b909f023a04501ee"}, + {file = "safetensors-0.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:998fbac99ca956c3a09fe07cc0b35fac26a521fa8865a690686d889f0ff4e4a6"}, + {file = "safetensors-0.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:845be0aafabf2a60c2d482d4e93023fecffe5e5443d801d7a7741bae9de41233"}, + {file = "safetensors-0.4.1-cp38-none-win32.whl", hash = "sha256:ce7a28bc8af685a69d7e869d09d3e180a275e3281e29cf5f1c7319e231932cc7"}, + {file = "safetensors-0.4.1-cp38-none-win_amd64.whl", hash = "sha256:e056fb9e22d118cc546107f97dc28b449d88274207dd28872bd668c86216e4f6"}, + {file = "safetensors-0.4.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:bdc0d039e44a727824639824090bd8869535f729878fa248addd3dc01db30eae"}, + {file = "safetensors-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c1b1d510c7aba71504ece87bf393ea82638df56303e371e5e2cf09d18977dd7"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd0afd95c1e497f520e680ea01e0397c0868a3a3030e128438cf6e9e3fcd671"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f603bdd8deac6726d39f41688ed353c532dd53935234405d79e9eb53f152fbfb"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8a85e3e47e0d4eebfaf9a58b40aa94f977a56050cb5598ad5396a9ee7c087c6"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0ccb5aa0f3be2727117e5631200fbb3a5b3a2b3757545a92647d6dd8be6658f"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d784938534e255473155e4d9f276ee69eb85455b6af1292172c731409bf9adee"}, + {file = "safetensors-0.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a257de175c254d39ccd6a21341cd62eb7373b05c1e618a78096a56a857e0c316"}, + {file = "safetensors-0.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6fd80f7794554091836d4d613d33a7d006e2b8d6ba014d06f97cebdfda744f64"}, + {file = "safetensors-0.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:35803201d980efcf964b75a0a2aee97fe5e9ecc5f3ad676b38fafdfe98e0620d"}, + {file = "safetensors-0.4.1-cp39-none-win32.whl", hash = "sha256:7ff8a36e0396776d3ed9a106fc9a9d7c55d4439ca9a056a24bf66d343041d3e6"}, + {file = "safetensors-0.4.1-cp39-none-win_amd64.whl", hash = "sha256:bfa2e20342b81921b98edba52f8deb68843fa9c95250739a56b52ceda5ea5c61"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ae2d5a31cfb8a973a318f7c4d2cffe0bd1fe753cdf7bb41a1939d45a0a06f964"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a45dbf03e8334d3a5dc93687d98b6dc422f5d04c7d519dac09b84a3c87dd7c6"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297b359d91126c0f9d4fd17bae3cfa2fe3a048a6971b8db07db746ad92f850c"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda3d98e2bcece388232cfc551ebf063b55bdb98f65ab54df397da30efc7dcc5"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8934bdfd202ebd0697040a3dff40dd77bc4c5bbf3527ede0532f5e7fb4d970f"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:42c3710cec7e5c764c7999697516370bee39067de0aa089b7e2cfb97ac8c6b20"}, + {file = "safetensors-0.4.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53134226053e56bd56e73f7db42596e7908ed79f3c9a1016e4c1dade593ac8e5"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:257d59e40a1b367cb544122e7451243d65b33c3f34d822a347f4eea6fdf97fdf"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d54c2f1826e790d1eb2d2512bfd0ee443f0206b423d6f27095057c7f18a0687"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645b3f1138fce6e818e79d4128afa28f0657430764cc045419c1d069ff93f732"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9a7ffb1e551c6df51d267f5a751f042b183df22690f6feceac8d27364fd51d7"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:44e230fbbe120de564b64f63ef3a8e6ff02840fa02849d9c443d56252a1646d4"}, + {file = "safetensors-0.4.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9d16b3b2fcc6fca012c74bd01b5619c655194d3e3c13e4d4d0e446eefa39a463"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5d95ea4d8b32233910734a904123bdd3979c137c461b905a5ed32511defc075f"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:dab431699b5d45e0ca043bc580651ce9583dda594e62e245b7497adb32e99809"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d8bbb7344e39cb9d4762e85c21df94ebeb03edac923dd94bb9ed8c10eac070"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1faf5111c66a6ba91f85dff2e36edaaf36e6966172703159daeef330de4ddc7b"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:660ca1d8bff6c7bc7c6b30b9b32df74ef3ab668f5df42cefd7588f0d40feadcb"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ae2f67f04ed0bb2e56fd380a8bd3eef03f609df53f88b6f5c7e89c08e52aae00"}, + {file = "safetensors-0.4.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c8ed5d2c04cdc1afc6b3c28d59580448ac07732c50d94c15e14670f9c473a2ce"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2b6a2814278b6660261aa9a9aae524616de9f1ec364e3716d219b6ed8f91801f"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3cfd1ca35eacc635f0eaa894e5c5ed83ffebd0f95cac298fd430014fa7323631"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4177b456c6b0c722d82429127b5beebdaf07149d265748e97e0a34ff0b3694c8"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313e8472197bde54e3ec54a62df184c414582979da8f3916981b6a7954910a1b"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fdb4adb76e21bad318210310590de61c9f4adcef77ee49b4a234f9dc48867869"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1d568628e9c43ca15eb96c217da73737c9ccb07520fafd8a1eba3f2750614105"}, + {file = "safetensors-0.4.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:573b6023a55a2f28085fc0a84e196c779b6cbef4d9e73acea14c8094fee7686f"}, + {file = "safetensors-0.4.1.tar.gz", hash = "sha256:2304658e6ada81a5223225b4efe84748e760c46079bffedf7e321763cafb36c9"}, +] + +[package.extras] +all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] +dev = ["safetensors[all]"] +jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] +numpy = ["numpy (>=1.21.6)"] +paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] +pinned-tf = ["safetensors[numpy]", "tensorflow (==2.11.0)"] +quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] +tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] +testing = ["h5py (>=3.7.0)", "huggingface_hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools_rust (>=1.5.2)"] +torch = ["safetensors[numpy]", "torch (>=1.10)"] + +[[package]] +name = "scikit-image" +version = "0.22.0" +description = "Image processing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d"}, + {file = "scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff"}, + {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6"}, + {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938"}, + {file = "scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc"}, + {file = "scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2"}, + {file = "scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2"}, + {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd"}, + {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e"}, + {file = "scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b"}, + {file = "scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff"}, + {file = "scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9"}, + {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452"}, + {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a"}, + {file = "scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2"}, + {file = "scikit_image-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22318b35044cfeeb63ee60c56fc62450e5fe516228138f1d06c7a26378248a86"}, + {file = "scikit_image-0.22.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9e801c44a814afdadeabf4dffdffc23733e393767958b82319706f5fa3e1eaa9"}, + {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c472a1fb3665ec5c00423684590631d95f9afcbc97f01407d348b821880b2cb3"}, + {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a"}, + {file = "scikit_image-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:5071b8f6341bfb0737ab05c8ab4ac0261f9e25dbcc7b5d31e5ed230fd24a7929"}, + {file = "scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236"}, +] + +[package.dependencies] +imageio = ">=2.27" +lazy_loader = ">=0.3" +networkx = ">=2.8" +numpy = ">=1.22" +packaging = ">=21" +pillow = ">=9.0.1" +scipy = ">=1.8" +tifffile = ">=2022.8.12" + +[package.extras] +build = ["Cython (>=0.29.32)", "build", "meson-python (>=0.14)", "ninja", "numpy (>=1.22)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.6)", "wheel"] +data = ["pooch (>=1.6.0)"] +developer = ["pre-commit", "tomli"] +docs = ["PyWavelets (>=1.1.1)", "dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.5)", "myst-parser", "numpydoc (>=1.6)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.14.1)", "pytest-runner", "scikit-learn (>=1.1)", "seaborn (>=0.11)", "sphinx (>=7.2)", "sphinx-copybutton", "sphinx-gallery (>=0.14)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] +optional = ["PyWavelets (>=1.1.1)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=1.1)"] +test = ["asv", "matplotlib (>=3.5)", "numpydoc (>=1.5)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-faulthandler", "pytest-localserver"] + +[[package]] +name = "scikit-learn" +version = "1.3.2" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.8" +files = [ + {file = "scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05"}, + {file = "scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1"}, + {file = "scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a"}, + {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c"}, + {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161"}, + {file = "scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c"}, + {file = "scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66"}, + {file = "scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157"}, + {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb"}, + {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"}, + {file = "scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b"}, + {file = "scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028"}, + {file = "scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5"}, + {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525"}, + {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c"}, + {file = "scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107"}, + {file = "scikit_learn-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93"}, + {file = "scikit_learn-1.3.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073"}, + {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d"}, + {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf"}, + {file = "scikit_learn-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0"}, + {file = "scikit_learn-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03"}, + {file = "scikit_learn-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e"}, + {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a"}, + {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9"}, + {file = "scikit_learn-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0"}, +] + +[package.dependencies] +joblib = ">=1.1.1" +numpy = ">=1.17.3,<2.0" +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]] +name = "scipy" +version = "1.11.4" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710"}, + {file = "scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41"}, + {file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4"}, + {file = "scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56"}, + {file = "scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446"}, + {file = "scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3"}, + {file = "scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be"}, + {file = "scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8"}, + {file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c"}, + {file = "scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff"}, + {file = "scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993"}, + {file = "scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd"}, + {file = "scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6"}, + {file = "scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d"}, + {file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4"}, + {file = "scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79"}, + {file = "scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660"}, + {file = "scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97"}, + {file = "scipy-1.11.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e619aba2df228a9b34718efb023966da781e89dd3d21637b27f2e54db0410d7"}, + {file = "scipy-1.11.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:f3cd9e7b3c2c1ec26364856f9fbe78695fe631150f94cd1c22228456404cf1ec"}, + {file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10e45a6c50211fe256da61a11c34927c68f277e03138777bdebedd933712fea"}, + {file = "scipy-1.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91af76a68eeae0064887a48e25c4e616fa519fa0d38602eda7e0f97d65d57937"}, + {file = "scipy-1.11.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6df1468153a31cf55ed5ed39647279beb9cfb5d3f84369453b49e4b8502394fd"}, + {file = "scipy-1.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee410e6de8f88fd5cf6eadd73c135020bfbbbdfcd0f6162c36a7638a1ea8cc65"}, + {file = "scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa"}, +] + +[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]] +name = "seaborn" +version = "0.12.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.7" +files = [ + {file = "seaborn-0.12.2-py3-none-any.whl", hash = "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"}, + {file = "seaborn-0.12.2.tar.gz", hash = "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139"}, +] + +[package.dependencies] +matplotlib = ">=3.1,<3.6.1 || >3.6.1" +numpy = ">=1.17,<1.24.0 || >1.24.0" +pandas = ">=0.25" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"] + +[[package]] +name = "segmentation-models-pytorch" +version = "0.3.3" +description = "Image segmentation models with pre-trained backbones. PyTorch." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "segmentation_models_pytorch-0.3.3-py3-none-any.whl", hash = "sha256:b4317d6f72cb1caf4b7e1d384096970e202600275f54deb8e774fc04d6c8b82e"}, + {file = "segmentation_models_pytorch-0.3.3.tar.gz", hash = "sha256:b3b21ab4cd26a6b2b9e7a6ed466ace6452eb26ed3c31ae491ea2d7cbb01e384b"}, +] + +[package.dependencies] +efficientnet-pytorch = "0.7.1" +pillow = "*" +pretrainedmodels = "0.7.4" +timm = "0.9.2" +torchvision = ">=0.5.0" +tqdm = "*" + +[package.extras] +test = ["black (==22.3.0)", "flake8 (==4.0.1)", "flake8-docstrings (==1.6.0)", "mock", "pre-commit", "pytest"] + +[[package]] +name = "send2trash" +version = "1.8.2" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, + {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "sentry-sdk" +version = "1.39.1" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-1.39.1.tar.gz", hash = "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5"}, + {file = "sentry_sdk-1.39.1-py2.py3-none-any.whl", hash = "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +grpcio = ["grpcio (>=1.21.1)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +loguru = ["loguru (>=0.5)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setproctitle" +version = "1.3.3" +description = "A Python module to customize the process title" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:897a73208da48db41e687225f355ce993167079eda1260ba5e13c4e53be7f754"}, + {file = "setproctitle-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c331e91a14ba4076f88c29c777ad6b58639530ed5b24b5564b5ed2fd7a95452"}, + {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbd6c7de0771c84b4aa30e70b409565eb1fc13627a723ca6be774ed6b9d9fa3"}, + {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c05ac48ef16ee013b8a326c63e4610e2430dbec037ec5c5b58fcced550382b74"}, + {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1342f4fdb37f89d3e3c1c0a59d6ddbedbde838fff5c51178a7982993d238fe4f"}, + {file = "setproctitle-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc74e84fdfa96821580fb5e9c0b0777c1c4779434ce16d3d62a9c4d8c710df39"}, + {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9617b676b95adb412bb69645d5b077d664b6882bb0d37bfdafbbb1b999568d85"}, + {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6a249415f5bb88b5e9e8c4db47f609e0bf0e20a75e8d744ea787f3092ba1f2d0"}, + {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:38da436a0aaace9add67b999eb6abe4b84397edf4a78ec28f264e5b4c9d53cd5"}, + {file = "setproctitle-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:da0d57edd4c95bf221b2ebbaa061e65b1788f1544977288bdf95831b6e44e44d"}, + {file = "setproctitle-1.3.3-cp310-cp310-win32.whl", hash = "sha256:a1fcac43918b836ace25f69b1dca8c9395253ad8152b625064415b1d2f9be4fb"}, + {file = "setproctitle-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:200620c3b15388d7f3f97e0ae26599c0c378fdf07ae9ac5a13616e933cbd2086"}, + {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:334f7ed39895d692f753a443102dd5fed180c571eb6a48b2a5b7f5b3564908c8"}, + {file = "setproctitle-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:950f6476d56ff7817a8fed4ab207727fc5260af83481b2a4b125f32844df513a"}, + {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:195c961f54a09eb2acabbfc90c413955cf16c6e2f8caa2adbf2237d1019c7dd8"}, + {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f05e66746bf9fe6a3397ec246fe481096664a9c97eb3fea6004735a4daf867fd"}, + {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5901a31012a40ec913265b64e48c2a4059278d9f4e6be628441482dd13fb8b5"}, + {file = "setproctitle-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64286f8a995f2cd934082b398fc63fca7d5ffe31f0e27e75b3ca6b4efda4e353"}, + {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:184239903bbc6b813b1a8fc86394dc6ca7d20e2ebe6f69f716bec301e4b0199d"}, + {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:664698ae0013f986118064b6676d7dcd28fefd0d7d5a5ae9497cbc10cba48fa5"}, + {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e5119a211c2e98ff18b9908ba62a3bd0e3fabb02a29277a7232a6fb4b2560aa0"}, + {file = "setproctitle-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:417de6b2e214e837827067048f61841f5d7fc27926f2e43954567094051aff18"}, + {file = "setproctitle-1.3.3-cp311-cp311-win32.whl", hash = "sha256:6a143b31d758296dc2f440175f6c8e0b5301ced3b0f477b84ca43cdcf7f2f476"}, + {file = "setproctitle-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a680d62c399fa4b44899094027ec9a1bdaf6f31c650e44183b50d4c4d0ccc085"}, + {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d4460795a8a7a391e3567b902ec5bdf6c60a47d791c3b1d27080fc203d11c9dc"}, + {file = "setproctitle-1.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bdfd7254745bb737ca1384dee57e6523651892f0ea2a7344490e9caefcc35e64"}, + {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477d3da48e216d7fc04bddab67b0dcde633e19f484a146fd2a34bb0e9dbb4a1e"}, + {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab2900d111e93aff5df9fddc64cf51ca4ef2c9f98702ce26524f1acc5a786ae7"}, + {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088b9efc62d5aa5d6edf6cba1cf0c81f4488b5ce1c0342a8b67ae39d64001120"}, + {file = "setproctitle-1.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6d50252377db62d6a0bb82cc898089916457f2db2041e1d03ce7fadd4a07381"}, + {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:87e668f9561fd3a457ba189edfc9e37709261287b52293c115ae3487a24b92f6"}, + {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:287490eb90e7a0ddd22e74c89a92cc922389daa95babc833c08cf80c84c4df0a"}, + {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe1c49486109f72d502f8be569972e27f385fe632bd8895f4730df3c87d5ac8"}, + {file = "setproctitle-1.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4a6ba2494a6449b1f477bd3e67935c2b7b0274f2f6dcd0f7c6aceae10c6c6ba3"}, + {file = "setproctitle-1.3.3-cp312-cp312-win32.whl", hash = "sha256:2df2b67e4b1d7498632e18c56722851ba4db5d6a0c91aaf0fd395111e51cdcf4"}, + {file = "setproctitle-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f38d48abc121263f3b62943f84cbaede05749047e428409c2c199664feb6abc7"}, + {file = "setproctitle-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:816330675e3504ae4d9a2185c46b573105d2310c20b19ea2b4596a9460a4f674"}, + {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f960bc22d8d8e4ac886d1e2e21ccbd283adcf3c43136161c1ba0fa509088e0"}, + {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e6e7adff74796ef12753ff399491b8827f84f6c77659d71bd0b35870a17d8f"}, + {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53bc0d2358507596c22b02db079618451f3bd720755d88e3cccd840bafb4c41c"}, + {file = "setproctitle-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6d20f9541f5f6ac63df553b6d7a04f313947f550eab6a61aa758b45f0d5657"}, + {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c1c84beab776b0becaa368254801e57692ed749d935469ac10e2b9b825dbdd8e"}, + {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:507e8dc2891021350eaea40a44ddd887c9f006e6b599af8d64a505c0f718f170"}, + {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b1067647ac7aba0b44b591936118a22847bda3c507b0a42d74272256a7a798e9"}, + {file = "setproctitle-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e71f6365744bf53714e8bd2522b3c9c1d83f52ffa6324bd7cbb4da707312cd8"}, + {file = "setproctitle-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:7f1d36a1e15a46e8ede4e953abb104fdbc0845a266ec0e99cc0492a4364f8c44"}, + {file = "setproctitle-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9a402881ec269d0cc9c354b149fc29f9ec1a1939a777f1c858cdb09c7a261df"}, + {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ff814dea1e5c492a4980e3e7d094286077054e7ea116cbeda138819db194b2cd"}, + {file = "setproctitle-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:accb66d7b3ccb00d5cd11d8c6e07055a4568a24c95cf86109894dcc0c134cc89"}, + {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554eae5a5b28f02705b83a230e9d163d645c9a08914c0ad921df363a07cf39b1"}, + {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a911b26264dbe9e8066c7531c0591cfab27b464459c74385b276fe487ca91c12"}, + {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2982efe7640c4835f7355fdb4da313ad37fb3b40f5c69069912f8048f77b28c8"}, + {file = "setproctitle-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df3f4274b80709d8bcab2f9a862973d453b308b97a0b423a501bcd93582852e3"}, + {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:af2c67ae4c795d1674a8d3ac1988676fa306bcfa1e23fddb5e0bd5f5635309ca"}, + {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af4061f67fd7ec01624c5e3c21f6b7af2ef0e6bab7fbb43f209e6506c9ce0092"}, + {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37a62cbe16d4c6294e84670b59cf7adcc73faafe6af07f8cb9adaf1f0e775b19"}, + {file = "setproctitle-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a83ca086fbb017f0d87f240a8f9bbcf0809f3b754ee01cec928fff926542c450"}, + {file = "setproctitle-1.3.3-cp38-cp38-win32.whl", hash = "sha256:059f4ce86f8cc92e5860abfc43a1dceb21137b26a02373618d88f6b4b86ba9b2"}, + {file = "setproctitle-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ab92e51cd4a218208efee4c6d37db7368fdf182f6e7ff148fb295ecddf264287"}, + {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c7951820b77abe03d88b114b998867c0f99da03859e5ab2623d94690848d3e45"}, + {file = "setproctitle-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc94cf128676e8fac6503b37763adb378e2b6be1249d207630f83fc325d9b11"}, + {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5d9027eeda64d353cf21a3ceb74bb1760bd534526c9214e19f052424b37e42"}, + {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e4a8104db15d3462e29d9946f26bed817a5b1d7a47eabca2d9dc2b995991503"}, + {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c32c41ace41f344d317399efff4cffb133e709cec2ef09c99e7a13e9f3b9483c"}, + {file = "setproctitle-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf16381c7bf7f963b58fb4daaa65684e10966ee14d26f5cc90f07049bfd8c1e"}, + {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e18b7bd0898398cc97ce2dfc83bb192a13a087ef6b2d5a8a36460311cb09e775"}, + {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69d565d20efe527bd8a9b92e7f299ae5e73b6c0470f3719bd66f3cd821e0d5bd"}, + {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ddedd300cd690a3b06e7eac90ed4452348b1348635777ce23d460d913b5b63c3"}, + {file = "setproctitle-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:415bfcfd01d1fbf5cbd75004599ef167a533395955305f42220a585f64036081"}, + {file = "setproctitle-1.3.3-cp39-cp39-win32.whl", hash = "sha256:21112fcd2195d48f25760f0eafa7a76510871bbb3b750219310cf88b04456ae3"}, + {file = "setproctitle-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:5a740f05d0968a5a17da3d676ce6afefebeeeb5ce137510901bf6306ba8ee002"}, + {file = "setproctitle-1.3.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6b9e62ddb3db4b5205c0321dd69a406d8af9ee1693529d144e86bd43bcb4b6c0"}, + {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e3b99b338598de0bd6b2643bf8c343cf5ff70db3627af3ca427a5e1a1a90dd9"}, + {file = "setproctitle-1.3.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ae9a02766dad331deb06855fb7a6ca15daea333b3967e214de12cfae8f0ef5"}, + {file = "setproctitle-1.3.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:200ede6fd11233085ba9b764eb055a2a191fb4ffb950c68675ac53c874c22e20"}, + {file = "setproctitle-1.3.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0d3a953c50776751e80fe755a380a64cb14d61e8762bd43041ab3f8cc436092f"}, + {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e08e232b78ba3ac6bc0d23ce9e2bee8fad2be391b7e2da834fc9a45129eb87"}, + {file = "setproctitle-1.3.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1da82c3e11284da4fcbf54957dafbf0655d2389cd3d54e4eaba636faf6d117a"}, + {file = "setproctitle-1.3.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:aeaa71fb9568ebe9b911ddb490c644fbd2006e8c940f21cb9a1e9425bd709574"}, + {file = "setproctitle-1.3.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:59335d000c6250c35989394661eb6287187854e94ac79ea22315469ee4f4c244"}, + {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3ba57029c9c50ecaf0c92bb127224cc2ea9fda057b5d99d3f348c9ec2855ad3"}, + {file = "setproctitle-1.3.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d876d355c53d975c2ef9c4f2487c8f83dad6aeaaee1b6571453cb0ee992f55f6"}, + {file = "setproctitle-1.3.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:224602f0939e6fb9d5dd881be1229d485f3257b540f8a900d4271a2c2aa4e5f4"}, + {file = "setproctitle-1.3.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d7f27e0268af2d7503386e0e6be87fb9b6657afd96f5726b733837121146750d"}, + {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5e7266498cd31a4572378c61920af9f6b4676a73c299fce8ba93afd694f8ae7"}, + {file = "setproctitle-1.3.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33c5609ad51cd99d388e55651b19148ea99727516132fb44680e1f28dd0d1de9"}, + {file = "setproctitle-1.3.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:eae8988e78192fd1a3245a6f4f382390b61bce6cfcc93f3809726e4c885fa68d"}, + {file = "setproctitle-1.3.3.tar.gz", hash = "sha256:c913e151e7ea01567837ff037a23ca8740192880198b7fbb90b16d181607caae"}, +] + +[package.extras] +test = ["pytest"] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "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] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +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]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "sympy" +version = "1.12" +description = "Computer algebra system (CAS) in Python" +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]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "tensorboardx" +version = "2.6.2.2" +description = "TensorBoardX lets you watch Tensors Flow without Tensorflow" +optional = false +python-versions = "*" +files = [ + {file = "tensorboardX-2.6.2.2-py2.py3-none-any.whl", hash = "sha256:160025acbf759ede23fd3526ae9d9bfbfd8b68eb16c38a010ebe326dc6395db8"}, + {file = "tensorboardX-2.6.2.2.tar.gz", hash = "sha256:c6476d7cd0d529b0b72f4acadb1269f9ed8b22f441e87a84f2a3b940bb87b666"}, +] + +[package.dependencies] +numpy = "*" +packaging = "*" +protobuf = ">=3.20" + +[[package]] +name = "terminado" +version = "0.18.0" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "terminado-0.18.0-py3-none-any.whl", hash = "sha256:87b0d96642d0fe5f5abd7783857b9cab167f221a39ff98e3b9619a788a3c0f2e"}, + {file = "terminado-0.18.0.tar.gz", hash = "sha256:1ea08a89b835dd1b8c0c900d92848147cef2537243361b2e3f4dc15df9b6fded"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "threadpoolctl" +version = "3.2.0" +description = "threadpoolctl" +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]] +name = "tifffile" +version = "2023.12.9" +description = "Read and write TIFF files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tifffile-2023.12.9-py3-none-any.whl", hash = "sha256:9b066e4b1a900891ea42ffd33dab8ba34c537935618b9893ddef42d7d422692f"}, + {file = "tifffile-2023.12.9.tar.gz", hash = "sha256:9dd1da91180a6453018a241ff219e1905f169384355cd89c9ef4034c1b46cdb8"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib", "zarr"] + +[[package]] +name = "timm" +version = "0.9.2" +description = "PyTorch Image Models" +optional = false +python-versions = ">=3.7" +files = [ + {file = "timm-0.9.2-py3-none-any.whl", hash = "sha256:8da40cc58ed32b0622bf87d8714f9b7023398ba4cfa8fa678578d2aefde4a909"}, + {file = "timm-0.9.2.tar.gz", hash = "sha256:d0977cc5e02c69bda979fca8b52aa315a5f2cb64ebf8ad2c4631b1e452762c14"}, +] + +[package.dependencies] +huggingface-hub = "*" +pyyaml = "*" +safetensors = "*" +torch = ">=1.7" +torchvision = "*" + +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + +[[package]] +name = "tokenize-rt" +version = "5.2.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, + {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "torch" +version = "2.0.1" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.0.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ced00b3ba471856b993822508f77c98f48a458623596a4c43136158781e306a"}, + {file = "torch-2.0.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:359bfaad94d1cda02ab775dc1cc386d585712329bb47b8741607ef6ef4950747"}, + {file = "torch-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7c84e44d9002182edd859f3400deaa7410f5ec948a519cc7ef512c2f9b34d2c4"}, + {file = "torch-2.0.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:567f84d657edc5582d716900543e6e62353dbe275e61cdc36eda4929e46df9e7"}, + {file = "torch-2.0.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:787b5a78aa7917465e9b96399b883920c88a08f4eb63b5a5d2d1a16e27d2f89b"}, + {file = "torch-2.0.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e617b1d0abaf6ced02dbb9486803abfef0d581609b09641b34fa315c9c40766d"}, + {file = "torch-2.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b6019b1de4978e96daa21d6a3ebb41e88a0b474898fe251fd96189587408873e"}, + {file = "torch-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbd68cbd1cd9da32fe5d294dd3411509b3d841baecb780b38b3b7b06c7754434"}, + {file = "torch-2.0.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:ef654427d91600129864644e35deea761fb1fe131710180b952a6f2e2207075e"}, + {file = "torch-2.0.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:25aa43ca80dcdf32f13da04c503ec7afdf8e77e3a0183dd85cd3e53b2842e527"}, + {file = "torch-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5ef3ea3d25441d3957348f7e99c7824d33798258a2bf5f0f0277cbcadad2e20d"}, + {file = "torch-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0882243755ff28895e8e6dc6bc26ebcf5aa0911ed81b2a12f241fc4b09075b13"}, + {file = "torch-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:f66aa6b9580a22b04d0af54fcd042f52406a8479e2b6a550e3d9f95963e168c8"}, + {file = "torch-2.0.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:1adb60d369f2650cac8e9a95b1d5758e25d526a34808f7448d0bd599e4ae9072"}, + {file = "torch-2.0.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:1bcffc16b89e296826b33b98db5166f990e3b72654a2b90673e817b16c50e32b"}, + {file = "torch-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e10e1597f2175365285db1b24019eb6f04d53dcd626c735fc502f1e8b6be9875"}, + {file = "torch-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:423e0ae257b756bb45a4b49072046772d1ad0c592265c5080070e0767da4e490"}, + {file = "torch-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8742bdc62946c93f75ff92da00e3803216c6cce9b132fbca69664ca38cfb3e18"}, + {file = "torch-2.0.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:c62df99352bd6ee5a5a8d1832452110435d178b5164de450831a3a8cc14dc680"}, + {file = "torch-2.0.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:671a2565e3f63b8fe8e42ae3e36ad249fe5e567435ea27b94edaa672a7d0c416"}, +] + +[package.dependencies] +filelock = "*" +jinja2 = "*" +networkx = "*" +sympy = "*" +typing-extensions = "*" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] + +[[package]] +name = "torch" +version = "2.0.1+cpu" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +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 = "torch_cpu" + +[[package]] +name = "torch" +version = "2.0.1+cu118" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.0.1+cu118-cp310-cp310-linux_x86_64.whl", hash = "sha256:a7a49d459bf4862f64f7bc1a68beccf8881c2fa9f3e0569608e16ba6f85ebf7b"}, + {file = "torch-2.0.1+cu118-cp310-cp310-win_amd64.whl", hash = "sha256:f58d75619bc96e4322343c030b893613701caa2d6db8017155da226c14171335"}, + {file = "torch-2.0.1+cu118-cp311-cp311-linux_x86_64.whl", hash = "sha256:143b6c658c17d43376e2dfbaa2c106d35639d615e5e8dec4429cf1e510dd8d61"}, + {file = "torch-2.0.1+cu118-cp311-cp311-win_amd64.whl", hash = "sha256:b663a4ee744d574095dbd612644de345944247c0605692309fd9f6c7ccdea022"}, + {file = "torch-2.0.1+cu118-cp38-cp38-linux_x86_64.whl", hash = "sha256:2ce38a6e4ea7c4b7f5baa51e65243a5f687f6e19ab7915ba5b2a431105f50bbe"}, + {file = "torch-2.0.1+cu118-cp38-cp38-win_amd64.whl", hash = "sha256:e58d26a11bd57ac19761c018c3151c15bc71d068afc8ec409bfd9b4cfcc63a52"}, + {file = "torch-2.0.1+cu118-cp39-cp39-linux_x86_64.whl", hash = "sha256:eb55f29db5744eda8a96f5594e637daed0d52278273005de759970e67cfa6a5a"}, + {file = "torch-2.0.1+cu118-cp39-cp39-win_amd64.whl", hash = "sha256:fa225b6f941ee0e78978ac85ed7744d3c19fff462473821f8060c14faa60043e"}, +] + +[package.dependencies] +filelock = "*" +jinja2 = "*" +networkx = "*" +sympy = "*" +triton = {version = "2.0.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +typing-extensions = "*" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] + +[package.source] +type = "legacy" +url = "https://download.pytorch.org/whl/cu118" +reference = "torch_gpu" + +[[package]] +name = "torchinfo" +version = "1.8.0" +description = "Model summary in PyTorch, based off of the original torchsummary." +optional = false +python-versions = ">=3.7" +files = [ + {file = "torchinfo-1.8.0-py3-none-any.whl", hash = "sha256:2e911c2918603f945c26ff21a3a838d12709223dc4ccf243407bce8b6e897b46"}, + {file = "torchinfo-1.8.0.tar.gz", hash = "sha256:72e94b0e9a3e64dc583a8e5b7940b8938a1ac0f033f795457f27e6f4e7afa2e9"}, +] + +[[package]] +name = "torchmetrics" +version = "1.2.1" +description = "PyTorch native Metrics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "torchmetrics-1.2.1-py3-none-any.whl", hash = "sha256:fe03a8c53d0ae5800d34ea615f56295fda281282cd83f647d2184e81c1d4efee"}, + {file = "torchmetrics-1.2.1.tar.gz", hash = "sha256:217387738f84939c39b534b20d4983e737cc448d27aaa5340e0327948d97ca3e"}, +] + +[package.dependencies] +lightning-utilities = ">=0.8.0" +numpy = ">1.20.0" +packaging = ">17.1" +torch = ">=1.8.1" + +[package.extras] +-tests = ["bert-score (==0.3.13)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.20)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "mir-eval (>=0.6)", "netcal (>1.0.0)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "pytorch-msssim (==1.0.0)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch-complex (<=0.4.3)"] +all = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.2.0)", "mypy (==1.7.1)", "nltk (>=3.6)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "torch (==2.1.1)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +audio = ["pystoi (>=0.3.0)", "torchaudio (>=0.10.0)"] +detection = ["pycocotools (>2.0.0)", "torchvision (>=0.8)"] +dev = ["SciencePlots (>=2.0.0)", "bert-score (==0.3.13)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.20)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "matplotlib (>=3.2.0)", "mir-eval (>=0.6)", "mypy (==1.7.1)", "netcal (>1.0.0)", "nltk (>=3.6)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "pytorch-msssim (==1.0.0)", "regex (>=2021.9.24)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch (==2.1.1)", "torch-complex (<=0.4.3)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +image = ["scipy (>1.0.0)", "torch-fidelity (<=0.4.0)", "torchvision (>=0.8)"] +multimodal = ["piq (<=0.8.0)", "transformers (>=4.10.0)"] +text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)", "transformers (>4.4.0)"] +typing = ["mypy (==1.7.1)", "torch (==2.1.1)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +visual = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.2.0)"] + +[[package]] +name = "torchvision" +version = "0.15.2" +description = "image and video datasets and models for torch deep learning" +optional = false +python-versions = ">=3.8" +files = [ + {file = "torchvision-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7754088774e810c5672b142a45dcf20b1bd986a5a7da90f8660c43dc43fb850c"}, + {file = "torchvision-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37eb138e13f6212537a3009ac218695483a635c404b6cc1d8e0d0d978026a86d"}, + {file = "torchvision-0.15.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:54143f7cc0797d199b98a53b7d21c3f97615762d4dd17ad45a41c7e80d880e73"}, + {file = "torchvision-0.15.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:1eefebf5fbd01a95fe8f003d623d941601c94b5cec547b420da89cb369d9cf96"}, + {file = "torchvision-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:96fae30c5ca8423f4b9790df0f0d929748e32718d88709b7b567d2f630c042e3"}, + {file = "torchvision-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f35f6bd5bcc4568e6522e4137fa60fcc72f4fa3e615321c26cd87e855acd398"}, + {file = "torchvision-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:757505a0ab2be7096cb9d2bf4723202c971cceddb72c7952a7e877f773de0f8a"}, + {file = "torchvision-0.15.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:012ad25cfd9019ff9b0714a168727e3845029be1af82296ff1e1482931fa4b80"}, + {file = "torchvision-0.15.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b02a7ffeaa61448737f39a4210b8ee60234bda0515a0c0d8562f884454105b0f"}, + {file = "torchvision-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:10be76ceded48329d0a0355ac33da131ee3993ff6c125e4a02ab34b5baa2472c"}, + {file = "torchvision-0.15.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f12415b686dba884fb086f53ac803f692be5a5cdd8a758f50812b30fffea2e4"}, + {file = "torchvision-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31211c01f8b8ec33b8a638327b5463212e79a03e43c895f88049f97af1bd12fd"}, + {file = "torchvision-0.15.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c55f9889e436f14b4f84a9c00ebad0d31f5b4626f10cf8018e6c676f92a6d199"}, + {file = "torchvision-0.15.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:9a192f2aa979438f23c20e883980b23d13268ab9f819498774a6d2eb021802c2"}, + {file = "torchvision-0.15.2-cp38-cp38-win_amd64.whl", hash = "sha256:c07071bc8d02aa8fcdfe139ab6a1ef57d3b64c9e30e84d12d45c9f4d89fb6536"}, + {file = "torchvision-0.15.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4790260fcf478a41c7ecc60a6d5200a88159fdd8d756e9f29f0f8c59c4a67a68"}, + {file = "torchvision-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:987ab62225b4151a11e53fd06150c5258ced24ac9d7c547e0e4ab6fbca92a5ce"}, + {file = "torchvision-0.15.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:63df26673e66cba3f17e07c327a8cafa3cce98265dbc3da329f1951d45966838"}, + {file = "torchvision-0.15.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b85f98d4cc2f72452f6792ab4463a3541bc5678a8cdd3da0e139ba2fe8b56d42"}, + {file = "torchvision-0.15.2-cp39-cp39-win_amd64.whl", hash = "sha256:07c462524cc1bba5190c16a9d47eac1fca024d60595a310f23c00b4ffff18b30"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" +requests = "*" +torch = "2.0.1" + +[package.extras] +scipy = ["scipy"] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" +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]] +name = "traitlets" +version = "5.14.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"}, + {file = "traitlets-5.14.0.tar.gz", hash = "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "trimesh" +version = "3.23.5" +description = "Import, export, process, analyze and view triangular meshes." +optional = false +python-versions = "*" +files = [ + {file = "trimesh-3.23.5-py3-none-any.whl", hash = "sha256:9cfc592c7ad6475ebfe51c90b0a1e686d627735cea6e6e18e40745be3ecfaab9"}, + {file = "trimesh-3.23.5.tar.gz", hash = "sha256:bdfd669eccc4b3faff2328200a49408cd5ecad9f19b6022c4adb554bbb3a2621"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +all = ["chardet", "colorlog", "embreex", "jsonschema", "lxml", "mapbox-earcut", "networkx", "pillow", "psutil", "pycollada", "pyglet (<2)", "python-fcl", "requests", "rtree", "scikit-image", "scipy", "setuptools", "shapely", "svg.path", "xatlas", "xxhash"] +easy = ["chardet", "colorlog", "embreex", "jsonschema", "lxml", "mapbox-earcut", "networkx", "pillow", "pycollada", "requests", "rtree", "scipy", "setuptools", "shapely", "svg.path", "xxhash"] +recommends = ["glooey", "meshio", "sympy"] +test = ["autopep8 (<2)", "coveralls", "ezdxf", "pyinstrument", "pymeshlab", "pytest", "pytest-cov", "ruff"] + +[[package]] +name = "triton" +version = "2.0.0" +description = "A language and compiler for custom Deep Learning operations" +optional = false +python-versions = "*" +files = [ + {file = "triton-2.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505"}, + {file = "triton-2.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1"}, + {file = "triton-2.0.0-1-cp36-cp36m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4c9fc8c89874bc48eb7e7b2107a9b8d2c0bf139778637be5bfccb09191685cfd"}, + {file = "triton-2.0.0-1-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2684b6a60b9f174f447f36f933e9a45f31db96cb723723ecd2dcfd1c57b778b"}, + {file = "triton-2.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9d4978298b74fcf59a75fe71e535c092b023088933b2f1df933ec32615e4beef"}, + {file = "triton-2.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74f118c12b437fb2ca25e1a04759173b517582fcf4c7be11913316c764213656"}, + {file = "triton-2.0.0-1-pp37-pypy37_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9618815a8da1d9157514f08f855d9e9ff92e329cd81c0305003eb9ec25cc5add"}, + {file = "triton-2.0.0-1-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aca3303629cd3136375b82cb9921727f804e47ebee27b2677fef23005c3851a"}, + {file = "triton-2.0.0-1-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3e13aa8b527c9b642e3a9defcc0fbd8ffbe1c80d8ac8c15a01692478dc64d8a"}, + {file = "triton-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f05a7e64e4ca0565535e3d5d3405d7e49f9d308505bb7773d21fb26a4c008c2"}, + {file = "triton-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd"}, + {file = "triton-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b4d70dc92fb40af553b4460492c31dc7d3a114a979ffb7a5cdedb7eb546c08"}, + {file = "triton-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fedce6a381901b1547e0e7e1f2546e4f65dca6d91e2d8a7305a2d1f5551895be"}, + {file = "triton-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75834f27926eab6c7f00ce73aaf1ab5bfb9bec6eb57ab7c0bfc0a23fac803b4c"}, + {file = "triton-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0117722f8c2b579cd429e0bee80f7731ae05f63fe8e9414acd9a679885fcbf42"}, + {file = "triton-2.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcd9be5d0c2e45d2b7e6ddc6da20112b6862d69741576f9c3dbaf941d745ecae"}, + {file = "triton-2.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a0d2c3fc2eab4ba71384f2e785fbfd47aa41ae05fa58bf12cb31dcbd0aeceb"}, + {file = "triton-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c47b72c72693198163ece9d90a721299e4fb3b8e24fd13141e384ad952724f"}, +] + +[package.dependencies] +cmake = "*" +filelock = "*" +lit = "*" +torch = "*" + +[package.extras] +tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)"] +tutorials = ["matplotlib", "pandas", "tabulate"] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.14" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = "*" +files = [ + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, +] + +[[package]] +name = "typeshed-client" +version = "2.4.0" +description = "A library for accessing stubs in typeshed." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typeshed_client-2.4.0-py3-none-any.whl", hash = "sha256:5358cab27cf2d7b1cd1e77dd92a3ac3cd9cd31df9eb2e958bd280a38160a3219"}, + {file = "typeshed_client-2.4.0.tar.gz", hash = "sha256:b4e4e3e40dca91ce1a667d2eb0eb350a0a2c0d80e18a232d18857aa61bed3492"}, +] + +[package.dependencies] +importlib-resources = ">=1.4.0" + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wand" +version = "0.6.13" +description = "Ctypes-based simple MagickWand API binding for Python" +optional = false +python-versions = "*" +files = [ + {file = "Wand-0.6.13-py2.py3-none-any.whl", hash = "sha256:e5dda0ac2204a40c29ef5c4cb310770c95d3d05c37b1379e69c94ea79d7d19c0"}, + {file = "Wand-0.6.13.tar.gz", hash = "sha256:f5013484eaf7a20eb22d1821aaefe60b50cc329722372b5f8565d46d4aaafcca"}, +] + +[package.extras] +doc = ["Sphinx (>=5.3.0)"] +test = ["pytest (>=7.2.0)"] + +[[package]] +name = "wandb" +version = "0.15.12" +description = "A CLI and library for interacting with the Weights & Biases API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wandb-0.15.12-py3-none-any.whl", hash = "sha256:75c57b5bb8ddae21d45a02f644628585bdd112fea686de3177099a0996f1c41c"}, + {file = "wandb-0.15.12.tar.gz", hash = "sha256:c344d92fb8044b072a6138afd9adc5d3801ad050cf11378fe2af2fe899dcca84"}, +] + +[package.dependencies] +appdirs = ">=1.4.3" +Click = ">=7.1,<8.0.0 || >8.0.0" +docker-pycreds = ">=0.4.0" +GitPython = ">=1.0.0,<3.1.29 || >3.1.29" +pathtools = "*" +protobuf = {version = ">=3.19.0,<4.21.0 || >4.21.0,<5", markers = "python_version > \"3.9\" or sys_platform != \"linux\""} +psutil = ">=5.0.0" +PyYAML = "*" +requests = ">=2.0.0,<3" +sentry-sdk = ">=1.0.0" +setproctitle = "*" +setuptools = "*" + +[package.extras] +async = ["httpx (>=0.22.0)"] +aws = ["boto3"] +azure = ["azure-identity", "azure-storage-blob"] +gcp = ["google-cloud-storage"] +kubeflow = ["google-cloud-storage", "kubernetes", "minio", "sh"] +launch = ["PyYAML (>=6.0.0)", "awscli", "azure-containerregistry", "azure-identity", "azure-storage-blob", "boto3", "botocore", "chardet", "google-auth", "google-cloud-artifact-registry", "google-cloud-compute", "google-cloud-storage", "iso8601", "kubernetes", "nbconvert", "nbformat", "optuna", "typing-extensions"] +media = ["bokeh", "moviepy", "numpy", "pillow", "plotly", "rdkit-pypi", "soundfile"] +models = ["cloudpickle"] +nexus = ["wandb-core (>=0.16.0b1)"] +perf = ["orjson"] +sweeps = ["sweeps (>=0.2.0)"] + +[[package]] +name = "warmup-scheduler" +version = "0.3" +description = "Gradually Warm-up LR Scheduler for Pytorch" +optional = false +python-versions = "*" +files = [ + {file = "warmup_scheduler-0.3.tar.gz", hash = "sha256:63624cf5010772252c530ceb1a64e3fd60e3ee54264054da389f1b421089b714"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.12" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, +] + +[[package]] +name = "webcolors" +version = "1.13" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, + {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.7.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "widgetsnbextension" +version = "4.0.9" +description = "Jupyter interactive widgets for Jupyter Notebook" +optional = false +python-versions = ">=3.7" +files = [ + {file = "widgetsnbextension-4.0.9-py3-none-any.whl", hash = "sha256:91452ca8445beb805792f206e560c1769284267a30ceb1cec9f5bcc887d15175"}, + {file = "widgetsnbextension-4.0.9.tar.gz", hash = "sha256:3c1f5e46dc1166dfd40a42d685e6a51396fd34ff878742a3e47c6f0cc4a2a385"}, +] + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[[package]] +name = "zarr" +version = "2.16.1" +description = "An implementation of chunked, compressed, N-dimensional arrays for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zarr-2.16.1-py3-none-any.whl", hash = "sha256:de4882433ccb5b42cc1ec9872b95e64ca3a13581424666b28ed265ad76c7056f"}, + {file = "zarr-2.16.1.tar.gz", hash = "sha256:4276cf4b4a653431042cd53ff2282bc4d292a6842411e88529964504fb073286"}, +] + +[package.dependencies] +asciitree = "*" +fasteners = "*" +numcodecs = ">=0.10.0" +numpy = ">=1.20,<1.21.0 || >1.21.0" + +[package.extras] +docs = ["numcodecs[msgpack]", "numpydoc", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx-issues", "sphinx-rtd-theme"] +jupyter = ["ipytree (>=0.2.2)", "ipywidgets (>=8.0.0)", "notebook"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "ac685f5636d094ec0bff328b3f92e0a29271a94e1e75f911210429b81a97478c" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..87bae8d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,175 @@ +[tool.poetry] +name = "vesuvius-challenge-rnd" +version = "0.1.0" +description = "Vesuvius challenge grand prize submission" +authors = ["Louis Schlessinger <2996982+lschlessinger1@users.noreply.github.com>"] +readme = "README.md" +packages = [{include = "vesuvius_challenge_rnd"}] + +[tool.poetry.dependencies] +python = "^3.10" +tifffile = "^2023.4.12" +numpy = "^1.25.0" +pillow = "^9.5.0" +matplotlib = "^3.7.1" +patchify = "^0.2.3" +rich = "^13.4.2" +pint = "^0.22" +wand = "^0.6.11" +warmup-scheduler = "^0.3" + + +[tool.poetry.group.test.dependencies] +pytest = "^7.3.2" +black = "^23.3.0" +isort = {extras = ["colors"], version = "^5.12.0"} +mypy = "^1.3.0" +pytest-cov = "^4.1.0" +pre-commit = "^3.3.3" +pyupgrade = "^3.7.0" + + +[tool.poetry.group.fragment-ink-det.dependencies] +torchvision = "^0.15.2" +pytorch-lightning = {extras = ["extra"], version = "^2.0.4"} +albumentations = "^1.3.1" +wandb = "^0.15.4" +python-dotenv = "^1.0.0" +jupyter = "^1.0.0" +seaborn = "^0.12.2" +segmentation-models-pytorch = "^0.3.3" + + +[tool.poetry.group.torch_cpu.dependencies] +torch = [ + {version = "^2.0.1", platform = "win32"}, + {version = "^2.0.1", platform = "linux", source = "torch_cpu"}, + {version = "^2.0.1", platform = "darwin"}, +] + +[tool.poetry.group.torch_gpu] +optional = true + +[tool.poetry.group.torch_gpu.dependencies] +torch = [ + {version = "^2.0.1", platform = "win32", source = "torch_gpu"}, + {version = "^2.0.1", platform = "linux"}, + {version = "^2.0.1", platform = "darwin"}, +] + +[tool.poetry.group.analysis] +optional = true + +[tool.poetry.group.analysis.dependencies] +torchinfo = "^1.8.0" +trimesh = "^3.23.5" +plotly = "^5.16.1" +zarr = "^2.16.1" + + +[tool.poetry.group.data-download.dependencies] +gdown = {version = "^4.7.1", optional = true} + + +[tool.poetry.group.scroll-ink-det.dependencies] +monai = "^1.3.0" +einops = "^0.7.0" + +[[tool.poetry.source]] +name = "torch_cpu" +url = "https://download.pytorch.org/whl/cpu" +priority = "explicit" + + +[[tool.poetry.source]] +name = "torch_gpu" +url = "https://download.pytorch.org/whl/cu118" +priority = "explicit" + + +[[tool.poetry.source]] +name = "PyPI" +priority = "primary" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +# https://github.com/psf/black +target-version = ["py310"] +line-length = 100 +color = true + +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | env + | venv +)/ +''' + +[tool.isort] +# https://github.com/timothycrosley/isort/ +py_version = 310 +line_length = 100 + +known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] +sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +include_trailing_comma = true +profile = "black" +multi_line_output = 3 +indent = 4 +color_output = true + +[tool.mypy] +# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file +python_version = "3.10" +pretty = true +show_traceback = true +color_output = true + +allow_redefinition = false +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +ignore_missing_imports = true +implicit_reexport = false +no_implicit_optional = true +show_column_numbers = true +show_error_codes = true +show_error_context = true +strict_equality = true +strict_optional = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.pytest.ini_options] +# https://docs.pytest.org/en/6.2.x/customize.html#pyproject-toml +# Directories that are not visited by pytest collector: +norecursedirs =["hooks", "*.egg", ".eggs", "dist", "build", "docs", ".tox", ".git", "__pycache__"] +doctest_optionflags = ["NUMBER", "NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] +markers = [ + "fragment_data: marks tests as requiring fragment data (deselect with '-m \"not fragment_data\"')", + "scroll_data: marks tests as requiring scroll data (deselect with '-m \"not scroll_data\"')", +] + +# Extra options: +addopts = [ + "--strict-markers", + "--tb=short", + "--doctest-modules", + "--doctest-continue-on-failure", +] diff --git a/scripts/download-fragment-mesh-data.sh b/scripts/download-fragment-mesh-data.sh new file mode 100755 index 0000000..d919409 --- /dev/null +++ b/scripts/download-fragment-mesh-data.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Please provide at least one fragment ID (1, 2, or 3) to download." + exit +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +FRAGMENT_DIRNAME=fragments +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$FRAGMENT_DIRNAME" + +for i in "$@" +do + FRAGMENT_DIR="$OUTPUT_DIR_ABSOLUTE/$i" + REMOTE_PATH=fragments/Frag$i.volpkg/working/54keV_exposed_surface + + # Download ppm, obj, tif, and mtl files. + rclone copy :http:/"$REMOTE_PATH" "$FRAGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ \ + --include "{result.ppm,result.obj,result.tif,result.mtl}" --transfers=4 --progress --multi-thread-streams=4 --size-only +done diff --git a/scripts/download-fragments.sh b/scripts/download-fragments.sh new file mode 100755 index 0000000..99c4658 --- /dev/null +++ b/scripts/download-fragments.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Please provide at least one fragment ID (1, 2, 3, or 4) to download." + exit +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +FRAGMENT_DIRNAME=fragments +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$FRAGMENT_DIRNAME" + +for i in "$@" +do + FRAGMENT_DIR="$OUTPUT_DIR_ABSOLUTE/$i" + REMOTE_PATH=fragments/Frag$i.volpkg/working/54keV_exposed_surface + + # 1. Download surface volumes. + SURFACE_VOL_PATH=$REMOTE_PATH/surface_volume + echo "Downloading fragment $i surface volumes to $FRAGMENT_DIR..." + + rclone copy :http:/"$SURFACE_VOL_PATH" "$FRAGMENT_DIR"/surface_volume --http-url http://$USER:$PASS@$HOST/ \ + --progress --multi-thread-streams=4 --transfers=4 --size-only --exclude "*.json" + + # 2. Download ink labels, IR image, and mask. + other_files=("inklabels.png" "ir.png" "mask.png") + for f in "${other_files[@]}" + do + rclone copyto :http:/"$REMOTE_PATH/$f" "$FRAGMENT_DIR"/"$f" --http-url http://$USER:$PASS@$HOST/ + done +done diff --git a/scripts/download-monster-segment-mesh-data.sh b/scripts/download-monster-segment-mesh-data.sh new file mode 100755 index 0000000..44c1de1 --- /dev/null +++ b/scripts/download-monster-segment-mesh-data.sh @@ -0,0 +1,42 @@ +HOST=dl.ash2txt.org + +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_1_DIRNAME=scrolls/1 +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_1_DIRNAME" + +if [ "$#" -eq 0 ]; then + echo "Please provide at least one orientation (recto or verso) as an argument." + exit 1 +fi + +for ORIENTATION in "$@"; do + if [[ "$ORIENTATION" != "recto" && "$ORIENTATION" != "verso" ]]; then + echo "Invalid orientation: $ORIENTATION. Accepted values are recto or verso." + exit 1 + fi + REMOTE_PATH=stephen-parsons-uploads/$ORIENTATION + + # 1. Download surface volumes and meta.json. + SCROLL_SUBPART_NAME="Scroll1_part_1_wrap" + SCROLL_PART_NAME=${SCROLL_SUBPART_NAME}_${ORIENTATION} + EXTRAS_PATH=$REMOTE_PATH/extras + SEGMENT_DIR="$OUTPUT_DIR_ABSOLUTE/${SCROLL_PART_NAME}" + + echo "Downloading monster segment mesh data to $SEGMENT_DIR..." + + # 1. Download obj, tif, and mtl. + rclone copy :http:/"$EXTRAS_PATH" "$SEGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ \ + --filter "+ $SCROLL_PART_NAME.tif" \ + --filter "+ $SCROLL_PART_NAME.obj" \ + --filter "+ $SCROLL_PART_NAME.mtl" \ + --filter "- *" \ + --transfers=4 --progress --multi-thread-streams=4 --size-only + + # 2. Download ppm. + PPM_PATH="$EXTRAS_PATH/${SCROLL_PART_NAME}.ppm" + rclone copy :http:/"$PPM_PATH" "$SEGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ --progress --multi-thread-streams=4 --transfers=4 --size-only +done diff --git a/scripts/download-monster-segment-surface-vols.sh b/scripts/download-monster-segment-surface-vols.sh new file mode 100755 index 0000000..5e3f48d --- /dev/null +++ b/scripts/download-monster-segment-surface-vols.sh @@ -0,0 +1,37 @@ +HOST=dl.ash2txt.org + +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_1_DIRNAME=scrolls/1 +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_1_DIRNAME" + +if [ "$#" -eq 0 ]; then + echo "Please provide at least one orientation (recto or verso) as an argument." + exit 1 +fi + +for ORIENTATION in "$@"; do + if [[ "$ORIENTATION" != "recto" && "$ORIENTATION" != "verso" ]]; then + echo "Invalid orientation: $ORIENTATION. Accepted values are recto or verso." + exit 1 + fi + REMOTE_PATH=stephen-parsons-uploads/$ORIENTATION + + # 1. Download surface volumes and meta.json. + SCROLL_SUBPART_NAME="Scroll1_part_1_wrap" + SCROLL_PART_NAME=${SCROLL_SUBPART_NAME}_${ORIENTATION} + SURFACE_VOL_PATH=$REMOTE_PATH/${SCROLL_PART_NAME}_surface_volume + SEGMENT_DIR="$OUTPUT_DIR_ABSOLUTE/${SCROLL_PART_NAME}" + + echo "Downloading monster segment surface volumes to $SEGMENT_DIR..." + + rclone copy :http:/"$SURFACE_VOL_PATH" "$SEGMENT_DIR"/surface_volume --http-url http://$USER:$PASS@$HOST/ \ + --progress --multi-thread-streams=4 --transfers=4 --size-only + + # 2. Download mask. + MASK_PATH="$REMOTE_PATH/${SCROLL_PART_NAME}_mask.png" + rclone copy :http:/"$MASK_PATH" "$SEGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ --progress +done diff --git a/scripts/download-scroll-mesh-data-without-ppm.sh b/scripts/download-scroll-mesh-data-without-ppm.sh new file mode 100755 index 0000000..642e053 --- /dev/null +++ b/scripts/download-scroll-mesh-data-without-ppm.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Please provide at least one scroll ID to download." + exit +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_DIRNAME=scrolls +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_DIRNAME" + +for i in "$@" +do + SCROLL_DIR="$OUTPUT_DIR_ABSOLUTE/$i" + REMOTE_PATH=full-scrolls/Scroll$i.volpkg/paths + echo "Downloading scroll $i mesh files to $SCROLL_DIR..." + + mkdir -p "$SCROLL_DIR" # Ensure that the destination directory exists + + rclone copy :http:/"$REMOTE_PATH" "$SCROLL_DIR" --http-url http://$USER:$PASS@$HOST/ \ + --filter "+ /[0-9]*/{{\d+}}.tif" \ + --filter "+ /[0-9]*/{{\d+}}.obj" \ + --filter "+ /[0-9]*/{{\d+}}.mtl" \ + --filter "- *" \ + --transfers=4 --progress --multi-thread-streams=4 --size-only +done diff --git a/scripts/download-scroll-mesh-data.sh b/scripts/download-scroll-mesh-data.sh new file mode 100755 index 0000000..e4cd307 --- /dev/null +++ b/scripts/download-scroll-mesh-data.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Please provide at least one scroll ID to download." + exit +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_DIRNAME=scrolls +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_DIRNAME" + +for i in "$@" +do + SCROLL_DIR="$OUTPUT_DIR_ABSOLUTE/$i" + REMOTE_PATH=full-scrolls/Scroll$i.volpkg/paths + echo "Downloading scroll $i mesh files to $SCROLL_DIR..." + + mkdir -p "$SCROLL_DIR" # Ensure that the destination directory exists + + rclone copy :http:/"$REMOTE_PATH" "$SCROLL_DIR" --http-url http://$USER:$PASS@$HOST/ \ + --filter "+ /[0-9]*/{{\d+}}.tif" \ + --filter "+ /[0-9]*/{{\d+}}.obj" \ + --filter "+ /[0-9]*/{{\d+}}.mtl" \ + --filter "+ /[0-9]*/{{\d+}}.ppm" \ + --filter "- *" \ + --transfers=4 --progress --multi-thread-streams=4 --size-only +done diff --git a/scripts/download-scroll-surface-vols-by-segment.sh b/scripts/download-scroll-surface-vols-by-segment.sh new file mode 100755 index 0000000..9bc4b48 --- /dev/null +++ b/scripts/download-scroll-surface-vols-by-segment.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Usage: $0 ..." + echo "Please provide at least one scroll ID (1, 2, PHerc1667, or PHerc0332) to download." + exit +fi + +# Access the scroll ID +scroll_id=$1 +# Check if the scroll ID is an integer +if [[ $scroll_id =~ ^-?[0-9]+$ ]]; then + SCROLL_NAME="Scroll$scroll_id" +else + SCROLL_NAME="$scroll_id" +fi +echo "Scroll name: $SCROLL_NAME" + +# Shift the arguments so that we can iterate over segment IDs +shift + +# Check if at least one segment ID is provided +if [ $# -lt 1 ]; then + echo "Error: No segment IDs provided." + echo "Please provide at least one segment ID after the scroll ID." + exit 1 +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_DIRNAME=scrolls +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_DIRNAME" + +SCROLL_DIR="$OUTPUT_DIR_ABSOLUTE/$scroll_id" +for segment_id in "$@"; do + echo "Downloading segment ID: $segment_id" + REMOTE_PATH=full-scrolls/$SCROLL_NAME.volpkg/paths/$segment_id + + # 1. Download surface volumes + SURFACE_VOL_PATH=$REMOTE_PATH/layers + SEGMENT_DIR="$SCROLL_DIR/${segment_id}" + + echo "Downloading surface volumes to $SEGMENT_DIR..." + rclone copy :http:/"$SURFACE_VOL_PATH" "$SEGMENT_DIR"/layers --http-url http://$USER:$PASS@$HOST/ \ + --progress --multi-thread-streams=4 --transfers=4 --size-only + + # 2. Download mask. + # Check if the segment_id ends with '_superseded' and modify accordingly + if [[ $segment_id == *_superseded ]]; then + mask_name_prefix="${segment_id%_superseded}" + else + mask_name_prefix="$segment_id" + fi + + MASK_PATH="$REMOTE_PATH/${mask_name_prefix}_mask.png" + rclone copy :http:/"$MASK_PATH" "$SEGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ --progress + + # 3. Download meta.json, area, and author. + rclone copy :http:/"$REMOTE_PATH" "$SEGMENT_DIR" --http-url http://$USER:$PASS@$HOST/ --progress \ + --multi-thread-streams=4 --transfers=4 --size-only --include "*.{json,txt}" +done diff --git a/scripts/download-scroll-surface-vols.sh b/scripts/download-scroll-surface-vols.sh new file mode 100755 index 0000000..8bfc3b4 --- /dev/null +++ b/scripts/download-scroll-surface-vols.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Check command-line arguments +if [ $# -eq 0 ]; then + echo "Please provide at least one scroll ID (1, 2, PHerc1667, or PHerc0332) to download." + exit +fi + +HOST=dl.ash2txt.org +SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 || exit ; pwd -P )" +WORKSPACE_DIR="$(dirname "$SCRIPT_DIR")" + +SCROLL_DIRNAME=scrolls +DEFAULT_OUTPUT_DATA_DIR="$WORKSPACE_DIR/data" +OUTPUT_DATA_DIR="${DATA_DIR:-$DEFAULT_OUTPUT_DATA_DIR}" +OUTPUT_DIR_ABSOLUTE="$OUTPUT_DATA_DIR/$SCROLL_DIRNAME" + +excluded_extensions=("obj" "cpp" "ppm" "vcps" "orig" "mtl") # List of file extensions to exclude + +# Join the elements of the array with commas +extension_string=$(IFS=,; echo "${excluded_extensions[*]}") + +for i in "$@" +do + SCROLL_DIR="$OUTPUT_DIR_ABSOLUTE/$i" + # Check if $i is an integer + if [[ $i =~ ^-?[0-9]+$ ]]; then + SCROLL_NAME="Scroll$i" + else + SCROLL_NAME="$i" + fi + REMOTE_PATH=full-scrolls/$SCROLL_NAME.volpkg/paths + echo "Downloading scroll $i surface volumes to $SCROLL_DIR..." + + rclone copy :http:/"$REMOTE_PATH" "$SCROLL_DIR" --http-url http://$USER:$PASS@$HOST/ \ + --progress --multi-thread-streams=4 --transfers=4 --size-only --exclude "*.{$extension_string}" +done diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/data/__init__.py b/tests/integration/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/data/test_fragment.py b/tests/integration/data/test_fragment.py new file mode 100644 index 0000000..68958a4 --- /dev/null +++ b/tests/integration/data/test_fragment.py @@ -0,0 +1,94 @@ +from pathlib import Path + +import numpy as np +import pytest + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR +from vesuvius_challenge_rnd.data import Fragment + +# Fragment data is required for these tests. +pytestmark = pytest.mark.fragment_data + + +@pytest.fixture(params=list(FRAGMENT_DATA_DIR.glob("*"))) +def available_fragment_id(request) -> int: + yield int(str(request.param.name)) + + +@pytest.fixture +def fragment(available_fragment_id: int) -> Fragment: + yield Fragment(available_fragment_id) + + +def test_fragment_init(fragment: Fragment): + assert fragment.data_dir.exists() + assert fragment.volume_dir_path.exists() + assert isinstance(fragment.segment_name, str) + assert isinstance(fragment.fragment_id, int) + assert isinstance(fragment.surface_volume_dir_name, str) + + +def test_fragment_load_surface_vol_paths(fragment: Fragment): + paths = fragment.load_surface_vol_paths() + assert all(isinstance(path, Path) for path in paths) + assert all(path.exists() for path in paths) + + +def test_fragment_load_volume_single_slice(fragment: Fragment): + img_stack = fragment.load_volume(z_start=27, z_end=28) + assert isinstance(img_stack, np.ndarray) + assert img_stack.dtype == np.float32 + assert img_stack.shape == fragment.surface_shape + + +def test_fragment_load_volume_as_memmap_single_slice(fragment: Fragment): + img_stack = fragment.load_volume_as_memmap(z_start=27, z_end=28) + assert isinstance(img_stack, np.memmap) + assert img_stack.shape == (1, *fragment.surface_shape) + + +def test_fragment_load_mask(fragment: Fragment): + mask = fragment.load_mask() + assert isinstance(mask, np.ndarray) + assert mask.dtype == bool + assert mask.shape == fragment.surface_shape + + +def test_fragment_shape(fragment: Fragment): + shape = fragment.shape + assert isinstance(shape, tuple) + assert len(shape) == 3 + assert all(isinstance(d, int) for d in shape) + + +def test_fragment_n_slices(fragment: Fragment): + n_slices = fragment.n_slices + assert isinstance(n_slices, int) + assert n_slices == 65 + + +def test_fragment_surface_shape(fragment: Fragment): + shape = fragment.surface_shape + assert isinstance(shape, tuple) + assert len(shape) == 2 + assert all(isinstance(d, int) for d in shape) + + +def test_fragment_load_ink_labels(fragment: Fragment): + ink_labels = fragment.load_ink_labels() + assert isinstance(ink_labels, np.ndarray) + assert ink_labels.dtype == bool + assert ink_labels.shape == fragment.surface_shape + + +def test_fragment_load_ir_img(fragment: Fragment): + ir_img = fragment.load_ir_img() + assert isinstance(ir_img, np.ndarray) + assert ir_img.dtype == np.uint8 + assert ir_img.shape == fragment.surface_shape + + +def test_fragment_voxel_size_microns(fragment: Fragment): + voxel_size = fragment.voxel_size_microns + assert isinstance(voxel_size, float) + assert voxel_size > 0 diff --git a/tests/integration/data/test_ppm.py b/tests/integration/data/test_ppm.py new file mode 100644 index 0000000..ff9beb7 --- /dev/null +++ b/tests/integration/data/test_ppm.py @@ -0,0 +1,62 @@ +import os + +import numpy as np +import pytest + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data.ppm import PPM + +# Scroll data is required for these tests. +pytestmark = pytest.mark.scroll_data + + +@pytest.fixture +def ppm_path() -> str: + scroll_id = "1" + segment_name = "20230504125349" + volume_dir_path = SCROLL_DATA_DIR / str(scroll_id) / segment_name + ppm_local_path = volume_dir_path / f"{segment_name}.ppm" + return str(ppm_local_path) + + +@pytest.fixture +def ppm(ppm_path: str) -> PPM: + return PPM.from_path(ppm_path) + + +def test_load_ppm(ppm: PPM): + assert ppm.is_loaded() + assert ppm.dim == 6 + assert isinstance(ppm.data, np.ndarray) + assert ppm.data.ndim == 3 + assert ppm.data.shape[2] == 6 + + +def test_ppm_shape(ppm: PPM): + # FIXME: Only works for segment: 20230504125349 + assert ppm.width == 556 + assert ppm.height == 652 + assert ppm.dim == 6 + assert ppm.ordered + assert ppm.type == "double" + assert ppm.version == "1" + + # Test data + assert ppm.data.shape == (652, 556, 6) + + # Test some random indices. + np.testing.assert_array_equal( + ppm.data[50, 50], + np.array( + [ + 3891.8581099869857, + 2473.7598214240725, + 27.339080890883583, + -0.7284660659909741, + -0.552310577979698, + -0.4053272950978821, + ] + ), + ) + np.testing.assert_array_equal(ppm.data[0, 0], np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])) + np.testing.assert_array_equal(ppm.data[-1, -1], np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])) diff --git a/tests/integration/data/test_scroll.py b/tests/integration/data/test_scroll.py new file mode 100644 index 0000000..540be1d --- /dev/null +++ b/tests/integration/data/test_scroll.py @@ -0,0 +1,115 @@ +from pathlib import Path + +import numpy as np +import pytest + +from vesuvius_challenge_rnd.data import Scroll, ScrollSegment + +# Scroll data is required for these tests. +pytestmark = pytest.mark.scroll_data + + +@pytest.fixture( + params=[ + ("1", "20230504093154"), + ("2", "20230421192746"), + ] +) +def scroll_segment(request) -> ScrollSegment: + scroll_id, segment_name = request.param + yield ScrollSegment(scroll_id, segment_name) + + +@pytest.fixture( + params=[ + "1", + "2", + ] +) +def scroll(request) -> Scroll: + scroll_id = request.param + yield Scroll(scroll_id) + + +def test_scroll_segment_init(scroll_segment: ScrollSegment): + assert scroll_segment.data_dir.exists() + assert scroll_segment.volume_dir_path.exists() + assert isinstance(scroll_segment.segment_name, str) + assert isinstance(scroll_segment.scroll_id, str) + assert isinstance(scroll_segment.surface_volume_dir_name, str) + + +def test_scroll_segment_load_surface_vol_paths(scroll_segment: ScrollSegment): + paths = scroll_segment.load_surface_vol_paths() + assert all(isinstance(path, Path) for path in paths) + assert all(path.exists() for path in paths) + + +def test_scroll_segment_load_volume_single_slice(scroll_segment: ScrollSegment): + img_stack = scroll_segment.load_volume(z_start=27, z_end=28) + assert isinstance(img_stack, np.ndarray) + assert img_stack.dtype == np.float32 + assert img_stack.shape == scroll_segment.surface_shape + + +def test_scroll_segment_load_volume_as_memmap_single_slice(scroll_segment: ScrollSegment): + img_stack = scroll_segment.load_volume_as_memmap(z_start=27, z_end=28) + assert isinstance(img_stack, np.memmap) + assert img_stack.shape == (1, *scroll_segment.surface_shape) + + +def test_scroll_segment_load_mask(scroll_segment: ScrollSegment): + mask = scroll_segment.load_mask() + assert isinstance(mask, np.ndarray) + assert mask.dtype == bool + assert mask.shape == scroll_segment.surface_shape + + +def test_scroll_segment_shape(scroll_segment: ScrollSegment): + shape = scroll_segment.shape + assert isinstance(shape, tuple) + assert len(shape) == 3 + assert all(isinstance(d, int) for d in shape) + + +def test_scroll_segment_n_slices(scroll_segment: ScrollSegment): + n_slices = scroll_segment.n_slices + assert isinstance(n_slices, int) + assert n_slices == 65 + + +def test_scroll_segment_surface_shape(scroll_segment: ScrollSegment): + shape = scroll_segment.surface_shape + assert isinstance(shape, tuple) + assert len(shape) == 2 + assert all(isinstance(d, int) for d in shape) + + +def test_fragment_voxel_size_microns(scroll_segment: ScrollSegment): + voxel_size = scroll_segment.voxel_size_microns + assert isinstance(voxel_size, float) + assert voxel_size > 0 + + +def test_scroll_segment_author(scroll_segment: ScrollSegment): + assert isinstance(scroll_segment.author, str) + + +def test_scroll_segment_area_cm2(scroll_segment: ScrollSegment): + area_cm2 = scroll_segment.area_cm2 + assert isinstance(area_cm2, float) + assert area_cm2 > 0 + + +def test_scroll_init(scroll: Scroll): + assert len(scroll.segments) > 0 + assert isinstance(scroll.segments[0], ScrollSegment) + + +def test_load_ppm(): + segment = ScrollSegment("1", "20230504125349") + ppm = segment.load_ppm() + + assert ppm.is_loaded() + assert ppm.dim == 6 + assert isinstance(ppm.data, np.ndarray) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/data/__init__.py b/tests/unit/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/data/test_preprocessing.py b/tests/unit/data/test_preprocessing.py new file mode 100644 index 0000000..1aa6e86 --- /dev/null +++ b/tests/unit/data/test_preprocessing.py @@ -0,0 +1,11 @@ +import numpy as np + +from vesuvius_challenge_rnd.data import preprocess_subvolume + + +def test_preprocess_subvolume(): + input_arr = np.ones((10, 5)) + output = preprocess_subvolume(input_arr) + assert isinstance(output, np.ndarray) + assert output.shape == input_arr.shape + assert output.dtype == np.float32 diff --git a/tests/unit/data/test_util.py b/tests/unit/data/test_util.py new file mode 100644 index 0000000..7ab6170 --- /dev/null +++ b/tests/unit/data/test_util.py @@ -0,0 +1,34 @@ +import numpy as np + +from vesuvius_challenge_rnd.data.util import indices_to_microns, microns_to_indices + + +def test_microns_to_indices_single_index(): + microns = 5.0 + voxel_size_microns = 2.5 + expected_output = 2 + output = microns_to_indices(microns, voxel_size_microns=voxel_size_microns) + assert output == expected_output + + +def test_microns_to_indices_multi_ind(): + microns = [5.0, 10.0, 15.0] + voxel_size_microns = 2.5 + expected_output = [2, 4, 6] + output = microns_to_indices(microns, voxel_size_microns=voxel_size_microns) + assert np.array_equal(output, expected_output) + + +def test_indices_to_microns_single_index(): + voxel_size_microns = 5.0 + indices = 2 + microns = indices_to_microns(indices, voxel_size_microns) + assert microns == 10 + + +def test_indices_to_microns_multi_ind(): + indices = [2, 4, 6] + voxel_size_microns = 2.5 + expected_output = [5.0, 10.0, 15.0] + output = indices_to_microns(indices, voxel_size_microns=voxel_size_microns) + assert np.array_equal(output, expected_output) diff --git a/tests/unit/fragment_ink_detection/__init__.py b/tests/unit/fragment_ink_detection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/fragment_ink_detection/ink_detection/__init__.py b/tests/unit/fragment_ink_detection/ink_detection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/fragment_ink_detection/ink_detection/models/__init__.py b/tests/unit/fragment_ink_detection/ink_detection/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/fragment_ink_detection/ink_detection/models/test_unet_3d_to_2d.py b/tests/unit/fragment_ink_detection/ink_detection/models/test_unet_3d_to_2d.py new file mode 100644 index 0000000..e6b342c --- /dev/null +++ b/tests/unit/fragment_ink_detection/ink_detection/models/test_unet_3d_to_2d.py @@ -0,0 +1,23 @@ +import pytest +import torch + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models import UNet3Dto2D + + +@pytest.fixture +def unet_3d_to_2d() -> UNet3Dto2D: + return UNet3Dto2D() + + +def test_unet_3d_to_2d_forward(unet_3d_to_2d): + unet_3d_to_2d.eval() + input_sample = torch.randn(2, 1, 10, 100, 80) + with torch.no_grad(): + output = unet_3d_to_2d(input_sample) + logits = output.logits + assert isinstance(logits, torch.Tensor) + assert logits.ndim == 4 + assert logits.shape[0] == 2 + assert logits.shape[1] == 1 + assert logits.shape[2] == 100 + assert logits.shape[3] == 80 diff --git a/tests/unit/test_patching.py b/tests/unit/test_patching.py new file mode 100644 index 0000000..41d865c --- /dev/null +++ b/tests/unit/test_patching.py @@ -0,0 +1,9 @@ +from vesuvius_challenge_rnd.patching import patch_index_to_pixel_position + + +def test_patch_index_to_pixel_position(): + (y0, x0), (y1, x1) = patch_index_to_pixel_position(0, 0, (500, 600), 100) + assert y0 == 0 + assert x0 == 0 + assert y1 == 500 + assert x1 == 600 diff --git a/tools/print_segment_ids_from_label_dir.py b/tools/print_segment_ids_from_label_dir.py new file mode 100644 index 0000000..9e3a85e --- /dev/null +++ b/tools/print_segment_ids_from_label_dir.py @@ -0,0 +1,44 @@ +import argparse +from pathlib import Path + + +def get_segment_ids(directory: str, with_subsegments: bool = False): + # Create a Path object for the directory + dir_path = Path(directory) + + # Initialize a set for unique segment IDs + segment_ids = set() + + # Patterns to match specific file names + patterns = ["*_inklabels.png", "*_papyrusnoninklabels.png"] + + # Search for files matching each pattern + for pattern in patterns: + for file_path in dir_path.glob(pattern): + # Extract the segment ID from the file stem + stem = file_path.stem + if not with_subsegments: + segment_id = ( + stem.split("_C")[0].split("_inklabels")[0].split("_papyrusnoninklabels")[0] + ) + else: + segment_id = stem.split("_inklabels")[0].split("_papyrusnoninklabels")[0] + segment_ids.add(segment_id) + + return segment_ids + + +def main(): + parser = argparse.ArgumentParser(description="Extract segment IDs from specific PNG filenames.") + parser.add_argument("directory", type=str, help="Directory containing PNG files") + parser.add_argument("--with_subsegments", help="Include sub-segments", action="store_true") + + args = parser.parse_args() + directory = args.directory + + segment_ids = get_segment_ids(directory, args.with_subsegments) + print(" ".join(segment_ids)) + + +if __name__ == "__main__": + main() diff --git a/tools/segment_id_data_to_yaml_string.py b/tools/segment_id_data_to_yaml_string.py new file mode 100644 index 0000000..1d57618 --- /dev/null +++ b/tools/segment_id_data_to_yaml_string.py @@ -0,0 +1,29 @@ +import argparse + + +def convert_set_to_string_list(segment_ids: set, scroll_id: str = "1"): + """ + Convert a set of strings to a string that visually represents a list of strings. + + Args: + input_set (set): A set of strings. + + Returns: + str: A string representing the list of strings. + """ + return "[" + ", ".join(f"['{scroll_id}', '" + str(id_) + "']" for id_ in segment_ids) + "]" + + +def main(): + parser = argparse.ArgumentParser(description="Convert a set of scroll IDs.") + parser.add_argument("segment_ids", nargs="+", type=str, help="List of segment IDs") + parser.add_argument("--scroll_id", type=str, default="1", help="Scroll ID.") + + args = parser.parse_args() + + out_str = convert_set_to_string_list(set(args.segment_ids), scroll_id=args.scroll_id) + print(out_str) + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/__init__.py b/vesuvius_challenge_rnd/__init__.py new file mode 100644 index 0000000..1fdcd35 --- /dev/null +++ b/vesuvius_challenge_rnd/__init__.py @@ -0,0 +1,13 @@ +import os +from pathlib import Path + +from pint import UnitRegistry + +REPO_DIR = Path(__file__).parents[1] +os.environ.setdefault("DATA_DIR", str(REPO_DIR / "data")) +DATA_DIR = Path(os.environ["DATA_DIR"]) +FRAGMENT_DATA_DIR = DATA_DIR / "fragments" +SCROLL_DATA_DIR = DATA_DIR / "scrolls" + +ureg = UnitRegistry() +Q_ = ureg.Quantity diff --git a/vesuvius_challenge_rnd/data/__init__.py b/vesuvius_challenge_rnd/data/__init__.py new file mode 100644 index 0000000..1d6bd5b --- /dev/null +++ b/vesuvius_challenge_rnd/data/__init__.py @@ -0,0 +1,4 @@ +from .fragment import Fragment +from .preprocessing import preprocess_subvolume +from .preprocessors import InkIdPreprocessor, ZoomPreprocessor +from .scroll import MonsterSegment, MonsterSegmentRecto, MonsterSegmentVerso, Scroll, ScrollSegment diff --git a/vesuvius_challenge_rnd/data/constants.py b/vesuvius_challenge_rnd/data/constants.py new file mode 100644 index 0000000..a21ef31 --- /dev/null +++ b/vesuvius_challenge_rnd/data/constants.py @@ -0,0 +1,95 @@ +# Scroll segments that have the z-dimension flipped (with high certainty). +Z_REVERSED_SEGMENT_IDS = { + "20231022170900", + "20231016151000", + "20231012173610", + "20231007101615", + "20231005123333", + "20230922174128", + "20230919113918", + "20230901234823", + "20230901184804", + "20230826135043", + "20230820174948", + "20230812170020", + "20230701020044", + "20230531193658", + "20230529203721", + "20230526205020", + "20230526164930", + "20230526015925", + "20230526002441", + "20230602213452", + "20230525200512", + "20230521114306", + "Scroll1_part_1_wrap_verso", +} +# Scroll segments that have the z-dimension in the canonical orientation (with high certainty). +Z_NON_REVERSED_SEGMENT_IDS = { + "20231031143850", + "20231024093300", + "20230702185753", + "20230929220924", + "20230702185752", + "20231012085431", + "20230522181603", + "20231001164029", + "20231012184420", + "20231004222109", + "20230926164853", + "20230926164631", + "20230925090314", + "20230925002745", + "20230918145743", + "20230909121925", + "20230905134255", + "20230904135535", + "20230904020426", + "20230903193206", + "20230902141231", + "20230827161847", + "20230826170124", + "20230820203112", + "20230813_real_1", + "20230813_frag_2", + "20230801193640", + "20230620230619", + "20230620230617", + "20230604112252", + "20230601204340", + "20230531211425", + "20230531121653", + "20230531101257", + "20230530212931", + "20230530172803", + "20230520175435", + "20230519195952", + "20230515162442", + "20230509182749", + "20231031143852", + "20231106155351", + "20231005123335", + "20231012184421", + "20231106155350", + "20231005123335", + "20231005123336", + "20230929220925", + "20230929220926", + "20231012184422", + "20231016151001", + "20231210121320", + "20231210121321", + "20231012184423", + "20231007101616", + "20231022170901", + "20231221180250", + "20231016151002", + "20231205141500", + "20231206155550", + "20231007101617", + "20231221180251", + "20231007101618", + "20231007101619", +} +assert Z_REVERSED_SEGMENT_IDS.isdisjoint(Z_NON_REVERSED_SEGMENT_IDS) +KNOWN_Z_ORIENTATION_SEGMENT_IDS = Z_NON_REVERSED_SEGMENT_IDS.union(Z_REVERSED_SEGMENT_IDS) diff --git a/vesuvius_challenge_rnd/data/fragment.py b/vesuvius_challenge_rnd/data/fragment.py new file mode 100644 index 0000000..4316870 --- /dev/null +++ b/vesuvius_challenge_rnd/data/fragment.py @@ -0,0 +1,120 @@ +from pathlib import Path + +import numpy as np +import pint +from PIL import Image + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR, ureg +from vesuvius_challenge_rnd.data.volumetric_segment import VolumetricSegment + +VOXEL_SIZE_MICRONS = 3.24 +VOXEL_SIZE = VOXEL_SIZE_MICRONS * ureg.micron + + +class Fragment(VolumetricSegment): + """A fragment of a scroll. + + The Fragment class represents a specific segment or fragment of a scroll. It inherits from + the VolumetricSegment class and defines additional properties and methods specific to the + fragment, such as paths to ink labels and infrared (IR) images. + """ + + def __init__(self, fragment_id: int, fragment_dir: Path = FRAGMENT_DATA_DIR): + """ + Initializes a Fragment instance. + + Args: + fragment_id (int): The unique identifier for the fragment. + fragment_dir (Path, optional): The directory where the fragment data is located. + Defaults to FRAGMENT_DATA_DIR. + """ + super().__init__(data_dir=fragment_dir, segment_name=str(fragment_id)) + self.fragment_id = fragment_id + + @property + def ink_labels_path(self) -> Path: + """Path to the ink labels file of the fragment. + + Returns: + Path: The file path to the ink labels. + """ + return self.volume_dir_path / "inklabels.png" + + @property + def ir_img_path(self): + """Path to the infrared (IR) image file of the fragment. + + Returns: + Path: The file path to the IR image. + """ + return self.volume_dir_path / "ir.png" + + @property + def papyrus_mask_file_name(self) -> str: + """File name for the papyrus mask of the fragment. + + Returns: + str: The file name for the papyrus mask. + """ + return "mask.png" + + def load_ink_labels_as_img(self) -> Image: + """Load ink labels of the fragment as a PIL.Image. + + Returns: + Image: The ink labels of the fragment. + """ + return Image.open(self.ink_labels_path) + + def load_ink_labels(self) -> np.ndarray: + """Load ink labels of the fragment as a NumPy array. + + Returns: + np.ndarray: The ink labels of the fragment. + """ + return np.array(self.load_ink_labels_as_img(), dtype=bool) + + def load_ir_img_as_img(self) -> Image: + """Load the infrared (IR) image of the fragment as a PIL.Image. + + Returns: + Image: The IR image of the fragment. + """ + return Image.open(self.ir_img_path) + + def load_ir_img(self) -> np.ndarray: + """Load the infrared (IR) image of the fragment as a NumPy array. + + Returns: + np.ndarray: The IR image of the fragment. + """ + return np.array(self.load_ir_img_as_img()) + + @property + def voxel_size_microns(self) -> float: + """Get the voxel size in microns. + + Returns: + float: The voxel size in microns. + """ + return VOXEL_SIZE_MICRONS + + @property + def voxel_size(self) -> pint.Quantity: + """Get the voxel size as a pint quantity. + + Returns: + pint.Quantity: The voxel size, using microns as the unit. + """ + return VOXEL_SIZE + + @property + def ppm_path(self) -> Path: + return self.volume_dir_path / "result.ppm" + + @property + def mesh_path(self) -> Path: + return self.volume_dir_path / "result.obj" + + def __repr__(self): + return f"{self.__class__.__name__}(fragment_id={self.fragment_id}, shape={self.shape})" diff --git a/vesuvius_challenge_rnd/data/ppm.py b/vesuvius_challenge_rnd/data/ppm.py new file mode 100644 index 0000000..d375bb2 --- /dev/null +++ b/vesuvius_challenge_rnd/data/ppm.py @@ -0,0 +1,368 @@ +"""Per-pixel map adapted from https://github.com/educelab/ink-id/blob/53e1d696d9270cc13e3c3674939f5b60eb78faaa/inkid/data/ppm.py""" +# For PPM.initialized_ppms https://stackoverflow.com/a/33533514 +from __future__ import annotations + +from typing import ClassVar + +import logging +import re +import struct +from io import BytesIO +from pathlib import Path + +import numpy as np +from tqdm import tqdm + +from vesuvius_challenge_rnd.data.util import find_pattern_offset, get_raw_data_from_file_or_url + + +class PPM: + """Class to handle PPM (Per-pixel map) data.""" + + initialized_ppms: ClassVar[dict[str, PPM]] = dict() + + def __init__(self, path: str, lazy_load: bool = False): + """Initialize a PPM object. + + Args: + path (str): Path to the PPM file. + lazy_load (bool, optional): Whether to load data lazily. Defaults to False. + """ + self._path = path + + header = PPM.parse_ppm_header(path) + self.width: int = header["width"] + self.height: int = header["height"] + self.dim: int = header["dim"] + self.ordered: bool = header["ordered"] + self.type: str = header["type"] + self.version: str = header["version"] + + self.data: np.typing.ArrayLike | None = None + + logging.info( + f"Initialized PPM for {self._path} with width {self.width}, " + f"height {self.height}, dim {self.dim}" + ) + + if not lazy_load: + self.ensure_loaded() + + def is_loaded(self) -> bool: + """Check if the PPM data is loaded. + + Returns: + bool: True if data is loaded, otherwise False. + """ + return self.data is not None + + def ensure_loaded(self) -> None: + """Ensure that the PPM data is loaded.""" + if not self.is_loaded(): + self.load_ppm_data() + + @classmethod + def from_path(cls, path: str, lazy_load: bool = False) -> PPM: + """Create a PPM object from a path, with optional lazy loading. + + Args: + path (str): Path to the PPM file. + lazy_load (bool, optional): Whether to load data lazily. Defaults to False. + + Returns: + PPM: The PPM object. + """ + if path in cls.initialized_ppms: + return cls.initialized_ppms[path] + cls.initialized_ppms[path] = PPM(path, lazy_load=lazy_load) + return cls.initialized_ppms[path] + + @staticmethod + def parse_ppm_header(filename: str) -> dict: + """Parse the header of a PPM file. + + Args: + filename (str): Path to the PPM file. + + Returns: + dict: The parsed header information. + """ + comments_re = re.compile("^#") + width_re = re.compile("^width") + height_re = re.compile("^height") + dim_re = re.compile("^dim") + ordered_re = re.compile("^ordered") + type_re = re.compile("^type") + version_re = re.compile("^version") + header_terminator_re = re.compile("^<>$") + + width, height, dim, ordered, val_type, version = [None] * 6 + + data = get_raw_data_from_file_or_url(filename) + while True: + line = data.readline().decode("utf-8") + if comments_re.match(line): + pass + elif width_re.match(line): + width = int(line.split(": ")[1]) + elif height_re.match(line): + height = int(line.split(": ")[1]) + elif dim_re.match(line): + dim = int(line.split(": ")[1]) + elif ordered_re.match(line): + ordered = line.split(": ")[1].strip() == "true" + elif type_re.match(line): + val_type = line.split(": ")[1].strip() + assert val_type in ["double"] + elif version_re.match(line): + version = line.split(": ")[1].strip() + elif header_terminator_re.match(line): + break + else: + logging.warning(f"PPM header contains unknown line: {line.strip()}") + + return { + "width": width, + "height": height, + "dim": dim, + "ordered": ordered, + "type": val_type, + "version": version, + } + + @staticmethod + def write_ppm_from_data( + path: str, + data: np.typing.ArrayLike, + width: int, + height: int, + dim: int, + ordered: bool = True, + version: str = "1.0", + pbar: bool = True, + ) -> None: + """Write PPM data to a file. + + Args: + path (str): The file path to write the PPM data. + data (np.typing.ArrayLike): The data array. + width (int): The width of the PPM. + height (int): The height of the PPM. + dim (int): The dimension of the PPM. + ordered (bool, optional): Whether the data is ordered. Defaults to True. + version (str, optional): The PPM version. Defaults to "1.0". + pbar (bool, optional): Whether to show a progress bar. Defaults to True. + """ + with open(path, "wb") as f: + logging.info(f"Writing PPM to file {path}...") + f.write(f"width: {width}\n".encode()) + f.write(f"height: {height}\n".encode()) + f.write(f"dim: {dim}\n".encode()) + f.write("ordered: {}\n".format("true" if ordered else "false").encode("utf-8")) + f.write(b"type: double\n") + f.write(f"version: {version}\n".encode()) + f.write(b"<>\n") + y_iter = range(height) + if pbar: + y_iter = tqdm(y_iter, desc="Writing PPM...") + for y in y_iter: + for x in range(width): + for idx in range(dim): + f.write(struct.pack("d", data[y, x, idx])) + + def load_ppm_data(self, count: int = -1) -> None: + """Read the PPM file data and store it in the PPM object. + + The data is stored in an internal array indexed by [y, x, idx] + where idx is an index into an array of size dim. + + Example: For a PPM of dimension 6 to store 3D points and + normals, the first component of the normal vector for the PPM + origin would be at self._data[0, 0, 3]. + + Parameters: + count : int, optional + Number of items to read. ``-1`` means all data in the buffer. + + Raises: + ValueError: If the header terminator "<>\n" is not found in the data stream. + IOError: If unable to read the file or URL specified by ppm_path. + + Notes: + - Assumes that the PPM data has a header that ends with "<>\n". + - Assumes that the pixel data is stored as float64. + - This function relies on `get_raw_data_from_file_or_url` to get a BytesIO object for the data, + and `find_ppm_header_terminator_offset` to find the header terminator offset. + """ + logging.info( + f"Loading PPM data for {self._path} with width {self.width}, " + f"height {self.height}, dim {self.dim}..." + ) + + data_io = get_raw_data_from_file_or_url(self._path) + n_offset_bytes = _find_ppm_header_terminator_offset(data_io) + self.data = np.frombuffer( + data_io.getbuffer(), dtype=np.float64, count=count, offset=n_offset_bytes + ).reshape((self.height, self.width, self.dim)) + + def load_ppm_data_as_memmap(self) -> None: + """Read the PPM file data and store it in the PPM object as a memory map.""" + data_io = get_raw_data_from_file_or_url(self._path) + n_offset_bytes = _find_ppm_header_terminator_offset(data_io) + self.data = np.memmap( + self._path, + dtype=np.float64, + mode="r", + offset=n_offset_bytes, + shape=(self.height, self.width, self.dim), + ) + + def get_point_with_normal(self, ppm_x: int, ppm_y: int) -> np.typing.ArrayLike: + """Get the point along with its normal at given coordinates. + + Args: + ppm_x (int): The x-coordinate. + ppm_y (int): The y-coordinate. + + Returns: + np.typing.ArrayLike: The point with its normal. + """ + self.ensure_loaded() + return self.data[ppm_y][ppm_x] + + def get_points(self) -> np.typing.ArrayLike: + """Get all 3D points in the PPM. + + Returns: + np.typing.ArrayLike: The 3D points. + """ + self.ensure_loaded() + return self.data[:, :, :3] + + def get_surface_normals(self) -> np.typing.ArrayLike: + """Get all surface normals in the PPM. + + Returns: + np.typing.ArrayLike: The surface normals. + """ + self.ensure_loaded() + return self.data[:, :, 3:] + + def scale_down_by(self, scale_factor: float, pbar: bool = True) -> None: + """Scale down the PPM by a given factor. + + Args: + scale_factor (float): The scale factor. + pbar (bool, optional): Whether to show a progress bar. Defaults to True. + """ + self.ensure_loaded() + + self.width //= scale_factor + self.height //= scale_factor + + new_data = np.empty((self.height, self.width, self.dim)) + + logging.info(f"Downscaling PPM by factor of {scale_factor} on all axes...") + y_iter = range(self.height) + if pbar: + y_iter = tqdm(y_iter, desc="Downscaling PPM...") + for y in y_iter: + for x in range(self.width): + for idx in range(self.dim): + new_data[y, x, idx] = self.data[y * scale_factor, x * scale_factor, idx] + + self.data = new_data + + def translate(self, dx: int, dy: int, dz: int, pbar: bool = True) -> None: + """Translate the PPM by given distances along each axis. + + Args: + dx (int): Distance to translate along the x-axis. + dy (int): Distance to translate along the y-axis. + dz (int): Distance to translate along the z-axis. + pbar (bool, optional): Whether to show a progress bar. Defaults to True. + """ + y_iter = range(self.height) + if pbar: + y_iter = tqdm(y_iter, desc="Translating PPM...") + for ppm_y in y_iter: + for ppm_x in range(self.width): + if np.any(self.data[ppm_y, ppm_x]): # Leave empty pixels unchanged + vol_x, vol_y, vol_z = self.data[ppm_y, ppm_x, 0:3] + self.data[ppm_y, ppm_x, 0] = vol_x + dx + self.data[ppm_y, ppm_x, 1] = vol_y + dy + self.data[ppm_y, ppm_x, 2] = vol_z + dz + + def write(self, filename: Path | str, pbar: bool = True) -> None: + """Write the PPM object to a file. + + Args: + filename (Union[Path, str]): The file path or name. + pbar (bool, optional): Whether to show a progress bar. Defaults to True. + """ + self.ensure_loaded() + + with open(filename, "wb") as f: + logging.info(f"Writing PPM to file {filename}...") + f.write(f"width: {self.width}\n".encode()) + f.write(f"height: {self.height}\n".encode()) + f.write(f"dim: {self.dim}\n".encode()) + f.write("ordered: {}\n".format("true" if self.ordered else "false").encode("utf-8")) + f.write(b"type: double\n") + f.write(f"version: {self.version}\n".encode()) + f.write(b"<>\n") + y_iter = range(self.height) + if pbar: + y_iter = tqdm(y_iter, desc="Saving PPM...") + for y in y_iter: + for x in range(self.width): + for idx in range(self.dim): + f.write(struct.pack("d", self.data[y, x, idx])) + + def __getitem__(self, coords: tuple[int, int, int]) -> float: + self.ensure_loaded() + return self.data[coords] + + def __setitem__(self, coords: tuple[int, int, int], value: float) -> None: + self.ensure_loaded() + self.data[coords] = value + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PPM): + return False + self.ensure_loaded() + other.ensure_loaded() + return np.array_equal(self.data, other.data) + + def __len__(self) -> int: + """The number of rows.""" + return self.width * self.height + + def __contains__(self, value: float) -> bool: + self.ensure_loaded() + return value in self.data + + def __repr__(self) -> str: + loaded_status = "Loaded" if self.is_loaded() else "Not Loaded" + return ( + f"PPM(path={self._path!r}, width={self.width}, height={self.height}, " + f"dim={self.dim}, ordered={self.ordered}, type={self.type!r}, " + f"version={self.version!r}, status={loaded_status})" + ) + + +def _find_ppm_header_terminator_offset(data_stream: BytesIO) -> int: + """ + Find the position of the header terminator "<>\n" in the data. + + Parameters: + data_stream (BytesIO): The BytesIO object containing the data stream. + + Returns: + int: The inclusive offset of the first occurrence of the header terminator "<>\n". + + Raises: + ValueError: If the header terminator "<>\n" is not found in the data stream. + """ + pattern_to_search = b"<>\n" + return find_pattern_offset(data_stream, pattern_to_search) diff --git a/vesuvius_challenge_rnd/data/preprocessing.py b/vesuvius_challenge_rnd/data/preprocessing.py new file mode 100644 index 0000000..1934fdc --- /dev/null +++ b/vesuvius_challenge_rnd/data/preprocessing.py @@ -0,0 +1,32 @@ +import numpy as np + +MAX_VAL_UINT16 = np.iinfo(np.uint16).max + + +def preprocess_subvolume(subvolume: np.ndarray, slice_dim_last: bool = False) -> np.ndarray: + """Preprocess a subvolume (convert to float32 and normalize to be in [0, 1]). + + This function takes a subvolume (such as a 3D array) and performs preprocessing steps on it. + Specifically, it converts the values to float32 and normalizes them to the range [0, 1]. + + Optionally, if the parameter `slice_dim_last` is set to True, the function will rearrange + the axes of the subvolume such that the first dimension (usually corresponding to different + slices or frames in a volumetric dataset) is moved to the last dimension. This can be useful + in certain contexts where a specific axis ordering is required. + + Args: + subvolume: A numpy array representing the subvolume to be preprocessed. It is expected + to be in an integer format, as the preprocessing includes normalization by + dividing by 65535. + slice_dim_last: A boolean value that determines whether to move the first dimension to + the last. Default is False, meaning no rearrangement of axes. + + Returns: + np.ndarray: A numpy array of the same shape as the input, but with values converted to + float32 and normalized to the range [0, 1]. The axes may also be rearranged + if `slice_dim_last` is True. + """ + subvolume_pre = subvolume.astype(np.float32) / MAX_VAL_UINT16 + if slice_dim_last: + subvolume_pre = np.moveaxis(subvolume_pre, 0, -1) + return subvolume_pre diff --git a/vesuvius_challenge_rnd/data/preprocessors/__init__.py b/vesuvius_challenge_rnd/data/preprocessors/__init__.py new file mode 100644 index 0000000..a9b966e --- /dev/null +++ b/vesuvius_challenge_rnd/data/preprocessors/__init__.py @@ -0,0 +1,2 @@ +from .ink_id_preprocessor import InkIdPreprocessor +from .zoom_preprocessor import ZoomPreprocessor diff --git a/vesuvius_challenge_rnd/data/preprocessors/fragment_preprocessor.py b/vesuvius_challenge_rnd/data/preprocessors/fragment_preprocessor.py new file mode 100644 index 0000000..3a4554d --- /dev/null +++ b/vesuvius_challenge_rnd/data/preprocessors/fragment_preprocessor.py @@ -0,0 +1,259 @@ +import json +import logging +from abc import ABC, abstractmethod +from pathlib import Path + +import cv2 +import numpy as np +from tifffile import tifffile + +from vesuvius_challenge_rnd import DATA_DIR +from vesuvius_challenge_rnd.data import Fragment +from vesuvius_challenge_rnd.data.fragment import VOXEL_SIZE_MICRONS as FRAGMENT_VOXEL_SIZE_MICRONS +from vesuvius_challenge_rnd.data.scroll import VOXEL_SIZE_MICRONS as SCROLL_VOXEL_SIZE_MICRONS +from vesuvius_challenge_rnd.data.util import get_img_width_height + +DEFAULT_ZOOM_FACTOR = FRAGMENT_VOXEL_SIZE_MICRONS / SCROLL_VOXEL_SIZE_MICRONS +DEFAULT_OUTPUT_DIR = DATA_DIR / "processed" + + +class FragmentPreprocessorBase(ABC): + """A class that preprocesses surface volumes, masks, ink labels, and optionally IR images.""" + + def __init__( + self, + output_dir: Path = DEFAULT_OUTPUT_DIR, + preprocess_ir_img: bool = False, + skip_if_exists: bool = True, + check_img_shapes: bool = True, + ): + self.output_dir = output_dir + self.preprocess_ir_img = preprocess_ir_img + self.skip_if_exists = skip_if_exists + self.check_img_shapes = check_img_shapes + + def __call__(self, fragment: Fragment) -> None: + """Preprocess surface volumes, mask, ink labels, IR image (optional), and save a param file.""" + output_volume_dir = self._get_new_volume_dir(fragment) + new_surface_volume_dir = output_volume_dir / fragment.surface_volume_dir_name + if self.skip_if_exists and new_surface_volume_dir.exists(): + logging.info( + "Skipping surface volume preprocessing because the directory already exists." + ) + else: + self._preprocess_surface_volumes(fragment, new_surface_volume_dir) + + output_mask_path = output_volume_dir / fragment.papyrus_mask_file_name + if self.skip_if_exists and output_mask_path.exists(): + logging.info("Skipping mask preprocessing because it already exists.") + else: + self._preprocess_mask(fragment, output_mask_path) + + output_ink_labels_path = output_volume_dir / fragment.ink_labels_path.name + if self.skip_if_exists and output_ink_labels_path.exists(): + logging.info("Skipping ink labels preprocessing because it already exists.") + else: + self._preprocess_labels(fragment, output_ink_labels_path) + + if self.preprocess_ir_img: + output_ir_img_path = output_volume_dir / fragment.ir_img_path.name + if self.skip_if_exists and output_ir_img_path.exists(): + logging.info("Skipping IR image preprocessing because it already exists.") + else: + self._preprocess_ir_img(fragment, output_ir_img_path) + else: + output_ir_img_path = None + + output_json_path = output_volume_dir / "preprocessing_params.json" + if self.skip_if_exists and output_json_path.exists(): + logging.info("Skipping param saving because it already exists.") + else: + self._save_params(output_json_path) + + if self.check_img_shapes: + # Check if all image shapes are the same. + self._verify_img_surface_shapes( + new_surface_volume_dir, output_mask_path, output_ink_labels_path, output_ir_img_path + ) + + @abstractmethod + def _preprocess_surface_volumes(self, fragment: Fragment, output_dir: Path) -> None: + raise NotImplementedError + + @abstractmethod + def _preprocess_mask(self, fragment: Fragment, output_path: Path) -> None: + raise NotImplementedError + + @abstractmethod + def _preprocess_labels(self, fragment: Fragment, output_path: Path) -> None: + raise NotImplementedError + + @abstractmethod + def _preprocess_ir_img(self, fragment: Fragment, output_path: Path) -> None: + raise NotImplementedError + + @property + @abstractmethod + def method_name(self) -> str: + raise NotImplementedError + + def _save_surface_volumes(self, surface_volumes: np.ndarray, output_dir: Path) -> None: + """Save the surface volumes to the preprocessing directory.""" + _verify_is_dtype(surface_volumes, dtype=np.uint16) + output_dir.mkdir(exist_ok=True, parents=True) + + for i, img in enumerate(surface_volumes): + output_path = output_dir / f"{str(i).zfill(2)}.tif" + tifffile.imwrite(output_path, img) + + logging.debug(f"Saved {len(surface_volumes)} surface volumes.") + + def _save_mask(self, mask: np.ndarray, output_path: Path) -> None: + """Save the papyrus mask to the preprocessing directory.""" + _imwrite_uint8(mask, output_path) + logging.debug(f"Saved mask of shape {mask.shape}.") + + def _save_ink_labels(self, ink_labels: np.ndarray, output_path: Path) -> None: + """Save the ink labels to the preprocessing directory.""" + _imwrite_uint8(ink_labels, output_path) + logging.debug(f"Saved ink labels of shape {ink_labels.shape}.") + + def _save_ir_img(self, ir_img: np.ndarray, output_path: Path) -> None: + """Save the IR image to the preprocessing directory.""" + _imwrite_uint8(ir_img, output_path) + logging.debug(f"Saved IR image of shape {ir_img.shape}.") + + @property + def preprocessing_dir(self): + return self.output_dir / self.method_name + + def _get_new_volume_dir(self, fragment: Fragment) -> Path: + return self.preprocessing_dir / fragment.data_dir.name / fragment.volume_dir_path.name + + def _save_params(self, output_json_path: Path): + # Collect parameters in a dictionary + params = {key: value for key, value in self.__dict__.items() if not key.startswith("_")} + + # Convert Path objects to strings + for key, value in params.items(): + if isinstance(value, Path): + params[key] = str(value) + + # Serialize and save the dictionary as a JSON file + with open(output_json_path, "w") as json_file: + json.dump(params, json_file) + + logging.debug(f"Saved preprocessing params to {output_json_path}.") + + @staticmethod + def _verify_img_surface_shapes( + new_surface_volume_dir: Path, + output_mask_path: Path, + output_ink_labels_path: Path, + output_ir_img_path: Path, + ) -> None: + shapes = {} + surface_vol_0_path = list(new_surface_volume_dir.glob("*.tif"))[0] + surface_width_height = get_img_width_height(surface_vol_0_path) + shapes["surface_vol_0"] = surface_width_height + + mask_width_height = get_img_width_height(output_mask_path) + shapes["mask"] = mask_width_height + + labels_width_height = get_img_width_height(output_ink_labels_path) + shapes["ink_labels"] = labels_width_height + + if output_ir_img_path is not None: + ir_width_height = get_img_width_height(output_ir_img_path) + shapes["ir_img"] = ir_width_height + + shapes_equal = _check_shapes_equal(list(shapes.values())) + if not shapes_equal: + logging.warning(f"Found unequal image shapes (width, height): {shapes}") + + def __repr__(self): + return f"{type(self).__name__}(output_dir={self.output_dir}, preprocess_ir_img={self.preprocess_ir_img})" + + +class FragmentPreprocessor(FragmentPreprocessorBase): + def _preprocess_surface_volumes(self, fragment: Fragment, output_dir: Path) -> None: + """Load, transform, and save surface volumes.""" + surface_volumes = fragment.load_volume_as_memmap() + surface_volumes = self._transform_surface_volumes(surface_volumes) + self._save_surface_volumes(surface_volumes, output_dir) + + def _preprocess_mask(self, fragment: Fragment, output_path: Path) -> None: + """Load, transform, and save the papyrus mask.""" + mask = fragment.load_mask() + mask = self._transform_mask(mask) + self._save_mask(mask, output_path) + + def _preprocess_labels(self, fragment: Fragment, output_path: Path) -> None: + """Load, transform, and save the ink labels.""" + labels = fragment.load_ink_labels() + labels = self._transform_labels(labels) + self._save_ink_labels(labels, output_path) + + def _preprocess_ir_img(self, fragment: Fragment, output_path: Path) -> None: + """Load, transform, and save the infrared image.""" + ir_img = fragment.load_ir_img() + ir_img = self._transform_ir_img(ir_img) + self._save_ir_img(ir_img, output_path) + + @abstractmethod + def _transform_surface_volumes(self, surface_volumes: np.ndarray) -> np.ndarray: + raise NotImplementedError + + @abstractmethod + def _transform_labels(self, labels: np.ndarray) -> np.ndarray: + raise NotImplementedError + + @abstractmethod + def _transform_mask(self, mask: np.ndarray) -> np.ndarray: + raise NotImplementedError + + def _transform_ir_img(self, ir_img: np.ndarray) -> np.ndarray: + return ir_img + + +def _imwrite_uint8(array: np.ndarray, output_path: str | Path): + _verify_is_dtype(array, dtype=np.uint8) + cv2.imwrite(str(output_path), array) + + +def _verify_is_dtype(arr: np.ndarray, dtype: type[np.dtype]) -> None: + """ + Verifies if the input NumPy array is of a specific dtype. + + Args: + arr (ndarray): The input NumPy array. + dtype (type[np.dtype]): The expected data type. + + Returns: + None: Returns nothing if dtype matches the expected dtype. + + Raises: + ValueError: If dtype of the input array does not match the expected dtype. + """ + if arr.dtype != dtype: + raise ValueError(f"Input array must be of dtype {dtype}.") + return None + + +def _check_shapes_equal(shapes: list[tuple[int, ...]]) -> bool: + """ + Check if all shapes in the list are equal. + + Args: + shapes (List[tuple[int, ...]]): A list of shapes, each represented as a tuple of ints. + + Returns: + bool: True if all shapes are equal, False otherwise. + + Example: + >>> _check_shapes_equal([(600, 800, 3), (600, 800, 3), (600, 800, 3)]) + True + >>> _check_shapes_equal([(600, 800, 3), (600, 800, 4)]) + False + """ + return len(set(shapes)) == 1 if shapes else False diff --git a/vesuvius_challenge_rnd/data/preprocessors/ink_id_preprocessor.py b/vesuvius_challenge_rnd/data/preprocessors/ink_id_preprocessor.py new file mode 100644 index 0000000..dc23497 --- /dev/null +++ b/vesuvius_challenge_rnd/data/preprocessors/ink_id_preprocessor.py @@ -0,0 +1,146 @@ +from pathlib import Path + +import numpy as np +from tqdm import tqdm +from wand.image import Image + +from vesuvius_challenge_rnd.data import Fragment +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import ( + DEFAULT_OUTPUT_DIR, + DEFAULT_ZOOM_FACTOR, + FragmentPreprocessorBase, +) + +DEFAULT_INT_ZOOM_FACTOR = int(1 / DEFAULT_ZOOM_FACTOR) + + +class InkIdPreprocessor(FragmentPreprocessorBase): + def __init__( + self, + output_dir: Path = DEFAULT_OUTPUT_DIR, + zoom_factor: int = DEFAULT_INT_ZOOM_FACTOR, + upsample: bool = False, + pbar: bool = True, + preprocess_ir_img: bool = False, + skip_if_exists: bool = True, + ): + super().__init__( + output_dir, preprocess_ir_img=preprocess_ir_img, skip_if_exists=skip_if_exists + ) + self.zoom_factor = zoom_factor + self.upsample = upsample + self.pbar = pbar + + def _preprocess_surface_volumes(self, fragment: Fragment, output_dir: Path) -> None: + paths = fragment.all_surface_volume_paths + output_volume = apply_inkid_resolution_matching( + paths, int_zoom_factor=self.zoom_factor, upsample=self.upsample, pbar=self.pbar + ) + + # Reshape the array to D x H x W. + output_volume = output_volume.transpose((2, 0, 1)) + + # Rescale to int16 range from uint8 range. + output_volume = np.iinfo(np.uint16).max * (output_volume / np.iinfo(np.uint8).max) + output_volume = output_volume.astype(np.uint16) + + # Save output volume. + self._save_surface_volumes(output_volume, output_dir) + + def _preprocess_mask(self, fragment: Fragment, output_path: Path) -> None: + height, width, _ = compute_img_shape( + fragment.all_surface_volume_paths, zoom_factor=self.zoom_factor, upsample=self.upsample + ) + mask = wand_resize_to_array( + fragment.papyrus_mask_path, width, height, blob_format="GRAY", dtype=np.uint8 + ) + self._save_mask(mask, output_path) + + def _preprocess_labels(self, fragment: Fragment, output_path: Path) -> None: + height, width, _ = compute_img_shape( + fragment.all_surface_volume_paths, zoom_factor=self.zoom_factor, upsample=self.upsample + ) + ink_labels = wand_resize_to_array( + fragment.ink_labels_path, width, height, blob_format="GRAY", dtype=np.uint8 + ) + self._save_ink_labels(ink_labels, output_path) + + def _preprocess_ir_img(self, fragment: Fragment, output_path: Path) -> None: + height, width, _ = compute_img_shape( + fragment.all_surface_volume_paths, zoom_factor=self.zoom_factor, upsample=self.upsample + ) + ir_img = wand_resize_to_array( + fragment.ir_img_path, width, height, blob_format="GRAY", dtype=np.uint8 + ) + self._save_ir_img(ir_img, output_path) + + @property + def method_name(self) -> str: + return f"ink-id__zoom={self.zoom_factor}__upsample={self.upsample}" + + def __repr__(self): + return ( + f"{type(self).__name__}(output_dir={self.output_dir}, preprocess_ir_img={self.preprocess_ir_img}, " + f"zoom_factor={self.zoom_factor}, upsample={self.upsample})" + ) + + +def wand_to_array( + image_wand: Image, blob_format: str | None = None, dtype: type[np.dtype] | None = None +) -> np.ndarray: + width, height = image_wand.size + blob = image_wand.make_blob(format=blob_format) + array = np.frombuffer(blob, dtype=dtype) + array = array.reshape(height, width) + return array + + +def wand_resize_to_array( + img_path: Path, + width: int, + height: int, + blob_format: str | None = None, + dtype: type[np.dtype] | None = None, +) -> np.ndarray: + with Image(filename=img_path) as img: + with img.clone() as img_clone: + img_clone.resize(width=width, height=height) + array = wand_to_array(img_clone, blob_format=blob_format, dtype=dtype) + return array + + +def compute_img_shape( + image_filenames: list[Path], zoom_factor: int = 2, upsample: bool = False +) -> tuple[int, int, int]: + with Image(filename=image_filenames[0]) as img: + original_height = img.height + original_width = img.width + n_slices = len(image_filenames) + if not upsample: + new_height = original_height // zoom_factor + new_width = original_width // zoom_factor + return new_height, new_width, n_slices + else: + return original_height, original_width, n_slices + + +def apply_inkid_resolution_matching( + image_filenames: list[Path], int_zoom_factor: int = 2, upsample: bool = False, pbar: bool = True +) -> np.ndarray: + # Remove slices to downsample on z-axis. + image_filenames = image_filenames[::int_zoom_factor] + + height, width, n_slices = compute_img_shape( + image_filenames, zoom_factor=int_zoom_factor, upsample=upsample + ) + output_volume = np.empty((height, width, n_slices), dtype=np.uint8) + + # Use ImageMagick to downsample remaining images along the x and y-axes. + enumerable = tqdm(image_filenames, "Resizing images...") if pbar else image_filenames + for i, image_filename in enumerate(enumerable): + with Image(filename=image_filename) as img: + with img.clone() as img_clone: + img_clone.resize(width=width, height=height) + output_volume[:, :, i] = np.array(img_clone, dtype=np.uint8).squeeze() + + return output_volume diff --git a/vesuvius_challenge_rnd/data/preprocessors/zoom_preprocessor.py b/vesuvius_challenge_rnd/data/preprocessors/zoom_preprocessor.py new file mode 100644 index 0000000..b6846b8 --- /dev/null +++ b/vesuvius_challenge_rnd/data/preprocessors/zoom_preprocessor.py @@ -0,0 +1,50 @@ +from pathlib import Path + +import numpy as np +from scipy.ndimage import zoom + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import ( + DEFAULT_OUTPUT_DIR, + DEFAULT_ZOOM_FACTOR, + FragmentPreprocessor, +) + + +class ZoomPreprocessor(FragmentPreprocessor): + # FIXME: this needs to be tested + def __init__( + self, + output_dir: Path = DEFAULT_OUTPUT_DIR, + zoom_factor: float = DEFAULT_ZOOM_FACTOR, + order: int = 3, + preprocess_ir_img: bool = False, + skip_if_exists: bool = True, + ): + super().__init__( + output_dir, preprocess_ir_img=preprocess_ir_img, skip_if_exists=skip_if_exists + ) + self.zoom_factor = zoom_factor + self.order = order + + def _transform_surface_volumes(self, surface_volumes: np.ndarray) -> np.ndarray: + return zoom( + surface_volumes, + zoom=(self.zoom_factor, self.zoom_factor, self.zoom_factor), + order=self.order, + ) + + def _transform_labels(self, labels: np.ndarray) -> np.ndarray: + return zoom(labels, zoom=(self.zoom_factor, self.zoom_factor), order=self.order) + + def _transform_mask(self, mask: np.ndarray) -> np.ndarray: + return zoom(mask, zoom=(self.zoom_factor, self.zoom_factor), order=self.order) + + @property + def method_name(self) -> str: + return f"zoom__zoom_factor={self.zoom_factor}__order={self.order}" + + def __repr__(self): + return ( + f"{type(self).__name__}(output_dir={self.output_dir}, preprocess_ir_img={self.preprocess_ir_img}, " + f"zoom_factor={self.zoom_factor}, order={self.order})" + ) diff --git a/vesuvius_challenge_rnd/data/scroll.py b/vesuvius_challenge_rnd/data/scroll.py new file mode 100644 index 0000000..193d46b --- /dev/null +++ b/vesuvius_challenge_rnd/data/scroll.py @@ -0,0 +1,387 @@ +import json +from functools import cached_property +from pathlib import Path + +import pint + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR, ureg +from vesuvius_challenge_rnd.data.volumetric_segment import VolumetricSegment + +VOXEL_SIZE_MICRONS = 7.91 +VOXEL_SIZE = VOXEL_SIZE_MICRONS * ureg.micron +MONSTER_SEGMENT_PREFIX = "Scroll1_part_1_wrap" + + +class ScrollSegment(VolumetricSegment): + """A segment of a scroll.""" + + surface_volume_dir_name = "layers" + + def __init__( + self, + scroll_id: str, + segment_name: str, + scroll_dir: Path = SCROLL_DATA_DIR, + ): + """ + Initializes a ScrollSegment instance. + + Args: + scroll_id (str): The unique identifier for the scroll. + segment_name (str): The name of the segment. + scroll_dir (Path, optional): The directory where the scroll data is located. + Defaults to SCROLL_DATA_DIR. + """ + data_dir = scroll_dir / scroll_id + # Infer superseded. + self.segment_name_orig = segment_name + seg_name_parts = self.segment_name_orig.split("_superseded") + self.is_superseded = len(seg_name_parts) == 2 + self.is_subsegment = len(self.segment_name_orig.split("_C")) == 2 + segment_name = seg_name_parts[0] + super().__init__(data_dir, segment_name) + self.scroll_id = scroll_id + self.scroll_dir = scroll_dir + + @property + def papyrus_mask_file_name(self) -> str: + """The file name for the papyrus mask of the segment. + + Returns: + str: The file name for the papyrus mask. + """ + if not self.is_subsegment: + return f"{self.segment_name}_mask.png" + else: + return f"{self.segment_name_orig}_mask.png" + + @property + def voxel_size_microns(self) -> float: + """Get the voxel size in microns. + + Returns: + float: The voxel size in microns. + """ + return VOXEL_SIZE_MICRONS + + @property + def voxel_size(self) -> pint.Quantity: + """Get the voxel size as a pint quantity. + + Returns: + pint.Quantity: The voxel size, using microns as the unit. + """ + return VOXEL_SIZE + + @cached_property + def author(self) -> str: + """Get the annotator of the scroll segment. + + Returns: + str: The name or identifier of the annotator. + """ + with open(self.volume_dir_path / "author.txt") as f: + author = f.read() + return author + + @cached_property + def area_cm2(self) -> float: + """Get the area of the scroll segment in units of centimeters squared. + + Returns: + float: The area of the scroll segment. + """ + with open(self.volume_dir_path / "area_cm2.txt") as f: + area_cm2 = float(f.read()) + return area_cm2 + + @property + def volume_dir_path(self) -> Path: + """The volumetric segment data directory path. + + Returns: + Path: Path to the volume directory. + """ + if not self.is_superseded: + return super().volume_dir_path + else: + return self.data_dir / self.segment_name_orig + + @cached_property + def metadata(self) -> dict[str, str]: + """Retrieve the metadata for the scroll segment. + + Returns: + dict[str, str]: A dictionary containing the metadata for the scroll segment. + """ + with open(self.volume_dir_path / "meta.json") as f: + metadata = json.load(f) + return metadata + + @property + def ppm_path(self) -> Path: + return self.volume_dir_path / f"{self.segment_name}.ppm" + + @property + def mesh_path(self) -> Path: + return self.volume_dir_path / f"{self.segment_name}.obj" + + def __repr__(self): + return f"{self.__class__.__name__}(scroll_id={self.scroll_id}, segment_name={self.segment_name}, shape={self.shape})" + + +class MonsterSegment(ScrollSegment): + """A specific type of scroll segment, known as a MonsterSegment.""" + + surface_volume_dir_name = "surface_volume" + + def __init__( + self, + segment_name: str, + scroll_dir: Path = SCROLL_DATA_DIR, + ): + """ + Initializes a MonsterSegment instance. + + Args: + segment_name (str): The name of the segment. + scroll_dir (Path, optional): The directory where the scroll data is located. + Defaults to SCROLL_DATA_DIR. + """ + super().__init__(scroll_id="1", segment_name=segment_name, scroll_dir=scroll_dir) + self._orientation = self.segment_name.split("_")[-1] + + @property + def orientation(self) -> str: + """Get the orientation of the MonsterSegment. + + Returns: + str: The orientation of the MonsterSegment. + """ + return self._orientation + + @classmethod + def from_orientation(cls, orientation: str, scroll_dir: Path = SCROLL_DATA_DIR): + """Create a MonsterSegment from a specific orientation. + + Args: + orientation (str): The orientation for the MonsterSegment. + scroll_dir (Path, optional): The directory where the scroll data is located. + Defaults to SCROLL_DATA_DIR. + + Returns: + MonsterSegment: The MonsterSegment instance. + """ + segment_name = f"{MONSTER_SEGMENT_PREFIX}_{orientation}" + return cls(segment_name, scroll_dir=scroll_dir) + + @cached_property + def author(self) -> str: + """ + Get the annotator of the scroll segment. + + Returns: + str: The name of the annotator. + """ + return "stephen" + + @cached_property + def area_cm2(self) -> float: + """ + Get the area of the scroll segment in centimeters squared. + + Returns: + float: The area of the segment, calculated using the mask and voxel size. + """ + seg_mask = self.load_mask() + return (seg_mask.sum() * self.voxel_size_microns**2) / 1e8 + + @cached_property + def metadata(self) -> dict[str, str]: + """ + Get the metadata of the segment. + + Returns: + dict[str, str]: The metadata, loaded from a JSON file. + """ + surface_volume_dir = self.volume_dir_path / self.surface_volume_dir_name + with open(surface_volume_dir / "meta.json") as f: + metadata = json.load(f) + return metadata + + +class MonsterSegmentVerso(MonsterSegment): + """ + Represents the Verso orientation (reverse side) of a monster scroll segment. + This class encapsulates specific behavior or properties associated with the reverse side of the segment. + """ + + def __init__( + self, + segment_name: str = f"{MONSTER_SEGMENT_PREFIX}_verso", + scroll_dir: Path = SCROLL_DATA_DIR, + ): + """ + Initialize a Verso (reverse side) monster scroll segment. + Args: + segment_name (str, optional): The name of the segment. Defaults to f"{MONSTER_SEGMENT_PREFIX}_verso". + scroll_dir (Path, optional): The directory path containing the scroll data. Defaults to SCROLL_DATA_DIR. + """ + super().__init__(segment_name, scroll_dir=scroll_dir) + + +class MonsterSegmentRecto(MonsterSegment): + """ + Represents the Recto orientation (front side) of a monster scroll segment. + This class encapsulates specific behavior or properties associated with the front side of the segment. + """ + + def __init__( + self, + segment_name: str = f"{MONSTER_SEGMENT_PREFIX}_recto", + scroll_dir: Path = SCROLL_DATA_DIR, + ): + """ + Initialize a Recto (front side) monster scroll segment. + Args: + segment_name (str, optional): The name of the segment. Defaults to f"{MONSTER_SEGMENT_PREFIX}_recto". + scroll_dir (Path, optional): The directory path containing the scroll data. Defaults to SCROLL_DATA_DIR. + """ + super().__init__(segment_name, scroll_dir=scroll_dir) + + +class Scroll: + """A collection of scroll segments.""" + + def __init__(self, scroll_id: str, scroll_dir: Path = SCROLL_DATA_DIR): + """ + Initializes a Scroll instance representing a collection of scroll segments. + + Args: + scroll_id (str): The unique identifier for the scroll. + scroll_dir (Path, optional): The directory where the scroll data is located. + Defaults to SCROLL_DATA_DIR. + """ + self.scroll_id = scroll_id + self.scroll_dir = scroll_dir + self.data_dir = self.scroll_dir / str(self.scroll_id) + + self.segments: list[ScrollSegment] = [] + self.missing_segment_names: list[str] = [] + for segment_path in self.data_dir.glob("*"): + segment_name = segment_path.name + try: + segment = create_scroll_segment(self.scroll_id, segment_name, self.scroll_dir) + self.segments.append(segment) + except ValueError: + self.missing_segment_names.append(segment_name) + + @property + def segment_names(self) -> list[str]: + """Get the segment names of the scroll. + + Returns: + list[str]: A list of segment names. + """ + return [segment.segment_name for segment in self.segments] + + @property + def n_missing_segments(self) -> int: + """Get the number of scroll segments missing data. + + Returns: + int: The number of missing segments. + """ + return len(self.missing_segment_names) + + @property + def voxel_size_microns(self) -> float: + """Get the voxel size in microns. + + Returns: + float: The voxel size in microns. + """ + return VOXEL_SIZE_MICRONS + + @property + def voxel_size(self) -> pint.Quantity: + """Get the voxel size as a pint quantity. + + Returns: + pint.Quantity: The voxel size, using microns as the unit. + """ + return VOXEL_SIZE + + def __getitem__(self, index: int) -> ScrollSegment: + """Get a scroll segment by its index within the collection. + + Args: + index (int): The index of the segment. + + Returns: + ScrollSegment: The scroll segment at the given index. + """ + return self.segments[index] + + def __len__(self) -> int: + """Get the number of segments in the scroll. + + Returns: + int: The number of segments. + """ + return len(self.segments) + + def __repr__(self): + return f"{self.__class__.__name__}(scroll_id={self.scroll_id}, num segments={len(self)})" + + +def create_scroll_segment( + scroll_id: str, segment_name: str, scroll_dir: Path = SCROLL_DATA_DIR +) -> ScrollSegment: + """ + Creates a scroll segment object based on the given parameters. + + Args: + scroll_id (str): The unique identifier for the scroll. + segment_name (str): The name of the segment to be created. + scroll_dir (Path, optional): The directory where scroll data is stored. Defaults to SCROLL_DATA_DIR. + + Returns: + ScrollSegment: The created scroll segment object. + + Raises: + ValueError: If no surface volumes are found for the given segment. + """ + segment_path = build_scroll_segment_path(scroll_dir, scroll_id, segment_name) + is_not_monster_segment = not segment_name.startswith(MONSTER_SEGMENT_PREFIX) + shared_segment_kwargs = {"segment_name": segment_name, "scroll_dir": scroll_dir} + if is_not_monster_segment: + segment_type = ScrollSegment + segment_kwargs = {"scroll_id": scroll_id} | shared_segment_kwargs + else: + segment_type = MonsterSegment + segment_kwargs = shared_segment_kwargs + + surface_vol_dir_path = segment_path / segment_type.surface_volume_dir_name + if surface_vol_dir_path.is_dir(): + segment = segment_type(**segment_kwargs) + else: + raise ValueError( + f"Could not create scroll segment {segment_name}. No surface volumes found." + ) + return segment + + +def build_scroll_segment_path(scroll_dir: Path, scroll_id: str, segment_name: str) -> Path: + """ + Builds the file path for a scroll segment. + + Args: + scroll_dir (Path): The directory where scroll data is stored. + scroll_id (str): The unique identifier for the scroll. + segment_name (str): The name of the segment. + + Returns: + Path: The constructed file path for the segment. + """ + return scroll_dir / scroll_id / segment_name diff --git a/vesuvius_challenge_rnd/data/util.py b/vesuvius_challenge_rnd/data/util.py new file mode 100644 index 0000000..ac2fb61 --- /dev/null +++ b/vesuvius_challenge_rnd/data/util.py @@ -0,0 +1,161 @@ +from typing import Any, TypeVar + +import os +import re +import tempfile +from collections.abc import Sequence +from io import BytesIO +from pathlib import Path +from urllib.parse import urlsplit + +import numpy as np +import requests +from PIL import Image + +_T = TypeVar("_T") +_SequenceLike = Sequence[_T] | np.ndarray +_ScalarOrSequence = _T | _SequenceLike[_T] +_BoolLike_co = bool | np.bool_ +_IntLike_co = _BoolLike_co | int | np.integer[Any] +_FloatLike_co = _IntLike_co | float | np.floating[Any] + + +def microns_to_indices( + microns: _ScalarOrSequence[_FloatLike_co], voxel_size_microns: float +) -> np.integer[Any] | np.ndarray: + """Convert spatial coordinates (in microns) to volumetric indices. + + This function takes spatial coordinates (in microns) and a given voxel size (also in microns) + to convert these coordinates into integer indices representing their position within a + volumetric space. This can be useful for converting real-world measurements into indices + that can be used to index into a volumetric data structure like a 3D array. + + Args: + microns: A scalar or sequence-like object of floating-point values representing spatial + coordinates in microns. + voxel_size_microns: The size of a voxel in microns. It defines the conversion factor + between spatial coordinates and indices. + + Returns: + np.integer[Any] | np.ndarray: Integer or array of integers representing the converted + indices corresponding to the spatial coordinates. + """ + return (np.asanyarray(microns) / voxel_size_microns).astype(int) + + +def indices_to_microns( + indices: _ScalarOrSequence[_IntLike_co], voxel_size_microns: float +) -> np.floating[Any] | np.ndarray: + """Convert volumetric indices to space (in microns). + + This function takes volumetric indices and a given voxel size (in microns) to convert + these indices into real-world spatial coordinates. This can be useful for mapping from + a volumetric data structure like a 3D array back to real-world measurements. + + Args: + indices: A scalar or sequence-like object of integer values representing volumetric + indices within a 3D space. + voxel_size_microns: The size of a voxel in microns. It defines the conversion factor + between indices and spatial coordinates. + + Returns: + np.floating[Any] | np.ndarray: Float or array of floats representing the converted + spatial coordinates corresponding to the volumetric indices. + """ + return (np.asanyarray(indices) * voxel_size_microns).astype(float) + + +def get_raw_data_from_file_or_url( + filename: str, return_relative_url: bool = False +) -> BytesIO | tuple[BytesIO, tuple]: + """Return the raw file contents from a filename or URL. + + Supports absolute and relative file paths as well as the http and https + protocols. + + """ + url = urlsplit(filename) + is_windows_path = len(filename) > 1 and filename[1] == ":" + if url.scheme in ("http", "https"): + response = requests.get(filename) + if response.status_code != 200: + raise ValueError(f"Unable to fetch URL " f"(code={response.status_code}): {filename}") + data = response.content + elif url.scheme == "" or is_windows_path: + with open(filename, "rb") as f: + data = f.read() + else: + raise ValueError(f"Unsupported URL: {filename}") + relative_url = ( + url.scheme, + url.netloc, + os.path.dirname(url.path), + url.query, + url.fragment, + ) + if return_relative_url: + return BytesIO(data), relative_url + else: + return BytesIO(data) + + +def find_pattern_offset(data_stream: BytesIO, pattern: bytes) -> int: + """ + Find the inclusive offset of the first occurrence of a pattern in a BytesIO object. + + Parameters: + data (BytesIO): The BytesIO object containing the data. + pattern (bytes): The byte pattern to search for. + + Returns: + int: The inclusive offset of the first occurrence of the pattern. + + Raises: + ValueError: If the pattern is not found in the data. + """ + compiled_pattern = re.compile(pattern) + offset = 0 + + while True: + line = data_stream.readline() + if not line: + raise ValueError( + f"The specified pattern {pattern.decode('utf-8')} was not found in the data." + ) + + match_result = compiled_pattern.search(line) + if match_result: + offset += match_result.start() + return offset + (match_result.end() - match_result.start()) + + offset += len(line) + + +def create_tempfile_name(tempdir: Path | str) -> str: + # Ensure the directory exists + if not os.path.exists(tempdir): + os.makedirs(tempdir) + + # Generate a unique filename in the directory + with tempfile.NamedTemporaryFile(dir=tempdir, delete=True) as tf: + temp_filename = tf.name + + return temp_filename + + +def get_img_width_height(img_path: str | Path) -> tuple[int, int]: + """ + Get the width and height of an image using PIL's Image.open method. + + Args: + img_path (str | Path): The file path to the image. + + Returns: + tuple[int, int]: A tuple containing the width and height of the image. + + Example: + >>> get_img_width_height("path/to/image.png") + (800, 600) + """ + with Image.open(img_path) as img: + return img.size diff --git a/vesuvius_challenge_rnd/data/visualization.py b/vesuvius_challenge_rnd/data/visualization.py new file mode 100644 index 0000000..6cf4f99 --- /dev/null +++ b/vesuvius_challenge_rnd/data/visualization.py @@ -0,0 +1,125 @@ +import numpy as np +import plotly.graph_objects as go +import trimesh +from matplotlib import colormaps +from matplotlib import pyplot as plt +from matplotlib.patches import Rectangle +from PIL import Image +from scipy.ndimage import zoom + + +def create_gif_from_2d_single_channel_images( + frames: np.ndarray, + frame_dim: int = 0, + out_path: str = "output.gif", + cmap: str = "viridis", + duration: float = 100, + loop: int = 0, + scale_factor: float = 1.0, + downsample_factor: float = 1.0, + downsample_order: int = 3, +): + """ + Create an animated GIF of single-channel 2D images. + + Parameters: + frames (np.ndarray): NumPy array representing the frames. + frame_dim (int, optional): Dimension along which frames are stacked in the input array. Defaults to 0. + cmap (str): Name of the colormap to use. + out_path (str): Path to save the animated GIF. + duration (float, optional): Duration of each frame in milliseconds. Defaults to 100. + loop (int, optional): Number of loops. 0 for infinite. Defaults to 0. + scale_factor (float, optional): Factor by which to scale the pixel values. Defaults to 1.0. + downsample_factor (float, optional): Factor by which to downsample the H and W dimensions. Should be between 0 and 1. + downsample_order (int, optional): The spline interpolation order for downsampling. Defaults to 3. + """ + if len(frames.shape) != 3: + raise ValueError("3D frames array expected to be in `THW` format.") + elif not 0 < downsample_factor <= 1: + raise ValueError("downsample_factor should be between 0 and 1.") + elif not 0 < scale_factor <= 255: + raise ValueError("scale_factor should be between 0 and 255.") + + frames = np.moveaxis(frames, frame_dim, 0) + + if downsample_factor < 1.0: + zoom_factors = [1, downsample_factor, downsample_factor] + frames = zoom(frames, zoom_factors, order=downsample_order) + + frames = (frames * scale_factor).astype(np.uint8, copy=False) + + colormap = colormaps[cmap] + frames_colored = colormap(frames / 255.0) + frames_colored = (frames_colored[:, :, :, :3] * 255).astype(np.uint8, copy=False) + frames_pil = [Image.fromarray(frame, "RGB") for frame in frames_colored] + + frames_pil[0].save( + out_path, save_all=True, append_images=frames_pil[1:], loop=loop, duration=duration + ) + + +def plot_rectangles(rectangles, stroke_width=2, edgecolor="r", xlim=None, ylim=None, ax=None): + """ + Plots a list of rectangles with customizable x and y axis limits. + + Parameters: + rectangles (list of tuples): List of rectangles defined by lower-left and upper-right corners. + stroke_width (int): The width of the stroke for the rectangles. + edgecolor (str): The color of the edges. + xlim (tuple): A tuple (min, max) to set the limit for the x-axis. + ylim (tuple): A tuple (min, max) to set the limit for the y-axis. + ax (plt.Axes): The current axes. + """ + if ax is None: + ax = plt.gca() + + for rect in rectangles: + (x1, y1), (x2, y2) = rect + width, height = x2 - x1, y2 - y1 + rect_patch = Rectangle( + (x1, y1), width, height, linewidth=stroke_width, edgecolor=edgecolor, facecolor="none" + ) + ax.add_patch(rect_patch) + + # Set custom or calculated axis limits + if xlim: + ax.set_xlim(*xlim) + else: + all_x = [x for rect in rectangles for x in [rect[0][0], rect[1][0]]] + ax.set_xlim(min(all_x), max(all_x)) + + if ylim: + ax.set_ylim(*ylim) + else: + all_y = [y for rect in rectangles for y in [rect[0][1], rect[1][1]]] + ax.set_ylim(min(all_y), max(all_y)) + + return ax + + +def show_mesh_without_texture( + mesh: trimesh.Trimesh, title: str = "Mesh", intensity=None, colorscale=None, **mesh_kwargs +): + vertices = mesh.vertices + triangles = mesh.faces + + fig = go.Figure( + data=[ + go.Mesh3d( + x=vertices[:, 0], + y=vertices[:, 1], + z=vertices[:, 2], + i=triangles[:, 0], + j=triangles[:, 1], + k=triangles[:, 2], + opacity=0.5, + intensity=intensity, + colorscale=colorscale, + **mesh_kwargs, + ) + ], + ) + + fig.update_layout(title=dict(text=title)) + + fig.show() diff --git a/vesuvius_challenge_rnd/data/volumetric_segment.py b/vesuvius_challenge_rnd/data/volumetric_segment.py new file mode 100644 index 0000000..b611319 --- /dev/null +++ b/vesuvius_challenge_rnd/data/volumetric_segment.py @@ -0,0 +1,292 @@ +from typing import TYPE_CHECKING + +import json +import os +from abc import ABC, abstractmethod +from functools import cached_property +from pathlib import Path + +import numpy as np +import pint +from PIL import Image +from tifffile import TiffSequence, ZarrFileSequenceStore, imread + +from vesuvius_challenge_rnd.data.ppm import PPM +from vesuvius_challenge_rnd.data.preprocessing import preprocess_subvolume +from vesuvius_challenge_rnd.data.util import create_tempfile_name + +if TYPE_CHECKING: + import trimesh + + +# Ignore PIL warnings about large images. +Image.MAX_IMAGE_PIXELS = 10_000_000_000 + + +class VolumetricSegment(ABC): + """Abstract base class of a volumetric segment.""" + + surface_volume_dir_name = "surface_volume" # The name of the surface volume directory name. + + def __init__(self, data_dir: Path, segment_name: str): + """Initialize the volumetric segment. + + Args: + data_dir (Path): The directory containing data. + segment_name (str): The name of the segment. + """ + if not data_dir.is_dir(): + raise NotADirectoryError(f"The data directory {data_dir} does not exist.") + + self.data_dir = data_dir + self.segment_name = segment_name + + if not self.volume_dir_path.is_dir(): + NotADirectoryError(f"The volume data directory {self.volume_dir_path} does not exist.") + if len(self.all_surface_volume_paths) == 0: + raise ValueError( + f"No surface volume paths found for segment {self.segment_name} in {self.volume_dir_path}" + ) + + self._tiff_sequence = TiffSequence(files=self.all_surface_volume_paths, mode="r") + self._zarr = self.tiff_sequence.aszarr() + + @property + @abstractmethod + def papyrus_mask_file_name(self) -> str: + """The papyrus mask file name directory name. + + Returns: + str: The mask file name. + """ + raise NotImplementedError("Child classes must implement this.") + + @property + @abstractmethod + def ppm_path(self) -> Path: + """The PPM file path. + + Returns: + Path: The PPM file path. + """ + raise NotImplementedError("Child classes must implement this.") + + @property + @abstractmethod + def mesh_path(self) -> Path: + """The PPM file path. + + Returns: + Path: The mesh (.obj) file path. + """ + raise NotImplementedError("Child classes must implement this.") + + @property + def volume_dir_path(self) -> Path: + """The volumetric segment data directory path. + + Returns: + Path: Path to the volume directory. + """ + return self.data_dir / self.segment_name + + @property + def all_surface_volume_paths(self) -> list[Path]: + """All surface volume paths. + + Returns: + list[Path]: A list of paths to surface volumes. + """ + surface_volume_dir = self.volume_dir_path / self.surface_volume_dir_name + return list(sorted(surface_volume_dir.glob("*.tif"), key=lambda x: int(x.stem))) + + @property + def papyrus_mask_path(self) -> Path: + """The papyrus mask path. + + Returns: + Path: Path to the papyrus mask. + """ + return self.volume_dir_path / self.papyrus_mask_file_name + + def load_surface_vol_paths( + self, z_start: int | None = None, z_end: int | None = None + ) -> list[Path]: + """Load surface volume paths. + + Args: + z_start (int | None, optional): The start of the z-range. Defaults to None. + z_end (int | None, optional): The end of the z-range. Defaults to None. + + Returns: + list[Path]: A list of paths to the surface volumes. + """ + return self.all_surface_volume_paths[z_start:z_end] + + def load_volume( + self, z_start: int | None = None, z_end: int | None = None, preprocess: bool = True + ) -> np.ndarray: + """Load a volumetric segment as an array. + + Args: + z_start (int | None, optional): The start of the z-range. Defaults to None. + z_end (int | None, optional): The end of the z-range. Defaults to None. + preprocess (bool, optional): Whether to apply sub-volume preprocessing. Defaults to True. + + Returns: + np.ndarray: The volumetric segment array. + """ + surface_volume_paths = self.load_surface_vol_paths(z_start=z_start, z_end=z_end) + image_stack = imread(surface_volume_paths) + if preprocess: + image_stack = preprocess_subvolume(preprocess) + return image_stack + + def load_volume_as_memmap( + self, z_start: int | None = None, z_end: int | None = None + ) -> np.memmap: + """Load a volume (image stack) as a memory-mapped file. + + Args: + z_start (int | None, optional): The start of the z-range. Defaults to None. + z_end (int | None, optional): The end of the z-range. Defaults to None. + """ + surface_volume_paths = self.load_surface_vol_paths(z_start=z_start, z_end=z_end) + tiff_sequence = TiffSequence(files=surface_volume_paths, mode="r") + + tempdir = os.environ.get("MEMMAP_DIR") + if tempdir is None: + image_stack_ref = tiff_sequence.asarray(out="memmap") + else: + temp_file_name = create_tempfile_name(tempdir) + image_stack_ref = tiff_sequence.asarray( + out=f"{temp_file_name}_volume_{self.segment_name}.memmap" + ) + assert isinstance(image_stack_ref, np.memmap) + return image_stack_ref + + def load_mask_as_img(self) -> Image: + """Load the mask as a PIL.Image. + + Returns: + Image: The mask as an image. + """ + return Image.open(self.papyrus_mask_path).convert("1") + + def load_mask(self) -> np.ndarray: + """Load a fragment papyrus mask. + + Returns: + np.ndarray: The mask array. + """ + return np.array(self.load_mask_as_img(), dtype=bool) + + def load_ppm(self, dtype: np.dtype = np.float32) -> PPM: + """Load the associated PPM object.""" + ppm = PPM.from_path(str(self.ppm_path)) + ppm.data = ppm.data.astype(dtype) + return ppm + + def load_ppm_as_memmap(self) -> PPM: + """Load the associated PPM object with the data as a memory-map.""" + ppm = PPM.from_path(str(self.ppm_path), lazy_load=True) + ppm.load_ppm_data_as_memmap() + return ppm + + def load_mesh(self) -> trimesh.Trimesh if TYPE_CHECKING else None: + """Load the associated triangular mesh (obj) file.""" + import trimesh + + return trimesh.load_mesh(self.mesh_path) + + @property + def tiff_sequence(self) -> TiffSequence: + """The volumetric segment as a TIFF Sequence. + + Returns: + TiffSequence: The sequence of TIFF files representing the volumetric segment. + """ + return self._tiff_sequence + + @property + def zarr(self) -> ZarrFileSequenceStore: + """The volumetric segment as a Zarr. + + Returns: + ZarrFileSequenceStore: The Zarr file sequence store representing the volumetric segment. + """ + return self._zarr + + @cached_property + def shape(self) -> tuple[int, int, int]: + """The shape of the volumetric segment. + + Returns: + tuple[int, int, int]: The shape of the volumetric segment in the form (z, y, x). + """ + z_array = json.loads(self.zarr[".zarray"]) + return tuple(z_array["shape"]) + + @cached_property + def dtype(self) -> np.dtype: + """The data type of the volumetric segment. + + Returns: + np.dtype: The data type of the volumetric segment.. + """ + z_array = json.loads(self.zarr[".zarray"]) + return np.dtype(z_array["dtype"]) + + @property + def voxel_size_microns(self) -> float: + """Voxel size in microns. + + Raises: + NotImplementedError: This property must be implemented in a subclass. + + Returns: + float: The voxel size in microns. + """ + raise NotImplementedError + + @property + def voxel_size(self) -> pint.Quantity: + """Voxel size as a pint quantity. + + Raises: + NotImplementedError: This property must be implemented in a subclass. + + Returns: + pint.Quantity: The voxel size as a physical quantity. + """ + raise NotImplementedError + + @property + def n_slices(self) -> int: + """The number of slices (layers) of the volumetric segment. + + Returns: + int: The total number of slices in the segment. + """ + return self.shape[0] + + @property + def surface_shape(self) -> tuple[int, int]: + """The shape of the surface of the volumetric segment. + + Returns: + tuple[int, int]: The shape of the surface in the form (y, x). + """ + return self.shape[-2:] + + @property + def ndim(self) -> int: + """The number of dimensions of the volumetric segment. + + Returns: + int: The total number of dimensions, typically 3 for a volumetric segment. + """ + return len(self.shape) + + def __repr__(self): + return f"{self.__class__.__name__}(segment_name={self.segment_name}, shape={self.shape})" diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/__init__.py new file mode 100644 index 0000000..9a348b8 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/__init__.py @@ -0,0 +1 @@ +from .ink_detection import EvalPatchDataModule, PatchDataModule, PatchLitModel diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/__main__.py b/vesuvius_challenge_rnd/fragment_ink_detection/__main__.py new file mode 100644 index 0000000..6ca3ce6 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/__main__.py @@ -0,0 +1,4 @@ +from vesuvius_challenge_rnd.fragment_ink_detection.experiment_runner.run_experiment import cli_main + +if __name__ == "__main__": + raise SystemExit(cli_main()) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/download_model.py b/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/download_model.py new file mode 100644 index 0000000..f1e7c6b --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/download_model.py @@ -0,0 +1,72 @@ +import os +from argparse import ArgumentParser +from pathlib import Path + +from dotenv import load_dotenv + +from vesuvius_challenge_rnd.util import download_wandb_artifact, get_wandb_artifact, get_wandb_run + + +def download_model_and_config_from_wandb( + run_id: str, entity: str, project: str, alias: str = "best", output_dir: str | None = None +) -> tuple[Path, Path]: + if output_dir is None: + output_dir = "." + + # Download model. + model_artifact_name = f"model-{run_id}:{alias}" + ckpt_artifact = get_wandb_artifact(entity, project, model_artifact_name, artifact_type="model") + ckpt_artifact.download() + ckpt_dir_path = download_wandb_artifact(ckpt_artifact, output_dir=output_dir) + if output_dir is None: + ckpt_path = ckpt_dir_path / "artifacts" / model_artifact_name / "model.ckpt" + else: + ckpt_path = ckpt_dir_path / "model.ckpt" + print(f"Downloaded model checkpoint to {ckpt_path}.") + + # Download config. + run = get_wandb_run(entity, project, run_id) + if run is None: + raise ValueError(f"Run {run_id} not found for {entity}/{project}.") + + config_filename = "config_pl.yaml" + config_path = Path(output_dir) / config_filename + config_file = run.file(config_filename) + config_file.download(root=output_dir, exist_ok=True, replace=True) + print(f"Downloaded model config to {config_path.resolve()}") + + return ckpt_path, config_path + + +def _set_up_parser() -> ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = ArgumentParser(description="Download a model fom Weights and Biases (W&B).") + parser.add_argument("run_id", type=str, help="Run ID") + parser.add_argument("-a", "--alias", type=str, help="Model alias", default="best") + parser.add_argument( + "-e", "--entity", type=str, help="W&B entity", default=os.getenv("WANDB_ENTITY") + ) + parser.add_argument( + "-p", "--project", type=str, help="W&B project", default=os.getenv("WANDB_PROJECT") + ) + parser.add_argument( + "-o", "--output_dir", type=str, help="Model artifact output directory", default="downloads" + ) + return parser + + +def main() -> None: + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + download_model_and_config_from_wandb( + args.run_id, args.entity, args.project, alias=args.alias, output_dir=args.output_dir + ) + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/visualize_predictions.py b/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/visualize_predictions.py new file mode 100644 index 0000000..1b552a0 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/evaluation/visualize_predictions.py @@ -0,0 +1,224 @@ +from argparse import ArgumentParser +from pathlib import Path + +import numpy as np +import torch +from dotenv import load_dotenv +from matplotlib import pyplot as plt +from pytorch_lightning.core.saving import load_hparams_from_yaml +from tqdm.auto import tqdm + +from vesuvius_challenge_rnd.fragment_ink_detection import EvalPatchDataModule, PatchLitModel +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.prediction import ( + predict_with_model_on_fragment, +) + + +def visualize_predictions( + lit_model: PatchLitModel, + patch_surface_shape: tuple[int, int], + z_min: int = 27, + z_max: int = 37, + downsampling: int | None = None, + patch_stride: int | None = None, + predict_fragment_ind: list[int] | None = None, + batch_size: int = 4, + num_workers: int = 0, + thresh: float = 0.5, + save_dir: Path | None = Path("outputs"), +) -> None: + """Visualize predictions on a given model. + + Args: + lit_model (PatchLitModel): The model for predictions. + patch_surface_shape (tuple[int, int]): Shape of the surface patch. + z_min (int, optional): Minimum z-value. Defaults to 27. + z_max (int, optional): Maximum z-value. Defaults to 37. + downsampling (int | None, optional): Downsampling factor. Defaults to None. + patch_stride (int | None, optional): Stride of the patch. Defaults to None. + predict_fragment_ind (list[int] | None, optional): Indices for prediction fragment. Defaults to None. + batch_size (int, optional): Batch size for predictions. Defaults to 4. + num_workers (int, optional): Number of workers for parallelism. Defaults to 0. + thresh (float, optional): Threshold for predictions. Defaults to 0.5. + save_dir (Path | None, optional): Directory to save outputs. Defaults to Path("outputs"). + """ + if predict_fragment_ind is None: + predict_fragment_ind = [1, 2, 3] + + if patch_stride is None: + patch_stride = patch_surface_shape[0] // 2 + + for index in tqdm(predict_fragment_ind): + # Initialize data module. + data_module = EvalPatchDataModule( + predict_fragment_ind=[index], + z_min=z_min, + z_max=z_max, + patch_surface_shape=patch_surface_shape, + patch_stride=patch_stride, + downsampling=downsampling, + num_workers=num_workers, + batch_size=batch_size, + ) + + # Get and parse predictions. + y_proba_smoothed = predict_with_model_on_fragment( + lit_model, data_module, patch_surface_shape + ) + + # Show predictions next to ground truth. + mask = data_module.data_predict.masks[0] + ink_labels = data_module.data_predict.labels[0] + ir_img = data_module.data_predict.fragments[0].load_ir_img() + + fig, ax = create_pred_fig(index, thresh, mask, y_proba_smoothed, ink_labels, ir_img) + + if save_dir is not None: + # Optionally save the figure. + save_dir.mkdir(exist_ok=True) + file_name = f"prediction_{index}.png" + output_path = save_dir / file_name + fig.savefig(output_path) + print(f"Saved prediction to {output_path.resolve()}") + + plt.show() + + +def create_pred_fig( + index: int, + thresh: float, + mask: np.ndarray, + y_proba_smoothed: np.ndarray, + ink_labels: np.ndarray, + ir_img: np.ndarray, + figsize: tuple[int, int] = (25, 10), +) -> tuple[plt.Figure, plt.Axes]: + """Create a figure with predictions, ink labels, IR image, and ink prediction. + + Args: + index (int): Fragment index. + thresh (float): Threshold for predictions. + mask (np.ndarray): Mask array. + y_proba_smoothed (np.ndarray): Smoothed probability array. + ink_labels (np.ndarray): Ink label array. + ir_img (np.ndarray): Infrared image array. + figsize (tuple[int, int], optional): Figure size. Defaults to (25, 10). + + Returns: + tuple[plt.Figure, plt.Axes]: Figure and Axes objects for the plot. + """ + fig, ax = plt.subplots(1, 4, figsize=figsize) + ax1, ax2, ax3, ax4 = ax + fig.suptitle(f"Fragment {index}") + + ax1.set_title("Predictions") + ax1.imshow(mask) + + im2 = ax1.imshow(y_proba_smoothed) + plt.colorbar(im2, ax=ax1) + ax2.set_title("Ink labels") + ax2.imshow(ink_labels, cmap="binary") + + ax3.imshow(ir_img, cmap="gray") + ax3.set_title("IR image") + + ax4.imshow(y_proba_smoothed > thresh, cmap="binary") + ax4.set_title(f"Ink prediction (thresh={thresh})") + + return fig, ax + + +def load_model(ckpt_path: str, map_location: torch.device | None = None) -> PatchLitModel: + """Load a model from a checkpoint. + + Args: + ckpt_path (str): Checkpoint path. + map_location (torch.device | None, optional): Device mapping location. Defaults to None. + + Returns: + PatchLitModel: Loaded model. + """ + # Initialize model. + lit_model = PatchLitModel.load_from_checkpoint(ckpt_path, map_location=map_location) + lit_model.eval() + return lit_model + + +def parse_config(config_path: str | Path) -> dict: + """Parse the configuration from a given YAML file. + + Args: + config_path (str): Path to the configuration file. + + Returns: + dict: Dictionary containing configuration parameters. + """ + if not Path(config_path).exists(): + raise FileNotFoundError(f"Config not found at path {config_path}.") + + config = load_hparams_from_yaml(config_path) + + if len(config) == 0: + raise ValueError(f"Found empty config from path: {config_path}") + + patch_surface_shape = config["data"]["init_args"]["patch_surface_shape"] + z_min = config["data"]["init_args"]["z_min"] + z_max = config["data"]["init_args"]["z_max"] + downsampling = config["data"]["init_args"]["downsampling"] + return { + "patch_surface_shape": patch_surface_shape, + "z_min": z_min, + "z_max": z_max, + "downsampling": downsampling, + } + + +def main( + ckpt_path: str, + config_path: str, + patch_stride: int | None = None, + pred_frag_ind: list[int] | None = None, +) -> None: + """Main function to run the visualization. + + Args: + ckpt_path (str): Model checkpoint path. + config_path (str): Training configuration path. + patch_stride (int | None, optional): Patch stride. Defaults to None. + pred_frag_ind (list[int] | None, optional): Prediction fragment indices. Defaults to None. + """ + data_config = parse_config(config_path) + lit_model = load_model(ckpt_path) + print(f"patch_stride: {patch_stride}") + print(f"pred_frag_ind: {pred_frag_ind}") + visualize_predictions( + lit_model, patch_stride=patch_stride, predict_fragment_ind=pred_frag_ind, **data_config + ) + + +def _set_up_parser() -> ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = ArgumentParser(description="Visualize a patch model's predictions on fragments.") + parser.add_argument("ckpt_path", type=str, help="Model checkpoint path") + parser.add_argument("cfg_path", type=str, help="Training config path") + parser.add_argument("-s", "--patch_stride", default=None, type=int, help="Patch stride") + parser.add_argument( + "-f", "--pred_frag_ind", nargs="+", type=int, help="Prediction fragment indices" + ) + return parser + + +if __name__ == "__main__": + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + main( + args.ckpt_path, + args.cfg_path, + patch_stride=args.patch_stride, + pred_frag_ind=args.pred_frag_ind, + ) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/fast_dev_run.yaml b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/fast_dev_run.yaml new file mode 100644 index 0000000..2e50787 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/fast_dev_run.yaml @@ -0,0 +1,50 @@ +# pytorch_lightning==2.0.4 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + logger: null + precision: 16-mixed + benchmark: true + accumulate_grad_batches: 8 + enable_progress_bar: true + callbacks: + - class_path: pytorch_lightning.callbacks.RichProgressBar + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/loss + mode: min + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/loss + mode: min + fast_dev_run: true +model: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchLitModel + init_args: + loss_fn: + class_path: torch.nn.BCEWithLogitsLoss + f_maps: 8 + num_levels: 2 +data: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchDataModule + init_args: + train_fragment_ind: + - 2 + - 3 + val_fragment_ind: + - 1 + z_min: 27 + z_max: 37 + patch_surface_shape: + - 64 + - 64 + patch_stride: 1024 + downsampling: null + batch_size: 4 + num_workers: 0 diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/slow_dev_run.yaml b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/slow_dev_run.yaml new file mode 100644 index 0000000..8082093 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/slow_dev_run.yaml @@ -0,0 +1,58 @@ +# pytorch_lightning==2.0.4 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + project: fragment-ink-detection + job_type: train_debug + log_model: all + precision: 16-mixed + benchmark: true + limit_train_batches: 100 + limit_val_batches: 100 + max_epochs: 5 + accumulate_grad_batches: 8 + enable_progress_bar: true + callbacks: + - class_path: pytorch_lightning.callbacks.RichProgressBar + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/loss + mode: min + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/loss + mode: min + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbLogPredictionSamplesCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSavePRandROCCallback +model: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchLitModel + init_args: + f_maps: 8 + num_levels: 2 +data: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchDataModule + init_args: + train_fragment_ind: + - 2 + - 3 + val_fragment_ind: + - 1 + z_min: 27 + z_max: 37 + patch_surface_shape: + - 64 + - 64 + patch_stride: 1024 + downsampling: 2 + batch_size: 2 + num_workers: 0 diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/staging_run.yaml b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/staging_run.yaml new file mode 100644 index 0000000..560daf7 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/staging_run.yaml @@ -0,0 +1,87 @@ +# pytorch_lightning==2.0.4 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + project: fragment-ink-detection + job_type: train_debug + log_model: all + precision: 16-mixed + benchmark: true + max_epochs: 50 + accumulate_grad_batches: 8 + enable_progress_bar: true + callbacks: + - class_path: pytorch_lightning.callbacks.RichProgressBar + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/loss + mode: min + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/loss + mode: min + patience: 5 + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbLogPredictionSamplesCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSavePRandROCCallback +model: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchLitModel + init_args: + loss_fn: + class_path: segmentation_models_pytorch.losses.SoftBCEWithLogitsLoss + init_args: + weight: null + ignore_index: -100 + reduction: mean + smooth_factor: null + pos_weight: null + f_maps: 16 + num_levels: 4 + se_type_str: PE + reduction_ratio: 2 + depth_dropout: 0 + pool_fn: mean +data: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchDataModule + init_args: + train_fragment_ind: + - 2 + - 3 + val_fragment_ind: + - 1 + z_min: 24 + z_max: 40 + patch_surface_shape: + - 256 + - 256 + patch_stride: 128 + downsampling: 2 + batch_size: 2 + num_workers: 0 + slice_dropout_p: 0.1 + non_rigid: true + non_destructive: true +optimizer: + class_path: torch.optim.AdamW + init_args: + lr: 0.001 + betas: + - 0.9 + - 0.999 + eps: 1.0e-08 + weight_decay: 0.01 + amsgrad: false + maximize: false + foreach: null + capturable: false + differentiable: false + fused: null diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/unet_3d_to_2d.yaml b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/unet_3d_to_2d.yaml new file mode 100644 index 0000000..61a6924 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/configs/unet_3d_to_2d.yaml @@ -0,0 +1,77 @@ +# pytorch_lightning==2.0.4 +seed_everything: true +trainer: + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + project: fragment-ink-detection + job_type: train + log_model: all + accelerator: auto + strategy: auto + devices: auto + max_epochs: 100 + precision: 16-mixed + benchmark: true + accumulate_grad_batches: 8 + enable_progress_bar: true + callbacks: + - class_path: pytorch_lightning.callbacks.RichProgressBar + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/loss + mode: min + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/loss + mode: min + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbLogPredictionSamplesCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSavePRandROCCallback +model: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchLitModel + init_args: + f_maps: 64 + num_levels: 4 + se_type_str: PE + reduction_ratio: 2 + depth_dropout: 0.0 + pool_fn: mean +data: + class_path: vesuvius_challenge_rnd.fragment_ink_detection.PatchDataModule + init_args: + train_fragment_ind: + - 2 + - 3 + val_fragment_ind: + - 1 + z_min: 24 + z_max: 40 + patch_surface_shape: + - 512 + - 512 + patch_stride: 256 + downsampling: 2 + batch_size: 4 + num_workers: 0 + slice_dropout_p: 0.1 + non_rigid: true + non_destructive: true +optimizer: + class_path: torch.optim.AdamW + init_args: + lr: 0.001 + betas: + - 0.9 + - 0.999 + eps: 1.0e-08 + weight_decay: 0.01 + amsgrad: false + maximize: false + foreach: null + capturable: false + differentiable: false + fused: null diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/run_experiment.py b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/run_experiment.py new file mode 100644 index 0000000..3729387 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/run_experiment.py @@ -0,0 +1,56 @@ +import logging + +import segmentation_models_pytorch.losses # noqa: F401 +from dotenv import load_dotenv +from pytorch_lightning.cli import ArgsType, LightningCLI +from pytorch_lightning.loggers import WandbLogger + +import vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.schedulers # noqa: F401 +from vesuvius_challenge_rnd.fragment_ink_detection.experiment_runner.util import TrainerWandb +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.util import compile_if_possible + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s" +) + + +class FragmentLightningCLI(LightningCLI): + def before_fit(self): + """Method to be run before the fitting process.""" + load_dotenv() # take environment variables from .env. + + if isinstance(self.trainer.logger, WandbLogger): + # log gradients and model topology + self.trainer.logger.watch(self.model, log_graph=False) + + self.model = compile_if_possible(self.model) + + def after_fit(self): + """Method to be run after the fitting process.""" + checkpoint_callback = self.trainer.checkpoint_callback + if checkpoint_callback is not None: + logging.info(f"Best model saved to: {checkpoint_callback.best_model_path}") + + +def cli_main(args: ArgsType | None = None) -> FragmentLightningCLI: + """Main CLI entry point. + + Args: + args (ArgsType, optional): Command-line arguments. + + Returns: + FragmentLightningCLI: An instance of the FragmentLightningCLI class. + """ + return FragmentLightningCLI( + trainer_class=TrainerWandb, + trainer_defaults={"max_epochs": 100, "precision": "16-mixed", "benchmark": True}, + save_config_kwargs={ + "config_filename": "config_pl.yaml", + "overwrite": True, + }, + args=args, + ) + + +if __name__ == "__main__": + cli_main() diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/util.py b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/util.py new file mode 100644 index 0000000..5f4ced5 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/experiment_runner/util.py @@ -0,0 +1,31 @@ +from pytorch_lightning import Trainer +from pytorch_lightning.loggers import TensorBoardLogger, WandbLogger + + +class TrainerWandb(Trainer): + """Customized trainer for W&B logger that fixes artifacts from experiment dir + to root dir.""" + + @property + def log_dir(self) -> str | None: + """The directory for the current experiment. Use this to save images to, etc... + + .. code-block:: python + + def training_step(self, batch, batch_idx): + img = ... + save_img(img, self.trainer.log_dir) + """ + logger = self.logger + if logger is not None: + if isinstance(logger, WandbLogger): + dirpath = logger.experiment.dir + elif not isinstance(logger, TensorBoardLogger): + dirpath = logger.save_dir + else: + dirpath = logger.log_dir + else: + dirpath = self.default_root_dir + + dirpath = self.strategy.broadcast(dirpath) + return dirpath diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/__init__.py new file mode 100644 index 0000000..994fba7 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/__init__.py @@ -0,0 +1,2 @@ +from .data import EvalPatchDataModule, PatchDataModule +from .lit_models import PatchLitModel diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/callbacks.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/callbacks.py new file mode 100644 index 0000000..48fa4ac --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/callbacks.py @@ -0,0 +1,366 @@ +from typing import Any, Optional + +import logging +import os +import warnings +from pathlib import Path + +import numpy as np +import pytorch_lightning as pl +import torch +import torch.nn.functional as F +import wandb +from matplotlib import pyplot as plt +from PIL import Image +from pytorch_lightning import Callback +from pytorch_lightning.core.saving import load_hparams_from_yaml +from pytorch_lightning.loggers import WandbLogger +from pytorch_lightning.utilities.types import STEP_OUTPUT + +from vesuvius_challenge_rnd.data import Fragment +from vesuvius_challenge_rnd.data.visualization import create_gif_from_2d_single_channel_images +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_fragment_data_module import ( + AbstractFragmentValPatchDataset, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.patch_dataset import ( + PatchDataset, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.patch_aggregation import ( + patches_to_y_proba, +) + + +class CheckValDataModule(Callback): + """Base callback class that ensures a data module is available.""" + + def __init__(self): + self._data_module: pl.LightningDataModule | None = None + + def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule"): + """Check for data module at the beginning of the validation phase.""" + data_module = getattr(trainer, "datamodule", None) + if data_module is None: + raise ValueError("A data module must be provided for validation in this callback.") + self._data_module = data_module + + +class WandbLogPredictionSamplesCallback(CheckValDataModule): + """ + Callback to log prediction samples to Weights & Biases during validation. + """ + + def __init__(self, downsize_factor: int = 25): + """ + Initialize the callback for logging prediction samples to WandB. + + Args: + downsize_factor (int, optional): The downsize factor for the output image. Defaults to 25. + """ + super().__init__() + self.validation_y_proba_patches: list[torch.Tensor] = [] + self.validation_patch_positions: list[torch.Tensor] = [] + self.y_proba_smoothed_seq: list[np.ndarray] = [] + self.downsize_factor = downsize_factor + + def on_validation_batch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int = 0, + ) -> None: + """ + Collect validation data at the end of each validation batch. + + Args: + trainer (pl.Trainer): PyTorch Lightning Trainer instance. + pl_module (pl.LightningModule): The model being trained. + outputs (STEP_OUTPUT | None): Outputs of the validation step. + batch (Any): Current batch data. + batch_idx (int): Index of the current batch. + dataloader_idx (int, optional): Index of the current dataloader. Defaults to 0. + """ + if dataloader_idx == 0: + y_proba_patches = F.sigmoid(outputs["logits"]) + patch_positions = outputs["patch_pos"] + self.validation_y_proba_patches.extend(y_proba_patches) + self.validation_patch_positions.extend(patch_positions) + + def on_validation_epoch_end( + self, trainer: "pl.Trainer", pl_module: "pl.LightningModule" + ) -> None: + """ + Log prediction samples at the end of the validation epoch. + + Args: + trainer (pl.Trainer): PyTorch Lightning Trainer instance. + pl_module (pl.LightningModule): The model being trained. + """ + logger = trainer.logger + if isinstance(logger, WandbLogger): + y_proba_patches = np.stack( + [p.detach().cpu().float().numpy() for p in self.validation_y_proba_patches], axis=0 + ) + patch_positions = np.stack( + [p.detach().cpu().numpy() for p in self.validation_patch_positions], axis=0 + ) + + dataset_val = self._get_val_dataset_if_possible() + fragment = self._get_val_fragment_if_possible(dataset_val) + + mask = dataset_val.masks[0] + y_proba_smoothed = patches_to_y_proba( + y_proba_patches, patch_positions, mask, dataset_val.patch_shape + ) + self.y_proba_smoothed_seq.append(y_proba_smoothed) + + # Apply the Viridis colormap to the normalized probability map + cmap = plt.cm.get_cmap("viridis") + output_image = Image.fromarray((cmap(y_proba_smoothed) * 255).astype(np.uint8)) + + ink_labels_img = fragment.load_ink_labels_as_img() + new_shape = list(x // self.downsize_factor for x in fragment.surface_shape) + + # Create thumbnails. + ink_labels_img.thumbnail(new_shape, Image.ANTIALIAS) + output_image.thumbnail(new_shape, Image.ANTIALIAS) + mask_img = fragment.load_mask_as_img() + mask_img.thumbnail(new_shape, Image.ANTIALIAS) + image = wandb.Image( + output_image, + masks={ + "ground_truth": { + "mask_data": np.array(ink_labels_img), + "class_labels": {0: "not ink", 1: "ink"}, + }, + "papyrus_mask": { + "mask_data": np.array(mask_img) + 2, # Convert to {2, 3} for visualization. + "class_labels": {2: "not papyrus", 3: "papyrus"}, + }, + }, + caption=f"Predicted fragment {fragment.fragment_id} ink probabilities", + ) + + metrics = {"Masked predictions": [image]} + # TODO: step in w&b UI should be epoch. + if not trainer.sanity_checking: + logger.log_metrics(metrics) + else: + raise TypeError(f"Expected a WandbLogger. Found type {type(logger)}.") + + # Reset. + self.validation_y_proba_patches = [] + self.validation_patch_positions = [] + + def on_fit_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + logger = trainer.logger + if isinstance(logger, WandbLogger): + dataset_val = self._get_val_dataset_if_possible() + fragment = self._get_val_fragment_if_possible(dataset_val) + + logging.info(f"Saving gif of fragment {fragment.fragment_id} predictions...") + + frames = np.array(self.y_proba_smoothed_seq) + gif_path = ( + Path(trainer.log_dir) / f"y_probas_smoothed_fragment_{fragment.fragment_id}.gif" + ) + create_gif_from_2d_single_channel_images( + frames, + out_path=str(gif_path), + scale_factor=255, + downsample_factor=0.1, + duration=250, + downsample_order=2, + ) + logging.info( + f"Saved gif of fragment {fragment.fragment_id} predictions to {gif_path.resolve()}." + ) + + image = wandb.Image( + str(gif_path), + caption=f"Predicted fragment {fragment.fragment_id} ink probabilities", + ) + metrics = {"y_probas_smoothed": [image]} + if not trainer.sanity_checking: + logger.log_metrics(metrics) + else: + raise TypeError(f"Expected a `WandbLogger`. Found type {type(logger)}.") + + def _get_val_dataset_if_possible(self) -> PatchDataset: + datamodule = self._data_module + if not isinstance(datamodule, AbstractFragmentValPatchDataset): + raise TypeError( + f"data module must be an instance of {type(AbstractFragmentValPatchDataset).__name__}. " + f"Found a type of {type(datamodule)}." + ) + dataset = datamodule.val_fragment_dataset + return dataset + + def _get_val_fragment_if_possible(self, val_dataset: PatchDataset) -> Fragment: + fragment = val_dataset.fragments[0] + n_val_fragments = len(val_dataset.fragments) + if n_val_fragments > 1: + warnings.warn( + f"This callback only supports a single validation fragment. Found {n_val_fragments} " + f"fragments. It will default to using the first fragment ({fragment.fragment_id})." + ) + return fragment + + +class WandbSavePRandROCCallback(Callback): + """ + Callback to log Precision-Recall and ROC curves to Weights & Biases during validation. + """ + + def __init__(self, threshold: float = 0.5, ignore_index: int = -100): + """ + Initialize the callback for saving PR and ROC curves. + """ + if not (0 <= threshold <= 1): + raise ValueError("`threshold` must be in the unit interval [0, 1].") + self.validation_y_proba_patches: list[torch.Tensor] = [] + self.validation_y_true_patches: list[torch.Tensor] = [] + self.threshold = threshold + self.ignore_index = ignore_index + + def on_validation_batch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + outputs: STEP_OUTPUT | None, + batch: Any, + batch_idx: int, + dataloader_idx: int = 0, + ) -> None: + """ + Collect validation data at the end of each validation batch. + + Args: + trainer (pl.Trainer): PyTorch Lightning Trainer instance. + pl_module (pl.LightningModule): The model being trained. + outputs (STEP_OUTPUT | None): Outputs of the validation step. + batch (Any): Current batch data. + batch_idx (int): Index of the current batch. + dataloader_idx (int, optional): Index of the current dataloader. Defaults to 0. + """ + if dataloader_idx == 0: + y_proba_patches = F.sigmoid(outputs["logits"]) + y_true_patches = outputs["y"] + self.validation_y_proba_patches.extend(y_proba_patches) + self.validation_y_true_patches.extend(y_true_patches) + + def on_validation_epoch_end( + self, trainer: "pl.Trainer", pl_module: "pl.LightningModule" + ) -> None: + """ + Log PR and ROC curves to W&B at the end of the validation epoch. + + Args: + trainer (pl.Trainer): PyTorch Lightning Trainer instance. + pl_module (pl.LightningModule): The model being trained. + """ + logger = trainer.logger + if isinstance(logger, WandbLogger): + y_proba_patches = np.stack( + [p.detach().cpu().float().numpy() for p in self.validation_y_proba_patches], axis=0 + ) + y_true = np.stack( + [p.detach().cpu().numpy() for p in self.validation_y_true_patches], axis=0 + ) + + valid_indices = y_true != self.ignore_index + y_true = y_true[valid_indices] + y_proba_patches = y_proba_patches[valid_indices] + + y_proba_patches = y_proba_patches.flatten() + y_true = y_true.flatten() + y_probas_2d = np.vstack([1 - y_proba_patches, y_proba_patches]).T + labels = ["not ink", "ink"] + classes_to_plot = [1] # Plot ink only. + if not trainer.sanity_checking: + try: + if np.any(y_true > 0): # Only possible if we have some ink labels. + # Possibly binarize true labels. + unique_values = np.unique(y_true) + non_integer_in_y_true = np.any(unique_values != np.floor(unique_values)) + if non_integer_in_y_true: + y_true = (y_true >= self.threshold).astype(int) + wandb.termwarn( + f"Binarizing labels with thresh={self.threshold} because non-integer values are present in y_true.", + repeat=False, + ) + + logger.log_metrics( + { + "roc-curve": wandb.plot.roc_curve( + y_true, + y_probas_2d, + labels=labels, + classes_to_plot=classes_to_plot, + ) + } + ) + logger.log_metrics( + { + "pr-curve": wandb.plot.pr_curve( + y_true, + y_probas_2d, + labels=labels, + classes_to_plot=classes_to_plot, + ) + } + ) + else: + wandb.termwarn( + "Skipping plotting of ROC and PR curves due no ink being present in the validation set.", + repeat=False, + ) + except FileNotFoundError: + wandb.termwarn( + "File not found error while logging ROC curve and PR curve.", repeat=False + ) + else: + raise TypeError(f"Expected a WandbLogger. Found type {type(logger)}.") + + # Reset. + self.validation_y_proba_patches = [] + self.validation_y_true_patches = [] + + +class WandbSaveConfigCallback(Callback): + """ + Callback to save the configuration file as an artifact in Weights & Biases. + """ + + def __init__(self, config_filename: str = "config_pl.yaml"): + """ + Initialize the callback for saving configuration. + + Args: + config_filename (str, optional): Name of the config file to save. Defaults to "config_pl.yaml". + """ + self.config_filename = config_filename + + def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + """ + Save configuration at the start of training. + + Args: + trainer (pl.Trainer): PyTorch Lightning Trainer instance. + pl_module (pl.LightningModule): The model being trained. + """ + logger = trainer.logger + if isinstance(logger, WandbLogger): + config_path = os.path.join(trainer.log_dir, self.config_filename) + + if os.path.isfile(config_path): + artifact = wandb.Artifact(name="config", type="dataset") + artifact.add_file(config_path) + logger.experiment.log_artifact(artifact) + logging.info(f"Saved config ({config_path}) artifact to W&B: {artifact}") + + # Log config as hyperparameters to W&B config. + config = load_hparams_from_yaml(config_path, use_omegaconf=False) + trainer.logger.log_hyperparams({"config": config}) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/__init__.py new file mode 100644 index 0000000..d1ef30b --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/__init__.py @@ -0,0 +1,2 @@ +from .eval_patch_data_module import EvalPatchDataModule +from .patch_data_module import PatchDataModule diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_fragment_data_module.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_fragment_data_module.py new file mode 100644 index 0000000..e859f39 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_fragment_data_module.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.patch_dataset import ( + PatchDataset, +) + + +class AbstractFragmentValPatchDataset(ABC): + def __init__(self): + super().__init__() + + @property + @abstractmethod + def val_fragment_dataset(self) -> PatchDataset: + raise NotImplementedError diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_patch_data_module.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_patch_data_module.py new file mode 100644 index 0000000..5583466 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/base_patch_data_module.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import pytorch_lightning as pl + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR + + +class BasePatchDataModule(pl.LightningDataModule): + """A base data module for handling patches of fragment data. + + This class provides a foundational structure for data loading and preparation + in the context of PyTorch Lightning. It can be used as a base class for more + specific data modules for different tasks. + + Attributes: + data_dir (Path): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int): Minimum z-slice to include. Defaults to 27. + z_max (int): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int]): Shape of the patches. Defaults to (512, 512). + patch_stride (int): Stride for patch creation. Defaults to 256. + downsampling (int | None): Downsampling factor, 1 if None. Defaults to None. + num_workers (int): Number of workers for data loading. Defaults to 0. + batch_size (int): Batch size for data loading. Defaults to 4. + """ + + def __init__( + self, + data_dir: Path = FRAGMENT_DATA_DIR, + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + downsampling: int | None = None, + batch_size: int = 4, + num_workers: int = 0, + ): + """Initialize the BasePatchDataModule. + + Args: + data_dir (Path, optional): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + downsampling (int | None, optional): Downsampling factor, 1 if None. Defaults to None. + batch_size (int, optional): Batch size for data loading. Defaults to 4. + num_workers (int, optional): Number of workers for data loading. Defaults to 0. + """ + super().__init__() + self.data_dir = data_dir + self.z_min = z_min + self.z_max = z_max + self.patch_surface_shape = patch_surface_shape + self.patch_stride = patch_stride + self.downsampling = 1 if downsampling is None else downsampling + + self.num_workers = num_workers + self.batch_size = batch_size + + def prepare_data(self) -> None: + """Download data to disk. + + This method checks if the data directory is empty and raises an exception + if the fragment data is not found. + + Raises: + ValueError: If the data directory is empty. + """ + data_dir_is_empty = not any(self.data_dir.iterdir()) + if data_dir_is_empty: + raise ValueError( + f"Data directory ({self.data_dir}) is empty. Please download the data." + ) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/eval_patch_data_module.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/eval_patch_data_module.py new file mode 100644 index 0000000..0dae029 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/eval_patch_data_module.py @@ -0,0 +1,117 @@ +from pathlib import Path + +import albumentations as A +from albumentations.pytorch import ToTensorV2 +from torch.utils.data import DataLoader + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.patch_dataset import ( + PatchDataset, +) + + +class EvalPatchDataModule(BasePatchDataModule): + """A data module for handling evaluation and prediction of patches. + + This class extends BasePatchDataModule and includes functionality + specific to the evaluation and prediction stages, such as dataset + setup and transformation. + + Attributes: + predict_fragment_ind (list[int]): List of fragment indices for prediction. + data_predict (PatchDataset): Dataset for the prediction stage. + data_dir (Path): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int): Minimum z-slice to include. Defaults to 27. + z_max (int): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int]): Shape of the patches. Defaults to (512, 512). + patch_stride (int): Stride for patch creation. Defaults to 256. + downsampling (int | None): Downsampling factor, 1 if None. Defaults to None. + num_workers (int): Number of workers for data loading. Defaults to 0. + batch_size (int): Batch size for data loading. Defaults to 4. + """ + + def __init__( + self, + predict_fragment_ind: list[int], + data_dir: Path = FRAGMENT_DATA_DIR, + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + downsampling: int | None = None, + batch_size: int = 4, + num_workers: int = 0, + ): + """Initialize the EvalPatchDataModule. + + Args: + predict_fragment_ind (list[int]): List of fragment indices for prediction. + data_dir (Path, optional): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + downsampling (int | None, optional): Downsampling factor, 1 if None. Defaults to None. + batch_size (int, optional): Batch size for data loading. Defaults to 4. + num_workers (int, optional): Number of workers for data loading. Defaults to 0. + """ + super().__init__( + data_dir, + z_min, + z_max, + patch_surface_shape, + patch_stride, + downsampling, + batch_size, + num_workers, + ) + self.predict_fragment_ind = predict_fragment_ind + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "predict"). + """ + if stage == "predict" or stage is None: + self.data_predict = PatchDataset( + self.data_dir, + self.predict_fragment_ind, + transform=self.predict_transform(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min, + z_max=self.z_max, + ) + + def predict_transform(self) -> A.Compose: + """Define the transformations for the prediction stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + if self.downsampling != 1: + height = self.patch_surface_shape[0] // self.downsampling + width = self.patch_surface_shape[1] // self.downsampling + transforms += [A.Resize(height, width, always_apply=True)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + return A.Compose(transforms) + + def predict_dataloader(self) -> DataLoader: + """Create a data loader for the prediction stage. + + Returns: + DataLoader: A PyTorch DataLoader for prediction. + """ + return DataLoader( + self.data_predict, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=False, + pin_memory=True, + drop_last=False, + ) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/in_memory_surface_volume_dataset.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/in_memory_surface_volume_dataset.py new file mode 100644 index 0000000..48e47b8 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/in_memory_surface_volume_dataset.py @@ -0,0 +1,186 @@ +import bisect +from abc import ABC, abstractmethod +from collections.abc import Callable +from pathlib import Path + +import numpy as np +from patchify import patchify +from torch.utils.data import Dataset +from tqdm import tqdm + +from vesuvius_challenge_rnd.data.volumetric_segment import VolumetricSegment +from vesuvius_challenge_rnd.patching import patch_index_to_pixel_position + + +class SurfaceVolumeDataset(Dataset, ABC): + """A dataset class to handle surface volume data. + + This class is responsible for loading and managing volumetric segments + for training, validation, and testing purposes. It supports operations + like patch creation and indexing based on usability criteria. + + Attributes: + data_dir (Path): Directory containing the data. + segment_names (list[str]): List of segment names to load. + z_start (int): Minimum z-slice to include. + z_end (int): Maximum z-slice to include. + patch_shape (tuple[int, int]): Shape of the patches. + patch_stride (int): Stride for patch creation. + transform (Callable | None): Optional transformation to apply. + volumetric_segments (list[VolumetricSegment]): Loaded volumetric segments. + masks (list): Loaded masks for each segment. + img_stacks (list): Loaded image stacks for each segment. + usable_patch_position_arrs (np.ndarray): Usable patch positions. + patch_pos_intervals (list): Patch position intervals. + """ + + def __init__( + self, + volumetric_segments: list[VolumetricSegment], + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + transform: Callable | None = None, + prog_bar: bool = True, + ): + """Initialize the SurfaceVolumeDataset. + + Args: + data_dir (Path): Directory containing the data. + segment_ids (list[int]): List of segment IDs to load. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + transform (Callable | None, optional): Optional transformation to apply. Defaults to None. + prog_bar (bool, optional): Whether to show a progress bar while loading. Defaults to True. + """ + self.transform = transform + + self.z_start = z_min + self.z_end = z_max + self.patch_shape = patch_surface_shape + self.patch_stride = patch_stride + + self.volumetric_segments = volumetric_segments + + self.masks = [segment.load_mask() for segment in self.volumetric_segments] + img_stack_iter = ( + tqdm(self.volumetric_segments, desc="Loading volumes...") + if prog_bar + else self.volumetric_segments + ) + self.img_stacks = [ + segment.load_volume_as_memmap(z_start=self.z_start, z_end=self.z_end) + for segment in img_stack_iter + ] + + ( + self.usable_patch_position_arrs, + original_patch_pos_sizes, + ) = self.set_up_patch_positions() + self.patch_pos_intervals = np.cumsum([0] + original_patch_pos_sizes).tolist() + + def __len__(self) -> int: + """Get the number of usable patches. + + Returns: + int: The number of usable patches in the dataset. + """ + return self.usable_patch_position_arrs.shape[0] + + def create_usable_patch_map(self, fragment_idx: int) -> np.ndarray: + """Create a usable patch map for a specific fragment. + + Args: + fragment_idx (int): Index of the fragment to create the patch map for. + + Returns: + np.ndarray: A boolean map indicating whether each patch is usable. + """ + papyrus_mask = self.masks[fragment_idx] + mask_patches = patchify(papyrus_mask, patch_size=self.patch_shape, step=self.patch_stride) + # Create a usable patch map where true indicates that the patch is usable for training/validation/test and + # false indicates there is not enough papyrus. + usable_patch_map = np.empty(shape=(mask_patches.shape[:2]), dtype=bool) + for i in range(mask_patches.shape[0]): + for j in range(mask_patches.shape[1]): + mask_patch = mask_patches[i, j] + usable_patch_map[ + i, j + ] = mask_patch.any() # A patch is "usable" if there are ANY papyrus pixels present. + return usable_patch_map + + def create_usable_patch_position_arr(self, usable_patch_map: np.ndarray) -> np.ndarray: + """Create an array of usable patch positions based on the patch map. + + Args: + usable_patch_map (np.ndarray): A boolean map indicating patch usability. + + Returns: + np.ndarray: An array of usable patch positions. + """ + usable_patch_positions = [] + for i in range(usable_patch_map.shape[0]): + for j in range(usable_patch_map.shape[1]): + if usable_patch_map[i, j]: + position = patch_index_to_pixel_position( + i, j, self.patch_shape, self.patch_stride + ) + usable_patch_positions.append(position) + + return np.array(usable_patch_positions) + + def set_up_patch_positions(self) -> tuple: + """Set up the usable patch positions across all segments. + + Returns: + tuple: A tuple containing an array of usable patch positions and a list of original patch position sizes. + """ + usable_patch_position_arrs = [] + original_patch_pos_sizes = [] + for i in range(self.n_segments): + usable_patch_map = self.create_usable_patch_map(i) + usable_patch_position_arr = self.create_usable_patch_position_arr(usable_patch_map) + original_patch_pos_sizes.append(usable_patch_position_arr.shape[0]) + usable_patch_position_arrs.append(usable_patch_position_arr) + + usable_patch_position_arrs = np.vstack(usable_patch_position_arrs) + return usable_patch_position_arrs, original_patch_pos_sizes + + def idx_to_segment_idx(self, index: int) -> int: + """Convert a global index to a segment index. + + Args: + index (int): Global index in the dataset. + + Returns: + int: Corresponding segment index. + """ + return bisect.bisect_right(self.patch_pos_intervals, index) - 1 + + @property + def n_slices(self) -> int: + """Get the number of surface volume layers used. + + Returns: + int: The number of surface volume layers. + """ + return self.z_end - self.z_start + + @property + def segment_names(self) -> list[str]: + return [segment.segment_name for segment in self.volumetric_segments] + + @property + def n_segments(self) -> int: + """Get the number of volumetric segments. + + Returns: + int: The number of volumetric segments. + """ + return len(self.segment_names) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(segment_names={self.segment_names})" diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_data_module.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_data_module.py new file mode 100644 index 0000000..4d2a030 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_data_module.py @@ -0,0 +1,224 @@ +from pathlib import Path + +import albumentations as A +from albumentations.pytorch import ToTensorV2 +from torch.utils.data import DataLoader +from tqdm import tqdm + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR +from vesuvius_challenge_rnd.data import Fragment +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import FragmentPreprocessorBase +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_fragment_data_module import ( + AbstractFragmentValPatchDataset, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.patch_dataset import ( + PatchDataset, +) + + +class PatchDataModule(BasePatchDataModule, AbstractFragmentValPatchDataset): + """A data module for handling training and validation of patches. + + This class extends BasePatchDataModule and includes functionality + specific to the training and validation stages, such as dataset + setup and transformation. + + Attributes: + train_fragment_ind (list[int]): List of fragment indices for training. + val_fragment_ind (list[int]): List of fragment indices for validation. + data_train (PatchDataset): Dataset for the training stage. + data_val (PatchDataset): Dataset for the validation stage. + data_dir (Path): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int): Minimum z-slice to include. Defaults to 27. + z_max (int): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int]): Shape of the patches. Defaults to (512, 512). + patch_stride (int): Stride for patch creation. Defaults to 256. + downsampling (int | None): Downsampling factor, 1 if None. Defaults to None. + num_workers (int | None): Number of workers for data loading. Defaults to 0. + batch_size (int): Batch size for data loading. Defaults to 4. + """ + + def __init__( + self, + train_fragment_ind: list[int], + val_fragment_ind: list[int], + data_dir: Path = FRAGMENT_DATA_DIR, + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + downsampling: int | None = None, + batch_size: int = 4, + num_workers: int | None = 0, + slice_dropout_p: float = 0, + non_destructive: bool = True, + non_rigid: bool = True, + fragment_preprocessor: FragmentPreprocessorBase | None = None, + ): + """Initialize the PatchDataModule. + + Args: + train_fragment_ind (list[int]): List of fragment indices for training. + val_fragment_ind (list[int]): List of fragment indices for validation. + data_dir (Path, optional): Directory containing the fragment data. Defaults to FRAGMENT_DATA_DIR. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + downsampling (int | None, optional): Downsampling factor, 1 if None. Defaults to None. + batch_size (int, optional): Batch size for data loading. Defaults to 4. + num_workers (int | None, optional): Number of workers for data loading. Defaults to 0. + slice_dropout_p (int, optional): The probability of applying slice dropout. Defaults to 0. + non_destructive (bool, optional): Apply non-destructive transformations. Defaults to True. + non_rigid (bool, optional): Apply non-rigid transformations. Defaults to False. + fragment_preprocessor (FragmentPreprocessorBase, optional): a fragment preprocessor. Defaults to None. + + """ + super().__init__( + data_dir, + z_min, + z_max, + patch_surface_shape, + patch_stride, + downsampling, + batch_size, + num_workers, + ) + self.train_fragment_ind = train_fragment_ind + self.val_fragment_ind = val_fragment_ind + self.non_destructive = non_destructive + self.non_rigid = non_rigid + self.slice_dropout_p = slice_dropout_p + self.fragment_preprocessor = fragment_preprocessor + self.processed_data_dir = ( + self.data_dir + if self.fragment_preprocessor is None + else self.fragment_preprocessor.preprocessing_dir / FRAGMENT_DATA_DIR.name + ) + + def prepare_data(self) -> None: + super().prepare_data() + if self.fragment_preprocessor is not None: + all_fragment_ind = self.train_fragment_ind + self.val_fragment_ind + fragments = [Fragment(fid, fragment_dir=self.data_dir) for fid in all_fragment_ind] + for fragment in tqdm(fragments, desc="Preprocessing fragments..."): + self.fragment_preprocessor(fragment) + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "fit"). + """ + if stage == "fit" or stage is None: + self.data_train = PatchDataset( + self.processed_data_dir, + self.train_fragment_ind, + transform=self.train_transform(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min, + z_max=self.z_max, + ) + + self.data_val = PatchDataset( + self.processed_data_dir, + self.val_fragment_ind, + transform=self.validation_transform(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min, + z_max=self.z_max, + ) + + def train_transform(self) -> A.Compose: + """Define the transformations for the training stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + height = self.patch_surface_shape[0] // self.downsampling + width = self.patch_surface_shape[1] // self.downsampling + if self.downsampling != 1: + transforms += [A.Resize(height, width, always_apply=True)] + + if self.non_destructive: + non_destructive_transformations = [ # Dihedral group D4 + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.5), + A.RandomRotate90(p=0.5), # Randomly rotates by 0, 90, 180, 270 degrees + A.Transpose(p=0.5), # Switch X and Y axis. + ] + transforms += non_destructive_transformations + + if self.non_rigid: + non_rigid_transformations = A.OneOf( + [ + A.GridDistortion(p=0.5), + A.MotionBlur(p=0.5), + A.OpticalDistortion(p=0.5), + ], + p=0.5, + ) + transforms += non_rigid_transformations + + transforms += [ + A.ChannelDropout(channel_drop_range=(1, 2), p=self.slice_dropout_p), + A.RandomGamma(p=0.5), + A.Normalize(mean=[0], std=[1]), + ToTensorV2(transpose_mask=True), + ] + + return A.Compose(transforms) + + def validation_transform(self) -> A.Compose: + """Define the transformations for the validation stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + if self.downsampling != 1: + height = self.patch_surface_shape[0] // self.downsampling + width = self.patch_surface_shape[1] // self.downsampling + transforms += [A.Resize(height, width, always_apply=True)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + return A.Compose(transforms) + + def train_dataloader(self) -> DataLoader: + """Create a data loader for the training stage. + + Returns: + DataLoader: A PyTorch DataLoader for training. + """ + return DataLoader( + self.data_train, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=True, + pin_memory=True, + drop_last=True, + ) + + def val_dataloader(self) -> DataLoader: + """Create a data loader for the validation stage. + + Returns: + DataLoader: A PyTorch DataLoader for validation. + """ + return DataLoader( + self.data_val, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=False, + pin_memory=True, + drop_last=False, + ) + + @property + def val_fragment_dataset(self) -> PatchDataset: + return self.data_val diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_dataset.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_dataset.py new file mode 100644 index 0000000..8be6873 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/data/patch_dataset.py @@ -0,0 +1,115 @@ +from collections.abc import Callable +from pathlib import Path + +import numpy as np +import torch + +from vesuvius_challenge_rnd.data import Fragment, preprocess_subvolume +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.in_memory_surface_volume_dataset import ( + SurfaceVolumeDataset, +) + + +class PatchDataset(SurfaceVolumeDataset): + """A subclass of SurfaceVolumeDataset for handling patches of surface volume data. + + This class extends SurfaceVolumeDataset to include the loading and processing + of patches, including ink labels. + + Attributes: + data_dir (Path): Directory containing the data. + fragment_ind (list[int]): List of fragment indices to load. + z_min (int): Minimum z-slice to include. + z_max (int): Maximum z-slice to include. + patch_surface_shape (tuple[int, int]): Surface (height x width) shape of the patches. + patch_stride (int): Stride for patch creation. + transform (Callable | None): Optional transformation to apply. + prog_bar (bool): Whether to show a progress bar while loading. + labels (list): Loaded ink labels for each fragment. + """ + + def __init__( + self, + data_dir: Path, + fragment_ind: list[int], + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + transform: Callable | None = None, + prog_bar: bool = True, + ): + """Initialize the PatchDataset. + + Args: + data_dir (Path): Directory containing the data. + fragment_ind (list[int]): List of fragment indices to load. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + transform (Callable | None, optional): Optional transformation to apply. Defaults to None. + prog_bar (bool, optional): Whether to show a progress bar while loading. Defaults to True. + """ + self._fragments = [Fragment(fid, fragment_dir=data_dir) for fid in fragment_ind] + super().__init__( + self._fragments, + z_min, + z_max, + patch_surface_shape, + patch_stride, + transform, + prog_bar, + ) + self.data_dir = data_dir + self.labels = [fragment.load_ink_labels() for fragment in self.fragments] + + def __getitem__(self, index: int) -> tuple: + """Get a patch, corresponding label, and patch position by index. + + Args: + index (int): Index of the patch to retrieve. + + Returns: + tuple: A tuple containing the patch, label, and patch position. + """ + fragment_idx = self.idx_to_segment_idx(index) + + patch_pos = self.usable_patch_position_arrs[index] + ((y0, x0), (y1, x1)) = patch_pos + + patch = preprocess_subvolume( + self.img_stacks[fragment_idx][:, y0:y1, x0:x1], slice_dim_last=True + ) + + patch_label = self.labels[fragment_idx][y0:y1, x0:x1].astype(np.uint8) + + if self.transform is not None: + transformed = self.transform(image=patch, mask=patch_label) + patch = transformed["image"] + patch_label = transformed["mask"] + + return patch, patch_label, patch_pos + + @property + def fragments(self) -> list[Fragment]: + """Get the fragments. + + Returns: + list[Fragment]: The list of fragments. + """ + return self._fragments + + @property + def fragment_ind(self) -> list[int]: + """Get the fragment indices, alias for segment IDs. + + Returns: + list[int]: List of fragment indices. + """ + return self.segment_names + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(data_dir={self.data_dir}, fragment_ind={self.fragment_ind})" + ) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/ensemble.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/ensemble.py new file mode 100644 index 0000000..f2a90f8 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/ensemble.py @@ -0,0 +1,40 @@ +import numpy as np + +from vesuvius_challenge_rnd.fragment_ink_detection import EvalPatchDataModule, PatchLitModel +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.prediction import ( + predict_with_model_on_fragment, +) + + +def ensemble_prediction(*y_proba_smoothed: np.ndarray) -> np.ndarray: + """Compute the ensemble prediction by averaging smoothed probabilities. + + Args: + *y_proba_smoothed (np.ndarray): Smoothed probabilities for each model in the ensemble. + + Returns: + np.ndarray: The averaged probability array representing the ensemble prediction. + """ + return np.dstack(y_proba_smoothed).mean(2) + + +def ensemble_predict_on_fragment( + lit_models: list[PatchLitModel], + data_module: EvalPatchDataModule, + patch_surface_shape: tuple[int, int], +) -> np.ndarray: + """Perform ensemble prediction on a fragment using multiple trained models. + + Args: + lit_models (list[PatchLitModel]): List of trained models to use for ensemble prediction. + data_module (EvalPatchDataModule): Data module containing evaluation data for the fragment. + patch_surface_shape (tuple[int, int]): Shape of the patch surface. + + Returns: + np.ndarray: The ensemble prediction for the fragment. + """ + y_proba_smoothed = [ + predict_with_model_on_fragment(model, data_module, patch_surface_shape) + for model in lit_models + ] + return ensemble_prediction(*y_proba_smoothed) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/patch_aggregation.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/patch_aggregation.py new file mode 100644 index 0000000..b1c66a4 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/patch_aggregation.py @@ -0,0 +1,141 @@ +import albumentations as A +import numpy as np + + +def resize_predictions(y_proba: np.ndarray, patch_surface_shape: tuple[int, int]) -> np.ndarray: + """Resize patches of predicted probabilities to the specified surface shape. + + Args: + y_proba (np.ndarray): Array of predicted probabilities. + patch_surface_shape (tuple[int, int]): Target shape for resizing. + + Returns: + np.ndarray: Resized array of predicted probabilities. + """ + predict_transform = A.Resize(*patch_surface_shape, always_apply=True) + y_proba_resized = np.empty((y_proba.shape[0], *patch_surface_shape)) + + for i, y_proba_patch in enumerate(y_proba): + transformed = predict_transform(image=y_proba_patch) + y_proba_resized[i] = transformed["image"] + return y_proba_resized + + +def average_y_proba_patches( + y_proba_patches_resized: np.ndarray, patch_positions: np.ndarray, img_shape: tuple[int, int] +) -> np.ndarray: + """Compute the arithmetic mean of predicted probabilities in overlapping patches. + + Args: + y_proba_patches_resized (np.ndarray): Resized patches of predicted probabilities. + patch_positions (np.ndarray): Positions of the patches. + img_shape (tuple[int, int]): Shape of the original image. + + Returns: + np.ndarray: Averaged predicted probabilities. + """ + overlap_count = np.zeros(img_shape, dtype=float) + y_proba_sum = np.zeros(img_shape, dtype=float) + for y_proba_patch, ((y1, x1), (y2, x2)) in zip(y_proba_patches_resized, patch_positions): + overlap_count[y1:y2, x1:x2] += 1 + y_proba_sum[y1:y2, x1:x2] += y_proba_patch + + y_proba_smoothed = np.divide(y_proba_sum, overlap_count, where=(overlap_count != 0)) + return y_proba_smoothed + + +def patches_to_y_proba( + y_proba_patches: np.ndarray, + patch_positions: np.ndarray, + mask: np.ndarray, + patch_surface_shape: tuple[int, int], +) -> np.ndarray: + """Convert predicted (overlapping) patch probabilities to predicted probabilities for the entire image. + + Args: + y_proba_patches (np.ndarray): Array of predicted probability of shape (num patches, down-sampled height, down-sampled width). + patch_positions (np.ndarray): Array of predicted probability of shape (num patches, 2, 2). + mask (np.ndarray): The papyrus mask of shape (height, width). + patch_surface_shape (tuple[int, int]): The shape of the patch surface. + + Returns: + np.ndarray: Predicted probabilities for the entire image. + + Raises: + ValueError: If any dimension mismatch is found. + """ + # Validate args. + if y_proba_patches.ndim != 3: + raise ValueError( + f"Expected `y_proba_patches` to have 3 dimensions. Found {y_proba_patches.ndim}." + ) + if patch_positions.ndim != 3: + raise ValueError( + f"Expected `patch_positions` to have 3 dimensions. Found {patch_positions.ndim}." + ) + + if y_proba_patches.shape[0] != patch_positions.shape[0]: + raise ValueError( + f"Expected first dimension of `y_proba_patches` to have the same as the first dim of `patch_positions` " + f"dimensions. Found `y_proba_patches.shape[0]`={y_proba_patches.shape[0]} and " + f"`patch_positions.shape[0]`={patch_positions.shape[0]}." + ) + + y_proba_patches_resized = resize_predictions(y_proba_patches, patch_surface_shape) + + y_proba_smoothed = average_y_proba_patches(y_proba_patches_resized, patch_positions, mask.shape) + + # Ensure masked region is zero. + y_proba_smoothed[~mask] = 0.0 + + return y_proba_smoothed + + +def parse_predictions_without_labels(predictions: list[tuple]) -> tuple[np.ndarray, np.ndarray]: + """Parse patch predictions without labels. + + Args: + predictions (list[tuple]): List of patch predictions. + + Returns: + tuple[np.ndarray, np.ndarray]: Tuple containing arrays of probability predictions and patch positions. + """ + # Assumes the dataset returns two items: y_proba_patch and patch position. + y_proba_patches = np.vstack([p[0].detach().cpu().numpy() for p in predictions]) + patch_positions = np.vstack([p[1] for p in predictions]) + return y_proba_patches, patch_positions + + +def parse_predictions_with_labels( + predictions: list[tuple], +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Parse patch predictions with labels. + + Args: + predictions (list[tuple]): List of patch predictions. + + Returns: + tuple[np.ndarray, np.ndarray, np.ndarray]: Tuple containing arrays of probability predictions, true labels, and patch positions. + """ + # Assumes the dataset returns three items: y_proba_patch, patch ink label, and patch position. + y_proba_patches = np.vstack([p[0].detach().cpu().numpy() for p in predictions]) + y_true_patches = np.vstack([p[1].detach().cpu().numpy() for p in predictions]) + patch_positions = np.vstack([p[2] for p in predictions]) + return y_proba_patches, y_true_patches, patch_positions + + +def predictions_to_y_proba( + predictions: list[tuple], mask: np.ndarray, patch_surface_shape: tuple[int, int] +) -> np.ndarray: + """Convert patch predictions to fragment predictions. + + Args: + predictions (list[tuple]): List of patch predictions. + mask (np.ndarray): The papyrus mask of shape (height, width). + patch_surface_shape (tuple[int, int]): The shape of the patch surface. + + Returns: + np.ndarray: Predicted probabilities for the entire fragment. + """ + y_proba_patches, patch_positions = parse_predictions_without_labels(predictions) + return patches_to_y_proba(y_proba_patches, patch_positions, mask, patch_surface_shape) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/prediction.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/prediction.py new file mode 100644 index 0000000..19d7664 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/inference/prediction.py @@ -0,0 +1,65 @@ +"""Single-model prediction.""" +import numpy as np +from pytorch_lightning import LightningDataModule, LightningModule, Trainer +from pytorch_lightning.callbacks import RichProgressBar + +from vesuvius_challenge_rnd.fragment_ink_detection import EvalPatchDataModule, PatchLitModel +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.patch_aggregation import ( + parse_predictions_without_labels, + patches_to_y_proba, +) + + +def get_predictions( + lit_model: LightningModule, data_module: LightningDataModule, prog_bar: bool = True +) -> list: + """Retrieve predictions using the given model and data module. + + Args: + lit_model (LightningModule): Trained model to make predictions. + data_module (LightningDataModule): Data module containing evaluation data. + prog_bar (bool, optional): Whether to display a progress bar. Defaults to True. + + Returns: + list: List of predictions made by the model. + """ + callbacks = [] + + if prog_bar: + callbacks.append(RichProgressBar()) + trainer = Trainer(callbacks=callbacks) + predictions = trainer.predict(lit_model, datamodule=data_module) + return predictions + + +def predict_with_model_on_fragment( + lit_model: PatchLitModel, + data_module: EvalPatchDataModule, + patch_surface_shape: tuple[int, int], + prog_bar: bool = True, +) -> np.ndarray: + """Predict with a patch model on a given fragment. + + This function retrieves predictions from the model and aggregates them into a smooth probability map + for the entire fragment. + + Args: + lit_model (PatchLitModel): Trained model to make predictions. + data_module (EvalPatchDataModule): Data module containing evaluation data for the fragment. + patch_surface_shape (tuple[int, int]): Shape of the patch surface. + prog_bar (bool, optional): Whether to display a progress bar. Defaults to True. + + Returns: + np.ndarray: The smoothed probability map for the fragment. + """ + predictions = get_predictions(lit_model, data_module, prog_bar) + + y_proba_patches, patch_positions = parse_predictions_without_labels(predictions) + + # Aggregate patch predictions. + mask = data_module.data_predict.masks[0] + y_proba_smoothed = patches_to_y_proba( + y_proba_patches, patch_positions, mask, patch_surface_shape + ) + + return y_proba_smoothed diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/__init__.py new file mode 100644 index 0000000..c12f797 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/__init__.py @@ -0,0 +1 @@ +from .patch_lit_model import PatchLitModel diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/patch_lit_model.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/patch_lit_model.py new file mode 100644 index 0000000..98d0d87 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/lit_models/patch_lit_model.py @@ -0,0 +1,195 @@ +from typing import Literal + +import pytorch_lightning as pl +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.optim import Optimizer +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models import UNet3Dto2D + +BatchType = tuple[torch.Tensor, torch.Tensor, torch.Tensor] +BatchTypePrediction = tuple[torch.Tensor, torch.Tensor] + + +class PatchLitModel(pl.LightningModule): + """LightningModule for handling Patch-based 3D to 2D UNet for ink detection. + + Attributes: + model: The underlying 3D to 2D UNet model. + loss_fn: Binary Cross Entropy loss with logits. + lr: Learning rate. + train_metrics: Metrics for training phase. + val_metrics: Metrics for validation phase. + """ + + def __init__( + self, + loss_fn: nn.Module | None = None, + lr: float = 1e-3, + thresh: float = 0.5, + f_maps: int = 64, + layer_order: str = "gcr", + num_groups: int = 8, + num_levels: int = 4, + conv_padding: int | tuple[int, ...] = 1, + se_type_str: str | None = "CSE3D", + reduction_ratio: int = 2, + depth_dropout: float = 0.0, + pool_fn: Literal["mean", "max"] = "mean", + ): + """Initializes the PatchLitModel. + + Args: + loss_fn (nn.Module): Loss function. Defaults to nn.BCEWithLogitsLoss(). + lr (float): Learning rate. Defaults to 1e-3. + thresh (float): Threshold for binary classification. Defaults to 0.5. + f_maps (int): Number of feature maps. Defaults to 64. + layer_order (str): Order of layer in the model. Defaults to "gcr". + num_groups (int): Number of groups for grouped convolutions. Defaults to 8. + num_levels (int): Number of levels in the model. Defaults to 4. + conv_padding (int | tuple[int, ...]): Convolution padding. Defaults to 1. + se_type_str (str | None): Squeeze-and-Excitation type string. Defaults to "CSE3D". + reduction_ratio (int): Reduction ratio for squeeze-and-excitation. Defaults to 2. + depth_dropout (float): Depth dropout value. Defaults to 0.0. + pool_fn (Literal["mean", "max"]): Pooling function to be used ("mean" or "max"). Defaults to "mean". + """ + super().__init__() + if loss_fn is None: + loss_fn = nn.BCEWithLogitsLoss() + + self.model = UNet3Dto2D( + f_maps=f_maps, + layer_order=layer_order, + num_groups=num_groups, + num_levels=num_levels, + conv_padding=conv_padding, + se_type_str=se_type_str, + reduction_ratio=reduction_ratio, + depth_dropout=depth_dropout, + pool_fn=pool_fn, + output_features=False, + ) + self.loss_fn = loss_fn + self.lr = lr + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=thresh), + BinaryFBetaScore(beta=0.5, threshold=thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + self.save_hyperparameters(ignore=["loss_fn"]) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Defines the forward pass for the model. + + Args: + x (torch.Tensor): Input tensor. + + Returns: + torch.Tensor: Logits. + """ + x = x.unsqueeze(1) # Add dummy channel dimension because it's grayscale. + outputs = self.model(x) + logits = outputs.logits.squeeze(1) # Remove dummy channel + return logits + + def _step(self, batch: BatchType) -> BatchType: + """Handles a single step of training or validation. + + This method processes a given batch to obtain logits, labels, and patch positions. + + Args: + batch (tuple): Batch of data. + + Returns: + tuple: Logits, labels, and patch positions. + """ + x, y, patch_pos = batch + y = y.float() + logits = self(x) + return logits, y, patch_pos + + def training_step(self, batch: BatchType, batch_idx: int) -> torch.Tensor: + """Training step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + + Returns: + torch.Tensor: Loss value. + """ + logits, y, _ = self._step(batch) + loss = self.loss_fn(logits, y) + self.log("train/loss", loss) + train_metrics_output = self.train_metrics(logits, y) + self.log_dict(train_metrics_output) + return loss + + def validation_step(self, batch: BatchType, batch_idx: int) -> dict[str, torch.Tensor]: + """Validation step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + + Returns: + dict: Validation outputs. + """ + logits, y, patch_pos = self._step(batch) + loss = self.loss_fn(logits, y) + self.log("val/loss", loss) + + y = y.int() + self.val_metrics.update(logits, y) + return {"logits": logits, "y": y, "loss": loss, "patch_pos": patch_pos} + + def on_validation_epoch_end(self) -> None: + """Method called at the end of a validation epoch.""" + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch: BatchType | BatchTypePrediction, batch_idx: int, dataloader_idx: int = 0 + ) -> BatchTypePrediction: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities, labels, and patch positions. + """ + if len(batch) == 3: + logits, _, patch_pos = self._step(batch) + elif len(batch) == 2: + x, patch_pos = batch + logits = self(x) + else: + raise ValueError("Expected number of items in a batch to be 2 or 3.") + + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self) -> Optimizer: + """Configures the optimizers. + + Returns: + torch.optim.Optimizer: Optimizer for the model. + """ + optimizer = torch.optim.Adam(self.parameters(), lr=self.lr) + return optimizer diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/__init__.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/__init__.py new file mode 100644 index 0000000..da09323 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/__init__.py @@ -0,0 +1 @@ +from .unet_3d_to_2d import UNet3Dto2D diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/depth_pooling.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/depth_pooling.py new file mode 100644 index 0000000..d4324d5 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/depth_pooling.py @@ -0,0 +1,170 @@ +from typing import Literal + +from functools import partial + +import torch +import torch.nn as nn + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.squeeze_and_excitation_3d import ( + SELayer3D, + se_layer3d_factory, +) + + +class DepthPoolingBlock(nn.Module): + """Block for pooling along the depth dimension with optional squeeze-and-excitation layer and dropout. + + Args: + input_channels (int): Number of input channels. + se_type (SELayer3D | None, optional): Squeeze-and-excitation layer type. Defaults to SELayer3D.CSSE3D. + reduction_ratio (int, optional): Reduction ratio for squeeze-and-excitation layer. Defaults to 2. + dim (int, optional): Slice/depth dimension. Defaults to 4. + depth_dropout (float, optional): 3D dropout rate. Defaults to 0.0. + pool_fn (Literal["mean", "max"], optional): Pooling function to use along the depth dimension. Defaults to "mean". + """ + + def __init__( + self, + input_channels: int, + se_type: SELayer3D | None = SELayer3D.CSSE3D, + reduction_ratio: int = 2, + dim: int = 2, + depth_dropout: float = 0.0, + pool_fn: Literal["mean", "max", "attention"] = "mean", + depth: int | None = None, + height: int | None = None, + width: int | None = None, + ): + super().__init__() + self.dim = dim # Slice/depth dimension. + + if se_type is not None: + se_layer = se_layer3d_factory( + se_type, num_channels=input_channels, reduction_ratio=reduction_ratio + ) + else: + se_layer = nn.Identity() + self.se = se_layer + + self.dropout = nn.Dropout3d(p=depth_dropout) + + depth_pool_fn_kwargs = {"dim": self.dim, "keepdim": False} + if pool_fn == "mean": + self.depth_pool_fn = partial(torch.mean, **depth_pool_fn_kwargs) + elif pool_fn == "max": + max_partial = partial(torch.max, **depth_pool_fn_kwargs) + # We must take the first return value because that contains the max values. + self.depth_pool_fn = lambda x: max_partial(x)[0] + elif pool_fn == "attention": + if depth is None or height is None or width is None: + raise ValueError("Depth, height and width are all required. Found None.") + self.depth_pool_fn = AttentionPool(depth, height, width, dim=self.dim) + else: + raise ValueError("Only 'max', 'mean', and 'attention' pooling are supported.") + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Pool along the depth dimension. + + Args: + x (torch.Tensor): N x C x D x H x W tensor. + + Returns: + torch.Tensor: N x C x H x W tensor. + """ + x = self.se(x) + x = self.dropout(x) + x = self.depth_pool_fn(x) + return x + + +class DepthPooling(nn.Module): + """Layer for depth pooling across multiple layers. + + Args: + num_layers (int): Number of layers to pool across. + input_channels (list[int]): List of input channels for each layer. + slice_dim (int, optional): Slice/depth dimension. Defaults to 4. + depth_dropout (float, optional): 3D dropout rate. Defaults to 0.0. + pool_fn (Literal["mean", "max"], optional): Pooling function to use along the depth dimension. Defaults to "mean". + se_type (SELayer3D | None, optional): Squeeze-and-excitation layer type. Defaults to SELayer3D.CSSE3D. + reduction_ratio (int, optional): Reduction ratio for squeeze-and-excitation layer. Defaults to 2. + """ + + def __init__( + self, + num_layers: int, + input_channels: list[int], + slice_dim: int = 2, + depth_dropout: float = 0.0, + pool_fn: Literal["mean", "max", "attention"] = "mean", + se_type: SELayer3D | None = SELayer3D.CSSE3D, + reduction_ratio: int = 2, + depths: list[int] | None = None, + heights: list[int] | None = None, + widths: list[int] | None = None, + ): + super().__init__() + self.num_layers = num_layers + self.slice_dim = slice_dim + + if depths is None: + depths = [None] * self.num_layers + if heights is None: + heights = [None] * self.num_layers + if widths is None: + widths = [None] * self.num_layers + self.layers = nn.ModuleList( + DepthPoolingBlock( + input_channels=c, + se_type=se_type, + reduction_ratio=reduction_ratio, + depth_dropout=depth_dropout, + pool_fn=pool_fn, + dim=self.slice_dim, + depth=d, + height=h, + width=w, + ) + for _, c, d, h, w in zip( + range(self.num_layers), input_channels, depths, heights, widths + ) + ) + + def forward(self, features: list[torch.Tensor]) -> list[torch.Tensor]: + """Apply depth pooling to the given list of features. + + Args: + features (list[torch.Tensor]): List of feature tensors. + + Returns: + list[torch.Tensor]: List of depth-pooled feature tensors. + + Raises: + ValueError: If the number of features does not match the number of layers. + """ + if len(features) != self.num_layers: + raise ValueError( + f"Expected to find the same number of features as layers ({self.num_layers}). Found {len(features)} features." + ) + + features_out = [] + for x, layer in zip(features, self.layers): + features_out.append(layer(x)) + return features_out + + +class AttentionPool(torch.nn.Module): + def __init__(self, depth: int, height: int, width: int, dim: int = 2): + super().__init__() + self.dim = dim + self.attention_weights = nn.Parameter(torch.ones(1, 1, depth, height, width)) + self.softmax = nn.Softmax(dim=self.dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Apply softmax along the depth dimension to obtain attention weights + attention_weights = self.softmax(self.attention_weights) + # Perform attention pooling by multiplying the attention weights with the input tensor + pooled_output = torch.mul(attention_weights, x) + # Sum the pooled output along the depth dimension + pooled_output = torch.sum(pooled_output, dim=self.dim) + return pooled_output diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/squeeze_and_excitation_3d.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/squeeze_and_excitation_3d.py new file mode 100644 index 0000000..dcfd48a --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/squeeze_and_excitation_3d.py @@ -0,0 +1,245 @@ +""" +Adapted from: https://github.com/ai-med/squeeze_and_excitation + +3D Squeeze and Excitation Modules +***************************** +3D Extensions of the following 2D squeeze and excitation blocks: + + 1. `Channel Squeeze and Excitation `_ + 2. `Spatial Squeeze and Excitation `_ + 3. `Channel and Spatial Squeeze and Excitation `_ + +New Project & Excite block, designed specifically for 3D inputs + 'quote' + + Coded by -- Anne-Marie Rickmann (https://github.com/arickm) +""" + +from enum import Enum + +import torch +from torch import nn as nn +from torch.nn import functional as F + + +class ChannelSELayer3D(nn.Module): + """3D extension of the Squeeze-and-Excitation (SE) block for channel-wise squeezing. + Described in: + * Hu et al., Squeeze-and-Excitation Networks, arXiv:1709.01507 + * Zhu et al., AnatomyNet, arXiv:1808.05238 + + Args: + num_channels (int): Number of input channels. + reduction_ratio (int, optional): Factor by which the number of channels should be reduced. Defaults to 2. + """ + + def __init__(self, num_channels: int, reduction_ratio: int = 2): + """ + :param num_channels: No. of input channels + :param reduction_ratio: By how much should the num_channels should be reduced + """ + super().__init__() + self.avg_pool = nn.AdaptiveAvgPool3d(1) + num_channels_reduced = num_channels // reduction_ratio + self.reduction_ratio = reduction_ratio + self.fc1 = nn.Linear(num_channels, num_channels_reduced, bias=True) + self.fc2 = nn.Linear(num_channels_reduced, num_channels, bias=True) + self.relu = nn.ReLU() + self.sigmoid = nn.Sigmoid() + + def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: + """Apply channel-wise squeeze and excitation. + + Args: + input_tensor (torch.Tensor): Input tensor of shape (batch_size, num_channels, D, H, W). + + Returns: + torch.Tensor: Output tensor. + """ + batch_size, num_channels, D, H, W = input_tensor.size() + # Average along each channel + squeeze_tensor = self.avg_pool(input_tensor) + + # channel excitation + fc_out_1 = self.relu(self.fc1(squeeze_tensor.view(batch_size, num_channels))) + fc_out_2 = self.sigmoid(self.fc2(fc_out_1)) + + output_tensor = torch.mul(input_tensor, fc_out_2.view(batch_size, num_channels, 1, 1, 1)) + + return output_tensor + + +class SpatialSELayer3D(nn.Module): + """3D extension of SE block for spatial squeezing and channel-wise excitation. + Described in: + * Roy et al., Concurrent Spatial and Channel Squeeze & Excitation in Fully Convolutional Networks, MICCAI 2018 + + Args: + num_channels (int): Number of input channels. + """ + + def __init__(self, num_channels: int): + """ + :param num_channels: No of input channels + + """ + super().__init__() + self.conv = nn.Conv3d(num_channels, 1, 1) + self.sigmoid = nn.Sigmoid() + + def forward( + self, input_tensor: torch.Tensor, weights: torch.Tensor | None = None + ) -> torch.Tensor: + """Apply spatial squeeze and channel-wise excitation. + + Args: + input_tensor (torch.Tensor): Input tensor of shape (batch_size, num_channels, D, H, W). + weights (torch.Tensor | None, optional): Weights for few-shot learning. Defaults to None. + + Returns: + torch.Tensor: Output tensor. + """ + # channel squeeze + batch_size, channel, D, H, W = input_tensor.size() + + if weights: + weights = weights.view(1, channel, 1, 1) + out = F.conv2d(input_tensor, weights) + else: + out = self.conv(input_tensor) + + squeeze_tensor = self.sigmoid(out) + + # spatial excitation + output_tensor = torch.mul(input_tensor, squeeze_tensor.view(batch_size, 1, D, H, W)) + + return output_tensor + + +class ChannelSpatialSELayer3D(nn.Module): + """3D extension for concurrent spatial and channel squeeze & excitation. + Described in: + * Roy et al., Concurrent Spatial and Channel Squeeze & Excitation in Fully Convolutional Networks, arXiv:1803.02579 + + Args: + num_channels (int): Number of input channels. + reduction_ratio (int, optional): Factor by which the number of channels should be reduced. Defaults to 2. + """ + + def __init__(self, num_channels: int, reduction_ratio: int = 2): + """ + :param num_channels: No of input channels + :param reduction_ratio: By how much should the num_channels should be reduced + """ + super().__init__() + self.cSE = ChannelSELayer3D(num_channels, reduction_ratio) + self.sSE = SpatialSELayer3D(num_channels) + + def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: + """Apply concurrent spatial and channel squeeze & excitation. + + Args: + input_tensor (torch.Tensor): Input tensor of shape (batch_size, num_channels, D, H, W). + + Returns: + torch.Tensor: Output tensor. + """ + output_tensor = torch.max(self.cSE(input_tensor), self.sSE(input_tensor)) + return output_tensor + + +class ProjectExciteLayer(nn.Module): + """Project & Excite Module designed for 3D inputs. A new block designed specifically for 3D inputs. + + Args: + num_channels (int): Number of input channels. + reduction_ratio (int, optional): Factor by which the number of channels should be reduced. Defaults to 2. + """ + + def __init__(self, num_channels: int, reduction_ratio: int = 2): + """ + :param num_channels: No of input channels + :param reduction_ratio: By how much should the num_channels should be reduced + """ + super().__init__() + num_channels_reduced = num_channels // reduction_ratio + self.reduction_ratio = reduction_ratio + self.relu = nn.ReLU() + self.conv_c = nn.Conv3d( + in_channels=num_channels, out_channels=num_channels_reduced, kernel_size=1, stride=1 + ) + self.conv_cT = nn.Conv3d( + in_channels=num_channels_reduced, out_channels=num_channels, kernel_size=1, stride=1 + ) + self.sigmoid = nn.Sigmoid() + + def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: + """Apply the Project & Excite operation. + + Args: + input_tensor (torch.Tensor): Input tensor of shape (batch_size, num_channels, D, H, W). + + Returns: + torch.Tensor: Output tensor. + """ + batch_size, num_channels, D, H, W = input_tensor.size() + + # Project: + # Average along channels and different axes + squeeze_tensor_w = F.adaptive_avg_pool3d(input_tensor, (1, 1, W)) + + squeeze_tensor_h = F.adaptive_avg_pool3d(input_tensor, (1, H, 1)) + + squeeze_tensor_d = F.adaptive_avg_pool3d(input_tensor, (D, 1, 1)) + + # tile tensors to original size and add: + final_squeeze_tensor = sum( + [ + squeeze_tensor_w.view(batch_size, num_channels, 1, 1, W), + squeeze_tensor_h.view(batch_size, num_channels, 1, H, 1), + squeeze_tensor_d.view(batch_size, num_channels, D, 1, 1), + ] + ) + + # Excitation: + final_squeeze_tensor = self.sigmoid( + self.conv_cT(self.relu(self.conv_c(final_squeeze_tensor))) + ) + output_tensor = torch.mul(input_tensor, final_squeeze_tensor) + + return output_tensor + + +class SELayer3D(Enum): + """Enum restricting the type of SE Blocks available.""" + + CSE3D = "CSE3D" + SSE3D = "SSE3D" + CSSE3D = "CSSE3D" + PE = "PE" + + +def se_layer3d_factory(se_type: SELayer3D, num_channels: int, reduction_ratio: int = 2): + """Factory method for creating a 3D Squeeze-and-Excitation layer. + + Args: + se_type (SELayer3D): Type of SE layer to create. + num_channels (int): Number of input channels to the SE layer. + reduction_ratio (int, optional): Reduction ratio for the SE layer. Default is 2. + + Returns: + nn.Module: An instance of the specified SE layer. + + Raises: + ValueError: If an invalid value is provided for se_type. + """ + if se_type == SELayer3D.CSE3D: + return ChannelSELayer3D(num_channels, reduction_ratio) + elif se_type == SELayer3D.SSE3D: + return SpatialSELayer3D(num_channels) + elif se_type == SELayer3D.CSSE3D: + return ChannelSpatialSELayer3D(num_channels, reduction_ratio) + elif se_type == SELayer3D.PE: + return ProjectExciteLayer(num_channels, reduction_ratio) + else: + raise ValueError(f"Invalid value for se_type: {se_type}") diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/unet_3d_to_2d.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/unet_3d_to_2d.py new file mode 100644 index 0000000..73b0e42 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/models/unet_3d_to_2d.py @@ -0,0 +1,927 @@ +"""Adapted from https://github.com/wolny/pytorch-3dunet""" +from typing import Literal + +import importlib +from collections.abc import Iterable +from dataclasses import dataclass +from functools import partial + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPooling, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.squeeze_and_excitation_3d import ( + ChannelSELayer3D, + ChannelSpatialSELayer3D, + SELayer3D, + SpatialSELayer3D, +) + + +@dataclass +class UNetOutput: + logits: torch.FloatTensor # Classification scores for each pixel. + encoder_features: tuple[torch.FloatTensor] | None = None + + +def get_number_of_learnable_parameters(model: nn.Module) -> int: + return sum(p.numel() for p in model.parameters() if p.requires_grad) + + +def number_of_features_per_level(init_channel_number: int, num_levels: int) -> list[int]: + return [init_channel_number * 2**k for k in range(num_levels)] + + +def get_class(class_name: str, modules: Iterable[str]) -> type: + for module in modules: + m = importlib.import_module(module) + clazz = getattr(m, class_name, None) + if clazz is not None: + return clazz + raise RuntimeError(f"Unsupported dataset class: {class_name}") + + +def create_conv( + in_channels: int, + out_channels: int, + kernel_size: int | tuple[int, ...], + order: str, + num_groups: int, + padding: int | tuple[int, ...], + is3d: bool, +) -> list[tuple[str, nn.Module]]: + """ + Create a list of modules with together constitute a single conv layer with non-linearity + and optional batchnorm/groupnorm. + + Args: + in_channels (int): number of input channels + out_channels (int): number of output channels + kernel_size(int or tuple): size of the convolving kernel + order (string): order of things, e.g. + 'cr' -> conv + ReLU + 'gcr' -> groupnorm + conv + ReLU + 'cl' -> conv + LeakyReLU + 'ce' -> conv + ELU + 'bcr' -> batchnorm + conv + ReLU + num_groups (int): number of groups for the GroupNorm + padding (int or tuple): add zero-padding added to all three sides of the input + is3d (bool): is3d (bool): if True use Conv3d, otherwise use Conv2d + Return: + list of tuple (name, module) + """ + assert "c" in order, "Conv layer MUST be present" + assert order[0] not in "rle", "Non-linearity cannot be the first operation in the layer" + + modules = [] + for i, char in enumerate(order): + if char == "r": + modules.append(("ReLU", nn.ReLU(inplace=True))) + elif char == "l": + modules.append(("LeakyReLU", nn.LeakyReLU(inplace=True))) + elif char == "e": + modules.append(("ELU", nn.ELU(inplace=True))) + elif char == "c": + # add learnable bias only in the absence of batchnorm/groupnorm + bias = not ("g" in order or "b" in order) + if is3d: + conv = nn.Conv3d(in_channels, out_channels, kernel_size, padding=padding, bias=bias) + else: + conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding, bias=bias) + + modules.append(("conv", conv)) + elif char == "g": + is_before_conv = i < order.index("c") + if is_before_conv: + num_channels = in_channels + else: + num_channels = out_channels + + # use only one group if the given number of groups is greater than the number of channels + if num_channels < num_groups: + num_groups = 1 + + assert ( + num_channels % num_groups == 0 + ), f"Expected number of channels in input to be divisible by num_groups. num_channels={num_channels}, num_groups={num_groups}" + modules.append( + ( + "groupnorm", + nn.GroupNorm(num_groups=num_groups, num_channels=num_channels), + ) + ) + elif char == "b": + is_before_conv = i < order.index("c") + if is3d: + bn = nn.BatchNorm3d + else: + bn = nn.BatchNorm2d + + if is_before_conv: + modules.append(("batchnorm", bn(in_channels))) + else: + modules.append(("batchnorm", bn(out_channels))) + else: + raise ValueError( + f"Unsupported layer type '{char}'. MUST be one of ['b', 'g', 'r', 'l', 'e', 'c']" + ) + + return modules + + +class SingleConv(nn.Sequential): + """ + Basic convolutional module consisting of a Conv3d, non-linearity and optional batchnorm/groupnorm. The order + of operations can be specified via the `order` parameter + + Args: + in_channels (int): number of input channels + out_channels (int): number of output channels + kernel_size (int or tuple): size of the convolving kernel + order (string): determines the order of layers, e.g. + 'cr' -> conv + ReLU + 'crg' -> conv + ReLU + groupnorm + 'cl' -> conv + LeakyReLU + 'ce' -> conv + ELU + num_groups (int): number of groups for the GroupNorm + padding (int or tuple): add zero-padding + is3d (bool): if True use Conv3d, otherwise use Conv2d + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: int | tuple[int, ...] = 3, + order: str = "gcr", + num_groups: int = 8, + padding: int | tuple[int, ...] = 1, + is3d: bool = True, + ): + super().__init__() + + for name, module in create_conv( + in_channels, out_channels, kernel_size, order, num_groups, padding, is3d + ): + self.add_module(name, module) + + +class DoubleConv(nn.Sequential): + """ + A module consisting of two consecutive convolution layers (e.g. BatchNorm3d+ReLU+Conv3d). + We use (Conv3d+ReLU+GroupNorm3d) by default. + This can be changed however by providing the 'order' argument, e.g. in order + to change to Conv3d+BatchNorm3d+ELU use order='cbe'. + Use padded convolutions to make sure that the output (H_out, W_out) is the same + as (H_in, W_in), so that you don't have to crop in the decoder path. + + Args: + in_channels (int): number of input channels + out_channels (int): number of output channels + encoder (bool): if True we're in the encoder path, otherwise we're in the decoder + kernel_size (int or tuple): size of the convolving kernel + order (string): determines the order of layers, e.g. + 'cr' -> conv + ReLU + 'crg' -> conv + ReLU + groupnorm + 'cl' -> conv + LeakyReLU + 'ce' -> conv + ELU + num_groups (int): number of groups for the GroupNorm + padding (int or tuple): add zero-padding added to all three sides of the input + is3d (bool): if True use Conv3d instead of Conv2d layers + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + encoder: bool, + kernel_size: int | tuple[int, ...] = 3, + order: str = "gcr", + num_groups: int = 8, + padding: int | tuple[int, ...] = 1, + is3d: bool = True, + ): + super().__init__() + if encoder: + # we're in the encoder path + conv1_in_channels = in_channels + conv1_out_channels = out_channels // 2 + if conv1_out_channels < in_channels: + conv1_out_channels = in_channels + conv2_in_channels, conv2_out_channels = conv1_out_channels, out_channels + else: + # we're in the decoder path, decrease the number of channels in the 1st convolution + conv1_in_channels, conv1_out_channels = in_channels, out_channels + conv2_in_channels, conv2_out_channels = out_channels, out_channels + + # conv1 + self.add_module( + "SingleConv1", + SingleConv( + conv1_in_channels, + conv1_out_channels, + kernel_size, + order, + num_groups, + padding=padding, + is3d=is3d, + ), + ) + # conv2 + self.add_module( + "SingleConv2", + SingleConv( + conv2_in_channels, + conv2_out_channels, + kernel_size, + order, + num_groups, + padding=padding, + is3d=is3d, + ), + ) + + +class ResNetBlock(nn.Module): + """ + Residual block that can be used instead of standard DoubleConv in the Encoder module. + Motivated by: https://arxiv.org/pdf/1706.00120.pdf + + Notice we use ELU instead of ReLU (order='cge') and put non-linearity after the groupnorm. + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: int | tuple[int, ...] = 3, + order: str = "cge", + num_groups: int = 8, + is3d: bool = True, + **kwargs, + ): + super().__init__() + + if in_channels != out_channels: + # conv1x1 for increasing the number of channels + if is3d: + self.conv1 = nn.Conv3d(in_channels, out_channels, 1) + else: + self.conv1 = nn.Conv2d(in_channels, out_channels, 1) + else: + self.conv1 = nn.Identity() + + # residual block + self.conv2 = SingleConv( + out_channels, + out_channels, + kernel_size=kernel_size, + order=order, + num_groups=num_groups, + is3d=is3d, + ) + # remove non-linearity from the 3rd convolution since it's going to be applied after adding the residual + n_order = order + for c in "rel": + n_order = n_order.replace(c, "") + self.conv3 = SingleConv( + out_channels, + out_channels, + kernel_size=kernel_size, + order=n_order, + num_groups=num_groups, + is3d=is3d, + ) + + # create non-linearity separately + if "l" in order: + self.non_linearity = nn.LeakyReLU(negative_slope=0.1, inplace=True) + elif "e" in order: + self.non_linearity = nn.ELU(inplace=True) + else: + self.non_linearity = nn.ReLU(inplace=True) + + def forward(self, x: Tensor) -> Tensor: + # apply first convolution to bring the number of channels to out_channels + residual = self.conv1(x) + + # residual block + out = self.conv2(residual) + out = self.conv3(out) + + out += residual + out = self.non_linearity(out) + + return out + + +class ResNetBlockSE(ResNetBlock): + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: int = 3, + order: str = "cge", + num_groups: int = 8, + se_module: str = "scse", + **kwargs, + ): + super().__init__( + in_channels, + out_channels, + kernel_size=kernel_size, + order=order, + num_groups=num_groups, + **kwargs, + ) + assert se_module in ["scse", "cse", "sse"] + if se_module == "scse": + self.se_module = ChannelSpatialSELayer3D(num_channels=out_channels, reduction_ratio=1) + elif se_module == "cse": + self.se_module = ChannelSELayer3D(num_channels=out_channels, reduction_ratio=1) + elif se_module == "sse": + self.se_module = SpatialSELayer3D(num_channels=out_channels) + + def forward(self, x): + out = super().forward(x) + out = self.se_module(out) + return out + + +class EncoderBlock(nn.Module): + """ + A single module from the encoder path consisting of the optional max + pooling layer (one may specify the MaxPool kernel_size to be different + from the standard (2,2,2), e.g. if the volumetric data is anisotropic + (make sure to use complementary scale_factor in the decoder path) followed by + a basic module (DoubleConv or ResNetBlock). + + Args: + in_channels (int): number of input channels + out_channels (int): number of output channels + conv_kernel_size (int or tuple): size of the convolving kernel + apply_pooling (bool): if True use MaxPool3d before DoubleConv + pool_kernel_size (int or tuple): the size of the window + pool_type (str): pooling layer: 'max' or 'avg' + basic_module(nn.Module): either ResNetBlock or DoubleConv + conv_layer_order (string): determines the order of layers + in `DoubleConv` module. See `DoubleConv` for more info. + num_groups (int): number of groups for the GroupNorm + padding (int or tuple): add zero-padding added to all three sides of the input + is3d (bool): use 3d or 2d convolutions/pooling operation + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + conv_kernel_size: int | tuple[int, ...] = 3, + apply_pooling: bool = True, + pool_kernel_size: int | tuple[int, ...] = 2, + pool_type: str = "max", + basic_module: type[nn.Module] = DoubleConv, + conv_layer_order: str = "gcr", + num_groups: int = 8, + padding: int | tuple[int, ...] = 1, + is3d: bool = True, + ): + super().__init__() + assert pool_type in ["max", "avg"] + if apply_pooling: + if pool_type == "max": + if is3d: + self.pooling = nn.MaxPool3d(kernel_size=pool_kernel_size) + else: + self.pooling = nn.MaxPool2d(kernel_size=pool_kernel_size) + else: + if is3d: + self.pooling = nn.AvgPool3d(kernel_size=pool_kernel_size) + else: + self.pooling = nn.AvgPool2d(kernel_size=pool_kernel_size) + else: + self.pooling = None + + self.basic_module = basic_module( + in_channels, + out_channels, + encoder=True, + kernel_size=conv_kernel_size, + order=conv_layer_order, + num_groups=num_groups, + padding=padding, + is3d=is3d, + ) + + def forward(self, x): + if self.pooling is not None: + x = self.pooling(x) + x = self.basic_module(x) + return x + + +def create_encoder_blocks( + in_channels: int, + f_maps: int | tuple[int, ...], + basic_module: type[nn.Module], + conv_kernel_size: int | tuple[int, ...], + conv_padding: int | tuple[int, ...], + layer_order: str, + num_groups: int, + pool_kernel_size: int | tuple[int, ...], + is3d: bool, +): + # create encoder path consisting of Encoder modules. Depth of the encoder is equal to `len(f_maps)` + encoders = [] + for i, out_feature_num in enumerate(f_maps): + if i == 0: + # apply conv_coord only in the first encoder if any + encoder = EncoderBlock( + in_channels, + out_feature_num, + apply_pooling=False, # skip pooling in the firs encoder + basic_module=basic_module, + conv_layer_order=layer_order, + conv_kernel_size=conv_kernel_size, + num_groups=num_groups, + padding=conv_padding, + is3d=is3d, + ) + else: + encoder = EncoderBlock( + f_maps[i - 1], + out_feature_num, + basic_module=basic_module, + conv_layer_order=layer_order, + conv_kernel_size=conv_kernel_size, + num_groups=num_groups, + pool_kernel_size=pool_kernel_size, + padding=conv_padding, + is3d=is3d, + ) + + encoders.append(encoder) + + return nn.ModuleList(encoders) + + +class AbstractUpsampling(nn.Module): + """ + Abstract class for upsampling. A given implementation should upsample a given 5D input tensor using either + interpolation or learned transposed convolution. + """ + + def __init__(self, upsample: callable): + super().__init__() + self.upsample = upsample + + def forward(self, encoder_features: Tensor, x: Tensor) -> Tensor: + # get the spatial dimensions of the output given the encoder_features + output_size = encoder_features.size()[2:] + # upsample the input and return + return self.upsample(x, output_size) + + +class InterpolateUpsampling(AbstractUpsampling): + """ + Args: + mode (str): algorithm used for upsampling: + 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'. Default: 'nearest' + used only if transposed_conv is False + """ + + def __init__(self, mode: str = "nearest"): + upsample = partial(self._interpolate, mode=mode) + super().__init__(upsample) + + @staticmethod + def _interpolate(x: Tensor, size: int | None, mode: str) -> Tensor: + return F.interpolate(x, size=size, mode=mode) + + +class TransposeConvUpsampling(AbstractUpsampling): + """ + Args: + in_channels (int): number of input channels for transposed conv + used only if transposed_conv is True + out_channels (int): number of output channels for transpose conv + used only if transposed_conv is True + kernel_size (int or tuple): size of the convolving kernel + used only if transposed_conv is True + scale_factor (int or tuple): stride of the convolution + used only if transposed_conv is True + + """ + + def __init__( + self, + in_channels: int | None = None, + out_channels: int | None = None, + kernel_size: int | tuple[int, ...] = 3, + scale_factor: int | tuple[int, ...] = (2, 2, 2), + ): + # make sure that the output size reverses the MaxPool3d from the corresponding encoder + upsample = nn.ConvTranspose3d( + in_channels, + out_channels, + kernel_size=kernel_size, + stride=scale_factor, + padding=1, + ) + super().__init__(upsample) + + +class NoUpsampling(AbstractUpsampling): + def __init__(self): + super().__init__(self._no_upsampling) + + @staticmethod + def _no_upsampling(x: Tensor, size: int | None) -> Tensor: + return x + + +class DecoderBlock(nn.Module): + """ + A single module for decoder path consisting of the upsampling layer + (either learned ConvTranspose3d or nearest neighbor interpolation) + followed by a basic module (DoubleConv or ResNetBlock). + + Args: + in_channels (int): number of input channels + out_channels (int): number of output channels + conv_kernel_size (int or tuple): size of the convolving kernel + scale_factor (tuple): used as the multiplier for the image H/W/D in + case of nn.Upsample or as stride in case of ConvTranspose3d, must reverse the MaxPool3d operation + from the corresponding encoder + basic_module(nn.Module): either ResNetBlock or DoubleConv + conv_layer_order (string): determines the order of layers + in `DoubleConv` module. See `DoubleConv` for more info. + num_groups (int): number of groups for the GroupNorm + padding (int or tuple): add zero-padding added to all three sides of the input + upsample (bool): should the input be upsampled + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + conv_kernel_size: int | tuple[int, ...] = 3, + scale_factor: tuple[int, ...] = (2, 2, 2), + basic_module: type[nn.Module] = DoubleConv, + conv_layer_order: str = "gcr", + num_groups: int = 8, + mode: str = "nearest", + padding: int | tuple = 1, + upsample: bool = True, + is3d: bool = True, + ): + super().__init__() + + if upsample: + if basic_module == DoubleConv: + # if DoubleConv is the basic_module use interpolation for upsampling and concatenation joining + self.upsampling = InterpolateUpsampling(mode=mode) + # concat joining + self.joining = partial(self._joining, concat=True) + else: + # if basic_module=ResNetBlock use transposed convolution upsampling and summation joining + self.upsampling = TransposeConvUpsampling( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=conv_kernel_size, + scale_factor=scale_factor, + ) + # sum joining + self.joining = partial(self._joining, concat=False) + # adapt the number of in_channels for the ResNetBlock + in_channels = out_channels + else: + # no upsampling + self.upsampling = NoUpsampling() + # concat joining + self.joining = partial(self._joining, concat=True) + + self.basic_module = basic_module( + in_channels, + out_channels, + encoder=False, + kernel_size=conv_kernel_size, + order=conv_layer_order, + num_groups=num_groups, + padding=padding, + is3d=is3d, + ) + + def forward(self, encoder_features, x): + x = self.upsampling(encoder_features=encoder_features, x=x) + x = self.joining(encoder_features, x) + x = self.basic_module(x) + return x + + @staticmethod + def _joining(encoder_features, x, concat): + if concat: + return torch.cat((encoder_features, x), dim=1) + else: + return encoder_features + x + + +def create_decoder_blocks( + f_maps: int | tuple[int, ...], + basic_module: type[nn.Module], + conv_kernel_size: int | tuple[int, ...], + conv_padding: int | tuple[int, ...], + layer_order: str, + num_groups: int, + is3d: bool, +): + # create decoder path consisting of the Decoder modules. The length of the decoder list is equal to `len(f_maps) - 1` + decoders = [] + reversed_f_maps = list(reversed(f_maps)) + for i in range(len(reversed_f_maps) - 1): + if basic_module == DoubleConv: + in_feature_num = reversed_f_maps[i] + reversed_f_maps[i + 1] + else: + in_feature_num = reversed_f_maps[i] + + out_feature_num = reversed_f_maps[i + 1] + + decoder = DecoderBlock( + in_feature_num, + out_feature_num, + basic_module=basic_module, + conv_layer_order=layer_order, + conv_kernel_size=conv_kernel_size, + num_groups=num_groups, + padding=conv_padding, + is3d=is3d, + ) + decoders.append(decoder) + return nn.ModuleList(decoders) + + +class AbstractUNet(nn.Module): + """ + Base class for standard and residual UNet. + + Args: + in_channels (int): number of input channels + out_channels (int): number of output segmentation masks; + Note that the of out_channels might correspond to either + different semantic classes or to different binary segmentation mask. + It's up to the user of the class to interpret the out_channels and + use the proper loss criterion during training (i.e. CrossEntropyLoss (multi-class) + or BCEWithLogitsLoss (two-class) respectively) + f_maps (int, tuple): number of feature maps at each level of the encoder; if it's an integer the number + of feature maps is given by the geometric progression: f_maps ^ k, k=1,2,3,4 + final_sigmoid (bool): if True apply element-wise nn.Sigmoid after the final 1x1 convolution, + otherwise apply nn.Softmax. In effect only if `self.training == False`, i.e. during validation/testing + basic_module: basic model for the encoder/decoder (DoubleConv, ResNetBlock, ....) + layer_order (string): determines the order of layers in `SingleConv` module. + E.g. 'crg' stands for GroupNorm3d+Conv3d+ReLU. See `SingleConv` for more info + num_groups (int): number of groups for the GroupNorm + num_levels (int): number of levels in the encoder/decoder path (applied only if f_maps is an int) + default: 4 + is_segmentation (bool): if True and the model is in eval mode, Sigmoid/Softmax normalization is applied + after the final convolution; if False (regression problem) the normalization layer is skipped + conv_kernel_size (int or tuple): size of the convolving kernel in the basic_module + pool_kernel_size (int or tuple): the size of the window + conv_padding (int or tuple): add zero-padding added to all three sides of the input + is3d_encoder (bool): if True the model uses a 3D encoder, otherwise 2D, default: True + is3d_decoder (bool): if True the model uses a 3D decoder, otherwise 2D, default: False + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + final_sigmoid: bool, + basic_module: type[nn.Module], + f_maps: int | tuple[int, ...] = 64, + layer_order: str = "gcr", + num_groups: int = 8, + num_levels: int = 4, + is_segmentation: bool = True, + conv_kernel_size: int | tuple[int, ...] = 3, + pool_kernel_size: int | tuple[int, ...] = 2, + conv_padding: int | tuple[int, ...] = 1, + is3d_encoder: bool = True, + is3d_decoder: bool = False, + output_features: bool | None = None, + return_dict: bool | None = True, + ): + super().__init__() + + if isinstance(f_maps, int): + f_maps = number_of_features_per_level(f_maps, num_levels=num_levels) + + assert isinstance(f_maps, list) or isinstance(f_maps, tuple) + assert len(f_maps) > 1, "Required at least 2 levels in the U-Net" + if "g" in layer_order: + assert num_groups is not None, "num_groups must be specified if GroupNorm is used" + + self._f_maps = f_maps + + # create encoder path + self.encoder_blocks = create_encoder_blocks( + in_channels, + f_maps, + basic_module, + conv_kernel_size, + conv_padding, + layer_order, + num_groups, + pool_kernel_size, + is3d_encoder, + ) + self.encoder_blocks.insert( + 0, nn.Identity() + ) # Add this to be compatible with segmentation_models.pytorch. + + # create decoder path + self.decoder_blocks = create_decoder_blocks( + f_maps, + basic_module, + conv_kernel_size, + conv_padding, + layer_order, + num_groups, + is3d_decoder, + ) + + # in the last layer a 1×1 convolution reduces the number of output channels to the number of labels + if is3d_decoder: + self.classifier = nn.Conv3d(f_maps[0], out_channels, 1) + else: + self.classifier = nn.Conv2d(f_maps[0], out_channels, 1) + + if is_segmentation: + # semantic segmentation problem + if final_sigmoid: + self.final_activation = nn.Sigmoid() + else: + self.final_activation = nn.Softmax(dim=1) + else: + # regression problem + self.final_activation = None + + self.output_features = output_features + self.use_return_dict = return_dict + + def encode(self, x: Tensor) -> list[Tensor]: + stages = self.encoder_blocks + + features = [] + for encoder_block in stages: + x = encoder_block(x) + features.append(x) + + return features + + def decode(self, features: list[Tensor]) -> Tensor: + # stages = self.decoder_blocks + features = features[1:] # remove first skip with same spatial resolution + features = features[::-1] # reverse channels to start from head of encoder + + head = features[0] + skips = features[1:] + + x = head + for decoder_block, skip in zip(self.decoder_blocks, skips): + x = decoder_block(skip, x) + return x + + def forward( + self, x: Tensor, output_features: bool | None = None, return_dict: bool | None = None + ) -> UNetOutput | tuple: + output_features = output_features if output_features is not None else self.output_features + return_dict = return_dict if return_dict is not None else self.use_return_dict + + features = self.encode(x) + x = self.decode(features) + + x = self.classifier(x) + + # apply final_activation (i.e. Sigmoid or Softmax) only during prediction. + # During training the network outputs logits + if not self.training and self.final_activation is not None: + x = self.final_activation(x) + + encoder_features = features if output_features else None + + if not return_dict: + return tuple(v for v in [x, encoder_features] if v is not None) + + return UNetOutput(logits=x, encoder_features=encoder_features) + + +class UNet3Dto2D(AbstractUNet): + """ + 3DUnet model from + `"3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation" + `. + + Uses `DoubleConv` as a basic_module and nearest neighbor upsampling in the decoder + """ + + def __init__( + self, + in_channels: int = 1, + out_channels: int = 1, + final_sigmoid: bool = False, + f_maps: int | tuple[int, ...] = 64, + layer_order: str = "gcr", + num_groups: int = 8, + num_levels: int = 4, + is_segmentation: bool = False, + conv_padding: int | tuple[int, ...] = 1, + se_type_str: str | None = "CSE3D", + reduction_ratio: int = 2, + depth_dropout: float = 0.0, + pool_fn: Literal["mean", "max"] = "mean", + output_features: bool | None = None, + return_dict: bool | None = True, + ): + super().__init__( + in_channels=in_channels, + out_channels=out_channels, + final_sigmoid=final_sigmoid, + basic_module=DoubleConv, + f_maps=f_maps, + layer_order=layer_order, + num_groups=num_groups, + num_levels=num_levels, + is_segmentation=is_segmentation, + conv_padding=conv_padding, + is3d_encoder=True, + is3d_decoder=False, + output_features=output_features, + return_dict=return_dict, + ) + + # Pool along the slice/depth dimension. + self.depth_pooler = DepthPooling( + len(self.encoder_blocks) - 1, + self._f_maps[::-1], + slice_dim=2, + depth_dropout=depth_dropout, + pool_fn=pool_fn, + se_type=SELayer3D[se_type_str] if se_type_str is not None else None, + reduction_ratio=reduction_ratio, + ) + + def decode(self, features: list[Tensor]) -> Tensor: + features = features[1:] # remove first skip with same spatial resolution + features = features[::-1] # reverse channels to start from head of encoder + + # Pool along slice/depth dimension. + features = self.depth_pooler(features) + + head = features[0] + skips = features[1:] + + x = head + for decoder_block, skip in zip(self.decoder_blocks, skips): + x = decoder_block(skip, x) + return x + + +def compute_unet_3d_to_2d_encoder_chwd( + patch_depth: int, + patch_height: int, + patch_width: int, + f_maps: int, + encoder_level: int, + in_channels: int, +) -> tuple[int, int, int, int]: + """Computes the dimensions and channels at a given encoder level for a 3D U-Net. + + Args: + patch_depth (int): Depth of the input 3D patch. + patch_height (int): Height of the input 3D patch. + patch_width (int): Width of the input 3D patch. + f_maps (int): Number of feature maps at the first encoder level. + encoder_level (int): The level of the encoder layer for which the dimensions are to be calculated. + in_channels (int): Number of input channels. + + Returns: + tuple[int, int, int, int]: Returns a tuple containing the number of output channels, depth, height, and width at + the given encoder level. + + Example: + >>> compute_unet_3d_to_2d_encoder_chwd(10, 256, 256, 32, 2, 1) + (128, 2, 64, 64) + """ + if encoder_level == 0: + return in_channels, patch_depth, patch_height, patch_width + + depth = encoder_level - 1 + spatial_reduction_factor = 2**depth + c_out = f_maps * spatial_reduction_factor + d_out = patch_depth // spatial_reduction_factor + h_out = patch_height // spatial_reduction_factor + w_out = patch_width // spatial_reduction_factor + return c_out, d_out, h_out, w_out diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/schedulers.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/schedulers.py new file mode 100644 index 0000000..94e895a --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/schedulers.py @@ -0,0 +1,159 @@ +""" +Gradual Warmup Scheduler for PyTorch's Optimizer. +Adapted from https://github.com/ildoonet/pytorch-gradual-warmup-lr +""" +from typing import Any + +from torch.optim import Optimizer +from torch.optim.lr_scheduler import LRScheduler, ReduceLROnPlateau + + +class GradualWarmupScheduler(LRScheduler): + """Gradually warm-up (increasing) learning rate in optimizer. + Proposed in 'Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour'. + + Args: + optimizer (Optimizer): Wrapped optimizer. + multiplier: Target learning rate multiplier. target learning rate = base lr * multiplier if multiplier > 1.0. + if multiplier = 1.0, lr starts from 0 and ends up with the base_lr. + total_epochs: Epochs to gradually reach target learning rate. + after_scheduler: Scheduler to use after total_epochs (e.g. ReduceLROnPlateau). + """ + + def __init__( + self, + optimizer: Optimizer, + multiplier: float, + total_epochs: int, + after_scheduler: LRScheduler | None = None, + ): + if multiplier < 1.0: + raise ValueError("multiplier should be >= 1.") + + self.multiplier = multiplier + self.total_epochs = total_epochs + self.after_scheduler = after_scheduler + self.finished = False + super().__init__(optimizer) + + def get_lr(self) -> list[float]: + """Calculate the learning rate based on the current epoch. + + Returns: + List[float]: List of learning rates for each parameter group. + """ + if self.last_epoch > self.total_epochs: + if self.after_scheduler: + if not self.finished: + self.after_scheduler.base_lrs = [ + base_lr * self.multiplier for base_lr in self.base_lrs + ] + self.finished = True + return self.after_scheduler.get_last_lr() + return [base_lr * self.multiplier for base_lr in self.base_lrs] + + if self.multiplier == 1.0: + return [ + base_lr * (float(self.last_epoch) / self.total_epochs) for base_lr in self.base_lrs + ] + else: + return [ + base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epochs + 1.0) + for base_lr in self.base_lrs + ] + + def step_ReduceLROnPlateau(self, metrics: Any, epoch: int | None = None) -> None: + """Special step method to handle ReduceLROnPlateau scheduling. + + Args: + metrics (float): Metric value used to compute learning rate. + epoch (int, optional): Current epoch number. + """ + if epoch is None: + epoch = self.last_epoch + 1 + self.last_epoch = ( + epoch if epoch != 0 else 1 + ) # ReduceLROnPlateau is called at the end of epoch, whereas others are called at beginning + if self.last_epoch <= self.total_epochs: + warmup_lr = [ + base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epochs + 1.0) + for base_lr in self.base_lrs + ] + for param_group, lr in zip(self.optimizer.param_groups, warmup_lr): + param_group["lr"] = lr + elif self.after_scheduler is not None: + if epoch is None: + self.after_scheduler.step(metrics, None) + else: + self.after_scheduler.step(metrics, epoch - self.total_epochs) + + def step(self, epoch: int | None = None, metrics: Any = None) -> None: + """Update the learning rate using the GradualWarmupScheduler. + + Args: + epoch (int, optional): Current epoch number. + metrics (float, optional): Metric value used to compute learning rate. + """ + if isinstance(self.after_scheduler, ReduceLROnPlateau): + if self.finished and self.after_scheduler: + if epoch is None: + self.after_scheduler.step(None) + else: + self.after_scheduler.step(epoch - self.total_epochs) + self._last_lr = self.after_scheduler.get_last_lr() + else: + return super().step(epoch) + else: + self.step_ReduceLROnPlateau(metrics, epoch) + + +class ReduceLROnPlateauWarmup(ReduceLROnPlateau): + """A scheduler that combines ReduceLROnPlateau with warm-up. + + This class first warms up the learning rate linearly over a number of epochs, + and then uses ReduceLROnPlateau to reduce the learning rate when a metric has stopped improving. + + Attributes: + warmup_epochs (int): Number of epochs for the warm-up phase. + """ + + def __init__(self, optimizer: Optimizer, warmup_epochs: int, **kwargs) -> None: + """Initialize the ReduceLROnPlateauWarmup scheduler. + + Args: + optimizer (Optimizer): Wrapped optimizer. + warmup_epochs (int): Number of epochs for the warm-up phase. + **kwargs: Additional keyword arguments for ReduceLROnPlateau. + """ + super().__init__(optimizer, **kwargs) + self.warmup_epochs = warmup_epochs + self.last_epoch = 0 + self.base_lrs = [group["lr"] for group in optimizer.param_groups] + self.step_rop(self.mode_worse, False) + + def warmup_lr(self, epoch: int) -> None: + """Warm up the learning rate linearly over the warm-up phase. + + Args: + epoch (int): Current epoch number. + """ + factor = epoch / self.warmup_epochs + self.last_epoch = epoch + for i, param_group in enumerate(self.optimizer.param_groups): + param_group["lr"] = self.base_lrs[i] * factor + + def step_rop(self, metrics: Any, evaluate: bool) -> None: + """Step method for ReduceLROnPlateauWarmup scheduler. + + This method updates the learning rate according to the warm-up phase and evaluation condition. + + Args: + metrics (Any): Metric value used to compute learning rate. + evaluate (bool): Whether to evaluate the metric and update the learning rate. + """ + epoch = self.last_epoch + 1 + + if epoch <= self.warmup_epochs: + self.warmup_lr(epoch) + elif evaluate: + super().step(metrics, epoch=epoch) diff --git a/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/util.py b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/util.py new file mode 100644 index 0000000..2d581d2 --- /dev/null +++ b/vesuvius_challenge_rnd/fragment_ink_detection/ink_detection/util.py @@ -0,0 +1,17 @@ +import logging + +import torch +import torch.nn as nn + + +def compile_if_possible(model: nn.Module) -> nn.Module: + try: + compiled_model = torch.compile(model) + if not isinstance(compiled_model, nn.Module): + logging.warning(f"Compiled model is not a nn.Module. Returning original model.") + return model + else: + return compiled_model + except Exception as e: + logging.warning(f"Failed to compile model {model.__class__.__name__}: {e}.") + return model diff --git a/vesuvius_challenge_rnd/patching.py b/vesuvius_challenge_rnd/patching.py new file mode 100644 index 0000000..10968b5 --- /dev/null +++ b/vesuvius_challenge_rnd/patching.py @@ -0,0 +1,19 @@ +def patch_index_to_pixel_position( + patch_i: int, patch_j: int, patch_shape: tuple[int, int], patch_stride: int +) -> tuple[tuple[int, int], tuple[int, int]]: + """Convert the patch index to the top-left and bottom right pixel positions. + + Args: + patch_i (int): The index of the patch along the vertical axis (i.e., row index). + patch_j (int): The index of the patch along the horizontal axis (i.e., column index). + patch_shape (tuple[int, int]): The shape of the patch as (height, width). + patch_stride (int): The stride used for moving between patches. + + Returns: + tuple[tuple[int, int], tuple[int, int]]: The top-left (x0, y0) and bottom-right (x1, y1) pixel positions. + """ + y0 = patch_i * patch_stride + y1 = y0 + patch_shape[0] + x0 = patch_j * patch_stride + x1 = x0 + patch_shape[1] + return (y0, x0), (y1, x1) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/__init__.py new file mode 100644 index 0000000..d087507 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/__init__.py @@ -0,0 +1,19 @@ +from .ink_detection import ( + CNN3dMANetPLModel, + CNN3DSegformerPLModel, + Cnn3dto2dCrackformerLitModel, + CNN3dUnetPlusPlusPLModel, + CombinedDataModule, + EvalScrollPatchDataModule, + HrSegNetLitModel, + I3DMeanTeacherPLModel, + LitDomainAdversarialSegmenter, + MedNextV1SegformerPLModel, + MedNextV13dto2dPLModel, + RegressionPLModel, + ScrollPatchDataModule, + SemiSupervisedScrollPatchDataModule, + SwinUNETRSegformerPLModel, + UNet3dSegformerPLModel, + UNETRSegformerPLModel, +) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/__main__.py b/vesuvius_challenge_rnd/scroll_ink_detection/__main__.py new file mode 100644 index 0000000..38ae7d3 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/__main__.py @@ -0,0 +1,4 @@ +from vesuvius_challenge_rnd.scroll_ink_detection.experiment_runner.run_experiment import cli_main + +if __name__ == "__main__": + raise SystemExit(cli_main()) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/ddp_pred_writer.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/ddp_pred_writer.py new file mode 100644 index 0000000..654d3d6 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/ddp_pred_writer.py @@ -0,0 +1,23 @@ +import os + +import torch +from pytorch_lightning.callbacks import BasePredictionWriter + + +class PredictionWriterDDP(BasePredictionWriter): + def __init__(self, output_dir, write_interval): + super().__init__(write_interval) + self.output_dir = output_dir + + def write_on_epoch_end(self, trainer, pl_module, predictions, batch_indices): + # this will create N (num processes) files in `output_dir` each containing + # the predictions of its respective rank + torch.save( + predictions, os.path.join(self.output_dir, f"predictions_{trainer.global_rank}.pt") + ) + + # optionally, you can also save `batch_indices` to get the information about the data index + # from your prediction data + torch.save( + batch_indices, os.path.join(self.output_dir, f"batch_indices_{trainer.global_rank}.pt") + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference.py new file mode 100644 index 0000000..0f88ebe --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference.py @@ -0,0 +1,305 @@ +import importlib +import logging +import os +from argparse import ArgumentParser +from pathlib import Path + +import cv2 +import numpy as np +import scipy.stats as st +import torch.nn.functional as F +from dotenv import load_dotenv +from PIL import Image +from pytorch_lightning import LightningModule, Trainer +from scipy.signal.windows import windows + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data.constants import Z_NON_REVERSED_SEGMENT_IDS, Z_REVERSED_SEGMENT_IDS +from vesuvius_challenge_rnd.scroll_ink_detection import UNet3dSegformerPLModel +from vesuvius_challenge_rnd.scroll_ink_detection.evaluation.util import ( + validate_file_path, + validate_positive_int, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection import ScrollPatchDataModuleEval + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +DEFAULT_OUTPUT_DIR = Path("outputs") + + +def gkern(kernlen=21, nsig=3): + """Returns a 2D Gaussian kernel.""" + x = np.linspace(-nsig, nsig, kernlen + 1) + kern1d = np.diff(st.norm.cdf(x)) + kern2d = np.outer(kern1d, kern1d) + return kern2d / kern2d.sum() + + +def process_predictions( + predictions, + pred_shape: tuple[int, int], + size: int, + orig_h: int, + orig_w: int, + scale_factor: int = 1, + window_type: str = "hann", +): + """ + Processes a list of predictions and their corresponding patch positions. + + :param predictions: List of tuples, each containing a tensor of predictions and a tensor of patch positions. + :param pred_shape: Tuple indicating the shape of the full prediction area. + :param size: Size of the square patch for each prediction. + :param window_type: The window type to use for the predictions patches. + :return: A numpy array representing the processed prediction mask. + """ + + # Initialize masks + mask_pred = np.zeros(pred_shape) + if window_type == "hann": + kernel = windows.hann(size)[:, None] * windows.hann(size)[None, :] + elif window_type == "gaussian": + kernel = gkern(size, 1) + kernel /= kernel.max() + else: + raise ValueError(f"Unknown window {window_type}. Must be either 'hann' or 'gaussian'.") + + # Iterate over the predictions and positions + for y_proba, patch_position in predictions: + xys = patch_position.numpy() + for i, (x1, y1, x2, y2) in enumerate(xys): + interpolated_pred = ( + F.interpolate( + y_proba[i].unsqueeze(0).float(), scale_factor=scale_factor, mode="bilinear" + ) + .squeeze(0) + .squeeze(0) + .numpy() + ) + y_proba_patch = np.multiply(interpolated_pred, kernel) + mask_pred[y1:y2, x1:x2] += y_proba_patch + + # Finalize the prediction mask + mask_pred /= mask_pred.max() + mask_pred = np.clip(np.nan_to_num(mask_pred), a_min=0, a_max=1) + mask_pred = (mask_pred * 255).astype(np.uint8) + mask_pred = mask_pred[:orig_h, :orig_w] + return mask_pred + + +def run_inference( + prediction_segment_id: str, + scroll_id: str, + model_ckpt_path: str, + stride: int, + z_start: int, + z_extent: int, + size: int, + batch_size: int = 512, + num_workers: int = 0, + output_dir: Path = DEFAULT_OUTPUT_DIR, + z_reverse: bool = False, + infer_z_reversal: bool = False, + data_dir: Path = SCROLL_DATA_DIR, + model_cls: type[LightningModule] | str = UNet3dSegformerPLModel, + skip_if_exists: bool = True, + scale_factor: int = 1, + window_type: str = "hann", +): + if isinstance(model_cls, str): + model_cls_path = ( + f"vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.lit_models.{model_cls}" + ) + module_path, class_name = model_cls_path.rsplit(".", 1) + model_cls = getattr(importlib.import_module(module_path), class_name) + + # Possibly infer the z-reversed flag. + if infer_z_reversal: + segment_name_base_part = prediction_segment_id.split("_C")[0].split("_superseded")[0] + if segment_name_base_part in Z_REVERSED_SEGMENT_IDS: + z_reverse_inferred = True + elif segment_name_base_part in Z_NON_REVERSED_SEGMENT_IDS: + z_reverse_inferred = False + else: + z_reverse_inferred = None + logger.warning( + f"Unable to infer z-reversal for segment {prediction_segment_id}. Defaulting to given z-reverse ({z_reverse})." + ) + + if ( + z_reverse_inferred is not None + and isinstance(z_reverse_inferred, bool) + and z_reverse_inferred != z_reverse + ): + logger.info( + f"Inferred z-reversal ({infer_z_reversal}) different from given z-reversal ({z_reverse}). Using z-reverse={z_reverse_inferred}." + ) + z_reverse = z_reverse_inferred + + mask_pred_stem = f"{prediction_segment_id}_{stride}_{z_start}_r{int(z_reverse)}" + mask_pred_path = output_dir / f"{mask_pred_stem}.png" + if mask_pred_path.exists() and skip_if_exists: + logger.info( + f"Skipping segment {prediction_segment_id} prediction because mask prediction path ({mask_pred_path}) already exists." + ) + return + + # Load model + model = model_cls.load_from_checkpoint(model_ckpt_path, strict=False) + + # Load data + data_module = ScrollPatchDataModuleEval( + prediction_segment_id=prediction_segment_id, + scroll_id=scroll_id, + data_dir=data_dir, + z_min=z_start, + z_max=z_start + z_extent, + size=size, + z_reverse=z_reverse, + tile_size=size, + patch_stride=stride, + batch_size=batch_size, + num_workers=num_workers, + ) + + # Predict + trainer = Trainer(precision="16-mixed", devices=1) + predictions = trainer.predict(model=model, datamodule=data_module) + + orig_h, orig_w = cv2.imread( + f"{data_module.segment_dir}/{prediction_segment_id}/{prediction_segment_id}_mask.png", 0 + ).shape + pad0 = size - orig_h % size + pad1 = size - orig_w % size + mask_pred = process_predictions( + predictions, + (orig_h + pad0, orig_w + pad1), + data_module.size, + orig_h, + orig_w, + scale_factor=scale_factor, + window_type=window_type, + ) + + # Save predictions + output_dir.mkdir(exist_ok=True, parents=True) + mask_pred = Image.fromarray(mask_pred) + mask_pred.save(mask_pred_path) + logger.info(f"Saved prediction mask to {mask_pred_path.resolve()}") + + +def _set_up_parser() -> ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = ArgumentParser(description="Visualize a patch model's predictions on scroll segments.") + parser.add_argument("ckpt_path", type=validate_file_path, help="Model checkpoint path") + parser.add_argument("prediction_segment_id", type=str, help="Prediction segment ID") + parser.add_argument( + "--scroll_id", type=str, default="1", help="The scroll ID for which the segment belongs" + ) + parser.add_argument( + "-o", "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR, help="Output path" + ) + parser.add_argument( + "--data_dir", type=str, default=SCROLL_DATA_DIR, help="Scroll data directory" + ) + parser.add_argument( + "-s", "--patch_stride", default=None, type=validate_positive_int, help="Patch stride" + ) + parser.add_argument( + "--z_min", + default=15, + type=validate_positive_int, + help="Minimum z-coordinate.", + ) + parser.add_argument( + "--z_extent", + default=32, + type=validate_positive_int, + help="Z-coordinate extent.", + ) + parser.add_argument("--z_reverse", default=False, action="store_true", help="Z-reverse flag") + parser.add_argument( + "--stride", + default=8, + type=validate_positive_int, + help="XY-plane stride.", + ) + parser.add_argument( + "--size", + default=64, + type=validate_positive_int, + help="Patch XY size.", + ) + parser.add_argument( + "--batch_size", + default=320, + type=validate_positive_int, + help="Batch size.", + ) + parser.add_argument( + "--num_workers", + default=4, + type=int, + help="Number of workers.", + ) + parser.add_argument( + "--skip_if_exists", + action="store_false", + default=True, + help="Skip segment if it already has predictions.", + ) + parser.add_argument( + "--infer_z_reversal", + action="store_true", + default=False, + help="Infer the z-reversal setting.", + ) + parser.add_argument( + "--window_type", + default="hann", + type=str, + help="The window type to apply on patch predictions.", + ) + parser.add_argument( + "--scale_factor", + default=1, + type=int, + help="The label upscaling factor.", + ) + parser.add_argument( + "--model_cls_type", + default="UNet3dSegformerPLModel", + type=str, + help="The pytorch lightning class path.", + ) + return parser + + +if __name__ == "__main__": + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + run_inference( + args.prediction_segment_id, + args.scroll_id, + args.ckpt_path, + data_dir=Path(args.data_dir), + stride=args.stride, + z_start=args.z_min, + z_extent=args.z_extent, + size=args.size, + batch_size=args.batch_size, + num_workers=args.num_workers, + z_reverse=args.z_reverse, + infer_z_reversal=args.infer_z_reversal, + output_dir=Path(args.output_dir), + skip_if_exists=args.skip_if_exists, + window_type=args.window_type, + scale_factor=args.scale_factor, + model_cls=args.model_cls_type, + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference_mmap.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference_mmap.py new file mode 100644 index 0000000..c5faff8 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/inference_mmap.py @@ -0,0 +1,331 @@ +import importlib +import logging +import os +from argparse import ArgumentParser +from pathlib import Path + +import numpy as np +import scipy.stats as st +import torch +import torch.nn.functional as F +from dotenv import load_dotenv +from PIL import Image +from pytorch_lightning import LightningModule, Trainer +from scipy.signal.windows import windows + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data.constants import Z_NON_REVERSED_SEGMENT_IDS, Z_REVERSED_SEGMENT_IDS +from vesuvius_challenge_rnd.scroll_ink_detection import UNet3dSegformerPLModel +from vesuvius_challenge_rnd.scroll_ink_detection.evaluation.util import ( + validate_file_path, + validate_positive_int, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data import ( + ScrollPatchDataModuleEvalMmap, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +DEFAULT_OUTPUT_DIR = Path("outputs") + + +def gkern(kernlen=21, nsig=3): + """Returns a 2D Gaussian kernel.""" + x = np.linspace(-nsig, nsig, kernlen + 1) + kern1d = np.diff(st.norm.cdf(x)) + kern2d = np.outer(kern1d, kern1d) + return kern2d / kern2d.sum() + + +def process_predictions( + predictions, + pred_shape: tuple[int, int], + size: int, + orig_h: int, + orig_w: int, + scale_factor: int = 1, + window_type: str = "hann", +): + """ + Processes a list of predictions and their corresponding patch positions. + + :param predictions: List of tuples, each containing a tensor of predictions and a tensor of patch positions. + :param pred_shape: Tuple indicating the shape of the full prediction area. + :param size: Size of the square patch for each prediction. + :param window_type: The window type to use for the predictions patches. + :return: A numpy array representing the processed prediction mask. + """ + + # Initialize masks + mask_pred = np.zeros(pred_shape) + if window_type == "hann": + kernel = windows.hann(size)[:, None] * windows.hann(size)[None, :] + elif window_type == "gaussian": + kernel = gkern(size, 1) + kernel /= kernel.max() + else: + raise ValueError(f"Unknown window {window_type}. Must be either 'hann' or 'gaussian'.") + + # Iterate over the predictions and positions + for y_proba, patch_position in predictions: + xys = patch_position.numpy() + for i, (x1, y1, x2, y2) in enumerate(xys): + interpolated_pred = ( + F.interpolate( + y_proba[i].unsqueeze(0).float(), scale_factor=scale_factor, mode="bilinear" + ) + .squeeze(0) + .squeeze(0) + .numpy() + ) + y_proba_patch = np.multiply(interpolated_pred, kernel) + mask_pred[y1:y2, x1:x2] += y_proba_patch + + # Finalize the prediction mask + mask_pred /= mask_pred.max() + mask_pred = np.clip(np.nan_to_num(mask_pred), a_min=0, a_max=1) + mask_pred = (mask_pred * 255).astype(np.uint8) + mask_pred = mask_pred[:orig_h, :orig_w] + return mask_pred + + +def _load_model( + ckpt_path: str, + model_cls_path: str, + map_location: torch.device | None = None, + strict: bool = False, + **kwargs, +) -> LightningModule: + """Load a model from a checkpoint. + + Args: + ckpt_path (str): Checkpoint path. + map_location (torch.device | None, optional): Device mapping location. Defaults to None. + + Returns: + LightningModule: Loaded model. + """ + # Initialize model. + module_path, class_name = model_cls_path.rsplit(".", 1) + lit_model_cls = getattr(importlib.import_module(module_path), class_name) + lit_model = lit_model_cls.load_from_checkpoint( + ckpt_path, map_location=map_location, strict=strict, **kwargs + ) + lit_model.eval() + return lit_model + + +def run_inference( + prediction_segment_id: str, + scroll_id: str, + model_ckpt_path: str, + stride: int, + z_start: int, + z_extent: int, + size: int, + batch_size: int = 512, + num_workers: int = 0, + output_dir: Path = DEFAULT_OUTPUT_DIR, + z_reverse: bool = False, + infer_z_reversal: bool = False, + data_dir: Path = SCROLL_DATA_DIR, + model_cls: type[LightningModule] | str = UNet3dSegformerPLModel, + skip_if_exists: bool = True, + scale_factor: int = 1, + window_type: str = "hann", +): + # Load model. + if isinstance(model_cls, str): + model_cls_path = ( + f"vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.lit_models.{model_cls}" + ) + model = _load_model(model_ckpt_path, model_cls_path, strict=False) + else: + model = model_cls.load_from_checkpoint(model_ckpt_path, strict=False) + model.eval() + + # Possibly infer the z-reversed flag. + if infer_z_reversal: + segment_name_base_part = prediction_segment_id.split("_C")[0].split("_superseded")[0] + if segment_name_base_part in Z_REVERSED_SEGMENT_IDS: + z_reverse_inferred = True + elif segment_name_base_part in Z_NON_REVERSED_SEGMENT_IDS: + z_reverse_inferred = False + else: + z_reverse_inferred = None + logger.warning( + f"Unable to infer z-reversal for segment {prediction_segment_id}. Defaulting to given z-reverse ({z_reverse})." + ) + + if ( + z_reverse_inferred is not None + and isinstance(z_reverse_inferred, bool) + and z_reverse_inferred != z_reverse + ): + logger.info( + f"Inferred z-reversal ({infer_z_reversal}) different from given z-reversal ({z_reverse}). Using z-reverse={z_reverse_inferred}." + ) + z_reverse = z_reverse_inferred + + mask_pred_stem = f"{prediction_segment_id}_{stride}_{z_start}_r{int(z_reverse)}" + mask_pred_path = output_dir / f"{mask_pred_stem}.png" + if mask_pred_path.exists() and skip_if_exists: + logger.info( + f"Skipping segment {prediction_segment_id} prediction because mask prediction path ({mask_pred_path}) already exists." + ) + return + + # Load data module. + data_module = ScrollPatchDataModuleEvalMmap( + prediction_segment_id=prediction_segment_id, + scroll_id=scroll_id, + data_dir=data_dir, + z_min=z_start, + z_max=z_start + z_extent, + size=size, + z_reverse=z_reverse, + tile_size=size, + patch_stride=stride, + batch_size=batch_size, + num_workers=num_workers, + ) + + # Predict + trainer = Trainer(precision="16-mixed", devices=1) + predictions = trainer.predict(model=model, datamodule=data_module) + + orig_h, orig_w = data_module.segment.surface_shape + pad0 = size - orig_h % size + pad1 = size - orig_w % size + mask_pred = process_predictions( + predictions, + (orig_h + pad0, orig_w + pad1), + data_module.size, + orig_h, + orig_w, + scale_factor=scale_factor, + window_type=window_type, + ) + + # Save predictions + output_dir.mkdir(exist_ok=True, parents=True) + mask_pred = Image.fromarray(mask_pred) + mask_pred.save(mask_pred_path) + logger.info(f"Saved prediction mask to {mask_pred_path.resolve()}") + + +def _set_up_parser() -> ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = ArgumentParser(description="Visualize a patch model's predictions on scroll segments.") + parser.add_argument("ckpt_path", type=validate_file_path, help="Model checkpoint path") + parser.add_argument("prediction_segment_id", type=str, help="Prediction segment ID") + parser.add_argument( + "--scroll_id", type=str, default="1", help="The scroll ID for which the segment belongs" + ) + parser.add_argument( + "-o", "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR, help="Output path" + ) + parser.add_argument( + "--data_dir", type=str, default=SCROLL_DATA_DIR, help="Scroll data directory" + ) + parser.add_argument( + "-s", "--patch_stride", default=None, type=validate_positive_int, help="Patch stride" + ) + parser.add_argument( + "--z_min", + default=15, + type=validate_positive_int, + help="Minimum z-coordinate.", + ) + parser.add_argument( + "--z_extent", + default=32, + type=validate_positive_int, + help="Z-coordinate extent.", + ) + parser.add_argument("--z_reverse", default=False, action="store_true", help="Z-reverse flag") + parser.add_argument( + "--stride", + default=8, + type=validate_positive_int, + help="XY-plane stride.", + ) + parser.add_argument( + "--size", + default=64, + type=validate_positive_int, + help="Patch XY size.", + ) + parser.add_argument( + "--batch_size", + default=320, + type=validate_positive_int, + help="Batch size.", + ) + parser.add_argument( + "--num_workers", + default=4, + type=int, + help="Number of workers.", + ) + parser.add_argument( + "--skip_if_exists", + action="store_false", + default=True, + help="Skip segment if it already has predictions.", + ) + parser.add_argument( + "--infer_z_reversal", + action="store_true", + default=False, + help="Infer the z-reversal setting.", + ) + parser.add_argument( + "--window_type", + default="hann", + type=str, + help="The window type to apply on patch predictions.", + ) + parser.add_argument( + "--scale_factor", + default=1, + type=int, + help="The label upscaling factor.", + ) + parser.add_argument( + "--model_cls_type", + default="UNet3dSegformerPLModel", + type=str, + help="The pytorch lightning class path.", + ) + return parser + + +if __name__ == "__main__": + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + run_inference( + args.prediction_segment_id, + args.scroll_id, + args.ckpt_path, + data_dir=Path(args.data_dir), + stride=args.stride, + z_start=args.z_min, + z_extent=args.z_extent, + size=args.size, + batch_size=args.batch_size, + num_workers=args.num_workers, + z_reverse=args.z_reverse, + infer_z_reversal=args.infer_z_reversal, + output_dir=Path(args.output_dir), + skip_if_exists=args.skip_if_exists, + window_type=args.window_type, + scale_factor=args.scale_factor, + model_cls=args.model_cls_type, + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/__init__.py new file mode 100644 index 0000000..29e0360 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/__init__.py @@ -0,0 +1,2 @@ +from .create_subsegment import create_subsegment +from .generate_flat_model_prediction_dir import generate_flat_model_prediction_dir diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_all_subsegments.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_all_subsegments.py new file mode 100644 index 0000000..e53724b --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_all_subsegments.py @@ -0,0 +1,82 @@ +import argparse +from pathlib import Path + +import pandas as pd +from dotenv import load_dotenv +from tqdm import tqdm + +from vesuvius_challenge_rnd.scroll_ink_detection.evaluation.tools import create_subsegment + +DEFAULT_SUBSEGMENT_INFO_PATH = Path(__file__).parent / "subsegment_info.csv" + + +def create_all_subsegments( + subsegment_info_path: Path = DEFAULT_SUBSEGMENT_INFO_PATH, + skip_if_exists: bool = True, + use_memmap: bool = True, +) -> None: + print(f"Creating subsegments from {subsegment_info_path}") + subsegment_info_df = pd.read_csv(subsegment_info_path).sort_values( + by=["scroll_id", "segment_id", "column_id"] + ) + + n_rows = len(subsegment_info_df) + print(f"Found {n_rows} sub-segments in file.") + for _, row in tqdm(subsegment_info_df.iterrows(), total=n_rows): + try: + create_subsegment( + str(row.segment_id), + str(row.scroll_id), + int(row.u1), + int(row.u2), + int(row.v1), + int(row.v2), + str(row.column_id), + skip_if_exists=skip_if_exists, + use_memmap=use_memmap, + ) + except Exception as e: + print(f"Failed to create sub-segment {row}: {e}") + + +def main(): + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + create_all_subsegments( + args.subsegment_info_path, + skip_if_exists=args.skip_if_exists, + use_memmap=not args.load_in_memory, + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate subsegment for scroll segments from a CSV file." + ) + parser.add_argument( + "--subsegment_info_path", + type=str, + default=DEFAULT_SUBSEGMENT_INFO_PATH, + help="The sub-segment info CSV file path.", + ) + parser.add_argument( + "--skip_if_exists", + action="store_true", + help="Skip the creation of a sub-segment if it already exists.", + ) + parser.add_argument( + "--load_in_memory", + action="store_true", + help="Load the image stack in RAM instead of as a memory-mapped file.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_subsegment.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_subsegment.py new file mode 100644 index 0000000..69065d8 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/create_subsegment.py @@ -0,0 +1,185 @@ +import argparse +from pathlib import Path + +import cv2 +import numpy as np +from skimage.util import img_as_ubyte +from tifffile import tifffile + +from vesuvius_challenge_rnd.data.scroll import ScrollSegment, create_scroll_segment + + +def _verify_is_dtype(arr: np.ndarray, dtype: type[np.dtype]) -> None: + """ + Verifies if the input NumPy array is of a specific dtype. + + Args: + arr (ndarray): The input NumPy array. + dtype (type[np.dtype]): The expected data type. + + Returns: + None: Returns nothing if dtype matches the expected dtype. + + Raises: + ValueError: If dtype of the input array does not match the expected dtype. + """ + if arr.dtype != dtype: + raise ValueError(f"Input array must be of dtype {dtype}.") + return None + + +def _imwrite_uint8(array: np.ndarray, output_path: str | Path): + _verify_is_dtype(array, dtype=np.uint8) + cv2.imwrite(str(output_path), array) + + +def save_mask(mask: np.ndarray, output_path: Path) -> None: + """Save the papyrus mask to the preprocessing directory.""" + _imwrite_uint8(mask, output_path) + print(f"Saved mask of shape {mask.shape}.") + + +def save_surface_volumes(surface_volumes: np.ndarray, output_dir: Path) -> None: + """Save the surface volumes to the preprocessing directory.""" + _verify_is_dtype(surface_volumes, dtype=np.uint16) + output_dir.mkdir(exist_ok=True, parents=True) + + for i, img in enumerate(surface_volumes): + output_path = output_dir / f"{str(i).zfill(2)}.tif" + tifffile.imwrite(output_path, img) + + print(f"Saved {len(surface_volumes)} surface volumes.") + + +def _prepare_subvolume( + segment: ScrollSegment, u1: int, u2: int, v1: int, v2: int, use_memmap: bool = True +) -> np.ndarray: + if use_memmap: + volume = segment.load_volume_as_memmap() + else: + volume = segment.load_volume(preprocess=False) + return volume[:, u1:u2, v1:v2] + + +def _prepare_subsegment_mask( + segment: ScrollSegment, u1: int, u2: int, v1: int, v2: int +) -> np.ndarray: + mask = segment.load_mask() + subsegment_mask = mask[u1:u2, v1:v2] + return img_as_ubyte(subsegment_mask) + + +def create_subsegment( + segment_name: str, + scroll_id: str, + u1: int, + u2: int | None, + v1: int, + v2: int | None, + column_id: str, + skip_if_exists: bool = True, + use_memmap: bool = True, +) -> None: + print(f"Creating segment {segment_name}") + segment = create_scroll_segment(scroll_id=scroll_id, segment_name=segment_name) + + output_segment_name = f"{segment_name}_{column_id}" + output_dir = segment.volume_dir_path.with_name(output_segment_name) + if skip_if_exists and output_dir.exists() and any(output_dir.iterdir()): + print(f"Skipping existing sub-segment: {output_segment_name}") + return + + if u2 is None: + u2 = segment.surface_shape[0] + + if v2 is None: + v2 = segment.surface_shape[1] + + print(f"Creating subvolume and mask u: {u1}-{u2}; v: {v1}-{v2}") + subvolume = _prepare_subvolume(segment, u1, u2, v1, v2, use_memmap=use_memmap) + subsegment_mask = _prepare_subsegment_mask(segment, u1, u2, v1, v2) + + output_dir.mkdir(exist_ok=True, parents=True) + + output_mask_name = segment.papyrus_mask_file_name.replace( + segment.segment_name, output_segment_name + ) + output_mask_path = output_dir / output_mask_name + print(f"Saving segment mask to {output_mask_path}") + save_mask(subsegment_mask, output_mask_path) + + output_surface_volume_path = output_dir / segment.surface_volume_dir_name + print(f"Saving surface volume to {output_surface_volume_path}") + save_surface_volumes(subvolume, output_surface_volume_path) + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + create_subsegment( + args.segment_id, + args.scroll_id, + args.u1, + args.u2, + args.v1, + args.v2, + args.column_id, + skip_if_exists=args.skip_if_exists, + use_memmap=not args.load_in_memory, + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate a subsegment for a given scroll segment." + ) + parser.add_argument( + "u1", + type=int, + help="The starting index (vertical axis) in the UV-coordinate system to cut the sub-segment.", + ) + parser.add_argument( + "u2", + type=int, + help="The ending index (vertical axis) in the UV-coordinate system to cut the sub-segment.", + ) + parser.add_argument( + "v1", + type=int, + help="The starting index (horizontal axis) in the UV-coordinate system to cut the sub-segment.", + ) + parser.add_argument( + "v2", + type=int, + help="The starting index (horizontal axis) in the UV-coordinate system to cut the sub-segment.", + ) + parser.add_argument("segment_id", type=str, help="The segment ID to cut.") + parser.add_argument( + "--scroll_id", type=str, default="1", help="The scroll ID for which the segment belongs" + ) + parser.add_argument( + "--column_id", + type=str, + default="C0", + help="The column ID for the new segment. The new segment name will be _", + ) + parser.add_argument( + "--skip_if_exists", + action="store_true", + help="Skip the creation of the sub-segment if it already exists.", + ) + parser.add_argument( + "--load_in_memory", + action="store_true", + help="Load the image stack in RAM instead of as a memory-mapped file.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/generate_flat_model_prediction_dir.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/generate_flat_model_prediction_dir.py new file mode 100644 index 0000000..052d3f1 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/generate_flat_model_prediction_dir.py @@ -0,0 +1,128 @@ +import argparse +import logging +import shutil +from pathlib import Path + +from tqdm import tqdm + +from vesuvius_challenge_rnd.data.constants import Z_NON_REVERSED_SEGMENT_IDS, Z_REVERSED_SEGMENT_IDS + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +DEFAULT_OUTPUT_DIR = Path("model_predictions") + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + generate_flat_model_prediction_dir( + args.prediction_base_dir, + args.model_id, + args.scroll_id, + args.start_idx, + args.output_dir, + args.stride, + ) + + +def _find_segment_pred_path( + segment_pred_dir: Path, stride: int, start_idx: int, z_reversed: bool = False +) -> Path: + seg_str = f"{segment_pred_dir.name}_{stride}_{start_idx}_r{int(z_reversed)}" + pred_paths = [p for p in segment_pred_dir.glob("*.png") if p.stem.startswith(seg_str)] + if len(pred_paths) != 1: + raise ValueError( + f"Found {len(pred_paths)} segment prediction paths starting with {seg_str}. Expected only one." + ) + return pred_paths[0] + + +def _infer_z_reversal(segment_name: str) -> bool | None: + if segment_name in Z_REVERSED_SEGMENT_IDS: + return True + elif segment_name in Z_NON_REVERSED_SEGMENT_IDS: + return False + else: + return None + + +def generate_flat_model_prediction_dir( + prediction_base_dir: str, + run_id: str, + scroll_id: str, + start_idx: int, + output_dir: str | Path = DEFAULT_OUTPUT_DIR, + stride: int = 32, +) -> None: + if isinstance(output_dir, Path): + output_dir = Path(output_dir) + + prediction_base_dir_path = Path(prediction_base_dir) + prediction_dir_path = prediction_base_dir_path / run_id / scroll_id + + segment_pred_paths = [] + for path in prediction_dir_path.glob("*"): + if path.is_dir(): + segment_name = path.name + z_reverse = _infer_z_reversal(segment_name) + if z_reverse is None: + logger.info( + f"Could not determine z_reverse for {segment_name}. Defaulting to non-reversed." + ) + z_reverse = True + + pred_path = _find_segment_pred_path( + path, stride=stride, start_idx=start_idx, z_reversed=z_reverse + ) + if not pred_path.is_file(): + raise FileNotFoundError( + f"Could not find segment {segment_name} prediction for path {pred_path}." + ) + segment_pred_paths.append(pred_path) + + output_dir_path = Path(output_dir) + output_dir_path.mkdir(exist_ok=True, parents=True) + + # Copy files. + for segment_pred_path in tqdm(segment_pred_paths, desc="Copying segment predictions..."): + destination_path = output_dir_path / segment_pred_path.name + shutil.copy(segment_pred_path, destination_path) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate a flattened directory of predictions for a given model." + ) + parser.add_argument( + "prediction_base_dir", type=str, help="Segment prediction base directory path" + ) + parser.add_argument("model_id", type=str, help="Model ID") + parser.add_argument( + "--scroll_id", type=str, default="1", help="The scroll ID for which the segment belongs" + ) + parser.add_argument( + "-o", "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR, help="Output path" + ) + parser.add_argument( + "--start_idx", + default=15, + type=int, + help="Z-plane start index.", + ) + parser.add_argument( + "--stride", + default=32, + type=int, + help="XY-plane stride.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/subsegment_info.csv b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/subsegment_info.csv new file mode 100644 index 0000000..6658c2e --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/tools/subsegment_info.csv @@ -0,0 +1,21 @@ +scroll_id,segment_id,column_id,u1,u2,v1,v2 +1,20231005123336,C2,0,10939,3600,14100 +1,20231005123336,C3,0,10939,13700,24100 +1,20231005123336,C4,0,10939,23200,29872 +1,20230929220926,C2,0,9919,3800,14100 +1,20230929220926,C3,0,9919,13700,23357 +1,20231022170901,C1,0,6655,0,5800 +1,20231022170901,C2,0,6655,4800,15640 +1,20231022170901,C3,0,6655,14500,26000 +1,20231022170901,C4,0,6655,25000,35653 +1,20231012184423,C1,0,15798,0,4300 +1,20231012184423,C2,0,15798,3200,13900 +1,20231012184423,C3,0,15798,13300,23364 +1,20231007101619,C1,0,16134,0,12100 +1,20231007101619,C2,0,16134,11500,22200 +1,20231007101619,C3,0,16134,21200,33305 +1,20231012184422_superseded,C2,0,11882,3200,13900 +1,20231012184422_superseded,C3,0,11882,13300,23364 +1,20231007101616_superseded,C1,0,11330,0,11900 +1,20231007101616_superseded,C2,0,11330,11200,22200 +1,20231007101616_superseded,C3,0,11330,21400,32600 \ No newline at end of file diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/util.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/util.py new file mode 100644 index 0000000..77f958f --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/util.py @@ -0,0 +1,39 @@ +from argparse import ArgumentTypeError +from pathlib import Path + + +def tuple_parser(input_str: str) -> tuple[int, str]: + try: + scroll_id, segment_id = input_str.split(",") + return int(scroll_id), segment_id + except Exception: + raise ValueError("Invalid tuple format. Expected 'scroll_id,segment_id'.") + + +def validate_file_path(file_path: str) -> str: + path = Path(file_path) + if not path.exists(): + raise ArgumentTypeError(f"The path {file_path} does not exist.") + elif not path.is_file(): + raise ArgumentTypeError(f"The path {file_path} exists but is not a file.") + return file_path + + +def validate_positive_int(value: str) -> int: + try: + int_value = int(value) + if int_value <= 0: + raise ValueError + except ValueError: + raise ArgumentTypeError(f"Invalid value: {value}. Must be a positive integer.") + return int_value + + +def validate_nonnegative_int(value: str) -> int: + try: + int_value = int(value) + if int_value < 0: + raise ValueError + except ValueError: + raise ArgumentTypeError(f"Invalid value: {value}. Must be a positive integer.") + return int_value diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/visualize_predictions.py b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/visualize_predictions.py new file mode 100644 index 0000000..852f351 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/evaluation/visualize_predictions.py @@ -0,0 +1,366 @@ +import importlib +import logging +from argparse import ArgumentParser +from pathlib import Path + +import numpy as np +import torch +from dotenv import load_dotenv +from matplotlib import pyplot as plt +from pytorch_lightning.core.saving import load_hparams_from_yaml +from tqdm.auto import tqdm + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data import Scroll, ScrollSegment +from vesuvius_challenge_rnd.data.scroll import create_scroll_segment +from vesuvius_challenge_rnd.fragment_ink_detection import PatchLitModel +from vesuvius_challenge_rnd.scroll_ink_detection.evaluation.util import ( + tuple_parser, + validate_file_path, + validate_positive_int, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.eval_scroll_patch_data_module import ( + EvalScrollPatchDataModule, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.inference.prediction import ( + predict_with_model_on_scroll, +) + +DEFAULT_OUTPUT_DIR = Path("outputs") + + +def visualize_predictions( + lit_model: PatchLitModel, + patch_surface_shape: tuple[int, int], + z_patch_size: int, + z_min: int = 27, + z_max: int = 37, + z_stride: int = 1, + downsampling: int | None = None, + patch_stride: int | None = None, + predict_scroll_ind: list[int] | None = None, + segment_data: list[tuple[int, str]] | None = None, + batch_size: int = 4, + num_workers: int = 0, + save_dir: Path | None = DEFAULT_OUTPUT_DIR, +) -> None: + """Visualize predictions on a given model. + + Args: + lit_model (PatchLitModel): The model for predictions. + patch_surface_shape (tuple[int, int]): Shape of the surface patch. + z_min (int, optional): Minimum z-value. Defaults to 27. + z_max (int, optional): Maximum z-value. Defaults to 37. + downsampling (int | None, optional): Downsampling factor. Defaults to None. + patch_stride (int | None, optional): Stride of the patch. Defaults to None. + predict_scroll_ind (list[int] | None, optional): Indices for prediction scroll. Defaults to None. + segment_data (list[tuple[int, str]] | None, optional): Scroll segment data tuples. Defaults to None. + batch_size (int, optional): Batch size for predictions. Defaults to 4. + num_workers (int, optional): Number of workers for parallelism. Defaults to 0. + save_dir (Path | None, optional): Directory to save outputs. Defaults to Path("outputs"). + """ + if patch_stride is None: + patch_stride = patch_surface_shape[0] // 2 + + segments = _get_scroll_segments(predict_scroll_ind, segment_data, patch_surface_shape) + + for segment in tqdm(segments): + for z_start in range(z_min, z_max - z_patch_size + 1, z_stride): + z_end = z_start + z_patch_size + # Initialize scroll segment data module. + data_module = EvalScrollPatchDataModule( + segment, + z_min=z_start, + z_max=z_end, + patch_surface_shape=patch_surface_shape, + patch_stride=patch_stride, + downsampling=downsampling, + num_workers=num_workers, + batch_size=batch_size, + ) + + # Get and parse predictions. + y_proba_smoothed = predict_with_model_on_scroll( + lit_model, data_module, patch_surface_shape + ) + + # Show prediction for segment. + z_mid = (z_end + z_start) // 2 + texture_img = segment.load_volume(z_mid, z_mid + 1) + + fig, ax = create_pred_fig( + segment.scroll_id, + segment.segment_name, + texture_img, + y_proba_smoothed, + z_mid, + z_min=z_start, + z_max=z_end, + ) + + if save_dir is not None: + # Optionally save the figure. + save_dir.mkdir(exist_ok=True, parents=True) + file_name = f"prediction_{segment.scroll_id}_{segment.segment_name}_z={z_start}-to-{z_end}.png" + output_path = save_dir / file_name + fig.savefig(output_path) + print(f"Saved prediction to {output_path.resolve()}") + + plt.show() + + +def create_pred_fig( + scroll_id: int, + segment_name: str, + texture_img: np.ndarray, + y_proba_smoothed: np.ndarray, + texture_slice_idx: int, + z_min: int, + z_max: int, + figsize: tuple[int, int] = (25, 10), +) -> tuple[plt.Figure, plt.Axes]: + """Create a figure with predictions and ink prediction. + + Args: + scroll_id (int): Scroll ID. + segment_name (str): Scroll segment name. + texture_img (np.ndarray): Texture image of the segment. + y_proba_smoothed (np.ndarray): Smoothed probability array. + figsize (tuple[int, int], optional): Figure size. Defaults to (25, 10). + + Returns: + tuple[plt.Figure, plt.Axes]: Figure and Axes objects for the plot. + """ + horizontal = texture_img.shape[1] > texture_img.shape[0] + n_plots = 2 + if horizontal: + n_rows = 1 + n_cols = n_plots + else: + n_rows = n_plots + n_cols = 1 + + fig, ax = plt.subplots(n_rows, n_cols, figsize=figsize) + ax1, ax2 = ax.flatten() + fig.suptitle(f"Scroll {scroll_id} - segment {segment_name} - z={z_min}-{z_max}") + + ax1.imshow(texture_img, cmap="gray", vmin=0, vmax=1) + ax1.set_title(f"Slice {texture_slice_idx}") + + ax2.imshow(y_proba_smoothed, cmap="viridis", vmin=0, vmax=1) + ax2.set_title(f"Ink prediction") + + return fig, ax + + +def load_model( + ckpt_path: str, + model_cls_path: str, + map_location: torch.device | None = None, + strict: bool = True, + **kwargs, +) -> PatchLitModel: + """Load a model from a checkpoint. + + Args: + ckpt_path (str): Checkpoint path. + map_location (torch.device | None, optional): Device mapping location. Defaults to None. + + Returns: + PatchLitModel: Loaded model. + """ + # Initialize model. + module_path, class_name = model_cls_path.rsplit(".", 1) + lit_model_cls = getattr(importlib.import_module(module_path), class_name) + lit_model = lit_model_cls.load_from_checkpoint( + ckpt_path, map_location=map_location, strict=strict, **kwargs + ) + lit_model.eval() + return lit_model + + +def parse_config(config_path: str | Path) -> dict: + """Parse the configuration from a given YAML file. + + Args: + config_path (str): Path to the configuration file. + + Returns: + dict: Dictionary containing configuration parameters. + """ + config = load_hparams_from_yaml(config_path) + data_init_args = config["data"]["init_args"] + patch_surface_shape = data_init_args["patch_surface_shape"] + + if "z_min_scroll" in data_init_args: + z_min = data_init_args["z_min_scroll"] + else: + z_min = data_init_args["z_min"] + if "z_max_scroll" in data_init_args: + z_max = data_init_args["z_max_scroll"] + else: + z_max = data_init_args["z_max"] + downsampling = data_init_args["downsampling"] + model_cls_path = config["model"]["class_path"] + return { + "patch_surface_shape": patch_surface_shape, + "z_min": z_min, + "z_max": z_max, + "downsampling": downsampling, + "model_cls_path": model_cls_path, + } + + +def _get_scroll_segments( + predict_scroll_ind: list[int] | None, + segment_data: list[tuple[int, str]] | None, + patch_surface_shape: tuple[int, int], + scroll_dir: Path = SCROLL_DATA_DIR, +) -> list[ScrollSegment]: + # Create scroll segments. + if predict_scroll_ind is not None: + scrolls = [Scroll(i) for i in predict_scroll_ind] + segments = [segment for scroll in scrolls for segment in scroll] + else: + segments = [ + create_scroll_segment(scroll_id, segment_name, scroll_dir=scroll_dir) + for scroll_id, segment_name in segment_data + ] + + # Discard segments that are smaller than the patch size. + filtered_segments = [] + for segment in segments: + if ( + segment.surface_shape[0] >= patch_surface_shape[0] + and segment.surface_shape[1] >= patch_surface_shape[1] + ): + filtered_segments.append(segment) + else: + logging.warning( + f"Skipping scroll {segment.scroll_id} segment {segment.segment_name} with shape {segment.shape} because " + f"it's smaller than the patch surface shape: {patch_surface_shape}." + ) + return filtered_segments + + +def main( + ckpt_path: str, + config_path: str, + patch_stride: int | None = None, + predict_scroll_ind: list[int] | None = None, + segment_data: list[tuple[int, str]] | None = None, + output_dir: Path | None = DEFAULT_OUTPUT_DIR, + z_min: int | None = None, + z_max: int | None = None, + z_stride: int = 1, +) -> None: + """Main function to run the visualization. + + Args: + ckpt_path (str): Model checkpoint path. + config_path (str): Training configuration path. + patch_stride (int | None, optional): Patch stride. Defaults to None. + predict_scroll_ind (list[int] | None, optional): Prediction scroll indices. Defaults to None. + segment_data (list[tuple[int, str]] | None, optional): Scroll segment data tuples. Defaults to None. + output_dir (Path | None, optional): The output directory. + """ + data_config = parse_config(config_path) + + # Parse z min and max. + z_extent_orig = data_config["z_max"] - data_config["z_min"] + if z_min is not None: + data_config["z_min"] = z_min + if z_max is not None: + data_config["z_max"] = z_max + z_extent = data_config["z_max"] - data_config["z_min"] + if z_extent < z_extent_orig: + raise ValueError( + f"Z-extent ({z_extent}) cannot be smaller than {z_extent_orig} as found in the config." + ) + + model_cls_path = data_config.pop("model_cls_path") + lit_model = load_model(ckpt_path, model_cls_path=model_cls_path) + + if predict_scroll_ind is None: + if segment_data is None: # If both are not set, predict on all scrolls. + predict_scroll_ind = [1, 2] + else: + if segment_data is not None: + raise ValueError("`predict_scroll_ind` and `segment_data` cannot both be set.") + + visualize_predictions( + lit_model, + patch_stride=patch_stride, + predict_scroll_ind=predict_scroll_ind, + segment_data=segment_data, + save_dir=output_dir, + z_stride=z_stride, + z_patch_size=z_extent_orig, + **data_config, + ) + + +def _set_up_parser() -> ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = ArgumentParser(description="Visualize a patch model's predictions on scroll segments.") + parser.add_argument("ckpt_path", type=validate_file_path, help="Model checkpoint path") + parser.add_argument("cfg_path", type=validate_file_path, help="Training config path") + parser.add_argument( + "-o", "--output_dir", type=str, default=DEFAULT_OUTPUT_DIR, help="Output path" + ) + parser.add_argument( + "-s", "--patch_stride", default=None, type=validate_positive_int, help="Patch stride" + ) + parser.add_argument( + "--z_min", + default=None, + type=validate_positive_int, + help="Z-min (defaults to same as used in the lit model config).", + ) + parser.add_argument( + "--z_max", + default=None, + type=validate_positive_int, + help="Z-max (defaults to same as used in the lit model config).", + ) + parser.add_argument( + "--z_stride", + default=1, + type=validate_positive_int, + help="Z stride. Only used if given a z-range larger than patch depth size.", + ) + + group = parser.add_mutually_exclusive_group() + group.add_argument( + "-f", "--predict_scroll_ind", nargs="+", type=int, help="Prediction scroll indices" + ) + group.add_argument( + "-g", + "--segments", + nargs="+", + type=tuple_parser, + help="Tuples of 'scroll,segment' (e.g., 2,20230421204550)", + ) + + return parser + + +if __name__ == "__main__": + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + main( + args.ckpt_path, + args.cfg_path, + patch_stride=args.patch_stride, + predict_scroll_ind=args.predict_scroll_ind, + z_min=args.z_min, + z_max=args.z_max, + z_stride=args.z_stride, + segment_data=args.segments, + output_dir=Path(args.output_dir), + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run.yaml new file mode 100644 index 0000000..72de1d0 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run.yaml @@ -0,0 +1,105 @@ +# pytorch_lightning==2.0.6 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + project: scroll-ink-det-debugging + job_type: train + log_model: all + name: valid=20230827161847_64x64_z35-45_i3d + callbacks: + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/mean_fbeta_auprc + mode: max + save_top_k: 1 + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + mode: min + patience: 5 + fast_dev_run: true + max_epochs: 12 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: "norm" + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + pred_shape: null + size: 64 + lr: 2.0e-05 + unet_feature_size: 16 + segformer_model_size: 1 +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - ["1", "20231210121320_superseded"] + val_segment_id: ["1", "20230905134255"] + data_dir: D:\data\scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 1 + num_workers: 0 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_erosion: 0 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: false + strict_sampling_only_ink_val: false + ink_label_dir: D:\labels\231223 + min_crop_num_offset: 8 + chunks_load: true + label_downscale: 1 + automatic_non_ink_labels: false + use_zarr: true + zarr_dir: E:\data\zarrs + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: false + zarr_load_in_memory: false diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run_docker.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run_docker.yaml new file mode 100644 index 0000000..318a5ac --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/fast_dev_run_docker.yaml @@ -0,0 +1,65 @@ +# pytorch_lightning==2.0.6 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + callbacks: + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/mean_fbeta_auprc + mode: max + save_top_k: 1 + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + mode: min + patience: 5 + fast_dev_run: true + gradient_clip_val: 1.0 + gradient_clip_algorithm: "norm" + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + pred_shape: null + size: 64 + lr: 2.0e-05 + unet_feature_size: 16 + segformer_model_size: 1 +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - ["1", "20230530172803"] + val_segment_id: ["1", "20230601204340"] + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + patch_stride_train: 8 + patch_stride_val: 32 + batch_size: 1 + num_workers: 0 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + ink_label_dir: data/labels/dev + label_downscale: 1 + automatic_non_ink_labels: false diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run.yaml new file mode 100644 index 0000000..a50398f --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run.yaml @@ -0,0 +1,105 @@ +# pytorch_lightning==2.0.6 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + project: scroll-ink-det-debugging + job_type: train + log_model: all + name: valid=20230827161847_64x64_z35-45_i3d + callbacks: + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/BinaryAveragePrecision + mode: max + save_top_k: 1 + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + mode: min + patience: 5 + fast_dev_run: false + max_epochs: 3 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: 150 + limit_val_batches: 150 + limit_test_batches: 150 + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: "norm" + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + pred_shape: null + size: 64 + lr: 2.0e-05 + unet_feature_size: 16 + segformer_model_size: 1 +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - ["1", "20231210121320_superseded"] + val_segment_id: ["1", "20230905134255"] + data_dir: D:\data\scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 3 + num_workers: 0 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_erosion: 0 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: false + strict_sampling_only_ink_val: false + ink_label_dir: D:\labels\231223 + min_crop_num_offset: 8 + chunks_load: true + label_downscale: 1 + automatic_non_ink_labels: false + use_zarr: true + zarr_dir: E:\data\zarrs + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: false diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run_docker.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run_docker.yaml new file mode 100644 index 0000000..3c7ef85 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/staging_run_docker.yaml @@ -0,0 +1,101 @@ +# pytorch_lightning==2.0.6 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20230601204340_64x64_z15-47_UNet3dSegformerPLModel + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: step + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + monitor: val/mean_fbeta_auprc + mode: max + save_top_k: 1 + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + mode: min + patience: 5 + fast_dev_run: false + max_epochs: 3 + limit_train_batches: 100 + limit_val_batches: 100 + gradient_clip_val: 1.0 + gradient_clip_algorithm: "norm" + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + pred_shape: null + size: 64 + lr: 2.0e-05 + unet_feature_size: 16 + segformer_model_size: 1 +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - ["1", "20230530172803"] + val_segment_id: ["1", "20230601204340"] + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + patch_stride_train: 8 + patch_stride_val: 32 + batch_size: 1 + num_workers: 0 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + ink_label_dir: data/labels/dev + label_downscale: 1 + automatic_non_ink_labels: false diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_1321_no0901c3_no5351.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_1321_no0901c3_no5351.yaml new file mode 100644 index 0000000..03a7364 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_1321_no0901c3_no5351.yaml @@ -0,0 +1,230 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20231210121321_64x64_z15-47_UNet3dSegformerPLModel_no901C3_no5351 + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20231005123336_C3' + - - '1' + - '20230826170124' + - - '1' + - '20231221180250_superseded' + - - '1' + - 20230929220926_C3 + - - '1' + - '20231031143852' + - - '1' + - 20231007101616_superseded_C1 + - - '1' + - '20230601204340' + - - '1' + - 20231022170901_C1 + - - '1' + - '20231012184422_superseded_C3' + - - '1' + - 20231012184422_superseded_C2 + - - '1' + - '20230904135535' + - - '1' + - 20231007101616_superseded_C2 + - - '1' + - '20230702185753' + - - '1' + - 20231007101616_superseded_C3 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231022170901_C2 + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + - - '1' + - 20230929220926_C2 + - - '1' + - 20231005123336_C2 + val_segment_id: + - '1' + - '20231210121321' + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3.yaml new file mode 100644 index 0000000..544984b --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3.yaml @@ -0,0 +1,234 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20231005123336_C3_64x64_z15-47_UNet3dSegformerPLModel + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20230702185753' + - - '1' + - '20230826170124' + - - '1' + - '20231221180250_superseded' + - - '1' + - 20230929220926_C3 + - - '1' + - '20231031143852' + - - '1' + - 20231007101616_superseded_C1 + - - '1' + - '20230601204340' + - - '1' + - 20231022170901_C1 + - - '1' + - '20231210121321' + - - '1' + - 20231012184422_superseded_C3 + - - '1' + - '20231106155351' + - - '1' + - '20230904135535' + - - '1' + - 20231007101616_superseded_C2 + - - '1' + - 20231012184422_superseded_C2 + - - '1' + - 20231022170901_C3 + - - '1' + - 20231007101616_superseded_C3 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231022170901_C2 + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + - - '1' + - 20230929220926_C2 + - - '1' + - 20231005123336_C2 + val_segment_id: + - '1' + - 20231005123336_C3 + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3_reduced.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3_reduced.yaml new file mode 100644 index 0000000..2366d32 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_3336_C3_reduced.yaml @@ -0,0 +1,212 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20231005123336_C3_64x64_z15-47_UNet3dSegformerPLModel_reduced + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20230702185753' + - - '1' + - '20230826170124' + - - '1' + - '20230601204340' + - - '1' + - '20231210121321' + - - '1' + - 20231012184422_superseded_C3 + - - '1' + - '20231106155351' + - - '1' + - '20230904135535' + - - '1' + - 20231012184422_superseded_C2 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + val_segment_id: + - '1' + - '20231005123336_C3' + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C2.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C2.yaml new file mode 100644 index 0000000..0143bb5 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C2.yaml @@ -0,0 +1,234 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20231012184422_superseded_C2_64x64_z15-47_UNet3dSegformerPLModel + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20231005123336_C3' + - - '1' + - '20230826170124' + - - '1' + - '20231221180250_superseded' + - - '1' + - 20230929220926_C3 + - - '1' + - '20231031143852' + - - '1' + - 20231007101616_superseded_C1 + - - '1' + - '20230601204340' + - - '1' + - 20231022170901_C1 + - - '1' + - '20231210121321' + - - '1' + - '20230702185753' + - - '1' + - '20231106155351' + - - '1' + - '20230904135535' + - - '1' + - 20231007101616_superseded_C2 + - - '1' + - 20231012184422_superseded_C3 + - - '1' + - 20231022170901_C3 + - - '1' + - 20231007101616_superseded_C3 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231022170901_C2 + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + - - '1' + - 20230929220926_C2 + - - '1' + - 20231005123336_C2 + val_segment_id: + - '1' + - '20231012184422_superseded_C2' + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C3.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C3.yaml new file mode 100644 index 0000000..94fc97e --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_4422_C3.yaml @@ -0,0 +1,234 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20231012184422_superseded_C3_64x64_z15-47_UNet3dSegformerPLModel + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20231005123336_C3' + - - '1' + - '20230826170124' + - - '1' + - '20231221180250_superseded' + - - '1' + - 20230929220926_C3 + - - '1' + - '20231031143852' + - - '1' + - 20231007101616_superseded_C1 + - - '1' + - '20230601204340' + - - '1' + - 20231022170901_C1 + - - '1' + - '20231210121321' + - - '1' + - '20230702185753' + - - '1' + - '20231106155351' + - - '1' + - '20230904135535' + - - '1' + - 20231007101616_superseded_C2 + - - '1' + - 20231012184422_superseded_C2 + - - '1' + - 20231022170901_C3 + - - '1' + - 20231007101616_superseded_C3 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231022170901_C2 + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + - - '1' + - 20230929220926_C2 + - - '1' + - 20231005123336_C2 + val_segment_id: + - '1' + - '20231012184422_superseded_C3' + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_5753.yaml b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_5753.yaml new file mode 100644 index 0000000..1ae33b2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/configs/unet3d_segformer/submission/val_5753.yaml @@ -0,0 +1,234 @@ +# pytorch_lightning==2.1.3 +seed_everything: true +trainer: + accelerator: auto + strategy: auto + devices: auto + num_nodes: 1 + precision: 16-mixed + logger: + class_path: pytorch_lightning.loggers.WandbLogger + init_args: + name: valid=20230702185753_C3_64x64_z15-47_UNet3dSegformerPLModel + save_dir: . + version: null + offline: false + dir: null + id: null + anonymous: null + project: null + log_model: true + experiment: null + prefix: '' + checkpoint_name: null + job_type: train + config: null + entity: null + reinit: null + tags: null + group: null + notes: null + magic: null + config_exclude_keys: null + config_include_keys: null + mode: null + allow_val_change: null + resume: null + force: null + tensorboard: null + sync_tensorboard: null + monitor_gym: null + save_code: null + settings: null + callbacks: + - class_path: pytorch_lightning.callbacks.EarlyStopping + init_args: + monitor: val/total_loss + min_delta: 0.0 + patience: 5 + verbose: false + mode: min + strict: true + check_finite: true + stopping_threshold: null + divergence_threshold: null + check_on_train_epoch_end: null + log_rank_zero_only: false + - class_path: pytorch_lightning.callbacks.LearningRateMonitor + init_args: + logging_interval: epoch + log_momentum: false + log_weight_decay: false + - class_path: pytorch_lightning.callbacks.ModelCheckpoint + init_args: + dirpath: null + filename: null + monitor: val/mean_fbeta_auprc + verbose: false + save_last: null + save_top_k: 1 + save_weights_only: false + mode: max + auto_insert_metric_name: true + every_n_train_steps: null + train_time_interval: null + every_n_epochs: null + save_on_train_epoch_end: null + enable_version_counter: true + - class_path: vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.callbacks.WandbSaveConfigCallback + init_args: + config_filename: config_pl.yaml + fast_dev_run: false + max_epochs: 15 + min_epochs: null + max_steps: -1 + min_steps: null + max_time: null + limit_train_batches: null + limit_val_batches: null + limit_test_batches: null + limit_predict_batches: null + overfit_batches: 0.0 + val_check_interval: null + check_val_every_n_epoch: 1 + num_sanity_val_steps: null + log_every_n_steps: null + enable_checkpointing: null + enable_progress_bar: null + enable_model_summary: null + accumulate_grad_batches: 1 + gradient_clip_val: 1.0 + gradient_clip_algorithm: norm + deterministic: null + benchmark: true + inference_mode: true + use_distributed_sampler: true + profiler: null + detect_anomaly: false + barebones: false + plugins: null + sync_batchnorm: false + reload_dataloaders_every_n_epochs: 0 + default_root_dir: null +ckpt_path: null +model: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.UNet3dSegformerPLModel + init_args: + smooth_factor: 0.1 + dice_weight: 0.5 + lr: 2.0e-05 + bce_pos_weight: null + metric_thresh: 0.5 + metric_gt_ink_thresh: 0.05 + unet_feature_size: 16 + unet_out_channels: 32 + unet_module_type: resnet_se + se_type_str: null + depth_pool_fn: max + segformer_model_size: 1 + dropout: 0.1 + in_channels: 1 + ckpt_path: null + ema: false +data: + class_path: vesuvius_challenge_rnd.scroll_ink_detection.ScrollPatchDataModule + init_args: + train_segment_ids: + - - '1' + - '20231005123336_C3' + - - '1' + - '20230826170124' + - - '1' + - '20231221180250_superseded' + - - '1' + - 20230929220926_C3 + - - '1' + - '20231031143852' + - - '1' + - 20231007101616_superseded_C1 + - - '1' + - '20230601204340' + - - '1' + - 20231022170901_C1 + - - '1' + - '20231210121321' + - - '1' + - 20231012184422_superseded_C3 + - - '1' + - '20231106155351' + - - '1' + - '20230904135535' + - - '1' + - 20231007101616_superseded_C2 + - - '1' + - 20231012184422_superseded_C2 + - - '1' + - 20231022170901_C3 + - - '1' + - 20231007101616_superseded_C3 + - - '1' + - '20230701020044' + - - '1' + - '20230905134255' + - - '1' + - 20231022170901_C2 + - - '1' + - 20231005123336_C4 + - - '1' + - '20231016151002' + - - '1' + - 20230929220926_C2 + - - '1' + - 20231005123336_C2 + val_segment_id: + - '1' + - '20230702185753' + ink_label_dir: data/labels + data_dir: data/scrolls + z_min: 15 + z_max: 47 + size: 64 + tile_size: 256 + min_labeled_coverage_frac: 1.0 + patch_stride_train: 8 + patch_stride_val: 32 + downsampling: null + batch_size: 32 + num_workers: 4 + blur_ink_labels: false + ink_labels_blur_kernel_size: 17 + ink_dilation_kernel_size: 256 + min_ink_component_size: 1000 + label_downscale: 1 + ink_erosion: 0 + ignore_idx: -100 + clip_min: 0 + clip_max: 255 + patch_train_stride_strict: 8 + patch_val_stride_strict: 8 + strict_sampling_only_ink_train: true + strict_sampling_only_ink_val: true + min_crop_num_offset: 8 + chunks_load: true + use_zarr: false + x_chunk_save_size: 512 + y_chunk_save_size: 512 + z_chunk_save_size: 32 + skip_save_zarr_if_exists: true + zarr_load_in_memory: true + zarr_dir: data/zarrs + model_prediction_dir: null + model_based_ink_correction_thresh_train: 0.1 + model_based_ink_correction_thresh_val: 0.1 + model_based_non_ink_correction_thresh_train: 0.3 + model_based_non_ink_correction_thresh_val: 0.3 + clean_up_ink_labels_train: false + clean_up_ink_labels_val: false + clean_up_non_ink_labels_train: false + clean_up_non_ink_labels_val: false + p_0_ink: 0.3 + p_2_ink: 0.6 + p_non_ink: 0.1 + automatic_non_ink_labels: false + cache_memmaps: true + memmap_dir: data/memmaps diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/run_experiment.py b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/run_experiment.py new file mode 100644 index 0000000..bf690a3 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/experiment_runner/run_experiment.py @@ -0,0 +1,78 @@ +import logging + +import segmentation_models_pytorch.losses # noqa: F401 +from dotenv import load_dotenv +from pytorch_lightning.cli import ArgsType, LightningCLI +from pytorch_lightning.loggers import WandbLogger + +import vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.schedulers # noqa: F401 +from vesuvius_challenge_rnd.fragment_ink_detection.experiment_runner.util import TrainerWandb +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.util import compile_if_possible + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s" +) + + +class ScrollLightningCLI(LightningCLI): + def add_arguments_to_parser(self, parser): + parser.link_arguments("data.ignore_idx", "model.init_args.ignore_idx") + parser.link_arguments( + "data.patch_depth", "model.init_args.patch_depth", apply_on="instantiate" + ) + parser.link_arguments( + "data.patch_height", "model.init_args.patch_height", apply_on="instantiate" + ) + parser.link_arguments( + "data.patch_width", "model.init_args.patch_width", apply_on="instantiate" + ) + parser.link_arguments( + "data.pred_shape", "model.init_args.pred_shape", apply_on="instantiate" + ) + parser.link_arguments( + ("data.z_max", "data.z_min"), + target="model.init_args.z_extent", + compute_fn=lambda x, y: x - y, + apply_on="instantiate", + ) + parser.link_arguments("data.size", "model.init_args.size", apply_on="instantiate") + + def before_fit(self): + """Method to be run before the fitting process.""" + load_dotenv() # take environment variables from .env. + + if isinstance(self.trainer.logger, WandbLogger): + # log gradients and model topology + self.trainer.logger.watch(self.model, log_graph=False) + + # self.model = compile_if_possible(self.model) + + def after_fit(self): + """Method to be run after the fitting process.""" + checkpoint_callback = self.trainer.checkpoint_callback + if checkpoint_callback is not None: + logging.info(f"Best model saved to: {checkpoint_callback.best_model_path}") + + +def cli_main(args: ArgsType | None = None) -> ScrollLightningCLI: + """Main CLI entry point. + + Args: + args (ArgsType, optional): Command-line arguments. + + Returns: + ScrollLightningCLI: An instance of the ScrollLightningCLI class. + """ + return ScrollLightningCLI( + trainer_class=TrainerWandb, + trainer_defaults={"max_epochs": 100, "precision": "16-mixed", "benchmark": True}, + save_config_kwargs={ + "config_filename": "config_pl.yaml", + "overwrite": True, + }, + args=args, + ) + + +if __name__ == "__main__": + cli_main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/__init__.py new file mode 100644 index 0000000..f9aedcb --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/__init__.py @@ -0,0 +1,22 @@ +from .data import ( + CombinedDataModule, + EvalScrollPatchDataModule, + ScrollPatchDataModule, + ScrollPatchDataModuleEval, + SemiSupervisedScrollPatchDataModule, +) +from .lit_models import ( + CNN3dMANetPLModel, + CNN3DSegformerPLModel, + Cnn3dto2dCrackformerLitModel, + CNN3dUnetPlusPlusPLModel, + HrSegNetLitModel, + I3DMeanTeacherPLModel, + LitDomainAdversarialSegmenter, + MedNextV1SegformerPLModel, + MedNextV13dto2dPLModel, + RegressionPLModel, + SwinUNETRSegformerPLModel, + UNet3dSegformerPLModel, + UNETRSegformerPLModel, +) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/callbacks.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/callbacks.py new file mode 100644 index 0000000..9673959 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/callbacks.py @@ -0,0 +1,44 @@ +from typing import Any, Literal, Optional + +from collections.abc import Sequence +from pathlib import Path + +import pytorch_lightning as pl +import torch +from pytorch_lightning.callbacks import BasePredictionWriter + + +class PredictionWriter(BasePredictionWriter): + def __init__( + self, + output_dir: str, + write_interval: Literal["batch", "epoch", "batch_and_epoch"] = "epoch", + ): + super().__init__(write_interval) + self.output_dir = Path(output_dir) + + def write_on_batch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + prediction: Any, + batch_indices: Sequence[int] | None, + batch: Any, + batch_idx: int, + dataloader_idx: int, + ) -> None: + batch_pred_dir = self.output_dir / str(dataloader_idx) + batch_pred_dir.mkdir(exist_ok=True, parents=True) + out_path = batch_pred_dir / f"{batch_idx}.pt" + torch.save(prediction, out_path) + + def write_on_epoch_end( + self, + trainer: "pl.Trainer", + pl_module: "pl.LightningModule", + predictions: Sequence[Any], + batch_indices: Sequence[Any] | None, + ) -> None: + self.output_dir.mkdir(exist_ok=True, parents=True) + out_path = self.output_dir / "predictions.pt" + torch.save(predictions, out_path) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/__init__.py new file mode 100644 index 0000000..7c11f16 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/__init__.py @@ -0,0 +1,6 @@ +from .eval_scroll_patch_data_module import EvalScrollPatchDataModule +from .scroll_fragment_combined_data_module import CombinedDataModule +from .scroll_patch_data_module import ScrollPatchDataModule +from .scroll_patch_data_module_eval import ScrollPatchDataModuleEval +from .scroll_patch_data_module_eval_mmap import ScrollPatchDataModuleEvalMmap +from .semi_supervised_scroll_patch_data_module import SemiSupervisedScrollPatchDataModule diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset.py new file mode 100644 index 0000000..7e4bae2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset.py @@ -0,0 +1,144 @@ +import numpy as np +import torch +import torch.nn.functional as F +import zarr +from skimage.util import img_as_ubyte +from torch.utils.data import Dataset + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_memmap_dataset import ( + _verify_patch_positions, + cut_and_paste_depth, +) + +ArrayLike = np.ndarray | np.memmap | zarr.Array + + +class BlockZConstantDataset(Dataset): + """A block dataset for a single segment.""" + + def __init__( + self, + img_stack: ArrayLike, + patch_positions: np.ndarray, + size: int, + z_start: int, + z_extent: int = 30, + label_downscale: int = 4, + labels=None, + transform=None, + augment: bool = False, + non_ink_ignored_patches: np.ndarray | None = None, + ignore_idx: int = -100, + min_crop_num_offset: int = 8, + z_reverse: bool = False, + padding_yx: tuple[int, int] = (0, 0), + clip_min: int = 0, + clip_max: int = 255, + ): + pad_y, pad_x = padding_yx + img_stack_z, img_stack_y, img_stack_x = img_stack.shape + _verify_patch_positions((img_stack_y + pad_y, img_stack_x + pad_x), patch_positions) + if non_ink_ignored_patches is not None: + if len(non_ink_ignored_patches) != len(patch_positions): + raise ValueError( + f"`non_ink_ignored_patches` length ({len(non_ink_ignored_patches)}) must be the same length as the `patch_positions` ({len(patch_positions)})." + ) + + if not (0 <= z_start <= img_stack_z): + raise ValueError( + f"z-start be between 0 (inclusive) and image depth {img_stack_z} (inclusive)." + ) + + if not (0 < z_extent <= img_stack_z): + raise ValueError( + f"z-start be between 0 (exclusive) and image depth {img_stack_z} (inclusive)." + ) + + if z_start + z_extent > img_stack_z: + raise ValueError( + f"z_start ({z_start}) + z_extent ({z_extent}) cannot exceed image depth ({img_stack_z})." + ) + + self.img_stack = img_stack + self.patch_positions = patch_positions + self.z_start = z_start + self.z_extent = z_extent + self.size = size + self.labels = labels + self.label_downscale = label_downscale + self.non_ink_ignored_patches = non_ink_ignored_patches + self.ignore_idx = ignore_idx + self.z_reverse = z_reverse + + self.transform = transform + self.augment = augment + self.min_crop_num_offset = min_crop_num_offset + self.clip_min = clip_min + self.clip_max = clip_max + + @property + def z_end(self) -> int: + return self.z_start + self.z_extent + + def __len__(self) -> int: + return len(self.patch_positions) + + def _preprocess_img_patch(self, img_patch: np.ndarray) -> np.ndarray: + # Pad image. + pad_y = self.size - img_patch.shape[1] + pad_x = self.size - img_patch.shape[2] + img_patch = np.pad(img_patch, [(0, 0), (0, pad_y), (0, pad_x)], constant_values=0) + + # Rescale intensities and convert to uint8. + img_patch = img_as_ubyte(img_patch) + + # Clip intensities + img_patch = np.clip(img_patch, self.clip_min, self.clip_max) + + # Swap axes (move depth dimension to be last). + img_patch = np.moveaxis(img_patch, 0, -1) + + return img_patch + + def __getitem__(self, idx: int): + x1, y1, x2, y2 = self.patch_positions[idx] + if self.z_reverse: # Reverse along z-dimension if necessary + z1 = -self.z_end + z2 = -self.z_start + z_step = -1 + else: + z1 = self.z_start + z2 = self.z_end + z_step = 1 + + img_patch = self.img_stack[z1:z2, y1:y2, x1:x2][::z_step] + img_patch = self._preprocess_img_patch(img_patch) + label_patch = self.labels[y1:y2, x1:x2, None] + if self.non_ink_ignored_patches is not None: + non_ink_is_ignored = self.non_ink_ignored_patches[idx] + if non_ink_is_ignored: + # Replace all 0s (non-ink) with ignore_idx + label_patch = np.where(label_patch == 0, self.ignore_idx, label_patch) + + if not self.augment: + img_patch, label_patch = self._transform_if_needed(img_patch, label_patch) + return img_patch, label_patch, self.patch_positions[idx] + else: + img_patch = cut_and_paste_depth( + img_patch, self.z_extent, min_crop_num_offset=self.min_crop_num_offset + ) + img_patch, label_patch = self._transform_if_needed(img_patch, label_patch) + return img_patch, label_patch + + def _transform_if_needed( + self, image: np.ndarray, label: np.ndarray + ) -> tuple[torch.Tensor, torch.Tensor]: + if self.transform: + data = self.transform(image=image, mask=label) + image = data["image"].unsqueeze(0) + label = data["mask"] + label = F.interpolate( + label.unsqueeze(0), + (self.size // self.label_downscale, self.size // self.label_downscale), + ).squeeze(0) + return image, label diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset_eval.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset_eval.py new file mode 100644 index 0000000..a99cc94 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/block_dataset_eval.py @@ -0,0 +1,103 @@ +import numpy as np +import torch +import zarr +from skimage.util import img_as_ubyte +from torch.utils.data import Dataset + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_memmap_dataset import ( + _verify_patch_positions, +) + +ArrayLike = np.ndarray | np.memmap | zarr.Array + + +class BlockZConstantDatasetEval(Dataset): + """A block dataset for a single segment.""" + + def __init__( + self, + img_stack: ArrayLike, + patch_positions: np.ndarray, + size: int, + z_start: int, + z_extent: int = 30, + transform=None, + z_reverse: bool = False, + padding_yx: tuple[int, int] = (0, 0), + clip_min: int = 0, + clip_max: int = 255, + ): + pad_y, pad_x = padding_yx + img_stack_z, img_stack_y, img_stack_x = img_stack.shape + _verify_patch_positions((img_stack_y + pad_y, img_stack_x + pad_x), patch_positions) + + if not (0 <= z_start <= img_stack_z): + raise ValueError( + f"z-start be between 0 (inclusive) and image depth {img_stack_z} (inclusive)." + ) + + if not (0 < z_extent <= img_stack_z): + raise ValueError( + f"z-start be between 0 (exclusive) and image depth {img_stack_z} (inclusive)." + ) + + if z_start + z_extent > img_stack_z: + raise ValueError( + f"z_start ({z_start}) + z_extent ({z_extent}) cannot exceed image depth ({img_stack_z})." + ) + + self.img_stack = img_stack + self.patch_positions = patch_positions + self.z_start = z_start + self.z_extent = z_extent + self.size = size + self.z_reverse = z_reverse + self.transform = transform + self.clip_min = clip_min + self.clip_max = clip_max + + @property + def z_end(self) -> int: + return self.z_start + self.z_extent + + def __len__(self) -> int: + return len(self.patch_positions) + + def _preprocess_img_patch(self, img_patch: np.ndarray) -> np.ndarray: + # Pad image. + pad_y = self.size - img_patch.shape[1] + pad_x = self.size - img_patch.shape[2] + img_patch = np.pad(img_patch, [(0, 0), (0, pad_y), (0, pad_x)], constant_values=0) + + # Rescale intensities and convert to uint8. + img_patch = img_as_ubyte(img_patch) + + # Clip intensities + img_patch = np.clip(img_patch, self.clip_min, self.clip_max) + + # Swap axes (move depth dimension to be last). + img_patch = np.moveaxis(img_patch, 0, -1) + + return img_patch + + def __getitem__(self, idx: int): + x1, y1, x2, y2 = self.patch_positions[idx] + if self.z_reverse: # Reverse along z-dimension if necessary + z1 = -self.z_end + z2 = -self.z_start + z_step = -1 + else: + z1 = self.z_start + z2 = self.z_end + z_step = 1 + + img_patch = self.img_stack[z1:z2, y1:y2, x1:x2][::z_step] + img_patch = self._preprocess_img_patch(img_patch) + img_patch = self._transform_if_needed(img_patch) + return img_patch, self.patch_positions[idx] + + def _transform_if_needed(self, image: np.ndarray) -> tuple[torch.Tensor]: + if self.transform: + data = self.transform(image=image) + image = data["image"].unsqueeze(0) + return image diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/eval_scroll_patch_data_module.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/eval_scroll_patch_data_module.py new file mode 100644 index 0000000..652e7a4 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/eval_scroll_patch_data_module.py @@ -0,0 +1,117 @@ +from pathlib import Path + +import albumentations as A +from albumentations.pytorch import ToTensorV2 +from torch.utils.data import DataLoader + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data import ScrollSegment +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.scroll_patch_dataset import ( + ScrollPatchDataset, +) + + +class EvalScrollPatchDataModule(BasePatchDataModule): + """A data module for handling evaluation and prediction of patches. + + This class extends BasePatchDataModule and includes functionality + specific to the evaluation and prediction stages, such as dataset + setup and transformation. + + Attributes: + segment (ScrollSegment): A scroll segment. + data_predict (ScrollPatchDataset): Dataset for the prediction stage. + data_dir (Path): Directory containing the scroll data. Defaults to SCROLL_DATA_DIR. + z_min (int): Minimum z-slice to include. Defaults to 27. + z_max (int): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int]): Shape of the patches. Defaults to (512, 512). + patch_stride (int): Stride for patch creation. Defaults to 256. + downsampling (int | None): Downsampling factor, 1 if None. Defaults to None. + num_workers (int): Number of workers for data loading. Defaults to 0. + batch_size (int): Batch size for data loading. Defaults to 4. + """ + + def __init__( + self, + segment: ScrollSegment, + data_dir: Path = SCROLL_DATA_DIR, + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + downsampling: int | None = None, + batch_size: int = 4, + num_workers: int = 0, + ): + """Initialize the EvalPatchDataModule. + + Args: + segment (ScrollSegment): A scroll segment. + data_dir (Path, optional): Directory containing the scroll data. Defaults to SCROLL_DATA_DIR. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + downsampling (int | None, optional): Downsampling factor, 1 if None. Defaults to None. + batch_size (int, optional): Batch size for data loading. Defaults to 4. + num_workers (int, optional): Number of workers for data loading. Defaults to 0. + """ + super().__init__( + data_dir, + z_min, + z_max, + patch_surface_shape, + patch_stride, + downsampling, + batch_size, + num_workers, + ) + self.segment = segment + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "predict"). + """ + if stage == "predict" or stage is None: + self.data_predict = ScrollPatchDataset( + [self.segment], + transform=self.predict_transform(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min, + z_max=self.z_max, + ) + + def predict_transform(self) -> A.Compose: + """Define the transformations for the prediction stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + if self.downsampling != 1: + height = self.patch_surface_shape[0] // self.downsampling + width = self.patch_surface_shape[1] // self.downsampling + transforms += [A.Resize(height, width, always_apply=True)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2()] + return A.Compose(transforms) + + def predict_dataloader(self) -> DataLoader: + """Create a data loader for the prediction stage. + + Returns: + DataLoader: A PyTorch DataLoader for prediction. + """ + return DataLoader( + self.data_predict, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=False, + pin_memory=True, + drop_last=False, + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/ink_label_correction.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/ink_label_correction.py new file mode 100644 index 0000000..78d162a --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/ink_label_correction.py @@ -0,0 +1,98 @@ +import numpy as np +from skimage.util import img_as_float32 + + +def apply_model_based_ink_label_correction( + ink_labels_binarized: np.ndarray, + non_ink_labels: np.ndarray, + ink_pred: np.ndarray, + model_based_ink_label_thresh: float = 0.1, +) -> tuple[np.ndarray, np.ndarray]: + ink_pred = img_as_float32(ink_pred) + ink_pred_binarized_ink_region = ink_pred > model_based_ink_label_thresh + new_ink_labels = ink_labels_binarized & ink_pred_binarized_ink_region + + # Fix non-ink labels. Wherever it previously overlapped with old ink labels, and it was removed, assign it to BG. + non_ink_labels = non_ink_labels.astype(bool) + non_ink_labels = np.where(~new_ink_labels & ink_labels_binarized, 1, non_ink_labels) + + return new_ink_labels, non_ink_labels + + +def apply_model_based_non_ink_label_correction( + non_ink_mask: np.ndarray, + ink_mask: np.ndarray, + ink_pred: np.ndarray, + ink_idx: int = 1, + model_based_ink_label_thresh: float = 0.3, +) -> tuple[np.ndarray, np.ndarray]: + ink_pred = img_as_float32(ink_pred) + non_ink_mask = non_ink_mask.astype(bool) + ink_pred_binarized_non_ink_region = ink_pred > model_based_ink_label_thresh + new_ink_mask = np.where(non_ink_mask & ink_pred_binarized_non_ink_region, ink_idx, ink_mask) + new_non_ink_mask = ~new_ink_mask & non_ink_mask + return new_ink_mask, new_non_ink_mask + + +def apply_model_based_label_correction( + ink_mask: np.ndarray, + non_ink_mask: np.ndarray, + ink_pred: np.ndarray, + ink_label_thresh: float = 0.05, + model_based_ink_correction_thresh: float = 0.1, + model_based_non_ink_correction_thresh: float = 0.3, + clean_up_ink_labels: bool = True, + clean_up_non_ink_labels: bool = True, + ignore_idx: int = -100, +) -> tuple[np.ndarray, np.ndarray]: + """ + + :param ink_mask: + :param non_ink_mask: + :param ink_pred: + :param ink_label_thresh: + :param model_based_ink_correction_thresh: anything less than model_based_ink_label_thresh within the ink mask will + be excluded from the ink mask. + :param model_based_non_ink_correction_thresh: anything greater than model_based_non_ink_label_thresh within the non-ink + mask will be added to the non-ink labels + :param clean_up_ink_labels: + :param clean_up_non_ink_labels: + :param ignore_idx: + :return: + """ + ink_mask_orig_dtype = ink_mask.dtype + non_ink_mask_orig_dtype = non_ink_mask.dtype + ink_pred = img_as_float32(ink_pred) + + # Zero the ignored index regions out. + ignore_mask = ink_mask == ignore_idx + ink_mask = np.where(ignore_mask, 0, ink_mask) + + ink_mask = ink_mask > ink_label_thresh + + # Clean up ink labels. + if clean_up_ink_labels: + ink_mask, non_ink_mask = apply_model_based_ink_label_correction( + ink_mask, + non_ink_mask, + ink_pred, + model_based_ink_label_thresh=model_based_ink_correction_thresh, + ) + + # Clean up non-ink labels. (Add nearby missed ink based on model's predictions) + if clean_up_non_ink_labels: + ink_mask, non_ink_mask = apply_model_based_non_ink_label_correction( + non_ink_mask, + ink_mask, + ink_pred, + model_based_ink_label_thresh=model_based_non_ink_correction_thresh, + ) + + # Add the ignored index back. + ink_mask = np.where(ignore_mask, ignore_idx, ink_mask) + + # Fix dtype. + ink_mask = ink_mask.astype(ink_mask_orig_dtype) + non_ink_mask = non_ink_mask.astype(non_ink_mask_orig_dtype) + + return ink_mask, non_ink_mask diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/memmap.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/memmap.py new file mode 100644 index 0000000..67fa115 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/memmap.py @@ -0,0 +1,72 @@ +from typing import Literal + +import gc +import os +from pathlib import Path + +import numpy as np + +from vesuvius_challenge_rnd import DATA_DIR +from vesuvius_challenge_rnd.data import ScrollSegment + +MEMMAP_DIR = DATA_DIR / "memmaps" + + +def segment_to_memmap_path( + segment: ScrollSegment, + memmap_dir: Path = MEMMAP_DIR, + z_min: int | None = None, + z_max: int | None = None, +) -> Path: + if (z_max is None and z_min is not None) or (z_max is not None and z_min is None): + raise ValueError("z_max and z_min must be both None or be set to integer values.") + + stem = segment.segment_name if not segment.is_subsegment else segment.segment_name_orig + if z_max is not None and z_min is not None: + if z_min > z_max: + raise ValueError(f"z_min ({z_min}) must be less than z_max ({z_max}).") + stem = "_".join((stem, f"{z_min}-{z_max}")) + return memmap_dir / segment.scroll_id / f"{stem}.npy" + + +def save_segment_as_memmap( + segment: ScrollSegment, + output_memmap_path: Path | None, + load_in_memory: bool = False, + z_min: int | None = None, + z_max: int | None = None, + memmap_dir: Path = MEMMAP_DIR, +) -> np.memmap: + if output_memmap_path is None: + output_memmap_path = segment_to_memmap_path( + segment, memmap_dir=memmap_dir, z_min=z_min, z_max=z_max + ) + + if load_in_memory: + img_stack = segment.load_volume(z_start=z_min, z_end=z_max, preprocess=False) + else: + img_stack = segment.load_volume_as_memmap(z_start=z_min, z_end=z_max) + + output_memmap_path.parent.mkdir(parents=True, exist_ok=True) + np.save(str(output_memmap_path), img_stack) + + if not load_in_memory: + delete_memmap(img_stack) + + return img_stack + + +def load_segment_as_memmap( + memmap_path: Path, + mmap_mode: Literal[None, "r+", "r", "w+", "c"] = "r", +) -> np.memmap: + return np.load(str(memmap_path), mmap_mode=mmap_mode, allow_pickle=True) + + +def delete_memmap(img_stack_ref: np.memmap) -> None: + filename = img_stack_ref.filename + img_stack_ref._mmap.close() + del img_stack_ref + gc.collect() + if os.path.exists(filename): + os.remove(filename) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_memmap_dataset.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_memmap_dataset.py new file mode 100644 index 0000000..09ba9cb --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_memmap_dataset.py @@ -0,0 +1,215 @@ +import random + +import numpy as np +import torch.nn.functional as F +from skimage.util import img_as_ubyte +from torch.utils.data import Dataset + + +class PatchMemMapDataset(Dataset): + """A patch memory-mapped dataset for a single segment.""" + + def __init__( + self, + img_stack_ref: np.memmap, + patch_positions: np.ndarray, + size: int, + z_extent: int = 30, + label_downscale: int = 4, + labels=None, + transform=None, + augment: bool = False, + non_ink_ignored_patches: np.ndarray | None = None, + ignore_idx: int = -100, + min_crop_num_offset: int = 8, + z_reverse: bool = False, + padding_yx: tuple[int, int] = (0, 0), + clip_min: int = 0, + clip_max: int = 255, + ): + # pad_y, pad_x = padding_yx + # img_stack_y, img_stack_x = img_stack_ref.shape[1:3] + # _verify_patch_positions((img_stack_y + pad_y, img_stack_x + pad_x), patch_positions) + _verify_patch_positions(img_stack_ref.shape[:2], patch_positions) + if non_ink_ignored_patches is not None: + if len(non_ink_ignored_patches) != len(patch_positions): + raise ValueError( + f"`non_ink_ignored_patches` length ({len(non_ink_ignored_patches)}) must be the same length as the `patch_positions` ({len(patch_positions)})." + ) + self.img_stack_ref = img_stack_ref + self.patch_positions = patch_positions + self.z_extent = z_extent + self.size = size + self.labels = labels + self.label_downscale = label_downscale + self.non_ink_ignored_patches = non_ink_ignored_patches + self.ignore_idx = ignore_idx + self.z_reverse = z_reverse + + self.transform = transform + self.augment = augment + self.min_crop_num_offset = min_crop_num_offset + self.clip_min = clip_min + self.clip_max = clip_max + + def __len__(self): + return len(self.patch_positions) + + def _preprocess_img_patch(self, img_patch: np.ndarray) -> np.ndarray: + # Pad image. + pad_y = self.size - img_patch.shape[1] + pad_x = self.size - img_patch.shape[2] + img_patch = np.pad(img_patch, [(0, 0), (0, pad_y), (0, pad_x)], constant_values=0) + + # Rescale intensities and convert to uint8. + img_patch = img_as_ubyte(img_patch) + + # Reverse along z-dimension if necessary + if self.z_reverse: + img_patch = img_patch[::-1] + + # Clip intensities + img_patch = np.clip(img_patch, self.clip_min, self.clip_max) + + # Swap axes (move depth dimension to be last). + img_patch = np.moveaxis(img_patch, 0, -1) + + return img_patch + + def __getitem__(self, idx: int): + x1, y1, x2, y2 = self.patch_positions[idx] + img_patch = self.img_stack_ref[y1:y2, x1:x2] + label_patch = self.labels[y1:y2, x1:x2, None] + if self.non_ink_ignored_patches is not None: + non_ink_is_ignored = self.non_ink_ignored_patches[idx] + if non_ink_is_ignored: + # Replace all 0s (non-ink) with ignore_idx + label_patch = np.where(label_patch == 0, self.ignore_idx, label_patch) + + if not self.augment: + img_patch, label_patch = self._transform_if_needed(img_patch, label_patch) + return img_patch, label_patch, self.patch_positions[idx] + else: + img_patch = cut_and_paste_depth( + img_patch, self.z_extent, min_crop_num_offset=self.min_crop_num_offset + ) + img_patch, label_patch = self._transform_if_needed(img_patch, label_patch) + return img_patch, label_patch + + def _transform_if_needed(self, image, label): + if self.transform: + data = self.transform(image=image, mask=label) + image = data["image"].unsqueeze(0) + label = data["mask"] + label = F.interpolate( + label.unsqueeze(0), + (self.size // self.label_downscale, self.size // self.label_downscale), + ).squeeze(0) + return image, label + + +class PatchMemMapDatasetUnlabeled(PatchMemMapDataset): + """A patch memory-mapped dataset for a single segment.""" + + def __init__( + self, + img_stack_ref: np.memmap, + patch_positions: np.ndarray, + size: int, + z_extent: int = 30, + transform=None, + augment: bool = False, + ): + super().__init__( + img_stack_ref, + patch_positions, + size, + z_extent=z_extent, + transform=transform, + augment=augment, + ) + + def __getitem__(self, idx: int): + x1, y1, x2, y2 = self.patch_positions[idx] + img_patch = self.img_stack_ref[y1:y2, x1:x2] + + if not self.augment: + img_patch = self._transform_image_if_needed(img_patch) + return img_patch, self.patch_positions[idx] + else: + img_patch = cut_and_paste_depth( + img_patch, self.z_extent, min_crop_num_offset=self.min_crop_num_offset + ) + img_patch = self._transform_image_if_needed(img_patch) + return img_patch + + def _transform_image_if_needed(self, image): + if self.transform: + data = self.transform(image=image) + image = data["image"].unsqueeze(0) + return image + + +def cut_and_paste_depth( + image: np.ndarray, z_extent: int, p: float = 0.4, min_crop_num_offset: int = 8 +) -> np.ndarray: + image_tmp = np.zeros_like(image) + + # Random crop. + cropping_num = random.randint(z_extent - min_crop_num_offset, z_extent) + + start_idx = random.randint(0, z_extent - cropping_num) + crop_indices = np.arange(start_idx, start_idx + cropping_num) + + start_paste_idx = random.randint(0, z_extent - cropping_num) + + tmp = np.arange(start_paste_idx, cropping_num) + np.random.shuffle(tmp) + + cutout_idx = random.randint(0, 2) + temporal_random_cutout_idx = tmp[:cutout_idx] + + # Random paste. + image_tmp[..., start_paste_idx : start_paste_idx + cropping_num] = image[..., crop_indices] + + # Random cutout. + if random.random() > p: + image_tmp[..., temporal_random_cutout_idx] = 0 + + image = image_tmp + return image + + +def _verify_patch_positions(img_xy_shape: tuple[int, int], patch_positions: np.ndarray) -> None: + if patch_positions.ndim != 2 or patch_positions.shape[1] != 4: + raise ValueError( + f"Expected patch positions to be N x 4. Found shape {patch_positions.shape}" + ) + + # Check x1 < x2 and y1 < y2 + x1 = patch_positions[:, 0] + y1 = patch_positions[:, 1] + x2 = patch_positions[:, 2] + y2 = patch_positions[:, 3] + if not (x1 < x2).all(): + raise ValueError("x1 must be less than x2.") + + if not (y1 < y2).all(): + raise ValueError("y1 must be less than y2.") + + if x1.min() < 0: + raise ValueError("x1 must be nonnegative.") + + if y1.min() < 0: + raise ValueError("y1 must be nonnegative.") + + # Check x2 <= img width and y2 <= img height + img_width = img_xy_shape[1] + if x2.max() > img_width: + raise ValueError(f"x2 must be less than image width {img_width}. Found max x2 {x2.max()}.") + + img_height = img_xy_shape[0] + if y2.max() > img_height: + raise ValueError( + f"y2 must be less than image height {img_height}. Found y2 max {y2.max()}." + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_sampling.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_sampling.py new file mode 100644 index 0000000..57eb31a --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/patch_sampling.py @@ -0,0 +1,456 @@ +import numpy as np +from patchify import patchify + +from vesuvius_challenge_rnd.patching import patch_index_to_pixel_position +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.util import pad_to_match + + +def find_valid_patch_positions( + ink_mask: np.ndarray, + non_ink_mask: np.ndarray, + segment_mask: np.ndarray, + patch_size: int, + stride: int, + min_non_ink_coverage_frac: float = 0.5, + ink_thresh: float = 0.05, +) -> list[tuple[int, int, int, int]]: + """ + Identifies valid positions for patches in a given area based on ink and non-ink regions. + + This function scans an image using a square patch of a specified size and stride, and + determines valid positions where the patch meets certain criteria of ink and non-ink coverage. + A position is considered valid if it either contains any part of the ink region or meets the + minimum non-ink region coverage threshold. + + Args: + ink_mask (np.ndarray): A binary mask indicating the ink region in the image. + non_ink_mask (np.ndarray): A binary mask indicating the non-ink region in the image. + segment_mask (np.ndarray): A binary mask indicating the segment region of the image. + patch_size (int): The size of the square patch (e.g., 64 for a 64x64 patch). + stride (int): The stride length for moving the patch across the image. + min_non_ink_coverage_frac (float): The minimum required fraction of non-ink coverage in a patch + for it to be considered valid. Defaults to 0.5. + ink_thresh (float): The threshold for a value to be considered ink. Defaults to 0.05. + + + Returns: + list[tuple[tuple[int, int], tuple[int, int]]]: A list of tuples, each containing two tuples. The first tuple + in each pair represents the top-left coordinates (x, y) of a valid patch, and the second tuple represents + the bottom-right coordinates (x + patch_size, y + patch_size). + """ + valid_positions = [] + rows, cols = ink_mask.shape + total_pixels = patch_size * patch_size + + for y1 in range(0, rows - patch_size + 1, stride): + y2 = y1 + patch_size + for x1 in range(0, cols - patch_size + 1, stride): + x2 = x1 + patch_size + ink_patch = ink_mask[y1:y2, x1:x2] + non_ink_patch = non_ink_mask[y1:y2, x1:x2] + segment_patch = segment_mask[y1:y2, x1:x2] + + # Check for overlap with non-segment region. + no_overlap_with_non_segment_region = not np.any(segment_patch == 0) + + # Check for presence of ink region + contains_ink = np.any(ink_patch > ink_thresh) + + # Calculate non-ink region coverage as a fraction + non_ink_coverage_frac = np.sum(non_ink_patch > 0) / total_pixels + + # Check if non-ink + sufficient_non_ink = non_ink_coverage_frac >= min_non_ink_coverage_frac + + if no_overlap_with_non_segment_region: + if contains_ink or sufficient_non_ink: + valid_positions.append((x1, y1, x2, y2)) + + return valid_positions + + +def sample_all_patch_positions_for_segment( + img_stack: np.ndarray, + segment_mask: np.ndarray, + size: int, + tile_size: int, + z_extent: int, + stride: int, +): + """Sample all possible patch positions for a segment within the segment mask.""" + x1_list = list(range(0, img_stack.shape[1] - tile_size + 1, stride)) + y1_list = list(range(0, img_stack.shape[0] - tile_size + 1, stride)) + + valid_xyxys = [] + for a in y1_list: + for b in x1_list: + for yi in range(0, tile_size, size): + for xi in range(0, tile_size, size): + y1 = a + yi + x1 = b + xi + y2 = y1 + size + x2 = x1 + size + + segment_context_window = segment_mask[a : a + tile_size, b : b + tile_size] + no_overlap_with_non_segment_region = not np.any(segment_context_window == 0) + if no_overlap_with_non_segment_region: + valid_xyxys.append([x1, y1, x2, y2]) + assert x2 - x1 == size + assert y2 - y1 == size + assert img_stack[y1:y2, x1:x2].shape == ( + size, + size, + z_extent, + ) + return valid_xyxys + + +def oversample_train_patch_positions_for_segment( + img_stack: np.ndarray, + ink_mask: np.ndarray, + segment_mask: np.ndarray, + size: int, + tile_size: int, + z_extent: int, + stride: int, +): + x1_list = list(range(0, img_stack.shape[1] - tile_size + 1, stride)) + y1_list = list(range(0, img_stack.shape[0] - tile_size + 1, stride)) + + train_positions = [] + for a in y1_list: + for b in x1_list: + for yi in range(0, tile_size, size): + for xi in range(0, tile_size, size): + y1 = a + yi + x1 = b + xi + y2 = y1 + size + x2 = x1 + size + + segment_context_window = segment_mask[a : a + tile_size, b : b + tile_size] + no_overlap_with_non_segment_region = not np.any(segment_context_window == 0) + ink_context_window = ink_mask[a : a + tile_size, b : b + tile_size] + context_window_has_ink = not np.all(ink_context_window < 0.05) + if context_window_has_ink: + if no_overlap_with_non_segment_region: + train_positions.append([x1, y1, x2, y2]) + assert x2 - x1 == size + assert y2 - y1 == size + assert img_stack[y1:y2, x1:x2].shape == ( + size, + size, + z_extent, + ) + return train_positions + + +def get_all_ink_patch_positions( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + patch_size: int, + stride: int, + ink_thresh: float = 0.05, +) -> list[tuple[int, int, int, int]]: + """ + Identifies valid positions for patches in a given area based on ink regions. + + This function scans an image using a square patch of a specified size and stride, and + determines valid positions where the patch meets certain criteria of ink coverage. + A position is considered valid if it either contains any part of the ink region. + + Args: + ink_mask (np.ndarray): A binary mask indicating the ink region in the image. + segment_mask (np.ndarray): A binary mask indicating the segment region of the image. + patch_size (int): The size of the square patch (e.g., 64 for a 64x64 patch). + stride (int): The stride length for moving the patch across the image. + ink_thresh (float): The threshold for a value to be considered ink. Defaults to 0.05. + + + Returns: + list[tuple[tuple[int, int], tuple[int, int]]]: A list of tuples, each containing two tuples. The first tuple + in each pair represents the top-left coordinates (x, y) of a valid patch, and the second tuple represents + the bottom-right coordinates (x + patch_size, y + patch_size). + """ + valid_positions = [] + rows, cols = ink_mask.shape + + for y1 in range(0, rows - patch_size + 1, stride): + y2 = y1 + patch_size + for x1 in range(0, cols - patch_size + 1, stride): + x2 = x1 + patch_size + ink_patch = ink_mask[y1:y2, x1:x2] + segment_patch = segment_mask[y1:y2, x1:x2] + + # Check for overlap with non-segment region. + no_overlap_with_non_segment_region = not np.any(segment_patch == 0) + + # Check for presence of ink region + contains_ink = np.all(ink_patch > ink_thresh) + + if no_overlap_with_non_segment_region: + if contains_ink: + position = (x1, y1, x2, y2) + valid_positions.append(position) + + return valid_positions + + +def get_any_ink_patch_positions( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + patch_size: int, + stride: int, + ink_thresh: float = 0.05, +) -> list[tuple[int, int, int, int]]: + """ + Identifies valid positions for patches in a given area based on ink regions. + + This function scans an image using a square patch of a specified size and stride, and + determines valid positions where the patch meets certain criteria of ink coverage. + A position is considered valid if it either contains any part of the ink region. + + Args: + ink_mask (np.ndarray): A binary mask indicating the ink region in the image. + segment_mask (np.ndarray): A binary mask indicating the segment region of the image. + patch_size (int): The size of the square patch (e.g., 64 for a 64x64 patch). + stride (int): The stride length for moving the patch across the image. + ink_thresh (float): The threshold for a value to be considered ink. Defaults to 0.05. + + + Returns: + list[tuple[tuple[int, int], tuple[int, int]]]: A list of tuples, each containing two tuples. The first tuple + in each pair represents the top-left coordinates (x, y) of a valid patch, and the second tuple represents + the bottom-right coordinates (x + patch_size, y + patch_size). + """ + valid_positions = [] + rows, cols = ink_mask.shape + + for y1 in range(0, rows - patch_size + 1, stride): + y2 = y1 + patch_size + for x1 in range(0, cols - patch_size + 1, stride): + x2 = x1 + patch_size + ink_patch = ink_mask[y1:y2, x1:x2] + segment_patch = segment_mask[y1:y2, x1:x2] + + # Check for overlap with non-segment region. + no_overlap_with_non_segment_region = not np.any(segment_patch == 0) + + # Check for presence of any ink in the patch. + contains_ink = np.any(ink_patch > ink_thresh) + + if no_overlap_with_non_segment_region and contains_ink: + position = (x1, y1, x2, y2) + valid_positions.append(position) + + return valid_positions + + +def get_ink_patch_positions_batched( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + patch_shape: tuple[int, int], + patch_stride: int, + ink_thresh: float = 0.05, + chunk_size: int = 256, + should_pad: bool = True, + all_ink_patches: bool = False, + skip_seg_masked_regions: bool = True, +) -> list[tuple[int, int, int, int]]: + # Pad to same shape. + if ink_mask.shape != segment_mask.shape: + if should_pad: + ink_mask = pad_to_match(ink_mask, segment_mask) + segment_mask = pad_to_match(segment_mask, ink_mask) + else: + raise ValueError( + f"ink mask and segment mask shapes ({ink_mask.shape}, {segment_mask.shape}) do not match." + f"If this is expected, set `should_pad` to True." + ) + + patch_pos_array_full = [] + y_max, x_max = segment_mask.shape + for y_offset in range(0, y_max, chunk_size): + for x_offset in range(0, x_max, chunk_size): + y1_tile = y_offset + y2_tile = min(y1_tile + chunk_size + patch_shape[0] - patch_stride, y_max) + x1_tile = x_offset + x2_tile = min(x1_tile + chunk_size + patch_shape[1] - patch_stride, x_max) + segment_mask_patch = segment_mask[y1_tile:y2_tile, x1_tile:x2_tile] + ink_mask_patch = ink_mask[y1_tile:y2_tile, x1_tile:x2_tile] + + if np.all(segment_mask_patch == 0): # Skip any fully masked chunk. + continue + elif not np.any( + ink_mask_patch > ink_thresh + ): # There must be at least some ink in the chunk. + continue + + segment_patches = patchify(segment_mask_patch, patch_shape, patch_stride) + ink_patches = patchify(ink_mask_patch, patch_shape, patch_stride) + + xy_dims = (2, 3) + contains_ink = ink_patches > ink_thresh + if all_ink_patches: + contains_ink = contains_ink.all(xy_dims) + else: + contains_ink = contains_ink.any(xy_dims) + + is_valid_patch = contains_ink + if not skip_seg_masked_regions: + no_overlap_with_non_segment_region = (segment_patches != 0).any(xy_dims) + is_valid_patch &= no_overlap_with_non_segment_region + + patch_x, patch_y = is_valid_patch.nonzero() + if len(patch_x) > 0 or len(patch_y) > 0: + valid_positions = [] + for patch_i, patch_j in zip(patch_x, patch_y): + (y1_patch, x1_patch), (y2_patch, x2_patch) = patch_index_to_pixel_position( + patch_i, patch_j, patch_shape, patch_stride + ) + + # Adjust positions for the chunk's offset in the original image. + y1_patch += y_offset + y2_patch += y_offset + x1_patch += x_offset + x2_patch += x_offset + + position = (x1_patch, y1_patch, x2_patch, y2_patch) + valid_positions.append(position) + patch_pos_array_full += valid_positions + + return patch_pos_array_full + + +def get_valid_patch_positions_batched( + ink_mask: np.ndarray, + non_ink_mask: np.ndarray, + segment_mask: np.ndarray, + patch_shape: tuple[int, int], + patch_stride: int, + min_labeled_coverage_frac: float = 0.5, + ink_thresh: float = 0.05, + chunk_size: int = 256, + should_pad: bool = True, + skip_seg_masked_regions: bool = True, +) -> list[tuple[int, int, int, int]]: + if not (0 < min_labeled_coverage_frac <= 1): + raise ValueError( + f"`min_labeled_coverage_frac` must be in the half-open interval (0, 1]. Found min_labeled_coverage_frac={min_labeled_coverage_frac}" + ) + + # Pad to same shape. + if ink_mask.shape != segment_mask.shape: + if should_pad: + ink_mask = pad_to_match(ink_mask, segment_mask) + segment_mask = pad_to_match(segment_mask, ink_mask) + non_ink_mask = pad_to_match(non_ink_mask, ink_mask) + else: + raise ValueError( + f"ink mask and segment mask shapes ({ink_mask.shape}, {segment_mask.shape}) do not match." + f"If this is expected, set `should_pad` to True." + ) + + patch_pos_array_full = [] + total_pixels = patch_shape[0] * patch_shape[1] + y_max, x_max = segment_mask.shape + for y_offset in range(0, y_max, chunk_size): + for x_offset in range(0, x_max, chunk_size): + y1_tile = y_offset + y2_tile = min(y1_tile + chunk_size + patch_shape[0] - patch_stride, y_max) + x1_tile = x_offset + x2_tile = min(x1_tile + chunk_size + patch_shape[1] - patch_stride, x_max) + segment_mask_tile = segment_mask[y1_tile:y2_tile, x1_tile:x2_tile] + ink_mask_tile = ink_mask[y1_tile:y2_tile, x1_tile:x2_tile] + non_ink_tile = non_ink_mask[y1_tile:y2_tile, x1_tile:x2_tile] + + if np.all(segment_mask_tile == 0): + # Skip any fully masked chunk. + continue + elif not np.any(ink_mask_tile > ink_thresh) and not np.any(non_ink_tile): + # There must be at least some ink or non-ink in the chunk. + continue + + segment_patches = patchify(segment_mask_tile, patch_shape, patch_stride) + ink_patches = patchify(ink_mask_tile, patch_shape, patch_stride) + non_ink_patches = patchify(non_ink_tile, patch_shape, patch_stride) + + xy_dims = (2, 3) + ink_patches_binarized = ink_patches > ink_thresh + ink_coverage = np.sum(ink_patches_binarized, axis=xy_dims) + + non_ink_patches_binarized = non_ink_patches > 0 + non_ink_coverage = np.sum(non_ink_patches_binarized > 0, axis=xy_dims) + + labeled_coverage_frac = (ink_coverage + non_ink_coverage) / total_pixels + sufficient_labeled_area = labeled_coverage_frac >= min_labeled_coverage_frac + is_valid_patch = sufficient_labeled_area + + if not skip_seg_masked_regions: + no_overlap_with_non_segment_region = (segment_patches != 0).any(xy_dims) + is_valid_patch &= no_overlap_with_non_segment_region + + if is_valid_patch.any(): + patch_x, patch_y = is_valid_patch.nonzero() + if len(patch_x) > 0 or len(patch_y) > 0: + valid_positions = [] + for patch_i, patch_j in zip(patch_x, patch_y): + (y1_patch, x1_patch), (y2_patch, x2_patch) = patch_index_to_pixel_position( + patch_i, patch_j, patch_shape, patch_stride + ) + + # Adjust positions for the chunk's offset in the original image. + y1_patch += y_offset + y2_patch += y_offset + x1_patch += x_offset + x2_patch += x_offset + + position = (x1_patch, y1_patch, x2_patch, y2_patch) + valid_positions.append(position) + patch_pos_array_full += valid_positions + + return patch_pos_array_full + + +def get_all_patch_positions_non_masked_batched( + segment_mask: np.ndarray, + patch_shape: tuple[int, int], + patch_stride: int, + chunk_size: int = 256, +) -> list[tuple[int, int, int, int]]: + patch_pos_array_full = [] + y_max, x_max = segment_mask.shape + for y_offset in range(0, y_max, chunk_size): + for x_offset in range(0, x_max, chunk_size): + y1_tile = y_offset + y2_tile = min(y1_tile + chunk_size + patch_shape[0] - patch_stride, y_max) + x1_tile = x_offset + x2_tile = min(x1_tile + chunk_size + patch_shape[1] - patch_stride, x_max) + segment_mask_tile = segment_mask[y1_tile:y2_tile, x1_tile:x2_tile] + + if np.all(segment_mask_tile == 0): + # Skip any fully masked chunk. + continue + + segment_patches = patchify(segment_mask_tile, patch_shape, patch_stride) + xy_dims = (2, 3) + no_overlap_with_non_segment_region = (segment_patches != 0).any(xy_dims) + if no_overlap_with_non_segment_region.any(): + patch_x, patch_y = no_overlap_with_non_segment_region.nonzero() + if len(patch_x) > 0 or len(patch_y) > 0: + valid_positions = [] + for patch_i, patch_j in zip(patch_x, patch_y): + (y1_patch, x1_patch), (y2_patch, x2_patch) = patch_index_to_pixel_position( + patch_i, patch_j, patch_shape, patch_stride + ) + + # Adjust positions for the chunk's offset in the original image. + y1_patch += y_offset + y2_patch += y_offset + x1_patch += x_offset + x2_patch += x_offset + + position = (x1_patch, y1_patch, x2_patch, y2_patch) + valid_positions.append(position) + patch_pos_array_full += valid_positions + + return patch_pos_array_full diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/probabilistic_patch_sampling.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/probabilistic_patch_sampling.py new file mode 100644 index 0000000..4529a48 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/probabilistic_patch_sampling.py @@ -0,0 +1,112 @@ +import numpy as np +from skimage.util import img_as_float32 + + +def get_probabilistic_patch_samples( + ink_mask_channel_0: np.ndarray, + ink_mask_channel_2, + non_ink_mask: np.ndarray, + size: int, + stride: int, + p0: float = 0.3, + p2: float = 0.6, + p_non_ink: float = 0.1, + ignore_idx: int = -100, +) -> list[tuple[int, int, int, int]]: + ink_mask_channel_0 = ink_mask_channel_0.copy() + ink_mask_channel_2 = ink_mask_channel_2.copy() + ink_mask_channel_2 = ink_mask_channel_2.copy() + ink_mask_channel_0 = np.where(ink_mask_channel_0 == ignore_idx, 0, ink_mask_channel_0) + ink_mask_channel_2 = np.where(ink_mask_channel_2 == ignore_idx, 0, ink_mask_channel_2) + prob_map = create_2d_probability_map( + ink_mask_channel_0, ink_mask_channel_2, non_ink_mask, p0=p0, p2=p2, p_non_ink=p_non_ink + ) + ink_mask_full = ink_mask_channel_0.astype(bool) | ink_mask_channel_2.astype(bool) + height, width = prob_map.shape[0], prob_map.shape[1] + n_samples = get_num_patches(size, stride, height, width, ink_mask_full, non_ink_mask) + sample_yxs = sample_from_2d_probability_array(prob_map, num_samples=n_samples) + patch_positions = centroids_to_xyxys(sample_yxs, size=size) + + # Filter patch positions exceeding image dimensions. + x2s = patch_positions[:, 2] + y2s = patch_positions[:, 3] + in_bounds_mask = (x2s < width) & (y2s < height) + patch_positions = patch_positions[in_bounds_mask] + + return patch_positions.tolist() + + +def create_2d_probability_map( + ink_mask_channel_0: np.ndarray, + ink_mask_channel_2, + non_ink_mask: np.ndarray, + p0: float = 0.3, + p2: float = 0.6, + p_non_ink: float = 0.1, +): + prob_map = np.zeros(ink_mask_channel_0.shape, dtype=np.float32) + prob_map[ink_mask_channel_0.astype(bool)] = p0 + prob_map[ink_mask_channel_2.astype(bool)] = p2 + prob_map[non_ink_mask.astype(bool)] = p_non_ink + prob_map = img_as_float32(prob_map) + return prob_map + + +def sample_from_2d_probability_array( + prob_2d: np.ndarray, num_samples: int = 10 +) -> list[tuple[int, int]]: + """ + Samples a specified number of coordinates from a 2D array of probabilities. + + Parameters: + prob_2d (np.array): A 2D numpy array of probabilities. + num_samples (int): The number of samples to draw. + + Returns: + list of tuples: A list of sampled 2D coordinates. + """ + # Normalize the probabilities + prob_flat = prob_2d.flatten() + prob_flat /= prob_flat.sum() + + # Sample from the flattened array + sample_indices = np.random.choice(len(prob_flat), size=num_samples, p=prob_flat) + + # Map the sampled indices back to 2D coordinates + yxs = [np.unravel_index(index, prob_2d.shape) for index in sample_indices] + + return yxs + + +def get_num_windows(patch_size: tuple[int, int], stride: int, height: int, width: int) -> int: + """Compute the number of patches for a rectangle with the given window size and stride.""" + ny = ((height - patch_size[1]) // stride) + 1 + nx = ((width - patch_size[0]) // stride) + 1 + return nx * ny + + +def get_num_patches( + size: int, stride: int, height: int, width: int, ink_mask: np.ndarray, non_ink_mask: np.ndarray +) -> int: + n_windows = get_num_windows((size, size), stride, height, width) + + # Adjust number of windows based on ink area ratio. + ink_or_non_ink_mask = ink_mask.astype(bool) | non_ink_mask.astype(bool) + area_segment = height * width + area_masks = np.sum(ink_or_non_ink_mask) + area_ratio = area_masks / area_segment + n_samples = int(area_ratio * n_windows) + return n_samples + + +def centroids_to_xyxys(centroids: np.ndarray, size: int) -> np.ndarray: + centroids = np.asarray(centroids) + + patch_positions = np.empty((centroids.shape[0], 4), dtype=np.int32) + half_size = size // 2 + patch_positions[:, 0] = centroids[:, 1] - half_size + patch_positions[:, 1] = centroids[:, 0] - half_size + patch_positions[:, 2] = centroids[:, 1] + half_size + patch_positions[:, 3] = centroids[:, 0] + half_size + + return patch_positions diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_fragment_combined_data_module.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_fragment_combined_data_module.py new file mode 100644 index 0000000..d876a85 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_fragment_combined_data_module.py @@ -0,0 +1,313 @@ +import logging +from pathlib import Path + +import albumentations as A +from albumentations.pytorch import ToTensorV2 +from pytorch_lightning import LightningDataModule +from pytorch_lightning.utilities import CombinedLoader +from pytorch_lightning.utilities.types import TRAIN_DATALOADERS +from torch.utils.data import DataLoader +from tqdm import tqdm + +from vesuvius_challenge_rnd import FRAGMENT_DATA_DIR, SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data import Fragment, Scroll, ScrollSegment +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import FragmentPreprocessorBase +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_fragment_data_module import ( + AbstractFragmentValPatchDataset, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.patch_dataset import ( + PatchDataset, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.scroll_patch_dataset import ( + ScrollPatchDataset, +) + +ScrollSegmentsDataType = str | list[tuple[str, str]] + + +class CombinedDataModule(LightningDataModule, AbstractFragmentValPatchDataset): + def __init__( + self, + train_fragment_ind: list[int], + val_fragment_ind: list[int], + train_scroll_segments: ScrollSegmentsDataType, + val_scroll_segments: ScrollSegmentsDataType, + fragment_data_dir: Path = FRAGMENT_DATA_DIR, + scroll_data_dir: Path = SCROLL_DATA_DIR, + fragment_batch_size: int = 2, + scroll_batch_size: int = 2, + z_min_fragment: int = 27, + z_max_fragment: int = 37, + z_min_scroll: int = 27, + z_max_scroll: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + downsampling: int | None = None, + num_workers: int | None = 0, + slice_dropout_p: float = 0, + non_destructive: bool = True, + non_rigid: bool = False, + fragment_preprocessor: FragmentPreprocessorBase | None = None, + ): + super().__init__() + # Validate z_min and z_max. + fragment_z_extent = z_max_fragment - z_min_fragment + scroll_z_extent = z_max_scroll - z_min_scroll + if fragment_z_extent != scroll_z_extent: + raise ValueError( + f"Fragment and scroll z-extents must be equal. Found fragment extent of {fragment_z_extent}" + f" and scroll extent of {scroll_z_extent}." + ) + + self.fragment_data_dir = fragment_data_dir + self.scroll_data_dir = scroll_data_dir + self.batch_size_scroll = fragment_batch_size + self.batch_size_fragment = scroll_batch_size + self.train_fragment_ind = train_fragment_ind + self.val_fragment_ind = val_fragment_ind + self.train_scroll_segments = train_scroll_segments + self.val_scroll_segments = val_scroll_segments + self.z_min_fragment = z_min_fragment + self.z_max_fragment = z_max_fragment + self.z_min_scroll = z_min_scroll + self.z_max_scroll = z_max_scroll + self.patch_surface_shape = patch_surface_shape + self.patch_stride = patch_stride + self.downsampling = 1 if downsampling is None else downsampling + self.num_workers = num_workers + self.slice_dropout_p = slice_dropout_p + self.non_destructive = non_destructive + self.non_rigid = non_rigid + self.fragment_preprocessor = fragment_preprocessor + self.processed_fragment_data_dir = ( + self.fragment_data_dir + if self.fragment_preprocessor is None + else self.fragment_preprocessor.preprocessing_dir / FRAGMENT_DATA_DIR.name + ) + + # Create the scroll segments. + self.segments_train = self._filter_segments( + self._instantiate_segments(self.train_scroll_segments) + ) + self.segments_val = self._filter_segments( + self._instantiate_segments(self.val_scroll_segments) + ) + + def _instantiate_segments( + self, scroll_segment_data: ScrollSegmentsDataType + ) -> list[ScrollSegment]: + if isinstance(scroll_segment_data, list): + segments = [] + for scroll_id, segment_name in scroll_segment_data: + segment = ScrollSegment(scroll_id, segment_name, scroll_dir=self.scroll_data_dir) + segments.append(segment) + elif scroll_segment_data in ("1", "2"): + scroll = Scroll(scroll_id=scroll_segment_data, scroll_dir=self.scroll_data_dir) + segments = scroll.segments + elif scroll_segment_data.lower() == "all": + scrolls = [Scroll(str(i + 1)) for i in range(2)] + segments = scrolls[0].segments + scrolls[1].segments + else: + raise ValueError( + "Scroll segments must be '1', '2', 'all', or a list of tuples of scroll IDs and segment names (e.g., " + "[(1, 20230828154913), (1, 20230819210052)])." + ) + + return segments + + def _filter_segments(self, segments: list[ScrollSegment]): + filtered_segments = [] + for segment in segments: + if ( + segment.surface_shape[0] >= self.patch_surface_shape[0] + and segment.surface_shape[1] >= self.patch_surface_shape[1] + ): + filtered_segments.append(segment) + else: + logging.warning( + f"Skipping scroll {segment.scroll_id} segment {segment.segment_name} with surface shape {segment.surface_shape} because " + f"it's smaller than the patch surface shape: {self.patch_surface_shape}." + ) + return filtered_segments + + def prepare_data(self) -> None: + super().prepare_data() + if self.fragment_preprocessor is not None: + all_fragment_ind = self.train_fragment_ind + self.val_fragment_ind + fragments = [ + Fragment(fid, fragment_dir=self.fragment_data_dir) for fid in all_fragment_ind + ] + for fragment in tqdm(fragments, desc="Preprocessing fragments..."): + self.fragment_preprocessor(fragment) + + def setup(self, stage: str) -> None: + if stage == "fit" or stage is None: + self.data_fragment_train = PatchDataset( + self.processed_fragment_data_dir, + self.train_fragment_ind, + transform=self.train_transform_fragment(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min_fragment, + z_max=self.z_max_fragment, + ) + self.data_fragment_val = PatchDataset( + self.processed_fragment_data_dir, + self.val_fragment_ind, + transform=self.validation_transform_fragment(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min_fragment, + z_max=self.z_max_fragment, + ) + self.data_scroll_train = ScrollPatchDataset( + self.segments_train, + transform=self.train_transform_scroll(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min_scroll, + z_max=self.z_max_scroll, + ) + self.data_scroll_val = ScrollPatchDataset( + self.segments_val, + transform=self.validation_transform_scroll(), + patch_surface_shape=self.patch_surface_shape, + patch_stride=self.patch_stride, + z_min=self.z_min_scroll, + z_max=self.z_max_scroll, + ) + + def train_dataloader(self) -> TRAIN_DATALOADERS: + source_data_loader = DataLoader( + self.data_fragment_train, + batch_size=self.batch_size_scroll, + shuffle=True, + pin_memory=True, + drop_last=True, + ) + target_data_loader = DataLoader( + self.data_scroll_train, + batch_size=self.batch_size_fragment, + shuffle=True, + pin_memory=True, + drop_last=True, + ) + iterables = { + "source": source_data_loader, + "target": target_data_loader, + } + return CombinedLoader(iterables, mode="min_size") + + def val_dataloader(self) -> list[DataLoader]: + source_data_loader = DataLoader( + self.data_fragment_val, + batch_size=self.batch_size_scroll, + shuffle=False, + pin_memory=True, + drop_last=False, + ) + target_data_loader = DataLoader( + self.data_scroll_val, + batch_size=self.batch_size_fragment, + shuffle=False, + pin_memory=True, + drop_last=False, + ) + return [source_data_loader, target_data_loader] + + def train_transform_fragment(self) -> A.Compose: + """Define the transformations for the training stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + + if self.downsampling != 1: + transforms += [A.Resize(self.patch_height, self.patch_width, always_apply=True)] + + if self.non_destructive: + non_destructive_transformations = [ # Dihedral group D4 + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.5), + A.RandomRotate90(p=0.5), # Randomly rotates by 0, 90, 180, 270 degrees + A.Transpose(p=0.5), # Switch X and Y axis. + ] + transforms += non_destructive_transformations + + if self.non_rigid: + non_rigid_transformations = A.OneOf( + [ + A.GridDistortion(p=0.5), + ], + p=0.5, + ) + transforms += non_rigid_transformations + + transforms += [A.ChannelDropout(channel_drop_range=(1, 2), p=self.slice_dropout_p)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + + return A.Compose(transforms) + + def validation_transform_fragment(self) -> A.Compose: + """Define the transformations for the validation stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + if self.downsampling != 1: + transforms += [A.Resize(self.patch_height, self.patch_width, always_apply=True)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + return A.Compose(transforms) + + def train_transform_scroll(self) -> A.Compose: + transforms = [] + + if self.downsampling != 1: + transforms += [A.Resize(self.patch_height, self.patch_width, always_apply=True)] + + if self.non_destructive: + non_destructive_transformations = [ # Dihedral group D4 + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.5), + A.RandomRotate90(p=0.5), # Randomly rotates by 0, 90, 180, 270 degrees + A.Transpose(p=0.5), # Switch X and Y axis. + ] + transforms += non_destructive_transformations + + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + + return A.Compose(transforms) + + def validation_transform_scroll(self) -> A.Compose: + """Define the transformations for the validation stage. + + Returns: + albumentations.Compose: A composed transformation object. + """ + transforms = [] + if self.downsampling != 1: + transforms += [A.Resize(self.patch_height, self.patch_width, always_apply=True)] + transforms += [A.Normalize(mean=[0], std=[1]), ToTensorV2(transpose_mask=True)] + return A.Compose(transforms) + + @property + def patch_height(self) -> int: + """The (possibly downsampled) patch height.""" + return self.patch_surface_shape[0] // self.downsampling + + @property + def patch_width(self) -> int: + """The (possibly downsampled) patch width.""" + return self.patch_surface_shape[1] // self.downsampling + + @property + def patch_depth(self) -> int: + """The patch depth.""" + return self.z_max_fragment - self.z_min_fragment + + @property + def val_fragment_dataset(self) -> PatchDataset: + """The fragment validation dataset.""" + return self.data_fragment_val diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module.py new file mode 100644 index 0000000..61b4e22 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module.py @@ -0,0 +1,1215 @@ +import logging +import os +from functools import partial +from pathlib import Path +from tempfile import mkdtemp +from warnings import warn + +import albumentations as A +import cv2 +import numpy as np +from albumentations.pytorch import ToTensorV2 +from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS +from skimage.util import img_as_ubyte +from torch.utils.data import ConcatDataset, DataLoader +from tqdm import tqdm +from tqdm.contrib import tzip + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data import Scroll, ScrollSegment +from vesuvius_challenge_rnd.data.constants import ( + KNOWN_Z_ORIENTATION_SEGMENT_IDS, + Z_REVERSED_SEGMENT_IDS, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.block_dataset import ( + BlockZConstantDataset, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.ink_label_correction import ( + apply_model_based_label_correction, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.memmap import ( + MEMMAP_DIR, + delete_memmap, + load_segment_as_memmap, + save_segment_as_memmap, + segment_to_memmap_path, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_memmap_dataset import ( + PatchMemMapDataset, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_sampling import ( + get_ink_patch_positions_batched, + get_valid_patch_positions_batched, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.probabilistic_patch_sampling import ( + get_probabilistic_patch_samples, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.ink_pred_utils import ( + read_ink_preds, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.label_utils import ( + create_non_ink_mask, + read_ink_mask, + read_papy_non_ink_labels, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.zarr import ( + ZARR_ARRAY_DIR, + load_segment_as_zarr_array, + save_segment_as_zarr_array, + segment_to_zarr_path, +) + +ScrollSegmentsDataType = str | list[tuple[str, str]] + + +class ScrollPatchDataModule(BasePatchDataModule): + def __init__( + self, + train_segment_ids: ScrollSegmentsDataType, + val_segment_id: str | tuple[str, str], + ink_label_dir: Path, + data_dir: Path = SCROLL_DATA_DIR, + z_min: int = 15, + z_max: int = 47, + size: int = 64, + tile_size: int = 256, + min_labeled_coverage_frac: float = 1, + patch_stride_train: int = 8, + patch_stride_val: int = 32, + downsampling: int | None = None, + batch_size: int = 256, + num_workers: int = 0, + blur_ink_labels: bool = False, + ink_labels_blur_kernel_size: int = 17, + ink_dilation_kernel_size: int = 256, + min_ink_component_size: int = 1000, + label_downscale: int = 1, + ink_erosion: int = 0, + ignore_idx: int = -100, + clip_min: int = 0, + clip_max: int = 255, + patch_train_stride_strict: int = 8, + patch_val_stride_strict: int = 8, + strict_sampling_only_ink_train: bool = True, + strict_sampling_only_ink_val: bool = True, + min_crop_num_offset: int = 8, + chunks_load: tuple[int, int, int] | int | bool = True, + use_zarr: bool = False, + zarr_dir: Path = ZARR_ARRAY_DIR, + x_chunk_save_size: int = 512, + y_chunk_save_size: int = 512, + z_chunk_save_size: int = 32, + skip_save_zarr_if_exists: bool = True, + zarr_load_in_memory: bool = True, + model_prediction_dir: Path | None = None, + model_based_ink_correction_thresh_train: float = 0.1, + model_based_ink_correction_thresh_val: float = 0.1, + model_based_non_ink_correction_thresh_train: float = 0.3, + model_based_non_ink_correction_thresh_val: float = 0.3, + clean_up_ink_labels_train: bool = False, + clean_up_ink_labels_val: bool = False, + clean_up_non_ink_labels_train: bool = False, + clean_up_non_ink_labels_val: bool = False, + p_0_ink: float = 0.3, + p_2_ink: float = 0.6, + p_non_ink: float = 0.1, + automatic_non_ink_labels: bool = False, + cache_memmaps: bool = False, + memmap_dir: Path = MEMMAP_DIR, + ): + if ink_labels_blur_kernel_size % 2 != 1: + raise ValueError("`ink_labels_blur_kernel_size` must be odd") + super().__init__( + data_dir=data_dir, + z_min=z_min, + z_max=z_max, + patch_surface_shape=(size, size), + patch_stride=patch_stride_train, + downsampling=downsampling, + batch_size=batch_size, + num_workers=num_workers, + ) + if tile_size < size: + raise ValueError(f"Tile size ({tile_size}) cannot be less than patch size ({size}).") + + if min_crop_num_offset > self.z_extent: + raise ValueError( + f"min_crop_num_offset ({min_crop_num_offset}) cannot be less than z-extent ({self.z_extent})." + ) + + if cache_memmaps and use_zarr: + raise ValueError( + f"zarr and memmap cache are mutually exclusive. Use neither or only one." + ) + + self.size = size + self.tile_size = tile_size + self.min_labeled_coverage_frac = min_labeled_coverage_frac + self.ink_dilation_kernel_size = ink_dilation_kernel_size + self.patch_stride_train = patch_stride_train + self.patch_stride_val = patch_stride_val + self.clip_min = clip_min + self.clip_max = clip_max + + self.ink_label_dir = ink_label_dir + self.model_prediction_dir = model_prediction_dir + self.model_based_ink_correction_thresh_train = model_based_ink_correction_thresh_train + self.model_based_ink_correction_thresh_val = model_based_ink_correction_thresh_val + self.model_based_non_ink_correction_thresh_train = ( + model_based_non_ink_correction_thresh_train + ) + self.model_based_non_ink_correction_thresh_val = model_based_non_ink_correction_thresh_val + self.clean_up_ink_labels_train = clean_up_ink_labels_train + self.clean_up_ink_labels_val = clean_up_ink_labels_val + self.clean_up_non_ink_labels_train = clean_up_non_ink_labels_train + self.clean_up_non_ink_labels_val = clean_up_non_ink_labels_val + + self.p_0_ink = p_0_ink + self.p_2_ink = p_2_ink + self.p_non_ink = p_non_ink + + self.blur_ink_labels = blur_ink_labels + self.ink_label_blur_kernel_size = ink_labels_blur_kernel_size + self.min_ink_component_size = min_ink_component_size + self.ink_erosion = ink_erosion + self.ignore_idx = ignore_idx + self.min_crop_num_offset = min_crop_num_offset + self.patch_train_stride_strict = patch_train_stride_strict + self.patch_val_stride_strict = patch_val_stride_strict + self.strict_sampling_only_ink_train = strict_sampling_only_ink_train + self.strict_sampling_only_ink_val = strict_sampling_only_ink_val + + # Set zarr parameters. + self.use_zarr = use_zarr + self.zarr_dir = zarr_dir + self.chunks_load = chunks_load + self.x_chunk_save_size = x_chunk_save_size + self.y_chunk_save_size = y_chunk_save_size + self.z_chunk_save_size = z_chunk_save_size + self.skip_save_zarr_if_exists = skip_save_zarr_if_exists + self.zarr_load_in_memory = zarr_load_in_memory + + # Memmaps parameters. + self.cache_memmaps = cache_memmaps + self.memmap_dir = memmap_dir + + # Create the scroll segments. + self.segments_train = self._filter_segments(self._instantiate_segments(train_segment_ids)) + self.segment_val = self._filter_segments(self._instantiate_segments([val_segment_id]))[0] + + all_segment_names = [s.segment_name_orig for s in self.segments_train] + [ + self.segment_val.segment_name_orig + ] + + _check_ink_labels(ink_label_dir, all_segment_names) + + if model_prediction_dir is not None: + _check_ink_pred_dir(model_prediction_dir, all_segment_names) + + self.pred_shape = self.segment_val.surface_shape + self.label_downscale = label_downscale + + # Check papyrus non-ink labels. + self.automatic_non_ink_labels = automatic_non_ink_labels + + if not self.automatic_non_ink_labels: + _check_papyrus_non_ink_labels(ink_label_dir, all_segment_names) + + def _instantiate_segments( + self, scroll_segment_data: ScrollSegmentsDataType + ) -> list[ScrollSegment]: + if isinstance(scroll_segment_data, list): + segments = [] + for scroll_id, segment_name in scroll_segment_data: + segment = ScrollSegment(scroll_id, segment_name, scroll_dir=self.data_dir) + segments.append(segment) + elif scroll_segment_data in ("1", "2"): + scroll = Scroll(scroll_id=scroll_segment_data, scroll_dir=self.data_dir) + segments = scroll.segments + elif scroll_segment_data.lower() == "all": + scrolls = [Scroll(str(i + 1)) for i in range(2)] + segments = scrolls[0].segments + scrolls[1].segments + else: + raise ValueError( + "Scroll segments must be '1', '2', 'all', or a list of tuples of scroll IDs and segment names (e.g., " + f"[(1, '20230828154913'), (1, '20230819210052')]). Found value {scroll_segment_data}." + ) + + return segments + + @property + def train_segment_ids(self) -> list[str]: + return [seg.segment_name for seg in self.segments_train] + + @property + def val_segment_id(self) -> str: + return self.segment_val.segment_name + + def _filter_segments(self, segments: list[ScrollSegment]): + filtered_segments = [] + for segment in segments: + if ( + segment.surface_shape[0] >= self.patch_surface_shape[0] + and segment.surface_shape[1] >= self.patch_surface_shape[1] + ): + filtered_segments.append(segment) + else: + logging.warning( + f"Skipping scroll {segment.scroll_id} segment {segment.segment_name} with surface shape {segment.surface_shape} because " + f"it's smaller than the patch surface shape: {self.patch_surface_shape}." + ) + return filtered_segments + + @property + def z_extent(self) -> int: + return self.z_max - self.z_min + + def prepare_data(self) -> None: + """Download data to disk. + + This method checks if the data directory is empty and raises an exception + if the data is not found. + + Raises: + ValueError: If the data directory is empty. + """ + super().prepare_data() + # If using zarr, possibly create the zarr arrays. + if self.use_zarr: + for segment in tqdm(self.segments_train + [self.segment_val], desc="Creating zarrs..."): + zarr_path = segment_to_zarr_path(segment, zarr_dir=self.zarr_dir) + if zarr_path.exists() and self.skip_save_zarr_if_exists: + logging.info(f"Found existing zarr for segment {segment.segment_name}.") + else: + logging.info(f"Creating new zarr for segment {segment.segment_name}.") + save_segment_as_zarr_array( + segment, + output_zarr_path=zarr_path, + z_chunk_size=self.z_chunk_save_size, + y_chunk_size=self.y_chunk_save_size, + x_chunk_size=self.x_chunk_save_size, + load_in_memory=self.zarr_load_in_memory, + ) + elif self.cache_memmaps: + self.memmap_dir.mkdir(exist_ok=True, parents=True) + for segment in tqdm( + self.segments_train + [self.segment_val], desc="Creating memmaps..." + ): + memmap_path = segment_to_memmap_path(segment, memmap_dir=self.memmap_dir) + if memmap_path.exists() and self.skip_save_zarr_if_exists: + logging.info(f"Found existing memmap for segment {segment.segment_name}.") + else: + logging.info(f"Creating new memmap for segment {segment.segment_name}.") + save_segment_as_memmap( + segment, + memmap_path, + load_in_memory=self.zarr_load_in_memory, + ) + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "fit"). + """ + if stage == "fit" or stage is None: + ( + img_stack_refs_train, + padding_list_train, + z_reversals_train, + segment_masks_train, + ink_masks_train, + ink_preds, + non_ink_masks_train, + ) = generate_dataset_inputs( + self.segments_train, + self.z_min, + self.z_max, + self.tile_size, + self.ink_label_dir, + should_blur_ink_labels=self.blur_ink_labels, + ink_labels_blur_kernel_size=self.ink_label_blur_kernel_size, + min_ink_component_size=self.min_ink_component_size, + ink_erosion=self.ink_erosion, + ignore_idx=self.ignore_idx, + chunks_load=self.chunks_load, + use_zarr=self.use_zarr, + zarr_dir=self.zarr_dir, + cache_memmaps=self.cache_memmaps, + memmap_dir=self.memmap_dir, + ink_preds_dir=self.model_prediction_dir, + automatic_non_ink=self.automatic_non_ink_labels, + clip_min=self.clip_min, + clip_max=self.clip_max, + ) + + train_patch_positions, non_ink_ignored_patches_train = create_train_patch_positions( + segment_masks_train, + ink_masks_train, + self.ink_dilation_kernel_size, + self.size, + self.patch_stride_train, + self.patch_train_stride_strict, + strict_sampling_only_ink=self.strict_sampling_only_ink_train, + tile_size=self.tile_size, + min_labeled_coverage_frac=self.min_labeled_coverage_frac, + ink_preds=ink_preds, + model_based_ink_correction_thresh=self.model_based_ink_correction_thresh_train, + model_based_non_ink_correction_thresh=self.model_based_non_ink_correction_thresh_train, + clean_up_ink_labels=self.clean_up_ink_labels_train, + clean_up_non_ink_labels=self.clean_up_non_ink_labels_train, + p_0_ink=self.p_0_ink, + p_2_ink=self.p_2_ink, + p_non_ink=self.p_non_ink, + ignore_idx=self.ignore_idx, + non_ink_masks=non_ink_masks_train, + ) + + ( + img_stack_refs_val, + padding_list_val, + z_reversals_val, + segment_masks_val, + ink_masks_val, + ink_preds, + non_ink_masks_val, + ) = generate_dataset_inputs( + [self.segment_val], + self.z_min, + self.z_max, + self.tile_size, + self.ink_label_dir, + min_ink_component_size=self.min_ink_component_size, + ink_erosion=self.ink_erosion, + ignore_idx=self.ignore_idx, + chunks_load=self.chunks_load, + use_zarr=self.use_zarr, + zarr_dir=self.zarr_dir, + cache_memmaps=self.cache_memmaps, + memmap_dir=self.memmap_dir, + ink_preds_dir=self.model_prediction_dir, + automatic_non_ink=self.automatic_non_ink_labels, + clip_min=self.clip_min, + clip_max=self.clip_max, + ) + + val_patch_positions, non_ink_ignored_patches_val = create_val_patch_positions( + segment_masks_val, + ink_masks_val, + self.ink_dilation_kernel_size, + self.size, + self.patch_stride_val, + self.patch_val_stride_strict, + strict_sampling_only_ink=self.strict_sampling_only_ink_val, + tile_size=self.tile_size, + min_labeled_coverage_frac=self.min_labeled_coverage_frac, + ink_preds=ink_preds, + model_based_ink_correction_thresh=self.model_based_ink_correction_thresh_val, + model_based_non_ink_correction_thresh=self.model_based_non_ink_correction_thresh_val, + clean_up_ink_labels=self.clean_up_ink_labels_val, + clean_up_non_ink_labels=self.clean_up_non_ink_labels_val, + p_0_ink=self.p_0_ink, + p_2_ink=self.p_2_ink, + p_non_ink=self.p_non_ink, + ignore_idx=self.ignore_idx, + non_ink_masks=non_ink_masks_val, + ) + + ink_masks_train = max_proj_multi_channel_ink_masks(ink_masks_train) + ink_masks_val = max_proj_multi_channel_ink_masks(ink_masks_val) + + self.data_train = create_dataset( + img_stack_refs_train, + train_patch_positions, + ink_masks_train, + z_reversals_train, + padding_list_train, + self.size, + self.z_min, + self.z_extent, + augment=True, + transform=self.train_transform(), + label_downscale=self.label_downscale, + non_ink_ignored_patches=non_ink_ignored_patches_train, + ignore_idx=self.ignore_idx, + clip_min=self.clip_min, + clip_max=self.clip_max, + min_crop_num_offset=self.min_crop_num_offset, + use_zarr_or_mmap_cache=self.use_zarr or self.cache_memmaps, + ) + + self.data_val = create_dataset( + img_stack_refs_val, + val_patch_positions, + ink_masks_val, + z_reversals_val, + padding_list_val, + self.size, + self.z_min, + self.z_extent, + augment=False, + transform=self.val_transform(), + label_downscale=self.label_downscale, + non_ink_ignored_patches=non_ink_ignored_patches_val, + ignore_idx=self.ignore_idx, + clip_min=self.clip_min, + clip_max=self.clip_max, + min_crop_num_offset=self.min_crop_num_offset, + use_zarr_or_mmap_cache=self.use_zarr or self.cache_memmaps, + ) + + def train_transform(self) -> A.Compose: + return A.Compose( + [ + A.Resize(self.size, self.size), + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.5), + A.RandomBrightnessContrast(p=0.75), + A.ShiftScaleRotate(rotate_limit=360, shift_limit=0.15, scale_limit=0.15, p=0.75), + A.OneOf( + [ + A.GaussNoise(var_limit=(10, 50)), + A.GaussianBlur(), + A.MotionBlur(), + ], + p=0.4, + ), + A.CoarseDropout( + max_holes=2, + max_width=int(self.size * 0.2), + max_height=int(self.size * 0.2), + mask_fill_value=0, + p=0.5, + ), + A.Normalize(mean=[0] * self.z_extent, std=[1] * self.z_extent), + ToTensorV2(transpose_mask=True), + ] + ) + + def val_transform(self) -> A.Compose: + return A.Compose( + [ + A.Resize(self.size, self.size), + A.Normalize(mean=[0] * self.z_extent, std=[1] * self.z_extent), + ToTensorV2(transpose_mask=True), + ] + ) + + def train_dataloader(self) -> TRAIN_DATALOADERS: + return DataLoader( + self.data_train, + batch_size=self.batch_size, + shuffle=False, + num_workers=self.num_workers, + pin_memory=True, + drop_last=True, + ) + + def val_dataloader(self) -> EVAL_DATALOADERS: + return DataLoader( + self.data_val, + batch_size=self.batch_size, + shuffle=False, + num_workers=self.num_workers, + pin_memory=True, + drop_last=False, + ) + + +def max_proj_multi_channel_ink_masks(ink_masks: list[np.ndarray]) -> list[np.ndarray]: + ink_masks_new = [] + for ink_mask in ink_masks: + if ink_mask.ndim == 3: + ink_masks_new.append(np.amax(ink_mask, axis=0)) + else: + ink_masks_new.append(ink_mask.squeeze()) + return ink_masks_new + + +def create_dataset( + img_stack_refs: list[np.memmap], + patch_position_list: list, + ink_masks: list[np.ndarray], + z_reversals: list[bool], + padding_list: list[tuple[int, int]], + size: int, + z_start: int, + z_extent: int, + augment: bool = False, + transform=None, + label_downscale: int = 4, + non_ink_ignored_patches: np.ndarray | None = None, + ignore_idx: int = -100, + clip_min: int = 0, + clip_max: int = 200, + min_crop_num_offset: int = 8, + use_zarr_or_mmap_cache: bool = True, +) -> ConcatDataset: + datasets = [] + for ( + img_stack_ref, + patch_positions, + ink_mask, + non_ink_ignored_patches_seg, + z_reverse, + padding_yx, + ) in zip( + img_stack_refs, + patch_position_list, + ink_masks, + non_ink_ignored_patches, + z_reversals, + padding_list, + ): + if use_zarr_or_mmap_cache: + dataset = BlockZConstantDataset( + img_stack_ref, + np.array(patch_positions), + size, + z_start=z_start, + z_extent=z_extent, + labels=ink_mask, + augment=augment, + transform=transform, + label_downscale=label_downscale, + non_ink_ignored_patches=non_ink_ignored_patches_seg, + ignore_idx=ignore_idx, + min_crop_num_offset=min_crop_num_offset, + z_reverse=z_reverse, + padding_yx=padding_yx, + clip_min=clip_min, + clip_max=clip_max, + ) + else: + dataset = PatchMemMapDataset( + img_stack_ref, + np.array(patch_positions), + size, + z_extent=z_extent, + labels=ink_mask, + augment=augment, + transform=transform, + label_downscale=label_downscale, + non_ink_ignored_patches=non_ink_ignored_patches_seg, + ignore_idx=ignore_idx, + min_crop_num_offset=min_crop_num_offset, + z_reverse=z_reverse, + padding_yx=padding_yx, + clip_min=clip_min, + clip_max=clip_max, + ) + datasets.append(dataset) + return ConcatDataset(datasets) + + +def generate_dataset_inputs( + segments: list[ScrollSegment], + z_start: int, + z_end: int, + tile_size: int, + ink_label_dir: Path, + should_blur_ink_labels: bool = False, + ink_labels_blur_kernel_size: int = 17, + min_ink_component_size: int = 1000, + ink_erosion: int = 0, + ignore_idx: int = -100, + chunks_load: tuple[int, int, int] | int | bool = True, + use_zarr: bool = True, + zarr_dir: Path = ZARR_ARRAY_DIR, + cache_memmaps: bool = False, + memmap_dir: Path = MEMMAP_DIR, + ink_preds_dir: Path | None = None, + automatic_non_ink: bool = True, + clip_min: int = 0, + clip_max: int = 200, +) -> tuple[ + list[np.memmap], + list[tuple[int, int]], + list[bool], + list[np.ndarray], + list[np.ndarray], + list[np.ndarray] | None, + list[np.ndarray | None], +]: + img_stack_refs = [] + segment_masks = [] + ink_masks = [] + non_ink_masks = [] + if ink_preds_dir is not None: + ink_preds = [] + else: + ink_preds = None + + pbar = tqdm(segments) + padding_list = [] + z_reversals = [] + for segment in pbar: + segment_name = ( + segment.segment_name if not segment.is_superseded else segment.segment_name_orig + ) + pbar.set_description(f"Generating dataset inputs for {segment_name}...") + pad_y, pad_x = compute_xy_padding(img_shape=segment.surface_shape, tile_size=tile_size) + + z_reverse = infer_z_reversal(segment_name) + + padding_list.append((pad_y, pad_x)) + z_reversals.append(z_reverse) + + if use_zarr: + zarr_path = segment_to_zarr_path(segment, zarr_dir=zarr_dir) + img_stack_ref = load_segment_as_zarr_array(zarr_path, chunks=chunks_load) + elif cache_memmaps: + mmap_path = segment_to_memmap_path(segment, memmap_dir=memmap_dir) + img_stack_ref = load_segment_as_memmap(mmap_path) + else: + logging.debug(f"Creating image stack memmap...") + img_stack_ref = create_img_stack_memmap( + segment, + z_min=z_start, + z_max=z_end, + pad_y=pad_y, + pad_x=pad_x, + z_reverse=z_reverse, + clip_min=clip_min, + clip_max=clip_max, + ) + + logging.debug(f"Loading segment mask...") + segment_mask = load_segment_mask(segment, pad_y=pad_y, pad_x=pad_x) + + logging.debug(f"Loading ink mask...") + ink_mask = load_ink_mask( + ink_label_dir, + segment_name, + segment_mask.shape, + should_blur_ink_labels=should_blur_ink_labels, + ink_labels_blur_kernel_size=ink_labels_blur_kernel_size, + min_ink_component_size=min_ink_component_size, + ink_erosion=ink_erosion, + ignore_idx=ignore_idx, + ) + + # Read ink predictions. + if ink_preds is not None: + ink_pred = read_ink_preds( + segment_name, ink_preds_dir, expected_shape=segment_mask.shape + ) + ink_preds.append(ink_pred) + if ink_pred.shape != ink_mask.shape[1:]: + raise ValueError( + f"Ink predictions must have the same shape as the ink mask. Segment {segment_name} ink " + f"preds has shape {ink_pred.shape} and ink labels shape {ink_mask.shape}." + ) + + # Read papyrus non-ink labels. + non_ink_mask = load_papyrus_non_ink_labels( + ink_label_dir, segment_name, automatic_non_ink, segment_mask.shape, ink_mask.shape + ) + + img_stack_refs.append(img_stack_ref) + segment_masks.append(segment_mask) + ink_masks.append(ink_mask) + non_ink_masks.append(non_ink_mask) + + return ( + img_stack_refs, + padding_list, + z_reversals, + segment_masks, + ink_masks, + ink_preds, + non_ink_masks, + ) + + +def load_ink_mask( + ink_label_dir: Path, + segment_name: str, + expected_shape: tuple[int, int], + should_blur_ink_labels: bool = False, + ink_labels_blur_kernel_size: int = 17, + min_ink_component_size: int = 1000, + ink_erosion: int = 0, + ignore_idx: int = -100, +) -> np.ndarray: + # Channel 0: default sampling + # Channel 1: strict sampling + # Channel 2: probabilistic sampling + mask_paths = [] + for i in range(3): + base_name = f"{segment_name}_inklabels" + mask_path = ink_label_dir / f"{base_name}_{i}.png" + if i == 0: + # Check that we don't simultaneously have _inklabels.png or _inklabels_0.png + mask_path_default = ink_label_dir / f"{base_name}.png" + if mask_path.exists() and mask_path_default.exists(): + raise ValueError( + f"Found ambiguous channel 0 existing masks {mask_path} and {mask_path_default}. Please choose one." + ) + elif mask_path.exists(): + mask_paths.append(mask_path) + elif mask_path_default.exists(): + mask_paths.append(mask_path_default) + else: + logging.info(f"Didn't find channel {i} ink mask for {segment_name}.") + mask_paths.append(None) + else: + if mask_path.exists(): + mask_paths.append(mask_path) + else: + logging.info(f"Didn't find channel {i} ink mask for {segment_name}.") + mask_paths.append(None) + + # Check that mask 0 is not None if probabilistic one is not none + if mask_paths[2] is not None: + if mask_paths[0] is None: + raise ValueError( + f"Found channel 2 ink mask without channel 0 ink mask for segment {segment_name}." + ) + + if all(p is None for p in mask_paths): + raise ValueError(f"No ink masks found for {segment_name}.") + + ink_mask_list = [] + for mask_path in mask_paths: + if mask_path is not None: + mask = read_ink_mask( + mask_path, + expected_shape=expected_shape, + should_blur=should_blur_ink_labels, + kernel_size=ink_labels_blur_kernel_size, + min_component_size=min_ink_component_size, + ink_erosion=ink_erosion, + ignore_idx=ignore_idx, + ) + else: + mask = np.zeros(expected_shape, dtype=np.float32) + ink_mask_list.append(mask) + + return np.stack(ink_mask_list) + + +def load_papyrus_non_ink_labels( + ink_label_dir: Path, + segment_name: str, + automatic_non_ink: bool, + expected_shape: tuple[int, int], + ink_mask_shape: tuple[int, int], +) -> np.ndarray | None: + non_ink_mask_path = ink_label_dir / f"{segment_name}_papyrusnoninklabels.png" + if non_ink_mask_path.exists(): + logging.info(f"Loading papyrus non-ink mask for segment {segment_name}.") + non_ink_mask = read_papy_non_ink_labels(non_ink_mask_path, expected_shape=expected_shape) + if non_ink_mask.shape != ink_mask_shape[1:]: + raise ValueError( + f"Non-ink mask must have the same shape as the ink mask. Segment {segment_name} ink " + f"non-ink mask has shape {non_ink_mask.shape} and ink labels shape {ink_mask_shape}." + ) + else: + if not automatic_non_ink: + raise FileNotFoundError(f"Non-ink mask file does not exist: {non_ink_mask_path}") + else: + logging.info( + f"Automatically generated non-ink mask will be used for segment {segment_name}." + ) + non_ink_mask = None + return non_ink_mask + + +def infer_z_reversal(segment_name: str) -> bool: + z_reverse = False + segment_name_base_part = segment_name.split("_C")[0].split("_superseded")[0] + if segment_name_base_part not in KNOWN_Z_ORIENTATION_SEGMENT_IDS: + warn(f"Segment {segment_name} has an unknown z-orientation and will not be flipped.") + + if segment_name_base_part in Z_REVERSED_SEGMENT_IDS: + logging.info(f"Reversing z-orientation of segment {segment_name}.") + z_reverse = True + + return z_reverse + + +def default_patch_sample( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + size: int, + stride: int, + dilation_kernel_size: int, + min_labeled_coverage_frac: float = 0.5, + ink_label_thresh: float = 0.05, + tile_size: int = 256, + ink_preds: np.ndarray | None = None, + model_based_ink_correction_thresh: float = 0.1, + model_based_non_ink_correction_thresh: float = 0.3, + clean_up_ink_labels: bool = True, + clean_up_non_ink_labels: bool = True, + ink_mask_channel_2: np.ndarray | None = None, + p_0_ink: float = 0.3, + p_2_ink: float = 0.6, + p_non_ink: float = 0.1, + ignore_idx: int = -100, + non_ink_mask: np.ndarray | None = None, +) -> tuple[list[tuple[int, int, int, int]], list[bool]]: + if non_ink_mask is None: + non_ink_mask = create_non_ink_mask(ink_mask, dilation_kernel_size=dilation_kernel_size) + + if ink_preds is not None: + ink_mask, non_ink_mask = apply_model_based_label_correction( + ink_mask, + non_ink_mask, + ink_preds, + ink_label_thresh=ink_label_thresh, + model_based_ink_correction_thresh=model_based_ink_correction_thresh, + model_based_non_ink_correction_thresh=model_based_non_ink_correction_thresh, + clean_up_ink_labels=clean_up_ink_labels, + clean_up_non_ink_labels=clean_up_non_ink_labels, + ) + else: + if clean_up_ink_labels or clean_up_non_ink_labels: + logging.warning( + f"No ink predictions were found. Skipping model-based label correction." + ) + + if ink_mask_channel_2 is None: + patch_positions_seg = get_valid_patch_positions_batched( + ink_mask, + non_ink_mask, + segment_mask, + patch_shape=(size, size), + patch_stride=stride, + min_labeled_coverage_frac=min_labeled_coverage_frac, + chunk_size=tile_size, + should_pad=True, + ink_thresh=ink_label_thresh, + ) + else: + patch_positions_seg = get_probabilistic_patch_samples( + ink_mask, + ink_mask_channel_2, + non_ink_mask, + size, + stride, + p0=p_0_ink, + p2=p_2_ink, + p_non_ink=p_non_ink, + ignore_idx=ignore_idx, + ) + + non_ink_ignored_patches_seg = [False] * len(patch_positions_seg) + return patch_positions_seg, non_ink_ignored_patches_seg + + +def all_ink_patch_sample( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + size: int, + stride: int, + tile_size: int = 256, +) -> tuple[list, list[bool]]: + patch_positions_seg = get_ink_patch_positions_batched( + ink_mask, + segment_mask, + patch_shape=(size, size), + patch_stride=stride, + chunk_size=tile_size, + should_pad=True, + all_ink_patches=True, + ) + non_ink_ignored_patches_seg = [True] * len(patch_positions_seg) + return patch_positions_seg, non_ink_ignored_patches_seg + + +def any_ink_patch_sample( + ink_mask: np.ndarray, + segment_mask: np.ndarray, + size: int, + stride: int, + tile_size: int = 256, +) -> tuple[list, list[bool]]: + patch_positions_seg = get_ink_patch_positions_batched( + ink_mask, + segment_mask, + patch_shape=(size, size), + patch_stride=stride, + chunk_size=tile_size, + should_pad=True, + all_ink_patches=False, + ) + non_ink_ignored_patches_seg = [True] * len(patch_positions_seg) + return patch_positions_seg, non_ink_ignored_patches_seg + + +def create_patch_position_data( + segment_masks: list[np.ndarray], + ink_masks: list[np.ndarray], + ink_dilation_kernel_size: int, + size: int, + stride: int, + stride_strict: int | None = None, + strict_sampling_only_ink: bool = True, + split: str | None = None, + tile_size: int = 256, + min_labeled_coverage_frac: float = 0.5, + ink_preds: list[np.ndarray] | None = None, + model_based_ink_correction_thresh: float = 0.1, + model_based_non_ink_correction_thresh: float = 0.3, + clean_up_ink_labels: bool = True, + clean_up_non_ink_labels: bool = True, + p_0_ink: float = 0.3, + p_2_ink: float = 0.6, + p_non_ink: float = 0.6, + ignore_idx: int = -100, + non_ink_masks: list[np.ndarray | None] | None = None, +) -> tuple[list[list[int, int, int, int]], list[list[bool]]]: + def format_str(left: str, right: str, middle: str | None = None): + seq = (left, right) if middle is None else (left, middle, right) + return " ".join(seq) + + if ink_preds is None: + ink_preds_placeholder = [None] * len(segment_masks) + else: + ink_preds_placeholder = ink_preds + + if non_ink_masks is None: + non_ink_masks_placeholder = [None] * len(segment_masks) + else: + non_ink_masks_placeholder = non_ink_masks + + non_ink_ignored_patches = [] + patch_positions = [] + pbar = tzip( + segment_masks, + ink_masks, + ink_preds_placeholder, + non_ink_masks_placeholder, + desc=format_str(left="Creating", middle=split, right="patch positions..."), + ) + for segment_mask, ink_mask, ink_pred, non_ink_mask in pbar: + positions = [] + non_ink_ignored_patches_multi_channel = [] + + # 1. Default sampling first. + ink_mask_channel_0 = ink_mask[0] + if ink_mask_channel_0[ink_mask_channel_0 != ignore_idx].any(): + ink_mask_channel_2 = ink_mask[2] + has_ink_channel_2 = ink_mask_channel_2[ink_mask_channel_2 != ignore_idx].any() + patch_positions_seg, non_ink_ignored_patches_seg = default_patch_sample( + ink_mask_channel_0, + segment_mask, + size, + stride, + ink_dilation_kernel_size, + tile_size=tile_size, + min_labeled_coverage_frac=min_labeled_coverage_frac, + ink_preds=ink_pred, + model_based_ink_correction_thresh=model_based_ink_correction_thresh, + model_based_non_ink_correction_thresh=model_based_non_ink_correction_thresh, + clean_up_ink_labels=clean_up_ink_labels, + clean_up_non_ink_labels=clean_up_non_ink_labels, + ink_mask_channel_2=ink_mask_channel_2 if has_ink_channel_2 else None, + p_0_ink=p_0_ink, + p_2_ink=p_2_ink, + p_non_ink=p_non_ink, + ignore_idx=ignore_idx, + non_ink_mask=non_ink_mask, + ) + sampling_type = "probabilistic-based" if has_ink_channel_2 else "default" + logging.info( + format_str( + left=f"Created {len(patch_positions_seg)}", + middle=split, + right=f"{sampling_type} patch positions for ink mask channel 0.", + ) + ) + positions += patch_positions_seg + non_ink_ignored_patches_multi_channel += non_ink_ignored_patches_seg + else: + logging.info(f"Skipping default sampling for ink mask.") + + # 2. Strict sampling. + ink_mask_channel_1 = ink_mask[1] + if ink_mask_channel_1[ink_mask_channel_1 != ignore_idx].any(): + channel_stride = stride if stride_strict is None else stride_strict + if strict_sampling_only_ink: + patch_positions_seg, non_ink_ignored_patches_seg = all_ink_patch_sample( + ink_mask_channel_1, + segment_mask, + size, + channel_stride, + tile_size=tile_size, + ) + else: + patch_positions_seg, non_ink_ignored_patches_seg = any_ink_patch_sample( + ink_mask_channel_1, + segment_mask, + size, + channel_stride, + tile_size=tile_size, + ) + logging.info( + format_str( + left=f"Created {len(patch_positions_seg)}", + middle=split, + right=f"strict patch positions for ink mask channel 1.", + ) + ) + positions += patch_positions_seg + non_ink_ignored_patches_multi_channel += non_ink_ignored_patches_seg + else: + logging.info(f"Skipping strict sampling for ink mask.") + + non_ink_ignored_patches.append(non_ink_ignored_patches_multi_channel) + logging.info( + format_str(left=f"Created {len(positions)}", middle=split, right="patch positions.") + ) + patch_positions.append(positions) + + return patch_positions, non_ink_ignored_patches + + +create_train_patch_positions = partial(create_patch_position_data, split="train") +create_val_patch_positions = partial(create_patch_position_data, split="validation") + + +def load_segment_mask(segment: ScrollSegment, pad_y: int = 0, pad_x: int = 0) -> np.ndarray: + segment_mask = segment.load_mask() + segment_mask = np.pad(segment_mask, [(0, pad_y), (0, pad_x)], constant_values=0) + if "frag" in segment.segment_name: + segment_mask = cv2.resize( + segment_mask, + (segment_mask.shape[1] // 2, segment_mask.shape[0] // 2), + interpolation=cv2.INTER_AREA, + ) + return segment_mask + + +def compute_xy_padding(img_shape: tuple[int, int], tile_size: int) -> tuple[int, int]: + pad_y = tile_size - img_shape[0] % tile_size + pad_x = tile_size - img_shape[1] % tile_size + return pad_y, pad_x + + +def preprocess_image_stack_memmap( + img_stack_ref: np.memmap, + pad_y: int, + pad_x: int, + z_reverse: bool = False, + clip_min: int = 0, + clip_max: int = 200, + depth_dim_last: bool = True, + memmap_prefix: str | None = None, +): + # Rescale intensities and convert to uint8. + img_stack_new = img_as_ubyte(img_stack_ref) + + # Pad image. + img_stack_new = np.pad(img_stack_new, [(0, 0), (0, pad_y), (0, pad_x)], constant_values=0) + + # Reverse along z-dimension if necessary + if z_reverse: + img_stack_new = img_stack_new[::-1] + + # Clip intensities + img_stack_new = np.clip(img_stack_new, clip_min, clip_max) + + # Swap axes (move depth dimension to be last). + if depth_dim_last: + img_stack_new = np.moveaxis(img_stack_new, 0, -1) + + # Create new memmap + memmap_dir = Path(os.environ.get("MEMMAP_DIR", mkdtemp())) + prefix = Path(img_stack_ref.filename).stem if memmap_prefix is None else memmap_prefix + file_name = memmap_dir / f"{prefix}_processed.dat" + new_memmap = np.memmap( + str(file_name), dtype=img_stack_new.dtype, mode="w+", shape=img_stack_new.shape + ) + new_memmap[:] = img_stack_new + + new_memmap.flush() + + return new_memmap + + +def create_img_stack_memmap( + segment: ScrollSegment, + z_min: int | None = None, + z_max: int | None = None, + pad_y: int = 0, + pad_x: int = 0, + z_reverse: bool = False, + clip_min: int = 0, + clip_max: int = 200, + depth_dim_last: bool = True, +) -> np.memmap: + logging.debug(f"Loading original volume as memmap...") + img_stack_orig = segment.load_volume_as_memmap(z_min, z_max) + + logging.debug(f"Creating new preprocessed image stack memmap...") + img_stack_ref_new = preprocess_image_stack_memmap( + img_stack_orig, + pad_y, + pad_x, + z_reverse=z_reverse, + clip_min=clip_min, + clip_max=clip_max, + depth_dim_last=depth_dim_last, + memmap_prefix=segment.segment_name_orig, + ) + + logging.debug(f"Deleting original memmap...") + delete_memmap(img_stack_orig) + + return img_stack_ref_new + + +def _check_ink_labels(ink_label_dir: Path, labeled_segment_names: list[str]) -> None: + if not ink_label_dir.is_dir(): + raise ValueError("Ink label directory does not exist") + + segment_names_in_ink_labels = { + p.stem.split("_inklabels")[0] for p in ink_label_dir.glob("*_inklabels*.png") + } + labeled_segment_names = set(labeled_segment_names) + if not labeled_segment_names.issubset(segment_names_in_ink_labels): + missing_ink_labels = labeled_segment_names - segment_names_in_ink_labels + raise ValueError( + f"Input segments must be a subset of ink label segments. Missing ink labels for the " + f"following segments: {missing_ink_labels}." + ) + + +def _check_ink_pred_dir(ink_pred_dir: Path, labeled_segment_names: list[str]) -> None: + if not ink_pred_dir.is_dir(): + raise ValueError("Ink prediction directory does not exist") + + segment_names_in_ink_preds = { + p.stem.split("_inklabels")[0] for p in ink_pred_dir.glob("*_inklabels*.png") + } + segment_names_in_ink_labels = set(labeled_segment_names) + if not segment_names_in_ink_labels.issubset(segment_names_in_ink_preds): + missing_ink_preds = segment_names_in_ink_labels - segment_names_in_ink_preds + raise ValueError( + f"Ink label segments must be a subset of ink prediction segments. Missing predictions for the " + f"following segments: {missing_ink_preds}." + ) + + +def _check_papyrus_non_ink_labels(ink_labels_dir: Path, labeled_segment_names: list[str]) -> None: + if not ink_labels_dir.is_dir(): + raise ValueError("Ink labels directory does not exist") + + # Check the every labeled segment name has a papyrus non-ink label. + non_ink_segment_names = { + p.stem.split("_papyrusnoninklabels")[0] + for p in ink_labels_dir.glob("*_papyrusnoninklabels.png") + } + segment_name_set = set(labeled_segment_names) + if not segment_name_set.issubset(non_ink_segment_names): + missing_non_ink_masks = segment_name_set - non_ink_segment_names + raise ValueError( + f"Each ink label must have a corresponding papyrus non-ink label. Missing non-ink " + f"labels for the following segments: {missing_non_ink_masks}" + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval.py new file mode 100644 index 0000000..be24325 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval.py @@ -0,0 +1,173 @@ +import os +from pathlib import Path + +import albumentations as A +import cv2 +import numpy as np +from albumentations.pytorch import ToTensorV2 +from pytorch_lightning.utilities.types import EVAL_DATALOADERS +from torch.utils.data import DataLoader +from tqdm import tqdm + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.younader_scroll_patch_dataset_eval import ( + CustomDatasetTest, +) + + +class ScrollPatchDataModuleEval(BasePatchDataModule): + def __init__( + self, + prediction_segment_id: str, + scroll_id: str, + data_dir: Path = SCROLL_DATA_DIR, + z_min: int = 15, + z_max: int = 45, + size: int = 64, + z_reverse: bool = False, + tile_size: int = 256, + patch_stride: int = 32, + downsampling: int | None = None, + batch_size: int = 512, + num_workers: int = 0, + ): + super().__init__( + data_dir=data_dir, + z_min=z_min, + z_max=z_max, + patch_surface_shape=(size, size), + patch_stride=patch_stride, + downsampling=downsampling, + batch_size=batch_size, + num_workers=num_workers, + ) + self.size = size + self.tile_size = tile_size + self.z_reverse = z_reverse + self.prediction_segment_id = prediction_segment_id + self.segment_dir = self.data_dir / scroll_id + + @property + def z_extent(self) -> int: + return self.z_max - self.z_min + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "predict"). + """ + if stage == "predict" or stage is None: + images, xyxys = get_img_splits( + self.prediction_segment_id, + str(self.segment_dir), + self.z_reverse, + self.tile_size, + self.patch_stride, + self.z_min, + self.z_max, + ) + if len(xyxys) == 0: + raise ValueError("No patches found.") + + self.data_predict = CustomDatasetTest( + images, + np.stack(xyxys), + transform=self.predict_transform(), + ) + + def predict_transform(self) -> A.Compose: + return A.Compose( + [ + A.Resize(self.size, self.size), + A.Normalize(mean=[0] * self.z_extent, std=[1] * self.z_extent), + ToTensorV2(transpose_mask=True), + ] + ) + + def predict_dataloader(self) -> EVAL_DATALOADERS: + return DataLoader( + self.data_predict, + batch_size=self.batch_size, + shuffle=False, + num_workers=self.num_workers, + pin_memory=True, + drop_last=False, + ) + + +def read_image_mask( + segment_id: str, + segment_path: str, + reverse: bool, + tile_size: int, + start_idx: int = 15, + end_idx: int = 45, +): + images = [] + + idxs = range(start_idx, end_idx) + is_monster_segment = segment_id.startswith("Scroll1_part_1_wrap") + for i in idxs: + surface_vol_dirname = "layers" if not is_monster_segment else "surface_volume" + surface_vol_dir = Path(segment_path) / segment_id / surface_vol_dirname + n_digits = len(list(surface_vol_dir.glob("*.tif"))[0].stem) + tif_num = str(i).zfill(n_digits) + tif_path = surface_vol_dir / f"{tif_num}.tif" + image = cv2.imread(str(tif_path), cv2.IMREAD_GRAYSCALE) + if image is None: + raise ValueError(f"image is None ({segment_id} - i={i})") + pad0 = tile_size - image.shape[0] % tile_size + pad1 = tile_size - image.shape[1] % tile_size + + image = np.pad(image, [(0, pad0), (0, pad1)], constant_values=0) + image = np.clip(image, 0, 255) + + images.append(image) + images = np.stack(images, axis=2) + if reverse: + images = images[:, :, ::-1] + segment_mask = None + if os.path.exists(f"{segment_path}/{segment_id}/{segment_id}_mask.png"): + segment_mask = cv2.imread(f"{segment_path}/{segment_id}/{segment_id}_mask.png", 0) + segment_mask = np.pad(segment_mask, [(0, pad0), (0, pad1)], constant_values=0) + kernel = np.ones((16, 16), np.uint8) + segment_mask = cv2.erode(segment_mask, kernel, iterations=1) + return images, segment_mask + + +def get_img_splits( + segment_id: str, + segment_path: str, + reverse: bool, + tile_size: int, + stride: int, + start_idx: int, + end_idx: int, +): + images = [] + xyxys = [] + image, segment_mask = read_image_mask( + segment_id, segment_path, reverse, tile_size, start_idx, end_idx + ) + + if image is None: + raise ValueError(f"image is None (segment={segment_id})") + + if segment_mask is None: + raise ValueError(f"segment_mask is None (segment={segment_id})") + + x1_list = list(range(0, image.shape[1] - tile_size + 1, stride)) + y1_list = list(range(0, image.shape[0] - tile_size + 1, stride)) + for y1 in tqdm(y1_list, desc="Getting img splits..."): + for x1 in x1_list: + y2 = y1 + tile_size + x2 = x1 + tile_size + # Must be fully within the masked region. + if not np.any(segment_mask[y1:y2, x1:x2] == 0): + images.append(image[y1:y2, x1:x2]) + xyxys.append([x1, y1, x2, y2]) + return images, xyxys diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval_mmap.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval_mmap.py new file mode 100644 index 0000000..8dc15e2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_data_module_eval_mmap.py @@ -0,0 +1,152 @@ +import logging +from pathlib import Path + +import albumentations as A +import numpy as np +from albumentations.pytorch import ToTensorV2 +from pytorch_lightning.utilities.types import EVAL_DATALOADERS +from torch.utils.data import DataLoader + +from vesuvius_challenge_rnd import SCROLL_DATA_DIR +from vesuvius_challenge_rnd.data import ScrollSegment +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.base_patch_data_module import ( + BasePatchDataModule, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.block_dataset_eval import ( + BlockZConstantDatasetEval, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.memmap import ( + MEMMAP_DIR, + load_segment_as_memmap, + save_segment_as_memmap, + segment_to_memmap_path, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_sampling import ( + get_all_patch_positions_non_masked_batched, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.scroll_patch_data_module import ( + compute_xy_padding, + load_segment_mask, +) + + +class ScrollPatchDataModuleEvalMmap(BasePatchDataModule): + def __init__( + self, + prediction_segment_id: str, + scroll_id: str, + data_dir: Path = SCROLL_DATA_DIR, + z_min: int = 15, + z_max: int = 45, + size: int = 64, + z_reverse: bool = False, + tile_size: int = 256, + patch_stride: int = 32, + clip_min: int = 0, + clip_max: int = 255, + downsampling: int | None = None, + batch_size: int = 512, + num_workers: int = 0, + load_in_memory: bool = True, + memmap_dir: Path = MEMMAP_DIR, + skip_save_mmap_if_exists: bool = True, + ): + super().__init__( + data_dir=data_dir, + z_min=z_min, + z_max=z_max, + patch_surface_shape=(size, size), + patch_stride=patch_stride, + downsampling=downsampling, + batch_size=batch_size, + num_workers=num_workers, + ) + self.size = size + self.tile_size = tile_size + self.z_reverse = z_reverse + self.prediction_segment_id = prediction_segment_id + self.clip_min = clip_min + self.clip_max = clip_max + self.load_in_memory = load_in_memory + self.memmap_dir = memmap_dir + self.skip_save_mmap_if_exists = skip_save_mmap_if_exists + + # Instantiate segment. + self.segment = ScrollSegment( + scroll_id, self.prediction_segment_id, scroll_dir=self.data_dir + ) + + @property + def z_extent(self) -> int: + return self.z_max - self.z_min + + def prepare_data(self) -> None: + """Download data to disk. + + This method checks if the data directory is empty and raises an exception + if the data is not found. + + Raises: + ValueError: If the data directory is empty. + """ + super().prepare_data() + + self.memmap_dir.mkdir(exist_ok=True, parents=True) + memmap_path = segment_to_memmap_path(self.segment, memmap_dir=self.memmap_dir) + if memmap_path.exists() and self.skip_save_mmap_if_exists: + logging.info(f"Found existing memmap for segment {self.segment.segment_name}.") + else: + logging.info(f"Creating new memmap for segment {self.segment.segment_name}.") + save_segment_as_memmap( + self.segment, + memmap_path, + load_in_memory=self.load_in_memory, + ) + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "predict"). + """ + if stage == "predict" or stage is None: + mmap_path = segment_to_memmap_path(self.segment, memmap_dir=self.memmap_dir) + img_stack_ref = load_segment_as_memmap(mmap_path) + pad_y, pad_x = compute_xy_padding( + img_shape=self.segment.surface_shape, tile_size=self.tile_size + ) + segment_mask = load_segment_mask(self.segment, pad_y=pad_y, pad_x=pad_x) + patch_positions = get_all_patch_positions_non_masked_batched( + segment_mask, (self.size, self.size), self.patch_stride, chunk_size=self.tile_size + ) + self.data_predict = BlockZConstantDatasetEval( + img_stack_ref, + np.array(patch_positions), + self.size, + z_start=self.z_min, + z_extent=self.z_extent, + transform=self.predict_transform(), + z_reverse=self.z_reverse, + padding_yx=(pad_y, pad_x), + clip_min=self.clip_min, + clip_max=self.clip_max, + ) + + def predict_transform(self) -> A.Compose: + return A.Compose( + [ + A.Resize(self.size, self.size), + A.Normalize(mean=[0] * self.z_extent, std=[1] * self.z_extent), + ToTensorV2(transpose_mask=True), + ] + ) + + def predict_dataloader(self) -> EVAL_DATALOADERS: + return DataLoader( + self.data_predict, + batch_size=self.batch_size, + shuffle=False, + num_workers=self.num_workers, + pin_memory=True, + drop_last=False, + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_dataset.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_dataset.py new file mode 100644 index 0000000..5cbaec2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/scroll_patch_dataset.py @@ -0,0 +1,64 @@ +from collections.abc import Callable + +from vesuvius_challenge_rnd.data import preprocess_subvolume +from vesuvius_challenge_rnd.data.volumetric_segment import VolumetricSegment +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.data.in_memory_surface_volume_dataset import ( + SurfaceVolumeDataset, +) + + +class ScrollPatchDataset(SurfaceVolumeDataset): + def __init__( + self, + segments: list[VolumetricSegment], + z_min: int = 27, + z_max: int = 37, + patch_surface_shape: tuple[int, int] = (512, 512), + patch_stride: int = 256, + transform: Callable | None = None, + prog_bar: bool = True, + ): + """Initialize the PatchDataset. + + Args: + segments (list[int]): List of segment IDs to load. + z_min (int, optional): Minimum z-slice to include. Defaults to 27. + z_max (int, optional): Maximum z-slice to include. Defaults to 37. + patch_surface_shape (tuple[int, int], optional): Shape of the patches. Defaults to (512, 512). + patch_stride (int, optional): Stride for patch creation. Defaults to 256. + transform (Callable | None, optional): Optional transformation to apply. Defaults to None. + prog_bar (bool, optional): Whether to show a progress bar while loading. Defaults to True. + """ + super().__init__( + segments, + z_min, + z_max, + patch_surface_shape, + patch_stride, + transform, + prog_bar, + ) + + def __getitem__(self, index: int) -> tuple: + """Get a patch and patch position by index. + + Args: + index (int): Index of the patch to retrieve. + + Returns: + tuple: A tuple containing the patch and patch position. + """ + fragment_idx = self.idx_to_segment_idx(index) + + patch_pos = self.usable_patch_position_arrs[index] + ((y0, x0), (y1, x1)) = patch_pos + + patch = preprocess_subvolume( + self.img_stacks[fragment_idx][:, y0:y1, x0:x1], slice_dim_last=True + ) + + if self.transform is not None: + transformed = self.transform(image=patch) + patch = transformed["image"] + + return patch, patch_pos diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/semi_supervised_scroll_patch_data_module.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/semi_supervised_scroll_patch_data_module.py new file mode 100644 index 0000000..a008f64 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/semi_supervised_scroll_patch_data_module.py @@ -0,0 +1,270 @@ +import logging +from pathlib import Path + +import numpy as np +from patchify import patchify +from pytorch_lightning.utilities import CombinedLoader +from pytorch_lightning.utilities.types import TRAIN_DATALOADERS +from torch.utils.data import ConcatDataset, DataLoader +from tqdm.contrib import tzip + +from vesuvius_challenge_rnd import REPO_DIR, SCROLL_DATA_DIR +from vesuvius_challenge_rnd.patching import patch_index_to_pixel_position +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.patch_memmap_dataset import ( + PatchMemMapDatasetUnlabeled, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.scroll_patch_data_module import ( + ScrollPatchDataModule, + ScrollSegmentsDataType, + create_dataset, + create_non_ink_mask, + create_train_patch_positions, + create_val_patch_positions, + generate_dataset_inputs, +) + + +class SemiSupervisedScrollPatchDataModule(ScrollPatchDataModule): + def __init__( + self, + train_segment_ids: ScrollSegmentsDataType, + val_segment_id: str | tuple[str, str], + data_dir: Path = SCROLL_DATA_DIR, + ink_label_dir: Path = REPO_DIR / "external" / "youssef-nader-first-letters" / "labels", + z_min: int = 15, + z_max: int = 45, + size: int = 64, + tile_size: int = 256, + patch_stride_train: int = 32, + patch_stride_val: int = 32, + downsampling: int | None = None, + batch_size: int = 256, + num_workers: int = 0, + blur_ink_labels: bool = False, + ink_labels_blur_kernel_size: int = 17, + ink_dilation_kernel_size: int = 256, + ): + super().__init__( + train_segment_ids, + val_segment_id, + data_dir=data_dir, + ink_label_dir=ink_label_dir, + z_min=z_min, + z_max=z_max, + size=size, + tile_size=tile_size, + patch_stride_train=patch_stride_train, + patch_stride_val=patch_stride_val, + downsampling=downsampling, + batch_size=batch_size, + num_workers=num_workers, + blur_ink_labels=blur_ink_labels, + ink_labels_blur_kernel_size=ink_labels_blur_kernel_size, + ink_dilation_kernel_size=ink_dilation_kernel_size, + ) + + def setup(self, stage: str) -> None: + """Set up the data for the given stage. + + Args: + stage (str): The stage for which to set up the data (e.g., "fit"). + """ + if stage == "fit" or stage is None: + img_stack_refs_train, segment_masks_train, ink_masks_train, _ = generate_dataset_inputs( + self.segments_train, + self.z_min, + self.z_max, + self.tile_size, + self.ink_label_dir, + should_blur_ink_labels=self.blur_ink_labels, + ink_labels_blur_kernel_size=self.ink_label_blur_kernel_size, + ) + + train_patch_positions_labeled = create_train_patch_positions( + img_stack_refs_train, + segment_masks_train, + ink_masks_train, + self.ink_dilation_kernel_size, + self.size, + self.patch_stride_train, + ) + + train_patch_positions_unlabeled = create_train_patch_positions_unlabeled( + img_stack_refs_train, + segment_masks_train, + ink_masks_train, + self.ink_dilation_kernel_size, + self.size, + self.patch_stride_train, + ) + + img_stack_refs_val, segment_masks_val, ink_masks_val, _ = generate_dataset_inputs( + [self.segment_val], self.z_min, self.z_max, self.tile_size, self.ink_label_dir + ) + val_patch_positions = create_val_patch_positions( + img_stack_refs_val, + segment_masks_val, + ink_masks_val, + self.ink_dilation_kernel_size, + self.size, + self.patch_stride_val, + ) + + self.data_train_labeled = create_dataset( + img_stack_refs_train, + train_patch_positions_labeled, + ink_masks_train, + self.size, + self.z_extent, + augment=True, + transform=self.train_transform(), + ) + + self.data_train_unlabeled = create_unlabeled_dataset( + img_stack_refs_train, + train_patch_positions_unlabeled, + self.size, + self.z_extent, + augment=True, + transform=self.train_transform(), + ) + + self.data_val = create_dataset( + img_stack_refs_val, + val_patch_positions, + ink_masks_val, + self.size, + self.z_extent, + augment=False, + transform=self.val_transform(), + ) + + def train_dataloader(self) -> TRAIN_DATALOADERS: + labeled_data_loader = DataLoader( + self.data_train_labeled, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=True, + pin_memory=True, + drop_last=True, + ) + unlabeled_data_loader = DataLoader( + self.data_train_unlabeled, + batch_size=self.batch_size, + num_workers=self.num_workers, + shuffle=True, + pin_memory=True, + drop_last=True, + ) + iterables = { + "labeled": labeled_data_loader, + "unlabeled": unlabeled_data_loader, + } + return CombinedLoader(iterables, mode="min_size") + + +def create_unlabeled_dataset( + img_stack_refs: list[np.memmap], + patch_position_list: list, + size: int, + z_extent: int, + augment: bool = False, + transform=None, +) -> ConcatDataset: + datasets = [] + for img_stack_ref, patch_positions in zip(img_stack_refs, patch_position_list): + datasets.append( + PatchMemMapDatasetUnlabeled( + img_stack_ref, + np.array(patch_positions), + size, + z_extent=z_extent, + augment=augment, + transform=transform, + ) + ) + return ConcatDataset(datasets) + + +def create_train_patch_positions_unlabeled( + img_stack_refs: list[np.memmap], + segment_masks: list[np.ndarray], + ink_masks: list[np.ndarray], + dilation_kernel_size: int, + size: int, + stride: int, +): + train_patch_positions = [] + pbar = tzip(img_stack_refs, segment_masks, ink_masks, desc="Creating train patches...") + for img_stack_ref, segment_mask, ink_mask in pbar: + # Pad ink mask + pad_y = segment_mask.shape[0] - ink_mask.shape[0] + pad_x = segment_mask.shape[1] - ink_mask.shape[1] + ink_mask = np.pad(ink_mask, [(0, pad_y), (0, pad_x)], constant_values=0) + + # Create non-ink mask + non_ink_mask = create_non_ink_mask(ink_mask, dilation_kernel_size=dilation_kernel_size) + + # Create patch positions + patch_shape = (size, size) + usable_patch_map = create_usable_patch_map_unlabeled_data( + segment_mask, ink_mask, non_ink_mask, patch_shape=patch_shape, patch_stride=stride + ) + usable_patch_position_arr = create_usable_patch_position_arr( + usable_patch_map, patch_shape=patch_shape, patch_stride=stride + ).tolist() + + logging.info(f"Created {len(usable_patch_position_arr)} training patch positions") + train_patch_positions.append(usable_patch_position_arr) + + return train_patch_positions + + +def create_usable_patch_map_unlabeled_data( + segment_mask: np.ndarray, + ink_mask: np.ndarray, + non_ink_mask: np.ndarray, + patch_shape: tuple[int, int], + patch_stride: int, + ink_thresh: float = 0.05, +) -> np.ndarray: + if (segment_mask.shape != ink_mask.shape) or (segment_mask.shape != non_ink_mask.shape): + raise ValueError( + f"Shapes of segment mask, ink mask, and non-ink mask must match. Found shapes " + f"{segment_mask.shape}, {ink_mask.shape}, {non_ink_mask.shape}, and respectively." + ) + segment_patches = patchify(segment_mask, patch_size=patch_shape, step=patch_stride) + ink_patches = patchify(ink_mask, patch_size=patch_shape, step=patch_stride) + non_ink_patches = patchify(non_ink_mask, patch_size=patch_shape, step=patch_stride) + # Create a usable patch map where true indicates that the patch is usable for training/validation/test and + # false indicates there is not enough papyrus. + usable_patch_map = np.empty(shape=(segment_patches.shape[:2]), dtype=bool) + for i in range(segment_patches.shape[0]): + for j in range(segment_patches.shape[1]): + segment_patch = segment_patches[i, j] + ink_patch = ink_patches[i, j] + non_ink_patch = non_ink_patches[i, j] + + no_overlap_with_non_segment_region = not np.any(segment_patch == 0) + ink_patch_has_no_ink = not np.any(ink_patch > ink_thresh) + non_ink_patch_has_no_non_ink = not np.any(non_ink_patch == 1) + + usable_patch_map[i, j] = ( + no_overlap_with_non_segment_region + and ink_patch_has_no_ink + and non_ink_patch_has_no_non_ink + ) + return usable_patch_map + + +def create_usable_patch_position_arr( + usable_patch_map: np.ndarray, patch_shape: tuple[int, int], patch_stride: int +) -> np.ndarray: + usable_patch_positions = [] + for i in range(usable_patch_map.shape[0]): + for j in range(usable_patch_map.shape[1]): + if usable_patch_map[i, j]: + (y0, x0), (y1, x1) = patch_index_to_pixel_position(i, j, patch_shape, patch_stride) + usable_patch_positions.append((x0, y0, x1, y1)) + + return np.array(usable_patch_positions) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_all_non_ink_labels_from_dir.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_all_non_ink_labels_from_dir.py new file mode 100644 index 0000000..e4d99d4 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_all_non_ink_labels_from_dir.py @@ -0,0 +1,76 @@ +"""Create all non-ink labels from an ink label directory.""" +import argparse +from pathlib import Path + +from dotenv import load_dotenv +from tqdm import tqdm + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.create_non_ink_labels import ( + DEFAULT_OUTPUT_DIR, + create_non_ink_labels, +) + + +def create_all_non_ink_labels_from_dir( + ink_label_dir: Path, output_dir: Path = DEFAULT_OUTPUT_DIR, dilation_kernel_size: int = 256 +) -> None: + """Create all non-ink labels from an ink label directory.""" + pattern_1 = "*_inklabels.png" + pattern_2 = "*_inklabels_0.png" + ink_labels_paths = [ + file for pattern in [pattern_1, pattern_2] for file in ink_label_dir.glob(pattern) + ] + print(f"Found {len(ink_labels_paths)} ink label paths in {ink_label_dir}.") + output_dir.mkdir(exist_ok=True, parents=True) + for path in tqdm(ink_labels_paths, desc="Generating non-ink labels"): + create_non_ink_labels( + path, + output_dir=output_dir, + dilation_kernel_size=dilation_kernel_size, + ) + + +def main(): + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + create_all_non_ink_labels_from_dir( + Path(args.ink_label_dir), + Path(args.output_dir), + dilation_kernel_size=args.dilation_kernel_size, + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate non-ink masks for many scroll segments from an ink label directory." + ) + parser.add_argument( + "ink_label_dir", + type=str, + help="The ink labels directory.", + ) + parser.add_argument( + "-o", + "--output_dir", + type=str, + default=DEFAULT_OUTPUT_DIR, + help="The output non-ink labels file path.", + ) + parser.add_argument( + "-k", + "--dilation_kernel_size", + type=int, + default=256, + help="The non-ink dilation kernel size.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_non_ink_labels.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_non_ink_labels.py new file mode 100644 index 0000000..da0571d --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/create_non_ink_labels.py @@ -0,0 +1,129 @@ +import argparse +from pathlib import Path + +import cv2 +import numpy as np +from dotenv import load_dotenv +from skimage.util import img_as_ubyte + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import _imwrite_uint8 +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.ink_label_correction import ( + apply_model_based_label_correction, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.label_utils import ( + create_non_ink_mask_from_ink_mask, + load_ink_mask, +) + +DEFAULT_OUTPUT_DIR = Path(__file__).parent + + +def create_non_ink_labels( + ink_labels_path: Path, + ink_pred_path: Path | None = None, + output_dir: Path = DEFAULT_OUTPUT_DIR, + ink_thresh: float = 0.05, + dilation_kernel_size: int = 256, + model_based_non_ink_correction_thresh: float = 0.99, +) -> None: + print(f"Creating non-ink labels from {ink_labels_path}") + ink_mask = (load_ink_mask(ink_labels_path) > ink_thresh).astype(np.uint8) + non_ink_mask = create_non_ink_mask_from_ink_mask( + ink_mask, dilation_kernel_size=dilation_kernel_size + ).astype(bool) + + # Possibly auto-clean non-ink mask. + if ink_pred_path is not None: + ink_pred_path = Path(ink_pred_path) + ink_pred = cv2.imread(str(ink_pred_path), cv2.IMREAD_GRAYSCALE) + if ink_pred.shape != ink_mask.shape: + raise ValueError( + f"Ink predictions must have the same shape as the ink mask. Ink " + f"preds has shape {ink_pred.shape} and ink labels shape {ink_mask.shape}." + ) + print(f"Applying model-based non-ink label correction using {ink_pred_path}") + _, non_ink_mask = apply_model_based_label_correction( + ink_mask, + non_ink_mask, + ink_pred, + ink_label_thresh=ink_thresh, + model_based_non_ink_correction_thresh=model_based_non_ink_correction_thresh, + clean_up_ink_labels=False, + clean_up_non_ink_labels=True, + ) + else: + print(f"Skipping model-based label correction.") + + # Save non-ink mask. + segment_name = ink_labels_path.stem.split("_inklabels")[0] + output_path = output_dir / f"{segment_name}_papyrusnoninklabels.png" + + print(f"Saving non-ink mask to {output_path}") + save_mask(img_as_ubyte(non_ink_mask), output_path) + + +def save_mask(mask: np.ndarray, output_path: Path) -> None: + """Save the non-ink mask to the preprocessing directory.""" + _imwrite_uint8(mask, output_path) + print(f"Saved non-ink mask of shape {mask.shape}.") + + +def main(): + load_dotenv() + parser = _set_up_parser() + args = parser.parse_args() + create_non_ink_labels( + Path(args.ink_labels_path), + args.ink_pred_path, + output_dir=Path(args.output_dir), + dilation_kernel_size=args.dilation_kernel_size, + model_based_non_ink_correction_thresh=args.model_based_non_ink_correction_thresh, + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate a non-ink mask for a scroll segment from an ink mask." + ) + parser.add_argument( + "ink_labels_path", + type=str, + help="The segment ink labels file path.", + ) + parser.add_argument( + "-p", + "--ink_pred_path", + type=str, + help="The segment prediction file path.", + ) + parser.add_argument( + "-o", + "--output_dir", + type=str, + default=DEFAULT_OUTPUT_DIR, + help="The output non-ink labels file path.", + ) + parser.add_argument( + "-k", + "--dilation_kernel_size", + type=int, + default=256, + help="The non-ink dilation kernel size.", + ) + parser.add_argument( + "-t", + "--model_based_non_ink_correction_thresh", + type=float, + default=0.99, + help="Model-based non-ink correction threshold. Only applicable if --ink_labels_path is provided.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/ink_pred_utils.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/ink_pred_utils.py new file mode 100644 index 0000000..fda6c14 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/ink_pred_utils.py @@ -0,0 +1,32 @@ +from pathlib import Path + +import cv2 +import numpy as np + + +def read_ink_preds( + segment_name: str, ink_preds_dir: Path, expected_shape: tuple[int, int] +) -> np.ndarray: + ink_pred_paths = list(p for p in ink_preds_dir.glob("*.png") if p.stem.startswith(segment_name)) + if len(ink_pred_paths) != 1: + raise ValueError( + f"Found {len(ink_pred_paths)} ink predictions for {segment_name} in {ink_preds_dir}. Expected one prediction per segment." + ) + + ink_pred_path = ink_pred_paths[0] + + if not ink_pred_path.is_file(): + raise FileNotFoundError(f"Could not find ink prediction: {ink_pred_path.resolve()}.") + + ink_pred = cv2.imread(str(ink_pred_path), cv2.IMREAD_GRAYSCALE) + + # Pad if necessary. + pad_y = expected_shape[0] - ink_pred.shape[0] + pad_x = expected_shape[1] - ink_pred.shape[1] + if pad_x < 0 or pad_y < 0: + raise ValueError( + f"expected shape ({expected_shape}) must be larger than raw ink pred shape ({ink_pred.shape}) for all dimensions." + ) + ink_pred = np.pad(ink_pred, [(0, pad_y), (0, pad_x)], constant_values=0) + + return ink_pred diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/label_utils.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/label_utils.py new file mode 100644 index 0000000..8492ed2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/label_utils.py @@ -0,0 +1,131 @@ +from pathlib import Path + +import cv2 +import numpy as np + + +def load_ink_mask( + mask_path: Path, +) -> np.ndarray: + mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) + + if "frag" in mask_path.stem: + mask = cv2.resize( + mask, (mask.shape[1] // 2, mask.shape[0] // 2), interpolation=cv2.INTER_AREA + ) + return mask + + +def preprocess_ink_mask( + ink_mask: np.ndarray, + expected_shape: tuple[int, int], + should_blur: bool = False, + kernel_size: int = 17, + min_component_size: int = 1_000, + ink_erosion: int = 0, + ignore_idx: int = -100, +) -> np.ndarray: + ink_mask = remove_small_components(ink_mask, min_size=min_component_size) + + if ink_erosion > 0: + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (ink_erosion, ink_erosion)) + ink_mask = cv2.erode(ink_mask, kernel, iterations=1) + + if should_blur: + ink_mask = cv2.GaussianBlur(ink_mask, (kernel_size, kernel_size), 0) + + ink_mask = ink_mask.astype("float32") + ink_mask /= 255 + + # Pad if necessary. + pad_y = expected_shape[0] - ink_mask.shape[0] + pad_x = expected_shape[1] - ink_mask.shape[1] + if pad_x < 0 or pad_y < 0: + raise ValueError( + f"expected shape ({expected_shape}) must be larger than raw ink mask shape ({ink_mask.shape}) for all dimensions." + ) + ink_mask = np.pad(ink_mask, [(0, pad_y), (0, pad_x)], constant_values=ignore_idx) + + return ink_mask + + +def remove_small_components(mask: np.ndarray, min_size: int = 1_000) -> np.ndarray: + if min_size == 0: + return mask + elif min_size < 0: + raise ValueError(f"min_size must be nonnegative. Found value {min_size}") + + num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, 8, cv2.CV_32S) + small_component_indices = np.where(stats[1:, cv2.CC_STAT_AREA] < min_size)[0] + 1 + small_components_mask = np.isin(labels, small_component_indices) + processed_mask = mask.copy() + processed_mask[small_components_mask] = 0 + + return processed_mask + + +def read_ink_mask( + mask_path: Path, + expected_shape: tuple[int, int], + should_blur: bool = False, + kernel_size: int = 17, + min_component_size: int = 1_000, + ink_erosion: int = 0, + ignore_idx: int = -100, +): + ink_mask = load_ink_mask(mask_path) + ink_mask = preprocess_ink_mask( + ink_mask, + expected_shape=expected_shape, + should_blur=should_blur, + kernel_size=kernel_size, + min_component_size=min_component_size, + ink_erosion=ink_erosion, + ignore_idx=ignore_idx, + ) + + return ink_mask + + +def read_papy_non_ink_labels( + non_ink_mask_path: Path, expected_shape: tuple[int, int] +) -> np.ndarray: + non_ink_mask = cv2.imread(str(non_ink_mask_path), cv2.IMREAD_GRAYSCALE) + + # Pad if necessary. + pad_y = expected_shape[0] - non_ink_mask.shape[0] + pad_x = expected_shape[1] - non_ink_mask.shape[1] + if pad_x < 0 or pad_y < 0: + raise ValueError( + f"expected shape ({expected_shape}) must be larger than raw non-ink shape ({non_ink_mask.shape}) for all dimensions." + ) + non_ink_mask = np.pad(non_ink_mask, [(0, pad_y), (0, pad_x)], constant_values=0) + non_ink_mask = non_ink_mask.astype(bool) + + return non_ink_mask + + +def create_non_ink_mask_from_ink_mask( + ink_mask: np.ndarray, dilation_kernel_size: int = 256 +) -> np.ndarray: + if ink_mask.dtype != np.uint8 or np.unique(ink_mask).size != 2: + raise ValueError("ink_mask must be a binary mask of uint8 type") + + structuring_element = cv2.getStructuringElement( + cv2.MORPH_RECT, (dilation_kernel_size, dilation_kernel_size) + ) + dilated_mask = cv2.dilate(ink_mask, structuring_element, iterations=1) + + # Subtract the original mask from the dilated mask to create the expanded mask + expanded_mask = cv2.subtract(dilated_mask, ink_mask) + return expanded_mask + + +def create_non_ink_mask( + ink_mask: np.ndarray, thresh: float = 0.5, dilation_kernel_size: int = 256 +) -> np.ndarray: + ink_mask_binarized = (ink_mask > thresh).astype(np.uint8) + non_ink_mask = create_non_ink_mask_from_ink_mask( + ink_mask_binarized, dilation_kernel_size=dilation_kernel_size + ).astype(np.float32) + return non_ink_mask diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/merge_masks.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/merge_masks.py new file mode 100644 index 0000000..7f96e64 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/merge_masks.py @@ -0,0 +1,69 @@ +"""Merge two sets of binary labels into a single mask.""" +import argparse +from pathlib import Path + +import cv2 +from skimage.util import img_as_ubyte + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import _imwrite_uint8 + +DEFAULT_OUTPUT_PATH = Path("merged_mask.png") + + +def merge_masks( + mask_path_1: Path, mask_path_2: Path, output_path: Path = DEFAULT_OUTPUT_PATH +) -> None: + # Load masks. + mask_1 = cv2.imread(str(mask_path_1), cv2.IMREAD_GRAYSCALE).astype(bool) + mask_2 = cv2.imread(str(mask_path_2), cv2.IMREAD_GRAYSCALE).astype(bool) + + # Merge masks. + output_mask = img_as_ubyte(mask_1 | mask_2) + + # Save masks. + print(f"Saving merged mask to {output_path.resolve()}") + _imwrite_uint8(output_mask, output_path) + print(f"Saved mask of shape {output_mask.shape}.") + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + merge_masks( + Path(args.mask_path_1), + Path(args.mask_path_2), + output_path=Path(args.output_path), + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate the union of two binary masks and save it as a uint8." + ) + parser.add_argument( + "mask_path_1", + type=str, + help="The first mask path.", + ) + parser.add_argument( + "mask_path_2", + type=str, + help="The second mask path.", + ) + parser.add_argument( + "-o", + "--output_path", + type=str, + default=DEFAULT_OUTPUT_PATH, + help="The path to the merged mask output file.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots.py new file mode 100644 index 0000000..5d741b3 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots.py @@ -0,0 +1,73 @@ +"""Merge two sets of binary labels into a single mask.""" +import argparse +from pathlib import Path + +import cv2 +from skimage.util import img_as_ubyte + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import _imwrite_uint8 +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.label_utils import ( + remove_small_components, +) + +DEFAULT_OUTPUT_PATH = Path("processed_mask.png") + + +def remove_small_dots( + mask_path: Path, min_size: int = 100, output_path: Path = DEFAULT_OUTPUT_PATH +) -> None: + # Load masks. + mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) + + # Remove small dots. + output_mask = img_as_ubyte(remove_small_components(mask, min_size=min_size)) + + # Save masks. + print(f"Saving processed mask to {output_path.resolve()}") + _imwrite_uint8(output_mask, output_path) + print(f"Saved mask of shape {output_mask.shape}.") + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + remove_small_dots( + Path(args.mask_path), + args.min_size, + output_path=Path(args.output_path), + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Remove small dots from a binary mask and save it as a uint8." + ) + parser.add_argument( + "mask_path", + type=str, + help="The mask path.", + ) + parser.add_argument( + "-m", + "--min_size", + type=int, + default=1000, + help="The minimum connected component size.", + ) + parser.add_argument( + "-o", + "--output_path", + type=str, + default=DEFAULT_OUTPUT_PATH, + help="The path to the processed mask output file.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots_from_dir.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots_from_dir.py new file mode 100644 index 0000000..2779db9 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/remove_small_dots_from_dir.py @@ -0,0 +1,73 @@ +"""Merge two sets of binary labels into a single mask.""" +import argparse +from pathlib import Path + +import cv2 +from skimage.util import img_as_ubyte +from tqdm import tqdm + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import _imwrite_uint8 +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.label_utils import ( + remove_small_components, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.tools.remove_small_dots import ( + remove_small_dots, +) + +DEFAULT_OUTPUT_DIR = Path("masks_no_dots_removed") + + +def remove_small_from_dir( + mask_dir: Path, min_size: int = 1000, output_dir: Path = DEFAULT_OUTPUT_DIR +) -> None: + mask_paths = list(mask_dir.glob("*.png")) + print(f"Found {len(mask_paths)} mask paths in {mask_dir}.") + output_dir.mkdir(exist_ok=True, parents=True) + for path in tqdm(mask_paths, desc="Removing small dots from masks..."): + output_path = output_dir / path.name + remove_small_dots(path, min_size=min_size, output_path=output_path) + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + remove_small_from_dir( + Path(args.mask_dir), + args.min_size, + output_dir=Path(args.output_dir), + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Remove small dots from a directory of binary masks and save them." + ) + parser.add_argument( + "mask_dir", + type=str, + help="The mask directory.", + ) + parser.add_argument( + "-m", + "--min_size", + type=int, + default=1000, + help="The minimum connected component size.", + ) + parser.add_argument( + "-o", + "--output_dir", + type=str, + default=DEFAULT_OUTPUT_DIR, + help="The path to the directory containing the processed mask output files.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/subtract_masks.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/subtract_masks.py new file mode 100644 index 0000000..fc80280 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/tools/subtract_masks.py @@ -0,0 +1,69 @@ +"""Subtract one binary mask from the other and save it as a uint8.""" +import argparse +from pathlib import Path + +import cv2 +from skimage.util import img_as_ubyte + +from vesuvius_challenge_rnd.data.preprocessors.fragment_preprocessor import _imwrite_uint8 + +DEFAULT_OUTPUT_PATH = Path("subtracted_mask.png") + + +def subtract_masks( + mask_path_1: Path, mask_path_2: Path, output_path: Path = DEFAULT_OUTPUT_PATH +) -> None: + # Load masks. + mask_1 = cv2.imread(str(mask_path_1), cv2.IMREAD_GRAYSCALE).astype(bool) + mask_2 = cv2.imread(str(mask_path_2), cv2.IMREAD_GRAYSCALE).astype(bool) + + # Subtract masks. + output_mask = img_as_ubyte(mask_1 & ~mask_2) + + # Save masks. + print(f"Saving subtracted mask to {output_path.resolve()}") + _imwrite_uint8(output_mask, output_path) + print(f"Saved mask of shape {output_mask.shape}.") + + +def main(): + parser = _set_up_parser() + args = parser.parse_args() + subtract_masks( + Path(args.mask_path_1), + Path(args.mask_path_2), + output_path=Path(args.output_path), + ) + + +def _set_up_parser() -> argparse.ArgumentParser: + """Set up argument parser for command-line interface. + + Returns: + ArgumentParser: Configured argument parser. + """ + parser = argparse.ArgumentParser( + description="Generate the subtraction (mask_1 - mask_2) of two binary masks and save it as a uint8." + ) + parser.add_argument( + "mask_path_1", + type=str, + help="The first mask path.", + ) + parser.add_argument( + "mask_path_2", + type=str, + help="The second mask path.", + ) + parser.add_argument( + "-o", + "--output_path", + type=str, + default=DEFAULT_OUTPUT_PATH, + help="The path to the subtracted mask output file.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset.py new file mode 100644 index 0000000..0659f22 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset.py @@ -0,0 +1,74 @@ +import random + +import numpy as np +import torch.nn.functional as F +from torch.utils.data import Dataset + + +class CustomDataset(Dataset): + def __init__( + self, + images, + size: int, + z_extent: int = 30, + label_downscale: int = 4, + xyxys=None, + labels=None, + transform=None, + ): + self.images = images + self.z_extent = z_extent + self.size = size + self.labels = labels + self.label_downscale = label_downscale + + self.transform = transform + self.xyxys = xyxys + + def __len__(self): + return len(self.images) + + def fourth_augment(self, image: np.ndarray) -> np.ndarray: + image_tmp = np.zeros_like(image) + cropping_num = random.randint(self.z_extent - 8, self.z_extent) + + start_idx = random.randint(0, self.z_extent - cropping_num) + crop_indices = np.arange(start_idx, start_idx + cropping_num) + + start_paste_idx = random.randint(0, self.z_extent - cropping_num) + + tmp = np.arange(start_paste_idx, cropping_num) + np.random.shuffle(tmp) + + cutout_idx = random.randint(0, 2) + temporal_random_cutout_idx = tmp[:cutout_idx] + + image_tmp[..., start_paste_idx : start_paste_idx + cropping_num] = image[..., crop_indices] + + if random.random() > 0.4: + image_tmp[..., temporal_random_cutout_idx] = 0 + image = image_tmp + return image + + def __getitem__(self, idx): + image = self.images[idx] + label = self.labels[idx] + if self.xyxys is not None: + xy = self.xyxys[idx] + image, label = self._transform_if_needed(image, label) + return image, label, xy + else: + image = self.fourth_augment(image) + image, label = self._transform_if_needed(image, label) + return image, label + + def _transform_if_needed(self, image, label): + if self.transform: + data = self.transform(image=image, mask=label) + image = data["image"].unsqueeze(0) + label = data["mask"] + label = F.interpolate( + label.unsqueeze(0), + (self.size // self.label_downscale, self.size // self.label_downscale), + ).squeeze(0) + return image, label diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset_eval.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset_eval.py new file mode 100644 index 0000000..2fbc6f1 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/younader_scroll_patch_dataset_eval.py @@ -0,0 +1,24 @@ +from typing import Optional + +from collections.abc import Callable + +from torch.utils.data import Dataset + + +class CustomDatasetTest(Dataset): + def __init__(self, images, xyxys, transform: Callable | None = None): + self.images = images + self.xyxys = xyxys + self.transform = transform + + def __len__(self) -> int: + return len(self.images) + + def __getitem__(self, idx: int): + image = self.images[idx] + xy = self.xyxys[idx] + if self.transform: + data = self.transform(image=image) + image = data["image"].unsqueeze(0) + + return image, xy diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/zarr.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/zarr.py new file mode 100644 index 0000000..61ba850 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/data/zarr.py @@ -0,0 +1,94 @@ +import logging +from pathlib import Path + +import zarr + +from vesuvius_challenge_rnd import DATA_DIR +from vesuvius_challenge_rnd.data.scroll import ScrollSegment +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.memmap import delete_memmap + +ZARR_ARRAY_DIR = DATA_DIR / "zarrs" + + +def segment_to_zarr_path( + segment: ScrollSegment, + zarr_dir: Path = ZARR_ARRAY_DIR, + z_min: int | None = None, + z_max: int | None = None, +) -> Path: + if (z_max is None and z_min is not None) or (z_max is not None and z_min is None): + raise ValueError("z_max and z_min must be both None or be set to integer values.") + + stem = segment.segment_name if not segment.is_subsegment else segment.segment_name_orig + if z_max is not None and z_min is not None: + if z_min > z_max: + raise ValueError(f"z_min ({z_min}) must be less than z_max ({z_max}).") + stem = "_".join((stem, f"{z_min}-{z_max}")) + return zarr_dir / segment.scroll_id / f"{stem}.zarr" + + +def save_segment_as_zarr_array( + segment: ScrollSegment, + output_zarr_path: Path | None, + z_chunk_size: int = 4, + y_chunk_size: int = 512, + x_chunk_size: int = 512, + load_in_memory: bool = False, + z_min: int | None = None, + z_max: int | None = None, + zarr_dir: Path = ZARR_ARRAY_DIR, +) -> zarr.Array: + if output_zarr_path is None: + output_zarr_path = segment_to_zarr_path( + segment, z_min=z_min, z_max=z_max, zarr_dir=zarr_dir + ) + + if load_in_memory: + img_stack = segment.load_volume(z_start=z_min, z_end=z_max, preprocess=False) + else: + img_stack = segment.load_volume_as_memmap(z_start=z_min, z_end=z_max) + + zarr_array = zarr.open( + str(output_zarr_path), + mode="w", + shape=img_stack.shape, + dtype=img_stack.dtype, + chunks=(z_chunk_size, y_chunk_size, x_chunk_size), + ) + zarr_array[:] = img_stack + + if not load_in_memory: + delete_memmap(img_stack) + + return zarr_array + + +def load_segment_as_zarr_array( + zarr_path: Path, chunks: tuple[int, int, int] | int | bool = True +) -> zarr.Array: + return zarr.open(str(zarr_path), mode="r", chunks=chunks) + + +def create_or_load_img_stack_zarr( + segment: ScrollSegment, + zarr_dir: Path = ZARR_ARRAY_DIR, + chunks_load: tuple[int, int, int] | int | bool = True, + z_chunk_save_size: int = 4, + y_chunk_save_size: int = 512, + x_chunk_save_size: int = 512, +) -> zarr.Array: + zarr_path = segment_to_zarr_path(segment, zarr_dir=zarr_dir) + if zarr_path.exists(): + logging.info(f"Found existing zarr for segment {segment.segment_name}.") + return load_segment_as_zarr_array(zarr_path, chunks=chunks_load) + else: + logging.info( + f"Did not find existing zarr for segment {segment.segment_name}. A new zarr array will be created." + ) + return save_segment_as_zarr_array( + segment, + output_zarr_path=zarr_path, + z_chunk_size=z_chunk_save_size, + y_chunk_size=y_chunk_save_size, + x_chunk_size=x_chunk_save_size, + ) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/inference/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/inference/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/inference/prediction.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/inference/prediction.py new file mode 100644 index 0000000..51679cb --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/inference/prediction.py @@ -0,0 +1,46 @@ +import numpy as np + +from vesuvius_challenge_rnd.fragment_ink_detection import PatchLitModel +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.patch_aggregation import ( + parse_predictions_without_labels, + patches_to_y_proba, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.inference.prediction import ( + get_predictions, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.data.eval_scroll_patch_data_module import ( + EvalScrollPatchDataModule, +) + + +def predict_with_model_on_scroll( + lit_model: PatchLitModel, + data_module: EvalScrollPatchDataModule, + patch_surface_shape: tuple[int, int], + prog_bar: bool = True, +) -> np.ndarray: + """Predict with a patch model on a given scroll. + + This function retrieves predictions from the model and aggregates them into a smooth probability map + for the entire scroll segment. + + Args: + lit_model (PatchLitModel): Trained model to make predictions. + data_module (EvalScrollPatchDataModule): Data module containing evaluation data for the scroll. + patch_surface_shape (tuple[int, int]): Shape of the patch surface. + prog_bar (bool, optional): Whether to display a progress bar. Defaults to True. + + Returns: + np.ndarray: The smoothed probability map for the scroll. + """ + predictions = get_predictions(lit_model, data_module, prog_bar) + + y_proba_patches, patch_positions = parse_predictions_without_labels(predictions) + + # Aggregate patch predictions. + mask = data_module.data_predict.masks[0] + y_proba_smoothed = patches_to_y_proba( + y_proba_patches, patch_positions, mask, patch_surface_shape + ) + + return y_proba_smoothed diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/__init__.py new file mode 100644 index 0000000..a7ca831 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/__init__.py @@ -0,0 +1,13 @@ +from .cnn3d_manet_lit_model import CNN3dMANetPLModel +from .cnn3d_segformer_lit_model import CNN3DSegformerPLModel +from .cnn3d_unetplusplus_lit_model import CNN3dUnetPlusPlusPLModel +from .cnn3dto2d_crackformer_lit_model import Cnn3dto2dCrackformerLitModel +from .hr_seg_net_lit_model import HrSegNetLitModel +from .i3d_lit_model import RegressionPLModel +from .i3d_mean_teacher_lit_model import I3DMeanTeacherPLModel +from .lit_domain_adversarial_segmenter import LitDomainAdversarialSegmenter +from .mednextv1_3d_to_2d_lit_model import MedNextV13dto2dPLModel +from .mednextv1_segformer_lit_model import MedNextV1SegformerPLModel +from .swin_unetr_segformer_lit_model import SwinUNETRSegformerPLModel +from .unet3d_segformer_lit_model import UNet3dSegformerPLModel +from .unetr_segformer_lit_model import UNETRSegformerPLModel diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_manet_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_manet_lit_model.py new file mode 100644 index 0000000..1d07d75 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_manet_lit_model.py @@ -0,0 +1,224 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import ( + CNN3DMANet, + CNN3DUnetPlusPlusEfficientNet, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class CNN3dMANetPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + in_channels: int = 1, + ignore_idx: int = -100, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = CNN3DMANet( + in_channels=in_channels, + n_classes=1, + ) + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_segformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_segformer_lit_model.py new file mode 100644 index 0000000..f239982 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_segformer_lit_model.py @@ -0,0 +1,205 @@ +from typing import Literal + +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.cnn3d_segformer import ( + create_cnn3d_segformer_model, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class CNN3DSegformerPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: torch.Tensor | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + model_size: Literal["b3", "b4", "b5"] = "b3", + ): + super().__init__() + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.save_hyperparameters() + + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary") + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = create_cnn3d_segformer_model(model_size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + self.example_input_array = torch.ones(4, 1, 32, self.size, self.size) + self.validation_step_outputs = [] + + def forward(self, x): + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_unetplusplus_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_unetplusplus_lit_model.py new file mode 100644 index 0000000..8ec0020 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3d_unetplusplus_lit_model.py @@ -0,0 +1,225 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import ( + CNN3DUnetPlusPlusEfficientNet, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class CNN3dUnetPlusPlusPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + efficient_net_size: int = 5, + in_channels: int = 1, + ignore_idx: int = -100, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = CNN3DUnetPlusPlusEfficientNet( + in_channels=in_channels, + n_classes=1, + efficient_net_size=efficient_net_size, + ) + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3dto2d_crackformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3dto2d_crackformer_lit_model.py new file mode 100644 index 0000000..06595d9 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/cnn3dto2d_crackformer_lit_model.py @@ -0,0 +1,221 @@ +from typing import Literal + +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.cnn3dto2d_crackformer import ( + CNN3Dto2dCrackformer, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class Cnn3dto2dCrackformerLitModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + with_norm: bool = False, + smooth_factor: float = 0.1, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + z_extent: int = 30, + se_type_str: str | None = None, + in_channels: int = 1, + depth_pool_fn: Literal["mean", "max", "attention"] = "attention", + ): + super().__init__() + self.pred_shape = pred_shape + self.size = size + self.with_norm = with_norm + self.lr = lr + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary") + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, + pos_weight=bce_pos_weight, + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.example_input_array = torch.ones(4, in_channels, z_extent, self.size, self.size) + + self.model = CNN3Dto2dCrackformer( + z_extent, + self.size, + self.size, + se_type_str=se_type_str, + depth_pool_fn=depth_pool_fn, + in_channels=in_channels, + out_channels=1, + ) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x): + x = self.model(x) + return x + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/hr_seg_net_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/hr_seg_net_lit_model.py new file mode 100644 index 0000000..ed9f4b4 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/hr_seg_net_lit_model.py @@ -0,0 +1,227 @@ +from typing import Literal + +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPoolingBlock, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import HrSegNetB16Model +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class HrSegNetLitModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + with_norm: bool = False, + smooth_factor: float = 0.1, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + z_extent: int = 30, + se_type_str: str | None = None, + in_channels: int = 1, + base: int = 16, + depth_pool_fn: Literal["mean", "max", "attention"] = "attention", + ): + super().__init__() + self.pred_shape = pred_shape + self.size = size + self.with_norm = with_norm + self.lr = lr + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary") + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, + pos_weight=bce_pos_weight, + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.example_input_array = torch.ones(4, in_channels, z_extent, self.size, self.size) + + self.encoder_3d = HrSegNetB16Model(in_channels=in_channels, num_classes=1, base=base) + self.pooler = DepthPoolingBlock( + input_channels=1, + se_type=se_type_str, + reduction_ratio=2, + depth_dropout=0, + pool_fn=depth_pool_fn, + dim=2, + depth=z_extent, + height=self.size, + width=self.size, + ) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x): + x = self.encoder_3d(x)[0] + x = self.pooler(x) + return x + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_lit_model.py new file mode 100644 index 0000000..ae541a6 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_lit_model.py @@ -0,0 +1,322 @@ +from typing import Literal + +import logging +import os + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn as nn +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPooling, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.squeeze_and_excitation_3d import ( + SELayer3D, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.i3dall import InceptionI3d +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class Decoder(nn.Module): + def __init__(self, encoder_dims, upscale): + super().__init__() + self.convs = nn.ModuleList( + [ + nn.Sequential( + nn.Conv2d( + encoder_dims[i] + encoder_dims[i - 1], + encoder_dims[i - 1], + 3, + 1, + 1, + bias=False, + ), + nn.BatchNorm2d(encoder_dims[i - 1]), + nn.ReLU(inplace=True), + ) + for i in range(1, len(encoder_dims)) + ] + ) + + self.logit = nn.Conv2d(encoder_dims[0], 1, 1, 1, 0) + self.up = nn.Upsample(scale_factor=upscale, mode="bilinear") + + def forward(self, feature_maps): + for i in range(len(feature_maps) - 1, 0, -1): + f_up = F.interpolate(feature_maps[i], scale_factor=2, mode="bilinear") + f = torch.cat([feature_maps[i - 1], f_up], dim=1) + f_down = self.convs[i - 1](f) + feature_maps[i - 1] = f_down + + x = self.logit(feature_maps[0]) + mask = self.up(x) + return mask + + +class RegressionPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + with_norm: bool = False, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + z_extent: int = 30, + se_type_str: str | None = None, + in_channels: int = 1, + depth_pool_fn: Literal["mean", "max", "attention"] = "mean", + ignore_idx: int = -100, + ckpt_path: str | None = None, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.with_norm = with_norm + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.backbone = InceptionI3d(in_channels=in_channels, num_classes=512) + + self.example_input_array = torch.ones(4, in_channels, z_extent, self.size, self.size) + encoder_output_shapes = np.array( + [x.shape[1:] for x in self.backbone(self.example_input_array)] + ) + encoder_dims = encoder_output_shapes[:, 0].tolist() + encoder_depths = encoder_output_shapes[:, 1].tolist() + encoder_heights = encoder_output_shapes[:, 2].tolist() + encoder_widths = encoder_output_shapes[:, 3].tolist() + self.depth_pooler = DepthPooling( + len(encoder_dims), + encoder_dims, + slice_dim=2, + se_type=SELayer3D[se_type_str] if se_type_str is not None else None, + pool_fn=depth_pool_fn, + depths=encoder_depths, + heights=encoder_heights, + widths=encoder_widths, + ) + + self.decoder = Decoder( + encoder_dims=encoder_dims, + upscale=1, + ) + + if ckpt_path is not None: + logging.info("Loading model parameters from checkpoint.") + self.load_weights(ckpt_path) + + if self.hparams.with_norm: + self.normalization = nn.BatchNorm3d(num_features=1) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x): + if x.ndim == 4: + x = x[:, None] + if self.hparams.with_norm: + x = self.normalization(x) + feat_maps = self.backbone(x) + feat_maps_pooled = self.depth_pooler(feat_maps) + pred_mask = self.decoder(feat_maps_pooled) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + F.interpolate(y_preds[i].unsqueeze(0).float(), scale_factor=4, mode="bilinear") + .squeeze(0) + .squeeze(0) + .numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] + + def load_weights(self, checkpoint_path: str | os.PathLike) -> None: + # Load the entire checkpoint + checkpoint = torch.load(checkpoint_path) + + # Extract the state dictionary corresponding to the entire model + state_dict = checkpoint["state_dict"] + + # Filter out and load weights for the backbone, decoder, and depth pooler + for component in ["backbone", "decoder", "depth_pooler"]: + component_dict = { + k.replace(f"{component}.", ""): v + for k, v in state_dict.items() + if k.startswith(f"{component}.") + } + getattr(self, component).load_state_dict(component_dict) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_mean_teacher_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_mean_teacher_lit_model.py new file mode 100644 index 0000000..9975cc1 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_mean_teacher_lit_model.py @@ -0,0 +1,292 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn as nn +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.i3dall import InceptionI3d +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class Decoder(nn.Module): + def __init__(self, encoder_dims, upscale): + super().__init__() + self.convs = nn.ModuleList( + [ + nn.Sequential( + nn.Conv2d( + encoder_dims[i] + encoder_dims[i - 1], + encoder_dims[i - 1], + 3, + 1, + 1, + bias=False, + ), + nn.BatchNorm2d(encoder_dims[i - 1]), + nn.ReLU(inplace=True), + ) + for i in range(1, len(encoder_dims)) + ] + ) + + self.logit = nn.Conv2d(encoder_dims[0], 1, 1, 1, 0) + self.up = nn.Upsample(scale_factor=upscale, mode="bilinear") + + def forward(self, feature_maps): + for i in range(len(feature_maps) - 1, 0, -1): + f_up = F.interpolate(feature_maps[i], scale_factor=2, mode="bilinear") + f = torch.cat([feature_maps[i - 1], f_up], dim=1) + f_down = self.convs[i - 1](f) + feature_maps[i - 1] = f_down + + x = self.logit(feature_maps[0]) + mask = self.up(x) + return mask + + +class Encoder3dDecoder2dModel(LightningModule): + def __init__(self): + super().__init__() + self.encoder = InceptionI3d(in_channels=1, num_classes=512) + + # Ideally, refactor the Decoder to adapt to input dimensions dynamically. + # If that's not feasible, use a dummy tensor for dimension calculation. + dummy_input = torch.rand(1, 1, 20, 256, 256) + encoder_dims = [x.size(1) for x in self.encoder(dummy_input)] + self.decoder = Decoder(encoder_dims=encoder_dims, upscale=1) + + def forward(self, x): + feat_maps = self.encoder(x) + feat_maps_pooled = self.pool_feat_maps(feat_maps) + pred_mask = self.decoder(feat_maps_pooled) + return pred_mask + + @staticmethod + def pool_feat_maps(feat_maps: list[torch.Tensor]) -> list[torch.Tensor]: + return [torch.mean(f, dim=2) for f in feat_maps] + + +class I3DMeanTeacherPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + with_norm: bool = False, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: torch.Tensor | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + ema_decay: float = 0.99, + ): + super().__init__() + self.pred_shape = pred_shape + self.size = size + self.with_norm = with_norm + self.lr = lr + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary") + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight + ) + bce_weight = 1 - dice_weight + self.supervised_loss_fn = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = Encoder3dDecoder2dModel() + self.ema_model = Encoder3dDecoder2dModel() + # Detach all parameters in the EMA model + for param in self.ema_model.parameters(): + param.detach_() + + if self.hparams.with_norm: + self.normalization = nn.BatchNorm3d(num_features=1) + + self.ema_decay = ema_decay + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + def forward(self, x): + if x.ndim == 4: + x = x[:, None] + if self.hparams.with_norm: + x = self.normalization(x) + + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + # Prepare batch. + x_labeled, y = batch["labeled"] + x_unlabeled = batch["unlabeled"] + + # Calculate supervised loss. + logits_labeled = self(x_labeled) + loss_supervised = self.supervised_loss_fn(logits_labeled, y) + if torch.isnan(loss_supervised): + logging.warning("Loss nan encountered") + self.log( + "train/sup_total_loss", + loss_supervised.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + ) + + # Calculate consistency loss. + noise = torch.clamp(torch.randn_like(x_unlabeled) * 0.1, -0.2, 0.2) + # This noise could also be from dropout, stochastic depth, or more data augmentation + ema_inputs = x_unlabeled + noise + + outputs_soft = torch.softmax(logits_labeled, dim=1) + with torch.no_grad(): + ema_output = torch.softmax(self.ema_model(ema_inputs), dim=1) + + consistency_weight = get_current_consistency_weight(self.global_step // 150) + if self.global_step < 1000: + consistency_loss = 0.0 + else: + consistency_loss = torch.mean((outputs_soft - ema_output) ** 2) + loss = loss_supervised + consistency_weight * consistency_loss + + update_ema_variables(self.model, self.ema_model, self.ema_decay, self.global_step) + + # Log other metrics. + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits_labeled, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.supervised_loss_fn(logits, y) + y_preds = torch.sigmoid(logits).to("cpu") + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + F.interpolate(y_preds[i].unsqueeze(0).float(), scale_factor=4, mode="bilinear") + .squeeze(0) + .squeeze(0) + .numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones((self.hparams.size, self.hparams.size)) + + self.log("val/total_loss", loss.item(), on_step=True, on_epoch=True, prog_bar=True) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image(key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"]) + + # reset mask + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] + + +def sigmoid_rampup(current: float, rampup_length: float) -> float: + """Exponential rampup from https://arxiv.org/abs/1610.02242""" + if rampup_length == 0: + return 1.0 + else: + current = np.clip(current, 0.0, rampup_length) + phase = 1.0 - current / rampup_length + return float(np.exp(-5.0 * phase * phase)) + + +def get_current_consistency_weight( + epoch: int, consistency: float = 0.1, consistency_rampup: float = 200.0 +): + # Consistency ramp-up from https://arxiv.org/abs/1610.02242 + return consistency * sigmoid_rampup(epoch, consistency_rampup) + + +def update_ema_variables( + model: nn.Module, ema_model: nn.Module, alpha: float, global_step: int +) -> None: + # Use the true average until the exponential average is more correct + alpha = min(1 - 1 / (global_step + 1), alpha) + for ema_param, param in zip(ema_model.parameters(), model.parameters()): + ema_param.data.mul_(alpha).add_(param.data, alpha=1 - alpha) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_semi_supervised_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_semi_supervised_lit_model.py new file mode 100644 index 0000000..eb95d75 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/i3d_semi_supervised_lit_model.py @@ -0,0 +1,292 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn as nn +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.i3dall import InceptionI3d +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class Decoder(nn.Module): + def __init__(self, encoder_dims, upscale): + super().__init__() + self.convs = nn.ModuleList( + [ + nn.Sequential( + nn.Conv2d( + encoder_dims[i] + encoder_dims[i - 1], + encoder_dims[i - 1], + 3, + 1, + 1, + bias=False, + ), + nn.BatchNorm2d(encoder_dims[i - 1]), + nn.ReLU(inplace=True), + ) + for i in range(1, len(encoder_dims)) + ] + ) + + self.logit = nn.Conv2d(encoder_dims[0], 1, 1, 1, 0) + self.up = nn.Upsample(scale_factor=upscale, mode="bilinear") + + def forward(self, feature_maps): + for i in range(len(feature_maps) - 1, 0, -1): + f_up = F.interpolate(feature_maps[i], scale_factor=2, mode="bilinear") + f = torch.cat([feature_maps[i - 1], f_up], dim=1) + f_down = self.convs[i - 1](f) + feature_maps[i - 1] = f_down + + x = self.logit(feature_maps[0]) + mask = self.up(x) + return mask + + +class Encoder3dDecoder2dModel(LightningModule): + def __init__(self): + super().__init__() + self.encoder = InceptionI3d(in_channels=1, num_classes=512) + + # Ideally, refactor the Decoder to adapt to input dimensions dynamically. + # If that's not feasible, use a dummy tensor for dimension calculation. + dummy_input = torch.rand(1, 1, 20, 256, 256) + encoder_dims = [x.size(1) for x in self.encoder(dummy_input)] + self.decoder = Decoder(encoder_dims=encoder_dims, upscale=1) + + def forward(self, x): + feat_maps = self.encoder(x) + feat_maps_pooled = self.pool_feat_maps(feat_maps) + pred_mask = self.decoder(feat_maps_pooled) + return pred_mask + + @staticmethod + def pool_feat_maps(feat_maps: list[torch.Tensor]) -> list[torch.Tensor]: + return [torch.mean(f, dim=2) for f in feat_maps] + + +class I3DSemiSupervisedPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + with_norm: bool = False, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: torch.Tensor | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + ema_decay: float = 0.99, + ): + super().__init__() + self.pred_shape = pred_shape + self.size = size + self.with_norm = with_norm + self.lr = lr + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary") + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight + ) + bce_weight = 1 - dice_weight + self.supervised_loss_fn = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = Encoder3dDecoder2dModel() + self.ema_model = Encoder3dDecoder2dModel() + # Detach all parameters in the EMA model + for param in self.ema_model.parameters(): + param.detach_() + + if self.hparams.with_norm: + self.normalization = nn.BatchNorm3d(num_features=1) + + self.ema_decay = ema_decay + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + def forward(self, x): + if x.ndim == 4: + x = x[:, None] + if self.hparams.with_norm: + x = self.normalization(x) + + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + # Prepare batch. + x_labeled, y = batch["labeled"] + x_unlabeled = batch["unlabeled"] + + # Calculate supervised loss. + logits_labeled = self(x_labeled) + loss_supervised = self.supervised_loss_fn(logits_labeled, y) + if torch.isnan(loss_supervised): + logging.warning("Loss nan encountered") + self.log( + "train/sup_total_loss", + loss_supervised.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + ) + + # Calculate consistency loss. + noise = torch.clamp(torch.randn_like(x_unlabeled) * 0.1, -0.2, 0.2) + # This noise could also be from dropout, stochastic depth, or more data augmentation + ema_inputs = x_unlabeled + noise + + outputs_soft = torch.softmax(logits_labeled, dim=1) + with torch.no_grad(): + ema_output = torch.softmax(self.ema_model(ema_inputs), dim=1) + + consistency_weight = get_current_consistency_weight(self.global_step // 150) + if self.global_step < 1000: + consistency_loss = 0.0 + else: + consistency_loss = torch.mean((outputs_soft - ema_output) ** 2) + loss = loss_supervised + consistency_weight * consistency_loss + + update_ema_variables(self.model, self.ema_model, self.ema_decay, self.global_step) + + # Log other metrics. + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits_labeled, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.supervised_loss_fn(logits, y) + y_preds = torch.sigmoid(logits).to("cpu") + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + F.interpolate(y_preds[i].unsqueeze(0).float(), scale_factor=4, mode="bilinear") + .squeeze(0) + .squeeze(0) + .numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones((self.hparams.size, self.hparams.size)) + + self.log("val/total_loss", loss.item(), on_step=True, on_epoch=True, prog_bar=True) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image(key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"]) + + # reset mask + self.mask_pred = np.zeros(self.hparams.pred_shape) + self.mask_count = np.zeros(self.hparams.pred_shape) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] + + +def sigmoid_rampup(current: float, rampup_length: float) -> float: + """Exponential rampup from https://arxiv.org/abs/1610.02242""" + if rampup_length == 0: + return 1.0 + else: + current = np.clip(current, 0.0, rampup_length) + phase = 1.0 - current / rampup_length + return float(np.exp(-5.0 * phase * phase)) + + +def get_current_consistency_weight( + epoch: int, consistency: float = 0.1, consistency_rampup: float = 200.0 +): + # Consistency ramp-up from https://arxiv.org/abs/1610.02242 + return consistency * sigmoid_rampup(epoch, consistency_rampup) + + +def update_ema_variables( + model: nn.Module, ema_model: nn.Module, alpha: float, global_step: int +) -> None: + # Use the true average until the exponential average is more correct + alpha = min(1 - 1 / (global_step + 1), alpha) + for ema_param, param in zip(ema_model.parameters(), model.parameters()): + ema_param.data.mul_(alpha).add_(param.data, alpha=1 - alpha) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/lit_domain_adversarial_segmenter.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/lit_domain_adversarial_segmenter.py new file mode 100644 index 0000000..606c044 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/lit_domain_adversarial_segmenter.py @@ -0,0 +1,242 @@ +from typing import Literal + +import math + +import pytorch_lightning as pl +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, + F1Score, +) + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models import UNet3Dto2D +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.unet_3d_to_2d import ( + compute_unet_3d_to_2d_encoder_chwd, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import GradientReversal +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mlp import FlattenAndMLP + + +class LitDomainAdversarialSegmenter(pl.LightningModule): + def __init__( + self, + patch_depth: int, + patch_height: int, + patch_width: int, + num_classes: int = 1, + in_channels: int = 1, + lr: float = 0.0001, + thresh: float = 0.5, + task_loss_fn: nn.Module | None = None, + f_maps: int = 64, + layer_order: str = "gcr", + num_groups: int = 8, + num_levels: int = 4, + conv_padding: int | tuple[int, ...] = 1, + se_type_str: str | None = "PE", + reduction_ratio: int = 2, + depth_dropout: float = 0.0, + pool_fn: Literal["mean", "max"] = "mean", + gamma: float = 10.0, # Adaptation factor + dc_input_encoder_depth: int = 4, + ): + super().__init__() + self.save_hyperparameters() + if task_loss_fn is None: + task_loss_fn = nn.BCEWithLogitsLoss() + + self.segmenter = UNet3Dto2D( + in_channels=in_channels, + out_channels=num_classes, + se_type_str=se_type_str, + depth_dropout=depth_dropout, + pool_fn=pool_fn, + f_maps=f_maps, + num_levels=num_levels, + reduction_ratio=reduction_ratio, + conv_padding=conv_padding, + num_groups=num_groups, + layer_order=layer_order, + output_features=True, + ) + + if dc_input_encoder_depth > num_levels: + raise ValueError( + f"Domain classifier input encoder depth ({dc_input_encoder_depth}) cannot be greater than U-Net depth (num_levels={num_levels})." + ) + + self.dc_input_encoder_depth = dc_input_encoder_depth + c, h, w, d = compute_unet_3d_to_2d_encoder_chwd( + patch_depth, + patch_height, + patch_width, + f_maps, + encoder_level=self.dc_input_encoder_depth, + in_channels=in_channels, + ) + self.domain_classifier = FlattenAndMLP( + input_dim=int(c * h * w * d), + output_dim=num_classes, + ) + self.loss_fn_sup = task_loss_fn + self.loss_fn_dc = nn.BCEWithLogitsLoss() + self.grl = GradientReversal(alpha=0.0) + + self.lr = lr + shared_fragment_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=thresh), + BinaryFBetaScore(beta=0.5, threshold=thresh), + ] + ) + self.train_fragment_metrics = shared_fragment_metrics.clone(prefix="train/") + self.val_fragment_metrics = shared_fragment_metrics.clone(prefix="val/") + self.val_fragment_metrics.add_metrics([BinaryAUROC(), BinaryAveragePrecision()]) + + # Domain classifier metrics. + shared_dc_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=thresh), + F1Score(task="binary", threshold=thresh, num_classes=2), + BinaryAUROC(), + BinaryAveragePrecision(), + ] + ) + self.train_dc_metrics = shared_dc_metrics.clone(prefix="train/domain_classifier/") + + # B x C x D x H x W + self.example_input_array = torch.Tensor( + 5, in_channels, patch_depth, patch_height, patch_width + ) + self.gamma = gamma + + def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: + if x.ndim == 4: + x = x.unsqueeze(1) # Add dummy channel dimension because it's grayscale. + output = self.segmenter(x) + logits = output.logits.squeeze(1) + features = output.encoder_features[self.dc_input_encoder_depth] + return logits, features + + def adversarial_classifier(self, x: torch.Tensor) -> torch.Tensor: + x = self.grl(x) + logits = self.domain_classifier(x) + return logits + + def training_step(self, batch, batch_idx: int): + # Compute new alpha. + num_batches = self.trainer.num_training_batches + current_steps = self.current_epoch * num_batches + max_steps = self.trainer.max_epochs * num_batches + p = (batch_idx + current_steps) / max_steps + alpha = calculate_dann_alpha(self.gamma, p) + self.grl.update_alpha(alpha) + self.log("alpha", alpha) + + # Prepare batch. + source_patch, source_labels, _ = batch["source"] + target_patch, _ = batch["target"] + patches = torch.cat((source_patch, target_patch), dim=0) + source_batch_size = source_patch.shape[0] + source_domain_labels = torch.zeros(source_batch_size, dtype=torch.float, device=self.device) + target_domain_labels = torch.ones( + target_patch.shape[0], dtype=torch.float, device=self.device + ) + domain_labels = torch.cat((source_domain_labels, target_domain_labels)) + source_labels = source_labels.float() + + # Label predictor. + logits, features = self(patches) + source_logits = logits[:source_batch_size] + loss_sup = self.loss_fn_sup(source_logits, source_labels) + self.log("train/task_loss", loss_sup, prog_bar=True) + train_fragment_metrics_output = self.train_fragment_metrics(source_logits, source_labels) + self.log_dict(train_fragment_metrics_output) + + # Domain classifier. + domain_logits = self.adversarial_classifier(features).squeeze() + loss_dc = self.loss_fn_dc(domain_logits, domain_labels) + self.log(f"{self.train_dc_metrics.prefix}loss", loss_dc) + train_dc_metrics_output = self.train_dc_metrics(domain_logits, domain_labels.int()) + self.log_dict(train_dc_metrics_output) + + loss = loss_sup + loss_dc + self.log("train/loss", loss, prog_bar=True) + + return loss + + def validation_step(self, batch, batch_idx, dataloader_idx): + if dataloader_idx == 0: + # For now, only validate on source/fragments. + source_patch, source_labels, source_patch_pos = batch + source_labels = source_labels.float() + source_logits, _ = self(source_patch) + + task_loss = self.loss_fn_sup(source_logits, source_labels) + self.log("val/loss", task_loss) + + source_labels = source_labels.int() + self.val_fragment_metrics.update(source_logits, source_labels) + return { + "logits": source_logits, + "y": source_labels, + "loss": task_loss, + "patch_pos": source_patch_pos, + } + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError("Expected number of items in a batch to be 2.") + + logits, _ = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def on_validation_epoch_end(self) -> None: + """Method called at the end of a validation epoch.""" + val_metrics_output = self.val_fragment_metrics.compute() + self.log_dict(val_metrics_output) + self.val_fragment_metrics.reset() + + def configure_optimizers(self): + optimizer = torch.optim.AdamW(self.parameters(), lr=self.lr) + return optimizer + + +def calculate_dann_alpha(gamma: float, p: float) -> float: + """Compute the domain adaptation parameter alpha (lambda) for Domain-Adversarial Neural Networks (DANN). + + This function calculates alpha using a scaled and shifted version of the logistic (sigmoid) function. + + Args: + gamma (float): Scaling factor that modulates the sigmoid function. + p (float): Progress variable, typically representing the training progress. + + Returns: + float: The computed value of alpha, in the range of [-1, 1]. + + Example: + >>> calculate_dann_alpha(10, 0.1) + 0.9757230564143254 + """ + return 2.0 / (1.0 + math.exp(-gamma * p)) - 1 diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_3d_to_2d_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_3d_to_2d_lit_model.py new file mode 100644 index 0000000..cd3f0b3 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_3d_to_2d_lit_model.py @@ -0,0 +1,230 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.create_mednext_v1_3d_to_2d import ( + create_mednext_v1_3d_to_2d, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class MedNextV13dto2dPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + mednextv1_model_id: str = "S", + kernel_size: int = 3, + deep_supervision: bool = False, + dropout: float = 0.1, + in_channels: int = 1, + ignore_idx: int = -100, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = create_mednext_v1_3d_to_2d( + num_input_channels=in_channels, + num_classes=1, + model_id=mednextv1_model_id.upper(), + kernel_size=kernel_size, + deep_supervision=deep_supervision, + ) + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_segformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_segformer_lit_model.py new file mode 100644 index 0000000..222aa42 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/mednextv1_segformer_lit_model.py @@ -0,0 +1,232 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.mednextv1_segformer import ( + MedNextV1Segformer, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class MedNextV1SegformerPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + mednextv1_model_id: str = "S", + segformer_model_size: int = 1, + kernel_size: int = 3, + deep_supervision: bool = False, + dropout: float = 0.1, + in_channels: int = 1, + ignore_idx: int = -100, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = MedNextV1Segformer( + in_channels=in_channels, + mednextv1_model_id=mednextv1_model_id.upper(), + segformer_model_size=segformer_model_size, + kernel_size=kernel_size, + deep_supervision=deep_supervision, + dropout=dropout, + ) + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/swin_unetr_segformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/swin_unetr_segformer_lit_model.py new file mode 100644 index 0000000..a974804 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/swin_unetr_segformer_lit_model.py @@ -0,0 +1,225 @@ +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import SwinUNETRSegformer +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class SwinUNETRSegformerPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 32, + smooth_factor: float = 0.25, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + segformer_model_size: int = 1, + dropout: float = 0.1, + in_channels: int = 1, + ignore_idx: int = -100, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = SwinUNETRSegformer( + in_channels=in_channels, + img_size=(z_extent, self.size, self.size), + segformer_model_size=segformer_model_size, + dropout=dropout, + ) + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + logits = self(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unet3d_segformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unet3d_segformer_lit_model.py new file mode 100644 index 0000000..694fa6c --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unet3d_segformer_lit_model.py @@ -0,0 +1,270 @@ +from typing import Literal + +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from timm.utils import ModelEmaV2 +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import UNet3DSegformer +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class UNet3dSegformerPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.1, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + unet_feature_size: int = 16, + unet_out_channels: int = 32, + unet_module_type: str = "resnet_se", + se_type_str: str | None = None, + depth_pool_fn: Literal["mean", "max", "attention"] = "max", + segformer_model_size: int = 1, + dropout: float = 0.1, + in_channels: int = 1, + ignore_idx: int = -100, + ckpt_path: str | None = None, + ema: bool = False, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.ema = ema + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = UNet3DSegformer( + in_channels=in_channels, + img_size=(z_extent, self.size, self.size), + unet_feature_size=unet_feature_size, + unet_out_channels=unet_out_channels, + unet_module_type=unet_module_type, + segformer_model_size=segformer_model_size, + se_type_str=se_type_str, + depth_pool_fn=depth_pool_fn, + dropout=dropout, + ) + + if self.ema: + self.model_ema = ModelEmaV2(self.model, decay=0.99) + else: + self.model_ema = None + + if ckpt_path is not None: + logging.info("Loading model parameters from checkpoint.") + self.load_weights(ckpt_path) + + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + if self.model_ema is not None: + self.model_ema.update(self.model) + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + if self.model_ema is None: + logits = self(x) + else: + logits = self.model_ema.module(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] + + def load_weights(self, checkpoint_path): + # Load the entire checkpoint + checkpoint = torch.load(checkpoint_path) + + # Extract the state dictionary for the model + model_dict = { + k.replace("model.", ""): v + for k, v in checkpoint["state_dict"].items() + if k.startswith("model.") + } + + # Load the weights into the model + self.model.load_state_dict(model_dict) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unetr_segformer_lit_model.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unetr_segformer_lit_model.py new file mode 100644 index 0000000..cf07a18 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/lit_models/unetr_segformer_lit_model.py @@ -0,0 +1,266 @@ +from typing import Literal + +import logging + +import numpy as np +import segmentation_models_pytorch as smp +import torch +import torch.nn.functional as F +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers import WandbLogger +from timm.utils import ModelEmaV2 +from torch.optim import AdamW +from torchmetrics import MetricCollection +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryAveragePrecision, + BinaryFBetaScore, +) + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import UNETRSegformer +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.schedulers import ( + GradualWarmupSchedulerV2, +) + + +class UNETRSegformerPLModel(LightningModule): + def __init__( + self, + pred_shape: tuple[int, int], + size: int = 64, + z_extent: int = 16, + smooth_factor: float = 0.1, + dice_weight: float = 0.5, + lr: float = 2e-5, + bce_pos_weight: float | list[float] | None = None, + metric_thresh: float = 0.5, + metric_gt_ink_thresh: float = 0.05, + unetr_feature_size: int = 16, + se_type_str: str | None = None, + depth_pool_fn: Literal["mean", "max", "attention"] = "max", + segformer_model_size: int = 1, + dropout: float = 0.1, + in_channels: int = 1, + ignore_idx: int = -100, + ckpt_path: str | None = None, + ema: bool = False, + ): + super().__init__() + # Parse bce_pos_weight. + if bce_pos_weight is not None: + if isinstance(bce_pos_weight, float): + bce_pos_weight = [bce_pos_weight] + bce_pos_weight = torch.tensor(bce_pos_weight) + + self.pred_shape = pred_shape + self.size = size + self.lr = lr + self.ignore_idx = ignore_idx + self.ema = ema + self.save_hyperparameters() + self.mask_pred = np.zeros(self.hparams.pred_shape, dtype=np.float32) + self.mask_count = np.zeros(self.hparams.pred_shape, dtype=np.int32) + + self.loss_func1 = smp.losses.DiceLoss(mode="binary", ignore_index=self.ignore_idx) + self.loss_func2 = smp.losses.SoftBCEWithLogitsLoss( + smooth_factor=smooth_factor, pos_weight=bce_pos_weight, ignore_index=self.ignore_idx + ) + bce_weight = 1 - dice_weight + self.loss_func = lambda x, y: dice_weight * self.loss_func1( + x, y + ) + bce_weight * self.loss_func2(x, y) + + self.model = UNETRSegformer( + in_channels=in_channels, + img_size=(z_extent, self.size, self.size), + segformer_model_size=segformer_model_size, + unetr_feature_size=unetr_feature_size, + se_type_str=se_type_str, + depth_pool_fn=depth_pool_fn, + dropout=dropout, + ) + + if self.ema: + self.model_ema = ModelEmaV2(self.model, decay=0.99) + else: + self.model_ema = None + + if ckpt_path is not None: + logging.info("Loading model parameters from checkpoint.") + self.load_weights(ckpt_path) + + self.example_input_array = torch.ones(3, in_channels, z_extent, self.size, self.size) + + self.metric_gt_ink_thresh = metric_gt_ink_thresh + shared_metrics = MetricCollection( + [ + BinaryAccuracy(threshold=metric_thresh, ignore_index=self.ignore_idx), + BinaryFBetaScore(beta=0.5, threshold=metric_thresh, ignore_index=self.ignore_idx), + ] + ) + self.train_metrics = shared_metrics.clone(prefix="train/") + self.val_metrics = shared_metrics.clone(prefix="val/") + self.val_metrics.add_metrics( + [ + BinaryAUROC(ignore_index=self.ignore_idx), + BinaryAveragePrecision(ignore_index=self.ignore_idx), + ] + ) + + mean_fbeta_auprc = ( + self.val_metrics["BinaryFBetaScore"] + self.val_metrics["BinaryAveragePrecision"] + ) / 2 + self.val_metrics.add_metrics({"mean_fbeta_auprc": mean_fbeta_auprc}) + self.validation_step_outputs = [] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if x.ndim == 4: + x = x[:, None] + pred_mask = self.model(x) + return pred_mask + + def training_step(self, batch, batch_idx): + if self.model_ema is not None: + self.model_ema.update(self.model) + x, y = batch + logits = self(x) + loss = self.loss_func(logits, y) + if torch.isnan(loss): + logging.warning("Loss nan encountered") + self.log( + "train/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + train_metrics_output = self.train_metrics(logits, y_binarized) + self.log_dict(train_metrics_output) + + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + x, y, xyxys = batch + if self.model_ema is None: + logits = self(x) + else: + logits = self.model_ema.module(x) + loss = self.loss_func(logits, y) + + self.log( + "val/total_loss", + loss.item(), + on_step=True, + on_epoch=True, + prog_bar=True, + sync_dist=True, + ) + + y_binarized = (y > self.metric_gt_ink_thresh).int() + self.val_metrics.update(logits, y_binarized) + + self.validation_step_outputs.append((logits.detach(), xyxys.detach().cpu())) + + return { + "logits": logits, + "y": y, + "loss": loss, + "patch_pos": xyxys, + } + + def on_validation_epoch_end(self): + outputs = self.all_gather(self.validation_step_outputs) + logits = [t[0] for t in outputs] + xyxys = [t[1] for t in outputs] + self.validation_step_outputs.clear() # free memory + + if self.trainer.world_size == 1: + # Create a dummy "device" dimension. + logits = [t.unsqueeze(0) for t in logits] + xyxys = [t.unsqueeze(0) for t in xyxys] + + logits = torch.cat(logits, dim=1) # D x N x C x patch height x patch width (C=1) + xyxys = torch.cat(xyxys, dim=1) # D x N x 4 + + if self.trainer.is_global_zero: + logits = logits.view(-1, *logits.shape[-3:]) + xyxys = xyxys.view(-1, xyxys.shape[-1]) + y_preds = torch.sigmoid(logits).cpu() + for i, (x1, y1, x2, y2) in enumerate(xyxys): + self.mask_pred[y1:y2, x1:x2] += ( + y_preds[i].unsqueeze(0).float().squeeze(0).squeeze(0).numpy() + ) + self.mask_count[y1:y2, x1:x2] += np.ones( + (self.hparams.size, self.hparams.size), dtype=self.mask_count.dtype + ) + self.mask_pred = np.divide( + self.mask_pred, + self.mask_count, + out=np.zeros_like(self.mask_pred), + where=self.mask_count != 0, + ) + logger = self.logger + if isinstance(logger, WandbLogger): + logger.log_image( + key="masks", images=[np.clip(self.mask_pred, 0, 1)], caption=["probs"] + ) + + # Reset mask. + self.mask_pred = np.zeros_like(self.mask_pred) + self.mask_count = np.zeros_like(self.mask_count) + + val_metrics_output = self.val_metrics.compute() + self.log_dict(val_metrics_output) + self.val_metrics.reset() + + def predict_step( + self, batch, batch_idx: int, dataloader_idx: int = 0 + ) -> tuple[torch.Tensor, torch.Tensor]: + """Prediction step for the model. + + Args: + batch (tuple): Batch of data. + batch_idx (int): Batch index. + dataloader_idx (int, optional): Index of the data loader. Defaults to 0. + + Returns: + tuple: Probabilities and patch positions. + """ + if len(batch) == 2: + x, patch_pos = batch + else: + raise ValueError(f"Expected number of items in a batch to be 2. Found {len(batch)}.") + + logits = self(x) + y_proba = F.sigmoid(logits) + return y_proba, patch_pos + + def configure_optimizers(self): + optimizer = AdamW(filter(lambda p: p.requires_grad, self.parameters()), lr=self.lr) + + scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, self.trainer.max_epochs - 2, eta_min=1e-6 + ) + scheduler = GradualWarmupSchedulerV2( + optimizer, multiplier=1.0, total_epoch=1, after_scheduler=scheduler_cosine + ) + + return [optimizer], [scheduler] + + def load_weights(self, checkpoint_path): + # Load the entire checkpoint + checkpoint = torch.load(checkpoint_path) + + # Extract the state dictionary for the model + model_dict = { + k.replace("model.", ""): v + for k, v in checkpoint["state_dict"].items() + if k.startswith("model.") + } + + # Load the weights into the model + self.model.load_state_dict(model_dict) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/__init__.py new file mode 100644 index 0000000..8967195 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/__init__.py @@ -0,0 +1,12 @@ +from .cnn3d_manet import CNN3DMANet +from .cnn3d_segformer import CNN3DSegformer +from .cnn3d_unetplusplus import CNN3DUnetPlusPlusEfficientNet +from .cnn3dto2d_crackformer import CNN3Dto2dCrackformer +from .gradient_reversal import GradientReversal +from .hrsegnet import HrSegNetB16Model +from .i3dall import InceptionI3d +from .mednext import create_mednext_v1, create_mednext_v1_3d_to_2d +from .mlp import FlattenAndMLP +from .swin_unetr_segformer import SwinUNETRSegformer +from .unet3d_segformer import UNet3DSegformer +from .unetr_segformer import UNETRSegformer diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_manet.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_manet.py new file mode 100644 index 0000000..026fb8b --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_manet.py @@ -0,0 +1,31 @@ +import segmentation_models_pytorch as smp +import torch.nn as nn + + +class CNN3DMANet(nn.Module): + def __init__(self, in_channels: int = 1, n_classes: int = 1): + super().__init__() + cnn3d_out_channels = 32 + self.conv3d_1 = nn.Conv3d( + in_channels, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1) + ) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d( + 16, cnn3d_out_channels, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1) + ) + + self.xy_encoder_2d = smp.MAnet( + encoder_name="resnet34", + encoder_weights="imagenet", + in_channels=cnn3d_out_channels, + classes=n_classes, + ) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output).max(axis=2)[0] + output = self.xy_encoder_2d(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_segformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_segformer.py new file mode 100644 index 0000000..08b8075 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_segformer.py @@ -0,0 +1,99 @@ +from typing import Literal + +import torch.nn as nn +from transformers import SegformerForSemanticSegmentation + + +def create_cnn3d_segformer_model(model_size: Literal["b3", "b4", "b5"]) -> nn.Module: + if model_size == "b3": + return CNN3DSegformer() + elif model_size == "b4": + return CNN3DSegformerB4() + elif model_size == "b5": + return CNN3DSegformerB5() + raise ValueError(f"Unknown model size {model_size}. Expected b3, b4, or b5.") + + +class CNN3DSegformer(nn.Module): + def __init__(self): + super().__init__() + self.conv3d_1 = nn.Conv3d(1, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d(16, 32, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + + self.xy_encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + "nvidia/mit-b3", num_labels=1, ignore_mismatched_sizes=True, num_channels=32 + ) + self.upscaler1 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + self.upscaler2 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output).max(axis=2)[0] + output = self.xy_encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output + + +class CNN3DSegformerB4(nn.Module): + def __init__(self): + super().__init__() + self.conv3d_1 = nn.Conv3d(1, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d(16, 32, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + + self.xy_encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + "nvidia/mit-b4", + num_labels=1, + ignore_mismatched_sizes=True, + num_channels=32, + ) + self.upscaler1 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + self.upscaler2 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output).max(axis=2)[0] + output = self.xy_encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output + + +class CNN3DSegformerB5(nn.Module): + def __init__(self): + super().__init__() + self.conv3d_1 = nn.Conv3d(1, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d(16, 32, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + + self.xy_encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + "nvidia/mit-b5", + num_labels=1, + ignore_mismatched_sizes=True, + num_channels=32, + classifier_dropout_prob=0.3, + drop_path_rate=0.3, + hidden_dropout_prob=0.3, + ) + + self.upscaler1 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + self.upscaler2 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output).max(axis=2)[0] + output = self.xy_encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_unetplusplus.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_unetplusplus.py new file mode 100644 index 0000000..1a3ca28 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3d_unetplusplus.py @@ -0,0 +1,31 @@ +import segmentation_models_pytorch as smp +import torch.nn as nn + + +class CNN3DUnetPlusPlusEfficientNet(nn.Module): + def __init__(self, in_channels: int = 1, n_classes: int = 1, efficient_net_size: int = 5): + super().__init__() + cnn3d_out_channels = 32 + self.conv3d_1 = nn.Conv3d( + in_channels, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1) + ) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d( + 16, cnn3d_out_channels, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1) + ) + + self.xy_encoder_2d = smp.UnetPlusPlus( + encoder_name=f"timm-efficientnet-b{efficient_net_size}", + encoder_weights="imagenet", + in_channels=cnn3d_out_channels, + classes=n_classes, + ) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output).max(axis=2)[0] + output = self.xy_encoder_2d(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3dto2d_crackformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3dto2d_crackformer.py new file mode 100644 index 0000000..705b83a --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/cnn3dto2d_crackformer.py @@ -0,0 +1,51 @@ +from typing import Literal + +import torch.nn as nn + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPoolingBlock, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.crackformer import Crackformer + + +class CNN3Dto2dCrackformer(nn.Module): + def __init__( + self, + z_extent: int, + height: int, + width: int, + se_type_str: str | None = None, + in_channels: int = 1, + out_channels: int = 1, + depth_pool_fn: Literal["mean", "max", "attention"] = "attention", + ): + super().__init__() + self.conv3d_1 = nn.Conv3d( + in_channels, 4, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1) + ) + self.conv3d_2 = nn.Conv3d(4, 8, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_3 = nn.Conv3d(8, 16, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + self.conv3d_4 = nn.Conv3d(16, 32, kernel_size=(3, 3, 3), stride=1, padding=(1, 1, 1)) + + self.depth_pooler = DepthPoolingBlock( + input_channels=1, + se_type=se_type_str, + reduction_ratio=2, + depth_dropout=0, + pool_fn=depth_pool_fn, + dim=2, + depth=z_extent, + height=height, + width=width, + ) + + self.xy_encoder_2d = Crackformer(in_channels=32, out_channels=out_channels) + + def forward(self, image): + output = self.conv3d_1(image) + output = self.conv3d_2(output) + output = self.conv3d_3(output) + output = self.conv3d_4(output) + output = self.depth_pooler(output) + output = self.xy_encoder_2d(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/crackformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/crackformer.py new file mode 100644 index 0000000..4c0d230 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/crackformer.py @@ -0,0 +1,501 @@ +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F +from timm.models.layers import DropPath, trunc_normal_ + + +class DWConv(nn.Module): + def __init__(self, dim=768, group_num=4): + super().__init__() + self.dwconv = nn.Conv2d(dim, dim, 3, 1, 1, bias=True, groups=dim // group_num) + + def forward(self, x): + x = self.dwconv(x) + return x + + +def Conv1X1(in_, out): + return torch.nn.Conv2d(in_, out, 1, padding=0) + + +def Conv3X3(in_, out): + return torch.nn.Conv2d(in_, out, 3, padding=1) + + +class Mlp(nn.Module): + def __init__(self, in_features, out_features, act_layer=nn.GELU, drop=0.0, linear=False): + super().__init__() + out_features = out_features or in_features + hidden_features = out_features // 4 + self.fc1 = Conv1X1(in_features, hidden_features) + self.gn1 = nn.GroupNorm(hidden_features // 4, hidden_features) + self.dwconv = DWConv(hidden_features) + self.gn2 = nn.GroupNorm(hidden_features // 4, hidden_features) + self.act = act_layer() + self.fc2 = Conv1X1(hidden_features, out_features) + self.gn3 = nn.GroupNorm(out_features // 4, out_features) + self.drop = nn.Dropout(drop) + self.linear = linear + if self.linear: + self.relu = nn.ReLU(inplace=True) + self.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Conv2d): + fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + fan_out //= m.groups + m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) + if m.bias is not None: + m.bias.data.zero_() + + def forward(self, x): + x = self.fc1(x) + x = self.gn1(x) + if self.linear: + x = self.relu(x) + x = self.dwconv(x) + x = self.gn2(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.gn3(x) + x = self.drop(x) + return x + + +class LocalSABlock(nn.Module): + def __init__(self, in_channels, out_channels, heads=4, k=16, u=1, m=7): + super().__init__() + self.kk, self.uu, self.vv, self.mm, self.heads = k, u, out_channels // heads, m, heads + self.padding = (m - 1) // 2 + + self.queries = nn.Sequential( + nn.Conv2d(in_channels, k * heads, kernel_size=1, bias=False), + nn.GroupNorm(k * heads // 4, k * heads), + ) + self.keys = nn.Sequential( + nn.Conv2d(in_channels, k * u, kernel_size=1, bias=False), + nn.GroupNorm(k * u // 4, k * u), + ) + self.values = nn.Sequential( + nn.Conv2d(in_channels, self.vv * u, kernel_size=1, bias=False), + nn.GroupNorm(self.vv * u // 4, self.vv * u), + ) + + self.softmax = nn.Softmax(dim=-1) + + self.embedding = nn.Parameter(torch.randn([self.kk, self.uu, 1, m, m]), requires_grad=True) + + def forward(self, x): + n_batch, C, w, h = x.size() + queries = self.queries(x).view(n_batch, self.heads, self.kk, w * h) # b, heads, k , w * h + softmax = self.softmax( + self.keys(x).view(n_batch, self.kk, self.uu, w * h) + ) # b, k, uu, w * h + values = self.values(x).view(n_batch, self.vv, self.uu, w * h) # b, v, uu, w * h + content = torch.einsum("bkum,bvum->bkv", (softmax, values)) + content = torch.einsum("bhkn,bkv->bhvn", (queries, content)) + values = values.view(n_batch, self.uu, -1, w, h) + context = F.conv3d(values, self.embedding, padding=(0, self.padding, self.padding)) + context = context.view(n_batch, self.kk, self.vv, w * h) + context = torch.einsum("bhkn,bkvn->bhvn", (queries, context)) + + out = content + context + out = out.contiguous().view(n_batch, -1, w, h) + + return out + + +class TFBlock(nn.Module): + def __init__( + self, + in_chnnels, + out_chnnels, + mlp_ratio=2.0, + drop=0.3, + drop_path=0.0, + act_layer=nn.GELU, + linear=False, + ): + super().__init__() + self.in_chnnels = in_chnnels + self.out_chnnels = out_chnnels + self.attn = LocalSABlock(in_channels=in_chnnels, out_channels=out_chnnels) + # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + self.mlp = Mlp( + in_features=in_chnnels, + out_features=out_chnnels, + act_layer=act_layer, + drop=drop, + linear=linear, + ) + self.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Conv2d): + fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + fan_out //= m.groups + m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) + if m.bias is not None: + m.bias.data.zero_() + + def forward(self, x): + x = x + self.drop_path(self.attn(x)) + x = x + self.drop_path(self.mlp(x)) + return x + + +class Bottleneck(nn.Module): + def __init__(self, in_planes, planes, stride=1): + super().__init__() + self.expansion = 4 + hidden_planes = max(planes, in_planes) // self.expansion + self.conv1 = nn.Conv2d(in_planes, hidden_planes, kernel_size=1, bias=False) + self.bn1 = nn.GroupNorm(hidden_planes // 4, hidden_planes) + self.conv2 = nn.ModuleList([TFBlock(hidden_planes, hidden_planes)]) + self.bn2 = nn.GroupNorm(hidden_planes // 4, hidden_planes) + self.conv2.append(nn.GELU()) + self.conv2 = nn.Sequential(*self.conv2) + self.conv3 = nn.Conv2d(hidden_planes, planes, kernel_size=1, bias=False) + self.bn3 = nn.GroupNorm(planes // 4, planes) + self.GELU = nn.GELU() + self.shortcut = nn.Sequential() + if in_planes != planes: + self.shortcut = nn.Sequential( + nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), + nn.GroupNorm(planes // 4, planes), + ) + + def forward(self, x): + out = self.GELU(self.bn1(self.conv1(x))) + out = self.conv2(out) + out = self.GELU(self.bn3(self.conv3(out))) + out += self.shortcut(x) + return out + + +class Trans_EB(nn.Module): + def __init__(self, in_, out): + super().__init__() + self.conv = Bottleneck(in_, out) + self.activation = torch.nn.GELU() + + def forward(self, x): + x = self.conv(x) + x = self.activation(x) + return x + + +class ConvRelu(nn.Module): + def __init__(self, in_, out): + super().__init__() + self.conv = Conv3X3(in_, out) + self.activation = torch.nn.ReLU(inplace=True) + + def forward(self, x): + x = self.conv(x) + x = self.activation(x) + return x + + +class LABlock(nn.Module): + def __init__(self, input_channels, output_channels): + super().__init__() + self.W_1 = nn.Sequential( + nn.Conv2d( + input_channels, output_channels, kernel_size=3, stride=1, padding=1, bias=True + ), + nn.GroupNorm(output_channels // 4, output_channels), + ) + self.psi = nn.Sequential( + nn.Conv2d( + output_channels, output_channels, kernel_size=3, stride=1, padding=1, bias=True + ), + nn.GroupNorm(output_channels // 4, output_channels), + nn.Sigmoid(), + ) + + self.relu = nn.ReLU(inplace=True) + self.gelu = nn.GELU() + + def forward(self, inputs): + sum = 0 + for input in inputs: + sum += input + sum = self.gelu(sum) + out = self.W_1(sum) + psi = self.psi(out) # Mask + return psi + + +class Fuse(nn.Module): + def __init__(self, nn, scale): + super().__init__() + self.nn = nn + self.scale = scale + self.conv = Conv3X3(64, 1) + + def forward(self, down_inp, up_inp, size, attention): + outputs = torch.cat([down_inp, up_inp], 1) + outputs = self.nn(outputs) + outputs = attention * outputs + outputs = self.conv(outputs) + outputs = F.interpolate(outputs, scale_factor=self.scale, mode="bilinear") + return outputs + + +class Down1(nn.Module): + def __init__(self, in_channels: int = 3): + super().__init__() + self.nn1 = ConvRelu(in_channels, 64) + self.nn2 = Trans_EB(64, 64) + self.patch_embed = torch.nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True) + + def forward(self, inputs): + scale1_1 = self.nn1(inputs) + scale1_2 = self.nn2(scale1_1) + unpooled_shape = scale1_2.size() + outputs, indices = self.patch_embed(scale1_2) + return outputs, indices, unpooled_shape, scale1_1, scale1_2 + + +class Down2(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(64, 128) + self.nn2 = Trans_EB(128, 128) + self.patch_embed = torch.nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True) + + def forward(self, inputs): + scale2_1 = self.nn1(inputs) + scale2_2 = self.nn2(scale2_1) + unpooled_shape = scale2_2.size() + outputs, indices = self.patch_embed(scale2_2) + return outputs, indices, unpooled_shape, scale2_1, scale2_2 + + +class Down3(nn.Module): + def __init__(self): + super().__init__() + + self.nn1 = Trans_EB(128, 256) + self.nn2 = Trans_EB(256, 256) + self.nn3 = Trans_EB(256, 256) + self.patch_embed = torch.nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True) + + def forward(self, inputs): + scale3_1 = self.nn1(inputs) + scale3_2 = self.nn2(scale3_1) + scale3_3 = self.nn2(scale3_2) + unpooled_shape = scale3_3.size() + outputs, indices = self.patch_embed(scale3_3) + return outputs, indices, unpooled_shape, scale3_1, scale3_2, scale3_3 + + +class Down4(nn.Module): + def __init__(self): + super().__init__() + + self.nn1 = Trans_EB(256, 512) + self.nn2 = Trans_EB(512, 512) + self.nn3 = Trans_EB(512, 512) + self.patch_embed = torch.nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True) + + def forward(self, inputs): + scale4_1 = self.nn1(inputs) + scale4_2 = self.nn2(scale4_1) + scale4_3 = self.nn2(scale4_2) + unpooled_shape = scale4_3.size() + outputs, indices = self.patch_embed(scale4_3) + return outputs, indices, unpooled_shape, scale4_1, scale4_2, scale4_3 + + +class Down5(nn.Module): + def __init__(self): + super().__init__() + + self.nn1 = Trans_EB(512, 512) + self.nn2 = Trans_EB(512, 512) + self.nn3 = Trans_EB(512, 512) + self.patch_embed = torch.nn.MaxPool2d(kernel_size=2, stride=2, return_indices=True) + + def forward(self, inputs): + scale5_1 = self.nn1(inputs) + scale5_2 = self.nn2(scale5_1) + scale5_3 = self.nn2(scale5_2) + unpooled_shape = scale5_3.size() + outputs, indices = self.patch_embed(scale5_3) + return outputs, indices, unpooled_shape, scale5_1, scale5_2, scale5_3 + + +class Up1(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(64, 64) + self.nn2 = Trans_EB(64, 64) + self.inv_patch_embed = torch.nn.MaxUnpool2d(2, 2) + + def forward(self, inputs, indices, output_shape): + outputs = self.inv_patch_embed(inputs, indices=indices, output_size=output_shape) + scale1_3 = self.nn1(outputs) + scale1_4 = self.nn2(scale1_3) + return scale1_3, scale1_4 + + +class Up2(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(128, 128) + self.nn2 = Trans_EB(128, 64) + self.inv_patch_embed = torch.nn.MaxUnpool2d(2, 2) + + def forward(self, inputs, indices, output_shape): + outputs = self.inv_patch_embed(inputs, indices=indices, output_size=output_shape) + scale2_3 = self.nn1(outputs) + scale2_4 = self.nn2(scale2_3) + return scale2_3, scale2_4 + + +class Up3(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(256, 256) + self.nn2 = Trans_EB(256, 256) + self.nn3 = Trans_EB(256, 128) + self.inv_patch_embed = torch.nn.MaxUnpool2d(2, 2) + + def forward(self, inputs, indices, output_shape): + outputs = self.inv_patch_embed(inputs, indices=indices, output_size=output_shape) + scale3_4 = self.nn1(outputs) + scale3_5 = self.nn2(scale3_4) + scale3_6 = self.nn3(scale3_5) + return scale3_4, scale3_5, scale3_6 + + +class Up4(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(512, 512) + self.nn2 = Trans_EB(512, 512) + self.nn3 = Trans_EB(512, 256) + self.inv_patch_embed = torch.nn.MaxUnpool2d(2, 2) + + def forward(self, inputs, indices, output_shape): + outputs = self.inv_patch_embed(inputs, indices=indices, output_size=output_shape) + scale4_4 = self.nn1(outputs) + scale4_5 = self.nn2(scale4_4) + scale4_6 = self.nn3(scale4_5) + return scale4_4, scale4_5, scale4_6 + + +class Up5(nn.Module): + def __init__(self): + super().__init__() + self.nn1 = Trans_EB(512, 512) + self.nn2 = Trans_EB(512, 512) + self.nn3 = Trans_EB(512, 512) + self.inv_patch_embed = torch.nn.MaxUnpool2d(2, 2) + + def forward(self, inputs, indices, output_shape): + outputs = self.inv_patch_embed(inputs, indices=indices, output_size=output_shape) + scale5_4 = self.nn1(outputs) + scale5_5 = self.nn2(scale5_4) + scale5_6 = self.nn3(scale5_5) + return scale5_4, scale5_5, scale5_6 + + +class Crackformer(nn.Module): + def __init__(self, in_channels: int = 1, out_channels: int = 1): + super().__init__() + + self.down1 = Down1(in_channels=in_channels) + self.down2 = Down2() + self.down3 = Down3() + self.down4 = Down4() + self.down5 = Down5() + + self.up1 = Up1() + self.up2 = Up2() + self.up3 = Up3() + self.up4 = Up4() + self.up5 = Up5() + + self.fuse5 = Fuse(ConvRelu(512 + 512, 64), scale=16) + self.fuse4 = Fuse(ConvRelu(512 + 256, 64), scale=8) + self.fuse3 = Fuse(ConvRelu(256 + 128, 64), scale=4) + self.fuse2 = Fuse(ConvRelu(128 + 64, 64), scale=2) + self.fuse1 = Fuse(ConvRelu(64 + 64, 64), scale=1) + + self.final = Conv1X1(5, out_channels) + + self.LABlock_1 = LABlock(64, 64) + self.LABlock_2 = LABlock(128, 64) + self.LABlock_3 = LABlock(256, 64) + self.LABlock_4 = LABlock(512, 64) + self.LABlock_5 = LABlock(512, 64) + + def forward(self, inputs): + # encoder part + out, indices_1, unpool_shape1, scale1_1, scale1_2 = self.down1(inputs) + out, indices_2, unpool_shape2, scale2_1, scale2_2 = self.down2(out) + out, indices_3, unpool_shape3, scale3_1, scale3_2, scale3_3 = self.down3(out) + out, indices_4, unpool_shape4, scale4_1, scale4_2, scale4_3 = self.down4(out) + out, indices_5, unpool_shape5, scale5_1, scale5_2, scale5_3 = self.down5(out) + # decoder part + scale5_4, scale5_5, up5 = self.up5(out, indices=indices_5, output_shape=unpool_shape5) + scale4_4, scale4_5, up4 = self.up4(up5, indices=indices_4, output_shape=unpool_shape4) + scale3_4, scale3_5, up3 = self.up3(up4, indices=indices_3, output_shape=unpool_shape3) + scale2_3, up2 = self.up2(up3, indices=indices_2, output_shape=unpool_shape2) + scale1_3, up1 = self.up1(up2, indices=indices_1, output_shape=unpool_shape1) + # attention part + attention1 = self.LABlock_1([scale1_1, scale1_3]) + attention2 = self.LABlock_2([scale2_1, scale2_3]) + attention3 = self.LABlock_3([scale3_1, scale3_2, scale3_4, scale3_5]) + attention4 = self.LABlock_4([scale4_1, scale4_2, scale4_4, scale4_5]) + attention5 = self.LABlock_5([scale5_1, scale5_2, scale5_4, scale5_5]) + # fuse part + fuse5 = self.fuse5( + down_inp=scale5_3, + up_inp=up5, + size=[inputs.shape[2], inputs.shape[3]], + attention=attention5, + ) + fuse4 = self.fuse4( + down_inp=scale4_3, + up_inp=up4, + size=[inputs.shape[2], inputs.shape[3]], + attention=attention4, + ) + fuse3 = self.fuse3( + down_inp=scale3_3, + up_inp=up3, + size=[inputs.shape[2], inputs.shape[3]], + attention=attention3, + ) + fuse2 = self.fuse2( + down_inp=scale2_2, + up_inp=up2, + size=[inputs.shape[2], inputs.shape[3]], + attention=attention2, + ) + fuse1 = self.fuse1( + down_inp=scale1_2, + up_inp=up1, + size=[inputs.shape[2], inputs.shape[3]], + attention=attention1, + ) + + output = self.final(torch.cat([fuse5, fuse4, fuse3, fuse2, fuse1], 1)) + + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/gradient_reversal.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/gradient_reversal.py new file mode 100644 index 0000000..e7dd477 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/gradient_reversal.py @@ -0,0 +1,47 @@ +import torch +import torch.nn as nn +from torch.autograd import Function + + +class _GradientReversal(Function): + @staticmethod + def forward(ctx, x, alpha: float): + ctx.save_for_backward(x, alpha) + return x + + @staticmethod + def backward(ctx, grad_output): + grad_input = None + _, alpha = ctx.saved_tensors + if ctx.needs_input_grad[0]: + grad_input = -alpha * grad_output + return grad_input, None + + +class GradientReversal(nn.Module): + """ + Implementation of the gradient reversal layer described in + [Domain-Adversarial Training of Neural Networks](https://arxiv.org/abs/1505.07818), + which 'leaves the input unchanged during forward propagation + and reverses the gradient by multiplying it + by a negative scalar during backpropagation.' + """ + + def __init__(self, alpha: float = 1.0, *args, **kwargs): + """ + A gradient reversal layer. + + This layer has no parameters, and simply reverses the gradient + in the backward pass. + """ + super().__init__(*args, **kwargs) + self.register_buffer("alpha", torch.tensor([alpha], requires_grad=False)) + + def update_alpha(self, alpha: float) -> None: + self.alpha[0] = alpha + + def forward(self, input_): + return _GradientReversal.apply(input_, self.alpha) + + def extra_repr(self): + return f"alpha={self.alpha.item()}" diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/hrsegnet.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/hrsegnet.py new file mode 100644 index 0000000..9a9b098 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/hrsegnet.py @@ -0,0 +1,277 @@ +import math + +import torch.nn as nn +import torch.nn.functional as F + + +class HrSegNetB16Model(nn.Module): + """ + The HrSegNet implementation based on PaddlePaddle.s + + Args: + num_classes (int): The unique number of target classes. + + in_channels (int, optional): The channels of input image. Default: 3. + + base (int, optional): The base channel number of the model. Default: 16. + """ + + def __init__( + self, + in_channels: int = 1, # input channel + num_classes: int = 1, # number of classes + base: int = 16, # base channel of the model, + ): + super().__init__() + self.base = base + self.num_classes = num_classes + # Stage 1 and 2 constitute the stem of the model, which is mainly used to extract low-level features. + # Meanwhile, stage1 and 2 reduce the input image to 1/2 and 1/4 of the original size respectively + self.stage1 = nn.Sequential( + nn.Conv3d( + in_channels=in_channels, out_channels=base // 2, kernel_size=3, stride=2, padding=1 + ), + nn.BatchNorm3d(base // 2), + nn.ReLU(), + ) + self.stage2 = nn.Sequential( + nn.Conv3d(in_channels=base // 2, out_channels=base, kernel_size=3, stride=2, padding=1), + nn.BatchNorm3d(base), + nn.ReLU(), + ) + + self.seg1 = SegBlock(base=base, stage_index=1) + self.seg2 = SegBlock(base=base, stage_index=2) + self.seg3 = SegBlock(base=base, stage_index=3) + + self.aux_head1 = SegHead( + inplanes=base, interplanes=base, outplanes=num_classes, aux_head=True + ) + self.aux_head2 = SegHead( + inplanes=base, interplanes=base, outplanes=num_classes, aux_head=True + ) + self.head = SegHead(inplanes=base, interplanes=base, outplanes=num_classes) + + self.init_weight() + + def forward(self, x): + d, h, w = x.shape[2:] + # aux_head only used in training + if self.training: + stem1_out = self.stage1(x) + stem2_out = self.stage2(stem1_out) + hrseg1_out = self.seg1(stem2_out) + hrseg2_out = self.seg2(hrseg1_out) + hrseg3_out = self.seg3(hrseg2_out) + last_out = self.head(hrseg3_out) + seghead1_out = self.aux_head1(hrseg1_out) + seghead2_out = self.aux_head2(hrseg2_out) + logit_list = [last_out, seghead1_out, seghead2_out] + logit_list = [ + F.interpolate(logit, size=(d, h, w), mode="trilinear", align_corners=True) + for logit in logit_list + ] + return logit_list + else: + stem1_out = self.stage1(x) + stem2_out = self.stage2(stem1_out) + hrseg1_out = self.seg1(stem2_out) + hrseg2_out = self.seg2(hrseg1_out) + hrseg3_out = self.seg3(hrseg2_out) + last_out = self.head(hrseg3_out) + logit_list = [last_out] + logit_list = [ + F.interpolate(logit, size=(d, h, w), mode="trilinear", align_corners=True) + for logit in logit_list + ] + return logit_list + + def init_weight(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + + +class SegBlock(nn.Module): + def __init__(self, base: int = 32, stage_index: int = 1): # stage_index=1,2,3. + super().__init__() + + # Convolutional layer for high-resolution paths with constant spatial resolution and constant channel + self.h_conv1 = nn.Sequential( + nn.Conv3d(in_channels=base, out_channels=base, kernel_size=3, stride=1, padding=1), + nn.BatchNorm3d(base), + nn.ReLU(), + ) + self.h_conv2 = nn.Sequential( + nn.Conv3d(in_channels=base, out_channels=base, kernel_size=3, stride=1, padding=1), + nn.BatchNorm3d(base), + nn.ReLU(), + ) + self.h_conv3 = nn.Sequential( + nn.Conv3d(in_channels=base, out_channels=base, kernel_size=3, stride=1, padding=1), + nn.BatchNorm3d(base), + nn.ReLU(), + ) + + # semantic guidance path/low-resolution path + if stage_index == 1: # first stage, stride=2, spatial resolution/2, channel*2 + self.l_conv1 = nn.Sequential( + nn.Conv3d( + in_channels=base, + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=2, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + ) + elif stage_index == 2: # second stage + self.l_conv1 = nn.Sequential( + nn.AvgPool3d(kernel_size=3, stride=2, padding=1), + nn.Conv3d( + in_channels=base, + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=2, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + ) + elif stage_index == 3: + self.l_conv1 = nn.Sequential( + nn.AvgPool3d(kernel_size=3, stride=2, padding=1), + nn.Conv3d( + in_channels=base, + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=2, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=2, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + ) + else: + raise ValueError("stage_index must be 1, 2 or 3") + self.l_conv2 = nn.Sequential( + nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=1, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + ) + self.l_conv3 = nn.Sequential( + nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base * int(math.pow(2, stage_index)), + kernel_size=3, + stride=1, + padding=1, + ), + nn.BatchNorm3d(base * int(math.pow(2, stage_index))), + nn.ReLU(), + ) + + self.l2h_conv1 = nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base, + kernel_size=1, + stride=1, + padding=0, + ) + self.l2h_conv2 = nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base, + kernel_size=1, + stride=1, + padding=0, + ) + self.l2h_conv3 = nn.Conv3d( + in_channels=base * int(math.pow(2, stage_index)), + out_channels=base, + kernel_size=1, + stride=1, + padding=0, + ) + + def forward(self, x): + size = x.shape[2:] + out_h1 = self.h_conv1(x) # high resolution path + out_l1 = self.l_conv1(x) # low resolution path + + out_l1_i = F.interpolate( + out_l1, size=size, mode="trilinear", align_corners=True + ) # upsample + out_hl1 = self.l2h_conv1(out_l1_i) + out_h1 # low to high + + out_h2 = self.h_conv2(out_hl1) + out_l2 = self.l_conv2(out_l1) + + out_l2_i = F.interpolate(out_l2, size=size, mode="trilinear", align_corners=True) + out_hl2 = self.l2h_conv2(out_l2_i) + out_h2 + + out_h3 = self.h_conv3(out_hl2) + out_l3 = self.l_conv3(out_l2) + + out_l3_i = F.interpolate(out_l3, size=size, mode="trilinear", align_corners=True) + out_hl3 = self.l2h_conv3(out_l3_i) + out_h3 + return out_hl3 + + +class SegHead(nn.Module): + def __init__(self, inplanes: int, interplanes: int, outplanes: int, aux_head: bool = False): + super().__init__() + self.bn1 = nn.BatchNorm3d(inplanes) + self.relu = nn.ReLU() + if aux_head: + self.con_bn_relu = nn.Sequential( + nn.Conv3d( + in_channels=inplanes, + out_channels=interplanes, + kernel_size=3, + stride=1, + padding=1, + ), + nn.BatchNorm3d(interplanes), + nn.ReLU(), + ) + else: + self.con_bn_relu = nn.Sequential( + nn.ConvTranspose3d( + in_channels=inplanes, + out_channels=interplanes, + kernel_size=3, + stride=2, + padding=1, + output_padding=1, + ), + nn.BatchNorm3d(interplanes), + nn.ReLU(), + ) + self.conv = nn.Conv3d( + in_channels=interplanes, out_channels=outplanes, kernel_size=1, stride=1, padding=0 + ) + + def forward(self, x): + x = self.bn1(x) + x = self.relu(x) + x = self.con_bn_relu(x) + out = self.conv(x) + return out diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/i3dall.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/i3dall.py new file mode 100644 index 0000000..fd1ff83 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/i3dall.py @@ -0,0 +1,446 @@ +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class MaxPool3dSamePadding(nn.MaxPool3d): + def compute_pad(self, dim, s): + if s % self.stride[dim] == 0: + return max(self.kernel_size[dim] - self.stride[dim], 0) + else: + return max(self.kernel_size[dim] - (s % self.stride[dim]), 0) + + def forward(self, x): + # compute 'same' padding + (batch, channel, t, h, w) = x.size() + # print t,h,w + out_t = np.ceil(float(t) / float(self.stride[0])) + out_h = np.ceil(float(h) / float(self.stride[1])) + out_w = np.ceil(float(w) / float(self.stride[2])) + # print out_t, out_h, out_w + pad_t = self.compute_pad(0, t) + pad_h = self.compute_pad(1, h) + pad_w = self.compute_pad(2, w) + # print pad_t, pad_h, pad_w + + pad_t_f = pad_t // 2 + pad_t_b = pad_t - pad_t_f + pad_h_f = pad_h // 2 + pad_h_b = pad_h - pad_h_f + pad_w_f = pad_w // 2 + pad_w_b = pad_w - pad_w_f + + pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b) + # print x.size() + # print pad + x = F.pad(x, pad) + return super().forward(x) + + +class Unit3D(nn.Module): + def __init__( + self, + in_channels, + output_channels, + kernel_shape=(1, 1, 1), + stride=(1, 1, 1), + padding=0, + activation_fn=F.relu, + use_batch_norm=True, + use_bias=False, + name="unit_3d", + ): + """Initializes Unit3D module.""" + super().__init__() + + self._output_channels = output_channels + self._kernel_shape = kernel_shape + self._stride = stride + self._use_batch_norm = use_batch_norm + self._activation_fn = activation_fn + self._use_bias = use_bias + self.name = name + self.padding = padding + + self.conv3d = nn.Conv3d( + in_channels=in_channels, + out_channels=self._output_channels, + kernel_size=self._kernel_shape, + stride=self._stride, + padding=0, + # we always want padding to be 0 here. We will dynamically pad based on input size in forward function + bias=self._use_bias, + ) + + if self._use_batch_norm: + self.bn = nn.BatchNorm3d(self._output_channels, eps=0.001, momentum=0.01) + + def compute_pad(self, dim, s): + if s % self._stride[dim] == 0: + return max(self._kernel_shape[dim] - self._stride[dim], 0) + else: + return max(self._kernel_shape[dim] - (s % self._stride[dim]), 0) + + def forward(self, x): + # compute 'same' padding + (batch, channel, t, h, w) = x.size() + # print t,h,w + out_t = np.ceil(float(t) / float(self._stride[0])) + out_h = np.ceil(float(h) / float(self._stride[1])) + out_w = np.ceil(float(w) / float(self._stride[2])) + # print out_t, out_h, out_w + pad_t = self.compute_pad(0, t) + pad_h = self.compute_pad(1, h) + pad_w = self.compute_pad(2, w) + # print pad_t, pad_h, pad_w + + pad_t_f = pad_t // 2 + pad_t_b = pad_t - pad_t_f + pad_h_f = pad_h // 2 + pad_h_b = pad_h - pad_h_f + pad_w_f = pad_w // 2 + pad_w_b = pad_w - pad_w_f + + pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b) + # print x.size() + # print pad + x = F.pad(x, pad) + # print x.size() + + x = self.conv3d(x) + if self._use_batch_norm: + x = self.bn(x) + if self._activation_fn is not None: + x = self._activation_fn(x) + return x + + +class InceptionModule(nn.Module): + def __init__(self, in_channels, out_channels, name): + super().__init__() + + self.b0 = Unit3D( + in_channels=in_channels, + output_channels=out_channels[0], + kernel_shape=[1, 1, 1], + padding=0, + name=name + "/Branch_0/Conv3d_0a_1x1", + ) + self.b1a = Unit3D( + in_channels=in_channels, + output_channels=out_channels[1], + kernel_shape=[1, 1, 1], + padding=0, + name=name + "/Branch_1/Conv3d_0a_1x1", + ) + self.b1b = Unit3D( + in_channels=out_channels[1], + output_channels=out_channels[2], + kernel_shape=[3, 3, 3], + name=name + "/Branch_1/Conv3d_0b_3x3", + ) + self.b2a = Unit3D( + in_channels=in_channels, + output_channels=out_channels[3], + kernel_shape=[1, 1, 1], + padding=0, + name=name + "/Branch_2/Conv3d_0a_1x1", + ) + self.b2b = Unit3D( + in_channels=out_channels[3], + output_channels=out_channels[4], + kernel_shape=[3, 3, 3], + name=name + "/Branch_2/Conv3d_0b_3x3", + ) + self.b3a = MaxPool3dSamePadding(kernel_size=[3, 3, 3], stride=(1, 1, 1), padding=0) + self.b3b = Unit3D( + in_channels=in_channels, + output_channels=out_channels[5], + kernel_shape=[1, 1, 1], + padding=0, + name=name + "/Branch_3/Conv3d_0b_1x1", + ) + self.name = name + + def forward(self, x): + b0 = self.b0(x) + b1 = self.b1b(self.b1a(x)) + b2 = self.b2b(self.b2a(x)) + b3 = self.b3b(self.b3a(x)) + return torch.cat([b0, b1, b2, b3], dim=1) + + +class InceptionI3d(nn.Module): + """Inception-v1 I3D architecture. + The model is introduced in: + Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset + Joao Carreira, Andrew Zisserman + https://arxiv.org/pdf/1705.07750v1.pdf. + See also the Inception architecture, introduced in: + Going deeper with convolutions + Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed, + Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, Andrew Rabinovich. + http://arxiv.org/pdf/1409.4842v1.pdf. + """ + + # Endpoints of the model in order. During construction, all the endpoints up + # to a designated `final_endpoint` are returned in a dictionary as the + # second return value. + VALID_ENDPOINTS = ( + "Conv3d_1a_7x7", + "MaxPool3d_2a_3x3", + "Conv3d_2b_1x1", + "Conv3d_2c_3x3", + "MaxPool3d_3a_3x3", + "Mixed_3b", + "Mixed_3c", + "MaxPool3d_4a_3x3", + "Mixed_4b", + "Mixed_4c", + "Mixed_4d", + "Mixed_4e", + "Mixed_4f", + "MaxPool3d_5a_2x2", + "Mixed_5b", + "Mixed_5c", + "Logits", + "Predictions", + ) + FEATURE_ENDPOINTS = [ + "Conv3d_2c_3x3", + "Mixed_3c", + "Mixed_4f", + "Mixed_5c", + ] + + def __init__( + self, + num_classes=400, + spatial_squeeze=True, + final_endpoint="Logits", + name="inception_i3d", + in_channels=3, + dropout_keep_prob=0.5, + forward_features=True, + ): + """Initializes I3D model instance. + Args: + num_classes: The number of outputs in the logit layer (default 400, which + matches the Kinetics dataset). + spatial_squeeze: Whether to squeeze the spatial dimensions for the logits + before returning (default True). + final_endpoint: The model contains many possible endpoints. + `final_endpoint` specifies the last endpoint for the model to be built + up to. In addition to the output at `final_endpoint`, all the outputs + at endpoints up to `final_endpoint` will also be returned, in a + dictionary. `final_endpoint` must be one of + InceptionI3d.VALID_ENDPOINTS (default 'Logits'). + name: A string (optional). The name of this module. + Raises: + ValueError: if `final_endpoint` is not recognized. + """ + + if final_endpoint not in self.VALID_ENDPOINTS: + raise ValueError("Unknown final endpoint %s" % final_endpoint) + + super().__init__() + self._num_classes = num_classes + self._spatial_squeeze = spatial_squeeze + self._final_endpoint = final_endpoint + self.logits = None + self.forward_features = forward_features + if self._final_endpoint not in self.VALID_ENDPOINTS: + raise ValueError("Unknown final endpoint %s" % self._final_endpoint) + + self.end_points = {} + end_point = "Conv3d_1a_7x7" + self.end_points[end_point] = Unit3D( + in_channels=in_channels, + output_channels=64, + kernel_shape=[7, 7, 7], + stride=(2, 2, 2), + padding=(3, 3, 3), + name=name + end_point, + ) + if self._final_endpoint == end_point: + return + + end_point = "MaxPool3d_2a_3x3" + self.end_points[end_point] = MaxPool3dSamePadding( + kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0 + ) + if self._final_endpoint == end_point: + return + + end_point = "Conv3d_2b_1x1" + self.end_points[end_point] = Unit3D( + in_channels=64, + output_channels=64, + kernel_shape=[1, 1, 1], + padding=0, + name=name + end_point, + ) + if self._final_endpoint == end_point: + return + + end_point = "Conv3d_2c_3x3" + self.end_points[end_point] = Unit3D( + in_channels=64, + output_channels=192, + kernel_shape=[3, 3, 3], + padding=1, + name=name + end_point, + ) + if self._final_endpoint == end_point: + return + + end_point = "MaxPool3d_3a_3x3" + self.end_points[end_point] = MaxPool3dSamePadding( + kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0 + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_3b" + self.end_points[end_point] = InceptionModule( + 192, [64, 96, 128, 16, 32, 32], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_3c" + self.end_points[end_point] = InceptionModule( + 256, [128, 128, 192, 32, 96, 64], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "MaxPool3d_4a_3x3" + self.end_points[end_point] = MaxPool3dSamePadding( + kernel_size=[3, 3, 3], stride=(2, 2, 2), padding=0 + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_4b" + self.end_points[end_point] = InceptionModule( + 128 + 192 + 96 + 64, [192, 96, 208, 16, 48, 64], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_4c" + self.end_points[end_point] = InceptionModule( + 192 + 208 + 48 + 64, [160, 112, 224, 24, 64, 64], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_4d" + self.end_points[end_point] = InceptionModule( + 160 + 224 + 64 + 64, [128, 128, 256, 24, 64, 64], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_4e" + self.end_points[end_point] = InceptionModule( + 128 + 256 + 64 + 64, [112, 144, 288, 32, 64, 64], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_4f" + self.end_points[end_point] = InceptionModule( + 112 + 288 + 64 + 64, [256, 160, 320, 32, 128, 128], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "MaxPool3d_5a_2x2" + self.end_points[end_point] = MaxPool3dSamePadding( + kernel_size=[2, 2, 2], stride=(2, 2, 2), padding=0 + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_5b" + self.end_points[end_point] = InceptionModule( + 256 + 320 + 128 + 128, [256, 160, 320, 32, 128, 128], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Mixed_5c" + self.end_points[end_point] = InceptionModule( + 256 + 320 + 128 + 128, [384, 192, 384, 48, 128, 128], name + end_point + ) + if self._final_endpoint == end_point: + return + + end_point = "Logits" + self.avg_pool = nn.AvgPool3d(kernel_size=[2, 7, 7], stride=(1, 1, 1)) + self.dropout = nn.Dropout(dropout_keep_prob) + self.logits = Unit3D( + in_channels=384 + 384 + 128 + 128, + output_channels=self._num_classes, + kernel_shape=[1, 1, 1], + padding=0, + activation_fn=None, + use_batch_norm=False, + use_bias=True, + name="logits", + ) + self.final_pool = self.avgpool = nn.AdaptiveMaxPool3d((1, 1, 1)) + + self.build() + + def replace_logits(self, num_classes): + self._num_classes = num_classes + self.logits = Unit3D( + in_channels=384 + 384 + 128 + 128, + output_channels=self._num_classes, + kernel_shape=[1, 1, 1], + padding=0, + activation_fn=None, + use_batch_norm=False, + use_bias=True, + name="logits", + ) + + def build(self): + for k in self.end_points.keys(): + self.add_module(k, self.end_points[k]) + + def forward(self, x): + if self.forward_features: + features = [] + for end_point in self.VALID_ENDPOINTS: + if end_point in self.end_points: + x = self._modules[end_point](x) # use _modules to work with dataparallel + if end_point in self.FEATURE_ENDPOINTS: + features.append(x) + # x = self.logits(self.dropout(self.avg_pool(x))) + # if self._spatial_squeeze: + # logits = x.squeeze(3).squeeze(3) + # # logits is batch X time X classes, which is what we want to work with + return features + else: + for end_point in self.VALID_ENDPOINTS: + if end_point in self.end_points: + x = self._modules[end_point](x) # use _modules to work with dataparallel + x = self.logits(self.dropout(self.avg_pool(x))) + if self._spatial_squeeze: + logits = x.squeeze(3).squeeze(3) + x = self.final_pool(x) + + x = x.view(x.size(0), -1) + return x + # # logits is batch X time X classes, which is what we want to work with + + def extract_features(self, x): + for end_point in self.VALID_ENDPOINTS: + if end_point in self.end_points: + x = self._modules[end_point](x) + return self.avg_pool(x) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1.py new file mode 100644 index 0000000..7dcd268 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1.py @@ -0,0 +1,401 @@ +import torch +import torch.nn as nn +import torch.utils.checkpoint as checkpoint + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.blocks import ( + MedNeXtBlock, + MedNeXtDownBlock, + MedNeXtUpBlock, + OutBlock, +) + + +class MedNeXt(nn.Module): + def __init__( + self, + in_channels: int, + n_channels: int, + n_classes: int, + exp_r: int = 4, # Expansion ratio as in Swin Transformers + kernel_size: int = 7, # Ofcourse can test kernel_size + enc_kernel_size: int = None, + dec_kernel_size: int = None, + deep_supervision: bool = False, # Can be used to test deep supervision + do_res: bool = False, # Can be used to individually test residual connection + do_res_up_down: bool = False, # Additional 'res' connection on up and down convs + checkpoint_style: bool = None, # Either inside block or outside block + block_counts: list = [2, 2, 2, 2, 2, 2, 2, 2, 2], # Can be used to test staging ratio: + # [3,3,9,3] in Swin as opposed to [2,2,2,2,2] in nnUNet + norm_type="group", + dim="3d", # 2d or 3d + grn=False, + ): + super().__init__() + + self.do_ds = deep_supervision + assert checkpoint_style in [None, "outside_block"] + self.inside_block_checkpointing = False + self.outside_block_checkpointing = False + if checkpoint_style == "outside_block": + self.outside_block_checkpointing = True + assert dim in ["2d", "3d"] + + if kernel_size is not None: + enc_kernel_size = kernel_size + dec_kernel_size = kernel_size + + if dim == "2d": + conv = nn.Conv2d + elif dim == "3d": + conv = nn.Conv3d + + self.stem = conv(in_channels, n_channels, kernel_size=1) + if type(exp_r) == int: + exp_r = [exp_r for i in range(len(block_counts))] + + self.enc_block_0 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels, + out_channels=n_channels, + exp_r=exp_r[0], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[0]) + ] + ) + + self.down_0 = MedNeXtDownBlock( + in_channels=n_channels, + out_channels=2 * n_channels, + exp_r=exp_r[1], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + ) + + self.enc_block_1 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 2, + out_channels=n_channels * 2, + exp_r=exp_r[1], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[1]) + ] + ) + + self.down_1 = MedNeXtDownBlock( + in_channels=2 * n_channels, + out_channels=4 * n_channels, + exp_r=exp_r[2], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.enc_block_2 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 4, + out_channels=n_channels * 4, + exp_r=exp_r[2], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[2]) + ] + ) + + self.down_2 = MedNeXtDownBlock( + in_channels=4 * n_channels, + out_channels=8 * n_channels, + exp_r=exp_r[3], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.enc_block_3 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 8, + out_channels=n_channels * 8, + exp_r=exp_r[3], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[3]) + ] + ) + + self.down_3 = MedNeXtDownBlock( + in_channels=8 * n_channels, + out_channels=16 * n_channels, + exp_r=exp_r[4], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.bottleneck = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 16, + out_channels=n_channels * 16, + exp_r=exp_r[4], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[4]) + ] + ) + + self.up_3 = MedNeXtUpBlock( + in_channels=16 * n_channels, + out_channels=8 * n_channels, + exp_r=exp_r[5], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.dec_block_3 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 8, + out_channels=n_channels * 8, + exp_r=exp_r[5], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[5]) + ] + ) + + self.up_2 = MedNeXtUpBlock( + in_channels=8 * n_channels, + out_channels=4 * n_channels, + exp_r=exp_r[6], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.dec_block_2 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 4, + out_channels=n_channels * 4, + exp_r=exp_r[6], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[6]) + ] + ) + + self.up_1 = MedNeXtUpBlock( + in_channels=4 * n_channels, + out_channels=2 * n_channels, + exp_r=exp_r[7], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.dec_block_1 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 2, + out_channels=n_channels * 2, + exp_r=exp_r[7], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[7]) + ] + ) + + self.up_0 = MedNeXtUpBlock( + in_channels=2 * n_channels, + out_channels=n_channels, + exp_r=exp_r[8], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.dec_block_0 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels, + out_channels=n_channels, + exp_r=exp_r[8], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + for i in range(block_counts[8]) + ] + ) + + self.out_0 = OutBlock(in_channels=n_channels, n_classes=n_classes, dim=dim) + + # Used to fix PyTorch checkpointing bug + self.dummy_tensor = nn.Parameter(torch.tensor([1.0]), requires_grad=True) + + if deep_supervision: + self.out_1 = OutBlock(in_channels=n_channels * 2, n_classes=n_classes, dim=dim) + self.out_2 = OutBlock(in_channels=n_channels * 4, n_classes=n_classes, dim=dim) + self.out_3 = OutBlock(in_channels=n_channels * 8, n_classes=n_classes, dim=dim) + self.out_4 = OutBlock(in_channels=n_channels * 16, n_classes=n_classes, dim=dim) + + self.block_counts = block_counts + + def iterative_checkpoint(self, sequential_block, x): + """ + This simply forwards x through each block of the sequential_block while + using gradient_checkpointing. This implementation is designed to bypass + the following issue in PyTorch's gradient checkpointing: + https://discuss.pytorch.org/t/checkpoint-with-no-grad-requiring-inputs-problem/19117/9 + """ + for l in sequential_block: + x = checkpoint.checkpoint(l, x, self.dummy_tensor) + return x + + def forward(self, x): + x = self.stem(x) + if self.outside_block_checkpointing: + x_res_0 = self.iterative_checkpoint(self.enc_block_0, x) + x = checkpoint.checkpoint(self.down_0, x_res_0, self.dummy_tensor) + x_res_1 = self.iterative_checkpoint(self.enc_block_1, x) + x = checkpoint.checkpoint(self.down_1, x_res_1, self.dummy_tensor) + x_res_2 = self.iterative_checkpoint(self.enc_block_2, x) + x = checkpoint.checkpoint(self.down_2, x_res_2, self.dummy_tensor) + x_res_3 = self.iterative_checkpoint(self.enc_block_3, x) + x = checkpoint.checkpoint(self.down_3, x_res_3, self.dummy_tensor) + + x = self.iterative_checkpoint(self.bottleneck, x) + if self.do_ds: + x_ds_4 = checkpoint.checkpoint(self.out_4, x, self.dummy_tensor) + + x_up_3 = checkpoint.checkpoint(self.up_3, x, self.dummy_tensor) + dec_x = x_res_3 + x_up_3 + x = self.iterative_checkpoint(self.dec_block_3, dec_x) + if self.do_ds: + x_ds_3 = checkpoint.checkpoint(self.out_3, x, self.dummy_tensor) + del x_res_3, x_up_3 + + x_up_2 = checkpoint.checkpoint(self.up_2, x, self.dummy_tensor) + dec_x = x_res_2 + x_up_2 + x = self.iterative_checkpoint(self.dec_block_2, dec_x) + if self.do_ds: + x_ds_2 = checkpoint.checkpoint(self.out_2, x, self.dummy_tensor) + del x_res_2, x_up_2 + + x_up_1 = checkpoint.checkpoint(self.up_1, x, self.dummy_tensor) + dec_x = x_res_1 + x_up_1 + x = self.iterative_checkpoint(self.dec_block_1, dec_x) + if self.do_ds: + x_ds_1 = checkpoint.checkpoint(self.out_1, x, self.dummy_tensor) + del x_res_1, x_up_1 + + x_up_0 = checkpoint.checkpoint(self.up_0, x, self.dummy_tensor) + dec_x = x_res_0 + x_up_0 + x = self.iterative_checkpoint(self.dec_block_0, dec_x) + del x_res_0, x_up_0, dec_x + + x = checkpoint.checkpoint(self.out_0, x, self.dummy_tensor) + + else: + x_res_0 = self.enc_block_0(x) + x = self.down_0(x_res_0) + x_res_1 = self.enc_block_1(x) + x = self.down_1(x_res_1) + x_res_2 = self.enc_block_2(x) + x = self.down_2(x_res_2) + x_res_3 = self.enc_block_3(x) + x = self.down_3(x_res_3) + + x = self.bottleneck(x) + if self.do_ds: + x_ds_4 = self.out_4(x) + + x_up_3 = self.up_3(x) + dec_x = x_res_3 + x_up_3 + x = self.dec_block_3(dec_x) + + if self.do_ds: + x_ds_3 = self.out_3(x) + del x_res_3, x_up_3 + + x_up_2 = self.up_2(x) + dec_x = x_res_2 + x_up_2 + x = self.dec_block_2(dec_x) + if self.do_ds: + x_ds_2 = self.out_2(x) + del x_res_2, x_up_2 + + x_up_1 = self.up_1(x) + dec_x = x_res_1 + x_up_1 + x = self.dec_block_1(dec_x) + if self.do_ds: + x_ds_1 = self.out_1(x) + del x_res_1, x_up_1 + + x_up_0 = self.up_0(x) + dec_x = x_res_0 + x_up_0 + x = self.dec_block_0(dec_x) + del x_res_0, x_up_0, dec_x + + x = self.out_0(x) + + if self.do_ds: + return [x, x_ds_1, x_ds_2, x_ds_3, x_ds_4] + else: + return x diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1_3d_to_2d.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1_3d_to_2d.py new file mode 100644 index 0000000..36243f2 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/MedNextV1_3d_to_2d.py @@ -0,0 +1,407 @@ +import torch +import torch.nn as nn +import torch.utils.checkpoint as checkpoint + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.blocks import ( + MedNeXtBlock, + MedNeXtDownBlock, + MedNeXtUpBlock, + OutBlock, +) + + +class MedNeXt3dto2d(nn.Module): + def __init__( + self, + in_channels: int, + n_channels: int, + n_classes: int, + exp_r: int = 4, # Expansion ratio as in Swin Transformers + kernel_size: int = 7, # Ofcourse can test kernel_size + enc_kernel_size: int = None, + dec_kernel_size: int = None, + deep_supervision: bool = False, # Can be used to test deep supervision + do_res: bool = False, # Can be used to individually test residual connection + do_res_up_down: bool = False, # Additional 'res' connection on up and down convs + checkpoint_style: bool = None, # Either inside block or outside block + block_counts: list = [2, 2, 2, 2, 2, 2, 2, 2, 2], # Can be used to test staging ratio: + # [3,3,9,3] in Swin as opposed to [2,2,2,2,2] in nnUNet + norm_type="group", + grn=False, + ): + super().__init__() + + self.do_ds = deep_supervision + assert checkpoint_style in [None, "outside_block"] + self.inside_block_checkpointing = False + self.outside_block_checkpointing = False + if checkpoint_style == "outside_block": + self.outside_block_checkpointing = True + if kernel_size is not None: + enc_kernel_size = kernel_size + dec_kernel_size = kernel_size + + self.stem = nn.Conv3d(in_channels, n_channels, kernel_size=1) + if type(exp_r) == int: + exp_r = [exp_r for i in range(len(block_counts))] + + self.enc_block_0 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels, + out_channels=n_channels, + exp_r=exp_r[0], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + for i in range(block_counts[0]) + ] + ) + + self.down_0 = MedNeXtDownBlock( + in_channels=n_channels, + out_channels=2 * n_channels, + exp_r=exp_r[1], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="3d", + ) + + self.enc_block_1 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 2, + out_channels=n_channels * 2, + exp_r=exp_r[1], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + for i in range(block_counts[1]) + ] + ) + + self.down_1 = MedNeXtDownBlock( + in_channels=2 * n_channels, + out_channels=4 * n_channels, + exp_r=exp_r[2], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + + self.enc_block_2 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 4, + out_channels=n_channels * 4, + exp_r=exp_r[2], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + for i in range(block_counts[2]) + ] + ) + + self.down_2 = MedNeXtDownBlock( + in_channels=4 * n_channels, + out_channels=8 * n_channels, + exp_r=exp_r[3], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + + self.enc_block_3 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 8, + out_channels=n_channels * 8, + exp_r=exp_r[3], + kernel_size=enc_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + for i in range(block_counts[3]) + ] + ) + + self.down_3 = MedNeXtDownBlock( + in_channels=8 * n_channels, + out_channels=16 * n_channels, + exp_r=exp_r[4], + kernel_size=enc_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + + self.bottleneck = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 16, + out_channels=n_channels * 16, + exp_r=exp_r[4], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="3d", + grn=grn, + ) + for i in range(block_counts[4]) + ] + ) + + self.up_3 = MedNeXtUpBlock( + in_channels=16 * n_channels, + out_channels=8 * n_channels, + exp_r=exp_r[5], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + + self.dec_block_3 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 8, + out_channels=n_channels * 8, + exp_r=exp_r[5], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + for i in range(block_counts[5]) + ] + ) + + self.up_2 = MedNeXtUpBlock( + in_channels=8 * n_channels, + out_channels=4 * n_channels, + exp_r=exp_r[6], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + + self.dec_block_2 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 4, + out_channels=n_channels * 4, + exp_r=exp_r[6], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + for i in range(block_counts[6]) + ] + ) + + self.up_1 = MedNeXtUpBlock( + in_channels=4 * n_channels, + out_channels=2 * n_channels, + exp_r=exp_r[7], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + + self.dec_block_1 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels * 2, + out_channels=n_channels * 2, + exp_r=exp_r[7], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + for i in range(block_counts[7]) + ] + ) + + self.up_0 = MedNeXtUpBlock( + in_channels=2 * n_channels, + out_channels=n_channels, + exp_r=exp_r[8], + kernel_size=dec_kernel_size, + do_res=do_res_up_down, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + + self.dec_block_0 = nn.Sequential( + *[ + MedNeXtBlock( + in_channels=n_channels, + out_channels=n_channels, + exp_r=exp_r[8], + kernel_size=dec_kernel_size, + do_res=do_res, + norm_type=norm_type, + dim="2d", + grn=grn, + ) + for i in range(block_counts[8]) + ] + ) + + self.out_0 = OutBlock(in_channels=n_channels, n_classes=n_classes, dim="2d") + + # Used to fix PyTorch checkpointing bug + self.dummy_tensor = nn.Parameter(torch.tensor([1.0]), requires_grad=True) + + if deep_supervision: + self.out_1 = OutBlock(in_channels=n_channels * 2, n_classes=n_classes, dim="2d") + self.out_2 = OutBlock(in_channels=n_channels * 4, n_classes=n_classes, dim="2d") + self.out_3 = OutBlock(in_channels=n_channels * 8, n_classes=n_classes, dim="2d") + self.out_4 = OutBlock(in_channels=n_channels * 16, n_classes=n_classes, dim="2d") + + self.block_counts = block_counts + + def iterative_checkpoint(self, sequential_block, x): + """ + This simply forwards x through each block of the sequential_block while + using gradient_checkpointing. This implementation is designed to bypass + the following issue in PyTorch's gradient checkpointing: + https://discuss.pytorch.org/t/checkpoint-with-no-grad-requiring-inputs-problem/19117/9 + """ + for l in sequential_block: + x = checkpoint.checkpoint(l, x, self.dummy_tensor) + return x + + def forward(self, x): + x = self.stem(x) + if self.outside_block_checkpointing: + x_res_0 = self.iterative_checkpoint(self.enc_block_0, x) + x = checkpoint.checkpoint(self.down_0, x_res_0, self.dummy_tensor) + x_res_1 = self.iterative_checkpoint(self.enc_block_1, x) + x = checkpoint.checkpoint(self.down_1, x_res_1, self.dummy_tensor) + x_res_2 = self.iterative_checkpoint(self.enc_block_2, x) + x = checkpoint.checkpoint(self.down_2, x_res_2, self.dummy_tensor) + x_res_3 = self.iterative_checkpoint(self.enc_block_3, x) + x = checkpoint.checkpoint(self.down_3, x_res_3, self.dummy_tensor) + + x = self.iterative_checkpoint(self.bottleneck, x) + if self.do_ds: + x_ds_4 = checkpoint.checkpoint(self.out_4, x, self.dummy_tensor) + + # Depth pool. + x_res_0 = x_res_0.max(axis=2)[0] + x_res_1 = x_res_1.max(axis=2)[0] + x_res_2 = x_res_2.max(axis=2)[0] + x_res_3 = x_res_3.max(axis=2)[0] + x = x.max(axis=2)[0] + + x_up_3 = checkpoint.checkpoint(self.up_3, x, self.dummy_tensor) + dec_x = x_res_3 + x_up_3 + x = self.iterative_checkpoint(self.dec_block_3, dec_x) + if self.do_ds: + x_ds_3 = checkpoint.checkpoint(self.out_3, x, self.dummy_tensor) + del x_res_3, x_up_3 + + x_up_2 = checkpoint.checkpoint(self.up_2, x, self.dummy_tensor) + dec_x = x_res_2 + x_up_2 + x = self.iterative_checkpoint(self.dec_block_2, dec_x) + if self.do_ds: + x_ds_2 = checkpoint.checkpoint(self.out_2, x, self.dummy_tensor) + del x_res_2, x_up_2 + + x_up_1 = checkpoint.checkpoint(self.up_1, x, self.dummy_tensor) + dec_x = x_res_1 + x_up_1 + x = self.iterative_checkpoint(self.dec_block_1, dec_x) + if self.do_ds: + x_ds_1 = checkpoint.checkpoint(self.out_1, x, self.dummy_tensor) + del x_res_1, x_up_1 + + x_up_0 = checkpoint.checkpoint(self.up_0, x, self.dummy_tensor) + dec_x = x_res_0 + x_up_0 + x = self.iterative_checkpoint(self.dec_block_0, dec_x) + del x_res_0, x_up_0, dec_x + + x = checkpoint.checkpoint(self.out_0, x, self.dummy_tensor) + + else: + x_res_0 = self.enc_block_0(x) + x = self.down_0(x_res_0) + x_res_1 = self.enc_block_1(x) + x = self.down_1(x_res_1) + x_res_2 = self.enc_block_2(x) + x = self.down_2(x_res_2) + x_res_3 = self.enc_block_3(x) + x = self.down_3(x_res_3) + + x = self.bottleneck(x) + if self.do_ds: + x_ds_4 = self.out_4(x) + + # Depth pool. + x_res_0 = x_res_0.max(axis=2)[0] + x_res_1 = x_res_1.max(axis=2)[0] + x_res_2 = x_res_2.max(axis=2)[0] + x_res_3 = x_res_3.max(axis=2)[0] + x = x.max(axis=2)[0] + + x_up_3 = self.up_3(x) + dec_x = x_res_3 + x_up_3 + x = self.dec_block_3(dec_x) + + if self.do_ds: + x_ds_3 = self.out_3(x) + del x_res_3, x_up_3 + + x_up_2 = self.up_2(x) + dec_x = x_res_2 + x_up_2 + x = self.dec_block_2(dec_x) + if self.do_ds: + x_ds_2 = self.out_2(x) + del x_res_2, x_up_2 + + x_up_1 = self.up_1(x) + dec_x = x_res_1 + x_up_1 + x = self.dec_block_1(dec_x) + if self.do_ds: + x_ds_1 = self.out_1(x) + del x_res_1, x_up_1 + + x_up_0 = self.up_0(x) + dec_x = x_res_0 + x_up_0 + x = self.dec_block_0(dec_x) + del x_res_0, x_up_0, dec_x + + x = self.out_0(x) + + if self.do_ds: + return [x, x_ds_1, x_ds_2, x_ds_3, x_ds_4] + else: + return x diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/__init__.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/__init__.py new file mode 100644 index 0000000..4cc7a0b --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/__init__.py @@ -0,0 +1,2 @@ +from .create_mednext_v1 import create_mednext_v1 +from .create_mednext_v1_3d_to_2d import create_mednext_v1_3d_to_2d diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/blocks.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/blocks.py new file mode 100644 index 0000000..ea521a7 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/blocks.py @@ -0,0 +1,284 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class MedNeXtBlock(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + exp_r: int = 4, + kernel_size: int = 7, + do_res: int = True, + norm_type: str = "group", + n_groups: int or None = None, + dim="3d", + grn=False, + ): + super().__init__() + + self.do_res = do_res + + assert dim in ["2d", "3d"] + self.dim = dim + if self.dim == "2d": + conv = nn.Conv2d + elif self.dim == "3d": + conv = nn.Conv3d + + # First convolution layer with DepthWise Convolutions + self.conv1 = conv( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=1, + padding=kernel_size // 2, + groups=in_channels if n_groups is None else n_groups, + ) + + # Normalization Layer. GroupNorm is used by default. + if norm_type == "group": + self.norm = nn.GroupNorm(num_groups=in_channels, num_channels=in_channels) + elif norm_type == "layer": + self.norm = LayerNorm(normalized_shape=in_channels, data_format="channels_first") + + # Second convolution (Expansion) layer with Conv3D 1x1x1 + self.conv2 = conv( + in_channels=in_channels, + out_channels=exp_r * in_channels, + kernel_size=1, + stride=1, + padding=0, + ) + + # GeLU activations + self.act = nn.GELU() + + # Third convolution (Compression) layer with Conv3D 1x1x1 + self.conv3 = conv( + in_channels=exp_r * in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + padding=0, + ) + + self.grn = grn + if grn: + if dim == "3d": + self.grn_beta = nn.Parameter( + torch.zeros(1, exp_r * in_channels, 1, 1, 1), requires_grad=True + ) + self.grn_gamma = nn.Parameter( + torch.zeros(1, exp_r * in_channels, 1, 1, 1), requires_grad=True + ) + elif dim == "2d": + self.grn_beta = nn.Parameter( + torch.zeros(1, exp_r * in_channels, 1, 1), requires_grad=True + ) + self.grn_gamma = nn.Parameter( + torch.zeros(1, exp_r * in_channels, 1, 1), requires_grad=True + ) + + def forward(self, x, dummy_tensor=None): + x1 = x + x1 = self.conv1(x1) + x1 = self.act(self.conv2(self.norm(x1))) + if self.grn: + # gamma, beta: learnable affine transform parameters + # X: input of shape (N,C,H,W,D) + if self.dim == "3d": + gx = torch.norm(x1, p=2, dim=(-3, -2, -1), keepdim=True) + elif self.dim == "2d": + gx = torch.norm(x1, p=2, dim=(-2, -1), keepdim=True) + nx = gx / (gx.mean(dim=1, keepdim=True) + 1e-6) + x1 = self.grn_gamma * (x1 * nx) + self.grn_beta + x1 + x1 = self.conv3(x1) + if self.do_res: + x1 = x + x1 + return x1 + + +class MedNeXtDownBlock(MedNeXtBlock): + def __init__( + self, + in_channels, + out_channels, + exp_r=4, + kernel_size=7, + do_res=False, + norm_type="group", + dim="3d", + grn=False, + ): + super().__init__( + in_channels, + out_channels, + exp_r, + kernel_size, + do_res=False, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + if dim == "2d": + conv = nn.Conv2d + elif dim == "3d": + conv = nn.Conv3d + self.resample_do_res = do_res + if do_res: + self.res_conv = conv( + in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=2 + ) + + self.conv1 = conv( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=2, + padding=kernel_size // 2, + groups=in_channels, + ) + + def forward(self, x, dummy_tensor=None): + x1 = super().forward(x) + + if self.resample_do_res: + res = self.res_conv(x) + x1 = x1 + res + + return x1 + + +class MedNeXtUpBlock(MedNeXtBlock): + def __init__( + self, + in_channels, + out_channels, + exp_r=4, + kernel_size=7, + do_res=False, + norm_type="group", + dim="3d", + grn=False, + ): + super().__init__( + in_channels, + out_channels, + exp_r, + kernel_size, + do_res=False, + norm_type=norm_type, + dim=dim, + grn=grn, + ) + + self.resample_do_res = do_res + + self.dim = dim + if dim == "2d": + conv = nn.ConvTranspose2d + elif dim == "3d": + conv = nn.ConvTranspose3d + if do_res: + self.res_conv = conv( + in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=2 + ) + + self.conv1 = conv( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=2, + padding=kernel_size // 2, + groups=in_channels, + ) + + def forward(self, x, dummy_tensor=None): + x1 = super().forward(x) + # Asymmetry but necessary to match shape + + if self.dim == "2d": + x1 = torch.nn.functional.pad(x1, (1, 0, 1, 0)) + elif self.dim == "3d": + x1 = torch.nn.functional.pad(x1, (1, 0, 1, 0, 1, 0)) + + if self.resample_do_res: + res = self.res_conv(x) + if self.dim == "2d": + res = torch.nn.functional.pad(res, (1, 0, 1, 0)) + elif self.dim == "3d": + res = torch.nn.functional.pad(res, (1, 0, 1, 0, 1, 0)) + x1 = x1 + res + + return x1 + + +class OutBlock(nn.Module): + def __init__(self, in_channels, n_classes, dim): + super().__init__() + + if dim == "2d": + conv = nn.ConvTranspose2d + elif dim == "3d": + conv = nn.ConvTranspose3d + self.conv_out = conv(in_channels, n_classes, kernel_size=1) + + def forward(self, x, dummy_tensor=None): + return self.conv_out(x) + + +class LayerNorm(nn.Module): + """LayerNorm that supports two data formats: channels_last (default) or channels_first. + The ordering of the dimensions in the inputs. channels_last corresponds to inputs with + shape (batch_size, height, width, channels) while channels_first corresponds to inputs + with shape (batch_size, channels, height, width). + """ + + def __init__(self, normalized_shape, eps=1e-5, data_format="channels_last"): + super().__init__() + self.weight = nn.Parameter(torch.ones(normalized_shape)) # beta + self.bias = nn.Parameter(torch.zeros(normalized_shape)) # gamma + self.eps = eps + self.data_format = data_format + if self.data_format not in ["channels_last", "channels_first"]: + raise NotImplementedError + self.normalized_shape = (normalized_shape,) + + def forward(self, x, dummy_tensor=False): + if self.data_format == "channels_last": + return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps) + elif self.data_format == "channels_first": + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None, None] * x + self.bias[:, None, None, None] + return x + + +if __name__ == "__main__": + # network = nnUNeXtBlock(in_channels=12, out_channels=12, do_res=False).cuda() + + # with torch.no_grad(): + # print(network) + # x = torch.zeros((2, 12, 8, 8, 8)).cuda() + # print(network(x).shape) + + # network = DownsampleBlock(in_channels=12, out_channels=24, do_res=False) + + # with torch.no_grad(): + # print(network) + # x = torch.zeros((2, 12, 128, 128, 128)) + # print(network(x).shape) + + network = MedNeXtBlock( + in_channels=12, out_channels=12, do_res=True, grn=True, norm_type="group" + ).cuda() + # network = LayerNorm(normalized_shape=12, data_format='channels_last').cuda() + # network.eval() + with torch.no_grad(): + print(network) + x = torch.zeros((2, 12, 64, 64, 64)).cuda() + print(network(x).shape) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1.py new file mode 100644 index 0000000..feb8b41 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1.py @@ -0,0 +1,80 @@ +from typing import Literal + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.MedNextV1 import ( + MedNeXt, +) + + +def create_mednextv1_small(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=2, + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[2, 2, 2, 2, 2, 2, 2, 2, 2], + ) + + +def create_mednextv1_base(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[2, 3, 4, 4, 4, 4, 4, 3, 2], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[2, 2, 2, 2, 2, 2, 2, 2, 2], + ) + + +def create_mednextv1_medium(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[2, 3, 4, 4, 4, 4, 4, 3, 2], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[3, 4, 4, 4, 4, 4, 4, 4, 3], + checkpoint_style="outside_block", + ) + + +def create_mednextv1_large(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[3, 4, 8, 8, 8, 8, 8, 4, 3], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[3, 4, 8, 8, 8, 8, 8, 4, 3], + checkpoint_style="outside_block", + ) + + +def create_mednext_v1( + num_input_channels: int, + num_classes: int, + model_id: Literal["S", "B", "M", "L"], + kernel_size: int = 3, + deep_supervision: bool = False, +): + model_dict = { + "S": create_mednextv1_small, + "B": create_mednextv1_base, + "M": create_mednextv1_medium, + "L": create_mednextv1_large, + } + + return model_dict[model_id](num_input_channels, num_classes, kernel_size, deep_supervision) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1_3d_to_2d.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1_3d_to_2d.py new file mode 100644 index 0000000..e429600 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/create_mednext_v1_3d_to_2d.py @@ -0,0 +1,80 @@ +from typing import Literal + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.mednext.MedNextV1_3d_to_2d import ( + MedNeXt3dto2d, +) + + +def create_mednextv1_3d_to_2d_small(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt3dto2d( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=2, + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[2, 2, 2, 2, 2, 2, 2, 2, 2], + ) + + +def create_mednextv1_3d_to_2d_base(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt3dto2d( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[2, 3, 4, 4, 4, 4, 4, 3, 2], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[2, 2, 2, 2, 2, 2, 2, 2, 2], + ) + + +def create_mednextv1_3d_to_2d_medium(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt3dto2d( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[2, 3, 4, 4, 4, 4, 4, 3, 2], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[3, 4, 4, 4, 4, 4, 4, 4, 3], + checkpoint_style="outside_block", + ) + + +def create_mednextv1_3d_to_2d_large(num_input_channels, num_classes, kernel_size=3, ds=False): + return MedNeXt3dto2d( + in_channels=num_input_channels, + n_channels=32, + n_classes=num_classes, + exp_r=[3, 4, 8, 8, 8, 8, 8, 4, 3], + kernel_size=kernel_size, + deep_supervision=ds, + do_res=True, + do_res_up_down=True, + block_counts=[3, 4, 8, 8, 8, 8, 8, 4, 3], + checkpoint_style="outside_block", + ) + + +def create_mednext_v1_3d_to_2d( + num_input_channels: int, + num_classes: int, + model_id: Literal["S", "B", "M", "L"], + kernel_size: int = 3, + deep_supervision: bool = False, +) -> MedNeXt3dto2d: + model_dict = { + "S": create_mednextv1_3d_to_2d_small, + "B": create_mednextv1_3d_to_2d_base, + "M": create_mednextv1_3d_to_2d_medium, + "L": create_mednextv1_3d_to_2d_large, + } + + return model_dict[model_id](num_input_channels, num_classes, kernel_size, deep_supervision) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/mednextv1_segformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/mednextv1_segformer.py new file mode 100644 index 0000000..c5b1e40 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mednext/mednextv1_segformer.py @@ -0,0 +1,44 @@ +from typing import Literal + +import torch.nn as nn +from transformers import SegformerForSemanticSegmentation + +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models import create_mednext_v1 + + +class MedNextV1Segformer(nn.Module): + def __init__( + self, + in_channels: int = 1, + mednextv1_model_id: Literal["S", "B", "M", "L"] = "M", + kernel_size: int = 3, + deep_supervision: bool = False, + dropout: float = 0.1, + segformer_model_size: int = 3, + ): + super().__init__() + self.encoder_3d = create_mednext_v1( + num_input_channels=in_channels, + num_classes=32, + model_id=mednextv1_model_id, + kernel_size=kernel_size, + deep_supervision=deep_supervision, + ) + self.dropout = nn.Dropout2d(dropout) + self.encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + f"nvidia/mit-b{segformer_model_size}", + num_labels=1, + ignore_mismatched_sizes=True, + num_channels=32, + ) + self.upscaler1 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + self.upscaler2 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + + def forward(self, image): + output = self.encoder_3d(image) + output = output.max(axis=2)[0] + output = self.dropout(output) + output = self.encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mlp.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mlp.py new file mode 100644 index 0000000..6fbcd34 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/mlp.py @@ -0,0 +1,30 @@ +import torch +import torch.nn as nn +import torchvision + + +class FlattenAndMLP(nn.Module): + def __init__( + self, input_dim: int, output_dim: int, hidden_dim: list[int] | None = None, **kwargs + ): + super().__init__() + if hidden_dim is not None: + hidden_channels = hidden_dim + [output_dim] + else: + hidden_channels = [output_dim] + + self.mlp = torchvision.ops.MLP( + in_channels=input_dim, hidden_channels=hidden_channels, **kwargs + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Flatten the input. + x = x.view(x.size(0), -1).contiguous() + + # Forward pass through the MLP + x = self.mlp(x) + + return x + + def __repr__(self): + return repr(self.mlp) diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/swin_unetr_segformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/swin_unetr_segformer.py new file mode 100644 index 0000000..cd56276 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/swin_unetr_segformer.py @@ -0,0 +1,38 @@ +import torch.nn as nn +from monai.networks.nets import SwinUNETR +from transformers import SegformerForSemanticSegmentation + + +class SwinUNETRSegformer(nn.Module): + def __init__( + self, + in_channels: int = 1, + img_size: tuple[int, int, int] = (32, 64, 64), + dropout: float = 0.1, + segformer_model_size: int = 5, + ): + super().__init__() + swin_unetr_out_channels = 32 + self.encoder_3d = SwinUNETR( + in_channels=in_channels, + out_channels=swin_unetr_out_channels, + img_size=img_size, + ) + self.dropout = nn.Dropout2d(dropout) + self.encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + f"nvidia/mit-b{segformer_model_size}", + num_labels=1, + ignore_mismatched_sizes=True, + num_channels=swin_unetr_out_channels, + ) + self.upscaler1 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + self.upscaler2 = nn.ConvTranspose2d(1, 1, kernel_size=(4, 4), stride=2, padding=1) + + def forward(self, image): + output = self.encoder_3d(image) + output = output.max(axis=2)[0] + output = self.dropout(output) + output = self.encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d.py new file mode 100644 index 0000000..920b611 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d.py @@ -0,0 +1,70 @@ +from torch import FloatTensor + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.unet_3d_to_2d import ( + AbstractUNet, + DoubleConv, + ResNetBlock, + ResNetBlockSE, +) + + +class UNet3D(AbstractUNet): + """ + 3DUnet model from + `"3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation" + `. + """ + + def __init__( + self, + in_channels: int = 1, + out_channels: int = 1, + final_sigmoid: bool = False, + f_maps: int | tuple[int, ...] = 64, + layer_order: str = "gcr", + num_groups: int = 8, + num_levels: int = 4, + is_segmentation: bool = False, + conv_padding: int | tuple[int, ...] = 1, + basic_module_type: str = "double_conv", + output_features: bool | None = None, + return_dict: bool | None = True, + ): + if basic_module_type == "double_conv": + basic_module_cls = DoubleConv + elif basic_module_type == "resnet": + basic_module_cls = ResNetBlock + elif basic_module_type == "resnet_se": + basic_module_cls = ResNetBlockSE + else: + raise ValueError( + f"Unknown basic_module_type: {basic_module_type}. Expected double_conv, resnet, or resnet_se." + ) + super().__init__( + in_channels=in_channels, + out_channels=out_channels, + final_sigmoid=final_sigmoid, + basic_module=basic_module_cls, + f_maps=f_maps, + layer_order=layer_order, + num_groups=num_groups, + num_levels=num_levels, + is_segmentation=is_segmentation, + conv_padding=conv_padding, + is3d_encoder=True, + is3d_decoder=True, + output_features=output_features, + return_dict=return_dict, + ) + + def decode(self, features: list[FloatTensor]) -> FloatTensor: + features = features[1:] # remove first skip with same spatial resolution + features = features[::-1] # reverse channels to start from head of encoder + + head = features[0] + skips = features[1:] + + x = head + for decoder_block, skip in zip(self.decoder_blocks, skips): + x = decoder_block(skip, x) + return x diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d_segformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d_segformer.py new file mode 100644 index 0000000..cde5bad --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unet3d_segformer.py @@ -0,0 +1,72 @@ +from typing import Literal + +import torch +import torch.nn as nn +from transformers import SegformerForSemanticSegmentation + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPoolingBlock, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.squeeze_and_excitation_3d import ( + SELayer3D, +) +from vesuvius_challenge_rnd.scroll_ink_detection.ink_detection.models.unet3d import UNet3D + + +class UNet3DSegformer(nn.Module): + def __init__( + self, + in_channels: int = 1, + img_size: tuple[int, int, int] = (32, 64, 64), + n_classes: int = 1, + unet_feature_size: int = 32, + unet_out_channels: int = 64, + unet_module_type: str = "resnet_se", + dropout: float = 0.1, + segformer_model_size: int = 5, + se_type_str: str | None = None, + depth_pool_fn: Literal["mean", "max", "attention"] = "max", + ): + super().__init__() + self.encoder_3d = UNet3D( + in_channels=in_channels, + out_channels=unet_out_channels, + basic_module_type=unet_module_type, + f_maps=unet_feature_size, + num_levels=5, + num_groups=8, + is_segmentation=False, + ) + self.pooler = DepthPoolingBlock( + input_channels=unet_out_channels, + se_type=SELayer3D[se_type_str] if se_type_str is not None else None, + reduction_ratio=2, + depth_dropout=0, + pool_fn=depth_pool_fn, + dim=2, + depth=img_size[0], + height=img_size[1], + width=img_size[2], + ) + self.dropout = nn.Dropout2d(dropout) + self.encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + f"nvidia/mit-b{segformer_model_size}", + num_labels=n_classes, + ignore_mismatched_sizes=True, + num_channels=unet_out_channels, + ) + self.upscaler1 = nn.ConvTranspose2d( + n_classes, n_classes, kernel_size=(4, 4), stride=2, padding=1 + ) + self.upscaler2 = nn.ConvTranspose2d( + n_classes, n_classes, kernel_size=(4, 4), stride=2, padding=1 + ) + + def forward(self, image: torch.FloatTensor) -> torch.FloatTensor: + output = self.encoder_3d(image).logits + output = self.pooler(output) + output = self.dropout(output) + output = self.encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unetr_segformer.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unetr_segformer.py new file mode 100644 index 0000000..4bcaa22 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/models/unetr_segformer.py @@ -0,0 +1,68 @@ +from typing import Literal + +import torch.nn as nn +from monai.networks.nets import UNETR +from transformers import SegformerForSemanticSegmentation + +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.depth_pooling import ( + DepthPoolingBlock, +) +from vesuvius_challenge_rnd.fragment_ink_detection.ink_detection.models.squeeze_and_excitation_3d import ( + SELayer3D, +) + + +class UNETRSegformer(nn.Module): + def __init__( + self, + in_channels: int = 1, + n_classes: int = 1, + img_size: tuple[int, int, int] = (30, 64, 64), + unetr_feature_size: int = 16, + dropout: float = 0.1, + segformer_model_size: int = 5, + se_type_str: str | None = None, + depth_pool_fn: Literal["mean", "max", "attention"] = "max", + ): + super().__init__() + unetr_out_channels = 32 + self.encoder_3d = UNETR( + in_channels=in_channels, + out_channels=unetr_out_channels, + img_size=img_size, + feature_size=unetr_feature_size, + conv_block=True, + ) + self.pooler = DepthPoolingBlock( + input_channels=unetr_out_channels, + se_type=SELayer3D[se_type_str] if se_type_str is not None else None, + reduction_ratio=2, + depth_dropout=0, + pool_fn=depth_pool_fn, + dim=2, + depth=img_size[0], + height=img_size[1], + width=img_size[2], + ) + self.dropout = nn.Dropout2d(dropout) + self.encoder_2d = SegformerForSemanticSegmentation.from_pretrained( + f"nvidia/mit-b{segformer_model_size}", + num_labels=n_classes, + ignore_mismatched_sizes=True, + num_channels=unetr_out_channels, + ) + self.upscaler1 = nn.ConvTranspose2d( + n_classes, n_classes, kernel_size=(4, 4), stride=2, padding=1 + ) + self.upscaler2 = nn.ConvTranspose2d( + n_classes, n_classes, kernel_size=(4, 4), stride=2, padding=1 + ) + + def forward(self, image): + output = self.encoder_3d(image) + output = self.pooler(output) + output = self.dropout(output) + output = self.encoder_2d(output).logits + output = self.upscaler1(output) + output = self.upscaler2(output) + return output diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/schedulers.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/schedulers.py new file mode 100644 index 0000000..051a725 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/schedulers.py @@ -0,0 +1,30 @@ +from warmup_scheduler import GradualWarmupScheduler + + +class GradualWarmupSchedulerV2(GradualWarmupScheduler): + """ + https://www.kaggle.com/code/underwearfitting/single-fold-training-of-resnet200d-lb0-965 + """ + + def __init__(self, optimizer, multiplier, total_epoch, after_scheduler=None): + super().__init__(optimizer, multiplier, total_epoch, after_scheduler) + + def get_lr(self): + if self.last_epoch > self.total_epoch: + if self.after_scheduler: + if not self.finished: + self.after_scheduler.base_lrs = [ + base_lr * self.multiplier for base_lr in self.base_lrs + ] + self.finished = True + return self.after_scheduler.get_lr() + return [base_lr * self.multiplier for base_lr in self.base_lrs] + if self.multiplier == 1.0: + return [ + base_lr * (float(self.last_epoch) / self.total_epoch) for base_lr in self.base_lrs + ] + else: + return [ + base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epoch + 1.0) + for base_lr in self.base_lrs + ] diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/transforms.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/transforms.py new file mode 100644 index 0000000..f9d19bd --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/transforms.py @@ -0,0 +1,46 @@ +from typing import Optional, Union + +from collections.abc import Sequence + +import torch +import torch.nn as nn + + +class RandFlip(nn.Module): + def __init__(self, p: float = 0.5, spatial_axis: Sequence[int] | int | None = None): + """ + Initialize the RandFlip module for single images. + + Parameters: + p (float): Probability of flipping an image. + spatial_axis (Sequence[int] | int | None): Spatial axes along which to flip. + """ + super().__init__() + self.p = p + self.spatial_axis = spatial_axis + + def normalize_axis(self, num_dims: int) -> list[int]: + """Normalize the spatial_axis to always be a list of positive integers.""" + if self.spatial_axis is None: + return list(range(num_dims)) + elif isinstance(self.spatial_axis, int): + return [self.spatial_axis % num_dims] + else: + return [axis % num_dims for axis in self.spatial_axis] + + def forward(self, img: torch.Tensor) -> torch.Tensor: + """ + Perform the random flip operation on a single image. + + Parameters: + img (Tensor): channel first array, must have shape: (num_channels, H[, W, ..., ]), + + Returns: + Tensor: Augmented tensor. + """ + should_flip = torch.rand(1).item() < self.p + if should_flip: + spatial_dims_to_flip = self.normalize_axis(img.dim()) + img = torch.flip(img, spatial_dims_to_flip) + + return img diff --git a/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/util.py b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/util.py new file mode 100644 index 0000000..5b85357 --- /dev/null +++ b/vesuvius_challenge_rnd/scroll_ink_detection/ink_detection/util.py @@ -0,0 +1,10 @@ +import numpy as np + + +def pad_to_match(a: np.ndarray, b: np.ndarray, pad_value: int = 0) -> np.ndarray: + """ + Pad the smaller of two arrays 'a' and 'b' so that they have the same shape. + """ + pad_y = max(a.shape[0], b.shape[0]) - a.shape[0] + pad_x = max(a.shape[1], b.shape[1]) - a.shape[1] + return np.pad(a, [(0, pad_y), (0, pad_x)], constant_values=pad_value) diff --git a/vesuvius_challenge_rnd/util.py b/vesuvius_challenge_rnd/util.py new file mode 100644 index 0000000..ba3c5d5 --- /dev/null +++ b/vesuvius_challenge_rnd/util.py @@ -0,0 +1,30 @@ +from pathlib import Path + +import wandb +from wandb.apis.public import Run + + +def get_wandb_artifact( + entity: str, + project: str, + artifact_name: str, + api: None | wandb.Api = None, + artifact_type: str | None = None, +) -> wandb.Artifact: + """Get a Weights and Biases Artifact.""" + if api is None: + api = wandb.Api() + return api.artifact(f"{entity}/{project}/{artifact_name}", type=artifact_type) + + +def get_wandb_run(entity: str, project: str, run_id: str, api: None | wandb.Api = None) -> Run: + """Get a Weights and Biases Run.""" + if api is None: + api = wandb.Api() + return api.run(f"{entity}/{project}/{run_id}") + + +def download_wandb_artifact(artifact: wandb.Artifact, output_dir: str | None = None) -> Path: + """Download a Weights and Biases Artifact to a local directory.""" + output_path = Path(artifact.download(root=output_dir)).resolve() + return output_path