当我们评价图像分割的质量和模型表现时,经常会用到各类表面距离的计算。这里推荐一个deepmind的表面距离度量计算库surface-distance。
该库的下载地址:https://download.csdn.net/download/Joker00007/12718748
Github地址:https://github.com/deepmind/surface-distance
(注:Github上的代码存在Bug,可直接在第一个链接下载,该文件是已经改完错误的)
这个库主要包含了以下几个表面距离计算:
该计算库的调用API都是类似的,首先调用compute_surface_distances这个函数计算出中间结果,再用这个中间结果去计算各类距离。
compute_surface_distances的参数有三个,mask_gt和mask_pred分别为ground truth和prediction的volume,spacing_mm是每个轴的体素间距。
体素间距是医疗影像上常见的一个参数,它的含义是,图像中相邻的两个体素(即两个点)间的直线距离转换为现实中的距离是多少毫米。例如,一个shape为(256, 256, 64)的array,spacing_mm为(1.1, 1.1, 3),实际表示的是 (281.6mm, 281.6mm, 192mm) 大小的块。体素间距不同,计算得到的surface distance也不同。如果不确定自己的数据体素间距是多少,就设为(1.0, 1.0, 1.0)。
顾名思义,这个指标就是P中所有点的表面距离的平均。这个指标又可称为Average Symmetric Surface Distance (ASSD),它也是医疗图像分割竞赛CHAOS中的一个评估指标。平均表面距离的计算代码如下:
import surface_distance as surfdist
surface_distances = surfdist.compute_surface_distances(mask_gt, mask_pred, spacing_mm=(1.0, 1.0, 1.0))
avg_surf_dist = surfdist.compute_average_surface_distance(surface_distances)
关于这个距离的计算,很多人都讲的不是非常清晰,甚至有很多人介绍的是错的。这里我介绍一个比较简单清晰的计算流程,请对照下图阅读。
1.给定两个点集合A{ a0, a1, … }和B{ b0, b1, b2, …}
2.取A集合中的一点a0,计算a0到B集合中所有点的距离,保留最短的距离d0
3.遍历A集合中所有点,图中一共两点a0和a1,计算出d0和d1
4.比较所有的距离{ d0, d1 },选出最长的距离d1
5.这个最长的距离就是h,它是A→B的单向豪斯多夫距离,记为h( A, B )
6.对于A集合中任意一点a,我们可以确定,以点a为圆心,h为半径的圆内部必有B集合中的点
7.交换A集合和B集合的角色,计算B→A的单向豪斯多夫距离h( B, A ),选出h( A, B )和h( B, A )中最长的距离,就是A,B集合的双向豪斯多夫距离
豪斯多夫距离95%的计算代码如下:
import surface_distance as surfdist
surface_distances = surfdist.compute_surface_distances(mask_gt, mask_pred, spacing_mm=(1.0, 1.0, 1.0))
hd_dist_95 = surfdist.compute_robust_hausdorff(surface_distances, 95)
compute_robust_hausdorff这个函数的第二个参数表示最大距离分位数,取值范围为0-100,它表示的是计算步骤4中,选取的距离能覆盖距离的百分比,例如我这里选取了95%,那么在计算步骤4中选取的不是最大距离,而是将距离从大到小排列后,取排名为5%的距离。这么做的目的是为了排除一些离群点所造成的不合理的距离,保持整体数值的稳定性。
给定一个容许的误差距离,在此容差范围内的表面视作重叠部分,计算mask_gt和mask_pred的表面重叠比例。表面重叠度的计算代码如下:
import surface_distance as surfdist
surface_distances = surfdist.compute_surface_distances(mask_gt, mask_pred, spacing_mm=(1.0, 1.0, 1.0))
surface_overlap = surfdist.compute_surface_overlap_at_tolerance(surface_distances, 1)
compute_surface_overlap_at_tolerance函数的第二个参数表示容许误差,以mm为单位,例如我这里选择了1,那么容许误差也就是空间欧氏距离小于1mm的点都会被当做是重叠部分。
注意,这里返回的surface_overlap有两个值,分别代表pred->gt和gt->pred的重叠比例。大多数情况下这两个比例是相等的,但是在某些情况下是不同的
给定一个容许的误差距离,在此容差范围内的表面视作重叠部分,计算mask_gt和mask_pred的表面重叠dice值。表面dice值的计算代码如下:
import surface_distance as surfdist
surface_distances = surfdist.compute_surface_distances(mask_gt, mask_pred, spacing_mm=(1.0, 1.0, 1.0))
surface_dice = surfdist.compute_surface_dice_at_tolerance(surface_distances, 1)
compute_surface_dice_at_tolerance这个函数的第二个参数跟表面重叠度函数的第二个参数是一样的用法。该函数的返回值只有一个,因为pred->gt和gt->pred的表面dice是相同的。
计算pred与gt之间的三维dice值。注意,这与上面计算的表面dice不同,三维dice值会考虑空间中的每一个点而不仅仅是表面。三维dice的计算代码如下:
import surface_distance as surfdist
volume_dice = surfdist.compute_dice_coefficient(mask_gt, mask_pred)
该函数的返回值为0~1之间的float值,如果gt和pred均为空,那么会返回NAN值。
这边统计一些公开竞赛中所选取的metric,不在surfdist库里的metric将加以括号。
这边可以看出,Volumetric dice和Hausdorff distance 95%是最常用的两种metric,推荐大家在评估自己的模型的时候优先使用这两种。
自己写的Average surface distance和95%Hausdorff distance函数调用
import surface_distance as surfdist
import nibabel as nib
import numpy as np
import h5py
mask_pred_path = r"E:\...\pred_segmentation_case000051.nii.gz"
mask_gt_path = r"E:\...\segmentation_case000051.nii.gz"
mask_gt = nib.load(mask_gt_path)
mask_pred = nib.load(mask_pred_path)
spacing_mm = h5py.File(r'E:\...\pixel_size.mat', 'r')['pixel_size'][:]
#Average surface distance 平均表面距离
def mBoudDis(mask_pred, mask_gt, spacing_mm_s):
width_t, height_t, queue0_t, queue1_t = mask_gt.dataobj.shape
width_p, height_p, queue0_p, queue1_p = mask_pred.dataobj.shape
mB = []
if(queue1_t != queue1_p):
return("Error,The two sets of data have different dimensions")
else:
for i in range(queue1_t):
gt = mask_gt.dataobj[:,:,:,i]
pred = mask_pred.dataobj[:,:,:,i]
gt = gt.astype(np.bool)
pred = pred.astype(np.bool)
surface_distances = surfdist.compute_surface_distances(gt, pred, spacing_mm = spacing_mm_s)
#avg_surf_dist有两个参数,第一个参数是average_distance_gt_to_pred,第二个参数是average_distance_pred_to_gt
surf_dist = surfdist.compute_average_surface_distance(surface_distances)
avg_surf_dist = (surf_dist[0]+surf_dist[1])/2
mB.append(avg_surf_dist)
return mB
print("ASD:",mBoudDis(mask_pred, mask_gt, spacing_mm))
#95%Hausdorff distance 豪斯多夫距离
def hausd95(mask_pred, mask_gt, spacing_mm_s):
width_t, height_t, queue0_t, queue1_t = mask_gt.dataobj.shape
width_p, height_p, queue0_p, queue1_p = mask_pred.dataobj.shape
mH = []
if(queue1_t != queue1_p):
return("Error,The two sets of data have different dimensions")
else:
for i in range(queue1_t):
gt = mask_gt.dataobj[:,:,:,i]
pred = mask_pred.dataobj[:,:,:,i]
gt = gt.astype(np.bool)
pred = pred.astype(np.bool)
surface_distances = surfdist.compute_surface_distances(gt, pred, spacing_mm = spacing_mm_s)
hd_dist_95 = surfdist.compute_robust_hausdorff(surface_distances, 95)
mH.append(hd_dist_95)
return mH
print("mH95:",hausd95(mask_pred, mask_gt, spacing_mm))