|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import numpy as np |
|
from medpy.metric.binary import __surface_distances |
|
|
|
|
|
def normalized_surface_dice(a: np.ndarray, b: np.ndarray, threshold: float, spacing: tuple = None, connectivity=1): |
|
""" |
|
This implementation differs from the official surface dice implementation! These two are not comparable!!!!! |
|
|
|
The normalized surface dice is symmetric, so it should not matter whether a or b is the reference image |
|
|
|
This implementation natively supports 2D and 3D images. Whether other dimensions are supported depends on the |
|
__surface_distances implementation in medpy |
|
|
|
:param a: image 1, must have the same shape as b |
|
:param b: image 2, must have the same shape as a |
|
:param threshold: distances below this threshold will be counted as true positives. Threshold is in mm, not voxels! |
|
(if spacing = (1, 1(, 1)) then one voxel=1mm so the threshold is effectively in voxels) |
|
must be a tuple of len dimension(a) |
|
:param spacing: how many mm is one voxel in reality? Can be left at None, we then assume an isotropic spacing of 1mm |
|
:param connectivity: see scipy.ndimage.generate_binary_structure for more information. I suggest you leave that |
|
one alone |
|
:return: |
|
""" |
|
assert all([i == j for i, j in zip(a.shape, b.shape)]), "a and b must have the same shape. a.shape= %s, " \ |
|
"b.shape= %s" % (str(a.shape), str(b.shape)) |
|
if spacing is None: |
|
spacing = tuple([1 for _ in range(len(a.shape))]) |
|
a_to_b = __surface_distances(a, b, spacing, connectivity) |
|
b_to_a = __surface_distances(b, a, spacing, connectivity) |
|
|
|
numel_a = len(a_to_b) |
|
numel_b = len(b_to_a) |
|
|
|
tp_a = np.sum(a_to_b <= threshold) / numel_a |
|
tp_b = np.sum(b_to_a <= threshold) / numel_b |
|
|
|
fp = np.sum(a_to_b > threshold) / numel_a |
|
fn = np.sum(b_to_a > threshold) / numel_b |
|
|
|
dc = (tp_a + tp_b) / (tp_a + tp_b + fp + fn + 1e-8) |
|
return dc |
|
|
|
|