diff --git a/README.md b/README.md index 8aa1d61ab..4218a8a79 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,8 @@ Illustrate reading NIfTI files and iterating over image patches of the volumes l This tutorial illustrates the flexible network APIs and utilities. ##### [postprocessing_transforms](./modules/postprocessing_transforms.ipynb) This notebook shows the usage of several postprocessing transforms based on the model output of spleen segmentation task. +##### [rankseg_integration](./modules/rankseg_integration.ipynb) +This notebook demonstrates how to integrate RankSEG and RankSEGd as optional third-party post-processing transforms in MONAI and compare RankSEG with argmax using a pretrained pancreas segmentation model. ##### [idc_dataset](./modules/idc_dataset.ipynb) This notebook shows how to query and download public cancer imaging data from NCI Imaging Data Commons (IDC) using `idc-index`, and how to load DICOM images and DICOM-SEG segmentations into MONAI for AI/ML preprocessing. ##### [public_datasets](./modules/public_datasets.ipynb) diff --git a/modules/rankseg_integration.ipynb b/modules/rankseg_integration.ipynb new file mode 100644 index 000000000..d6d4b202d --- /dev/null +++ b/modules/rankseg_integration.ipynb @@ -0,0 +1,1040 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright (c) MONAI Consortium \n", + "Licensed under the Apache License, Version 2.0 (the \"License\"); \n", + "you may not use this file except in compliance with the License. \n", + "You may obtain a copy of the License at \n", + "    http://www.apache.org/licenses/LICENSE-2.0 \n", + "Unless required by applicable law or agreed to in writing, software \n", + "distributed under the License is distributed on an \"AS IS\" BASIS, \n", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \n", + "See the License for the specific language governing permissions and \n", + "limitations under the License.\n", + "\n", + "# Use RankSEG in a MONAI post-processing pipeline\n", + "\n", + "This tutorial demonstrates how to expose [RankSEG](https://github.com/rankseg/rankseg) as custom MONAI array and dictionary transforms and add it to a standard decollated post-processing pipeline. RankSEG is kept as an optional third-party dependency; no change to MONAI core is required.\n", + "\n", + "The example runs inference on a real 3D CT volume from the Medical Segmentation Decathlon Task07 Pancreas dataset. It uses the official pretrained MONAI `pancreas_ct_dints_segmentation` Bundle and compares RankSEG with the usual `AsDiscrete(argmax=True)` decoder on exactly the same softmax probabilities. No model training or method-specific tuning is required.\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Project-MONAI/tutorials/blob/main/modules/rankseg_integration.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What is RankSEG?\n", + "\n", + "A segmentation network usually produces a score or probability for every class at every voxel. A conventional multiclass pipeline converts these values to a mask with `argmax`, independently assigning each voxel to the class with the largest score. This is a natural rule for voxel-wise classification, but it does not directly target region-level metrics such as Dice or IoU, whose values depend on the predicted region as a whole.\n", + "\n", + "RankSEG is an inference-time, metric-aware alternative to argmax or fixed thresholding. It leaves the trained model and its probability maps unchanged, and only replaces the final probability-to-mask decision. Unlike connected-component filtering or morphological post-processing, RankSEG does not impose connectivity, shape, or anatomical constraints.\n", + "\n", + "| Property | Argmax / fixed threshold | RankSEG |\n", + "| --- | --- | --- |\n", + "| Decision | Independent at each voxel | Based on each class probability map as a whole |\n", + "| Target | Voxel-wise class decision | A selected region metric, such as Dice or IoU |\n", + "| Region size | Implicit or controlled by a fixed threshold | Adaptively selected for each image and class |\n", + "| Model training | Unchanged | Unchanged |\n", + "| Additional inference cost | Minimal | Probability ranking and metric-aware optimization |\n", + "\n", + "### How the algorithm works\n", + "\n", + "For each class, RankSEG ranks voxels from highest to lowest predicted probability. The `RMA` solver used in this tutorial then estimates how many of the highest-ranked voxels should be included to target the selected metric. In multiclass mode, the class masks are finally combined into one non-overlapping label map. No model retraining or threshold tuning is required.\n", + "\n", + "### When should RankSEG be considered?\n", + "\n", + "RankSEG can be useful when Dice or IoU is the evaluation target and the model produces meaningful probability maps, particularly for uncertain predictions or small-to-medium foreground regions. When predictions are already confident, RankSEG and argmax may produce nearly identical masks.\n", + "\n", + "RankSEG does not correct errors in the underlying probability maps, and its output depends on their quality and calibration. It also adds computation and memory use during inference because the probabilities must be ranked. Improvement is not guaranteed for every case or deployment distribution, so the metric, output mode, and solver should be selected deliberately and validated on representative data. The case below illustrates the integration rather than serving as a standalone performance benchmark." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:35:12.417757Z", + "iopub.status.busy": "2026-07-31T08:35:12.417209Z", + "iopub.status.idle": "2026-07-31T08:35:14.658736Z", + "shell.execute_reply": "2026-07-31T08:35:14.658045Z" + } + }, + "outputs": [], + "source": [ + "%pip install -q --upgrade-strategy only-if-needed \"monai-weekly[nibabel, tqdm,matplotlib]\" \"rankseg==0.0.5\"\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup imports" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:35:14.660688Z", + "iopub.status.busy": "2026-07-31T08:35:14.660543Z", + "iopub.status.idle": "2026-07-31T08:35:15.759133Z", + "shell.execute_reply": "2026-07-31T08:35:15.758449Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MONAI version: 1.7.dev2630\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Numpy version: 2.2.6\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pytorch version: 2.13.0+cu130\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MONAI flags: HAS_EXT = False, USE_COMPILED = False, USE_META_DICT = False\n", + "MONAI rev id: 82bbaeeb45e5490606c4ff4615dd85e0e83fce7c\n", + "MONAI __file__: /home//rankseg/tutorials/.venv/lib/python3.10/site-packages/monai/__init__.py\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Optional dependencies:\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pytorch Ignite version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ITK version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nibabel version: 5.4.2\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "scikit-image version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "scipy version: 1.15.3\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pillow version: 12.3.0\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tensorboard version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "gdown version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TorchVision version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tqdm version: 4.69.1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "lmdb version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "psutil version: 7.2.2\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pandas version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "einops version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "transformers version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlflow version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pynrrd version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "clearml version: NOT INSTALLED or UNKNOWN VERSION.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "For details about installing the optional dependencies, please visit:\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies\n", + "\n" + ] + } + ], + "source": [ + "import hashlib\n", + "import os\n", + "import shutil\n", + "import tarfile\n", + "import tempfile\n", + "from collections.abc import Hashable, Mapping\n", + "from pathlib import Path\n", + "from urllib.request import Request, urlopen\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import torch\n", + "from monai.bundle import ConfigParser, download\n", + "from monai.config import print_config\n", + "from monai.data import decollate_batch\n", + "from monai.inferers import SlidingWindowInferer\n", + "from monai.metrics import DiceMetric\n", + "from monai.transforms import (\n", + " Activations,\n", + " Activationsd,\n", + " AsDiscrete,\n", + " Compose,\n", + " EnsureChannelFirstd,\n", + " EnsureTyped,\n", + " LoadImaged,\n", + " MapTransform,\n", + " Orientationd,\n", + " ScaleIntensityRanged,\n", + " Spacingd,\n", + " Transform,\n", + ")\n", + "from monai.utils import TransformBackends, convert_to_tensor\n", + "from monai.utils.type_conversion import convert_to_dst_type\n", + "from rankseg.functional import rankseg\n", + "\n", + "print_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bridge the MONAI and RankSEG shape conventions\n", + "\n", + "MONAI post-processing transforms normally receive one decollated, channel-first sample with shape `(C, *spatial)`. The RankSEG functional API receives a batch of probability maps with shape `(B, C, *spatial)`. In multiclass mode it returns class-index maps with shape `(B, *spatial)`.\n", + "\n", + "The array transform below adds a temporary batch dimension. Because that dimension has size one, the returned `(1, *spatial)` tensor already follows the same single-channel output convention as `AsDiscrete(argmax=True)`. `convert_to_dst_type` restores the source container type and MONAI metadata when applicable." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:35:15.760621Z", + "iopub.status.busy": "2026-07-31T08:35:15.760514Z", + "iopub.status.idle": "2026-07-31T08:35:15.763598Z", + "shell.execute_reply": "2026-07-31T08:35:15.763149Z" + } + }, + "outputs": [], + "source": [ + "class RankSEG(Transform):\n", + " \"\"\"Decode a channel-first multiclass probability map with RankSEG.\"\"\"\n", + "\n", + " backend = [TransformBackends.TORCH]\n", + "\n", + " def __init__(\n", + " self,\n", + " metric: str = \"dice\",\n", + " solver: str = \"RMA\",\n", + " pruning_prob: float = 0.5,\n", + " **solver_params,\n", + " ) -> None:\n", + " self.metric = metric\n", + " self.solver = solver\n", + " self.pruning_prob = pruning_prob\n", + " self.solver_params = solver_params\n", + "\n", + " def __call__(self, probabilities):\n", + " source = probabilities\n", + " probabilities = convert_to_tensor(probabilities, track_meta=False)\n", + " if probabilities.ndim < 2:\n", + " raise ValueError(\"probabilities must have shape (C, *spatial)\")\n", + "\n", + " prediction = rankseg(\n", + " probabilities.unsqueeze(0),\n", + " metric=self.metric,\n", + " solver=self.solver,\n", + " output_mode=\"multiclass\",\n", + " pruning_prob=self.pruning_prob,\n", + " **self.solver_params,\n", + " )\n", + " prediction, *_ = convert_to_dst_type(prediction, source, dtype=torch.float32)\n", + " return prediction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup data directory\n", + "\n", + "Set `MONAI_DATA_DIRECTORY` to reuse the dataset and model Bundle across runs. Otherwise, this tutorial downloads them into a temporary directory and removes that directory at the end." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:35:15.764617Z", + "iopub.status.busy": "2026-07-31T08:35:15.764534Z", + "iopub.status.idle": "2026-07-31T08:35:15.766794Z", + "shell.execute_reply": "2026-07-31T08:35:15.766376Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/tmp/tmpj2w6asvp\n" + ] + } + ], + "source": [ + "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n", + "if directory is not None:\n", + " os.makedirs(directory, exist_ok=True)\n", + "root_dir = tempfile.mkdtemp() if directory is None else directory\n", + "print(root_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Download one MSD Pancreas validation case\n", + "\n", + "Task07 Pancreas contains 3D abdominal CT volumes with pancreas and pancreatic tumor labels. The Medical Segmentation Decathlon data is distributed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). To combine first-party provenance with a small download, the helper below uses HTTP Range requests against a fixed version of [MONAI's official MSD archive](https://msd-for-monai.s3-us-west-2.amazonaws.com/Task07_Pancreas.tar). It fetches only the tar headers and contents for `pancreas_102` and its label (about 29.7 MB total), then verifies the member names, sizes, tar checksums, and SHA-256 digests." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:35:15.767888Z", + "iopub.status.busy": "2026-07-31T08:35:15.767803Z", + "iopub.status.idle": "2026-07-31T08:36:11.191547Z", + "shell.execute_reply": "2026-07-31T08:36:11.189020Z" + } + }, + "outputs": [], + "source": [ + "case_id = \"pancreas_102\"\n", + "data_dir = Path(root_dir) / \"Task07_Pancreas\"\n", + "msd_archive_url = (\n", + " \"https://msd-for-monai.s3-us-west-2.amazonaws.com/Task07_Pancreas.tar\" \"?versionId=lVhRU5n2ulILHz6suffqgfgJ__KFj.3q\"\n", + ")\n", + "msd_archive_etag = '\"7b7524e869b49821724aa61235443ae1-1466\"'\n", + "# Each tuple is (relative path, tar-header offset, content size, SHA-256).\n", + "# These values are tied to the immutable S3 object version above.\n", + "msd_members = {\n", + " \"image\": (\n", + " f\"imagesTr/{case_id}.nii.gz\",\n", + " 8_795_902_464,\n", + " 29_648_519,\n", + " \"a57b50ac7d4a7053c46a801de99a17607e61d35970d1f2dc17738008690a395b\",\n", + " ),\n", + " \"label\": (\n", + " f\"labelsTr/{case_id}.nii.gz\",\n", + " 12_286_287_360,\n", + " 29_653,\n", + " \"63f3b90e305b07939bda0b8ae651934dadbaab5509a3c586d13e02a01be9bb49\",\n", + " ),\n", + "}\n", + "\n", + "\n", + "def fetch_archive_range(start, size):\n", + " request = Request(\n", + " msd_archive_url,\n", + " headers={\n", + " \"Range\": f\"bytes={start}-{start + size - 1}\",\n", + " \"If-Match\": msd_archive_etag,\n", + " },\n", + " )\n", + " with urlopen(request, timeout=120) as response:\n", + " if response.status != 206:\n", + " raise RuntimeError(f\"unexpected HTTP status for range request: {response.status}\")\n", + " content = response.read(size + 1)\n", + " if len(content) != size:\n", + " raise RuntimeError(f\"unexpected range size: expected {size}, received {len(content)}\")\n", + " return content\n", + "\n", + "\n", + "def download_msd_member(relative_path, header_offset, size, expected_sha256):\n", + " output_path = data_dir / relative_path\n", + " if output_path.exists():\n", + " digest = hashlib.sha256(output_path.read_bytes()).hexdigest()\n", + " if digest == expected_sha256:\n", + " return str(output_path)\n", + "\n", + " # Parse the tar header to validate its checksum, name, size, and type.\n", + " header = fetch_archive_range(header_offset, 512)\n", + " member = tarfile.TarInfo.frombuf(header, encoding=\"utf-8\", errors=\"surrogateescape\")\n", + " expected_name = f\"Task07_Pancreas/{relative_path}\"\n", + " if member.name != expected_name or member.size != size or not member.isfile():\n", + " raise RuntimeError(f\"official MSD tar member metadata changed for {expected_name}\")\n", + "\n", + " content = fetch_archive_range(header_offset + 512, size)\n", + " if hashlib.sha256(content).hexdigest() != expected_sha256:\n", + " raise RuntimeError(f\"SHA-256 mismatch for {expected_name}\")\n", + " output_path.parent.mkdir(parents=True, exist_ok=True)\n", + " output_path.write_bytes(content)\n", + " return str(output_path)\n", + "\n", + "\n", + "evaluation_file = {key: download_msd_member(*spec) for key, spec in msd_members.items()}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preprocess the real validation volume\n", + "\n", + "This case belongs to the Bundle's fixed validation split. It was selected from a reproducible 20-case comparison because it makes the decoder change easy to see; the full comparison should be used for performance claims. The transforms reproduce the Bundle's inference preprocessing, with nearest-neighbor resampling added for the reference label." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:36:11.199101Z", + "iopub.status.busy": "2026-07-31T08:36:11.198567Z", + "iopub.status.idle": "2026-07-31T08:36:13.640380Z", + "shell.execute_reply": "2026-07-31T08:36:13.639599Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "image: pancreas_102.nii.gz\n", + "preprocessed shape: (1, 406, 406, 228)\n" + ] + } + ], + "source": [ + "validation_transforms = Compose(\n", + " [\n", + " LoadImaged(keys=[\"image\", \"label\"]),\n", + " EnsureChannelFirstd(keys=[\"image\", \"label\"]),\n", + " Orientationd(keys=[\"image\", \"label\"], axcodes=\"RAS\", labels=None),\n", + " Spacingd(\n", + " keys=[\"image\", \"label\"],\n", + " pixdim=(1.0, 1.0, 1.0),\n", + " mode=(\"bilinear\", \"nearest\"),\n", + " ),\n", + " ScaleIntensityRanged(\n", + " keys=\"image\",\n", + " a_min=-87,\n", + " a_max=199,\n", + " b_min=0.0,\n", + " b_max=1.0,\n", + " clip=True,\n", + " ),\n", + " EnsureTyped(keys=[\"image\", \"label\"]),\n", + " ]\n", + ")\n", + "\n", + "evaluation_data = validation_transforms(evaluation_file)\n", + "print(f\"image: {os.path.basename(evaluation_file['image'])}\")\n", + "print(f\"preprocessed shape: {tuple(evaluation_data['image'].shape)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load the pretrained MONAI Pancreas Bundle\n", + "\n", + "The official [`pancreas_ct_dints_segmentation`](https://github.com/Project-MONAI/model-zoo/tree/dev/models/pancreas_ct_dints_segmentation) Bundle contains a three-class DiNTS model trained on MSD Task07. We use its architecture checkpoint and model weights without retraining or modification." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:36:13.642004Z", + "iopub.status.busy": "2026-07-31T08:36:13.641769Z", + "iopub.status.idle": "2026-07-31T08:36:27.905632Z", + "shell.execute_reply": "2026-07-31T08:36:27.904976Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,766 - INFO - --- input summary of monai.bundle.scripts.download ---\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,766 - INFO - > name: 'pancreas_ct_dints_segmentation'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - > version: '0.5.2'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - > bundle_dir: '/tmp/tmpj2w6asvp'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - > source: 'monaihosting'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - > remove_prefix: 'monai_'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - > progress: True\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:13,767 - INFO - ---\n", + "\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-07-31 16:36:26.295027 - Length of input patch is recommended to be a multiple of 32.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inference device: cuda:0\n" + ] + } + ], + "source": [ + "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", + "download(\n", + " name=\"pancreas_ct_dints_segmentation\",\n", + " version=\"0.5.2\",\n", + " bundle_dir=root_dir,\n", + ")\n", + "bundle_dir = os.path.join(root_dir, \"pancreas_ct_dints_segmentation\")\n", + "architecture = torch.load(\n", + " os.path.join(bundle_dir, \"models\", \"search_code_18590.pt\"),\n", + " map_location=device,\n", + " weights_only=False,\n", + ")\n", + "parser = ConfigParser()\n", + "parser.read_config(os.path.join(bundle_dir, \"configs\", \"inference.yaml\"))\n", + "parser[\"arch_ckpt\"] = architecture\n", + "parser[\"dints_space#device\"] = str(device)\n", + "model = parser.get_parsed_content(\"network_def\").to(device)\n", + "checkpoint = torch.load(\n", + " os.path.join(bundle_dir, \"models\", \"model.pt\"),\n", + " map_location=device,\n", + " weights_only=False,\n", + ")\n", + "model.load_state_dict(checkpoint.get(\"model\", checkpoint))\n", + "model.eval()\n", + "print(f\"inference device: {device}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run sliding-window inference once\n", + "\n", + "Inference produces one batched three-channel logit volume. Both decoders below receive exactly these logits; there is no retraining, threshold tuning, or method-specific preprocessing. A GPU is recommended, although the code also supports CPU execution with a smaller sliding-window batch." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:36:27.907491Z", + "iopub.status.busy": "2026-07-31T08:36:27.907383Z", + "iopub.status.idle": "2026-07-31T08:36:59.946308Z", + "shell.execute_reply": "2026-07-31T08:36:59.945748Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "logits shape: (1, 3, 406, 406, 228)\n" + ] + } + ], + "source": [ + "input_volume = evaluation_data[\"image\"].unsqueeze(0).to(device)\n", + "reference_volume = evaluation_data[\"label\"].unsqueeze(0).to(device)\n", + "inferer = SlidingWindowInferer(\n", + " roi_size=(96, 96, 96),\n", + " sw_batch_size=2 if device.type == \"cuda\" else 1,\n", + " overlap=0.625,\n", + ")\n", + "\n", + "with torch.no_grad():\n", + " logits = inferer(input_volume, model)\n", + "\n", + "print(f\"logits shape: {tuple(logits.shape)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare argmax and RankSEG post-processing\n", + "\n", + "Both pipelines operate after `decollate_batch`, so the class dimension is dimension zero. Consequently, `Activations(softmax=True)` uses the correct default softmax dimension. If softmax is applied before decollation, its dimension should instead be set to the batched class dimension, normally `dim=1`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:36:59.948368Z", + "iopub.status.busy": "2026-07-31T08:36:59.948262Z", + "iopub.status.idle": "2026-07-31T08:37:00.114036Z", + "shell.execute_reply": "2026-07-31T08:37:00.113470Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "argmax output shape: (1, 406, 406, 228)\n", + "RankSEG output shape: (1, 406, 406, 228)\n" + ] + } + ], + "source": [ + "post_argmax = Compose([Activations(softmax=True), AsDiscrete(argmax=True)])\n", + "post_rankseg = Compose([Activations(softmax=True), RankSEG(metric=\"dice\")])\n", + "\n", + "decollated_logits = decollate_batch(logits)\n", + "argmax_predictions = [post_argmax(item) for item in decollated_logits]\n", + "rankseg_predictions = [post_rankseg(item) for item in decollated_logits]\n", + "\n", + "assert argmax_predictions[0].shape == rankseg_predictions[0].shape\n", + "print(f\"argmax output shape: {tuple(argmax_predictions[0].shape)}\")\n", + "print(f\"RankSEG output shape: {tuple(rankseg_predictions[0].shape)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Measure foreground Dice\n", + "\n", + "Dice is measured on the same transformed reference volume used for both predictions. Background is excluded, and the pancreas and tumor scores are macro-averaged. This selected case is an integration example rather than a standalone benchmark." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:37:00.115547Z", + "iopub.status.busy": "2026-07-31T08:37:00.115440Z", + "iopub.status.idle": "2026-07-31T08:37:00.161600Z", + "shell.execute_reply": "2026-07-31T08:37:00.161077Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Argmax macro Dice: 0.7747\n", + "RankSEG macro Dice: 0.8502\n", + "Paired Dice change: +0.0755\n", + "Pancreas Dice: 0.8764 -> 0.8898\n", + "Tumor Dice: 0.6730 -> 0.8106\n" + ] + } + ], + "source": [ + "to_one_hot = AsDiscrete(to_onehot=3)\n", + "reference_predictions = decollate_batch(reference_volume)\n", + "reference_one_hot = [to_one_hot(item) for item in reference_predictions]\n", + "argmax_one_hot = [to_one_hot(item) for item in argmax_predictions]\n", + "rankseg_one_hot = [to_one_hot(item) for item in rankseg_predictions]\n", + "\n", + "dice_metric = DiceMetric(include_background=False, reduction=\"mean_batch\")\n", + "dice_metric(y_pred=argmax_one_hot, y=reference_one_hot)\n", + "argmax_class_dice = dice_metric.aggregate()\n", + "dice_metric.reset()\n", + "dice_metric(y_pred=rankseg_one_hot, y=reference_one_hot)\n", + "rankseg_class_dice = dice_metric.aggregate()\n", + "dice_metric.reset()\n", + "\n", + "argmax_dice = argmax_class_dice.mean().item()\n", + "rankseg_dice = rankseg_class_dice.mean().item()\n", + "print(f\"Argmax macro Dice: {argmax_dice:.4f}\")\n", + "print(f\"RankSEG macro Dice: {rankseg_dice:.4f}\")\n", + "print(f\"Paired Dice change: {rankseg_dice - argmax_dice:+.4f}\")\n", + "print(f\"Pancreas Dice: {argmax_class_dice[0]:.4f} -> {rankseg_class_dice[0]:.4f}\")\n", + "print(f\"Tumor Dice: {argmax_class_dice[1]:.4f} -> {rankseg_class_dice[1]:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize the real CT result\n", + "\n", + "The axial slice is selected by maximum ground-truth tumor area, and the crop is based only on the ground-truth tumor bounds. The final panel highlights tumor-label decisions changed by RankSEG." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:37:00.162953Z", + "iopub.status.busy": "2026-07-31T08:37:00.162822Z", + "iopub.status.idle": "2026-07-31T08:37:00.922904Z", + "shell.execute_reply": "2026-07-31T08:37:00.922410Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAACCkAAAGGCAYAAABcnkk8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAAl5lJREFUeJzs3Xd4VHXa+P97MpmSTHojCYQEAtIRFxQRKWIBROx1LWDFtburPqv7VcB17btrWwvPuugqa8G1KyIoFuwFLPSShEBCeu8zc35/+Ms8Tu4PcCgOCO/XdXldzjtn5tQ5OTP5MOOwLMsSAAAAAAAAAAAAAACAX1jU3l4AAAAAAAAAAAAAAABwYGCQAgAAAAAAAAAAAAAAiAgGKQAAAAAAAAAAAAAAgIhgkAIAAAAAAAAAAAAAAIgIBikAAAAAAAAAAAAAAICIYJACAAAAAAAAAAAAAACICAYpAAAAAAAAAAAAAACAiGCQAgAAAAAAAAAAAAAAiAgGKQAAAAAAAAAAAAAAgIhgkAIAAAAAAAAAAPuR6dOnS1xc3N5eDAAAACMGKQAAAAAAACDk0UcfFYfDISNHjtzbi7LXffrppzJr1iypra3d24sC4FfmqaeeEofDEfovOjpaunfvLtOnT5ctW7bs7cULCQaD8u9//1tGjhwpKSkpEh8fLwcddJBccMEF8vnnn4em++CDD8LWp+t/zz//vPFxjz32WElLSxOXyyUZGRly3HHHyZw5c6StrW2Hy/boo4/KU089tadXGQAA7AOi9/YCAAAAAAAAYN8xb948ycvLky+//FLWr18vffr02duLtNd8+umnMnv2bJk+fbokJSXt7cUB8Ct0++23S69evaS1tVU+//xzeeqpp2Tp0qXy448/itfr3duLJ9dcc4384x//kJNOOknOPfdciY6OljVr1siCBQukd+/ecvjhh6vpDz30UPU4o0aNCv1/S0uLnHLKKbJw4UI54ogj5IYbbpBu3bpJdXW1fPjhh3LFFVfIF198IU8++eR2l+3RRx+VtLQ0mT59+h5ZVwAAsO9gkAIAAAAAAABERKSgoEA+/fRTefnll2XGjBkyb948mTlz5g7v5/f7JRgMitvtjsBSwi72C7D3TZ48WUaMGCEiIpdccomkpaXJPffcI6+//rqceeaZe3XZysrK5NFHH5VLL71U5syZE/azBx54QCoqKtR9xowZI6effvp2H/f666+XhQsXygMPPCDXXntt2M/+8Ic/yLp162TRokW7vwL7uKamJvH5fHt7MQAA2CfxdQ8AAAAAAAAQkZ8+RSE5OVmmTJkip59+usybN09NU1hYKA6HQ+6//3554IEHJD8/Xzwej6xcuVJEfvpI8BEjRojX65X8/Hx54oknZNasWeJwOMIex+FwyFVXXSXz58+XgQMHSkxMjIwaNUp++OEHERF54oknpE+fPuL1emX8+PFSWFgYdv+PP/5YzjjjDOnZs6d4PB7JycmR66+/XlpaWkLTlJeXS3p6uowfP14sywr19evXi8/nk7POOmub22LWrFly4403iohIr169Qh9pXlhYGNoGpo8hdzgcMmvWrLDHcTgcsnbtWjnvvPMkMTFR0tPT5dZbbxXLsqS4uFhOOukkSUhIkMzMTPnrX/+qHrO8vFwuvvhi6datm3i9Xjn44IPl6aef3qn9AmDfMGbMGBER2bBhQ6i1t7fLbbfdJsOHD5fExETx+XwyZswYWbJkSdh9f/48nzNnTuh5fuihh8pXX321w3kvX748dE5sbGyUgoICsSxLRo8eraZ1OBySkZGx0+tXXFws//znP2XSpElqgEKnvn37yhVXXLHdx8nLy5MVK1bIhx9+GDr/jh8/XkTE+DtF5P++YuPnvy/y8vLkhBNOCP1uiomJkSFDhsgHH3wgIiIvv/yyDBkyRLxerwwfPlyWLVumHvf999+XMWPGiM/nk6SkJDnppJNk1apVYdN0LtPKlSvlt7/9rSQnJ8uRRx653XUEAOBAxicpAAAAAAAAQER+GqRw6qmnitvtlnPOOUcee+wx+eqrr4wf7T137lxpbW2Vyy67TDwej6SkpMiyZctk0qRJkpWVJbNnz5ZAICC33367pKenG+f38ccfy+uvvy5XXnmliIjcddddcsIJJ8hNN90kjz76qFxxxRVSU1Mj9957r1x00UXy/vvvh+47f/58aW5ult/97neSmpoqX375pTz88MOyefNmmT9/voiIZGRkyGOPPSZnnHGGPPzww3LNNddIMBiU6dOnS3x8vDz66KPb3BannnqqrF27Vp577jn5+9//LmlpaSIikp6ebvzXxTty1llnyYABA+Tuu++Wt956S+644w5JSUmRJ554QiZMmCD33HOPzJs3T2644QY59NBDZezYsSLy08em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4/HIlClT5IsvvpB33nlnh8uampoqJ554orz99tsybtw4Wbp0qRoItSP9+/cXEZEff/wxrKenp4cGNGRlZe3UY3a1s+fgXf1dsys4BwMAYA+DFAAAAAAAAA5w8+bNk4yMDJk/f77675xzzpFXXnlFWlpatvsYGRkZ4vV6Zf369epnprY7fvjhB1m7dq389a9/lf/5n/+Rk046SY455piwr5n4uXfeeUf++c9/yk033STp6ekybdo08fv9O5zPtv4Q1vmvlmtra8P6zv4xz47c3FxZt26d+kPl6tWrQz8H8OvgdDrlrrvukpKSEnnkkUdC/aWXXpLevXvLyy+/LOeff75MnDhRjjnmGGltbd2t+TkcDpk3b54cffTRcsYZZ8gHH3xg+74jRowQkZ++8mBnTJ48WZxOp3Fw287a2+fgzvPrmjVr1M9Wr14taWlp4vP59ug8AQA4UDBIAQAAAAAA4ADW0tIiL7/8spxwwgly+umnq/+uuuoqaWhokNdff327j+N0OuWYY46RV199VUpKSkJ9/fr1smDBgj26zJ3/+vXn/9rVsix58MEH1bS1tbVyySWXyGGHHSZ33nmn/POf/5Rvv/1W7rzzzh3Op/OPT13/EJaQkCBpaWny0UcfhfVHH310Z1dlh44//njZunWrvPDCC6Hm9/vl4Ycflri4uNBXXAD4dRg/frwcdthh8sADD4QGIZjOaV988YV89tlnuz0/t9stL7/8shx66KEydepU+fLLL0M/27p1q6xcuVLdp729Xd577z2Jiora5tf1bEvPnj3loosukgULFoQNxPg5u59U4PP51PlXRCQ/P19EJOwc3NTUJE8//fROLeuOZGVlybBhw+Tpp58OW44ff/xR3n33XTn++OP36PwAADiQRO/tBQAAAAAAAMDe8/rrr0tDQ4OceOKJxp8ffvjhkp6eLvPmzZOzzjpru481a9Yseffdd2X06NHyu9/9TgKBgDzyyCMyePBgWb58+R5b5v79+0t+fr7ccMMNsmXLFklISJD//ve/6vvYRUSuvfZaqaqqksWLF4vT6ZRJkybJJZdcInfccYecdNJJcvDBB29zPsOHDxcRkT/96U9y9tlni8vlkqlTp4rP55NLLrlE7r77brnkkktkxIgR8tFHH8natWv32Dp2uuyyy+SJJ56Q6dOnyzfffCN5eXny0ksvySeffCIP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L+Suu+6SVatWySuvvBI2nzVr1sg555wjM2bMkEsvvVT69eu33eWqqqqSyZMny9lnny3nnXeedOvWTYLBoJx44omydOlSueyyy2TAgAHyww8/yN///ndZu3atvPrqq5Keni7PPPOM/OUvf5HGxka56667ROT/PqZ9xowZ8tRTT8mFF14o11xzjRQUFMgjjzwiy5Ytk08++STso79Ny9zc3Czjxo2TLVu2yIwZM6Rnz57y6aefys033yylpaXywAMPhK3Hf/7zH2loaJAZM2aIw+GQe++9V0499VTZuHFjaF7ff/+9jBkzRlwul1x22WWSl5cnGzZskDfeeCP03cBlZWVy+OGHhy6009PTZcGCBXLxxRdLfX29XHfddbu0/wH8MvbVc2tNTY0cf/zxcuaZZ8o555wjL774ovzud78Tt9stF110Udi0f/7zn8XtdssNN9wgbW1t4na7Q+fPQw89VO666y4pKyuTBx98UD755BN1rRkIBGTSpEly+OGHy7333ivvvPOOzJw5U/x+f+gPaSIiDz74oJx44oly7rnnSnt7uzz//PNyxhlnyJtvvilTpkwJW6YPP/xQXnjhBbnmmmvE4/HIo48+KpMmTZIvv/xSBg8evLO7KWTGjBlSUlKivubD4XDIeeedJ/fee69UV1dLSkpK6GdvvPGG1NfXy3nnnbfL8wWwd51//vlyyy23yLvvviuXXnrpNqebPXu2zJo1S4444gi5/fbbxe12yxdffCHvv/9+6CsSnnnmGZk2bZpMnDhR7rnnHmlubpbHHntMjjzySFm2bNl2Pxp32bJl4vP51NcKHXbYYaGfH3nkkdu8/+LFi6V///7y9ttvy4033ihbtmyR5ORkufLKK2X27Nnb/UPZvHnzRES2OUihvr5e2tvbpbKyUv7973/Ljz/+GBo8DAB27C/n2vHjx8sLL7wgM2bMkN///vehr3t4+eWX5b777tuJLQJgb9iVvyUtXbpUXn75ZbniiiskPj5eHnroITnttNNk06ZNkpqaKiIiX331lXz66ady9tlnS48ePaSwsFAee+wxGT9+vKxcuVJiY2PDHvPqq6+W5ORkmTlzphQWFsoDDzwgV111lbzwwguhaW6++Wa59957ZerUqTJx4kT57rvvZOLEidLa2hr2WHbfo7QsS0466SRZunSpXH755TJgwAB55ZVXZNq0aTvcBmPHjpVrrrlGHnroIbnllltC59Bd/TrMq6++WjIzM2X27Nny+eefy5w5cyQpKUk+/fRT6dmzp9x5553y9ttvy3333SeDBw+WCy64ILQOJ554oixZskQuvvhiGTZsmCxcuDB07dv1637ff/99efHFF+Wqq66StLS0bf5+OOuss+SCCy6Qr776Sg499NBQLyoqks8//zzs/L6j92ocDof861//kqFDh8rll18uL7/8soiIzJw5U1asWCEffPCB+Hw+Edn1v+MtWrRIzjnnHDn66KPlnnvuERGRVatWySeffLLH/sE2hM/YxI7V1dVZImKddNJJO33fXfm6h86PLisoKAi13NxcS0SsTz/9NNQWLlxoiYgVExNjFRUVhfoTTzyhPhLH9DFnzz33nCUi1kcffRRqU6dOtWJjY60tW7aE2rp166zo6Oiwj6QtLCy0nE6n9Ze//CXsMX/44QcrOjpadQC/fqaPps7NzbWmTZumpu36UddLliyxRMQaPHiw1d7eHurnnHOO5XA4rMmTJ4fdf9SoUWEfL7V8+XJLRKxLLrkkbLobbrjBEhHr/fffD1smEbHeeecdW+s1btw4S0Ssxx9/PKw/88wzVlRUlPXxxx+H9ccff9wSEeuTTz4Je4yuX/fw8ccfWyJizZs3L6y/8847qm9rmf/85z9bPp/PWrt2bVj/4x//aDmdTmvTpk2WZf3fR6alpqZa1dXVoelee+01S0SsN954I9TGjh1rxcfHh/3esKyfPnqy08UXX2xlZWVZlZWVYdOcffbZVmJi4l7/6Exgf7K/n1v/+te/hlpbW5s1bNgwKyMjI7S8nevQu3fvsHNLe3u7lZGRYQ0ePDjsoxLffPNNS0Ss2267LdSmTZtmiYh19dVXh1owGLSmTJliud1uq6KiItS7nr/a29utwYMHWxMmTAjrImKJiPX111+HWlFRkeX1eq1TTjkl1EzX7V33k+ljLbf1dQ9r1qyxRMR67LHHwvqJJ55o5eXlhZ2rAexbtvcR5J0SExOtQw45JHR75syZYeeCdevWWVFRUdYpp5yivvam8/nf0NBgJSUlWZdeemnYz7du3WolJiaq3tWUKVOs3r17q97U1GSJiPXHP/5xu/dPSEiwkpOTLY/HY916663WSy+9ZP32t7/d4X39fr/VrVs367DDDtvmNBMnTgydf91utzVjxoxf7cfl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X5uTn58vwWBQVq5cGfapAl2nEfnp9fuuXKsNGzZM/vnPf8qqVavC/hXxF198Efr59gwfPlzWrVsnW7ZsCfudVFJSIiJ6IK6IyOuvvy4NDQ3b/aoHk5aWFrEsSxoaGhikAMCW/eVcW1ZWZvyjW0dHh4gIX4UD7ON+qb8lvfTSSzJt2jT561//Gmqtra3G9w/s6HxdvX79+rBPA6iqqlIDT+2+R5mbmyvvvfeeNDY2hv0BfM2aNbaWaXuvlU3vlbS3t4cGgO0pubm5snjxYmloaAj7NIXVq1eHfr6rfD6fnHDCCTJ//nz529/+Ji+88IKMGTMmbPDazrxXU1paKldffbUcd9xx4na75YYbbpCJEyeGptndv+O53W6ZOnWqTJ06VYLBoFxxxRXyxBNPyK233spguT0kaseTACI33XST+Hw+ueSSS6SsrEz9fMOGDfLggw/uhSXbsc4RUl1Hg3V9s9PpdMoxxxwjr776augNBpGffkl1/Z6dU089VZxOp8yePVs9rmVZUlVVtQfXAMC+Kj8/Xz7//HNpb28PtTfffFOKi4v36HyOP/54EdHnrc5POZgyZcoenZ/IT59As2XLFvnf//1f9bOWlhZpamra4f0DgYD8+c9/Vj/z+/22XkCceeaZ8tlnn8nChQvVz2pra3f6jYn09HQZO3as/Otf/5JNmzaF/azzXO50OuW0006T//73v8bBDBUVFTs1TwA7b385t/r9fnniiSdCt9vb2+WJJ56Q9PR0GT58+HbvO2LECMnIyJDHH3887KNyFyxYIKtWrTIu2yOPPBL6f8uy5JFHHhGXyyVHH320iPx0fnM4HGFv+BYWFm7ze30/++yzsO/1LC4ultdee02OO+64bf6LDLs6B25s63fB+eefLytXrpQbb7xRnE6nnH322bs1PwB71/vvvy9//vOfpVevXtv9Q/3JJ58sUVFRcvvtt6t/Ddd5rTZx4kRJSEiQO++8M/THqp/b0bXaSSedJC6XSx599NGwx3788cele/fucsQRR4R6aWmprF69Omw+nV/X8OSTT4ZaMBiUuXPnSkpKivH8/p///EdiY2PllFNOMS6T6avEamtr5b///a/k5ORs92PbAaDT/nSuPeigg2TRokVh768GAgF58cUXJT4+Pux76wHse36pvyU5nU71t5iHH354m58ksCNHH320REdHy2OPPRbWf/7aupPd9yiPP/548fv9YY8ZCATk4YcftrVM23utnJ+fLx999FFYmzNnzi6v/7Ycf/zxEggE1Hb4+9//Lg6HQyZPnrxbj3/WWWdJSUmJ/POf/5TvvvtOfR3azrxXc+mll0owGJQnn3xS5syZI9HR0XLxxReHjpPd+Tte159FRUWFPl3X9JVG2DV8kgJsyc/Pl//85z9y1llnyYABA+SCCy6QwYMHS3t7u3z66acyf/58mT59+t5eTKOEhAQZO3as3HvvvdLR0SHdu3eXd999VwoKCtS0s2bNknfffVdGjx4tv/vd70In48GDB8vy5ctD0+Xn58sdd9whN998sxQWFsrJJ58s8fHxUlBQIK+88opcdtllcsMNN0RwLQHsDZdccom89NJLMmnSJDnzzDNlw4YN8uyzz+7xF8wHH3ywTJs2TebMmSO1tbUybtw4+fLLL+Xpp5+Wk08+WY466qg9Oj+Rn/5A9OKLL8rll18uS5YskdGjR0sgEJDVq1fLiy++KAsXLpQRI0Zs8/7jxo2TGTNmyF133SXLly+X4447Tlwul6xbt07mz58vDz74oJx++unbXYYbb7xRXn/9dTnhhBNk+vTpMnz4cGlqapIffvhBXnrpJSksLJS0tLSdWq+HHnpIjjzySPnNb34jl112mfTq1UsKCwvlrbfeCp3n7777blmyZImMHDlSLr30Uhk4cKBUV1fLt99+K4sXL5bq6uqdmieAnbO/nFuzs7PlnnvukcLCQjnooIPkhRdekOXLl8ucOXO2+72HIj99L+I999wjF154oYwbN07OOeccKSsrkwcffFDy8vLk+uuvD5ve6/XKO++8I9OmTZORI0fKggUL5K233pJbbrkl9K96p0yZIn/7299k0qRJ8tvf/lbKy8vlH//4h/Tp00e+//57tQyDBw+WiRMnyjXXXCMejyf0JvPs2bN3a7uISOiPeNdcc41MnDhRDUSYMmWKpKamyvz582Xy5Mn8gQ74FVmwYIGsXr1a/H6/lJWVyfvvvy+LFi2S3Nxcef3118Xr9W7zvn369JE//elP8uc//1nGjBkjp556qng8Hvnqq68kOztb7rrrLklISJDHHntMzj//fPnNb34jZ599tqSnp8umTZvkrbfektGjRxvfWO7Uo0cPue666+S+++6Tjo4OOfTQQ+XVV1+Vjz/+WObNmxc2COvmm2+W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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image = input_volume[0, 0].detach().cpu()\n", + "reference = reference_volume[0, 0].detach().cpu()\n", + "probabilities = torch.softmax(logits[0], dim=0).detach().cpu()\n", + "argmax_prediction = argmax_predictions[0][0].detach().cpu()\n", + "rankseg_prediction = rankseg_predictions[0][0].detach().cpu()\n", + "reference_tumor = reference == 2\n", + "argmax_tumor = argmax_prediction == 2\n", + "rankseg_tumor = rankseg_prediction == 2\n", + "slice_index = int(reference_tumor.sum(dim=(0, 1)).argmax().item())\n", + "coordinates = torch.nonzero(reference_tumor[:, :, slice_index], as_tuple=False)\n", + "margin = 24\n", + "row_start = max(0, int(coordinates[:, 0].min()) - margin)\n", + "row_end = min(reference.shape[0], int(coordinates[:, 0].max()) + margin + 1)\n", + "column_start = max(0, int(coordinates[:, 1].min()) - margin)\n", + "column_end = min(reference.shape[1], int(coordinates[:, 1].max()) + margin + 1)\n", + "decoder_difference = argmax_tumor != rankseg_tumor\n", + "\n", + "\n", + "def crop(panel):\n", + " return panel[row_start:row_end, column_start:column_end, slice_index]\n", + "\n", + "\n", + "figure, axes = plt.subplots(1, 6, figsize=(21, 4))\n", + "panels = [\n", + " (crop(image), \"CT image\", \"gray\"),\n", + " (crop(reference_tumor), \"Tumor reference\", \"gray\"),\n", + " (crop(probabilities[2]), \"Tumor probability\", \"magma\"),\n", + " (crop(argmax_tumor), f\"Argmax tumor\\nDice {argmax_class_dice[1]:.3f}\", \"gray\"),\n", + " (crop(rankseg_tumor), f\"RankSEG tumor\\nDice {rankseg_class_dice[1]:.3f}\", \"gray\"),\n", + " (crop(decoder_difference), \"Changed tumor voxels\", \"hot\"),\n", + "]\n", + "for axis, (panel, title, color_map) in zip(axes, panels):\n", + " axis.imshow(panel, cmap=color_map)\n", + " axis.set_title(title)\n", + " axis.axis(\"off\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use RankSEGd in a dictionary post-processing pipeline\n", + "\n", + "MONAI dictionary transforms inherit from `MapTransform`, copy the input mapping, and apply an array transform to the requested keys. `RankSEGd` therefore keeps the RankSEG-specific logic in `RankSEG` while fitting workflows in which predictions and metadata are carried under named keys." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:37:00.924290Z", + "iopub.status.busy": "2026-07-31T08:37:00.924192Z", + "iopub.status.idle": "2026-07-31T08:37:01.352625Z", + "shell.execute_reply": "2026-07-31T08:37:01.352115Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dictionary output shape: (1, 406, 406, 228)\n" + ] + } + ], + "source": [ + "class RankSEGd(MapTransform):\n", + " \"\"\"Dictionary wrapper for :class:`RankSEG`.\"\"\"\n", + "\n", + " backend = RankSEG.backend\n", + "\n", + " def __init__(\n", + " self,\n", + " keys,\n", + " metric: str = \"dice\",\n", + " solver: str = \"RMA\",\n", + " pruning_prob: float = 0.5,\n", + " allow_missing_keys: bool = False,\n", + " **solver_params,\n", + " ) -> None:\n", + " super().__init__(keys, allow_missing_keys)\n", + " self.converter = RankSEG(\n", + " metric=metric,\n", + " solver=solver,\n", + " pruning_prob=pruning_prob,\n", + " **solver_params,\n", + " )\n", + "\n", + " def __call__(self, data: Mapping[Hashable, object]) -> dict[Hashable, object]:\n", + " output = dict(data)\n", + " for key in self.key_iterator(output):\n", + " output[key] = self.converter(output[key])\n", + " return output\n", + "\n", + "\n", + "post_ranksegd = Compose(\n", + " [\n", + " Activationsd(keys=\"pred\", softmax=True),\n", + " RankSEGd(keys=\"pred\", metric=\"dice\"),\n", + " ]\n", + ")\n", + "\n", + "dictionary_samples = decollate_batch({\"pred\": logits})\n", + "dictionary_result = post_ranksegd(dictionary_samples[0])\n", + "torch.testing.assert_close(dictionary_result[\"pred\"], rankseg_predictions[0])\n", + "print(f\"dictionary output shape: {tuple(dictionary_result['pred'].shape)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Practical notes\n", + "\n", + "- Use `metric=\"iou\"` when IoU is the target metric.\n", + "- RankSEG expects finite probabilities in `[0, 1]`; apply softmax to multiclass logits or sigmoid to multilabel logits first.\n", + "- In the fixed 20-case comparison from which this case was selected, RankSEG (Dice) changed mean samplewise Dice by `+0.0209` and won on 18 of 20 cases. Use that paired evaluation, rather than this selected example alone, for performance claims.\n", + "- This wrapper intentionally demonstrates non-overlapping multiclass output. Multilabel RankSEG returns `(C, *spatial)` binary masks after removing the temporary batch dimension and needs slightly different shape handling.\n", + "- The Bundle and this tutorial are research examples and are not intended for diagnostic use.\n", + "\n", + "Further reading:\n", + "\n", + "- [RankSEG repository and documentation](https://github.com/rankseg/rankseg)\n", + "- [RankSEG: A Consistent Ranking-based Framework for Segmentation](https://www.jmlr.org/papers/v24/22-0712.html)\n", + "- [RankSEG-RMA: An Efficient Segmentation Algorithm via Reciprocal Moment Approximation](https://openreview.net/forum?id=4tRMm1JJhw)\n", + "- [Reproducible MONAI Pancreas comparison](https://github.com/rankseg/rankseg/tree/2f9e7e9e36145d960d3b568f340c4ccb60b397d4/experiments/monai-pr-8908-reproducibility)\n", + "- [MONAI Pancreas CT DiNTS Segmentation Bundle](https://github.com/Project-MONAI/model-zoo/tree/dev/models/pancreas_ct_dints_segmentation)\n", + "- [Discussion of Option B in MONAI PR #8908](https://github.com/Project-MONAI/MONAI/pull/8908#issuecomment-5077397523)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup data directory\n", + "\n", + "Remove downloaded data and model files only when a temporary directory was used." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-31T08:37:01.354156Z", + "iopub.status.busy": "2026-07-31T08:37:01.353986Z", + "iopub.status.idle": "2026-07-31T08:37:01.428784Z", + "shell.execute_reply": "2026-07-31T08:37:01.428162Z" + } + }, + "outputs": [], + "source": [ + "if directory is None:\n", + " shutil.rmtree(root_dir)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/runner.sh b/runner.sh index fc9c19ee8..5a24aa2a2 100755 --- a/runner.sh +++ b/runner.sh @@ -24,6 +24,7 @@ doesnt_contain_max_epochs=() doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" msd_datalist_generator.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" load_medical_images.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" integrate_3rd_party_transforms.ipynb) +doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" rankseg_integration.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" transform_speed.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" transforms_demo_2d.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" nifti_read_example.ipynb)