Skip to content

Commit

Permalink
Merge pull request #102 from BrainLesion/relative_volume_difference
Browse files Browse the repository at this point in the history
added relative_volume_difference metric, where average positive means…
  • Loading branch information
Hendrik-code authored Apr 18, 2024
2 parents 0d7e0c4 + b6b72fc commit dcee093
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 16 deletions.
1 change: 1 addition & 0 deletions panoptica/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
UnmatchedInstancePair,
MatchedInstancePair,
)
from panoptica.metrics import Metric, MetricMode, MetricType
11 changes: 10 additions & 1 deletion panoptica/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from panoptica.metrics.assd import (
_average_surface_distance,
_compute_instance_average_symmetric_surface_distance,
_average_symmetric_surface_distance,
)
from panoptica.metrics.cldice import (
Expand All @@ -10,6 +10,15 @@
_compute_dice_coefficient,
_compute_instance_volumetric_dice,
)

# from panoptica.metrics.overunder_segmentation import (
# _compute_instance_segmentation_tendency,
# _compute_segmentation_tendency,
# )
from panoptica.metrics.relative_volume_difference import (
_compute_instance_relative_volume_difference,
_compute_relative_volume_difference,
)
from panoptica.metrics.iou import _compute_instance_iou, _compute_iou
from panoptica.metrics.metrics import (
Evaluation_List_Metric,
Expand Down
25 changes: 25 additions & 0 deletions panoptica/metrics/assd.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
from scipy.ndimage._nd_image import euclidean_feature_transform


def _compute_instance_average_symmetric_surface_distance(
ref_labels: np.ndarray,
pred_labels: np.ndarray,
ref_instance_idx: int | None = None,
pred_instance_idx: int | None = None,
voxelspacing=None,
connectivity=1,
):
if ref_instance_idx is None and pred_instance_idx is None:
return _average_symmetric_surface_distance(
reference=ref_labels,
prediction=pred_labels,
voxelspacing=voxelspacing,
connectivity=connectivity,
)
ref_instance_mask = ref_labels == ref_instance_idx
pred_instance_mask = pred_labels == pred_instance_idx
return _average_symmetric_surface_distance(
reference=ref_instance_mask,
prediction=pred_instance_mask,
voxelspacing=voxelspacing,
connectivity=connectivity,
)


def _average_symmetric_surface_distance(
reference,
prediction,
Expand Down
9 changes: 7 additions & 2 deletions panoptica/metrics/cldice.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def cl_score(volume: np.ndarray, skeleton: np.ndarray):
def _compute_centerline_dice(
ref_labels: np.ndarray,
pred_labels: np.ndarray,
ref_instance_idx: int,
pred_instance_idx: int,
ref_instance_idx: int | None = None,
pred_instance_idx: int | None = None,
) -> float:
"""Compute the centerline Dice (clDice) coefficient between a specific pair of instances.
Expand All @@ -32,6 +32,11 @@ def _compute_centerline_dice(
Returns:
float: clDice coefficient
"""
if ref_instance_idx is None and pred_instance_idx is None:
return _compute_centerline_dice_coefficient(
reference=ref_labels,
prediction=pred_labels,
)
ref_instance_mask = ref_labels == ref_instance_idx
pred_instance_mask = pred_labels == pred_instance_idx
return _compute_centerline_dice_coefficient(
Expand Down
9 changes: 7 additions & 2 deletions panoptica/metrics/dice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
def _compute_instance_volumetric_dice(
ref_labels: np.ndarray,
pred_labels: np.ndarray,
ref_instance_idx: int,
pred_instance_idx: int,
ref_instance_idx: int | None = None,
pred_instance_idx: int | None = None,
) -> float:
"""
Compute the Dice coefficient between a specific pair of instances.
Expand All @@ -25,6 +25,11 @@ def _compute_instance_volumetric_dice(
float: Dice coefficient between the specified instances. A value between 0 and 1, where higher values
indicate better overlap and similarity between instances.
"""
if ref_instance_idx is None and pred_instance_idx is None:
return _compute_dice_coefficient(
reference=ref_labels,
prediction=pred_labels,
)
ref_instance_mask = ref_labels == ref_instance_idx
pred_instance_mask = pred_labels == pred_instance_idx
return _compute_dice_coefficient(
Expand Down
9 changes: 7 additions & 2 deletions panoptica/metrics/iou.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
def _compute_instance_iou(
reference_arr: np.ndarray,
prediction_arr: np.ndarray,
ref_instance_idx: int,
pred_instance_idx: int,
ref_instance_idx: int | None = None,
pred_instance_idx: int | None = None,
) -> float:
"""
Compute Intersection over Union (IoU) between a specific pair of reference and prediction instances.
Expand All @@ -19,6 +19,11 @@ def _compute_instance_iou(
Returns:
float: IoU between the specified instances.
"""
if ref_instance_idx is None and pred_instance_idx is None:
return _compute_iou(
reference_arr=reference_arr,
prediction_arr=prediction_arr,
)
ref_instance_mask = reference_arr == ref_instance_idx
pred_instance_mask = prediction_arr == pred_instance_idx
return _compute_iou(ref_instance_mask, pred_instance_mask)
Expand Down
31 changes: 23 additions & 8 deletions panoptica/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import numpy as np

from panoptica.metrics import (
_average_symmetric_surface_distance,
_compute_centerline_dice_coefficient,
_compute_dice_coefficient,
_compute_iou,
_compute_instance_average_symmetric_surface_distance,
_compute_centerline_dice,
_compute_instance_volumetric_dice,
_compute_instance_iou,
_compute_instance_relative_volume_difference,
# _compute_instance_segmentation_tendency,
)
from panoptica.utils.constants import _Enum_Compare, auto

Expand Down Expand Up @@ -89,10 +91,12 @@ class Metric(_Enum_Compare):
_type_: _description_
"""

DSC = _Metric("DSC", False, _compute_dice_coefficient)
IOU = _Metric("IOU", False, _compute_iou)
ASSD = _Metric("ASSD", True, _average_symmetric_surface_distance)
clDSC = _Metric("clDSC", False, _compute_centerline_dice_coefficient)
DSC = _Metric("DSC", False, _compute_instance_volumetric_dice)
IOU = _Metric("IOU", False, _compute_instance_iou)
ASSD = _Metric("ASSD", True, _compute_instance_average_symmetric_surface_distance)
clDSC = _Metric("clDSC", False, _compute_centerline_dice)
RVD = _Metric("RVD", True, _compute_instance_relative_volume_difference)
# ST = _Metric("ST", False, _compute_instance_segmentation_tendency)

def __call__(
self,
Expand Down Expand Up @@ -166,6 +170,8 @@ class MetricMode(_Enum_Compare):
AVG = auto()
SUM = auto()
STD = auto()
MIN = auto()
MAX = auto()


class MetricType(_Enum_Compare):
Expand Down Expand Up @@ -287,9 +293,18 @@ def __init__(
if is_edge_case:
self.AVG: float | None = edge_case_result
self.SUM: None | float = edge_case_result
self.MIN: None | float = edge_case_result
self.MAX: None | float = edge_case_result
else:
self.AVG = None if self.ALL is None else np.average(self.ALL)
self.SUM = None if self.ALL is None else np.sum(self.ALL)
self.MIN = (
None if self.ALL is None or len(self.ALL) == 0 else np.min(self.ALL)
)
self.MAX = (
None if self.ALL is None or len(self.ALL) == 0 else np.max(self.ALL)
)

self.STD = (
None
if self.ALL is None
Expand Down
70 changes: 70 additions & 0 deletions panoptica/metrics/relative_volume_difference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import numpy as np


def _compute_instance_relative_volume_difference(
ref_labels: np.ndarray,
pred_labels: np.ndarray,
ref_instance_idx: int | None = None,
pred_instance_idx: int | None = None,
) -> float:
"""
Compute the Dice coefficient between a specific pair of instances.
The Dice coefficient measures the similarity or overlap between two binary masks representing instances.
It is defined as:
Dice = (2 * intersection) / (ref_area + pred_area)
Args:
ref_labels (np.ndarray): Reference instance labels.
pred_labels (np.ndarray): Prediction instance labels.
ref_instance_idx (int): Index of the reference instance.
pred_instance_idx (int): Index of the prediction instance.
Returns:
float: Dice coefficient between the specified instances. A value between 0 and 1, where higher values
indicate better overlap and similarity between instances.
"""
if ref_instance_idx is None and pred_instance_idx is None:
return _compute_relative_volume_difference(
reference=ref_labels,
prediction=pred_labels,
)
ref_instance_mask = ref_labels == ref_instance_idx
pred_instance_mask = pred_labels == pred_instance_idx
return _compute_relative_volume_difference(
reference=ref_instance_mask,
prediction=pred_instance_mask,
)


def _compute_relative_volume_difference(
reference: np.ndarray,
prediction: np.ndarray,
*args,
) -> float:
"""
Compute the relative volume difference between two binary masks.
The relative volume difference is the predicted volume of an instance in relation to the reference volume (>0 oversegmented, <0 undersegmented)
RVD = ((pred_volume-ref_volume) / ref_volume)
Args:
reference (np.ndarray): Reference binary mask.
prediction (np.ndarray): Prediction binary mask.
Returns:
float: Relative volume Error between the two binary masks. A value between 0 and 1, where higher values
indicate better overlap and similarity between masks.
"""
reference_mask = np.sum(reference)
prediction_mask = np.sum(prediction)

# Handle division by zero
if reference_mask == 0 and prediction_mask == 0:
return 0.0

# Calculate Dice coefficient
rvd = (prediction_mask - reference_mask) / reference_mask
return rvd
3 changes: 2 additions & 1 deletion panoptica/panoptic_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@


class Panoptic_Evaluator:

def __init__(
self,
expected_input: (
Expand All @@ -26,7 +27,7 @@ def __init__(
instance_approximator: InstanceApproximator | None = None,
instance_matcher: InstanceMatchingAlgorithm | None = None,
edge_case_handler: EdgeCaseHandler | None = None,
eval_metrics: list[Metric] = [Metric.DSC, Metric.IOU, Metric.ASSD],
eval_metrics: list[Metric] = [Metric.DSC, Metric.IOU, Metric.ASSD, Metric.RVD],
decision_metric: Metric | None = None,
decision_threshold: float | None = None,
log_times: bool = False,
Expand Down
48 changes: 48 additions & 0 deletions panoptica/panoptic_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_compute_centerline_dice_coefficient,
_compute_dice_coefficient,
_average_symmetric_surface_distance,
_compute_relative_volume_difference,
)
from panoptica.utils import EdgeCaseHandler

Expand Down Expand Up @@ -142,6 +143,14 @@ def __init__(
global_bin_assd,
long_name="Global Binary Average Symmetric Surface Distance",
)
#
self.global_bin_rvd: int
self._add_metric(
"global_bin_rvd",
MetricType.GLOBAL,
global_bin_rvd,
long_name="Global Binary Relative Volume Difference",
)
# endregion
#
# region IOU
Expand Down Expand Up @@ -232,6 +241,23 @@ def __init__(
long_name="Segmentation Quality Assd Standard Deviation",
)
# endregion
#
# region RVD
self.sq_rvd: float
self._add_metric(
"sq_rvd",
MetricType.INSTANCE,
sq_rvd,
long_name="Segmentation Quality Relative Volume Difference",
)
self.sq_rvd_std: float
self._add_metric(
"sq_rvd_std",
MetricType.INSTANCE,
sq_rvd_std,
long_name="Segmentation Quality Relative Volume Difference Standard Deviation",
)
# endregion

##################
# List Metrics #
Expand Down Expand Up @@ -468,6 +494,18 @@ def sq_assd_std(res: PanopticaResult):
# endregion


# region RVD
def sq_rvd(res: PanopticaResult):
return res.get_list_metric(Metric.RVD, mode=MetricMode.AVG)


def sq_rvd_std(res: PanopticaResult):
return res.get_list_metric(Metric.RVD, mode=MetricMode.STD)


# endregion


# region Global
def global_bin_dsc(res: PanopticaResult):
if res.tp == 0:
Expand Down Expand Up @@ -499,6 +537,16 @@ def global_bin_assd(res: PanopticaResult):
return _average_symmetric_surface_distance(ref_binary, pred_binary)


def global_bin_rvd(res: PanopticaResult):
if res.tp == 0:
return 0.0
pred_binary = res._prediction_arr.copy()
ref_binary = res._reference_arr.copy()
pred_binary[pred_binary != 0] = 1
ref_binary[ref_binary != 0] = 1
return _compute_relative_volume_difference(ref_binary, pred_binary)


# endregion


Expand Down
9 changes: 9 additions & 0 deletions panoptica/utils/edge_case_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,18 @@ def __str__(self) -> str:


class EdgeCaseHandler:

def __init__(
self,
listmetric_zeroTP_handling: dict[Metric, MetricZeroTPEdgeCaseHandling] = {
Metric.DSC: MetricZeroTPEdgeCaseHandling(
no_instances_result=EdgeCaseResult.NAN,
default_result=EdgeCaseResult.ZERO,
),
Metric.clDSC: MetricZeroTPEdgeCaseHandling(
no_instances_result=EdgeCaseResult.NAN,
default_result=EdgeCaseResult.ZERO,
),
Metric.IOU: MetricZeroTPEdgeCaseHandling(
no_instances_result=EdgeCaseResult.NAN,
empty_prediction_result=EdgeCaseResult.ZERO,
Expand All @@ -94,6 +99,10 @@ def __init__(
no_instances_result=EdgeCaseResult.NAN,
default_result=EdgeCaseResult.INF,
),
Metric.RVD: MetricZeroTPEdgeCaseHandling(
no_instances_result=EdgeCaseResult.NAN,
default_result=EdgeCaseResult.NAN,
),
},
empty_list_std: EdgeCaseResult = EdgeCaseResult.NAN,
) -> None:
Expand Down
Loading

0 comments on commit dcee093

Please sign in to comment.