放射科医生可以在CT扫描上通过画一条直线来标记肿瘤。创建完整的3D分割需要在肿瘤出现的切片上进行轮廓描绘,这更耗时。
本教程解释了Lumina的工作原理。它是一个系统,使用CT扫描和一条RECIST线来生成标记肿瘤的3D分割掩码。
Lumina是为FLARE 2026全癌症分割挑战赛而开发的。该系统被设计为在CPU上运行,内存限制为8 GB,推理时间限制为60秒。
本教程涵盖了塑造最终系统的主要设计决策、实现细节和实验。
我们将涵盖的内容:
Lumina 所做的
放射科医生可以通过在肿瘤最长可见直径上画一条线来测量肿瘤。这是一种 RECIST 测量(实体瘤的响应评估标准),广泛用于在癌症治疗期间测量肿瘤反应。
RECIST 线测量的是 2D 直径标记。它不描述肿瘤的完整 3D 形状。
Lumina 使用此测量来提示 3D 分割模型。
任务可以描述为:
Input: 一个 3D CT 扫描和一个标记一个肿瘤的 2D RECIST 线。
Output: 该肿瘤的 3D 掩码,每个体素有一个标签。
体素 是像素的 3D 等价物。每个体素代表 CT 扫描中的一个小体积组织。
RECIST 线告诉模型需要分割哪个肿瘤。然后模型预测肿瘤的三维范围。
这使得分割问题更具特异性,因为模型不需要识别扫描中的每一个可能的肿瘤。
Lumina 管道概览
完整的过程包含几个阶段。我们从一个 3D CT 扫描和一个标记一个肿瘤的 RECIST 线开始。然后我们将该线转换为额外的输入通道,裁剪并重采样图像,并将三个通道通过一个 3D 分割网络。
网络生成一个概率图,我们通过重采样、阈值化和连通分量选择将其转换为最终的3D肿瘤掩码。
主要阶段如下:
准备CT扫描和RECIST标记。
将RECIST线编码为额外的网络输入。
裁剪并重采样图像到通用网格。
使用3D分割网络预测肿瘤概率图。
优化概率图并将其转换为二值掩码。
控制CPU推理,使整个案例在运行时间和内存限制内。
先决条件
如果您具备以下经验,将能从本教程中获益更多:
Python
NumPy
PyTorch
基本卷积神经网络。
不需要详细的医学背景。教程在介绍概念时会解释医学影像概念。
该实现使用NumPy、SciPy、PyTorch以及MONAI,这是一个基于PyTorch构建的医学影像框架。
步骤1:编码前审查您的数据
我们的数据以.npz文件形式提供,这是NumPy的压缩数组格式。
每个文件包含类似以下字段:
imgs # the CT scan, a 3D array
recist # the marker lines, same shape, one integer per tumour
spacing # how many millimetres apart the voxels are
origin # where the scan sits in the scanner's coordinates
direction # how the scan is rotated
gts # the ground-truth segmentation, training files only
在构建模型之前,我们检查了图像数值、数组形状以及空间元数据。
有两个细节尤为重要。
扫描已进行亮度调整
CT 扫描仪通常将图像存储为 Hounsfield 单位,水大约为 0 HU,骨骼可超过 1000 HU。
在此数据集中,扫描已被转换为固定的 0–255 范围,原始的 Hounsfield 单位数值不可用。
我们不能对原始数值使用常规的 CT 窗口处理,而是测量了所提供数据的强度分布。
例如,53.4% 的体素值恰好为 0,对应身体外的空气。
坐标顺序很重要
The spacing array is stored as (X, Y, Z), while NumPy arrays are indexed as (Z, Y, X).
如果混用这两种约定,空间测量可能会出错。
我们在输入边界处仅进行一次坐标转换,并在内部使用 (Z, Y, X) 的约定:
def _to_zyx(vec3, order):
vec3 = np.asarray(vec3, dtype=float).ravel()
if order == "xyz":
return vec3[::-1].copy() # (x, y, z) -> (z, y, x)
if order == "zyx":
return vec3.copy()
raise ValueError(f"unknown geometry_order {order!r}")
重要的教训是在设计预处理管道之前检查多个文件。
检查:
数组形状
强度范围
体素间距
坐标约定
元数据
可用标签
避免假设数据遵循另一个CT数据集或教程的约定。
步骤2:将RECIST线转换为网络输入
神经网络接收输入通道的堆栈。CT扫描提供第一个通道。然后我们以网络可使用的形式表示RECIST线。
我们使用三个通道:
CT扫描
RECIST线,绘制厚度为3个体素
端点热图,包含围绕每个端点的高斯函数
两个端点定义了测量的直径,并为网络提供了RECIST测量的位置和长度。
端点信息通过高斯函数表示。每个高斯函数在端点附近具有高值,并随距离逐渐减小。
def _endpoint_heatmap(endpoints_zyx, shape, sigma):
d, h, w = (int(s) for s in shape)
heat = np.zeros((d, h, w), dtype=np.float32)
rad = max(1, int(np.ceil(3 * sigma)))
two_s2 = 2.0 * sigma * sigma
for z0, y0, x0 in np.asarray(endpoints_zyx, float):
zc, yc, xc = int(round(z0)), int(round(y0)), int(round(x0)))
zl, zr = max(0, zc - rad), min(d, zc + rad + 1)
yl, yr = max(0, yc - rad), min(h, yc + rad + 1)
xl, xr = max(0, xc - rad), min(w, xc + rad + 1)
zz, yy, xx = np.mgrid[
zl:zr, yl:yr, xl:xr
].astype(np.float32)
g = np.exp(
-((zz-z0)**2 + (yy-y0)**2 + (xx-x0)**2) / two_s2
)
np.maximum(
heat[zl:zr, yl:yr, xl:xr],
g,
out=heat[zl:zr, yl:yr, xl:xr]
)
return heat
高斯仅在每个端点周围的小区域内计算。
当 sigma = 1.5 时,距离端点几个体素远处的值会变得非常小。因此,在完整体积上计算高斯会做很多无用功。
在所需分辨率下绘制线条
我们还根据网络所需的分辨率,从两个端点重新绘制 RECIST 线。
我们不会先在某一分辨率下创建线条,然后随图像一起缩放。对细线进行缩放可能会使其变得更细或导致断开。从端点重新创建线条可以保持提示与图像分辨率的一致。
步骤 3:将所有肿瘤对齐到共同网格
CT 扫描的体素间距可能不同。
例如,一次扫描可能切片较薄,而另一次扫描可能切片较厚。因此,相同数量的体素所代表的物理大小在不同扫描之间可能会有所不同。
对于每个标记的肿瘤,我们将创建一个固定大小的裁剪,并将其重采样到固定的体素间距。
我们的配置如下:
target_spacing: [2.5, 1.0, 1.0] # mm per voxel: z, y, x
crop_size: [64, 160, 160] # voxels: z, y, x
该面内尺寸对应的实际视野范围为:
160 × 160 mm
网络因此获得一致的 64 × 160 × 160 输入。
我们根据训练数据使用目标间距。2.5 mm 的切片间距接近训练集中切片间距的中位数。
我们根据训练数据中病灶大小的分布使用 160 mm 的裁剪,覆盖病灶范围的第 99 百分位。
强度归一化
我们还使用从训练裁剪中计算得到的统计量对 CT 强度值进行归一化:
intensity_mean: 96.88
intensity_std: 79.00
归一化公式如下:
image = (image - 96.88) / 79.00
这些统计量仅基于训练数据计算。
验证集和测试集沿用相同的数值。我们不会从验证集或测试集重新计算这些统计量,否则这两个集合的信息就会渗入预处理环节。
在 Lumina 中,基于训练数据得到的统计量为:均值 96.88,标准差 79.00。这两个数值保存在配置里,训练、验证和推理阶段都使用同一套预处理。
第 4 步:构建 3D 分割网络
这里我们使用 MONAI 提供的 DynUNet。
DynUNet 是一种借鉴 nnU-Net 思路的 3D U-Net 架构,网络深度、卷积核、步长和残差连接均可灵活配置。
U-Net 包含两个主要部分。
编码器逐步降低空间分辨率,同时学习越来越高级的特征。
解码器再逐步恢复空间分辨率,生成分割图。
跳跃连接会把编码器中的精细空间细节传递给解码器,帮助它在生成最终分割结果时更好地还原肿瘤边界。
在 Lumina 中,网络的输入包含三个通道:
CT
RECIST 线
端点热力图。
模型配置如下:
from monai.networks.nets import DynUNet
# 5 levels; the first downsample skips z (see below).
kernels = [[3, 3, 3]] * 5
strides = [[1, 1, 1], [1, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2]]
model = DynUNet(
spatial_dims=3,
in_channels=3, # CT + line + endpoint blobs
out_channels=1, # one probability per voxel
kernel_size=kernels,
strides=strides,
upsample_kernel_size=strides[1:],
filters=features,
res_block=True,
)
strides 列表是配置的重要部分。我们的体素大小不同:切片之间相隔 2.5 毫米,而每个切片内部相隔仅 1.0 毫米。如果在每一层都减少三个维度,我们会过快地失去切片间的细节。因此,我们将在第一次下采样步骤中保持 z 维度不变。这有助于在三个方向上保留有用信息。
Lumina 的主要设计工作集中在输入表示、预处理、训练策略、后处理以及 CPU 推理行为上。
步骤 5: 使用包含边界信息的损失函数
在训练过程中,网络需要一种方法来衡量其预测与真实分割之间的差异。这种度量称为 损失函数。网络通过调整权重来降低此损失。
对于 Lumina,我们将两种常见的损失函数结合使用:Dice loss 和 binary cross-entropy (BCE)。
Dice 损失
Dice 损失关注预测肿瘤区域与真实区域之间的重叠。
当我们希望预测区域的整体大小和形状与参考分割匹配时,这非常有用。
二元交叉熵
二元交叉熵在体素级别起作用。对于每个体素,网络预测一个介于 0 和 1 之间的概率。
真实值为:
如果体素属于肿瘤,则为 1
如果体素属于背景,则为 0
当预测概率与真实值不同时,BCE 会对网络进行惩罚。
例如,如果一个肿瘤体素的预测概率接近1,则惩罚较小。如果网络有把握地将一个肿瘤体素预测为背景,则惩罚较大。
我们通过将两个损失相加来组合它们:
loss = dice_loss + bce_loss
这两个术语提供不同的训练信号:
Dice 损失:鼓励整体区域重叠度良好。
BCE:鼓励单个体素预测准确。
添加边界信息
Dice 和 BCE 未对病变边界给予特殊处理。
这一点很重要,因为分割可能整体重叠度良好,但边界仍然不准确。
Lumina 同时使用以下两项进行评估:
Dice,衡量区域重叠度
NSD (归一化表面 Dice),衡量指定距离内的表面一致性
官方挑战的容忍度为 1 mm。
为了向网络提供更多边界信息,我们添加了边界带损失项。
我们通过膨胀和收缩目标掩码,在真实表面周围创建一个薄带。
shell = dilate(target) & ~erode(target)
loss = (
dice_loss
+ bce_loss
+ 0.5 * bce_loss_on(shell)
)
因此,这三个术语具有不同的作用:
Dice loss:整体区域重叠
BCE:体素级预测准确度
Boundary-band loss:对病变表面附近的体素给予额外关注
额外的边界项权重为 0.5。
比较边界损失
MONAI 还提供 HausdorffDTLoss,它使用基于距离变换的公式。
我们可以在训练过程中比较不同方法的计算开销:
| 损失 | 每步耗时 |
|---|---|
| Dice + 交叉熵 | 7 ms |
| 我们的(边界带) | 203 ms |
| MONAI HausdorffDTLoss | 957 ms |
Hausdorff 距离损失的快速路径需要 cupy 库,但在我们的训练环境中无法编译。其 CPU 实现开销要大得多。
我们的边界带方法基于最大池化的形态学操作,计算开销较低。
边界项使大肿瘤的 Dice 提高了 0.0136。对于小肿瘤,结果呈现混合:15 例有所改善,13 例有所恶化。
步骤 6:将概率图转换为分割掩码
分割网络输出一个概率图。该图中的每个体素具有一个 0 到 1 之间的值,表示该体素属于肿瘤的可能性。我们现在可以将此概率图转换为最终的 3D 分割掩码。共有三个步骤:
将概率图重新采样回原始 CT 网格。
应用阈值以生成二值掩码。
保留与 RECIST 线相关的连通分量。
重新采样概率图
在预处理过程中,我们将 CT 图像裁剪并重新采样到固定大小的 64 × 160 × 160 体素。因此,网络在此处理后的网格上生成其预测。
在创建最终掩码之前,我们将 概率图 重新采样回原始 CT 图像网格。
prob_original = resample_to_original_grid(
probability_map,
original_image
)
我们在阈值处理前进行此步骤,以便在原始网格上对概率值进行插值。这样可以更准确地表示最终边界。
应用阈值
网络为每个体素输出介于 0 和 1 之间的概率。我们使用阈值 0.35 将此概率图转换为二值分割掩码。
mask = prob_original >= 0.35
概率至少为 0.35 的体素成为预测肿瘤的一部分,其余体素则被视为背景。
阈值 0.35 在 Lumina 的验证实验中被选定,并且是最终推理管道的一部分。
保持肿瘤与 RECIST 线相连
阈值掩码可能包含一些小的不连通区域。其中有些区域可能不属于肿瘤。
由于我们知道肿瘤标记的位置,我们使用 RECIST 线来选择相关的连通分量。
components = connected_components(mask)
tumor_mask = select_component(
components,
recist_line
)
我们将与 RECIST 线相交的组件作为最终的肿瘤分割结果。
如果在阈值处理后 RECIST 线未与任何组件相交,则使用距离线中点最近的组件作为备选。
这些操作的顺序很重要:
Probability map
↓
Resample to original CT grid
↓
Apply threshold (0.35)
↓
Connected-component selection
↓
Final 3D tumor mask
为什么在阈值化前要进行重采样?
我们在阈值化前对 概率图在阈值化前 进行重采样,以便在原始 CT 网格上对概率值进行插值。在开发过程中,我们也测试了在重采样前进行阈值化。该方法会在病变边界产生小的变化,因为插值作用于已经二值化的掩码。
使用概率图可以在插值过程中保留更多信息,并使最终分割得到更精确的边界。
结果是一个与原始 CT 扫描对齐的 3D 二值掩码,可用于评估或可视化。
步骤 7:加速并确保 CPU 推理的一致性
CPU 和内存限制会影响推理设计。
我们使用多种技术来确保推理符合挑战的限制。
固定线程数量
在不同线程数下运行计算时,CPU 操作可能会产生微小的数值差异。
在诸如 0.35 这样的分割阈值附近,概率的微小变化可能会影响体素是否被纳入最终掩码。
因此,我们明确设置了 PyTorch 的线程数:
torch.set_num_threads(8)
在每次运行中使用相同的线程配置有助于保持推理过程的可重复性。
推理通道预算
我们使用集成方法,因为将多次模型推理的预测结果结合起来可以提升分割效果。
该集成方法使用来自不同输入的预测结果,包括图像的翻转版本以及单独训练的 SegResNet 模型。
病灶数量在不同扫描之间可能有所不同。如果对每个病灶使用四次推理,那么包含五个病灶的扫描需要 20 次模型推理,这可能会超出挑战的运行时间限制。
因此,我们为每次扫描设定了最大推理次数,并根据标记的病灶数量调整推理次数。
简化版如下:
want = max(
1,
min(len(members) + 1, cap // max(len(ids), 1))
)
| 肿瘤数量 | 每个肿瘤的遍历次数 |
|---|---|
| 1 | 4 |
| 2 | 3 |
| 3 | 2 |
| 超过3 | 1 |
计划在推理开始前根据标记的数量来确定。这使得计算量可预测,并避免结果取决于执行过程中恰好剩余的时间。
测量结果如下:
| 设置 | 得分 |
|---|---|
| 无集成 | 0.7242 |
| 上限 4 | 0.7324 |
| 上限 6 | 0.7361 |
| 无上限(始终 4 次遍历) | 0.7410 |
无限制的集成获得了最高得分,但未满足运行时间限制。上限为 6 时保留了集成改进的大部分,同时使推理保持在所需限制内。
处理单个失败
处理多个病例时,单个失败的病例不应导致整个管道停止。
如果某个病例无法处理,我们会生成一个空掩码并记录错误。然后管道继续处理剩余的病例。
即使单个病例出现问题,这也能使整批处理完成。
结果
在217个隐藏测试扫描上,Lumina 取得了以下成绩:
Dice:0.7619
NSD:0.6094
NSD 值采用官方的 1 mm 容忍度。
每个病例的中位推理时间为 20.3 秒。观察到的最慢运行时间为 31.8 秒。
峰值内存使用量为 2.22 GB(在 8 GB 容器内)。
Docker 镜像大约为 559 MB。
定性结果
边界清晰的大病变
第一个例子是一个体积为 272.9 cm³ 的大病变。它取得了:
DSC:0.961
NSD:0.850
该病变边界清晰,与周围脂肪具有清晰的界面。
预测轮廓在病变可见范围内紧密遵循参考边界。
边界不明确的中等病灶
第二个例子是一个体积为 28.4 cm³ 的中等病灶。它实现了:
DSC: 0.563
NSD: 0.114
预测覆盖的区域大于参考标注。
放射科医生的评审表明,此类病例在参考边界本身可能存在不确定性。因此,低分数本身并不能说明预测轮廓在临床上不可接受。
放射科医生评审显示了什么
我们还与一位放射科医生一起审查了 30 个病灶,以更好地了解 Lumina 所犯的错误类型。
评审显示,病灶大小并不是影响分割的唯一因素。
边界清晰的病灶通常更易于分割。更困难的病例边界定义不清或与周围组织外观相似。
主要错误包括:
欠分割:病灶的一部分被遗漏。
过分割:周围组织被包含进去。
边界错误:预测轮廓未能遵循不明确或浸润性的边界。
评审还表明,数值指标应与可见的病灶边界一起解读。
在某些情况下,参考边界本身难以定义。因此,低分并不一定意味着预测轮廓在临床上不可接受。
这与在困难病灶中观察到的较低表面准确度一致,其中微小的边界差异可能对 NSD 产生强烈影响。
适用于其他项目的三个经验教训
1. 使验证数据具有代表性
验证集应反映最终系统将被评估的分布。
我们的内部验证划分来源于训练数据。其中肿瘤体积的中位数为 814 mm³。
竞赛评分集的肿瘤体积中位数为 16,805 mm³,这要大得多。
这种差异影响了一些改进在开发过程中表现的方式。
例如,边界损失在内部验证集上几个 epoch 内几乎没有改善。在公共验证分布上(其中包含较大的病灶),其效果更有用。
如果改进针对数据分布的特定部分,则该分布应在验证集中得到体现。
2. 测量预处理的极限
在Lumina中,160 mm的裁剪是一个重要的设计选择。
一些较大的病变超出了此视野范围。
在更改裁剪尺寸之前,我们测量了裁剪和重采样引入的误差。
我们将真实掩码通过相同的裁剪-重采样管道,并测量得到的表面得分。
对于较大的病变,几何上限为 0.9699 NSD,而模型达到的为 0.5353 NSD。
这表明预处理管道在保留病变表面方面表现相当好。仅靠预处理无法解释剩余的差距。
我们还测试了几种扩大视野的方法,包括:
重叠瓦片
自适应缩放
更大的裁剪
这些更改未能提升最终得分。
测量预处理上限帮助我们将后续开发重点放在模型和推理管道上。
3. 记录数字的来源
模型开发涉及许多配置值:
阈值
裁剪尺寸
体素间距
损失权重
采样权重
集成设置
运行时限制
每个值都应有明确的来源。
例如,记录:
测量了什么
使用了哪个数据集
测量是在何时进行的
测试了哪些替代方案
为何选择最终值
这有助于以后复现实验并理解设计决策。
结论
Lumina表明,单条RECIST线可从CT扫描中重建肿瘤的3D范围。
该系统结合了基于提示的输入通道、固定的物理裁剪、3D DynUNet、感知边界的训练、基于概率的后处理以及受控的CPU推理。
在217个保留测试病例上,Lumina在官方1mm容忍度下取得了Dice得分0.7619和NSD 0.6094。
中位推理时间为20.3秒,峰值内存使用为2.22 GB,使系统保持在挑战约束之内。
实验还表明,边界精度仍是改进的重要方向,尤其是对较大且边界不明确的病变。
几何天花板分析表明,裁剪和重采样管道很好地保留了病变表面,说明进一步的改进应集中在分割的准确性和鲁棒性上。
总体而言,Lumina 展示了一种实用的方法,能够在满足严格的计算限制的同时将 2D RECIST 测量转换为 3D 病变分割。