两个分割结果的 Dice 都超过 0.99,它们对后续建模或定位的影响可能很不一样。体积重叠、边界位置、远处孤立组件和细结构完整性,回答的是不同问题。选指标时应先确定任务要避免哪种错误,再解释分数。
**阅读范围:**本文是三维视觉与医学影像软件工程系列的独立技术教程。数据和数值实验均为合成示例,不读取患者数据,不代表实际设备性能或临床精度。文中长度按示例明确的单位解释。资料核对日期:2026 年 10 月 16 日。

图 1:一个高分,不能覆盖所有错误类型(原创教学配图)
一、重叠指标先要求空间对齐
Dice=2|A∩B|/(|A|+|B|),IoU=|A∩B|/|A∪B|。当两个集合非空时,IoU=Dice/(2−Dice)。它们都没有物理长度单位,主要描述集合重叠,而不是边界偏移了多少毫米。
在逐体素比较前,预测与参考必须处于同一物理网格。shape 相同但原点、方向或 spacing 不同,仍不能直接比较。本例输入已经明确处于同一网格;真实影像应先核对空间元数据,必要时采用明确策略重采样。
二、表面距离的定义必须写出来
把预测边界上的点到参考边界的最近距离记为一组,再反向计算另一组,可以构造平均表面距离或分位数。需要说明边界如何定义、是否双向、怎样加权、用哪些物理单位。不同实现的"ASD"或"HD95"可能并不完全相同。
本文用布尔腐蚀提取边界体素,使用考虑 spacing 的欧氏距离变换,合并双向距离后取均值与第 95 百分位。这是不加表面积权重的体素边界约定,不是连续三角面或 surfel 加权指标。跨论文或库比较前应先对齐这些细节。
三、为什么 HD95 也会漏掉小区域?
最大 Hausdorff 距离关注最坏的最近边界距离,对远处噪点敏感。HD95 用分位数削弱极端值影响,但代价是可能忽略占比较小的异常表面。是否应该忽略,取决于任务而不是指标名字。
若大部分表面完全重合,而远处小组件贡献不到合并距离的 5%,则 HD95 可以为零,同时最大 Hausdorff 距离很大。这个现象不违反定义,反而提醒我们不要把"鲁棒分位数"当成"所有局部区域都可靠"。
四、空掩码、多类别与汇总方式
双空掩码的 Dice 有的实现设为 1,有的设为未定义;单空掩码的距离也需要明确约定。本篇直接拒绝空掩码,要求调用方单独制定统计策略,避免悄悄把失败病例从均值里删掉。
多类别任务应区分逐类宏平均与按体素汇总。大类别会掩盖小结构错误;把所有病例合并计算,也可能让大体积病例支配分数。建议保留逐病例、逐类别分布以及失败案例,而不是只报告一个平均数。
五、可运行的完整合成实验
在 64³ 的网格上生成半径 12 体素的球体。方案一沿 x 平移 2 体素;方案二保留球体,在远处加入 3³ 的小立方组件。spacing 按 z,y,x 输入,本例均为 1 mm。代码同时检查相同掩码应得到完美指标,并验证 Dice--IoU 关系。
环境说明:代码在本机 Python 3.14 环境中实际执行。为便于复现,请保存为 demo.py,在支持这些库的独立环境中安装依赖;若目标解释器没有兼容 wheel,应先选择受支持的解释器。环境差异可能影响最后几位数值,不影响本文所检查的几何或统计关系。
python -m pip install numpy scipy
python demo.py
import numpy as np
from scipy.ndimage import binary_erosion,distance_transform_edt
def metrics(a,b,spacing=(1.,1.,1.)):
a,b=np.asarray(a,bool),np.asarray(b,bool)
s=np.asarray(spacing,float)
if a.shape!=b.shape or a.ndim!=3 or s.shape!=(3,) or not np.isfinite(s).all() or (s<=0).any():
raise ValueError('Same 3D grid and positive spacing required')
if not a.any() or not b.any():raise ValueError('Empty mask: define separate evaluation policy')
intersection=np.count_nonzero(a&b)
dice=2*intersection/(a.sum()+b.sum())
iou=intersection/np.count_nonzero(a|b)
sa=a^binary_erosion(a);sb=b^binary_erosion(b)
da=distance_transform_edt(~sb,sampling=s)[sa]
db=distance_transform_edt(~sa,sampling=s)[sb]
# This demo uses an unweighted pooled voxel-surface convention.
distances=np.r_[da,db]
return dice,iou,distances.mean(),np.percentile(distances,95),distances.max()
z,y,x=np.indices((64,64,64));a=(x-30)**2+(y-30)**2+(z-30)**2<=12**2
b=np.roll(a,2,axis=2)
c=a.copy();c[55:58,55:58,55:58]=True
same=metrics(a,a)
assert np.allclose(same,[1,1,0,0,0])
for name,mask in [('shift_2_voxels',b),('tiny_remote_component',c)]:
v=metrics(a,mask)
assert np.isclose(v[1],v[0]/(2-v[0]))
print(name,'Dice IoU pooled_ASD HD95 HD',v)
try:metrics(a,np.zeros_like(a))
except ValueError:pass
else:raise AssertionError('Empty mask policy bypassed')
print('PASS: identity, Dice-IoU relation and empty-mask rejection')
六、运行结果与应该怎样解读
平移方案 Dice 约 0.87725,HD95=2 mm,最大 HD=2 mm。远处小组件方案 Dice 约 0.99812,按本文约定 HD95=0,但最大 HD 约 35.22783 mm。说明体积高分与分位数高分都可能漏掉少量远处异常。

图 2:实际实验掩码的最大投影(原创教学配图)
代码中的断言验证了本篇明确描述的性质。配图中标为原理示意的部分用于解释模型,不是实测截图;由公式生成的曲线只在图注与正文列出的假设下成立。不要把合成实验输出当成真实产品的性能报告。
七、常见现象与排查方向
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| Dice 极高但局部严重偏移 | 大体积结构掩盖小区域 | 增加表面、连通分量与关键结构检查 |
| 两个库 HD95 不一样 | 双向、分位数或表面加权不同 | 用同一小型案例对齐定义 |
| 误差单位不清楚 | 距离变换未传 spacing | 按数组轴顺序传入物理间距 |
| 均值漂亮但漏检病例不见了 | 空掩码被忽略 | 预先约定失败与空值统计策略 |
八、独立验证与适用边界
对任务相关的小结构、狭窄连接、远处组件和边界误差分别设计验证案例。参考分割本身也有观察者差异,不能把它不加讨论地视为绝对真值。本文没有实现表面积加权、病例汇总或临床有效性验证;指标反例只证明特定定义的边界,不代表任何模型总体性能。
建议把原始输入、参数、软件版本、输出结果和失败状态一起保留。若把同一份检查数据反复用于选择参数,它就不再是完全独立的最终验证集。将算法演示接入真实系统时,应另行验证输入质量、参考不确定性和完整处理链路。
参考资料
上述资料用于核对相关接口与基本约定;本文的合成数据、推导示例与代码不声称复刻库内全部实现。
写在最后
"知之为知之,不知为不知,是知也。"
------ 孔子,《论语·为政》
一个分数回答一个问题;把问题问清楚,评价才有价值。