!pip install panoptica auxiliary rich numpy > /dev/null
If you installed the packages and requirements on your own machine, you can skip this section and start from the import section.
Otherwise you can follow and execute the tutorial on your browser. In order to start working on the notebook, click on the following button, this will open this page in the Colab environment and you will be able to execute the code on your own (Google account required).
Now that you are visualizing the notebook in Colab, run the next cell to install the packages we will use. There are few things you should follow in order to properly set the notebook up:
If you run the next cell in a Google Colab environment, it will clone the 'tutorials' repository in your google drive. This will create a new folder called "tutorials" in your Google Drive. All generated file will be created/uploaded to your Google Drive respectively.
After the first execution of the next cell, you might receive some warnings and notifications, please follow these instructions:
Afterwards the "tutorials" folder has been created. You can navigate it through the lefthand panel in Colab. You might also have received an email that informs you about the access on your Google Drive.
import sys
# Check if we are in google colab currently
try:
import google.colab
colabFlag = True
except ImportError as r:
colabFlag = False
# Execute certain steps only if we are in a colab environment
if colabFlag:
# Create a folder in your Google Drive
from google.colab import drive
drive.mount("/content/drive")
# clone repository and set path
!git clone https://github.com/BrainLesion/tutorials.git /content/drive/MyDrive/tutorials
BASE_PATH = "/content/drive/MyDrive/tutorials/panoptica"
sys.path.insert(0, BASE_PATH)
else: # normal jupyter notebook environment
BASE_PATH = "." # current working directory would be BraTs-Toolkit anyways if you are not in colab
import numpy as np
from auxiliary.nifti.io import read_nifti
from rich import print as pprint
from panoptica import InputType, Panoptica_Evaluator
from panoptica.metrics import Metric
For demonstration purposes, we will quickly define some numpy predictions and will compare it to the same reference array
reference_array = np.array(
[
[0, 1, 0, 0, 0],
[1, 1, 0, 0, 0],
[0, 0, 0, 2, 0],
[0, 0, 2, 2, 2],
[0, 0, 0, 2, 0],
]
)
# Lets give them names, in practice this should be sample/subject names for easy recognition
predictions = {
"subject_perfect": np.array(
[
[0, 1, 0, 0, 0],
[1, 1, 0, 0, 0],
[0, 0, 0, 2, 0],
[0, 0, 2, 2, 2],
[0, 0, 0, 2, 0],
]
),
"subject_horrible": np.array(
[
[0, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 0, 0, 0, 2],
[0, 0, 0, 0, 2],
[0, 0, 0, 0, 2],
]
),
"subject_overprediction": np.array(
[
[0, 1, 0, 0, 0],
[1, 1, 1, 0, 0],
[0, 1, 2, 2, 2],
[0, 0, 2, 2, 2],
[0, 0, 0, 2, 2],
]
),
}
# let's calculate one sample as usual
# Let's define that label 1 and 2 (see arrays above) should be treated as different groups
from panoptica.utils import SegmentationClassGroups, LabelGroup
# (in practice, this could be different classes of labels, instead of multiple instances of the same class)
segmentation_class_groups = SegmentationClassGroups(
{"Structure1": LabelGroup(1), "Structure2": LabelGroup(2)}
)
evaluator = Panoptica_Evaluator(
expected_input=InputType.MATCHED_INSTANCE,
decision_metric=Metric.IOU,
decision_threshold=0.0,
segmentation_class_groups=segmentation_class_groups,
)
result = evaluator.evaluate(predictions["subject_perfect"], reference_array)
Panoptic: Start Evaluation -- Got MatchedInstancePair, will evaluate instances Panoptic: Start Evaluation -- Got MatchedInstancePair, will evaluate instances
The results object allows access to individual metrics and provides helper methods for further processing
# print all results by using the group names
print(result["structure1"])
print(result["structure2"])
+++ MATCHING +++ Number of instances in reference (num_ref_instances): 1 Number of instances in prediction (num_pred_instances): 1 True Positives (tp): 1 False Positives (fp): 0 False Negatives (fn): 0 Recognition Quality / F1-Score (rq): 1.0 +++ GLOBAL +++ Global Binary Dice (global_bin_dsc): 1.0 +++ INSTANCE +++ Segmentation Quality IoU (sq): 1.0 +- 0.0 Panoptic Quality IoU (pq): 1.0 Segmentation Quality Dsc (sq_dsc): 1.0 +- 0.0 Panoptic Quality Dsc (pq_dsc): 1.0 Segmentation Quality ASSD (sq_assd): 0.0 +- 0.0 Segmentation Quality Relative Volume Difference (sq_rvd): 0.0 +- 0.0 +++ MATCHING +++ Number of instances in reference (num_ref_instances): 1 Number of instances in prediction (num_pred_instances): 1 True Positives (tp): 1 False Positives (fp): 0 False Negatives (fn): 0 Recognition Quality / F1-Score (rq): 1.0 +++ GLOBAL +++ Global Binary Dice (global_bin_dsc): 1.0 +++ INSTANCE +++ Segmentation Quality IoU (sq): 1.0 +- 0.0 Panoptic Quality IoU (pq): 1.0 Segmentation Quality Dsc (sq_dsc): 1.0 +- 0.0 Panoptic Quality Dsc (pq_dsc): 1.0 Segmentation Quality ASSD (sq_assd): 0.0 +- 0.0 Segmentation Quality Relative Volume Difference (sq_rvd): 0.0 +- 0.0
Now lets define a Panoptica-Aggregator that aggregates and collects these results in a file.
from panoptica import Panoptica_Aggregator
from pathlib import Path
import os
output_file = str(Path(os.path.abspath("")).parent.joinpath("example_aggregation.tsv"))
aggregator = Panoptica_Aggregator(
panoptica_evaluator=evaluator,
output_file=output_file,
log_times=True,
)
print(aggregator.evaluation_metrics)
['num_ref_instances', 'num_pred_instances', 'tp', 'fp', 'fn', 'prec', 'rec', 'rq', 'sq', 'sq_std', 'pq', 'sq_dsc', 'sq_dsc_std', 'pq_dsc', 'sq_assd', 'sq_assd_std', 'sq_rvd', 'sq_rvd_std', 'global_bin_dsc', 'computation_time']
# loop over our predictions
# in a real scenario, any other form of iteratively receiving prediction data and feeding it into the aggregator works fine as well!
for name, pred_arr in predictions.items():
aggregator.evaluate(pred_arr, reference_array, name)
Call evaluate on subject_perfect Saved entry subject_perfect into /DATA/NAS/ongoing_projects/hendrik/panoptica/tutorials/example_aggregation.tsv Call evaluate on subject_horrible Saved entry subject_horrible into /DATA/NAS/ongoing_projects/hendrik/panoptica/tutorials/example_aggregation.tsv Call evaluate on subject_overprediction Saved entry subject_overprediction into /DATA/NAS/ongoing_projects/hendrik/panoptica/tutorials/example_aggregation.tsv
After evaluating all samples in a dataset, we can use the created .tsv externally. Or we use the inbuild panoptica statistics features to easily get statisics. Let's do that!
statistics_obj = aggregator.make_statistic()
# equivalent would be
# from panoptica import Panoptica_Statistic
# statistics_obj = Panoptica_Statistic.from_file(<path to your .tsv file>)
statistics_obj.print_summary()
Found 3 entries Found metrics: ['num_ref_instances', 'num_pred_instances', 'tp', 'fp', 'fn', 'prec', 'rec', 'rq', 'sq', 'sq_std', 'pq', 'sq_dsc', 'sq_dsc_std', 'pq_dsc', 'sq_assd', 'sq_assd_std', 'sq_rvd', 'sq_rvd_std', 'global_bin_dsc', 'computation_time'] Found groups: ['structure2', 'structure1'] Group across_groups: num_ref_instances : 1.0 +- 0.0 num_pred_instances : 1.0 +- 0.0 tp : 1.0 +- 0.0 fp : 0.0 +- 0.0 fn : 0.0 +- 0.0 prec : 1.0 +- 0.0 rec : 1.0 +- 0.0 rq : 1.0 +- 0.0 sq : 0.617 +- 0.028 sq_std : 0.0 +- 0.0 pq : 0.617 +- 0.028 sq_dsc : 0.712 +- 0.038 sq_dsc_std : 0.0 +- 0.0 pq_dsc : 0.712 +- 0.038 sq_assd : 0.311 +- 0.038 sq_assd_std : 0.0 +- 0.0 sq_rvd : 0.033 +- 0.033 sq_rvd_std : 0.0 +- 0.0 global_bin_dsc : 0.712 +- 0.038 computation_time : 0.982 +- 0.057
With this statistics object, we can get the metric values for each group, metric, and sample interchangeably
statistics_obj.get_one_subject("subject_perfect")
# gets the values for one subject (map from metric to value)
statistics_obj.get_one_group("structure1")
# This gets one group, so mapping metric to the values
statistics_obj.get_one_metric("global_bin_dsc")
# This gets one metric, so mapping group to the values
{'structure1': [1.0, 0.5, 0.75], 'structure2': [1.0, 0.25, 0.7692307692307693]}
# get the metric values for a group and metric
statistics_obj.get("structure1", "global_bin_dsc")
[1.0, 0.5, 0.75]
# get a summary dictionary across groups and metrics
statistics_obj.get_summary_dict(
include_across_group=False
) # we set this to false because we only have on group, otherwise this would yield also the averages across all groups
{'structure1': {'num_ref_instances': [1.0, 1.0], avg = 1.0 +- 0.0, 'num_pred_instances': [1.0, 1.0], avg = 1.0 +- 0.0, 'tp': [1.0, 1.0], avg = 1.0 +- 0.0, 'fp': [0.0, 0.0], avg = 0.0 +- 0.0, 'fn': [0.0, 0.0], avg = 0.0 +- 0.0, 'prec': [1.0, 1.0], avg = 1.0 +- 0.0, 'rec': [1.0, 1.0], avg = 1.0 +- 0.0, 'rq': [1.0, 1.0], avg = 1.0 +- 0.0, 'sq': [0.333, 1.0], avg = 0.644 +- 0.274, 'sq_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'pq': [0.333, 1.0], avg = 0.644 +- 0.274, 'sq_dsc': [0.5, 1.0], avg = 0.75 +- 0.204, 'sq_dsc_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'pq_dsc': [0.5, 1.0], avg = 0.75 +- 0.204, 'sq_assd': [0.0, 0.417], avg = 0.273 +- 0.193, 'sq_assd_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'sq_rvd': [-0.667, 0.667], avg = 0.0 +- 0.544, 'sq_rvd_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'global_bin_dsc': [0.5, 1.0], avg = 0.75 +- 0.204, 'computation_time': [0.76, 1.19], avg = 0.926 +- 0.189}, 'structure2': {'num_ref_instances': [1.0, 1.0], avg = 1.0 +- 0.0, 'num_pred_instances': [1.0, 1.0], avg = 1.0 +- 0.0, 'tp': [1.0, 1.0], avg = 1.0 +- 0.0, 'fp': [0.0, 0.0], avg = 0.0 +- 0.0, 'fn': [0.0, 0.0], avg = 0.0 +- 0.0, 'prec': [1.0, 1.0], avg = 1.0 +- 0.0, 'rec': [1.0, 1.0], avg = 1.0 +- 0.0, 'rq': [1.0, 1.0], avg = 1.0 +- 0.0, 'sq': [0.143, 1.0], avg = 0.589 +- 0.351, 'sq_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'pq': [0.143, 1.0], avg = 0.589 +- 0.351, 'sq_dsc': [0.25, 1.0], avg = 0.673 +- 0.314, 'sq_dsc_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'pq_dsc': [0.25, 1.0], avg = 0.673 +- 0.314, 'sq_assd': [0.0, 0.833], avg = 0.349 +- 0.353, 'sq_assd_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'sq_rvd': [-0.4, 0.6], avg = 0.067 +- 0.411, 'sq_rvd_std': [0.0, 0.0], avg = 0.0 +- 0.0, 'global_bin_dsc': [0.25, 1.0], avg = 0.673 +- 0.314, 'computation_time': [0.878, 1.123], avg = 1.039 +- 0.114}}
We can also create figures and some useful ordering with this
!pip install --upgrade nbformat
Requirement already satisfied: nbformat in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (5.10.4) Requirement already satisfied: fastjsonschema>=2.15 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from nbformat) (2.21.1) Requirement already satisfied: jsonschema>=2.6 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from nbformat) (4.23.0) Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from nbformat) (5.7.1) Requirement already satisfied: traitlets>=5.1 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from nbformat) (5.14.1) Requirement already satisfied: attrs>=22.2.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from jsonschema>=2.6->nbformat) (23.1.0) Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from jsonschema>=2.6->nbformat) (2024.10.1) Requirement already satisfied: referencing>=0.28.4 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from jsonschema>=2.6->nbformat) (0.36.2) Requirement already satisfied: rpds-py>=0.7.1 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from jsonschema>=2.6->nbformat) (0.22.3) Requirement already satisfied: platformdirs>=2.5 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from jupyter-core!=5.0.*,>=4.12->nbformat) (4.1.0) Requirement already satisfied: typing-extensions>=4.4.0 in /opt/anaconda3/envs/seg11panoptdev/lib/python3.11/site-packages (from referencing>=0.28.4->jsonschema>=2.6->nbformat) (4.9.0)
statistics_obj.get_summary_figure(metric="global_bin_dsc", horizontal=False)
# This plots one bar for each group on this metric by default
# We can also make plots over multiple statistics objects (usually reflecting different algorithms/predictors)
from panoptica.panoptica_statistics import make_curve_over_setups
# we simulate this by multiplying our statistics object
make_curve_over_setups(
statistics_dict={
"algorithm1": statistics_obj,
"algorithm2": statistics_obj,
"algorithm3": statistics_obj,
},
metric="global_bin_dsc",
)
# of course, as we use the same statistic object multiple times, each pair of bars is identical