Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added relative_volume_difference metric, where average positive means… #102

Merged
merged 4 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading