2. PET/CT原始数据与DICOM解析

PET/CT 的原始数据在进入重建与融合流程之前,首先以 DICOM 文件的形式存在。理解这些文件的标签结构、序列组织方式以及 SUV 相关字段,是工程化读取模块能够稳定工作的前提。本节从 PET 与 CT 两类数据的标签差异入手,逐步展开到多帧与动态数据的读取,并给出一个可参考的解析模块实现思路。

PET 与 CT 的 DICOM 标签差异

CT 图像通常是单帧灰度图像,一个 SOP Instance 对应一个切片,像素数据存放在 (7FE0,0010) 中,配合 Rows、Columns、PixelSpacing、SliceThickness、ImagePositionPatient、ImageOrientationPatient 即可确定空间位置。CT 的 RescaleSlope 与 RescaleIntercept 用于把存储值转换为 HU 值,这是后续与 PET 做衰减校正和融合显示的基础。

PET 图像则复杂得多。它可能是单帧、多帧,也可能是动态或门控数据。PET 的像素值本身不是 SUV,而是经过衰减校正、散射校正和归一化后的计数或图像强度。要得到 SUV,必须结合 RadiopharmaceuticalInformationSequence 中的放射性药物信息、PatientWeight、SeriesTime、AcquisitionTime 以及 Units 字段共同计算。PET 的 Units 常见取值为 BQML、CNTS、GML 等,不同取值决定了后续 SUV 换算的路径。

PET/CT DICOM 标签解析与 SUV 计算链路 CT 图像标签 (7FE0,0010) PixelData (0028,0010) Rows / Columns (0028,0030) PixelSpacing (0028,1052) RescaleIntercept (0028,1053) RescaleSlope (0020,0032) ImagePositionPatient → 单帧,逐切片存储 PET 图像标签 (7FE0,0010) PixelData (0028,0008) NumberOfFrames (0054,0016) Radiopharm.InfoSeq (0010,1030) PatientWeight (0008,0031) SeriesTime (0054,1001) Units → 多帧/动态,需按帧解析 SUV 计算关键字段 注射剂量 (RadionuclideTotalDose) 注射时间 (Radiopharm.StartTime) 体重 (PatientWeight, kg) 半衰期 (RadionuclideHalfLife) 采集时间 (AcquisitionTime) 衰减校正因子 (DecayCorrection) → SUVbw = 像素值 × 体重 / 衰减剂量 序列组织与读取策略 SeriesInstanceUID 分组 按 ImagePositionPatient 排序 多帧按 FrameIncrementPointer 动态数据按 TemporalPosition 工程化读取要点 • 先读 Metadata(StopBeforePixelData),再按需加载 PixelData,避免大内存占用 • 多帧 PET 需检查 NumberOfFrames 与 FrameIncrementPointer,确认帧间时间间隔 • SUV 计算前必须验证 Units 字段,BQML 与 CNTS 的换算路径不同

序列组织与多帧数据读取

一个 PET/CT 检查通常包含多个 Series:CT 定位像、CT 螺旋扫描、PET 发射扫描、PET 透射扫描(若为旧式 PET/CT)以及可能的融合图像。工程上首先按 SeriesInstanceUID 分组,再按 ImagePositionPatient 的 Z 轴坐标排序,才能还原出正确的三维体数据顺序。对于多帧 PET,NumberOfFrames 给出总帧数,FrameIncrementPointer 指向描述帧间时间或位置的标签,常见如 (0018,1063) FrameTime 或 (0054,1300) FrameReferenceTime。

动态 PET 的读取需要额外关注 TemporalPositionIdentifier 和 ActualFrameDuration。例如一个 60 分钟的动态采集可能包含 20 帧,每帧持续时间不同,早期帧短、后期帧长。读取时不能简单地把所有帧当作等间隔处理,而应逐帧提取时间戳,建立“帧号—起始时间—持续时间”的映射表。这个映射表是后续时间-活度曲线(TAC)分析的基础。

SUV 相关字段与计算路径

SUVbw 的常用公式为:SUVbw = 像素值 × 体重(kg) × 1000 / 注射剂量(Bq) × 衰减校正因子。其中衰减校正因子由注射时间与采集时间之差、放射性核素半衰期共同决定。在 DICOM 中,注射剂量位于 RadiopharmaceuticalInformationSequence 的 RadionuclideTotalDose,注射时间位于 RadiopharmaceuticalStartTime,半衰期位于 RadionuclideHalfLife。采集时间则来自 AcquisitionTime 或 SeriesTime。

需要特别注意的是,部分 PET 设备的 Units 字段为 CNTS,此时像素值代表计数而非活度浓度,直接套用 SUV 公式会得到错误结果。工程实现中应先判断 Units,若为 BQML 则按标准路径计算;若为 CNTS,则需要设备厂商提供的标定因子或改用其他半定量指标。此外,DecayCorrection 字段若为 START,表示数据已按扫描起始时间做衰减校正,计算 SUV 时不应重复校正。

常见解析库与工程化读取模块

Python 生态中,pydicom 是最常用的 DICOM 解析库,适合逐标签读取和元数据提取;SimpleITK 和 ITK 更适合直接读取像素数据并做几何变换;nibabel 对 NIfTI 支持更好,但 DICOM 读取能力有限。工程化模块通常采用“元数据先行、像素按需加载”的策略:先用 pydicom.dcmread(path, stop_before_pixels=True) 读取标签,建立索引,再根据业务需要加载特定序列的像素数据。

import pydicom
import numpy as np

def load_pet_series(series_paths):
    """读取 PET 序列,返回体数据与 SUV 因子"""
    slices = []
    for p in series_paths:
        ds = pydicom.dcmread(p)
        slices.append(ds)

    # 按 Z 轴位置排序
    slices.sort(key=lambda s: float(s.ImagePositionPatient[2]))

    # 提取 SUV 计算参数
    ds0 = slices[0]
    weight_kg = float(ds0.PatientWeight)
    dose_bq = float(ds0.RadiopharmaceuticalInformationSequence[0]
                    .RadionuclideTotalDose)
    half_life = float(ds0.RadiopharmaceuticalInformationSequence[0]
                      .RadionuclideHalfLife)
    # 注射与采集时间差(秒)
    from datetime import datetime
    inj_time = datetime.strptime(
        ds0.RadiopharmaceuticalInformationSequence[0]
        .RadiopharmaceuticalStartTime, "%H%M%S")
    acq_time = datetime.strptime(ds0.AcquisitionTime, "%H%M%S")
    dt = (acq_time - inj_time).total_seconds()
    decay_factor = 2 ** (-dt / half_life)

    # 组装体数据
    volume = np.stack([s.pixel_array for s in slices], axis=0)
    suv_factor = weight_kg * 1000 / (dose_bq * decay_factor)
    return volume, suv_factor

上述代码展示了最基本的读取与 SUV 因子计算流程。实际工程中还需要处理:多帧数据的 pixel_array 形状判断、RescaleSlope 与 RescaleIntercept 的应用、不同厂商私有标签的兼容、以及大体积数据的分块加载。建议在模块中增加校验步骤:检查 Modality 是否为 PT、Units 是否可识别、NumberOfFrames 与像素数组维度是否一致。只有通过这些校验,才能保证后续 SUV 计算和融合显示的可靠性。

在读取 PET 数据时,优先使用 stop_before_pixels=True 建立元数据索引,再按序列 UID 批量加载像素数据。这样可以在处理多检查、多序列时显著降低内存峰值,也便于并行化。