简介:密歇根大学开发的 Michigan Image Reconstruction Toolbox(MIRT)Matlab 版本,是一套面向医学成像与图像重建研究的开源算法库,适合从事 CT、MRI、PET 数据重建的研究人员、工程师及相关专业学生使用。资源包共包含 1569 个文件,大小仅 2.07MB,其中以 1187 个 M 文件为核心,涵盖滤波反投影、ART、SART、EM 等经典与迭代重建算法,另有 C/C++ 头文件与源码用于加速计算,以及文档、示例数据和测试脚本辅助上手。MIRT 还提供数据模拟模块,可模拟不同成像系统的探测器响应与噪声特性,便于在无物理设备条件下验证和优化算法。文件目录按功能组织,主代码、文档、示例与测试脚本分层清晰,并附带大量成像参数与元素数据文件,方便二次开发。目前已有 219 人学习下载,适合希望在 Matlab 环境中系统开展图像重建实验、算法对比与科研验证的读者。
1. 为什么做了几年 CT 重建,最后还是回头用 MIRT
做医学 CT、PET、SPECT 重建的同学,大概率都有过类似经历:直接调iradon跑滤波反投影,速度快得惊人,但投影角度稍微稀疏一点,或者视野里有金属高密度体,图像立刻被星芒伪影淹没。把重建从“一行算子变换”升级成“目标函数 + 迭代优化”,才是能落地的解法。Michigan Image Reconstruction Toolbox(MIRT)的 Matlab 版,就是这方向上一套很完整的工具库:它把图像重建、配准、正则化、系统矩阵建模统一到一套迭代框架里。适合医学图像重建研究、毕业设计,以及需要拿不同算法做对比实验的工程师。这篇按我自己的落地习惯,从安装拆到参数调优。
2. 安装与目录结构:让 Matlab 版工具箱真正变成可用状态
2.1 先搞懂 MIRT 的目录结构,再谈安装
MIRT 不是根目录下一个大脚本,而是按成像任务划分的一组 Matlab 目录。常见做法是下载后解压得到一个mirt文件夹,里面按功能拆成若干子目录:处理正则化项的reg相关目录、处理 MRI 重建的mri相关目录、处理 CT/PET 的ct相关目录,还有存放通用迭代算法的iter目录。老版本的代码大量使用 struct 和独立函数,新版引入了类似对象封装的前向/逆向算子,风格偏命令式,不是纯 OOP 设计。这与很多人设想的“现代软件工程架构”差距很大,但好处是每个函数都是独立文件,断点调试非常直观。
装这个工具箱最核心的动作不是解压,而是把整棵目录树递归加入 Matlab 搜索路径。我一般这样处理:
mirt_root = '/your/local/path/mirt'; addpath(genpath(mirt_root)); savepath;genpath会递归收集所有子目录,保证reg、ct、iter里的函数能被直接调用。savepath的作用是把这个路径设置固化到 Matlab 的默认路径配置里,否则新版 Matlab 重启后路径会丢失,所有函数都报“未定义”。如果savepath提示没有写权限,通常是因为系统盘上的matlabrc文件不可写,这时用userpath指到自己的用户目录,再执行一次savepath就能落盘。
路径加好之后,下一步是验证函数没有冲突。我每次装完都会在命令行跑一遍:
which im -all which ct -allMIRT 里有一个自己的显示函数im,这个名字在 Matlab 生态里非常通用,很容易和某个第三方工具函数重名。which im -all会列出所有同名函数的搜索命中顺序,如果命中的第一条不是 MIRT 目录里的文件,那么在调用前要加绝对路径前缀,或者调整 Matlab 搜索路径的优先级。
2.2 跑通第一个最小 Demo,确认核心算子可用
路径没问题之后,不要急着加载自己的数据。先跑 MIRT 自带的最简例程,确认前向投影、反投影和迭代循环三个环节能串联起来。老版本工具箱里一般有demo_ct、demo_mri这类短脚本,作用不是展示完整解决方案,而是让你走通最小链路:已知真值 → 生成模拟投影 → 迭代重建 → 对比结果。
如果没有现成 demo,或者自带 demo 和你需要解决的问题离得太远,就用 Matlab 自带的合成模型先做冒烟测试:
xtrue = phantom(128); % Shepp-Logan 头模型作为真值 angles = 0:1:179; % 角度范围 sino = radon(xtrue, angles); % 平行束投影,等价于 CT 正弦图 % 用最简梯度下降验证算子闭环 A = @(x) radon(x, angles); At = @(y) iradon(y, angles, 'linear', 'none'); x = double(iradon(sino, angles, 'linear', 'none')); for it = 1:50 grad = At(A(x) - sino); x = x - 0.05 * grad; end imshow(x, []); title('Simple Iterative Reconstruction');这里A是正算子,At是反算子。At(A(x) - sino)计算的是残差反投影,也就是目标函数梯度的方向。步长0.05是我随手给的固定步长,实际工程里不会用固定步长。这个脚本的目的是验证“正投影 → 计算残差 → 反投影回图像域”这条链路在机器上能跑通。如果这一步出来的图像全是雪花点或者直接 NaN,说明路径、数据类型或内存哪儿没对,这时候调后面的参数没有意义。
2.3 版本兼容性:老代码在新版 Matlab 下的表现
MIRT 代码是从学术项目里积累出来的,很多函数写于十几年前,对较新的 Matlab 版本存在兼容性问题。我这边常用的版本是 R2021a 到 R2023b,R2018a 也试过,基本能跑。新版主要会遇到maxNumCompThreads这类函数被标记将要移除的警告,以及字符串写法、strread等老函数被替换。遇到警告不要慌,看命令窗口里的具体提示。被替代的函数通常在新版里还有兼容别名,只是输出额外警告。真出现红色报错,最常见的解决路径是找到调用处,把老函数改成新版推荐函数。
3. 用 MIRT 重建仿真头模:目标函数、迭代优化与效果观察
3.1 为什么不直接调滤波反投影:把重建当成优化问题
滤波反投影的问题是它把噪声当信号处理。投影数据里的泊松噪声会被 ramp 滤波放大,导致高噪声环境下重建图像方差极大。迭代重建为什么能压住噪声,核心在于:重建结果不是解析公式算出来的,而是某个目标函数的最小值点。目标函数里除了“投影数据和重建图像之间的误差”,还可以塞进一个正则项R(x),用来约束图像的空间平滑性或者边缘保持。MIRT 能流行这么多年,不是因为它的反投影算法比 Matlab 自带函数快,而是因为它把“目标函数 + 优化器 + 系统矩阵”这三个模块拆开,你可以自由组合。
典型的统计迭代重建目标函数写作:
psi(x) = 0.5 * || A x - y ||_W^2 + beta * R(x)
这里的A是系统矩阵,y是投影数据,W是统计权重矩阵。W通常取投影数据的倒数,反应每个探测通道的噪声方差。beta是正则化强度,控制数据拟合项和先验项之间的平衡。这个形式不是我定义的,是 PET 和 CT 迭代重建领域多年来的标准写法,MIRT 里所有算法基本都围绕这个函数展开。
3.2 写一个能跑的迭代重建最小脚本
在 MIRT 里真正做重建时,系统矩阵往往不用radon/iradon,而是用专门的对象来描述具体成像几何,比如平行束、扇束、锥束。为了演示迭代逻辑,下面用匿名函数代替系统矩阵,核心思想完全一致:
% 真值:128x128 头模型 xtrue = phantom(128); angles = 0:0.5:179.5; % 模拟投影数据,得到正弦图 sino = radon(xtrue, angles); % 构造前向/逆向算子 A = @(x) radon(x, angles); At = @(y) iradon(y, angles, 'linear', 'none'); % 初始解用滤波反投影,迭代从该点出发 x = double(iradon(sino, angles, 'linear', 'none')); % 加入简单 Tikhonov 正则:gradient 作为平滑惩罚 beta = 0.01; step = 0.1; for it = 1:100 data_grad = At(A(x) - sino); reg_grad = beta * 4 * del2(x); % 离散拉普拉斯近似 x = x - step * (data_grad + reg_grad); end figure; subplot(131); imshow(xtrue, []); title('Ground Truth'); subplot(132); imshow(x, []); title('Reconstruction'); subplot(133); imshow(abs(x - xtrue), []); title('Abs Error');这段代码逻辑不复杂:data_grad是数据拟合项对图像的梯度,reg_grad是正则项梯度,del2是 Matlab 自带的离散拉普拉斯算子。beta控制平滑强度,step是梯度下降步长。直接跑这段,你会看到重建结果比单纯 50 步梯度下降要更平滑,但头模型的边缘也有一定模糊。这里有几个值得关注的参数:beta太小,噪声压不住;beta太大,边缘被抹圆;step太大,迭代发散,图像直接变成噪声花;step太小,走到 100 步时还没到稳定解。实际调法不是拍脑袋,是跑 20 次不同 beta 看输出图,我后面会讲具体怎么量化选择。
3.3 观察迭代收敛:不只看最终图
很多人跑完迭代重建只看最后一张图,这个习惯在工程里很危险。迭代算法可能在第 30 步看起来很好,到第 100 步反而变差,这是因为正则化项和数据项在互相拉扯,优化过程其实在往目标函数更低的方向走,但物理上可能过拟合噪声。我一般会在迭代过程中把目标函数值和重建图像一起存下来:
hist_cost = zeros(1, 100); x = double(iradon(sino, angles, 'linear', 'none')); for it = 1:100 data_grad = At(A(x) - sino); reg_grad = beta * 4 * del2(x); x = x - step * (data_grad + reg_grad); cost = 0.5 * sum((A(x) - sino).^2) + beta * sum(sum(del2(x).^2)); hist_cost(it) = cost; end semilogy(hist_cost); xlabel('Iteration'); ylabel('Cost');semilogy的好处是指数级收敛时能直观看出下降速率。曲线如果前 10 步快速下降然后进入平台,说明参数基本合理。如果曲线后半段上升,说明步长太大或 beta 太小,数值发散的前奏。每次调参都保留这条收敛曲线,比盯着重建图猜原因靠谱得多。
4. 三个真正影响重建质量的参数:正则化强度、迭代次数、优化器选择
4.1 正则化强度 beta:从噪声到糊成一团的分界线
beta 是迭代重建里最容易翻车的参数。beta 过小的表现是重建图像噪声纹明显,图像像颗粒感很强的照片;beta 过大的表现是边缘变软,小结构直接消失,图像“油润”得像水彩画。比较坑的是,不同数据规模下 beta 的可接受区间差异巨大,64x64 头模和 512x512 临床数据不能共用一套值。
我一般用交叉验证式的做法:固定投影数据,beta 取一个对数序列,比如[0.0001, 0.001, 0.01, 0.1, 1],每次重建后计算重建结果和真值的均方根误差。给一组参考曲线,这样选 beta 才有依据。这里给个粗参照:128x128 头模、投影角度 180 个、噪声水平中等,beta 落在0.001 ~ 0.05区间比较常见。如果换成高质量低噪声数据,beta 可以降到0.0001量级。真正的临床数据需要你自己扫曲线。
4.2 迭代次数:看收敛曲线定,不是越多越好
迭代次数的选择不存在“默认 100 次”这种金标准。不同优化器收敛速度差好几倍。普通梯度下降在 128x128 图像上经常要几百次才稳定;而使用预条件或顺序子集类算法,十几轮就能达到可用的视觉效果。
判断原则很简单:看收敛曲线是否进入平台期。在hist_cost跑出来之后,计算最近 20 轮的成本下降幅度,如果相对变化小于 1%,再加迭代次数已经没有意义。另一个实用手段是把迭代过程存成 AVI 或 GIF,观察每一帧的图像变化。如果最后 20 帧肉眼分不出区别,就说明已经收敛。工程上我一般会多留 20% 的迭代次数作为冗余,宁可多算一点,也不要拿到一张欠收敛的图。
4.3 优化器选择:梯度下降、SQS、OS-SQS 怎么选
MIRT 涉及到的迭代优化器很多,但新手只需掌握三个层次。第一层是普通梯度下降,实现简单,步长难调,收敛慢,只适合做教学演示。第二层是 SQS,全称是 Separable Quadratic Surrogates,本质上构造一个逐元素可分离的二次代理函数来近似原目标函数,从而把高维耦合优化拆成逐像素更新,收敛速度显著优于朴素梯度下降。第三层是 OS-SQS,在 SQS 基础上把投影数据分成多个子集,每轮只用一个子集计算梯度,十几轮就能出图。
这三个层次的核心差异可以用一张表说清楚:
| 优化器 | 收敛速度 | 每轮计算量 | 步长敏感度 | 适合场景 |
|---|---|---|---|---|
| 朴素梯度下降 | 慢 | 低 | 高 | 教学演示、逻辑验证 |
| SQS | 中 | 中 | 低 | 中等规模重建 |
| OS-SQS | 快 | 低 | 低 | 临床规模 CT/PET 重建 |
选择逻辑是:如果只有 128x128 的仿真数据,SQS 足够;如果投影数据是 1024x1024 级别的临床数据,OS-SQS 是底线配置。我一般先在普通梯度下降上把 beta 调好,再切换到 SQS 或 OS-SQS 做正式重建,因为调参时梯度的行为更容易预判,正式跑的时候用加速算法省时间。
5. MIRT 常见问题排查:路径冲突、内存不足与 GPU 加速失败实录
这一章的内容是我在多次重建实验里踩过的真实坑,每一条都按现象、原因、解决的逻辑写。
坑一:MIRT 的im函数遮蔽了其他工具箱的同名函数。现象是调用某个显示函数后图像窗口不刷新,或者报错说输入参数类型不对。原因是 MIRT 自带一个名为im的工具函数,功能和imagesc类似,但只接受特定类型的输入,如果你搜索路径里它排在别的同名函数前面,就会接管你的调用。解决方法是运行which im -all,把 MIRT 目录的优先级移动到合适的位置,或者调用时写绝对路径形式mirt.im(...),绕开同名歧义。这个问题在新手机器上出现频率极高,建议装完就查一遍。
坑二:重建结果整幅图是 NaN 或者全黑。现象就是迭代几轮后图像矩阵全部变成 NaN,延续的imshow直接显示白板。原因通常是投影数据里有 NaN 或 Inf,比如正弦图数据包含 0 值,权重矩阵 W 直接取倒数变成 Inf,梯度计算时乘出 NaN。另外系统矩阵A的尺寸和投影数据y的尺寸必须严格匹配,如果角度数量变了而正弦图没有重新生成,就会有维度上的隐式错误。解决方法是进入迭代前先检查数据:any(isnan(sino(:)))和any(isinf(sino(:)))。如果发现 0 值占多数,考虑给投影数据加一个极小量偏移,比如sino(sino <= 0) = 1e-6。注意这不是权宜之计,是实践里绕不开的数据清洗步骤。
坑三:内存瞬间被吃光,系统直接卡死。现象是构造系统矩阵时内存占用一路飙升,Matlab 还没开始迭代就报“内存不足”。原因在于部分 MIRT 接口在构建fatrix类系统矩阵时会尝试预计算并存储所有投影系数,对 512x512 图像加上 1000 个角度的平行束投影,这个系数矩阵的存储需求非常夸张。解决方法是优先使用流式系统模型,不要强制把系统矩阵显式化为普通稠密矩阵;如果工具箱版本支持隐式存储,它会只保存几何参数,每次正向投影时即时计算射线路径。另一个可行措施是降低数据规模做初步调试,先用 128x128 跑通流程,最后才切换到大矩阵做正式实验。
坑四:GPU 加速版本报错,提示没有合适的 mex 文件。现象是切换 GPU 模式后运行命令,Matlab 报“未找到已编译的 CUDA 文件”或直接抛出mexcuda错误。原因是 MIRT 的 GPU 模块需要你本机有 C 编译器和 CUDA 工具包,并且要在安装后手动编译 mex 才可能运行。解决方法是先确认mex -setup选择了正确的 C 编译器,再检查nvcc -V能输出 CUDA 版本,然后进到 MIRT 对应的 cuda 目录执行编译脚本。如果编译失败,最实际的选择是先用 CPU 版完成实验验证,GPU 作为后续加速项而不是依赖项。GPU 并不是必须的,很多教学项目里 CPU 版完全够用。
坑五:同一套代码跑两次,结果差一点点。现象是两次重建图像的像素值在小数点后三位开始分叉,不仔细看发现不了,但严谨对比时会造成困扰。原因通常是重建代码里存在随机初始化,比如某些算法用随机数生成初始图像,或者脚本依赖并行计算导致浮点累加顺序不一致。并行池的浮动运算不保证结合律,不同线程间叠加顺序不同会有微小误差。解决方法是每次实验前固定随机流:rng(2025),实测很多做重现实验的代码加这一行就能解决 90% 的波动问题。如果你想彻底锁死浮点行为,还要考虑把并行池锁在单线程,但这会牺牲速度,一般研究场景没必要。还有就是因为 MIRT 老代码里用randn和rand的版本不同,在新版 Matlab 里默认随机流算法也换了,同一个脚本在不同版本下结果有细微差别是正常的,实验室内部统一 Matlab 版本即可。
6. 一个验证重建正确性的小套路:用已知真值做逐像素对比
最后分享一个我自己常用的验证习惯。每次换数据集或者换系统模型后,我不会直接上真实临床数据,而是先生成一个模拟数据:用phantom生成一个已知像素真值,加已知噪声水平,跑投影,重建,最后和真值做逐像素误差对比。关键指标不只是 RMSE,而是误差分布图。很多错误在整幅 RMSE 上反映不出来,但如果把abs(x - xtrue)画出来,你会发现误差往往集中在特定区域,比如图像边缘或高密度结构周围,这时候就能针对性地调整系统模型而不是盲目改参数。
rng(42); xtrue = phantom(256); sino = radon(xtrue, 0:0.5:179.5); sino_noisy = sino + 0.01 * randn(size(sino)); % 调用你自己写好的重建函数,或者 MIRT 里的现成重建 xrec = my_reconstruct(sino_noisy, 0:0.5:179.5); % 逐像素误差图和整体误差 err = abs(xrec - xtrue); figure; imshow(err, []); fprintf('RMSE=%.4f, maxAbsErr=%.4f\n', sqrt(mean(err(:).^2)), max(err(:)));对比时要注意视野和取样网格一一对应,真值和重建图的尺寸必须一致,必要时先把真值插值到重建网格上再计算误差。我早期犯过的错就是直接用不同尺寸矩阵做差值,Matlab 广播规则自动扩展了向量维度,结果误差图一大片全是假阳性,还以为是算法问题。找到了原因之后,我养成了每个脚本开头固定rng、做任何矩阵运算前检查size对齐的习惯,这套流程后来帮我省了非常多排查时间。
MIRT 这个工具箱看起来是学术代码堆出来的老工程,但它把重建问题拆成“数据项、正则项、优化器”三个独立模块,这个设计至今仍能打。希望这篇从安装、重建到调参排坑的笔记能帮到你,你一开始想通跑通哪个成像场景,就从相应 demo 开始,单步断点直接改成自己的数据,反复迭代几次系统模型和参数,跑出来的图像一定对得起你花的时间。
本文还有配套的精品资源,点击获取