简介:面向医学图像处理与科研分析人员,这套Matlab脚本集合系统解决了DCM(DICOM)文件转NII格式的常见需求,并可通过Matlab原生save等途径扩展至NPZ、MAT等格式,适合需要将医学影像从采集端转入后续分析流程的研究者使用。包体共9个文件,含8个.m脚本和1个txt说明文件,压缩包仅20KB,轻量但功能明确。核心包括make_nii、save_nii、load_nii、save_nii_hdr、load_nii_hdr等NIfTI系列读写函数,以及Get3DData立体数据提取脚本;txt文件给出了函数调用方式与转换示例。资源已有3174人学习下载,热度可观。通过这套脚本,用户无需从零实现DICOM解析和NIfTI格式转换,可结合dicomread直接读取DCM数据后快速保存为NII;同时脚本注释清晰、结构模块化,便于二次修改与嵌入脑影像预处理、多模态配准等科研流程,有效提升医学影像数据整理与转换效率。 做医学影像处理、神经影像分析或者医学AI的同行,肯定都遇到过这种场景:设备科或者数据平台拷回来的文件夹里躺着一堆DCM文件,文件名还是“IM-0001-0001”这种看一眼就头疼的编号,而你要做的后续分析要么要在SPM、FSL里跑,要么要喂给Python的深度学习框架,还有可能只是想在MATLAB里先把数据读出来做个ROI预览。这时候,文件格式就成了第一道横在面前的坎。
今天这篇就只聊一个问题:在MATLAB里怎么把DCM文件完整地读进来,然后干净利落地存成NII、NPZ或者MAT三种格式。文章里会给出可以直接跑的代码、关键参数的取舍逻辑,以及我实际踩过的坑。适合的人群很明确:手里已经有一批DICOM序列、正准备做预处理或者喂模型的人,不管你是刚接触医学影像的研究生,还是常年跟影像数据打交道的工程师,这篇都能帮你把转换这一步走稳。
1. 为什么要在MATLAB里做这种转换
1.1 三种格式分别解决什么问题
DCM,也就是DICOM,是医学影像设备的标准输出格式。它的特点是“数据+元数据”捆绑在一起,一个文件里既有像素矩阵,也有患者ID、采集日期、扫描序列、层厚、像素间距这些信息。这种设计对临床存档来说很完善,但对科研分析来说有点拖沓,因为大多数分析工具并不会直接消费DICOM的整套元数据。
NII是神经影像领域的事实标准,SPM、FSL、AFNI这些工具都认它,fMRI、VBM、DTI这类分析流程基本都要求先把DICOM转成NIfTI。NPZ是NumPy的压缩存储格式,Python生态里读取最方便,深度学习框架做数据加载时几乎都是这个路子。MAT则是MATLAB原生格式,如果后续所有处理都在MATLAB里完成,用它是效率和稳定性最高的选择。
三种格式的适用场景可以简单归纳成一句话:想进传统神经影像分析流程,选NII;想进Python数据管线或深度学习流程,选NPZ;只想留在MATLAB环境内继续处理,选MAT。
| 目标格式 | 适用场景 | 主要优势 | 主要限制 |
|---|---|---|---|
| NII | SPM/FSL/AFNI分析 | 行业标准、信息完整 | 需要NIfTI工具包 |
| NPZ | Python数据管线、深度学习 | 读取快、可压缩 | MATLAB侧不能直接“原生写” |
| MAT | MATLAB内继续分析 | 零依赖、保存快 | 其他语言读取不如前两种友好 |
1.2 转换前先想清楚数据流向
很多人上来就写代码转换,结果转完发现数据进不了下一步流程。我自己的习惯是,动工之前先问三个问题:
第一,DICOM序列是单帧还是多帧?有些设备导出的增强DICOM一个文件里就包含几十上百帧,处理逻辑跟一帧一文件完全不同。第二,后续分析是否需要方向信息?如果只是把体素数值喂给网络,你只需要保证数组顺序一致;但如果要做空间配准或者和标准模板对齐,那DICOM头的ImageOrientationPatient和ImagePositionPatient必须被正确带到NIfTI头里。第三,像素值是原始采集值还是需要做线性变换?CT图像的灰度值如果不应用RescaleSlope和RescaleIntercept,转出来的数值范围会完全不对。
这三点想清楚之后再做选择,就不会出现“转出来的数据在工具里位置是歪的”或者“深度模型训练出来的结果莫名其妙”这种问题。
2. 准备阶段:工具箱和第三方依赖
2.1 MATLAB端需要哪些基础组件
读取DICOM最核心的功能来自Image Processing Toolbox,里面的dicominfo和dicomread是整套方案的基石。如果你的MATLAB装的是教育版或者试用版,建议先确认一下这个工具箱是否可用,简单的方式是执行:
ver('images') license('test', 'image_toolbox')第一条命令如果输出了Image Processing Toolbox的版本信息,说明工具箱已经装了;第二条命令返回1表示当前许可证允许使用。这两个检查都不花时间,但在别人的机器上跑脚本时能省去很多Debug时间。
除了官方工具箱,还需要一个能读写NIfTI的第三方函数包。目前广泛使用的是由Jimmy Shen维护的NIfTI工具包,在MATLAB File Exchange里搜索“Tools for NIfTI and ANALYZE image”就能找到。下载后解压到任意目录,然后执行:
addpath(genpath('你的解压路径/NIfTI_20140122')); savepath;addpath只对当前会话生效,savepath会写进启动配置,下次打开MATLAB不用重新添加。
2.2 另一个思路:dcm2niix要不要用
除了纯MATLAB方案,还有一个被广泛使用的工具叫dcm2niix,它本身是一个命令行程序,在很多预处理环境里都是标配。它在MATLAB里也能被调用,核心代码就是一行system命令。如果你的数据最终目标是NIfTI,而且你非常在意方向矩阵是否正确,我的建议是优先用dcm2niix,而不是自己手拼NIfTI头。原因很简单,DICOM病人的扫描方向千变万化,手动处理很容易在坐标变换上出错,而dcm2niix把这一层逻辑封装得很成熟。
纯MATLAB方案的场景在于:你不想引入外部可执行文件,或者你需要在转换之前对数据做大量自定义预处理,比如按InstanceNumber重新排序、做偏置场校正、截取特定时间帧等。所以更准确的说法是,dcm2niix适合“只管转”,MATLAB手写方案适合“边转边处理”。
3. 核心实操:DCM序列的读取与预处理
3.1 先读元数据,再读像素,顺序不能反
很多新手会直接对文件执行dicomread,这样虽然能把图像像素读出来,但很多后续必要的信息没有拿到。正确的习惯是先用dicominfo解析出元数据,再用dicomread读像素,这样你还能直接用元数据对象作为读取函数的输入,效率更高。
一个DICOM文件里比较关键的字段包括:
Rows和Columns:图像的行列数,决定矩阵尺寸。PixelSpacing:像素间距,通常是一个[行方向间距, 列方向间距]的二元素向量,单位是毫米。SliceThickness:层厚,用于决定体素在Z方向上的尺寸。InstanceNumber:层面位置序号,批量读取时排序必须靠它。RescaleSlope和RescaleIntercept:把存储值转换为真实物理值的线性系数,公式很简单:真实值 = 存储值 × RescaleSlope + RescaleIntercept。ImageOrientationPatient和ImagePositionPatient:前者描述图像坐标系相对于病人坐标系的旋转,后者记录图像原点的世界坐标,这两个字段是空间配准的关键。
3.2 批量读取多帧序列并重建三维体
实际的DICOM序列通常是一个文件夹下几百个单帧文件,每个文件一层。下面这段代码是我常用的读取函数,它的作用是把一个文件夹下的所有DICOM读取成一个三维矩阵,同时完成Rescale变换:
function vol = load_dcm_volume(dcmDir) files = dir(fullfile(dcmDir, '*.dcm')); if isempty(files) files = dir(fullfile(dcmDir, '*.DCM')); end % 先读元数据,拿InstanceNumber排序 infoList = cell(length(files), 1); instNum = zeros(length(files), 1); for i = 1:length(files) infoList{i} = dicominfo(fullfile(files(i).folder, files(i).name)); instNum(i) = infoList{i}.InstanceNumber; end [~, idx] = sort(instNum); files = files(idx); infoList = infoList(idx); first = infoList{1}; rows = first.Rows; cols = first.Columns; nSlices = length(files); vol = zeros(rows, cols, nSlices, 'single'); for i = 1:nSlices info = infoList{i}; slice = dicomread(info); if isfield(info, 'RescaleSlope') slope = info.RescaleSlope; else slope = 1; end if isfield(info, 'RescaleIntercept') intercept = info.RescaleIntercept; else intercept = 0; end vol(:, :, i) = single(slice) * slope + intercept; end end这里我刻意把输出矩阵的类型设成了single。医学影像数据用单精度做存储基本够用,而且能比double省一半内存,当你处理一个200×200×200的体数据时,这个差距会直接体现在会不会内存溢出上。
InstanceNumber这个字段在绝大多数临床序列里都存在,但偶尔会遇到某些设备导出的数据里这一个字段全是0,这时候我的处理办法是从文件名里提取序号。常见文件名比如IM-0001-0001.dcm,用正则表达式即可:
tokens = regexp(files(i).name, '(\d+)', 'tokens'); serialNum = str2double(tokens{end}{1});3.3 方向信息一忽略,后面全白做
继续往下走之前,必须说一下方向这个容易被忽略的大坑。DICOM里的图像数据在文件里是按“行优先”存储的,但它描述的解剖位置并不是简简单单的横断面、矢状面或冠状面可以概括,尤其是三维采集序列,图像的坐标轴相对于病人坐标系可能是任意旋转的。
如果你只是把数据保存成NII、NPZ或者MAT拿去给模型训练,不涉及坐标配准和标准空间,那体素数组本身的顺序保持一致就够用了,方向信息可以暂时不处理。但如果你要把NII丢进SPM做标准化或者和模板比较,那方向头不对,结果就是你的脑区坐标全部错乱,而且这种错误往往特别隐蔽,不会报错,只是结果图怪怪的。
两种处理方式:最省心的是走dcm2niix,它会把方向矩阵完整写入NIfTI头;如果坚持纯MATLAB路线,我至少会把ImageOrientationPatient和ImagePositionPatient存进后续的元数据变量里,让这些信息不丢失。输出NII时如果用的是NIfTI工具箱,可以用类似下面的方式把方向信息带进去:
nii.hdr.hist.qform_code = 1; nii.hdr.hist.sform_code = 1;这两个字段的完整填充已经超出今天这篇的范畴,但知道它们存在,能帮你在排查“为什么图像是歪的”时多一个思路。
4. 三种输出格式的转换与保存
4.1 保存为NII:NIfTI工具箱方案
假设你已经按照前面的代码拿到了三维矩阵vol,也提取了体素尺寸信息:
pixelSpacing = first.PixelSpacing; % [行方向, 列方向],毫米单位 dx = pixelSpacing(1); dy = pixelSpacing(2); if isfield(first, 'SliceThickness') dz = first.SliceThickness; else dz = 1; % 没有层厚字段时给个默认值,尽量避免尺寸信息为空 end然后用NIfTI工具包生成并保存文件:
nii = make_nii(vol, [dx dy dz], [0 0 0], 'float32'); save_nii(nii, 'output_volume.nii');make_nii里的四个参数分别是数据矩阵、体素尺寸、原点坐标和数据类型。float32的类型足够保存经过Rescale变换后的数据精度。如果你的数据本身是整数型的原始CT值,也可以考虑用int16,但如果你后续还有各种数学运算,float32会更省心,省得中途类型转换出错。
这里插一句,如果你最终的目标序列是四维数据,比如fMRI或者DTI带了时间帧或梯度方向,make_nii其实也支持直接传入四维矩阵,但你自己要保证维度顺序是[行, 列, 层, 时间帧]。顺序乱了,后续做时间层校正的时候画面会非常美丽。
4.2 保存为NPZ:MATLAB和Python之间怎么搭桥
NPZ在MATLAB里没有现成的写入函数,最稳妥、也是我最常用的做法是:先存成MAT临时文件,再用Python脚本转成NPZ。不要嫌麻烦,这个流程的可靠性远比在MATLAB里强行调numpy要高,特别是在涉及三维以上数据和元数据组合的时候。
MATLAB侧保存临时文件:
meta = struct(); try meta.PixelSpacing = first.PixelSpacing; meta.SliceThickness = first.SliceThickness; meta.ImageOrientationPatient = first.ImageOrientationPatient; meta.ImagePositionPatient = first.ImagePositionPatient; catch meta.Info = 'Metadata fields missing.'; end save('temp_for_npz.mat', 'vol', 'dx', 'dy', 'dz', 'meta', '-v7');Python侧负责真正的NPZ输出:
import scipy.io as sio import numpy as np data = sio.loadmat('temp_for_npz.mat') vol = data['vol'] dx = data['dx'].item() dy = data['dy'].item() dz = data['dz'].item() meta = data['meta'] np.savez_compressed( 'output_volume.npz', vol=vol, pixel_spacing=(dx, dy, dz), meta=meta )选择存成临时MAT而不是直接搬数据,是因为MAT是MATLAB的原生格式,写入速度很快,而且scipy.io读取MAT文件非常成熟。有一点要特别注意,-v7格式的MAT文件Python读取没问题,如果数据量巨大,内存又紧张,保存时可以考虑-v7.3,scipy在高版本里对v7.3的支持也已经跟上了。
如果你实在不想中间落盘,MATLAB也提供了Python接口,可以直接调用numpy函数。但这要求你的MATLAB安装了Python支持,而且配置好了Python环境。实测下来,大数组通过MATLAB的py.numpy接口传递时,内存开销会翻倍,所以我个人只在数组小的时候才用它。
4.3 保存为MAT:最省事的方案也有讲究
MAT格式最大的优势是零依赖,save一行命令就能搞定。但它也有讲究,核心注意两点:一是变量名要有语义,二是大数组要加-v7.3。
建议的保存方式:
save('output_volume.mat', 'vol', 'dx', 'dy', 'dz', 'meta', '-v7.3');我的习惯是把读取到的关键元数据全部塞进一个结构体里保存,这样后面任何一个人拿到这个MAT文件,不用去翻原始DICOM也能知道数据的空间分辨率、方向信息、像素间距这些关键参数。这对科研复现尤其重要,因为一份没有元数据的影像数组,很多时候等于废数据。
关于-v7.3,MAT格式在v7.0及以前版本对单个变量超过2GB的支持很差,而医学影像体数据动辄几百MB甚至上GB,加上四维数据之后轻松越过这个门槛。用v7.3虽然文件体积会略大一些,写入速度也稍慢,但稳定性优先,我认为完全值得。
5. 常见问题与排查技巧实录
5.1 文件排序乱了导致层序错乱
这是最常踩的坑。dir函数返回的文件列表是按字母序排的,所以IM-0001-0010.dcm会排在IM-0001-0002.dcm前面。如果直接用这个顺序堆叠层,你的三维体就是乱的。处理办法就是我前面代码里展示的:读元数据,拿InstanceNumber排序,或者从文件名提取序号排序。实测里,文件名提取方案对某些国产设备尤其重要,因为它们的InstanceNumber字段填写并不可靠。
5.2 灰度值不对,CT值全变了
如果你处理的是CT数据,在应用RescaleSlope和RescaleIntercept之前,像素值通常是设备里存储的整数,经过变换后才能得到以HU为单位的真实CT值。很多人读出来发现图像整体偏暗或者亮度范围对不上,十有八九是漏了这一步。处理逻辑很简单,但对后续窗宽窗位计算和分割任务来说,影响是决定性的。
5.3 多帧Enhanced DICOM读出来是四维
现在很多高端设备导出的DICOM并不是一帧一个文件,而是一个文件包含几十帧甚至几百帧,这种情况dicomread读出来直接就是一个四维数组。如果你按单帧文件的方式去循环处理,代码就会出问题。排查方式是在读取之前先检查元数据里是否有NumberOfFrames字段,有的话就按多帧逻辑处理,并在堆叠时指定正确的维度顺序。
5.4 大数组导致内存不足
三维体数据在读取过程中会经历多次复制:元数据读入、像素读入、Rescale变换、类型转换,每一步都可能产生一份临时副本。处理大序列时,建议提前检查系统内存,同时尽量用single而不是double,避免不必要的类型提升。转NPZ时如果遇到内存瓶颈,优先走“先存MAT再转NPZ”的路线,不要硬在MATLAB里用Python接口,实测这个姿势的内存占用最友好。
| 常见症状 | 可能原因 | 解决方案 |
|---|---|---|
| 层序颠倒或乱序 | dir按文件名排序 | 按InstanceNumber或文件名数字重排 |
| CT值范围异常 | 未做Rescale变换 | 应用RescaleSlope和RescaleIntercept |
| 读取结果是四维 | 多帧Enhanced DICOM | 检查NumberOfFrames并按多帧处理 |
| 内存不足 | 用double且变量多次复制 | 改用single、必要时用-v7.3分块保存 |
这个需求我隔一段时间就会做一次,每次都能在细节上发现新的问题。最想告诉你的一点是,格式转换本身并不难,难的是转换过程中别把关键信息弄丢。方向、像素间距、层厚、Rescale参数,这些看起来不起眼的数字,在后续分析里任何一项出错都能让你折腾好几天。所以我的习惯永远是:宁可多存一点元数据,也不要任性精简到只剩一个矩阵。
本文还有配套的精品资源,点击获取