影像组学这个方向,这几年在科研文章里出现的频率越来越高。说白了,就是从CT、MRI、PET这些医学影像里,用程序批量挖出人眼看不出来的定量特征,再用这些特征去做诊断、分型、预后预测、疗效评估。而整个流程里最基础、也最关键的一步,就是特征提取。这步做不好,后面建什么模型都是空中楼阁。今天这篇,我就把用3D Slicer和Python两条路径提取影像组学特征的完整流程讲清楚,包括环境怎么配、ROI怎么勾、参数怎么设、批量怎么跑,还有那些文档里从来不写的坑。不管是刚入门影像组学的研究生,还是想把手头数据跑通的医生朋友,这篇应该都能直接参考使用。
1. 影像组学流程与工具选型思路
1.1 影像组学到底在做什么
先厘清一个概念。影像组学(Radiomics)的底层逻辑,是把医学图像当作可挖掘的数据源,利用自动化的算法从感兴趣区域(ROI)里提取大量高维定量特征。这些特征大致分四类:一阶统计特征(灰度分布、均值、方差、熵等)、形状特征(体积、表面积、球度、紧凑度等)、纹理特征(灰度共生矩阵、灰度游程矩阵、灰度区域大小矩阵等)、以及经过滤波器变换后的高阶特征(小波特征、拉普拉斯高斯特征等)。
一个常规的影像组学研究流程是:图像采集 → ROI分割 → 特征提取 → 特征筛选 → 建模 → 验证。今天这篇聚焦在“ROI分割”和“特征提取”这两个环节,也就是从原始图像到特征矩阵这一步。很多新手把精力全放在后面的机器学习和深度学习上,结果前端的特征提取链路没打通,或者参数用得不规范,导致最后结果复现不了,审稿人一问就露馅。所以特征提取这一步,值得沉下心来好好打磨。
1.2 为什么推荐3D Slicer和Python
影像组学特征提取的工具其实不少。商业软件有syngo.via Radiomics、IntelliSpace Discovery等,但授权费不低,而且可定制性差。开源方案里,3D Slicer和Python(配合pyradiomics库)是最主流的两个选择,两者各司其职:
3D Slicer适合交互式操作。它是个免费开源的医学图像处理和三维可视化平台,内置了Radiomics模块,也支持安装SlicerRadiomics扩展。你可以直观地看图像、勾画ROI、调整参数、导出特征表。适合单病例分析、算法验证、小样本研究,也适合不熟悉编程的临床医生快速上手。
Python适合批量化处理和流程化建模。借助pyradiomics库,你可以写脚本一次性跑几十上百例,特征筛选、交叉验证、模型训练这些后续步骤也能无缝衔接。适合多中心队列研究、大样本筛选、以及需要复现和追溯的科研场景。
这两个工具完全可以组合使用:在3D Slicer里完成ROI勾画和可视化验证,导出mask后,再用Python做批量特征提取。这是我在实际项目中用得最顺手的工作流,后面会详细展开。
1.3 特征从哪里来:PyRadiomics是谁
不管是在3D Slicer里点鼠标,还是在Python里写代码,背后真正的特征计算引擎,绝大多数情况下都是同一个开源库——PyRadiomics。这是哈佛医学院等机构维护的一个开源项目,实现了影像组学领域公认的特征定义标准,包括一阶统计(firstorder)、形状(shape)、灰度共生矩阵(glcm)、灰度游程矩阵(glrlm)、灰度大小区域矩阵(glszm)、邻域灰度差分矩阵(ngtdm)、灰度依赖矩阵(gldm)等多个特征类别。
PyRadiomics默认不开启滤波变换时,每类特征加起来大约90多个。如果在参数设置里加上Wavelet(小波)、LoG(拉普拉斯高斯)、Square、SquareRoot、Exponential、Logarithm等图像变换,特征数量立刻膨胀到几千个。这也是影像组学“高维小样本”特点的来源。搞清楚特征是怎么算出来的,你才能懂为什么参数设置会影响最终结果——后面我会反复提到这一点。
2. 用3D Slicer提取影像组学特征的完整流程
2.1 版本选择与Radiomics模块准备
3D Slicer的版本迭代比较快,不同版本模块位置略有差异。我个人的建议是:直接用较新的稳定版(比如5.x系列),新版本对DICOM读取、分割模块和Radiomics的集成都更完善。
在3D Slicer里确认Radiomics模块是否存在,有两个方法。第一个,在模块搜索框里直接输入“Radiomics”,如果存在就能看到。第二个,如果搜索不到,说明需要手动安装扩展。点击菜单栏的“View” → “Extension Manager”,在弹出的扩展管理器里搜索“Radiomics”或“SlicerRadiomics”,找到后点击Install安装,重启Slicer即可。安装扩展时需要联网,网络环境不稳定的话装到一半容易失败,重新点一次安装通常能解决。
有一个小提示:扩展管理器里的版本可能与当前Slicer主版本不完全兼容。如果装完后模块列表里依然找不到,要么换一个扩展版本试试,要么干脆升级或降级Slicer主版本。旧版本Slicer的安装包,在Slicer官网的“Downloads”页面下方可以找到历史版本入口,有时候项目里需要复现旧流程,会有意识地使用旧版本环境。
2.2 数据导入与ROI勾画实操
打开3D Slicer后,先把影像数据导进来。DICOM序列可以用菜单栏的“File” → “Load DICOM”,进入DICOM数据库导入界面;如果已经有nii.gz、nrrd等格式的预处理数据,直接用“Add Data”加载即可。
ROI勾画是整个流程里最讲手感、也是最影响结果质量的环节。在3D Slicer里,常用的是“Segment Editor”模块。步骤是这样的:先选中影像数据,点击“Add”新建一个分割段(Segment),命名成“tumor”之类;然后用工具栏里的画笔(Paint)、阈值(Threshold)、剪刀(Scissors)等工具逐层勾画。阈值工具适合增强CT里边界相对清晰的病灶,可以先用阈值圈出大体范围,再用画笔或橡皮擦精修边缘。对于边界模糊的病灶,只能老实手工逐层勾画,这是最费时间的环节,但也是决定特征质量的核心环节。
勾画完成后,一定要检查三个要素:第一,分割段是否覆盖了你需要的所有层面;第二,是否误把周围正常组织、血管、骨骼勾了进来;第三,不同层面的勾画是否连续。这些细节直接决定特征提取的结果准不准。不要把时间省在这里,一个粗制滥造的mask,提取出的特征就是一堆噪声。
2.3 从分割结果到特征导出的完整步骤
ROI勾画完成之后,下一步是把分割结果转换成mask,并导出为影像组学工具能识别的文件格式。
在“Segmentations”模块中,右键点击分割节点,选择“Export visible segments to binary labelmap”,然后保存为.nii.gz或.nrrd格式。这几步看起来简单,但很多人会踩坑:导出的labelmap必须和原始影像保存在相同的空间坐标系下,两者维度、原点、体素间距、方向必须完全一致。如果不一致,后续特征提取时PyRadiomics会自动重采样或直接报错,结果自然不可信。我的习惯是,勾画完成后先肉眼验证一下,在Slice视图把mask叠加到原始图像上看一秒钟,没问题再导出。
导出mask后,就可以用Radiomics模块提取特征了。以Slicer 5.x内置的Radiomics模块为例,界面中需要设置四个核心部分:
- Input Volume:选择原始影像;
- Input Segmentation:选择刚才的分割节点;
- Output Table:选择输出表格节点(勾选“CSV”可以导出CSV文件);
- 左下角的参数区可以设置特征类别、图像类型、binWidth等参数。
点Apply,几秒到几分钟不等,表格里就会列出所有提取到的特征。特征名以original_、wavelet-LLH_等开头,后面跟特征类别和具体特征名,比如original_firstorder_Mean、wavelet-LLH_glcm_ClusterShade。
关于参数设置,这里要重点说两句。binWidth默认值一般是25,这个值表示直方图分箱宽度。它对纹理特征影响很大:binWidth越大,灰度被压缩的越粗,纹理细节丢失越多;binWidth越小,对噪声越敏感。文献里常见的是16或25,具体用哪个,建议整批数据统一,别中途换了。另外,如果扫描数据来自不同机器、层厚不一,建议在设置里开启重采样,把体素间距统一到比如1×1×1毫米或1×1×3毫米,这样不同病例的纹理特征才具有可比性。
3. 用Python批量提取影像组学特征的代码实战
3.1 Python环境与pyradiomics安装
3D Slicer适合单病例、交互式操作,但如果你要处理几十上百例数据,一个个点Slicer是不现实的。这时候用Python写批量脚本,是唯一靠谱的路径。
先解决环境问题。pyradiomics这个库的安装,是很多新手卡住的第一关。我的建议是:直接用Anaconda创建一个Python 3.8或3.9的虚拟环境。pyradiomics对Python版本比较挑,我在Python 3.11上遇到过一次编译失败的尴尬,后来换到3.9就顺利通过了。如果你对Anaconda不熟,也可以直接装官方Python,但强烈建议用虚拟环境管理,避免多个项目依赖互相污染。
conda create -n radiomics python=3.9 -y conda activate radiomics pip install pyradiomics SimpleITK pandas numpy安装完成后验证一下:
python -c "import radiomics; print(radiomics.__version__)"如果这条命令能正常输出版本号,说明安装成功。如果在这里报错,大多是两个原因:一是网络问题导致安装包下载不完整,换个镜像源重试;二是Windows下缺少Visual C++ Build Tools编译器,需要先安装这个工具链。
另外,很多人的图像数据是DICOM格式的,pyradiomics直接吃DICOM会比较麻烦,建议统一转成.nii.gz或.nrrd格式再处理。转格式可以用3D Slicer导出,也可以用dcm2niix这个工具批量转换。
3.2 单例特征提取:从图像到特征向量
pyradiomics的特征提取接口非常简洁,核心只有几步:加载图像、加载mask、执行提取器。下面是一段可以直接跑的完整示例代码:
import pandas as pd import radiomics.featureextractor as fe image_path = "patient001/image.nii.gz" mask_path = "patient001/mask.nii.gz" extractor = fe.RadiomicsFeatureExtractor() # 默认参数:提取Original和Wavelet两种图像类型,计算所有特征类别 feature_vector = extractor.execute(image_path, mask_path) # feature_vector是OrderedDict,键为特征名,值为特征值 df = pd.DataFrame([feature_vector]) df.to_csv("single_case_features.csv", index=False) print(df.shape)执行之后,你会得到几百到几千个特征,具体数量取决于你的参数设置。默认情况下,PyRadiomics会同时计算Original图像和Wavelet变换后的8个子带(LLL、LLH、LHL、LHH、HLL、HLH、HHL、HHH)上的特征,所以维度很高。第一次跑的时候,建议先拿一例数据试一下,打印特征数量和前几个特征名,确认流程通了再跑全量。
如果你的数据需要更精细的参数控制,可以单独写一个参数文件,比如params.yaml:
imageType: Original: {} Wavelet: {} featureClass: firstorder: [] glcm: [] glrlm: [] glszm: [] shape: [] setting: binWidth: 25 resampledPixelSpacing: [1, 1, 1] interpolator: 'sitkBSpline' normalize: true normalizeScale: 100然后在代码里指定参数文件:
extractor = fe.RadiomicsFeatureExtractor("params.yaml")关于参数文件里的几个配置,我再详细说明一下。resampledPixelSpacing设置成[1,1,1],表示把每个体素重采样成1毫米×1毫米×1毫米的立方体,这对跨机器、跨参数的图像数据特别重要。interpolator是体素插值方式,sitkBSpline平滑性好,适合特征计算。normalize和normalizeScale做灰度标准化,把灰度范围映射到0到100之间,但要注意,对CT图像而言灰度本身有物理意义(亨氏单位HU),是否归一化要谨慎考虑,文献里对CT做不做归一化的都有。这些参数一旦定了,整批数据必须保持一致,这样提取出的特征才有可比性。
3.3 批量提取:几十例影像一次跑完
单例跑通之后,批量就是简单的循环遍历。关键是要把数据文件夹的命名规范统一。我的习惯是:
data/ ├── patient001/ │ ├── image.nii.gz │ └── mask.nii.gz ├── patient002/ │ ├── image.nii.gz │ └── mask.nii.gz └── patient003/ ├── image.nii.gz └── mask.nii.gz每个病人一个文件夹,里面都统一叫image.nii.gz和mask.nii.gz,这样代码里路径拼接就非常省事。批量提取的参考代码如下:
import os import pandas as pd import radiomics.featureextractor as fe extractor = fe.RadiomicsFeatureExtractor("params.yaml") data_root = "./data" results = [] for patient in sorted(os.listdir(data_root)): img_path = os.path.join(data_root, patient, "image.nii.gz") mask_path = os.path.join(data_root, patient, "mask.nii.gz") if not (os.path.exists(img_path) and os.path.exists(mask_path)): print(f"Skip {patient}: missing image or mask") continue try: feature_vector = extractor.execute(img_path, mask_path) feature_vector["patient"] = patient results.append(feature_vector) print(f"Done: {patient}") except Exception as e: print(f"Error: {patient}, {e}") df = pd.DataFrame(results) df.to_csv("radiomics_features_all.csv", index=False) print(f"Total: {df.shape[0]} samples, {df.shape[1]} features")这里有几个细节需要留意。遍历时用sorted()排序,保证输出顺序稳定可复现。个别病例的mask可能和图像坐标系对不上,导致这一例直接报错,所以try/except是必须的,不能让一个坏数据毁了整批任务。输出表里那列“patient”在下游建模时能派上大用场,方便对应回临床表型数据。
跑完批处理之后,强烈建议做个快速完整性检查:数一下行数是否等于预期病例数,特征列数是否等于单例运行时的列数,随机抽一两例和单例运行结果做对比,确保数值一致。
3.4 特征筛选:提完之后干什么
特征矩阵拿到手,不代表就能直接丢进模型里。影像组学特征动辄几百上千,而样本量往往只有几十或一两百,直接建模几乎必然过拟合。提完特征,还要经过标准化(z-score归一化)、相关性去冗余(比如删掉相关系数大于0.8的特征)、稳定性筛选(ICC或重复扫描评估)、以及LASSO或随机森林等嵌入法选择,最终才能得到数量较少、具有独立预测价值的特征子集。
这一步是独立的机器学习建模环节,涉及的技术细节很多,不是今天这篇的重点。但我在实操中深刻体会到,特征提取的参数设置会直接影响后续筛选和建模的结果。比如binWidth不同,提取出的特征矩阵差异巨大,最后筛选出的关键特征可能完全不同。所以提特征这件事,一定要在研究设计阶段就明确参数,全程统一,并记录备查,这是发表可复现研究的基础。
4. 两个路径怎么选:场景驱动而不是跟风
4.1 3D Slicer与Python的功能对比
很多人纠结到底该用3D Slicer还是Python。我直接给一张对比表,看完你应该有自己的判断:
| 对比维度 | 3D Slicer界面操作 | Python脚本批处理 |
|---|---|---|
| 上手门槛 | 低,可视化操作 | 中,需要Python基础 |
| ROI勾画 | 支持,所见即所得 | 一般配合外部勾画工具 |
| 单例特征提取 | 很方便 | 也方便,但多一步写脚本 |
| 批量特征提取 | 麻烦,一例例点 | 核心强项,数十上百例轻松 |
| 参数可定制性 | 中,图形界面受限 | 高,自由调参 |
| 下游建模衔接 | 弱,需要导出特征再建模 | 天然衔接机器学习流程 |
| 可复现性 | 中等,依赖手动操作 | 高,脚本即记录 |
| 适合场景 | 教学演示、小样本探索、ROI快速查看 | 真实队列研究、多中心大样本、发表级分析 |
两者不是替代关系,而是互补关系。我见过不少新手上手就是Python,结果卡在ROI勾画上——因为Python端做医学图像分割标注,界面体验远不如3D Slicer。反过来,也有医生朋友只会在Slicer里勾画和提取,到需要跑一百例数据、做特征筛选和建模时,只能在Excel里手动复制粘贴,效率感人。最理想的工作流,是把两者串起来用。
4.2 我推荐的工作流:Slicer勾画,Python提取
以我个人经验,最顺畅的一套流程是这样的:
第一步,用3D Slicer导入DICOM数据,逐例查看图像质量,排除伪影严重或扫描参数异常的病例。
第二步,用Slicer的Segment Editor勾画ROI。对于MRI或增强CT,先用阈值工具圈住大范围,再逐层精修。勾画标准要在打头几例时就确定好,比如边缘是否纳入、坏死区域是否排除、周围血管是否剔除,并记录在案。同一批数据最好由同一个人勾画,如果要多人协作,建议做一致性检验。
第三步,把勾画好的分割导出为.nii.gz格式的mask。
第四步,用Python脚本批量读取image和mask,调用pyradiomics提取特征,生成特征矩阵。
第五步,在Python里继续做特征标准化、筛选、建模和评估。整个流程无缝衔接,且每一步都有记录,可追溯可复现。
这套流程我从头用到尾,最大的体会是:把ROI勾画和特征提取解耦,各用各最擅长的工具,既保住了可视化质量,又保留了批量化效率。
4.3 关于特征一致性的重要提示
这里必须强调一个很多初学者不知道的坑:3D Slicer里的Radiomics模块和Python里的pyradiomics库,虽然底层是同一个引擎,但如果Slicer内置的pyradiomics版本和Python环境里的版本不同,或者GUI设置里默认参数不同,提取出的特征数值可能出现微小差异。更麻烦的是,不同版本的pyradiomics对某些特征的计算逻辑有过修正,版本差太多时,特征值可能不可直接对比。
所以在做正式研究前,建议用同一例数据分别跑一遍Slicer和Python,对比提取结果,确认关键特征数值是否一致,确认你的参数设置是否真的生效。这一步花不了几分钟,但能在后面省下大量“为什么我的特征和某某文献对不上”的苦恼。
5. 常见问题与排查技巧实录
5.1 提取出的特征为空值或异常偏小
最常见的原因有两个。第一,mask和图像的空间位置不一致,比如mask的维度、原点或方向与图像不同,导致ROI落在图像有效范围外,特征自然算不出来。这种情况下,把mask叠加到图像上肉眼检查一下就能发现问题。第二个原因是分割段的内容不合法,比如没有导出成二值化labelmap,而是保留了软标签或部分体积值,导致PyRadiomics在计算时自动跳过。检查方式很简单,用SimpleITK读入mask,查看其取值集合,应当只有0和1两个值。
5.2 安装pyradiomics时编译失败
这个我在前面提过,Windows下最常见的错误是提示“Microsoft Visual C++ 14.0 or greater is required”。解决思路有两种。一是安装Visual C++ Build Tools,装完再重试pip install pyradiomics。二是用Anaconda安装预编译版本:conda install -c conda-forge pyradiomics,避免现场编译。在Linux或macOS下,如果编译失败,多数情况是缺少构建工具,Ubuntu下可以先执行sudo apt-get install build-essential。
还有一类问题是在Python 3.10以上版本pip install时找不到匹配的wheel包。这种情况的硬核解法就是创建Python 3.8或3.9的虚拟环境,别和高版本Python死磕。说实话,很多科研库对最新Python版本的支持本来就滞后,挑一个稳定版本环境是保平安的最优解。
5.3 批处理时某些病例报错导致整个脚本中断
我在批量跑几百例数据时,经常遇到个别数据有问题:dicom序列对不上的、mask导出错误的、文件名带中文路径的。如果在循环体里不加异常处理,一个坏文件就能让整个脚本崩溃,前面跑完的数据也白跑。所以批处理脚本里务必加try/except,并记录失败的病例名和原因,全部跑完后单独处理这些坏数据。另外,文件路径和文件名最好全部用英文,中文路径在Windows下有时候会触发编码问题,非常隐蔽且难以排查。
5.4 特征数量与文献报道不一致
很多人在写文章时会发现,自己提取的wavelet特征数量跟别人发的文章不太一样。这背后可能是三个原因。第一,PyRadiomics的版本不同,旧版本可能不包含某些特征。第二,参数设置不同,比如开启或关闭了某些图像变换类型、特征类别。第三,某些特征的启用条件是数据本身满足特定要求,如果ROI内体素数量太少或灰度值范围异常,个别特征会被跳过。
排查方法也很简单:在Python里打印extractor.enabledImagetypes和extractor.enabledFeatures,确认哪些图像类型和特征类别被启用了;再对比官方文档中特征总数和你提取到的特征数,就知道是哪里被砍掉了。
5.5 勾画的ROI在提取时出现“内存不够”或长时间不动
如果mask层面特别多,或者图像分辨率特别高,特征提取时计算量会非常大。LoG滤波尤其费时间,wavelet变换次之。我的建议是:先跑一例,记录耗时,评估全量任务的时间预算;如果每例要跑十几分钟,考虑只用Original和Wavelet,或者去掉LoG,或者降低重采样分辨率(比如用[2,2,2]代替[1,1,1])。对大多数任务来说,这个取舍对模型性能的影响远没有想象中大,却能省下大把排队时间。
5.6 可重复性验证:同一例数据每次提取结果是否一致
影像组学最怕被人质疑“结果不可复现”。为了自证,我在跑完一批数据后,通常会用同一例数据重复提取两次,确认特征数值完全一致。pyradiomics在参数不变、输入一致的情况下,结果是确定性的,不会引入随机性。所以如果发现两次结果不一致,那问题一定出在输入数据上,比如图像或mask被其他程序修改过,或者文件路径读取到了不同版本的数据。这种异常要高度重视,因为它意味着整批数据的可靠性都值得怀疑。
6. 写在最后的实操体会
啰嗦了这么多,最后分享几点个人在实操中沉淀下来的体会。
第一,影像组学的天花板不在算法,而在数据质量。ROI勾画的准确性与一致性,直接决定了特征提取结果的质量。勾画标准模糊、勾画人不统一、层厚参差不齐,后面再怎么调参都是事倍功半。我见过不少论文被审稿人质疑,问题都不在模型花不花哨,而在最基础的ROI分割和数据预处理环节立不住。
第二,参数设置要在动手之前想清楚,并且全程统一。binWidth、重采样体素间距、filter类型,这些一旦定了就不要中途修改。建议把参数存成文件,随脚本一起归档保存,方便日后溯源。
第三,多中心研究时,建议加上特征鲁棒性分析环节。同一个病人,在不同机器或不同扫描参数下,提取出的特征数值可能差异很大。常见做法是使用重复扫描数据计算组内相关系数(ICC),剔除稳定性的特征;或者使用ComBat等方法对特征进行中心效应校正。把这些放到流程里,文章的说服力会强不少。
最后再送一个小技巧。如果你想快速验证整个链路的正确性,可以在Slicer里用同一例图像勾出两个略有差异的ROI,分别提取特征,比较结果。你会发现某些特征对ROI边界极敏感,某些则很稳定。这个现象能帮你理解为什么ROI勾画要精益求精,也能帮助你在建模前特征筛选中,优先保留那些鲁棒性更好的特征。
影像组学这个方向,入门门槛不高,但做扎实不容易。找到适合自己的工具组合,把基础流程理清楚,把细节做到位,你就已经跑赢了大多数人。