From c7baf40afa63737b81301df0950c232ece349006 Mon Sep 17 00:00:00 2001 From: Joao P C Bertoldo <24547377+jpcbertoldo@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:03:24 +0200 Subject: [PATCH] metrics[1]: auc boxplot [GSoC 2023 @ OpenVINO] (#1294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add per-image overlap (pimo) * modif plot pimo curves * add warning about memory * tiny bug * add tuto ipynb * make image classes a return * fix ipynb * add tests for binclf curve * add test to binclf * add aupimo tests * ruff * Configure readthedocs via `.readthedocs.yaml` file (#1229) * Update binclf_curve.py * 🚚 Refactor Benchmarking Script (#1216) * New printing stuff * Remove dead code + address codacy issues * Refactor try/except + log to comet/wandb during runs * pre-commit error * third-party configuration --------- Co-authored-by: Ashwin Vaidya * Update CODEOWNERS * Enable training with only normal images for MVTec (#1241) * ignore mask check when dataset has only normal samples * update changelog * Revert "🚚 Refactor Benchmarking Script" (#1239) Revert "🚚 Refactor Benchmarking Script (#1216)" This reverts commit 784767fc2f19a8f354f152aba7f4338cb628118c. * Update benchmarking notebook (#1242) * Fix metadata path * Update benchmarking notebook * add per-image overlap (pimo) * modif plot pimo curves * add warning about memory * tiny bug * add tuto ipynb * make image classes a return * fix ipynb * add tests for binclf curve * add test to binclf * add aupimo tests * ruff * Update binclf_curve.py * refactor from future pr * add auc boxplot * Apply suggestions from code review * update demo nb * correct tests * add test * fix test * add plots tests * add tests to pimo * fix plt warning * fix docstring warning * add tests to common * add tests for plot module and small fixes * --amend * clear ouputs in notebook * correct typo * correct codacy stuff * correct codacy stuff * merge * fix kernel spec in 502_perimg_metrics.ipynb * fix types in boxplot * Update src/anomalib/utils/metrics/perimg/pimo.py Co-authored-by: Samet Akcay --------- Co-authored-by: Samet Akcay Co-authored-by: Ashwin Vaidya Co-authored-by: Ashwin Vaidya Co-authored-by: Dick Ameln --- .../500_use_cases/502_perimg_metrics.ipynb | 544 +++++++----------- src/anomalib/utils/metrics/perimg/common.py | 123 ++++ src/anomalib/utils/metrics/perimg/pimo.py | 92 ++- src/anomalib/utils/metrics/perimg/plot.py | 251 ++++++++ .../utils/metrics/test_perimg/test_common.py | 22 + .../utils/metrics/test_perimg/test_pimo.py | 32 +- .../utils/metrics/test_perimg/test_plot.py | 73 ++- 7 files changed, 770 insertions(+), 367 deletions(-) create mode 100644 tests/pre_merge/utils/metrics/test_perimg/test_common.py diff --git a/notebooks/500_use_cases/502_perimg_metrics.ipynb b/notebooks/500_use_cases/502_perimg_metrics.ipynb index 7ff4ba5b48..8454bb2e8a 100644 --- a/notebooks/500_use_cases/502_perimg_metrics.ipynb +++ b/notebooks/500_use_cases/502_perimg_metrics.ipynb @@ -21,106 +21,34 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: anomalib in /home/jcasagrandebertoldo/repos/anomalib/src (1.0.0.dev0)\n", - "Requirement already satisfied: albumentations>=1.1.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (1.3.1)\n", - "Requirement already satisfied: av>=10.0.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (10.0.0)\n", - "Requirement already satisfied: einops>=0.3.2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.6.1)\n", - "Requirement already satisfied: freia>=0.2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.2)\n", - "Requirement already satisfied: imgaug==0.4.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.4.0)\n", - "Requirement already satisfied: jsonargparse[signatures]>=4.3 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (4.23.0)\n", - "Requirement already satisfied: kornia<0.6.10,>=0.6.6 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.6.9)\n", - "Requirement already satisfied: matplotlib>=3.4.3 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (3.7.2)\n", - "Requirement already satisfied: omegaconf>=2.1.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (2.3.0)\n", - "Requirement already satisfied: opencv-python>=4.5.3.56 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (4.8.0.74)\n", - "Requirement already satisfied: pandas>=1.1.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (2.0.3)\n", - "Requirement already satisfied: pytorch-lightning<1.10.0,>=1.7.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (1.9.5)\n", - "Requirement already satisfied: timm<=0.6.12,>=0.5.4 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.6.12)\n", - "Requirement already satisfied: torchmetrics==0.10.3 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from anomalib) (0.10.3)\n", - "Requirement already satisfied: scikit-image>=0.14.2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (0.21.0)\n", - "Requirement already satisfied: Shapely in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (2.0.1)\n", - "Requirement already satisfied: numpy>=1.15 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (1.23.5)\n", - "Requirement already satisfied: Pillow in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (10.0.0)\n", - "Requirement already satisfied: six in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (1.16.0)\n", - "Requirement already satisfied: imageio in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (2.31.1)\n", - "Requirement already satisfied: scipy in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from imgaug==0.4.0->anomalib) (1.10.1)\n", - "Requirement already satisfied: packaging in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torchmetrics==0.10.3->anomalib) (23.1)\n", - "Requirement already satisfied: torch>=1.3.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torchmetrics==0.10.3->anomalib) (2.0.1)\n", - "Requirement already satisfied: qudida>=0.0.4 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from albumentations>=1.1.0->anomalib) (0.0.4)\n", - "Requirement already satisfied: opencv-python-headless>=4.1.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from albumentations>=1.1.0->anomalib) (4.8.0.74)\n", - "Requirement already satisfied: PyYAML in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from albumentations>=1.1.0->anomalib) (6.0.1)\n", - "Requirement already satisfied: typeshed-client>=2.1.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from jsonargparse[signatures]>=4.3->anomalib) (2.3.0)\n", - "Requirement already satisfied: docstring-parser>=0.15 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from jsonargparse[signatures]>=4.3->anomalib) (0.15)\n", - "Requirement already satisfied: fonttools>=4.22.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (4.41.1)\n", - "Requirement already satisfied: contourpy>=1.0.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (1.1.0)\n", - "Requirement already satisfied: python-dateutil>=2.7 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (2.8.2)\n", - "Requirement already satisfied: pyparsing<3.1,>=2.3.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (2.4.7)\n", - "Requirement already satisfied: cycler>=0.10 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (0.11.0)\n", - "Requirement already satisfied: kiwisolver>=1.0.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from matplotlib>=3.4.3->anomalib) (1.4.4)\n", - "Requirement already satisfied: antlr4-python3-runtime==4.9.* in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from omegaconf>=2.1.1->anomalib) (4.9.3)\n", - "Requirement already satisfied: tzdata>=2022.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pandas>=1.1.0->anomalib) (2023.3)\n", - "Requirement already satisfied: pytz>=2020.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pandas>=1.1.0->anomalib) (2023.3)\n", - "Requirement already satisfied: lightning-utilities>=0.6.0.post0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pytorch-lightning<1.10.0,>=1.7.0->anomalib) (0.9.0)\n", - "Requirement already satisfied: typing-extensions>=4.0.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pytorch-lightning<1.10.0,>=1.7.0->anomalib) (4.7.1)\n", - "Requirement already satisfied: tqdm>=4.57.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pytorch-lightning<1.10.0,>=1.7.0->anomalib) (4.65.0)\n", - "Requirement already satisfied: fsspec[http]>2021.06.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from pytorch-lightning<1.10.0,>=1.7.0->anomalib) (2023.6.0)\n", - "Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (3.8.5)\n", - "Requirement already satisfied: requests in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (2.31.0)\n", - "Requirement already satisfied: yarl<2.0,>=1.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (1.9.2)\n", - "Requirement already satisfied: multidict<7.0,>=4.5 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (6.0.4)\n", - "Requirement already satisfied: aiosignal>=1.1.2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (1.3.1)\n", - "Requirement already satisfied: charset-normalizer<4.0,>=2.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (3.2.0)\n", - "Requirement already satisfied: frozenlist>=1.1.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (1.4.0)\n", - "Requirement already satisfied: async-timeout<5.0,>=4.0.0a3 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (4.0.2)\n", - "Requirement already satisfied: attrs>=17.3.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (22.1.0)\n", - "Requirement already satisfied: scikit-learn>=0.19.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from qudida>=0.0.4->albumentations>=1.1.0->anomalib) (1.3.0)\n", - "Requirement already satisfied: tifffile>=2022.8.12 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-image>=0.14.2->imgaug==0.4.0->anomalib) (2023.7.18)\n", - "Requirement already satisfied: networkx>=2.8 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-image>=0.14.2->imgaug==0.4.0->anomalib) (2.8.2)\n", - "Requirement already satisfied: lazy_loader>=0.2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-image>=0.14.2->imgaug==0.4.0->anomalib) (0.3)\n", - "Requirement already satisfied: PyWavelets>=1.1.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-image>=0.14.2->imgaug==0.4.0->anomalib) (1.4.1)\n", - "Requirement already satisfied: threadpoolctl>=2.0.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-learn>=0.19.1->qudida>=0.0.4->albumentations>=1.1.0->anomalib) (3.2.0)\n", - "Requirement already satisfied: joblib>=1.1.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from scikit-learn>=0.19.1->qudida>=0.0.4->albumentations>=1.1.0->anomalib) (1.3.1)\n", - "Requirement already satisfied: huggingface-hub in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from timm<=0.6.12,>=0.5.4->anomalib) (0.16.4)\n", - "Requirement already satisfied: torchvision in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from timm<=0.6.12,>=0.5.4->anomalib) (0.15.2)\n", - "Requirement already satisfied: nvidia-cusolver-cu11==11.4.0.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.4.0.1)\n", - "Requirement already satisfied: nvidia-cuda-nvrtc-cu11==11.7.99 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.7.99)\n", - "Requirement already satisfied: nvidia-cuda-runtime-cu11==11.7.99 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.7.99)\n", - "Requirement already satisfied: nvidia-nccl-cu11==2.14.3 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (2.14.3)\n", - "Requirement already satisfied: sympy in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (1.12)\n", - "Requirement already satisfied: nvidia-cuda-cupti-cu11==11.7.101 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.7.101)\n", - "Requirement already satisfied: filelock in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (3.12.2)\n", - "Requirement already satisfied: triton==2.0.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (2.0.0)\n", - "Requirement already satisfied: nvidia-cudnn-cu11==8.5.0.96 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (8.5.0.96)\n", - "Requirement already satisfied: nvidia-cublas-cu11==11.10.3.66 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.10.3.66)\n", - "Requirement already satisfied: nvidia-cufft-cu11==10.9.0.58 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (10.9.0.58)\n", - "Requirement already satisfied: nvidia-cusparse-cu11==11.7.4.91 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.7.4.91)\n", - "Requirement already satisfied: nvidia-nvtx-cu11==11.7.91 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (11.7.91)\n", - "Requirement already satisfied: nvidia-curand-cu11==10.2.10.91 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (10.2.10.91)\n", - "Requirement already satisfied: jinja2 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from torch>=1.3.1->torchmetrics==0.10.3->anomalib) (3.1.2)\n", - "Requirement already satisfied: wheel in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (0.38.4)\n", - "Requirement already satisfied: setuptools in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from nvidia-cublas-cu11==11.10.3.66->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (57.4.0)\n", - "Requirement already satisfied: lit in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from triton==2.0.0->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (16.0.6)\n", - "Requirement already satisfied: cmake in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from triton==2.0.0->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (3.27.0)\n", - "Requirement already satisfied: importlib-resources>=1.4.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from typeshed-client>=2.1.0->jsonargparse[signatures]>=4.3->anomalib) (5.2.0)\n", - "Requirement already satisfied: idna>=2.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from yarl<2.0,>=1.0->aiohttp!=4.0.0a0,!=4.0.0a1->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (3.4)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from jinja2->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (2.1.1)\n", - "Requirement already satisfied: certifi>=2017.4.17 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from requests->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (2023.7.22)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from requests->fsspec[http]>2021.06.0->pytorch-lightning<1.10.0,>=1.7.0->anomalib) (2.0.4)\n", - "Requirement already satisfied: mpmath>=0.19 in /home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages (from sympy->torch>=1.3.1->torchmetrics==0.10.3->anomalib) (1.3.0)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "%pip install anomalib" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# make a cell print all the outputs instead of just the last one\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "\n", + "InteractiveShell.ast_node_interactivity = \"all\"" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -136,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -156,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -174,17 +102,9 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Image Shape: torch.Size([32, 3, 256, 256]) Mask Shape: torch.Size([32, 256, 256])\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from anomalib.data.mvtec import MVTec\n", "\n", @@ -220,87 +140,9 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/torch/cuda/__init__.py:546: UserWarning: Can't initialize NVML\n", - " warnings.warn(\"Can't initialize NVML\")\n", - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:36: UserWarning: Metric `PrecisionRecallCurve` will save all targets and predictions in buffer. For large datasets this may lead to large memory footprint.\n", - " warnings.warn(*args, **kwargs)\n", - "FeatureExtractor is deprecated. Use TimmFeatureExtractor instead. Both FeatureExtractor and TimmFeatureExtractor will be removed in a future release.\n", - "GPU available: False, used: False\n", - "TPU available: False, using: 0 TPU cores\n", - "IPU available: False, using: 0 IPUs\n", - "HPU available: False, using: 0 HPUs\n", - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/pytorch_lightning/trainer/connectors/logger_connector/logger_connector.py:67: UserWarning: Starting from v1.9.0, `tensorboardX` has been removed as a dependency of the `pytorch_lightning` package, due to potential conflicts with other packages in the ML ecosystem. For this reason, `logger=True` will use `CSVLogger` as the default logger, unless the `tensorboard` or `tensorboardX` packages are found. Please `pip install lightning[extra]` or one of them to enable TensorBoard support by default\n", - " warning_cache.warn(\n", - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/pytorch_lightning/core/optimizer.py:183: UserWarning: `LightningModule.configure_optimizers` returned `None`, this fit will run with no optimizer\n", - " rank_zero_warn(\n", - "\n", - " | Name | Type | Params\n", - "-------------------------------------------------------------\n", - "0 | image_threshold | AnomalyScoreThreshold | 0 \n", - "1 | pixel_threshold | AnomalyScoreThreshold | 0 \n", - "2 | model | PadimModel | 683 K \n", - "3 | image_metrics | AnomalibMetricCollection | 0 \n", - "4 | pixel_metrics | AnomalibMetricCollection | 0 \n", - "-------------------------------------------------------------\n", - "683 K Trainable params\n", - "0 Non-trainable params\n", - "683 K Total params\n", - "2.732 Total estimated model params size (MB)\n", - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/pytorch_lightning/trainer/trainer.py:1609: PossibleUserWarning: The number of training batches (13) is smaller than the logging interval Trainer(log_every_n_steps=50). Set a lower value for log_every_n_steps if you want to see logs for the training epoch.\n", - " rank_zero_warn(\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "50512e08fe424e5798ecae08f54b0dc8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Training: 0it [00:00, ?it/s]" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/pytorch_lightning/loops/optimization/optimizer_loop.py:138: UserWarning: `training_step` returned `None`. If this was on purpose, ignore this warning...\n", - " self.warning_cache.warn(\"`training_step` returned `None`. If this was on purpose, ignore this warning...\")\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "50bef6c851b343299e00f2c8d7ec5fd8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: 0it [00:00, ?it/s]" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "`Trainer.fit` stopped: `max_epochs=1` reached.\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from pytorch_lightning import Trainer\n", "\n", @@ -346,17 +188,9 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "anomaly_maps.shape=torch.Size([110, 256, 256]) masks.shape=torch.Size([110, 256, 256])\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import torch\n", "\n", @@ -384,48 +218,9 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:36: UserWarning: Metric `ROC` will save all targets and predictions in buffer. For large datasets this may lead to large memory footprint.\n", - " warnings.warn(*args, **kwargs)\n", - "/home/jcasagrandebertoldo/miniconda3/envs/anomalib-dev-gsoc/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:36: UserWarning: Metric `PrecisionRecallCurve` will save all targets and predictions in buffer. For large datasets this may lead to large memory footprint.\n", - " warnings.warn(*args, **kwargs)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AUROC()=0.9798059463500977\n", - "AUPR()=0.5285573601722717\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHHCAYAAABTMjf2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABRUElEQVR4nO3deVwV9f7H8ddhO4AIosiiouRuau65pWaRdDXLupZp5XIrb2XL1fpdtUWzRcvMvKXlzTTrZmqWlluaUbZalkplKeWWKyipbCrr/P4YBY6gAgJzYN7Px2MenO+cmTkfPHh4853vfMdhGIaBiIiIiA15WF2AiIiIiFUUhERERMS2FIRERETEthSERERExLYUhERERMS2FIRERETEthSERERExLYUhERERMS2FIRERETEthSERERExLYUhETELbz66qs4HA46d+5c6Lk9e/bgcDiYNm1akftOmzYNh8PBnj178tZdeeWVOByOvKVmzZp06tSJefPmkZubm7fd8OHDXbZzOp00bdqUCRMmcOrUqTL/PkXEvXhZXYCICMCCBQuIiopi48aN7Nixg8aNG1/0MevVq8eUKVMAOHLkCG+//TZ33nknv//+O88991zedk6nkzfeeAOA5ORkPvroI55++ml27tzJggULLroOEXFf6hESEcvt3r2bb7/9lunTp1O7du0yCx9BQUHcfvvt3H777YwePZpvvvmGevXqMXPmTLKysvK28/Lyyttu1KhRrF27li5durBw4UISExPLpBYRcU8KQiJiuQULFhAcHEy/fv0YOHBgufXC+Pv706VLF9LT0zly5Mg5t3M4HFxxxRUYhsGuXbvKpRYRcQ8KQiJiuQULFnDTTTfh4+PD4MGD+eOPP/jhhx/K5bV27dqFp6cnNWrUOO92Z8YbBQcHl0sdIuIeNEZIRCy1adMmtm/fziuvvALAFVdcQb169ViwYAGdOnW6qGPn5OSQlJQEQFJSEq+99hqbN2+mf//++Pv7u2x7Zrvk5GQ+/PBDPvjgA1q1akWzZs0uqgYRcW8KQiJiqQULFhAWFkbv3r0B87TUoEGDeOedd3jxxRfx9PQs9bG3b99O7dq189oOh4N+/foxb948l+3S09NdtgMzkL311ls4HI5Sv76IuD8FIRGxTE5ODosWLaJ3797s3r07b33nzp158cUXiY2NpU+fPsU+3tmh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wdfX1+pypAyd773N+/2dnJw3jqos2HceITRGSERExO5sHIRs1REmIiIiRbBvELLXGUEREREpgn2DkHqEREREbM++QUg9QiIilZrNrvWxBSveU/sGIQ2WFhGplM5M+HfixAmLK5GylpmZCYCnp2eFvaZ9J1TUXxIiIpWSp6cnNWrU4PDhwwD4+/sX+55b4r5yc3M5cuQI/v7+eHlVXDyxbxDSGCERkUrrzP2vzoQhqRo8PDyoX79+hQZb+wYh9QiJiFRaDoeDiIgIQkNDycrKsrocKSM+Pj54eFTsqB0bByGNERIRqew8PT0rdDyJVD02HiytHiERERG7s28Q0qkxERER27NvEFKPkIiIiO3ZNwipR0hERMT27BuE1CMkIiJie/YNQuoREhERsT37BiH1CImIiNiefYOQ5hESERGxPRsHIfUIiYiI2J19g5BOjYmIiNiefYOQeoRERERsz75BCI0REhERsTv7BiH1CImIiNiefYOQxgiJiIjYnn2DkHqEREREbM/GQUhjhEREROzOvkFIp8ZERERsz75BSKfGREREbM++QUhERERsz8ZBSD1CIiIidmffIKTB0iIiIrZn4yCkHiERERG7s28Q0qkxERER21MQEhEREduybxDSGCERERHbs28QUo+QiIiI7SkIiYiIiG3ZNwjpqjERERHbs3EQ0hghERERu7NvENKpMREREduzbxDSqTERERHbs28QUo+QiIiI7dk3CKlHSERExPbsG4RERETE9uwdhNQrJCIiYmuWB6FZs2YRFRWFr68vnTt3ZuPGjefdfsaMGTRr1gw/Pz8iIyMZPXo0p06dKuWrKwiJiIjYmaVBaPHixYwZM4aJEyeyefNm2rRpQ0xMDIcPHy5y+3fffZdx48YxceJEtm3bxty5c1m8eDGPPvpo6QrQXEIiIiK2ZmkQmj59OnfffTcjRozg0ksvZfbs2fj7+zNv3rwit//222/p3r07Q4YMISoqij59+jB48OAL9iKdk06NiYiI2JplQSgzM5NNmzYRHR2dX4yHB9HR0WzYsKHIfbp168amTZvygs+uXbtYvXo1ffv2PefrZGRkkJKS4rLkUxASERGxMy+rXjgpKYmcnBzCwsJc1oeFhbF9+/Yi9xkyZAhJSUlcccUVGIZBdnY299xzz3lPjU2ZMoVJkyYV/aR6hERERGzN8sHSJbF+/Xom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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import torch\n", "from anomalib.utils.metrics import AUROC, AUPR\n", @@ -445,65 +240,49 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Area Under the Per-Image Overlap (AUPImO)" + "# `AUPImO` (init, update, compute)\n", + "\n", + "Area Under the Per-Image Overlap (`AUPImO`) \n", + "\n", + "Let's instantiate, load the data, then compute PImO curves and their AUCs (AUPImO scores)." ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/jcasagrandebertoldo/repos/anomalib/src/anomalib/utils/metrics/perimg/binclf_curve.py:350: UserWarning: Metric `AUPImO` will save all targets and predictions in buffer. For large datasets this may lead to large memory footprint.\n", - " warnings.warn(\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ + "%%time\n", + "%autoreload 2\n", + "\n", "from anomalib.utils.metrics.perimg import AUPImO\n", "\n", "aupimo = AUPImO()\n", "aupimo.cpu()\n", - "aupimo.update(anomaly_maps, masks)" + "aupimo.update(anomaly_maps, masks)\n", + "\n", + "pimoresult, aucs = aupimo.compute()\n", + "(thresholds, fprs, shared_fpr, tprs, image_classes) = pimoresult" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "The AUPImO has a shared X-axis and a per-image Y-axis.\n", - "\n", - "The X-axis is a metric of False Positives in the normal images.\n", - "\n", - "The Y-axis is the overlap between the binary predicted mask and the ground truth mask of anomalous images, which corresponds to the True Positive Rate (TPR) in a single image." + "pimoresult?" ] }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "thresholds.shape=torch.Size([10000])\n", - "shared_fpr.shape=torch.Size([10000])\n", - "tprs.shape=torch.Size([110, 10000])\n", - "image_classes.shape=torch.Size([110])\n", - "aucs.shape=torch.Size([110])\n", - "CPU times: user 66 µs, sys: 0 ns, total: 66 µs\n", - "Wall time: 65.8 µs\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "%%time\n", - "(thresholds, fprs, shared_fpr, tprs, image_classes), aucs = aupimo.compute()\n", "print(f\"{thresholds.shape=}\")\n", + "print(f\"{fprs.shape=}\")\n", "print(f\"{shared_fpr.shape=}\")\n", "print(f\"{tprs.shape=}\")\n", "print(f\"{image_classes.shape=}\")\n", @@ -514,104 +293,177 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Notice that `aucs` has the number of images seen in `outputs`." + "# `PImO` curves (plot)\n", + "\n", + "The PImO curve has a shared X-axis and a per-image Y-axis.\n", + "\n", + "The X-axis:\n", + "- is a metric of False Positives only in the normal images (here it is the set-FPR)\n", + "- is shared by all image instances\n", + "\n", + "The Y-axis: \n", + "- is the **overlap** between the binary predicted mask and the ground truth mask, which corresponds to the True Positive Rate (TPR) in a single image\n", + "- has one value per image, so there is one PImO curve per image. " ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([110, 256, 256])" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "masks.shape" + "aupimo.plot_all_pimo_curves()\n", + "# TODO add functional interface" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "which includes normal images.\n", + "# `AUPImO` = AUC(`PImO`)\n", "\n", - "However, the `Per-Image Overlap`, by definition, is not defined on the normal images, so they have `nan`s." + "The Area Under the Curve (AUC) is, by consequence, computed for each image, which will be used as is score.\n", + "\n", + "Notice that `aucs` has the number of images seen in `outputs` (cf. `masks` below).\n", + "\n", + "`aucs` has `nan` values for the normal images because the `Per-Image Overlap`, by definition, is not defined on them (they do not have any positive/anomalous pixels).\n", + "\n", + "This is done by design choice so the indexes in `aucs` correspond to the indices of the actual images." ] }, { "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "tensor([0.9939, 0.9957, 0.9823, 0.9893, 0.9966, 0.9928, 0.9915, 0.9792, 0.9972,\n", - " 0.9833, 0.9664, 0.9917, 0.9630, 0.9794, 0.9909, 0.9828, 0.9929, 0.9745,\n", - " 0.9964, 0.9890, 0.9951, 0.9958, 0.9906, 0.9963, 0.9921, 0.9951, 0.9793,\n", - " 0.9921, 0.9973, 0.9968, 0.9974, 0.9941, 0.9830, 0.9950, 0.9954, nan,\n", - " nan, nan, nan, nan, nan, nan, nan, nan, nan,\n", - " nan, nan, nan, nan, nan, nan, nan, nan, nan,\n", - " nan, nan, nan, nan, nan, nan, nan, nan, nan,\n", - " nan, nan, nan, nan, nan, nan, nan, nan, nan,\n", - " nan, nan, nan, 0.9984, 0.9994, 0.9991, 0.9990, 0.9990, 0.9984,\n", - " 0.9884, 0.9999, 0.9980, 0.9978, 0.9955, 0.9848, 0.9899, 0.9956, 0.9776,\n", - " 0.9993, 0.9729, 0.9966, 0.9987, 0.9991, 0.9993, 0.9938, 0.9990, 0.9898,\n", - " 0.9993, 0.9982, 0.9995, 0.9976, 0.9991, 0.9978, 0.9987, 0.9931, 0.9964,\n", - " 0.9974, 0.9982], dtype=torch.float64)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aucs" + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"{masks.shape[0]=} == {aucs.shape[0]=}\")\n", + "print(aucs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is intentional. This way, the indexes in `aucs` correspond to the indices of the actual images." + "# `AUPImO` distribuion (boxplot)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "One can now analyze the destribution of this True Posivity metric across images:" + "One can now analyze the distribution of this True Posivity metric across images and take statistics from the test set (e.g. with `sp.stats.describe`).\n", + "\n", + "`AUPImO` has an integrated feature to plot a boxplot from the distribution and inspect representative cases using its statistics." ] }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DescribeResult(nobs=70, minmax=(0.9629851726316055, 0.9999250647309539), mean=0.9924038034625862, variance=6.711429900129006e-05, skewness=-1.6850158197600387, kurtosis=2.4583862579200915)" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import scipy as sp\n", "\n", - "sp.stats.describe(aucs[~torch.isnan(aucs)])" + "print(sp.stats.describe(aucs[~torch.isnan(aucs)])) # `~torch.isnan(aucs)` is removing the `nan`s\n", + "aupimo.plot_boxplot()\n", + "aupimo.boxplot_stats()[-3:]\n", + "# TODO add functional interface" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Representative samples (curve + boxplot)\n", + "\n", + "The two plots (`PImO` curve + `AUPImO` boxplot) are combined with the method `AUPImO.plot()`.\n", + "\n", + "The `PImO` curves are plot only for the samples that correspond to the boxplot's statistics (see `AUPImO.boxplot_stats()`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aupimo.plot()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Appendix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# scrathc/cache (please ignore this section)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "del AUPImO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "(CACHE := Path.home() / \".cache\").mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "torch.save(anomaly_maps, CACHE / \"anomaly_maps.pt\")\n", + "torch.save(masks, CACHE / \"masks.pt\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "(CACHE := Path.home() / \".cache\").mkdir(exist_ok=True)\n", + "import torch\n", + "\n", + "anomaly_maps = torch.load(CACHE / \"anomaly_maps.pt\")\n", + "masks = torch.load(CACHE / \"masks.pt\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload" ] }, { @@ -624,9 +476,9 @@ ], "metadata": { "kernelspec": { - "display_name": "anomalib-dev-gsoc", + "display_name": "anomalib", "language": "python", - "name": "anomalib-dev-gsoc" + "name": "python3" }, "language_info": { "codemirror_mode": { diff --git a/src/anomalib/utils/metrics/perimg/common.py b/src/anomalib/utils/metrics/perimg/common.py index af85d00076..07fc987622 100644 --- a/src/anomalib/utils/metrics/perimg/common.py +++ b/src/anomalib/utils/metrics/perimg/common.py @@ -1,5 +1,7 @@ from __future__ import annotations +import matplotlib as mpl +import numpy import torch from torch import Tensor @@ -104,6 +106,36 @@ def _validate_image_classes(image_classes: Tensor): ) +def _validate_aucs(aucs: Tensor, nan_allowed: bool = False): + if not isinstance(aucs, Tensor): + raise ValueError(f"Expected argument `aucs` to be a Tensor, but got {type(aucs)}.") + + if aucs.ndim != 1: + raise ValueError(f"Expected argument `aucs` to be a 1D tensor, but got {aucs.ndim}D tensor.") + + if not torch.is_floating_point(aucs): + raise ValueError(f"Expected argument `aucs` to have dtype float, but got {aucs.dtype}.") + + valid_aucs = aucs[~torch.isnan(aucs)] if nan_allowed else aucs + + if torch.any((valid_aucs < 0) | (valid_aucs > 1)): + raise ValueError("Expected argument `aucs` to be in [0, 1], but got values outside this range.") + + +def _validate_image_class(image_class: int | None): + if image_class is None: + return + + if not isinstance(image_class, int): + raise ValueError(f"Expected argument `image_class` to be either None or an int, but got {type(image_class)}.") + + if image_class not in (0, 1): + raise ValueError( + "Expected argument `image_class` to be either 0, 1 or None (respec., 'normal', 'anomalous', or 'both') " + f"but got {image_class}." + ) + + def _validate_atleast_one_anomalous_image(image_classes: Tensor): if (image_classes == 1).sum() == 0: raise ValueError("Expected argument at least one anomalous image, but found none.") @@ -112,3 +144,94 @@ def _validate_atleast_one_anomalous_image(image_classes: Tensor): def _validate_atleast_one_normal_image(image_classes: Tensor): if (image_classes == 0).sum() == 0: raise ValueError("Expected argument at least one normal image, but found none.") + + +# =========================================== FUNCTIONAL =========================================== + + +def _perimg_boxplot_stats( + values: Tensor, image_classes: Tensor, only_class: int | None = None +) -> list[dict[str, str | int | float | None]]: + """Compute boxplot statistics for a given tensor of values. + + This function uses `matplotlib.cbook.boxplot_stats`, which is the same function used by `matplotlib.pyplot.boxplot`. + + Args: + values (Tensor): Tensor of per-image values. + image_classes (Tensor): Tensor of image classes. + only_class (int | None): If not None, only compute statistics for images of the given class. + None means both image classes are used. Defaults to None. + + Returns: + list[dict[str, str | int | float | None]]: List of boxplot statistics. + Each dictionary has the following keys: + - 'statistic': Name of the statistic. + - 'value': Value of the statistic (same units as `values`). + - 'nearest': Some statistics (e.g. 'mean') are not guaranteed to be in the tensor, so this is the + closest to the statistic in an actual image (i.e. in `values`). + - 'imgidx': Index of the image in `values` that has the `nearest` value to the statistic. + """ + + _validate_image_classes(image_classes) + _validate_image_class(only_class) + + if values.ndim != 1: + raise ValueError(f"Expected argument `values` to be a 1D tensor, but got {values.ndim}D tensor.") + + if values.shape != image_classes.shape: + raise ValueError( + "Expected arguments `values` and `image_classes` to have the same shape, " + f"but got {values.shape} and {image_classes.shape}." + ) + + if only_class is not None and only_class not in image_classes: + raise ValueError(f"Argument `only_class` is {only_class}, but `image_classes` does not contain this class.") + + # convert to numpy because of `matplotlib.cbook.boxplot_stats` + values = values.cpu().numpy() + image_classes = image_classes.cpu().numpy() + + # only consider images of the given class + imgs_mask = numpy.ones_like(image_classes, dtype=bool) if only_class is None else (image_classes == only_class) + values = values[imgs_mask] + imgs_idxs = numpy.nonzero(imgs_mask)[0] + + def arg_find_nearest(stat_value): + return (numpy.abs(values - stat_value)).argmin() + + # function used in `matplotlib.boxplot` + boxplot_stats = mpl.cbook.boxplot_stats(values)[0] # [0] is for the only boxplot + + records = [] + + def append_record(stat_, val_): + # make sure to use a value that is actually in the array + # because some statistics (e.g. 'mean') are not guaranteed to be in the array + invalues_idx = arg_find_nearest(val_) + nearest = values[invalues_idx] + imgidx = imgs_idxs[invalues_idx] + records.append( + dict( + statistic=stat_, + value=float(val_), + nearest=float(nearest), + imgidx=int(imgidx), + ) + ) + + for stat, val in boxplot_stats.items(): + if stat in ("iqr", "cilo", "cihi"): + continue + + elif stat != "fliers": + append_record(stat, val) + continue + + for val_ in val: + append_record( + "flierhi" if val_ > boxplot_stats["med"] else "flierlo", + val_, + ) + + records = sorted(records, key=lambda r: r["value"]) + return records diff --git a/src/anomalib/utils/metrics/perimg/pimo.py b/src/anomalib/utils/metrics/perimg/pimo.py index 66e33ec5e2..50adc75bce 100644 --- a/src/anomalib/utils/metrics/perimg/pimo.py +++ b/src/anomalib/utils/metrics/perimg/pimo.py @@ -17,6 +17,7 @@ from collections import namedtuple +import matplotlib.pyplot as plt import torch from matplotlib.axes import Axes from matplotlib.pyplot import Figure @@ -25,11 +26,14 @@ from .binclf_curve import PerImageBinClfCurve from .common import ( + _perimg_boxplot_stats, _validate_atleast_one_anomalous_image, _validate_atleast_one_normal_image, ) from .plot import ( plot_all_pimo_curves, + plot_aupimo_boxplot, + plot_boxplot_pimo_curves, ) # =========================================== METRICS =========================================== @@ -47,10 +51,10 @@ PImOResult.__doc__ = """PImO result (from `PImO.compute()`). [0] thresholds: shape (num_thresholds,), a `float` dtype as given in update() -[1] fprs: shape (num_images, num_thresholds), dtype `float64`, \in [0, 1] -[2] shared_fpr: shape (num_thresholds,), dtype `float64`, \in [0, 1] -[3] tprs: shape (num_images, num_thresholds), dtype `float64`, \in [0, 1] for anom images, `nan` for norm images -[4] image_classes: shape (num_images,), dtype `int32`, \in {0, 1} +[1] fprs: shape (num_images, num_thresholds), dtype `float64`, \\in [0, 1] +[2] shared_fpr: shape (num_thresholds,), dtype `float64`, \\in [0, 1] +[3] tprs: shape (num_images, num_thresholds), dtype `float64`, \\in [0, 1] for anom images, `nan` for norm images +[4] image_classes: shape (num_images,), dtype `int32`, \\in {0, 1} - `num_thresholds` is an attribute of `PImO` and is given in the constructor (from parent class). - `num_images` depends on the data seen by the model at the update() calls. @@ -144,7 +148,7 @@ def compute(self) -> tuple[PImOResult, Tensor]: # type: ignore Returns: (PImOResult, aucs) [0] PImOResult: PImOResult, see `anomalib.utils.metrics.perimg.pimo.PImOResult` for details. - [1] aucs: shape (num_images,), dtype `float64`, \in [0, 1] + [1] aucs: shape (num_images,), dtype `float64`, \\in [0, 1] """ if self.is_empty: @@ -186,6 +190,56 @@ def plot_all_pimo_curves( ax=ax, ) ax.set_xlabel("Mean FPR on Normal Images") + + return fig, ax + + def boxplot_stats(self) -> list[dict[str, str | int | float | None]]: + """Compute boxplot stats of AUPImO values (e.g. median, mean, quartiles, etc.). + + Returns: + list[dict[str, str | int | float | None]]: List of AUCs statistics from a boxplot. + refer to `anomalib.utils.metrics.perimg.common._perimg_boxplot_stats()` for the keys and values. + """ + (_, __, ___, ____, image_classes), aucs = self.compute() + stats = _perimg_boxplot_stats(values=aucs, image_classes=image_classes, only_class=1) + return stats + + def plot_boxplot_pimo_curves( + self, + ax: Axes | None = None, + ) -> tuple[Figure | None, Axes]: + """Plot shared FPR vs Per-Image Overlap (PImO) curves (boxplot images only). + + The 'boxplot images' are those from the boxplot of AUPImO values (see `AUPImO.boxplot_stats()`). + Integration range is shown when `self.ubound < 1`. + """ + + if self.is_empty: + return None, None + + (thresholds, fprs, shared_fpr, tprs, image_classes), aucs = self.compute() + fig, ax = plot_boxplot_pimo_curves( + shared_fpr, + tprs, + image_classes, + self.boxplot_stats(), + ax=ax, + ) + ax.set_xlabel("Mean FPR on Normal Images") + + return fig, ax + + def plot_boxplot( + self, + ax: Axes | None = None, + ) -> tuple[Figure | None, Axes]: + """Plot boxplot of AUPImO values.""" + + if self.is_empty: + return None, None + + (thresholds, fprs, shared_fpr, tprs, image_classes), aucs = self.compute() + fig, ax = plot_aupimo_boxplot(aucs, image_classes, ax=ax) return fig, ax def plot( @@ -194,7 +248,33 @@ def plot( ) -> tuple[Figure | None, Axes | ndarray]: """Plot AUPImO boxplot with its statistics' PImO curves.""" - return self.plot_all_pimo_curves(ax) + if self.is_empty: + return None, None + + if ax is None: + fig, ax = plt.subplots(1, 2, figsize=(14, 6), width_ratios=[6, 8]) + fig.suptitle("Area Under the Per-Image Overlap (AUPImO) Curves") + fig.set_layout_engine("tight") + else: + fig, ax = (None, ax) + + if isinstance(ax, Axes): + return self.plot_boxplot_pimo_curves(ax=ax) + + if not isinstance(ax, ndarray): + raise ValueError(f"Expected argument `axes` to be a matplotlib Axes or ndarray, but got {type(ax)}.") + + if ax.size != 2: + raise ValueError( + f"Expected argument `axes` , when type `ndarray`, to be of size 2, but got size {ax.size}." + ) + + ax = ax.flatten() + self.plot_boxplot(ax=ax[0]) + ax[0].set_title("AUC Boxplot") + self.plot_boxplot_pimo_curves(ax=ax[1]) + ax[1].set_title("Curves") + return fig, ax class AULogPImO(PImO): diff --git a/src/anomalib/utils/metrics/perimg/plot.py b/src/anomalib/utils/metrics/perimg/plot.py index f0a142b5e7..1423bcd248 100644 --- a/src/anomalib/utils/metrics/perimg/plot.py +++ b/src/anomalib/utils/metrics/perimg/plot.py @@ -7,13 +7,17 @@ import matplotlib.pyplot as plt import numpy +import torch from matplotlib.axes import Axes from matplotlib.pyplot import Figure from matplotlib.ticker import FixedLocator, LogFormatter, PercentFormatter from torch import Tensor from .common import ( + _perimg_boxplot_stats, _validate_atleast_one_anomalous_image, + _validate_aucs, + _validate_image_class, _validate_image_classes, _validate_perimg_rate_curves, _validate_rate_curve, @@ -95,9 +99,94 @@ def _format_axis_rate_metric_log(ax: Axes, axis: int, lower_lim: float = 1e-3) - raise ValueError(f"`axis` must be 0 (X-axis) or 1 (Y-axis), but got {axis}.") +def _bounded_lims( + ax: Axes, axis: int, bounds: tuple[float | None, float | None] = (None, None), lims_epsilon: float = 0.01 +): + """Snap X/Y-axis limits to stay within the given bounds.""" + + assert len(bounds) == 2, f"Expected argument `bounds` to be a tuple of size 2, but got size {len(bounds)}." + bounds = ( + None if bounds[0] is None else bounds[0] - lims_epsilon, + None if bounds[1] is None else bounds[1] + lims_epsilon, + ) + + if axis == 0: + lims = ax.get_xlim() + elif axis == 1: + lims = ax.get_ylim() + else: + raise ValueError(f"Unknown axis {axis}. Must be 0 (X-axis) or 1 (Y-axis).") + + newlims = list(lims) + + if bounds[0] is not None and lims[0] < bounds[0]: + newlims[0] = bounds[0] + + if bounds[1] is not None and lims[1] > bounds[1]: + newlims[1] = bounds[1] + + if axis == 0: + ax.set_xlim(newlims) + else: + ax.set_ylim(newlims) + + # =========================================== GENERIC =========================================== +def _plot_perimg_metric_boxplot( + ax: Axes, + values: Tensor, + image_classes: Tensor, + bp_stats: list, + only_class: int | None = None, +): + _validate_image_classes(image_classes) + _validate_image_class(only_class) + + if values.ndim != 1: + raise ValueError(f"Expected argument `values` to be a 1D tensor, but got {values.ndim}D tensor.") + + if values.shape != image_classes.shape: + raise ValueError( + "Expected arguments `values` and `image_classes` to have the same shape, " + f"but got {values.shape} and {image_classes.shape}." + ) + + if only_class is not None and only_class not in image_classes: + raise ValueError(f"Argument `only_class` is {only_class}, but `image_classes` does not contain this class.") + + # only consider images of the given class + imgs_mask = ( + torch.ones_like(image_classes, dtype=torch.bool) if only_class is None else (image_classes == only_class) + ) + imgs_idxs = torch.nonzero(imgs_mask).squeeze(1) + + ax.boxplot( + values[imgs_mask], + vert=False, + widths=0.5, + showmeans=True, + showcaps=True, + notch=False, + ) + _ = ax.set_yticks([]) + + num_images = len(imgs_idxs) + num_flierlo = len([s for s in bp_stats if s["statistic"] == "flierlo" and s["imgidx"] in imgs_idxs]) + num_flierhi = len([s for s in bp_stats if s["statistic"] == "flierhi" and s["imgidx"] in imgs_idxs]) + + ax.annotate( + text=f"Number of images\n total: {num_images}\n fliers: {num_flierlo} low, {num_flierhi} high", + xy=(0.03, 0.95), + xycoords="axes fraction", + xytext=(0, 0), + textcoords="offset points", + annotation_clip=False, + verticalalignment="top", + ) + + def _plot_perimg_curves( ax: Axes, x: Tensor, @@ -163,6 +252,38 @@ def _plot_perimg_curves( ax.plot(x, y, **kw) +# =========================================== BOXPLOTS =========================================== + + +def plot_aupimo_boxplot( + aucs: Tensor, + image_classes: Tensor, + ax: Axes | None = None, +) -> tuple[Figure | None, Axes]: + _validate_aucs(aucs, nan_allowed=True) + _validate_atleast_one_anomalous_image(image_classes) + + fig, ax = plt.subplots() if ax is None else (None, ax) + + bp_stats = _perimg_boxplot_stats(aucs, image_classes, only_class=1) + + _plot_perimg_metric_boxplot( + ax=ax, + values=aucs, + image_classes=image_classes, + only_class=1, + bp_stats=bp_stats, + ) + + # don't go beyond the [0, 1] + _bounded_lims(ax, axis=0, bounds=(0, 1)) + ax.xaxis.set_major_formatter(PercentFormatter(1)) + ax.set_xlabel("AUPImO [%]") + ax.set_title("Area Under the Per-Image Overlap (AUPImO) Boxplot") + + return fig, ax + + # =========================================== PImO =========================================== @@ -223,3 +344,133 @@ def plot_all_pimo_curves( ax.set_title("Per-Image Overlap Curves") return fig, ax + + +def plot_boxplot_pimo_curves( + shared_fpr, + tprs, + image_classes, + bp_stats: list[dict[str, str | int | float | None]], + ax: Axes | None = None, +) -> tuple[Figure | None, Axes]: + """Plot shared FPR vs Per-Image Overlap (PImO) curves only for the boxplot stats cases. + + Args: + ax: matplotlib Axes + shared_fpr: shape (num_thresholds,) + tprs: shape (num_images, num_thresholds) + image_classes: shape (num_images,) + The `image_classes` tensor is used to filter out the normal images, while making it possible to + keep the indices of the anomalous images. + + bp_stats: list of dicts, each dict is a boxplot stat of AUPImO values + refer to `anomalib.utils.metrics.perimg.common._perimg_boxplot_stats()` + + Returns: + fig, ax + """ + + # ** validate ** + _validate_rate_curve(shared_fpr) + _validate_perimg_rate_curves(tprs, nan_allowed=True) # normal images have `nan`s + _validate_image_classes(image_classes) + + if tprs.shape[0] != image_classes.shape[0]: + raise ValueError( + f"Expected argument `tprs` to have the same number of images as argument `image_classes`, " + f"but got {tprs.shape[0]} images and {image_classes.shape[0]} images, respectively." + ) + + _validate_atleast_one_anomalous_image(image_classes) + # there may be `nan`s but only in the normal images + # in the curves of anomalous images, there should NOT be `nan`s + _validate_perimg_rate_curves(tprs[image_classes == 1], nan_allowed=False) + + if len(bp_stats) == 0: + raise ValueError("Expected argument `bp_stats` to have at least one dict, but got none.") + + # ** kwargs_perimg ** + + # it is sorted so that only the first one has a label (others are plotted but don't show in the legend) + imgidxs_toplot_fliers: list[int] = sorted( + {s["imgidx"] for s in bp_stats if s["statistic"] in ("flierlo", "flierhi")} # type: ignore + ) + imgidxs_toplot_others = {s["imgidx"] for s in bp_stats if s["statistic"] not in ("flierlo", "flierhi")} + + kwargs_perimg = [] + num_images = len(image_classes) + + for imgidx in range(num_images): + if imgidx in imgidxs_toplot_fliers: + kw = dict(linewidth=0.5, color="gray", alpha=0.8, linestyle="--") + + # only one of them will show in the legend + if imgidx == imgidxs_toplot_fliers[0]: + kw["label"] = "flier" + else: + kw["label"] = None + + kwargs_perimg.append(kw) + + continue + + if imgidx not in imgidxs_toplot_others: + # don't plot this curve + kwargs_perimg.append(None) # type: ignore + continue + + imgidx_stats = [s for s in bp_stats if s["imgidx"] == imgidx] + stat_dict = imgidx_stats[0] + + # edge case where more than one stat falls on the same image + if len(imgidx_stats) > 1: + stat_dict["statistic"] = " & ".join(s["statistic"] for s in imgidx_stats) # type: ignore + + stat, nearest = stat_dict["statistic"], stat_dict["nearest"] + kwargs_perimg.append(dict(label=f"{stat} (AUPImO={nearest:.1%}) (imgidx={imgidx})")) + + # ** plot ** + + fig, ax = plt.subplots(figsize=(7, 6)) if ax is None else (None, ax) + + _plot_perimg_curves(ax, shared_fpr, tprs, *kwargs_perimg) + + # ** legend ** + + def _sort_legend(handles: list, labels: list[str]): + """sort the legend by label and put 'flier' at the bottom + it makes the legend 'more deterministic' + """ + + # [(handle0, label0), (handle1, label1),...] + handles_labels = list(zip(handles, labels)) + handles_labels = sorted(handles_labels, key=lambda tup: tup[1]) + + # ([handle0, handle1, ...], [label0, label1, ...]) + handles, labels = tuple(map(list, zip(*handles_labels))) # type: ignore + + # put flier at the last position + if "flier" in labels: + idx = labels.index("flier") + handles.append(handles.pop(idx)) + labels.append(labels.pop(idx)) + + return handles, labels + + ax.legend( + *_sort_legend(*ax.get_legend_handles_labels()), + title="Boxplot Stats", + loc="lower right", + fontsize="small", + title_fontsize="small", + ) + + # ** format ** + + _format_axis_rate_metric_linear(ax, axis=0) + ax.set_xlabel("Shared FPR") + _format_axis_rate_metric_linear(ax, axis=1) + ax.set_ylabel("Per-Image Overlap (in-image TPR)") + ax.set_title("Per-Image Overlap Curves (AUC boxplot statistics)") + + return fig, ax diff --git a/tests/pre_merge/utils/metrics/test_perimg/test_common.py b/tests/pre_merge/utils/metrics/test_perimg/test_common.py new file mode 100644 index 0000000000..c3eca86090 --- /dev/null +++ b/tests/pre_merge/utils/metrics/test_perimg/test_common.py @@ -0,0 +1,22 @@ +import torch + +from anomalib.utils.metrics.perimg.common import _perimg_boxplot_stats + + +def test__perimg_boxplot_stats(): + data = torch.arange(100) / 7 - 4 # arbitrary data + img_cls = (torch.arange(100) % 3 == 0).to(torch.long) + + stats = _perimg_boxplot_stats(data, img_cls) + assert len(stats) > 0 + statdic = stats[0] + assert "statistic" in statdic + assert "value" in statdic + assert "nearest" in statdic + assert "imgidx" in statdic + + _perimg_boxplot_stats(data, img_cls, only_class=0) + assert len(stats) > 0 + + _perimg_boxplot_stats(data, img_cls, only_class=1) + assert len(stats) > 0 diff --git a/tests/pre_merge/utils/metrics/test_perimg/test_pimo.py b/tests/pre_merge/utils/metrics/test_perimg/test_pimo.py index 68009013dd..5c4d33f550 100644 --- a/tests/pre_merge/utils/metrics/test_perimg/test_pimo.py +++ b/tests/pre_merge/utils/metrics/test_perimg/test_pimo.py @@ -110,4 +110,34 @@ def test_aupimo( assert (com_image_classes == expected_image_classes).all() assert com_aupimos[:2].isnan().all() assert (com_aupimos[2:] == expected_aupimos[2:]).all() - aupimo.plot_all_pimo_curves() # should not break + + stats = aupimo.boxplot_stats() + assert len(stats) > 0 + + +def test_aupimo_plots(anomaly_maps, masks): + aupimo = AUPImO(num_thresholds=1000) + aupimo.update(anomaly_maps, masks) + aupimo.compute() + + aupimo.plot() + + fig, ax = aupimo.plot_all_pimo_curves() + assert fig is not None + assert ax is not None + aupimo.plot_all_pimo_curves(ax=ax) + + fig, ax = aupimo.plot_boxplot() + assert fig is not None + assert ax is not None + aupimo.plot_boxplot(ax=ax) + + fig, ax = aupimo.plot_boxplot_pimo_curves() + assert fig is not None + assert ax is not None + aupimo.plot_boxplot_pimo_curves(ax=ax) + + fig, ax = aupimo.plot_boxplot_pimo_curves() + assert fig is not None + assert ax is not None + aupimo.plot_boxplot_pimo_curves(ax=ax) diff --git a/tests/pre_merge/utils/metrics/test_perimg/test_plot.py b/tests/pre_merge/utils/metrics/test_perimg/test_plot.py index e40765ffb9..1c6015ae8c 100644 --- a/tests/pre_merge/utils/metrics/test_perimg/test_plot.py +++ b/tests/pre_merge/utils/metrics/test_perimg/test_plot.py @@ -3,31 +3,58 @@ import matplotlib.pyplot as plt import torch + +from anomalib.utils.metrics.perimg.common import _perimg_boxplot_stats from anomalib.utils.metrics.perimg.plot import ( _plot_perimg_curves, + _plot_perimg_metric_boxplot, plot_all_pimo_curves, + plot_aupimo_boxplot, + plot_boxplot_pimo_curves, ) def pytest_generate_tests(metafunc): # curves with same general, monotonic shape as tprs/fprs curves - ys = (torch.linspace(-10, 10, 300)[None, :] + torch.arange(1, 5)[:, None]).sigmoid().flip(1) + rates = (torch.linspace(-10, 10, 300)[None, :] + torch.arange(1, 15)[:, None]).sigmoid().flip(1) th = torch.linspace(0, 15, 300) - cases = [ - (th, ys), # th vs tpr like - (ys[0], ys), # fpr vs tpr like - ] + img_cls = (torch.arange(1, 15) % 3 == 0).to(torch.int32) + aucs = rates.mean(1) if metafunc.function is test__plot_perimg_curves: metafunc.parametrize( argnames=("x", "ys"), - argvalues=cases, + argvalues=[ + (th, rates), # th vs tpr like + (rates[0], rates), # fpr vs tpr like + ], ) if metafunc.function is test__plot_perimg_curves_kwargs: metafunc.parametrize( argnames=("x", "ys"), - argvalues=cases[1:2], + argvalues=[ + (rates[0], rates), + ], + ) + + if "rates" in metafunc.fixturenames: + metafunc.parametrize(("rates",), [(rates,)]) + + if "image_classes" in metafunc.fixturenames: + metafunc.parametrize(("image_classes",), [(img_cls,)]) + + if "aucs" in metafunc.fixturenames: + metafunc.parametrize(("aucs",), [(aucs,)]) + + if "only_class" in metafunc.fixturenames: + metafunc.parametrize( + ("only_class",), + [ + (None,), + (0,), + (1,), + ], ) @@ -41,7 +68,7 @@ def test__plot_perimg_curves_kwargs(x, ys): _, ax = plt.subplots() _plot_perimg_curves( ax, - x, + ys[0], ys, # per-curve kwargs *[dict(linewidth=i) for i in range(ys.shape[0])], @@ -50,11 +77,29 @@ def test__plot_perimg_curves_kwargs(x, ys): ) -def test_plot_all_pimo_curves(): - """Test plot_all_pimo_curves.""" - rates = (torch.linspace(-10, 10, 300)[None, :] + torch.arange(1, 5)[:, None]).sigmoid().flip(1) - img_cls = (torch.arange(1, 5) % 2).to(torch.int32) - fig, ax = plot_all_pimo_curves(rates[0], rates, img_cls) +def test_plot_all_pimo_curves(rates, image_classes): + fig, ax = plot_all_pimo_curves(rates[0], rates, image_classes) + assert fig is not None + assert ax is not None + plot_all_pimo_curves(rates[0], rates, image_classes, ax=ax) + + +def test__plot_perimg_metric_boxplot(aucs, image_classes, only_class): + bp_stats = _perimg_boxplot_stats(aucs, image_classes) + _, ax = plt.subplots() + _plot_perimg_metric_boxplot(ax, aucs, image_classes, bp_stats, only_class=only_class) + + +def test_plot_aupimo_boxplot(aucs, image_classes): + fig, ax = plot_aupimo_boxplot(aucs, image_classes) + assert fig is not None + assert ax is not None + plot_aupimo_boxplot(aucs, image_classes, ax=ax) + + +def test_plot_boxplot_pimo_curves(aucs, rates, image_classes): + bp_stats = _perimg_boxplot_stats(aucs, image_classes) + fig, ax = plot_boxplot_pimo_curves(rates[0], rates, image_classes, bp_stats) assert fig is not None assert ax is not None - plot_all_pimo_curves(rates[0], rates, img_cls, ax=ax) + plot_boxplot_pimo_curves(rates[0], rates, image_classes, bp_stats, ax=ax)