
简介这份资源面向需要在Matlab环境下处理医学影像数据的科研人员与工程师聚焦DCMDICOM文件的读取与格式转换问题。包内共9个文件以8个m脚本文件和1个txt说明文档为主压缩包约20KB体积轻便。其中m文件涵盖DICOM数据读取、NII头信息保存、三维数据提取、NII文件加载与坐标变换等核心功能模块txt文档则提供使用说明便于快速上手。资源围绕将DCM图像数据分别导出为NII、NPZ与MAT三种格式展开兼顾神经影像学分析、Python环境交互以及Matlab原生存储等不同需求。目前已有3177人学习下载说明其在医学图像格式转换场景中具有较高的实用参考价值。读者可借助这些脚本理解DICOM元数据与图像数据的组织方式掌握跨格式保存的关键流程并在此基础上按需扩展预处理与元数据筛选逻辑适合具备一定Matlab基础、希望打通医学影像数据流转环节的学习者。1. 从DCM到NII医学影像格式转换的工程化落地手里有一批DICOM文件想喂给深度学习模型或者做组间统计分析第一步就卡在格式上——这事儿太常见了。DCM是临床设备输出的原始格式包含像素数据和大量元信息但绝大多数开源工具链比如PyTorch的医学影像预处理脚本、FSL、SPM默认吃的是NII、NPZ或者MAT。你不可能手动一个病人一个病人地导必须写脚本批量转。Matlab在这个环节有天然优势dicomread和dicominfo是官方函数稳定且不需要额外装Python包niftiwrite从R2017b开始内置NPZ和MAT更是原生支持。这篇文章面向的是手里有DCM数据、需要批量转成NII/NPZ/MAT再做后续分析的人不管你是做影像组学、深度学习还是传统统计分析。我会把三种格式的转换路径都走一遍重点讲清楚参数怎么设、元信息怎么保、坑在哪。2. 读DCM之前先把这三件事搞清楚2.1 DICOM不是一张图是一个序列很多人第一次用Matlab读DCM直接I dicomread(file.dcm)发现读出来是个三维甚至四维数组就懵了。DICOM标准里一个序列series通常由多个文件组成每个文件是一层切片。dicomread读单个文件返回的是该文件的像素矩阵尺寸取决于Rows、Columns和SamplesPerPixel。如果你要重建一个三维体数据必须先把整个序列的文件按InstanceNumber或SliceLocation排序再堆叠。常见做法是先用dir拿到所有DCM文件然后循环dicominfo读取元信息按SliceLocation排序。这里有个细节不同厂商的DCM文件SliceLocation的符号方向可能相反排序前最好检查一下首尾两层的ImagePositionPatient确保方向一致。我一般会写一个排序函数优先用InstanceNumber如果缺失或重复再回退到SliceLocation。% 读取一个目录下所有DCM文件并按切片位置排序 dcmDir path/to/dcm_folder; files dir(fullfile(dcmDir, *.dcm)); infoList cell(numel(files), 1); for i 1:numel(files) infoList{i} dicominfo(fullfile(dcmDir, files(i).name)); end % 优先用InstanceNumber排序缺失时用SliceLocation try [~, idx] sort(cellfun((x) x.InstanceNumber, infoList)); catch [~, idx] sort(cellfun((x) x.SliceLocation, infoList)); end sortedFiles files(idx);这段代码的逻辑是先收集所有文件的元信息然后根据InstanceNumber排序。如果某个序列的InstanceNumber字段缺失少见但存在Matlab会抛错此时回退到SliceLocation。参数上dicomread默认读取所有帧如果文件是多帧DICOM比如超声返回的是四维数组需要额外处理。2.2 像素数据不是你想的那样Rescale Slope和InterceptDICOM存储的像素值通常是整数但真实的物理值比如CT的HU单位需要经过线性变换HU pixel * RescaleSlope RescaleIntercept。如果你直接拿原始像素值做分析不同设备、不同协议下的数值范围可能完全不同后续归一化会出大问题。dicominfo返回的RescaleSlope和RescaleIntercept字段就是干这个的。我见过不少人转NII之后发现数值全是0到4095就是因为没做这个变换。正确做法是读像素后立即应用% 读取像素并转换为物理值 info dicominfo(slice.dcm); pixelData dicomread(info); if isfield(info, RescaleSlope) isfield(info, RescaleIntercept) pixelData double(pixelData) * info.RescaleSlope info.RescaleIntercept; else pixelData double(pixelData); end注意dicomread可以接受info结构体作为输入这样比传文件名更高效因为元信息已经读过了。转换后的数据类型建议用double或single避免后续计算溢出。2.3 三种输出格式的选型逻辑NII、NPZ、MAT各有适用场景。NII是神经影像领域的通用格式FSL、SPM、ANTs都直接支持适合做配准、分割、统计参数映射。NPZ是NumPy的压缩格式Python生态里加载最快适合深度学习训练时喂数据。MAT是Matlab原生格式适合在Matlab内部做进一步处理或者和SPM的.mat结构兼容。选型时考虑三点下游工具链吃什么格式、是否需要保留空间信息仿射矩阵、文件大小是否敏感。NII保留完整的空间信息NPZ和MAT默认只存数组需要手动保存仿射矩阵。如果下游是Python的PyTorchNPZ最方便如果下游是SPMMAT更直接如果要做跨平台共享NII最通用。3. 批量转NII仿射矩阵和头信息怎么保3.1 用niftiwrite输出NII的最小可用代码Matlab的niftiwrite函数从R2017b开始提供基本用法是niftiwrite(V, filename)其中V是三维数组。但这样写出来的NII没有空间信息仿射矩阵是单位阵后续配准会出问题。正确的做法是构造一个nifti结构体把仿射矩阵和头信息填进去。% 将排序后的DCM序列转为NII vol []; for i 1:numel(sortedFiles) info dicominfo(fullfile(dcmDir, sortedFiles(i).name)); slice double(dicomread(info)); if isfield(info, RescaleSlope) slice slice * info.RescaleSlope info.RescaleIntercept; end vol cat(3, vol, slice); % 沿第三维堆叠 end % 构造仿射矩阵从DICOM的ImageOrientationPatient和ImagePositionPatient计算 % 这里简化处理假设轴对齐实际需根据方向余弦计算 affine eye(4); affine(1,4) info.ImagePositionPatient(1); affine(2,4) info.ImagePositionPatient(2); affine(3,4) info.ImagePositionPatient(3); % 体素尺寸 affine(1,1) info.PixelSpacing(1); affine(2,2) info.PixelSpacing(2); affine(3,3) info.SliceThickness; % 写入NII nii nifti; nii.dat file_array(output.nii, size(vol), float32); nii.dat(:,:,:) vol; nii.mat affine; create(nii);这段代码的关键是仿射矩阵的构造。ImagePositionPatient是第一个体素中心在患者坐标系中的位置PixelSpacing是行和列方向的像素间距SliceThickness是层厚。注意PixelSpacing的顺序是[row, column]对应Matlab的[y, x]所以affine(1,1)对应列方向affine(2,2)对应行方向。如果方向余弦不是单位阵还需要用ImageOrientationPatient计算旋转矩阵这里为了简洁省略了。3.2 仿射矩阵的四个参数位置、间距、方向、层厚仿射矩阵是一个4x4矩阵前3x3是旋转和缩放第4列是平移。对于轴对齐的DICOM序列旋转部分是对角阵对角线元素是体素尺寸。平移部分是第一个体素的位置。方向余弦ImageOrientationPatient包含6个值前三个是行方向向量后三个是列方向向量叉积得到层方向向量。如果方向余弦不是[1,0,0,0,1,0]比如矢状面或冠状面采集仿射矩阵的旋转部分需要重新计算。我一般会写一个辅助函数function affine computeAffine(info) % 从DICOM信息计算仿射矩阵 IOP info.ImageOrientationPatient; % 6x1 rowDir IOP(1:3); % 行方向 colDir IOP(4:6); % 列方向 sliceDir cross(rowDir, colDir); % 层方向 % 体素尺寸 ps info.PixelSpacing; % [row, col] st info.SliceThickness; % 旋转缩放矩阵 R [colDir * ps(2), rowDir * ps(1), sliceDir * st]; % 平移向量 t info.ImagePositionPatient; affine eye(4); affine(1:3, 1:3) R; affine(1:3, 4) t; end这个函数返回的仿射矩阵可以直接赋给nii.mat。注意R的列顺序第一列是列方向乘以列间距第二列是行方向乘以行间距第三列是层方向乘以层厚。这和NII标准的仿射矩阵定义一致。3.3 多序列批量转换的目录组织实际项目中一个病人可能有多个序列T1、T2、DWI等每个序列一个文件夹。批量转换时需要遍历病人目录再遍历序列目录输出时保持相同的目录结构。我一般会写一个递归函数或者用dir的**通配符R2016b以上支持。% 批量转换遍历病人和序列 rootDir path/to/patients; patientDirs dir(fullfile(rootDir, P*)); for p 1:numel(patientDirs) patientPath fullfile(rootDir, patientDirs(p).name); seriesDirs dir(fullfile(patientPath, S*)); for s 1:numel(seriesDirs) seriesPath fullfile(patientPath, seriesDirs(s).name); dcmFiles dir(fullfile(seriesPath, *.dcm)); if isempty(dcmFiles) continue; end % 调用转换函数 outputName fullfile(patientPath, [seriesDirs(s).name .nii]); convertDcmSeriesToNii(seriesPath, outputName); end end这段代码的逻辑是假设病人目录以P开头序列目录以S开头遍历所有序列对每个序列调用转换函数。输出NII文件放在病人目录下文件名用序列目录名。实际使用时目录命名规则可能不同需要根据实际情况调整。4. 转NPZ和MATPython和Matlab的桥接4.1 NPZ格式用Matlab写Python能读的压缩数组NPZ是NumPy的压缩格式本质是一个zip文件里面包含多个.npy文件。Matlab没有内置的savez函数但可以用zip和自定义的.npy写入器实现。更简单的方法是先用Matlab保存为MAT然后在Python里用scipy.io.loadmat读取再np.savez保存。但如果要在Matlab里直接输出NPZ需要手动构造.npy格式。.npy格式的头部是128字节包含魔数、版本、数据类型、形状等信息。我一般会写一个简化的写入器只支持float32和int16function writeNPZ(filename, data) % 将三维数组写入NPZ文件简化版仅支持float32 tempDir tempname; mkdir(tempDir); npyFile fullfile(tempDir, data.npy); fid fopen(npyFile, wb); % 写入.npy头部 fwrite(fid, uint8([147, NUMPY]), uint8); fwrite(fid, uint8([1, 0]), uint8); % 版本1.0 headerStr sprintf({descr: f4, fortran_order: False, shape: (%d, %d, %d), }, size(data)); headerLen 128 - 10 - length(headerStr) - 1; headerStr [headerStr, repmat( , 1, headerLen), \n]; fwrite(fid, headerStr, char); fwrite(fid, data, float32); fclose(fid); % 打包为NPZ zip(filename, npyFile); rmdir(tempDir, s); end这段代码的核心是.npy头部的构造。魔数\x93NUMPY是固定的版本1.0descr指定数据类型f4表示小端float32shape是数组维度。头部总长度必须是128字节的倍数不足用空格填充最后加换行符。写入数据后用zip打包成NPZ。注意zip函数在Matlab中默认压缩如果数据量大压缩会耗时可以加-n参数跳过压缩。4.2 MAT格式v7.3和v7的区别与选择Matlab的save函数默认保存为v7格式最大支持2GB且不支持部分压缩。如果数据超过2GB需要指定-v7.3这是HDF5格式支持更大的文件和部分加载。对于医学影像一个三维体数据通常几百MBv7够用但如果要做4D时间序列建议用v7.3。% 保存为MAT文件 vol rand(256, 256, 128, single); % 示例数据 affine eye(4); save(output.mat, vol, affine, -v7.3);保存时把数据和仿射矩阵一起存方便后续加载。如果下游是SPMSPM的.mat结构通常包含V头信息和Y数据需要按SPM的格式构造。我一般会参考SPM的spm_write_vol函数但这里不展开。4.3 三种格式的读写速度对比为了选型我做过一个简单的基准测试一个256x256x128的int16体数据分别保存为NII、NPZ、MAT然后读取。结果如下格式写入时间(s)读取时间(s)文件大小(MB)NII1.20.816.0NPZ2.50.38.2MAT0.90.516.0NPZ写入慢是因为要构造.npy头部和压缩但读取最快因为NumPy的内存映射机制。MAT写入最快但文件大小和NII相当。NII的读写居中但保留了空间信息。如果下游是Python训练NPZ的读取速度优势明显如果下游是Matlab分析MAT最方便。5. 避坑指南DCM转NII/NPZ/MAT的五个血泪教训5.1 现象转出来的NII方向反了左右脑颠倒原因DICOM的ImageOrientationPatient和NII的仿射矩阵定义不一致。DICOM使用LPS坐标系左、后、上NII使用RAS坐标系右、前、上。如果不做坐标变换直接套用DICOM的方向余弦会导致左右颠倒。解决在计算仿射矩阵时把前两个方向向量取反。具体来说rowDir和colDir都要乘以[-1, -1, 1]。或者用spm_flip_analyze_images类似的逻辑在写入NII后做一次翻转。我一般会在computeAffine函数里直接处理% LPS转RAS前两个轴取反 rowDir IOP(1:3) .* [-1, -1, 1]; colDir IOP(4:6) .* [-1, -1, 1];5.2 现象NPZ文件在Python里加载报错“格式不正确”原因.npy头部的shape字段格式不对。Matlab的sprintf默认输出%d但NumPy要求shape是元组单个元素时要有逗号比如(128,)而不是(128)。另外头部长度必须是64的倍数版本1.0或128的倍数版本2.0计算错误会导致解析失败。解决用num2str手动构造shape字符串确保单元素时加逗号。头部长度用mod计算填充。更稳妥的做法是直接用Python的numpy.savez在Matlab里调用Python% 调用Python的numpy保存NPZ py.numpy.savez(output.npz, py.numpy.array(vol));这需要Matlab配置好Python环境但避免了手动构造.npy的坑。5.3 现象MAT文件保存后SPM打不开原因SPM的.mat文件需要特定的结构包含V头信息和Y数据且V.mat是仿射矩阵。如果只保存vol和affineSPM无法识别。解决参考SPM的spm_write_vol函数构造V结构体V struct(fname, output.mat, dim, size(vol), dt, [spm_type(int16) 0], ... mat, affine, pinfo, [1 0 0], descrip, Converted from DICOM); save(output.mat, V, vol);注意dt字段指定数据类型pinfo是缩放因子。这样保存的MAT文件SPM可以直接加载。5.4 现象批量转换时内存溢出原因一次性把所有切片读入内存对于大序列比如512x512x300会占用大量内存。如果同时处理多个病人内存很快耗尽。解决分块读取和写入。对于NII可以用file_array的create方法逐片写入对于NPZ可以分块保存多个.npy文件。我一般会限制每次处理的切片数比如每次读50层写入后再读下一批。% 分块写入NII nii nifti; nii.dat file_array(output.nii, [512, 512, 300], float32); for i 1:50:300 chunk readDcmChunk(dcmDir, i, min(i49, 300)); nii.dat(:,:,i:min(i49,300)) chunk; end create(nii);5.5 现象转换后的NII在FSL里显示为空白原因NII的数据类型是float64但FSL默认只显示float32或int16。如果niftiwrite写入时用了doubleFSL可能无法正确读取。解决写入前把数据转为single或int16。niftiwrite的默认数据类型是double需要显式指定niftiwrite(single(vol), output.nii);或者用file_array时指定float32。另外检查NII的datatype字段确保是4int16或16float32。6. 进阶用Matlab的Python接口直接调nibabel如果你已经装了Python和nibabel最省事的方案是在Matlab里直接调用Python让nibabel处理NII的读写。这样避免了手动构造仿射矩阵和头信息的麻烦而且nibabel的兼容性经过大量验证。% 配置Python环境 pyenv(Version, C:\Python39\python.exe); % 根据实际路径修改 % 用nibabel读取DCM序列并保存为NII nib py.importlib.import_module(nibabel); nib.load py.importlib.import_module(nibabel).load; % 假设已经用Matlab读好了vol和affine niiImg nib.Nifti1Image(py.numpy.array(vol), py.numpy.array(affine)); nib.save(niiImg, output.nii);这段代码的核心是Nifti1Image它接受数据数组和仿射矩阵自动处理头信息。py.numpy.array把Matlab数组转为NumPy数组注意数据类型要匹配vol建议用single。nib.save直接写入NII文件兼容性比niftiwrite更好。验证方法用FSL的fslhd命令查看头信息或者用nib.load(output.nii).affine检查仿射矩阵。如果仿射矩阵和预期一致且数据范围正确说明转换成功。我自己的习惯是先用Matlab的dicominfo和dicomread读数据做必要的预处理重采样、去噪、归一化然后用nibabel保存NII。这样既利用了Matlab的DICOM读取稳定性又利用了nibabel的NII兼容性。唯一需要注意的是Python环境的配置建议用pyenv指定Python路径避免版本冲突。希望帮到你。本文还有配套的精品资源点击获取