代码
基于 MindSpore 实现图像分割系列之平均表面距离

基于 MindSpore 实现图像分割系列之平均表面距离

基于 MindSpore 实现图像分割系列之平均表面距离

本文来源:知乎

作者:李嘉琪

今天为大家带来的内容是Mean surface distance 平均表面距离的原理介绍及MindSpore的实现代码。

当我们评价图像分割的质量和模型表现时,经常会用到各类表面距离的计算。比如:

  • Mean surface distance 平均表面距离
  • Hausdorff distance 豪斯多夫距离(也被称为max_surface_distance 最大表面距离MSD)
  • Surface overlap 表面重叠度
  • Surface dice 表面dice值
  • Volumetric dice 三维dice值

等,都可以称为表面距离系列了。今天简单的讲解一下Mean surface distance 平均表面距离。

image.png

顾名思义,Mean surface distance这个指标就是P中所有点的表面距离的平均。这个指标又可称为Average Symmetric Surface Distance (ASSD),它也是医疗图像分割竞赛CHAOS中的一个评估指标。

This function is used to compute the Average Surface Distance from `y_pred` to `y` under the default setting. Mean Surface Distance(MSD), the mean of the vector is taken. This tell us how much, on average, the surface varies between the segmentation and the GT.

其实这个表面距离也没啥可介绍的,似乎就这么简单。来看看公式吧:

先定义image.png代表的是预测的image.png中的边界的像素,同样可以得到image.png的定义。然后对image.png的定义,同理可得image.png的定义。

image.png

也可以这样写:

2.5.4. Average Symmetric Surface Distance (ASD)

Let S(A) denote the set of surface voxels of A. The shortest distance of an arbitrary voxel v to S(A) is defined as:

image.png

where ||. || denotes the Euclidean distance. The average symmetric surface distance is then given by:

image.png

这样是不是就清楚多了。

image.png

原理已经讲完,话不多说,我们开始上代码。使用的是MindSpore框架实现的代码。

"""MeanSurfaceDistance."""
from scipy.ndimage import morphology
import numpy as np
from mindspore._checkparam import Validator as validator
from .metric import Metric
class MeanSurfaceDistance(Metric):
    def __init__(self, symmetric=False, distance_metric="euclidean"):
        super(MeanSurfaceDistance, self).__init__()
        self.distance_metric_list = ["euclidean", "chessboard", "taxicab"]
        distance_metric = validator.check_value_type("distance_metric", distance_metric, [str])
        self.distance_metric = validator.check_string(distance_metric, self.distance_metric_list, "distance_metric")
        self.symmetric = validator.check_value_type("symmetric", symmetric, [bool])
        self.clear()
    def clear(self):
        """Clears the internal evaluation result."""
        self._y_pred_edges = 0
        self._y_edges = 0
        self._is_update = False
    def _get_surface_distance(self, y_pred_edges, y_edges):
        """
        计算从预测图片边界到真实图片边界的表面距离。
        """
        if not np.any(y_pred_edges):
            return np.array([])
        if not np.any(y_edges):
            dis = np.full(y_edges.shape, np.inf)
        else:
            if self.distance_metric == "euclidean":
                dis = morphology.distance_transform_edt(~y_edges)
            elif self.distance_metric in self.distance_metric_list[-2:]:
                dis = morphology.distance_transform_cdt(~y_edges, metric=self.distance_metric)
        surface_distance = dis[y_pred_edges]
        return surface_distance
    def update(self, *inputs):
        """
        更新输入数据。
        """
        if len(inputs) != 3:
            raise ValueError('MeanSurfaceDistance need 3 inputs (y_pred, y, label), but got {}.'.format(len(inputs)))
        y_pred = self._convert_data(inputs[0])
        y = self._convert_data(inputs[1])
        label_idx = inputs[2]
        if y_pred.size == 0 or y_pred.shape != y.shape:
            raise ValueError("y_pred and y should have same shape, but got {}, {}.".format(y_pred.shape, y.shape))
        if y_pred.dtype != bool:
            y_pred = y_pred == label_idx
        if y.dtype != bool:
            y = y == label_idx
        self._y_pred_edges = morphology.binary_erosion(y_pred) ^ y_pred
        self._y_edges = morphology.binary_erosion(y) ^ y
        self._is_update = True
    def eval(self):
        """
        计算平均表面距离。
        """
        if self._is_update is False:
            raise RuntimeError('Call the update method before calling eval.')
        mean_surface_distance = self._get_surface_distance(self._y_pred_edges, self._y_edges)
        if mean_surface_distance.shape == (0,):
            return np.inf
        avg_surface_distance = mean_surface_distance.mean()
        if not self.symmetric:
            return avg_surface_distance
        contrary_mean_surface_distance = self._get_surface_distance(self._y_edges, self._y_pred_edges)
        if contrary_mean_surface_distance.shape == (0,):
            return np.inf
        contrary_avg_surface_distance = contrary_mean_surface_distance.mean()
        return np.mean((avg_surface_distance, contrary_avg_surface_distance))

使用方法如下:

import numpy as np
from mindspore import Tensor
from mindspore.nn.metrics import MeanSurfaceDistance
def test_mean_surface_distance():
    """test_mean_surface_distance"""
    x = Tensor(np.array([[3, 0, 1], [1, 3, 0], [1, 0, 2]]))
    y = Tensor(np.array([[0, 2, 1], [1, 2, 1], [0, 0, 1]]))
    metric = MeanSurfaceDistance()
    metric.clear()
    metric.update(x, y, 0)
    distance = metric.eval()
    print(distance)
test_mean_surface_distance()
1.4142135623730951

每个batch(比如两组数据)进行计算的时候如下:

import numpy as np
from mindspore import Tensor
from mindspore.nn.metrics import MeanSurfaceDistance
x = Tensor(np.array([[3, 0, 1], [1, 3, 0], [1, 0, 2]]))
y = Tensor(np.array([[0, 2, 1], [1, 2, 1], [0, 0, 1]]))
metric = MeanSurfaceDistance()
metric.clear()
metric.update(x, y, 0)
x1 = Tensor(np.array([[3, 0, 1], [1, 3, 0], [1, 0, 2]]))
y1 = Tensor(np.array([[0, 2, 1], [1, 2, 1], [0, 0, 1]]))
metric.update(x1, y1, 0)
distance = metric.eval()
print(distance)