ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MATLAB读取NIfTI医学图像:从load_nii到可视化与重采样

MATLAB读取NIfTI医学图像:从load_nii到可视化与重采样 简介对于从事神经影像与生物医学数据分析的MATLAB用户这是一套专门用来打开、读取和处理NIfTI格式图像的工具包。NIfTI是脑成像研究的通用格式但MATLAB原生支持有限该工具集正好补齐了读取头信息、导入图像矩阵、交互式显示、坐标变换、重切片等常用功能帮助科研人员在MATLAB环境中顺利完成MRI等影像数据的预处理与分析。压缩包共50个文件、4.24MB以43个m源码文件为主体覆盖load_nii、save_nii、view_nii等核心函数的原始代码便于理解与二次开发附带3份PDF帮助文档和2个txt说明对文件格式与使用细节做了补充另有2个nii示例数据用户可立即验证工具效果。资源已有超过700人浏览学习说明其在医学影像处理场景中有一定参考价值。借助这套工具既可快速读懂NIfTI文件结构也能直接调用或修改函数完成批量处理、格式转换与可视化分析是一条高效处理脑成像数据的实用路径。1. 拿到 nii 图像的第一步先别急着 imread医学影像里最常见的格式之一就是 NIfTI扩展名通常是.nii或.nii.gz。很多从公开数据集比如 BraTS、ADNI下载的样本解压后是一堆体积不小的.nii文件于是很自然有人想在 MATLAB 里直接imread(image.nii)然后报错。原因很简单nii 不是图片是一个带有 348 字节头部的多维数组里面除了像素值还有体素大小、图像方向、坐标变换矩阵、时间点数量等信息。用普通图像工具箱去读MATLAB 根本不知道这个文件内部到底是怎么组织的。NIfTI_20140122.zip 是多年前流传很广的一个 NIfTI 工具箱压缩包里面包含load_nii.m、save_nii.m、make_nii.m等函数配合nii_viewer.m可以直接可视化三维体数据。虽然官方对它的维护更新很慢但在处理标准 nii 文件时它依然是最容易上手的方案之一。这篇文章就围绕这个 zip 包展开讲清楚 nii 文件的数据结构、如何在 MATLAB 里正确加载、可视化、做基本处理以及加载过程中最容易踩的坑。适合刚接触医学图像处理的算法工程师、研究生也适合需要把 nii 数据转成矩阵做深度学习的同学——读完你至少能彻底搞明白为什么load_nii读出来的结构里全是.img和.hdr。2. NIfTI 格式到底存了什么hdr 与 img 的对应关系2.1 348 字节头部里最关键的 8 个字段NIfTI-1 标准的头部固定占 348 字节剩下的数据区按类型顺序排列。很多人在 MATLAB 里直接fopen后fread读坏文件就是因为没有先把头部解析出来。理解头部的关键字段后续用load_nii时你才知道返回的结构体里每个字段在说什么。常用字段及含义如下字段名字节偏移常见值作用sizeof_hdr0348校验是否为 NIfTI-1datatype702/4/8/16/64体素数据类型2uint84int168int3216float3264float64bitpix728/16/32/64每个体素占用的比特数dim40[3 256 256 150]维数及各维度大小pixdim76[1 0.5 0.5 0.5]每个维度的物理体素尺寸srow_x280[1 0 0 -128]从体素坐标到世界坐标的行向量qform_code2521/2坐标变换类型scl_slope2001斜率用于实际值 存储值 * slope interload_nii返回的结构体里nii.hdr.dime对应这些头部字段nii.img是真正的体素数据。很多人不理解为什么读出来的矩阵尺寸是[X Y Z]而不是[行 列]——因为 NIfTI 的维度顺序是[x y z t]x 方向是第一个维度y 是第二个z 是第三个。和 MATLAB 里matrix(row, col, slice)的习惯刚好反着显示的时候需要permute或rot90调整。2.2 为什么用 MAT 文件里的矩阵直接保存不行有人会问nii 文件里的img和 MATLAB 的工作区变量不都是一个三维矩阵吗为什么不直接.mat保存区别在于 nii 保留了一整套物理空间信息。比如一个 256×256×150 的 MRI 数据如果只知道体素值你不知道每一层在真实人体里对应哪个位置也无法对齐另一个受试者的数据。srow_x、srow_y、srow_z共同构成一个仿射矩阵能把体素坐标映射到毫米级的脑模板空间。如果你只存矩阵训练模型前做配准时就得重新手工编码坐标信息而 NIfTI 天然把数据与坐标绑定在一起。NIfTI_20140122.zip这种工具箱做的事情本质就是把这 348 字节解析出来再根据datatype和dim用fread读入对应大小的数组。它没有用任何魔法所以当文件头损坏或者dim写错时load_nii会报出非常怪异的错误——接下来章节里我们逐个处理。2.3 压缩过的.nii.gz不是简单解压就能读.nii.gz是 gzip 压缩后的 nii 文件很多官方的数据都使用这种形式。直接在 MATLAB 里用gunzip解压也能拿到.nii但NIfTI_20140122工具箱自带一个load_nii的变体会在读取时临时解压到内存不会在磁盘上留下额外的中间文件。不过需要注意的是这个版本的工具箱内部调用的是gzip命令Windows 下如果没有安装相应工具通常会报系统找不到指定的路径。更稳妥的做法是用 MATLAB 自带的gunzip先解压到临时目录再用load_nii读取。3. MATLAB 里加载 nii 的最小实现从 addpath 到 load_nii3.1 准备工具包把下载的 NIfTI_20140122.zip 变成可执行文件夹假设你手头已经有一个NIfTI_20140122.zip这不是某个私有项目的专属包而是经过广泛传播的公开工具箱。在 MATLAB 中使用它不需要安装只需要解压后添加路径即可unzip(NIfTI_20140122.zip, NIfTI_folder); addpath(genpath(NIfTI_folder));这段代码先解压 zip 到当前工作目录下的NIfTI_folder然后用genpath递归添加所有子目录到 MATLAB 路径。如果不加genpath而工具箱内部又引用了子目录里的view_nii或rri_*辅助函数运行时会报Undefined function。另外解压之后建议先确认目录里是否存在load_nii.m因为有些历史版本会缺少部分文件被精简过的压缩包可能只含一个单文件版功能不全。3.2 读取一个标准 nii 文件的基本命令nii load_nii(subject_T1.nii); img nii.img; hdr nii.hdr;运行之后如果你在命令窗口输入whos img会看到img是一个 uint16 类型的三维数组。为什么是 uint16因为 MRI 数据常用 16 位灰度。保留原始类型是明智的——很多人在这一步直接double(img)然后再做归一化这本身没错但要注意如果原始数据是灰度值而非物理量转换前应该先查看scl_slope字段是否非 1。scl_slope表示存储值需要经过线性变换才能得到真实信号强度if nii.hdr.dime.scl_slope ~ 0 img double(nii.img) * nii.hdr.dime.scl_slope nii.hdr.dime.scl_inter; else img double(nii.img); end3.3 处理.nii.gz压缩格式如果文件名是T1_3D.nii.gzload_nii的直接调用可能因为找不到对应 gzip 程序而失败。常见做法是先用 MATLAB 的gunzip解压到内存或临时目录。下面这段代码可以自动识别并处理inputFile T1_3D.nii.gz; if endsWith(inputFile, .gz) gunzip(inputFile); % 解压到当前目录生成 .nii inputFile erase(inputFile, .gz); end nii load_nii(inputFile);注意gunzip之后的文件名是不带.gz的原名。如果原先的文件名里包含_3D等标识解压后仍保留。若担心覆盖原文件可以先tempdir建一个临时目录再解压处理完删除。3.4 批量读取多个 nii 文件时的路径组织实际操作中经常有几十个.nii文件分散在子目录里推荐用dir配合模式匹配循环读取。下面这个方法比手动拼路径稳健得多files dir(fullfile(data, **, *.nii)); for i 1:length(files) filePath fullfile(files(i).folder, files(i).name); nii load_nii(filePath); img nii.img; % 这里把每个文件的体素尺寸等记录到表里 enddir(data, **, *.nii)是 MATLAB R2016b 之后才支持递归搜索的写法老版本需要自己用genpath构造目录列表。另一个容易忽略的点是如果数据是 4D nii例如功能磁共振 fMR维度是[x y z t]nii.img的大小是[x y z t]后续当你要把时间序列提取出来做频谱分析或 FFT 时通常要squeeze(permute(img, [4 1 2 3]))才能得到[t x y z]的顺序方便按时间切片。4. nii 图像的可视化与常用操作切片、体素坐标与重采样4.1 在 MATLAB 中显示横断面、矢状面和冠状面NIfTI_20140122自带一个图形界面工具view_nii.m或nii_viewer.m不同版本名称略有差异直接调用nii load_nii(T1.nii); view_nii(nii);这个界面里可以拖动十字线定位三维坐标同时切换三个正交面的视图。它内部实现是通过slice函数依次提取第 z 层、x 方向和 y 方向的数据。如果不习惯界面交互自己动手画三个方向的切片也只需要几十行代码。下面是一个经典的横断面绘制img nii.img; sliceIdx round(size(img, 3) / 2); % 取中间层 imgSlice squeeze(img(:, :, sliceIdx)); if size(imgSlice, 1) size(imgSlice, 2) imgSlice permute(imgSlice, [2 1]); % 调整行列方向使其不旋转 end imagesc(imgSlice); axis image; colormap(gray);这里axis image保证体素在屏幕上按 1:1 显示时不会因坐标轴缩放拉伸。但要注意如果pixdim不是 1例如 x 方向体素间隔为 0.5 mmy 方向为 0.8 mm直接axis image会显示为正方形视觉上会失真。正确的显示方式应该按物理比例进行拉伸pixdim nii.hdr.dime.pixdim(2:4); aspect pixdim(2) / pixdim(1); % y / x set(gca, DataAspectRatio, [1 aspect 1]);这个细节很容易被忽略但在论文里贴图时体素非各向同性数据的显示比例错误会直接影响审稿人对病灶形状的判断。4.2 体素坐标与世界坐标的互相转换在医学图像处理中你不会只关心数组下标特别是在做病灶中心点定位、或者与标准脑模板比对时必须知道某个体素位置 (x, y, z) 对应的毫米坐标。NIfTI 头部里srow_x、srow_y、srow_z这三个四维行向量定义了仿射变换% 假设 vxl 是体素坐标 [x; y; z; 1] vxl [55; 120; 80; 1]; worldX nii.hdr.hist.srow_x * vxl; worldY nii.hdr.hist.srow_y * vxl; worldZ nii.hdr.hist.srow_z * vxl;如果要反过来从世界坐标映射回体素坐标则先构建 3×4 仿射矩阵再求逆。NIfTI_20140122没有直接提供这个逆变换函数但可以手动实现。更简单的做法是直接用nii_viewer界面里的鼠标位置读出世界坐标。手写代码时要注意 NIfTI 中世界坐标的 x 方向是从右到左R-Ly 方向是后前P-Az 方向是下上I-S具体方向要看qform或sform的类型不要想当然地当成物理轴。4.3 提取感兴趣区域ROI并生成 mask处理 nii 的另一个高频需求是把某一阈值的区域提取出来或者用已有 mask 裁剪图像。例如分割结果中标签为 1 的区域是病灶需要把它周围的图像裁下来nii load_nii(T1.nii); seg load_nii(seg.nii); mask (seg.img 1); [row, col, sli] ind2sub(size(mask), find(mask)); minRow min(row); maxRow max(row); minCol min(col); maxCol max(col); minSli min(sli); maxSli max(sli); roiImg nii.img(minRow:maxRow, minCol:maxCol, minSli:maxSli);裁剪之后如果你要把roiImg保存回 NIfTI 文件需要单独重建make_nii并且要注意原点的变化否则保存出来的数据在空间上会和原图对不上。NIfTI_20140122的make_nii默认把原点设在体素中心若裁剪后不修改srow用view_nii打开时会看到图像偏到奇怪的位置。推荐的做法是保留原nii.hdr修改dim和srow后通过save_nii写入但这样做容易因头部细节不一致导致报错所以我通常建议在做 ROI 裁剪时直接用矩阵运算仅在最终需要落盘时再构建新 nii。4.4 重采样到同尺寸多模态数据配准的第一步深度学习预处理中常常要把 T1、T2、FLAIR 三种模态的 nii 统一到同一尺寸。最简单粗暴的方法是imresize对三维矩阵逐层缩放但这样会破坏体素坐标信息。更稳妥的算法是用interp3结合仿射坐标进行物理空间重采样。这里给出一个适用于 NIfTI 工具箱的最小实现思路function resliced reslice_nii_general(nii, newDim, method) ox nii.hdr.dime.pixdim(2); oy nii.hdr.dime.pixdim(3); oz nii.hdr.dime.pixdim(4); [x, y, z] ndgrid(0:newDim(1)-1, 0:newDim(2)-1, 0:newDim(3)-1); newX x * (ox * size(nii.img,1)) / newDim(1); newY y * (oy * size(nii.img,2)) / newDim(2); newZ z * (oz * size(nii.img,3)) / newDim(3); resliced interp3(double(nii.img), newX, newY, newZ, method); end这段代码按物理尺寸等比例缩放而不是简单均匀插值。如果原始图像的pixdim不是各向同性这个重采样方式会保留空间比例关系。method可以传入nearest或linear对于标注 mask 用nearest对于灰度图用linear或cubic。要注意interp3的坐标是三元网格且坐标范围必须落在原始数据范围内否则out of range会返回 NaN导致后续网络训练报错。5. NIfTI 加载中的 4 个高频报错与检查顺序5.1Size of header (348) not correct说明文件头不对出现这个报错多数情况下并不是文件损坏而是你把 ANALYZE 格式用 NIfTI 加载器去读了。ANALYZE 7.5 的头文件也是 348 字节但某些字段的偏移和 NIfTI 不同导致sizeof_hdr被读错。遇到这种情况先检查扩展名是不是.hdr/.img成对出现的老式写法。处理办法是打开 RAW 流直接读头部的前 40 字节看魔数如果第 344-347 字节不是n、、1、\0那大概率是 ANALYZE 格式。可以修改加载逻辑用load_untouch_nii.m工具箱里通常附带尝试兼容。5.2Error using fopen或Permission denied常见于路径与缓存问题Windows 下如果文件正被其他程序如 ITK-SNAP、MRIcron占用fopen会拿不到句柄导致工具箱内部报fopen失败。检查对象不只是那个.nii文件还有你解压后的.hdr因为 NIfTI 有时会以分离头文件的方式存在。另外如果 MATLAB 当前目录是中文路径或者带空格工具箱内部某些老代码里可能有硬编码路径问题。建议统一把数据放在纯英文、无空格的目录下。这里有一个排查顺序先跑一遍最小实验fid fopen(T1.nii, r); if fid -1 disp(文件无法打开); else fclose(fid); end nii load_nii(T1.nii); % 再跑一次如果 fopen 成功但 load_nii 失败则问题出在解析逻辑上继续看下一步。5.3 读出来全 0 或矩阵尺寸不符检查dim与datatype有朋友共享的 nii 文件在 MATLAB 里读出来img全是 0但在其他软件里显示正常。最常见的原因是文件中数据真实类型是 float32但datatype字段被误设成 uint16导致每个体素被错误解释为 2 个字节。遇到这种问题先用以下命令查看实际 datatypenii.hdr.dime.datatype nii.hdr.dime.bitpix如果看到bitpix是 32 但datatype是 4int16可以手动强制转换后重新保存。另一个可能是文件里存在缩放斜率原数据存储值在 int16 范围内真实值是img * slope inter而你直接用原始img做可视化由于斜率是 0.003 之类的小数所有值都被 imshow 当作接近 0 的暗色显示。解决办法是先按缩放系数计算见 3.2 节。5.4 4D 数据读出来维度是 5 层还是 4 层fMRI 的 nii 文件维度是[x y z t]但有时dim(1)表示维数dim(2)到dim(5)才是各维度大小。工具箱中nii.hdr.dime.dim的第一个元素是 4表示 4D后面四个数依次为 x、y、z、t。当你squeeze(nii.img)后发现变成了二维矩阵不要惊讶——因为 z 方向可能只有 1 层。检查维度大小是否和dim一致必要时补上reshape不要直接依赖squeeze来调整顺序。5.5 验证加载是否正确的黄金法则确定文件读取无误的最快方式是把原始 nii 和保存后的 nii 做一个像素级差分nii1 load_nii(T1_original.nii); nii2 load_nii(T1_retest.nii); diffImg double(nii1.img) - double(nii2.img); fprintf(Max diff: %g, Num non-zero: %d\n, max(abs(diffImg(:))), nnz(diffImg));如果max diff为 0说明加载和保存过程没有数据损坏。如果有差值优先怀疑浮点类型与整型的转换误差再检查scl_slope有没有被重新写入。通常处理 nii 数据的管线里这步验证值得加进去尤其是数据流水线里有多个人在改代码时。毕竟 nii 文件本身是标准格式但每一版工具的解析细节仍有些历史遗留差异用基线文件做回归检查比肉眼扫切片靠谱得多。本文还有配套的精品资源点击获取
返回列表