Repository: pytorch/ignite

Issue:
Adding Dice Coefficient Metric for Image Segmentation
I wanted to implement Dice Coefficient as a metric for image segmentation problems. any interest?


Discussion:
@anmolsjoshi yes, please send a PR
@vfdev-5 thanks. i'll add it to ignite.contrib.metrics
@vfdev-5 were you thinking a binary or categorical implementation for this?

I've mainly seen binary implementations only, but a few blog posts mention categorical. 
@anmolsjoshi maybe it would be more interesting to implement [Tversky index](https://en.wikipedia.org/wiki/Tversky_index) - generalization of Dice and Jaccard.
Categorial index makes sens too, we can tryout to implement both :)
@vfdev-5 working on this now, found an implementation here - https://github.com/kevinzakka/pytorch-goodies/blob/master/losses.py#L131-L175

Idea is that we create a general Tversky Index Metric, and then create children Metrics such as Jaccard Index and Dice Coefficient by forcing values of alpha and beta. 
Tversky loss is indeed more interesting and offer more flexibility afterwards with fixed alpha and beta values. 
Does using the Confusion Matrix object the best way to implement this or inheriting from _BaseClassification is better ? 
@anmolsjoshi sorry, I haven't notice your link on kevinzakka's repo. His implementation seems like to be image-wise and can not be used in our `update-compute` paradigm.

@pldelisle we think to implement it using a confusion matrix and provide it as `IoU` or `mIoU` methods... @anmolsjoshi should be working on it...
I have also been working a bit on this (needed to implement Dice coefficient for my own project, then stumbled on this library which proposes a pretty good structure, so I decided to contribute). I've done it for the Dice Coefficient, but I struggle a bit for the Tversky Index and the relative complement.

As I can understand, the relative complement is the position N_xy in the diagonal where x=y minus everything else, for each position in the diagonal.

I just don't know how to implement this using the confusion matrix while taking into account the possibility of adding weights to each class.
Hi @vfdev-5 , I am looking to contribute to ignite.
Could you please help me by detailing out this issue please? I have gone through the conversation but couldn't understand it exactly. Would be of great help to me.

