← 文章 / AI技术
freeCodeCamp 5小时前 · 2026-09-19 09:21:59 · 3 阅读

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

放射科医生可以通过在 CT 扫描上画一条穿过肿瘤的直线来标注肿瘤。而要生成完整的 3D 分割,则需要在肿瘤出现的每个切片上都进行勾画,这要耗时得多。

本教程介绍 Lumina 的工作原理。它是一个只需 CT 扫描加一条 RECIST 线,就能生成所标注肿瘤的 3D 分割掩码的系统。

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

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

我们将涵盖的内容:

  • Lumina 做什么

  • 前置知识

  • 第 1 步:动手写代码前先检查数据

  • 第 2 步:把 RECIST 线转换为网络输入

  • 第 3 步:把所有肿瘤对齐到统一网格上

  • 第 4 步:构建 3D 分割网络

  • 第 5 步:使用包含边界信息的损失函数

  • 第 6 步:把概率图转换为分割掩码

    • 对概率图进行重采样

    • ... Axial CT image of the abdomen with a green diameter marker placed across a liver lesion.

      体素是像素的三维对应物。在 CT 扫描中,每个体素代表一小块组织体积。

      RECIST 标注线告诉模型需要分割哪个肿瘤,随后模型预测该肿瘤的三维范围。

      这使得分割任务更具针对性,因为模型无需识别扫描中所有可能的肿瘤。

      Lumina 流程概览

      完整流程包含多个阶段。我们从一幅 3D CT 扫描和一条标记特定肿瘤的 RECIST 线开始,然后将该线转换为额外的输入通道,对图像进行裁剪和重采样,最后将三个通道送入 3D 分割网络。

      网络输出概率图,经重采样、阈值处理和连通域筛选后,得到最终的 3D 肿瘤掩膜。

      Diagram showing the Lumina pipeline from a CT scan and RECIST line to a 3D tumor segmentation mask.

      主要阶段包括:

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

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

      3. 将图像裁剪并重采样到统一网格。

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

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

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

      前置知识

      如果你熟悉以下内容,会更容易理解本教程:

      • Python

      • NumPy

      • PyTorch

      • 基础卷积神经网络。

      无需深入的医学背景知识,教程会在介绍相关概念时解释医学成像原理。

      实现基于 NumPy、SciPy、PyTorch 和 MONAI(一个基于 PyTorch 的医学影像框架)。

      第 1 步:编码前检查数据

      我们的数据以 .npz 文件形式提供,这是 NumPy 的压缩数组格式。

      每个文件包含如下字段:

      imgs       # CT 扫描,一个 3D 数组
      recist     # 标记线,形状相同,每个肿瘤对应一个整数
      spacing    # 体素之间的间距(毫米)
      origin     # 扫描在扫描仪坐标系中的位置
      direction  # 扫描的旋转方式
      gts        # 真值分割标注(仅训练文件有)
      

      在搭建模型之前,我们先检查了图像数值、数组形状和空间元数据。

      其中有两个细节尤为重要。

      扫描图像已经做过亮度调整

      CT 扫描仪通常以 Hounsfield 单位(HU)存储图像。水约为 0 HU,骨骼则可超过 1000 HU。

      而在本数据集中,扫描图像已被转换到固定的 0–255 范围,原始的 HU 值并不可得。

      这意味着我们无法对原始值做常规的 CT 窗宽窗位处理,只能改为测量所提供数据的强度分布。

      比如,有 53.4% 的体素值恰好为 0,对应身体外的空气。

      坐标顺序很关键

      spacing 数组按 (X, Y, Z) 存储,而 NumPy 数组的索引顺序是 (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 image and the two RECIST prompt channels used as input to Lumina: the RECIST line and endpoint heatmap.
      • 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]     # 每个体素的毫米数:z, y, x
      crop_size:      [64, 160, 160]      # 体素数量: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 的网络接收三个通道:

      • 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 mm,而层内间距只有 1.0 mm。如果在每一级都对三个维度同时下采样,层间细节会丢失得太快。因此我们在第一次下采样时保持 z 维不变,这样能在三个方向上都保留有用信息。

      Lumina 的主要设计工作集中在输入表示、预处理、训练策略、后处理以及 CPU 推理行为上。

      第 5 步:使用包含边界信息的损失函数

      训练时,网络需要一种方式来衡量预测结果与真实分割之间的差异,这个衡量指标就是损失函数,网络通过调整权重来降低损失。

      Lumina 组合了两种常见的损失:Dice 损失二元交叉熵(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 损失:整体区域重叠度

      • BCE:体素级预测准确度

      • 边界带损失:对病灶表面附近体素的额外关注。

      额外的边界项权重设为 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 网格上进行插值。开发过程中我们也试过先阈值化再重采样,但那样做等于对已经二值化的 mask 做插值,会在病灶边界产生细微变化。

      而先处理概率图,可以在插值过程中保留更多信息,让最终分割获得更精确的边界。

      最终得到的是一个与原始 CT 扫描对齐的 3D 二值 mask,可以直接用于评估或可视化。

      第 7 步:加速 CPU 推理并保证结果一致性

      CPU 和内存限制直接影响推理方案的设计。

      为了满足挑战赛的资源约束,我们采用了多种技术手段。

      固定线程数

      CPU 计算在不同线程数下运行时,可能产生微小的数值差异。

      0.35 这样的分割阈值附近,概率值哪怕极小的变化都可能影响某个体素是否被纳入最终 mask。

      因此我们显式设置了 PyTorch 的线程数:

      torch.set_num_threads(8)
      

      每次运行使用相同的线程配置,有助于保证推理过程可复现。

      控制推理次数

      我们使用 ensemble,因为综合多个模型 pass 的预测能提升分割效果。

      这个 ensemble 融合了来自不同输入的预测,包括图像的翻转版本,以及一个单独训练的 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 取得:

      • Dice: 0.7619

      • NSD: 0.6094

      NSD 数值采用官方 1 mm 容差 标准。

      单案例推理中位耗时为 20.3 秒,观察到的最慢耗时为 31.8 秒

      8 GB 容器内峰值内存占用为 2.22 GB

      Docker 镜像大小约为 559 MB

      定性结果

      边界清晰的大病灶

      第一个示例是一个体积为 272.9 cm³ 的大病灶,指标如下:

      • DSC:0.961

      • NSD:0.850

      CT images of a well-circumscribed large lesion with a clear boundary; predicted segmentation closely follows the reference, with DSC 0.961 and NSD 0.850.

      该病灶边界清晰,与周围脂肪组织的界面分明。

      在病灶可见范围内,预测轮廓紧贴参考边界。

      边界模糊的中等病灶

      第二个示例是一个体积为 28.4 cm³ 的中等病灶,指标如下:

      • DSC:0.563

      • NSD:0.114

      CT images of a medium lesion where the prediction covers a larger region than the reference annotation; the boundary is unclear, with DSC 0.563 and NSD 0.114.

      预测区域明显大于参考标注。

      放射科医师审查指出,此类病例中参考边界本身可能存在不确定性。因此,低数值评分本身并不足以证明预测轮廓在临床上不可接受。

      放射科医师审查结果

      我们还邀请放射科医师共同审查了 30 个病灶,以深入理解 Lumina 产生的错误类型。

      审查发现,病灶大小并非影响分割效果的唯一因素。

      边界清晰的病灶通常更容易分割,而边缘模糊或与周围组织外观相似的情况则更具挑战性。

      主要错误类型包括:

      • 欠分割:遗漏部分病灶。

      • 过分割:将周围组织误纳入。

      • 边界错误:预测轮廓未能贴合模糊或浸润性边缘。

      审查还表明,数值指标应与可见的病灶边界结合解读。

      在某些情况下,参考边界本身难以界定。因此,低分并不一定意味着预测轮廓在临床上不可接受。

      这与困难病灶中观察到的较低表面精度一致——在这种病灶上,边界的微小差异就会对 NSD 产生很大影响。

      三条适用于其他项目的经验

      1. 让验证数据具有代表性

      验证集应能反映最终系统实际面对的数据分布。

      我们的内部验证集来自训练数据,其中位肿瘤体积为 814 mm³

      而竞赛评分集的中位肿瘤体积高达 16,805 mm³,大得多。

      这一差异影响了一些改进在开发过程中的表现。

      例如,边界损失在内部验证集上连续多个 epoch 几乎没有提升;但在包含更大病灶的公开验证分布上,它的效果却更有用。

      如果某项改进针对的是数据分布的特定部分,那么验证集中就应体现这部分分布。

      2. 量化预处理的极限

      160 mm 的裁剪范围是 Lumina 中的一个重要设计选择。

      一些大病灶超出了这个视野范围。

      在调整裁剪大小之前,我们测量了裁剪和重采样引入的误差。

      方法是把 ground-truth 掩码跑一遍同样的裁剪加重采样流程,然后测量由此得到的表面分数。

      对于大病灶,几何上限是 0.9699 NSD,而模型达到了 0.5353 NSD

      这说明预处理流程对病灶表面的保留是相当不错的,剩余的差距并不能归咎于预处理。

      我们还尝试了几种扩大视野的方法,包括:

      • 重叠切片(tiles)

      • 自适应缩放

      • 更大的裁剪范围

      这些改动都没有提升最终分数。

      量化预处理的上限,让我们得以把后续开发精力集中在模型和推理流程上。

      3. 记录每个数字的来源

      模型开发涉及大量配置值:

      • 阈值

      • 裁剪大小

      • 体素间距

      • 损失权重

      • 采样权重

      • Ensemble 设置

      • 运行时限

      每个值都应有明确的依据。

      比如,要记录:

      • 测量对象是什么

      • 使用了哪个数据集

      • 测量发生的时间

      • 测试了哪些备选方案

      • 最终选择该数值的理由

      这样做有助于后续复现实验和理解设计决策。

      结论

      Lumina 证明仅凭单条 RECIST 线即可从 CT 扫描中重建肿瘤的三维范围。

      该系统结合了基于提示的输入通道、固定物理裁剪、3D DynUNet、边界感知训练、基于概率的后处理以及受控的 CPU 推理。

      在 217 个留置病例上,Lumina 在官方 1 mm 容差下实现了0.7619 的 Dice 分数0.6094 的 NSD

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

      实验还表明,边界精度仍是需要改进的重要领域,尤其是对于体积较大且边界定义不清的病灶。

      几何上限分析显示,裁剪和重采样流程很好地保留了病灶表面,这意味着进一步的改进应聚焦于分割精度和鲁棒性。

      总体而言,Lumina 展示了一种切实可行的方法,在满足严格计算约束的同时,将二维 RECIST 测量值转换为三维病灶分割。

原始来源: freeCodeCamp

评论 (0)