{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Use Case: Unmatched Instances Input" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Install Dependencies" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: panoptica in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from -r requirements.txt (line 1)) (1.0.0.post2.dev0+2f7d01f)\n", "Requirement already satisfied: auxiliary in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from -r requirements.txt (line 2)) (0.0.42)\n", "Requirement already satisfied: rich in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from -r requirements.txt (line 3)) (13.6.0)\n", "Requirement already satisfied: numpy in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from -r requirements.txt (line 4)) (1.25.2)\n", "Requirement already satisfied: connected-components-3d<4.0.0,>=3.12.3 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from panoptica->-r requirements.txt (line 1)) (3.12.3)\n", "Requirement already satisfied: ruamel.yaml<0.19.0,>=0.18.6 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from panoptica->-r requirements.txt (line 1)) (0.18.6)\n", "Requirement already satisfied: scikit-image<0.23.0,>=0.22.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from panoptica->-r requirements.txt (line 1)) (0.22.0)\n", "Requirement already satisfied: scipy<2.0.0,>=1.7.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from panoptica->-r requirements.txt (line 1)) (1.11.2)\n", "Requirement already satisfied: nibabel>=3.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from auxiliary->-r requirements.txt (line 2)) (5.1.0)\n", "Requirement already satisfied: path>=16.10.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from auxiliary->-r requirements.txt (line 2)) (17.0.0)\n", "Requirement already satisfied: pathlib>=1.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from auxiliary->-r requirements.txt (line 2)) (1.0.1)\n", "Requirement already satisfied: pillow>=10.0.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from auxiliary->-r requirements.txt (line 2)) (10.0.0)\n", "Requirement already satisfied: tifffile>=2023.8.25 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from auxiliary->-r requirements.txt (line 2)) (2023.8.30)\n", "Requirement already satisfied: markdown-it-py>=2.2.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from rich->-r requirements.txt (line 3)) (3.0.0)\n", "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from rich->-r requirements.txt (line 3)) (2.17.2)\n", "Requirement already satisfied: mdurl~=0.1 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from markdown-it-py>=2.2.0->rich->-r requirements.txt (line 3)) (0.1.2)\n", "Requirement already satisfied: packaging>=17 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from nibabel>=3.0->auxiliary->-r requirements.txt (line 2)) (23.1)\n", "Requirement already satisfied: ruamel.yaml.clib>=0.2.7 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from ruamel.yaml<0.19.0,>=0.18.6->panoptica->-r requirements.txt (line 1)) (0.2.8)\n", "Requirement already satisfied: networkx>=2.8 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from scikit-image<0.23.0,>=0.22.0->panoptica->-r requirements.txt (line 1)) (3.1)\n", "Requirement already satisfied: imageio>=2.27 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from scikit-image<0.23.0,>=0.22.0->panoptica->-r requirements.txt (line 1)) (2.31.3)\n", "Requirement already satisfied: lazy_loader>=0.3 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from scikit-image<0.23.0,>=0.22.0->panoptica->-r requirements.txt (line 1)) (0.3)\n" ] } ], "source": [ "!pip install -r requirements.txt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup Imports" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from auxiliary.nifti.io import read_nifti\n", "from rich import print as pprint\n", "from panoptica import NaiveThresholdMatching, Panoptica_Evaluator, InputType\n", "from panoptica.utils.segmentation_class import LabelGroup, SegmentationClassGroups" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load Example Data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To demonstrate we use a reference and predicition of spine a segmentation with unmatched instances.\n", "\n", "\n", "![unmatched_instance_figure](figures/unmatched_instance.png)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(array([ 0, 2, 3, 4, 5, 6, 7, 8, 26, 102, 103, 104, 105,\n", " 106, 107, 108, 202, 203, 204, 205, 206, 207, 208], dtype=uint8),\n", " array([ 0, 3, 4, 5, 6, 7, 8, 9, 26, 103, 104, 105, 106,\n", " 107, 108, 109, 203, 204, 205, 206, 207, 208, 209], dtype=uint8))" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ref_masks = read_nifti(\"./spine_seg/unmatched_instance/ref.nii.gz\")\n", "pred_masks = read_nifti(\"./spine_seg/unmatched_instance/pred.nii.gz\")\n", "\n", "# labels are unmatched instances\n", "pred_masks[pred_masks == 27] = 26 # For later\n", "np.unique(ref_masks), np.unique(pred_masks)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define Class Groups" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# Define (optionally) semantic groups\n", "# This means that only instance within one group can be matched to each other\n", "segmentation_class_groups = SegmentationClassGroups(\n", " {\n", " \"vertebra\": LabelGroup(list(range(1, 11))),\n", " \"ivd\": LabelGroup(list(range(101, 111))),\n", " \"sacrum\": ([26], True),\n", " \"endplate\": LabelGroup(list(range(201, 211))),\n", " }\n", ")\n", "# In this case, the label 26 can only be matched with label 26 (thats why have to ensure above that 26 exists in both masks, otherwise they wouldn't be matched)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's do it ourselves!\n", "Panoptica allows you to call everything yourself if you really want to" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "prediction_arr= [ 0 2 3 4 5 6 7 8 26 102 103 104 105 106 107 108 202 204\n", " 206 207 209 210 211]\n", "reference_arr= [ 0 2 3 4 5 6 7 8 26 102 103 104 105 106 107 108 202 203\n", " 204 205 206 207 208]\n" ] } ], "source": [ "# Input are unmatched instances, so lets match em!\n", "from panoptica import Metric\n", "\n", "# This will match based on IoU metric, will only match if instance have a IoU of 0.5 or higher and will not allow multiple predictions to be matched to the same reference\n", "matcher = NaiveThresholdMatching(\n", " matching_metric=Metric.IOU, matching_threshold=0.5, allow_many_to_one=False\n", ")\n", "\n", "# Now we have to do our processing object ourselves\n", "from panoptica import UnmatchedInstancePair\n", "\n", "unmatched_instance_input = UnmatchedInstancePair(pred_masks, ref_masks)\n", "\n", "matched_instance_output = matcher.match_instances(unmatched_instance_input)\n", "print(\"prediction_arr=\", np.unique(matched_instance_output.prediction_arr))\n", "print(\"reference_arr=\", np.unique(matched_instance_output.reference_arr))\n", "\n", "# Based of this, we see that some references are not sucessfully hit (203, 205, 208)\n", "# We can also see that we indeed have the same number of prediction instances that got no match, they will be appended afterwards (209, 210, 211)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's match 'em all!" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "prediction_arr= [ 0 2 3 4 5 6 7 8 26 102 103 104 105 106 107 108 202 203\n", " 204 205 206 207 208]\n", "reference_arr= [ 0 2 3 4 5 6 7 8 26 102 103 104 105 106 107 108 202 203\n", " 204 205 206 207 208]\n" ] } ], "source": [ "# This will match based on IoU metric, will only match if instance have a IoU of 0.0 or higher and will not allow multiple predictions to be matched to the same reference\n", "matcher = NaiveThresholdMatching(\n", " matching_metric=Metric.IOU, matching_threshold=0.0, allow_many_to_one=False\n", ")\n", "\n", "matched_instance_output = matcher.match_instances(unmatched_instance_input)\n", "print(\"prediction_arr=\", np.unique(matched_instance_output.prediction_arr))\n", "print(\"reference_arr=\", np.unique(matched_instance_output.reference_arr))\n", "\n", "# With a threshold of 0.0, we ensure that we match as much as possible.\n", "# We see, that contrary to before, instances 203, 205, and 208 are now matched" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Do it yourself\n", "\n", "Now it is up to you to explore the different matching algorithms and the best setup for your project\n", "\n", "Just remember, this setup can have drastic differences in the resulting metrics as well as interpretation of those results. For example, if you always match everything, of course your F1-Score will be 1.0. This becomes meaningless then. Also the choice of metric does matter!" ] } ], "metadata": { "kernelspec": { "display_name": "brainles", "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.11.6" } }, "nbformat": 4, "nbformat_minor": 2 }