← 返回
IT技术

如何将RECIST线转换为3D肿瘤分割掩码

✍️ zhirenhun 📅 2026/9/19 👁 62 阅读 ⏱ 35 分钟
如何将RECIST线转换为3D肿瘤分割掩码

放射科医生可以在CT扫描上通过画一条直线来标记肿瘤。创建完整的3D分割需要在肿瘤出现的切片上进行轮廓描绘,这更耗时。

本教程解释了Lumina的工作原理。它是一个系统,使用CT扫描和一条RECIST线来生成标记肿瘤的3D分割掩码。

Lumina是为FLARE 2026全癌症分割挑战赛而开发的。该系统被设计为在CPU上运行,内存限制为8 GB,推理时间限制为60秒。

本教程涵盖了塑造最终系统的主要设计决策、实现细节和实验。

我们将涵盖的内容:

  • 结果

  • 放射科医生评审显示了什么

  • 适用于其他项目的三个经验

  • 结论

  • Lumina 所做的

    放射科医生可以通过在肿瘤最长可见直径上画一条线来测量肿瘤。这是一种 RECIST 测量(实体瘤的响应评估标准),广泛用于在癌症治疗期间测量肿瘤反应。

    RECIST 线测量的是 2D 直径标记。它不描述肿瘤的完整 3D 形状。

    腹部的轴向 CT 图像,其中有一个绿色直径标记横跨肝脏病变。

    Lumina 使用此测量来提示 3D 分割模型。

    任务可以描述为:

    Input: 一个 3D CT 扫描和一个标记一个肿瘤的 2D RECIST 线。

    Output: 该肿瘤的 3D 掩码,每个体素有一个标签。

    体素 是像素的 3D 等价物。每个体素代表 CT 扫描中的一个小体积组织。

    RECIST 线告诉模型需要分割哪个肿瘤。然后模型预测肿瘤的三维范围。

    这使得分割问题更具特异性,因为模型不需要识别扫描中的每一个可能的肿瘤。

    Lumina 管道概览

    完整的过程包含几个阶段。我们从一个 3D CT 扫描和一个标记一个肿瘤的 RECIST 线开始。然后我们将该线转换为额外的输入通道,裁剪并重采样图像,并将三个通道通过一个 3D 分割网络。

    网络生成一个概率图,我们通过重采样、阈值化和连通分量选择将其转换为最终的3D肿瘤掩码。

    显示从CT扫描和RECIST线到3D肿瘤分割掩码的Lumina流水线的示意图

    主要阶段如下:

    1. 准备CT扫描和RECIST标记。

    2. 将RECIST线编码为额外的网络输入。

    3. 裁剪并重采样图像到通用网格。

    4. 使用3D分割网络预测肿瘤概率图。

    5. 优化概率图并将其转换为二值掩码。

    6. 控制CPU推理,使整个案例在运行时间和内存限制内。

    先决条件

    如果您具备以下经验,将能从本教程中获益更多:

    不需要详细的医学背景。教程在介绍概念时会解释医学影像概念。

    该实现使用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图像和用作Lumina输入的两个RECIST提示通道:RECIST线和端点热图。

    两个端点定义了测量的直径,并为网络提供了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 架构,网络深度、卷积核、步长和残差连接均可灵活配置。

    DynUNet 架构简化图,展示了编码器、解码器、瓶颈层以及用于保留空间信息的跳跃连接

    U-Net 包含两个主要部分。

    跳跃连接会把编码器中的精细空间细节传递给解码器,帮助它在生成最终分割结果时更好地还原肿瘤边界。

    在 Lumina 中,网络的输入包含三个通道:

    模型配置如下:

    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 lossbinary cross-entropy (BCE)

    Dice 损失

    Dice 损失关注预测肿瘤区域与真实区域之间的重叠。

    当我们希望预测区域的整体大小和形状与参考分割匹配时,这非常有用。

    二元交叉熵

    二元交叉熵在体素级别起作用。对于每个体素,网络预测一个介于 0 和 1 之间的概率。

    真实值为:

    当预测概率与真实值不同时,BCE 会对网络进行惩罚。

    例如,如果一个肿瘤体素的预测概率接近1,则惩罚较小。如果网络有把握地将一个肿瘤体素预测为背景,则惩罚较大。

    我们通过将两个损失相加来组合它们:

    loss = dice_loss + bce_loss
    

    这两个术语提供不同的训练信号:

    添加边界信息

    Dice 和 BCE 未对病变边界给予特殊处理。

    这一点很重要,因为分割可能整体重叠度良好,但边界仍然不准确。

    Lumina 同时使用以下两项进行评估:

    官方挑战的容忍度为 1 mm

    为了向网络提供更多边界信息,我们添加了边界带损失项。

    我们通过膨胀和收缩目标掩码,在真实表面周围创建一个薄带。

    shell = dilate(target) & ~erode(target)
    
    loss = (
        dice_loss
        + bce_loss
        + 0.5 * bce_loss_on(shell)
    )
    

    因此,这三个术语具有不同的作用:

    额外的边界项权重为 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 分割掩码。共有三个步骤:

    1. 将概率图重新采样回原始 CT 网格。

    2. 应用阈值以生成二值掩码。

    3. 保留与 RECIST 线相关的连通分量。

    重新采样概率图

    在预处理过程中,我们将 CT 图像裁剪并重新采样到固定大小的 64 × 160 × 160 体素。因此,网络在此处理后的网格上生成其预测。

    在创建最终掩码之前,我们将 概率图 重新采样回原始 CT 图像网格。

    prob_original = resample_to_original_grid(
        probability_map,
        original_image
    )
    

    我们在阈值处理前进行此步骤,以便在原始网格上对概率值进行插值。这样可以更准确地表示最终边界。

    应用阈值

    网络为每个体素输出介于 01 之间的概率。我们使用阈值 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
    5c02d6d1-2f89-45e3-a5d6-06524acdb5ea

    计划在推理开始前根据标记的数量来确定。这使得计算量可预测,并避免结果取决于执行过程中恰好剩余的时间。

    测量结果如下:

    设置 得分
    无集成 0.7242
    上限 4 0.7324
    上限 6 0.7361
    无上限(始终 4 次遍历) 0.7410

    无限制的集成获得了最高得分,但未满足运行时间限制。上限为 6 时保留了集成改进的大部分,同时使推理保持在所需限制内。

    处理单个失败

    处理多个病例时,单个失败的病例不应导致整个管道停止。

    如果某个病例无法处理,我们会生成一个空掩码并记录错误。然后管道继续处理剩余的病例。

    即使单个病例出现问题,这也能使整批处理完成。

    结果

    在217个隐藏测试扫描上,Lumina 取得了以下成绩:

    NSD 值采用官方的 1 mm 容忍度

    每个病例的中位推理时间为 20.3 秒。观察到的最慢运行时间为 31.8 秒

    峰值内存使用量为 2.22 GB(在 8 GB 容器内)。

    Docker 镜像大约为 559 MB

    定性结果

    边界清晰的大病变

    第一个例子是一个体积为 272.9 cm³ 的大病变。它取得了:

    显示边界清晰的大病变的CT图像;预测分割紧密参考,DSC 0.961,NSD 0.850。

    该病变边界清晰,与周围脂肪具有清晰的界面。

    预测轮廓在病变可见范围内紧密遵循参考边界。

    边界不明确的中等病灶

    第二个例子是一个体积为 28.4 cm³ 的中等病灶。它实现了:

    中等病灶的CT图像,其中预测覆盖的区域大于参考标注;边界不明确,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.7619NSD 0.6094

    中位推理时间为20.3秒,峰值内存使用为2.22 GB,使系统保持在挑战约束之内。

    实验还表明,边界精度仍是改进的重要方向,尤其是对较大且边界不明确的病变。

    几何天花板分析表明,裁剪和重采样管道很好地保留了病变表面,说明进一步的改进应集中在分割的准确性和鲁棒性上。

    总体而言,Lumina 展示了一种实用的方法,能够在满足严格的计算限制的同时将 2D RECIST 测量转换为 3D 病变分割。

    ——

    🧑‍💻

    zhirenhun

    一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。