Thank you
Hi @PyExtreme thanks for the proposition.
Idea here is to provide precoded Dice Coefficient metric. It is more simple to start from [Confusion Matrix](https://github.com/pytorch/ignite/blob/master/ignite/metrics/confusion_matrix.py) and implement Dice Coefficient as it is done for [IoU](https://github.com/pytorch/ignite/blob/master/ignite/metrics/confusion_matrix.py#L118).
Please, take a look at our [contributing rules](https://github.com/pytorch/ignite/blob/master/CONTRIBUTING.md) and feel free to ask if you have a question.

Resolving patch:
diff --git a/ignite/metrics/__init__.py b/ignite/metrics/__init__.py
--- a/ignite/metrics/__init__.py
+++ b/ignite/metrics/__init__.py
@@ -11,6 +11,6 @@
 from ignite.metrics.top_k_categorical_accuracy import TopKCategoricalAccuracy
 from ignite.metrics.running_average import RunningAverage
 from ignite.metrics.metrics_lambda import MetricsLambda
-from ignite.metrics.confusion_matrix import ConfusionMatrix, IoU, mIoU
+from ignite.metrics.confusion_matrix import ConfusionMatrix, IoU, mIoU, DiceCoefficient
 from ignite.metrics.accumulation import VariableAccumulation, Average, GeometricAverage
 from ignite.metrics.fbeta import Fbeta
diff --git a/ignite/metrics/confusion_matrix.py b/ignite/metrics/confusion_matrix.py
--- a/ignite/metrics/confusion_matrix.py
+++ b/ignite/metrics/confusion_matrix.py
@@ -116,7 +116,7 @@ def compute(self):
 
 
 def IoU(cm, ignore_index=None):
-    """Calculates Intersection over Union
+    """Calculates Intersection over Union using :class:`~ignite.metrics.ConfusionMatrix` metric.
 
     Args:
         cm (ConfusionMatrix): instance of confusion matrix metric
@@ -164,7 +164,7 @@ def ignore_index_fn(iou_vector):
 
 
 def mIoU(cm, ignore_index=None):
-    """Calculates mean Intersection over Union
+    """Calculates mean Intersection over Union using :class:`~ignite.metrics.ConfusionMatrix` metric.
 
     Args:
         cm (ConfusionMatrix): instance of confusion matrix metric
@@ -191,8 +191,8 @@ def mIoU(cm, ignore_index=None):
 
 
 def cmAccuracy(cm):
-    """
-    Calculates accuracy using :class:`~ignite.metrics.ConfusionMatrix` metric.
+    """Calculates accuracy using :class:`~ignite.metrics.ConfusionMatrix` metric.
+
     Args:
         cm (ConfusionMatrix): instance of confusion matrix metric
 
@@ -205,8 +205,8 @@ def cmAccuracy(cm):
 
 
 def cmPrecision(cm, average=True):
-    """
-    Calculates precision using :class:`~ignite.metrics.ConfusionMatrix` metric.
+    """Calculates precision using :class:`~ignite.metrics.ConfusionMatrix` metric.
+
     Args:
         cm (ConfusionMatrix): instance of confusion matrix metric
         average (bool, optional): if True metric value is averaged over all classes
@@ -238,3 +238,37 @@ def cmRecall(cm, average=True):
     if average:
         return recall.mean()
     return recall
+
+
+def DiceCoefficient(cm, ignore_index=None):
+    """Calculates Dice Coefficient for a given :class:`~ignite.metrics.ConfusionMatrix` metric.
+
+    Args:
+        cm (ConfusionMatrix): instance of confusion matrix metric
+        ignore_index (int, optional): index to ignore, e.g. background index
+    """
+
+    if not isinstance(cm, ConfusionMatrix):
+        raise TypeError("Argument cm should be instance of ConfusionMatrix, but given {}".format(type(cm)))
+
+    if ignore_index is not None:
+        if not (isinstance(ignore_index, numbers.Integral) and 0 <= ignore_index < cm.num_classes):
+            raise ValueError("ignore_index should be non-negative integer, but given {}".format(ignore_index))
+
+    # Increase floating point precision and pass to CPU
+    cm = cm.type(torch.DoubleTensor)
+    dice = 2.0 * cm.diag() / (cm.sum(dim=1) + cm.sum(dim=0) + 1e-15)
+
+    if ignore_index is not None:
+
+        def ignore_index_fn(dice_vector):
+            if ignore_index >= len(dice_vector):
+                raise ValueError("ignore_index {} is larger than the length of Dice vector {}"
+                                 .format(ignore_index, len(dice_vector)))
+            indices = list(range(len(dice_vector)))
+            indices.remove(ignore_index)
+            return dice_vector[indices]
+
+        return MetricsLambda(ignore_index_fn, dice)
+    else:
+        return dice


Test patch:
diff --git a/tests/ignite/metrics/test_confusion_matrix.py b/tests/ignite/metrics/test_confusion_matrix.py
--- a/tests/ignite/metrics/test_confusion_matrix.py
+++ b/tests/ignite/metrics/test_confusion_matrix.py
@@ -1,4 +1,3 @@
-from __future__ import division
 import os
 import torch
 
@@ -7,8 +6,7 @@
 
 from ignite.exceptions import NotComputableError
 from ignite.metrics import ConfusionMatrix, IoU, mIoU
-from ignite.metrics.confusion_matrix import cmAccuracy, cmPrecision, cmRecall
-
+from ignite.metrics.confusion_matrix import cmAccuracy, cmPrecision, cmRecall, DiceCoefficient
 import pytest
 
 
@@ -477,6 +475,42 @@ def test_cm_with_average():
     np.testing.assert_almost_equal(true_pr, res)
 
 
+def test_dice_coefficient():
+
+    y_true, y_pred = get_y_true_y_pred()
+    th_y_true, th_y_logits = compute_th_y_true_y_logits(y_true, y_pred)
+
+    true_res = [0, 0, 0]
+    for index in range(3):
+        bin_y_true = y_true == index
+        bin_y_pred = y_pred == index
+        # dice coefficient: 2*intersection(x, y) / (|x| + |y|)
+        # union(x, y) = |x| + |y| - intersection(x, y)
+        intersection = bin_y_true & bin_y_pred
+        union = bin_y_true | bin_y_pred
+        true_res[index] = 2.0 * intersection.sum() / (union.sum() + intersection.sum())
+
+    cm = ConfusionMatrix(num_classes=3)
+    dice_metric = DiceCoefficient(cm)
+
+    # Update metric
+    output = (th_y_logits, th_y_true)
+    cm.update(output)
+
+    res = dice_metric.compute().numpy()
+    np.testing.assert_allclose(res, true_res)
+
+    for ignore_index in range(3):
+        cm = ConfusionMatrix(num_classes=3)
+        dice_metric = DiceCoefficient(cm, ignore_index=ignore_index)
+        # Update metric
+        output = (th_y_logits, th_y_true)
+        cm.update(output)
+        res = dice_metric.compute().numpy()
+        true_res_ = true_res[:ignore_index] + true_res[ignore_index + 1:]
+        assert np.all(res == true_res_), "{}: {} vs {}".format(ignore_index, res, true_res_)
+
+
 def _test_distrib_multiclass_images(device):
 
     import torch.distributed as dist