
我先说结论在拿到 Lung-PET-CT-Dx 这份数据之前我差点被“找数据”这件事劝退。做肺癌目标检测特别是想在 PET/CT 这种多模态影像上训练检测模型的朋友应该都有同感公开的自然图像数据集一抓一大把COCO、VOC 随便下但医学影像领域尤其是带真实医生勾画标注的肺部肿瘤数据少得可怜。后来我把目光锁定在 TCIAThe Cancer Imaging Archive上的 Lung-PET-CT-Dx 集合才算是真正迈过了第一步。这篇文章我会把从 TCIA 获取这份数据集的完整流程梳理出来包括注册、协议、三种下载方式、下载后的数据体检以及为后续目标检测训练做准备的坐标转换和避坑经验。无论你是第一次接触 DICOM 和 PET/CT还是已经在医学影像目标检测里扑腾过一段时间这篇都能帮你省下不少瞎摸索的时间。1. 这份数据能干什么Lung-PET-CT-Dx 的结构与价值1.1 集合里到底有哪些模态标注是什么形式Lung-PET-CT-Dx 是 TCIA 上的一个公开癌症影像集合数据主体是肺癌患者的 FDG-PET/CT 扫描。FDG-PET 的原理是给患者注射带有放射性示踪剂的葡萄糖类似物肿瘤组织代谢旺盛会大量摄取所以在 PET 图像上表现为高亮区域这也是为什么它在肿瘤检测任务里比单纯 CT 更有判别力。具体到模态每个患者通常会包含几个不同的 SeriesCT螺旋 CT 扫描提供解剖结构单位是 HUHounsfield Unit。PET代谢图像提供功能信息单位经过校正后可以换算成 SUV。CT_PET_Fused部分设备会生成融合预览图这个在目标检测里一般不直接用但可以拿来人工核对位置。RTSTRUCT医生手工勾画的肿瘤轮廓这是整个数据集里最值钱的部分。RTSTRUCT 是 DICOM 格式的一种对象里面存的不是像素掩膜而是一系列世界坐标系下的轮廓点。你可以理解为医生的笔在医学影像上圈出来的边界线而这条边界线最终可以转换成我们目标检测训练需要的边界框bounding box。整个集合覆盖了几百例患者来自多个临床机构。多机构数据意味着不同扫描仪、不同采集协议对于训练目标检测模型来说反而是好事——模型见过足够多样的数据泛化能力才靠得住。1.2 为什么医学目标检测不建议直接套用自然图像流程很多人拿到数据后第一反应是像 COCO 一样把图切成 2D 切片然后丢给 YOLO。这个思路没错但直接套会有几个认知偏差。第一医学图像是三维的。一个病灶在 CT 里是一个立体团块往往横跨几十张切片。你用 2D 检测框去框它需要先决定是每一张带病灶的切片都生成框还是只挑中心切片。这两种策略做出来的数据集性质完全不同。第二目标检测的“目标”和自然图像不一样。自然图像里的目标有清晰的边缘、独立的语义而肺部肿瘤和周围组织在 CT 上常常边界模糊在 PET 上又容易出现高摄取的炎症伪影。检测器学到的不只是“找轮廓”还要学会结合两种模态的信息做判断。第三RTSTRUCT 勾画的是轮廓不是框。从轮廓到框之间的转换涉及坐标系变换、层厚对应、方向矩阵处理这些坑自然图像数据集里永远不会遇到。我在后续章节会把这些点全部铺开但第一步还是得先把数据从 TCIA 下载下来。2. 下载前的准入门槛注册、条款与存储规划2.1 注册账号时最容易卡住的几个环节TCIA 的注册入口并不难找在官网右上角点击 Login / Register填姓名、邮箱、所属机构、职位类型提交后邮箱里会收到验证链接。这里有两个容易被卡住的点。一是邮箱选择。如果你用的是某些免费邮箱验证邮件有概率被扔进垃圾箱甚至被直接拒收。我自己的经历是用机构邮箱注册后几分钟就收到邮件换用个人免费邮箱时折腾了半小时。强烈建议用带 edu 或机构域名的邮箱注册这也是 TCIA 数据使用政策里比较欢迎的方式。二是注册完成后不要急着关页面。需要先登录进到个人 profile 里确认账号已经处于 Active 状态。很多新手在注册后直接跑到 Data Portal 去下载结果发现页面一直提示没有权限其实只是账号还没激活或者没登录。2.2 许可条款、引用规范和隐私红线Lung-PET-CT-Dx 虽然属于公开集合但 TCIA 在生成下载清单时还是会要求你确认数据使用条款。这里要提醒一点不要为了图快直接点“同意”稍微扫一眼里面的硬性要求。TCIA 的数据使用协议通常包含两类约束。一类是隐私红线数据已经做了去标识化但你不得尝试重新识别患者身份也不得将数据用于任何可能追溯到个人的目的。另一类是引用规范在学术论文、公开项目中使用这份数据时必须正确引用 TCIA 以及数据集的原始文献和 DOI。很多期刊审稿人会专门检查数据集引用漏引会被要求返工。另外商业用途限制要格外注意。部分 TCIA 集合允许学术研究但对商用有额外约束。如果你是在公司内部做预研建议先让法务看一遍当时勾选的条款。2.3 先算清楚磁盘与流量账Lung-PET-CT-Dx 整个集合的体积并不小。TCIA 集合详情页通常会标注 Image Count 和 Total Size实际解压后的体积会比压缩包大不少因为 DICOM 文件几乎没有经过有损压缩。我的建议是动手指下载前先打开集合页看清楚数据量然后至少准备“预估体积 × 2”的磁盘空间。为什么是两倍因为你先要保留压缩包或原始下载目录再留出解压和转格式的空间。如果磁盘差几个 GB下载到一半写满那个感觉真的很酸爽。网络也是个隐性成本。TCIA 服务器在海外国内网络环境下几十 GB 的数据分批下载往往要跑好几个小时中间还可能断流。所以先规划好目录结构、确认出口带宽、计算好总时长比下载本身更重要。3. 从 TCIA 把数据拉下来的三套方案图形工具、API 脚本与托管存储3.1 图形客户端 NBIA Data Retriever适合第一次就用如果你只是想快速拿到一份数据先跑通流程我推荐直接用 NBIA Data Retriever这是 TCIA 官方提供的桌面客户端Java 写的支持 Windows / macOS / Linux。操作链路是这样的先在 TCIA 数据门户里找到 Lung-PET-CT-Dx 集合勾选需要的患者也可以全选点击 Download 后系统会生成一个 .tcia 格式的 manifest 文件。这个 manifest 本质上是一个清单记录了所有需要下载的 Series 的标识信息。然后打开本地安装的 NBIA Data Retriever把 manifest 拖进去选择输出目录它就会开始拉数据。有几个实操细节值得注意输出目录尽量不要带空格和中文Data Retriever 对路径的兼容性偶尔会抽风。下载过程中如果某几个文件失败可以先挂起再重新加载 manifest它会对未完成的项继续尝试但对已完成文件往往会跳过不过有时候判断不够聪明会重复下载。所以最稳妥的做法是下载完成后用第 4 章的目录结构核对一遍。如果一次选了全部患者manifest 会非常大Data Retriever 启动时要等一会儿。建议第一次只勾选 5–10 个患者试跑确认整个目录结构和你的预期一致再全量拉取。3.2 TCIA REST API 命令行批量拉取适合做数据流水线如果你打算把这个数据集做成一个可复现的数据流水线或者需要反复下载不同子集建议用 TCIA 的 REST API 写脚本。TCIA 提供了一套公开的查询接口基础地址是BASEhttps://services.cancerimagingarchive.net/services/v4/TCIA/query先用几个简单的 GET 请求摸清数据范围# 查看集合列表 curl $BASE/getCollectionValues | jq . # 查看某个集合下的患者列表 curl $BASE/getPatient?CollectionLung-PET-CT-Dx | jq . # 查看患者的检查列表 curl $BASE/getStudy?CollectionLung-PET-CT-DxPatientIDxxx | jq .拿到 StudyInstanceUID 之后继续往下一层找 Seriescurl $BASE/getSeries?StudyInstanceUID1.2.xxx | jq .Series 这一层会返回 Modality、SeriesDescription、SeriesInstanceUID 等字段这时就可以按需筛选比如只下载 CT、PT 和 RTSTRUCT。确定要下载的 SeriesInstanceUID 后用 getImage 端点拉取curl -u your_username:your_api_key \ $BASE/getImage?SeriesInstanceUID1.2.xxx \ -o series.zip这里有个细节TCIA 部分端点对匿名访问有限制建议注册后在个人中心生成 API Key请求时用 Basic Auth 方式加上。返回的是一个 zip 包里面是该 Series 下所有 DICOM 文件。API 方式最大的好处是可脚本化、可重跑而且能把“哪些患者、哪些系列、下载时间”这些信息全部记录成日志对论文复现非常有帮助。我的习惯是先把 getSeries 返回的完整 JSON 存到本地后面无论做数据统计还是排查缺失都能直接对着这个 JSON 查。3.3 进阶玩法直接访问官方托管存储TCIA 也在公有云对象存储上放置了数据文件路径结构可以按 DICOM 的层级去拼接。对习惯 S3 风格工具的工程师来说这种方式更适合大批量拉取速度往往比 Data Retriever 更快还可以用 aria2 这类多线程下载工具做加速。具体做法是先从 manifest 或 API 拿到某个文件的相对路径再拼上托管存储的根地址构造出可直链下载的 URL。需要说明的是TCIA 对直接拉取托管存储的路径规则并没有像 API 那样有完整的 OpenAPI 文档所以这个方法更适合有一定经验、愿意自己排查路径的读者。如果你只是想快速拿到一份可用的数据不建议从这条路入手。官方 Data Retriever 和 API 已经覆盖了绝大多数场景直接拉存储适合后面做大规模镜像或者需要断点续传优化的时候再考虑。三种方案的对比可以看这个表方案适合场景上手难度可复现性NBIA Data Retriever第一次尝试、小规模下载低一般TCIA REST API批量下载、数据流水线中高公开托管存储直连高级用户、大并发拉取高中4. 下载完先体检目录结构、DICOM 核对与 RTSTRUCT 坐标转换4.1 学会看每个患者的目录树搞清 Series 划分逻辑下载完成后第一件事不是急着跑模型而是把目录结构搞明白。TCIA 下载下来的目录通常是按“患者 → 检查 → Series → 文件”层层嵌套的大致长这样Lung-PET-CT-Dx/ └── LUNG1-001/ └── 1.2.276.0.7230010.3.1.2.1234/ ├── 1.2.276.0.7230010.3.1.3.1001/ # CT Series │ ├── 1.2.276.0.7230010.3.1.4.2001.dcm │ ├── ... ├── 1.2.276.0.7230010.3.1.3.1002/ # PET Series │ ├── ... └── 1.2.276.0.7230010.3.1.3.1003/ # RTSTRUCT └── 1.2.276.0.7230010.3.1.4.3001.dcm最底层的每一层 ID 都是 DICOM 里定义的唯一标识符。CT 和 PET 通常各自是一个或多个 SeriesRTSTRUCT 可能是单独一个 Series。有的患者也可能有不止一次检查所以同一个 PatientID 下可能有多个 StudyInstanceUID每个 Study 下又挂多组 Series。看到这种目录千万别晕核心要记住一件事目标检测需要的是“同一空间坐标系下的影像 标注”。CT 和 PET 因为是同机 PET/CT 采集坐标体系基本对齐但它们的 Series 是独立的必须通过 StudyInstanceUID 来绑定。4.2 用 pydicom 快速核对 CT、PET 与 RTSTRUCT目录结构只是表象真正要想确认每个 Series 是什么得打开 DICOM 文件看元数据。我一般习惯用 pydicom 写个几十行的扫描脚本一次性把所有文件过一遍import pydicom from pathlib import Path for dcm_path in Path(... ).rglob(*.dcm): ds pydicom.dcmread(dcm_path, stop_before_pixelsTrue) print( ds.PatientID, ds.Modality, ds.SeriesDescription, ds.SeriesInstanceUID, )输出里几个字段要重点关注DICOM 字段含义用途ModalityCT / PT / RTSTRUCT / PET 等区分图像类型SeriesDescription采集序列的文字描述人工确认更直观SeriesInstanceUIDSeries 的唯一 ID建立 CT/PET/RTSTRUCT 对应关系ImagePositionPatient图像在病人坐标系中的位置坐标转换必需特别是 ImagePositionPatientIPP它是算世界坐标到像素坐标转换的关键输入。后面把 RTSTRUCT 轮廓转成目标框时必须用到它。体检时如果发现某个患者只有 CT 没有 PET或者只有影像没有 RTSTRUCT要记下来。这种情况在公开数据集里并不少见早期筛选病例时就应该把这些缺模态的样本单独标记。4.3 从勾画轮廓到边界框坐标系换算的完整处理接下来是整篇实操里最需要静下心看的一步把 RTSTRUCT 里的轮廓点转成图像上的边界框。RTSTRUCT 里的 ContourData 存的是病人坐标系下的三维坐标点单位是毫米。比如rt pydicom.dcmread(RTSTRUCT.dcm) for roi in rt.ROIContourSequence: for contour in roi.ContourSequence: data contour.ContourData # [x1, y1, z1, x2, y2, z2, ...]这些坐标是世界坐标不是像素坐标。要转成像素坐标必须知道对应 CT/PET 图像是怎么摆放的。对于轴位图像DICOM 的坐标系关系可以这样理解每张切片的左上角有一个世界坐标 IPP像素沿 X 方向的间距和方向由 IOPImage Orientation Patient和 PixelSpacing 决定。已知一个世界坐标点 P想得到它在某张切片上的像素坐标 (u, v)可以用这个简化方式import numpy as np def world_to_pixel_2d(point, ipp, iop, spacing): # point: 世界坐标 [x, y, z] # ipp: 切片左上角世界坐标 # iop: 6 个方向余弦前3个是行方向后3个是列方向 # spacing: [行间距, 列间距] position np.array(ipp, dtypefloat) u_vec np.array(iop[:3], dtypefloat) * spacing[0] v_vec np.array(iop[3:], dtypefloat) * spacing[1] d np.array(point, dtypefloat) - position u np.dot(d, u_vec) / np.dot(u_vec, u_vec) v np.dot(d, v_vec) / np.dot(v_vec, v_vec) return int(round(u)), int(round(v))这只是单张切片的换算。实际处理 3D volume 时还需要找到轮廓 z 坐标对应的切片索引方法是用所有切片的 IPP 的 z 值做最近邻匹配。拿到每个轮廓点的像素坐标后取最小最大 x、y 就能得到边界框。这里有个很重要的前提RTSTRUCT 中勾画的坐标是基于 CT 图像的坐标系而不是 PET。PET/CT 虽然是同机采集、空间对齐但如果你要做 PET 上的目标检测最好的做法还是把 PET Series 和 CT Series 的空间信息都读出来确认两者的 Position 匹配然后再统一用 CT 坐标去换算。我当时在这里踩过一个坑直接假设所有患者 PET 和 CT 的 IPP 完全一致结果部分患者 PET 的层厚和 CT 不同坐标换算后框偏了。后来把所有 Series 的 spacing 和切片数都打印出来才发现不同模态的采集参数并不一样。所以这段代码一定要写到流程里每次都用参数去算不要做任何假设。5. 构建检测数据集的前置避坑数据划分、体素方向与空标注5.1 训练/验证/测试划分必须以患者为粒度很多人做自然图像目标检测时习惯把图片随机 shuffle 后按 8:1:1 划分。这个习惯在医学影像上会出大问题。同一个患者的 CT、PET、RTSTRUCT 之间高度相关如果同一个患者的切片一部分进了训练集、一部分进了验证集模型相当于提前“见过”了验证集的同源数据指标会虚高但到了真正的独立测试集上性能立刻缩水。解决办法很简单划分前先用 PatientID 聚合把所有患者 ID 排序后按比例划分不留任何一张切片越界。写代码时要注意PatientID 可能是字符串排序方式会影响分布建议用基于随机种子的方式先打乱患者列表再切分。import random patients sorted(metadata[PatientID].unique()) random.seed(42) random.shuffle(patients) n len(patients) train_patients patients[:int(n * 0.8)] val_patients patients[int(n * 0.8):int(n * 0.9)] test_patients patients[int(n * 0.9):]这个顺序最好在数据处理一开始就固定下来之后无论做增广还是调参都用同一个划分文件避免“不同实验用不同测试集”导致的结果不可比。5.2 NIfTI 与 DICOM 的方向差异坐标转换必须先查 affine如果你打算把 DICOM 转成 NIfTI 再用 nnU-Net 或者 3D 检测框架训练这里有一个特别容易出错的点。DICOM 的坐标系统是 LPS左、后、上而 NIfTI 经过 dcm2niix 转换后通常是 RAS右、前、上方向。这会导致同一个世界坐标在转换前后的体素索引完全不同。如果直接从 RTSTRUCT 转出的坐标系硬套到 NIfTI 上结果必然是框的位置错位。正确做法是用 NIfTI 自带的 affine 矩阵做坐标变换import nibabel as nib import numpy as np img nib.load(volume.nii.gz) affine img.affine # 世界坐标转体素坐标 world_point np.array([x, y, z, 1.0]) voxel np.linalg.inv(affine) world_point voxel np.round(voxel[:3]).astype(int)有了这一步不管底层是 LPS 还是 RAS都能用统一的数学方式做映射。唯一要注意的是RTSTRUCT 的轮廓点本身是世界坐标这个坐标在 DICOM 体系里是 LPS如果你已经在某个转换工具里把它变成了物理坐标就要先搞清楚这个物理坐标是 LPS 还是 RAS再喂给 affine。这个坑隐蔽在“看起来没问题”里。我建议你在第一次做完转换后用 3D Slicer 或者 ITK-SNAP 加载原始影像和转换后的边界框人工抽查几个病例确认轮廓和框确实贴在病灶上再批量处理。可视化这一步花不了多少时间但能省掉后续训练时无数排查问题的时间。5.3 空标注和尺度失衡比模型参数更值得担心检查完坐标还要统计标注分布。医学影像数据集最常见的坑是“很多图像没有标注”或者“绝大部分目标都很小”。Lung-PET-CT-Dx 的 RTSTRUCT 是医生手工勾画的肿瘤轮廓存在几种情况有的患者肿瘤很小只有几个像素的直径有的患者肿瘤很大或者有多个病灶还有的 Series 虽然有影像但并没有对应的标注文件。如果你想做 2D 切片级别的检测首先要决定“哪些切片算正样本”。一个常见策略是只有包含 ROI 中心的那些切片才生成边界框距离中心太远的切片虽然有病灶边缘但信息量不足可以作为难负样本。统计每个切片的病灶数量之后你会对数据分布有更直观的判断。用一句话概括拿到数据后先花半天时间把数据体检和统计做完强过直接开训练然后对着一个离奇的 mAP 发呆一星期。6. 离 YOLO 训练还差一步标注汇总与数据统计6.1 设计一个统一标注清单喂给常用检测框架把 RTSTRUCT 转成边界框之后接下来就是标准的目标检测数据工程问题了。无论是 YOLOv8 还是其他检测框架基本都需要一个“图像路径 类别 归一化框坐标”的标注文件。我的习惯是先做一个中间表示的 CSVpatient_idseries_uidslice_indeximage_pathclass_idx_miny_minx_maxy_max注意这里的 x_min 等坐标是相对于单张切片的整数像素坐标后续在写训练脚本时再统一归一化到 0–1。CSV 的好处是便于检查如果哪一行的坐标超出图像尺寸一眼就能发现。然后再写一个转换脚本把 CSV 按检测框架需要的格式输出。以 YOLO 为例每张切片对应一个 .txt 文件每一行是class_id cx cy w h其中 cx、cy、w、h 都是归一化后的值。转的时候要小心边界坐标不要超过 [0,1]否则训练时可能会产生 NaN loss。6.2 用基础统计判断这份数据是否值得投入训练数据整理完毕开工训练之前建议先跑一遍基础统计总共有多少个有效病例、多少张带标注切片每个病例平均多少个病灶最大最小面积病灶尺寸分布直方图小目标占比每种模态CT/PET的切片总数和分辨率。这些数字会直接影响训练策略。比如病灶普遍偏小就意味着要重点处理小目标检测问题可能要用更大分辨率的输入、或者专门做切片级增广再比如 PET 和 CT 的强度分布差异大就必须在数据预处理阶段做各自独立的规范化这个我后面专门写一篇具体聊。我做这件事的体感是Lung-PET-CT-Dx 这份数据虽然要折腾的地方不少但信息量足够扎实尤其是医生勾画的轮廓质量比很多自动标注数据高一个档次。用这份数据训练出来的模型至少是“见过真正肿瘤”的模型而不是在自然图像上过拟合出来的玩具。最后分享一个我在整个流程里最受益的小习惯每下载一个患者就在本地维护一个 CSV 记录它的 PatientID、StudyInstanceUID、有哪些 Modality、文件数量、平均体积。后面但凡遇到“训练到一半发现某个患者数据缺失”或者“验证集里混进了训练集同源数据”这种问题这个 CSV 就是你的救命稻草。医学影像数据集从来都是“数据准备 80%模型训练 20%”把前面这 80% 做扎实后面的实验才会真正顺畅。