简介:这份医学图像配准MATLAB代码包面向医学影像处理学习者与研究者,聚焦多时相、多设备及多模态图像的对齐与融合需求,尤其适合希望理解非刚性配准实现细节的中高级读者。压缩包共34个文件,约240KB,以22个m脚本和6个C语言源码为核心,辅以4张png示例图像、1个fig界面配置及1个txt说明,覆盖B样条形变、刚体变换、互信息相似度、梯度优化与结果可视化等环节。代码中可见二维与三维B样条变换、刚体配准、互信息直方图计算及自定义最小化函数等实现,并提供多个配准示例脚本与网格生成、图像缩放等辅助工具,便于结合自身数据复现实验、比较不同形变模型与相似度度量的效果。已有1336人学习,适合作为医学图像配准算法剖析、课程实验与课题预研的参考材料。
1. 项目定位:一套 MATLAB 医学图像配准工程能替你省掉哪些手工对齐
同一个患者前后两次扫描,体位角度差个几度是常态,想在两张图上比较病灶边界,第一步永远不是测量,而是把两张图对齐。医学图像配准在 MATLAB 里其实有一整套现成的函数族,但真正拿来做项目,难点不在调一个函数,而在把读图、预处理、变换模型选择、优化器参数、结果评估、批量落盘这一串串起来。这套工程就是把这条链拆成可配置的模块:一个入口脚本、一层预处理、一层配准核心、一层评估与输出。
它适合两类人。手上有一批影像数据、想快速验证刚体配准或多模态配准思路的算法同学,改几个参数就能跑出对比图;需要在既有 MATLAB 分析流程里嵌入配准环节的工程开发,直接调用函数即可,不必重写优化器。整个工程不依赖深度学习框架,纯 Image Processing Toolbox 就能运行,对没有 GPU 的机器相当友好。
2. 工程结构与技术选型:入口脚本、配准核心与配置层的三层拆分
2.1 目录结构与文件职责
工程没有把所有逻辑塞进一个 .m 文件,而是按职责切成了 io、preprocess、register、eval 四块,入口只负责串流程。这样做的直接好处是:换一个配准算法只动 register 目录,换一种评估指标只动 eval 目录,入口脚本基本不动。目录大致长这样。
medical_reg_demo/ ├── main_register.m # 入口:读配置、跑单例、出图 ├── config/ │ └── default_params.m # 所有可调参数集中在这 ├── src/ │ ├── io/ │ │ ├── load_pair.m # 读固定图/浮动图,统一类型 │ │ └── save_result.m # 存变换矩阵、配准图、指标 │ ├── preprocess/ │ │ ├── normalize_intensity.m │ │ └── crop_roi.m # 裁掉无关背景,提速也提精度 │ ├── register/ │ │ ├── build_registrar.m # 组装 optimizer + metric │ │ ├── run_rigid.m # 刚体/仿射配准 │ │ ├── run_multimodal.m # 互信息配准 │ │ └── run_demons.m # 形变配准 │ └── eval/ │ ├── metric_mi.m # 基于联合直方图的互信息 │ ├── metric_ncc.m │ └── metric_dice.m ├── data/ │ ├── fixed/ # 参考图 │ └── moving/ # 待配准图 ├── output/ # tform 矩阵、融合图、csv 指标 └── tests/ └── smoke_test.m # 一对图的冒烟测试| 文件 / 目录 | 职责 | 改它的典型场景 |
|---|---|---|
main_register.m | 读配置、调用链路、出图 | 加新的输出形式 |
config/default_params.m | 参数唯一来源 | 调迭代次数、金字塔层数 |
src/register/build_registrar.m | 组装优化器与相似性度量 | 换单模态/多模态策略 |
src/eval/metric_*.m | 配准质量量化 | 加新的评估指标 |
output/ | 变换矩阵与结果图 | 下游分析直接读 |
这里有一个血泪经验:参数千万别分散写在各个函数里。早期我把 MaximumIterations 写在 run_rigid 里,把金字塔层数写在 main 里,调参时来回翻三个文件,改完一个忘了另一个,跑出来的结果完全对不上,排查了半天才发现是两处参数打架。后来统一收进 default_params,返回一个 struct 往下传,谁要用谁读,问题立刻少了。
2.2 技术选型:为什么用 imregister 而不是自己写优化器
配准本质上是个最优化问题:找一个空间变换 T,让 moving 图经过 T 变换后和 fixed 图的相似性度量最大。自己写不是不行,遗传算法、Powell 法、粒子群都能上,但工程里我一般不这么干,原因很实在。
第一,MATLAB 的imregconfig已经把两套常用组合封装好了。单模态(比如两次 CT 之间)走monomodal,得到的是均方差度量加常规步长梯度下降;多模态(比如 CT 对 MRI、或 T1 对 T2)走multimodal,度量换成 Mattes 互信息,优化器换成 OnePlusOneEvolutionary,一种类似进化策略的搜索方式,专门应付互信息这种非凸、梯度不好求的目标函数。这两套组合是踩过大量实际数据调出来的默认值,直接拿来用比手撸靠谱。
第二,imregtform内部做了多分辨率金字塔。它会在低分辨率层先估一个大致的变换,逐层细化到原图。对医学影像这种初始位置可能偏出去几十个像素的情况,金字塔几乎是必须的,不然优化器很容易卡在局部极小值上,表现为配准结果只挪动了一点点就停了,看着像没跑。
第三,几何变换对象tform是仿射/投影矩阵的封装,能直接存、能复用到别的图、能反向求逆。这一点在二次开发里价值很大:同一序列的多张图,可以用一个变换矩阵批量imwarp,不用每张图重新配准。
选型上还有一层权衡:imregister一行出结果,方便但黑匣子;imregtform只返回变换,图像重采样自己用imwarp做,灵活但多写两行。工程里我选后者,因为要保存变换矩阵供下游复用,也要自己控制插值方式。
2.3 运行环境与参数落盘
环境要求不高:MATLAB 主程序加 Image Processing Toolbox,imregconfig、imregtform、imwarp、imregdemons、imregcorr都在这个工具箱里。三维数据要额外注意内存,后面避坑章会专门讲。配置文件写成一个返回 struct 的函数,好处是可以在函数里写默认值、做合法性检查,也能被测试脚本直接调用。
function cfg = default_params() % 配准工程默认参数,所有可调项集中在这里,避免散落在各脚本中 cfg.io.fixedDir = fullfile('data', 'fixed'); cfg.io.movingDir = fullfile('data', 'moving'); cfg.io.outDir = 'output'; % 配准模式:monomodal / multimodal / demons cfg.reg.mode = 'monomodal'; % 变换类型:translation / rigid / similarity / affine cfg.reg.transform = 'rigid'; % 优化器参数(单模态梯度下降用) cfg.reg.maxIter = 300; % 最大迭代次数,太小会提前停 cfg.reg.gradTol = 1e-5; % 梯度幅值容差,越大停得越早 cfg.reg.minStep = 1e-5; % 最小步长 cfg.reg.maxStep = 0.0625; % 最大步长,太大容易震荡 cfg.reg.relax = 0.5; % 松弛因子 % 优化器参数(多模态进化策略用) cfg.reg.growth = 1.05; % 搜索半径增长因子 cfg.reg.epsilon = 1.5e-6; cfg.reg.initRadius = 6.25e-3; % 度量参数 cfg.reg.numSamples = 5000; % 互信息采样点数,别低于 1000 cfg.reg.numBins = 64; % 联合直方图分箱数 cfg.reg.pyramidLevels = 3; % 金字塔层数,初始偏差大就调高 cfg.reg.verbose = false;% 是否画优化过程曲线 % 预处理开关 cfg.pre.normalize = true; cfg.pre.cropROI = true; cfg.pre.roiMargin = 10; % 裁剪时四周留的像素 end这段配置里最值得关注的是maxStep和pyramidLevels。maxStep决定单次迭代最多能挪多远,值给小了收敛慢,给大了在最优解附近来回跳,指标曲线会呈锯齿状;pyramidLevels是给初始偏差留的余量,两张图初始位置差得越多,层数越要往上加。numSamples则直接影响多模态配准的稳定性,样本太少时互信息估计方差大,优化器会被噪声带着走。
3. 核心链路拆解:从读图到输出 tform 的完整代码走一遍
3.1 预处理:灰度归一化、直方图匹配与 ROI 裁剪
预处理这一步很多人会跳过,觉得直接扔给imregtform就行。单模态、同设备、同序列的图确实可以偷懒,但只要跨设备或者跨时间,不做归一化,配准结果就是玄学。常见的问题是两张图灰阶范围差太多,均方差度量会被整体亮度差主导,优化器为了压亮度差把几何位置调歪。
function [F, M, Rfixed] = prepare_pair(fixedPath, movingPath, cfg) % 读取一对图像并做统一预处理:灰度化、归一化、可选 ROI 裁剪 F = imread(fixedPath); M = imread(movingPath); % 三通道直接转灰度,配准函数只接受二维或三维单通道 if size(F, 3) == 3, F = rgb2gray(F); end if size(M, 3) == 3, M = rgb2gray(M); end % 转单精度,避免 uint8 在梯度计算里溢出 F = im2single(F); M = im2single(M); if cfg.pre.normalize % mat2gray 线性拉伸到 [0,1],先对齐量纲 F = mat2gray(F); M = mat2gray(M); % 直方图匹配把 moving 的灰度分布往 fixed 上靠, % 对同模态不同设备的图很有效,多模态时不要开 if strcmpi(cfg.reg.mode, 'monomodal') M = imhistmatch(M, F, 256); end end % 用固定图的非零区域估一个包围盒,两张图裁同一块 if cfg.pre.cropROI mask = F > graythresh(F) * 0.5; stats = regionprops(mask, 'BoundingBox'); if ~isempty(stats) bb = stats(1).BoundingBox; m = cfg.pre.roiMargin; rect = [max(1, bb(1)-m), max(1, bb(2)-m), ... bb(3)+2*m, bb(4)+2*m]; rect(3) = min(rect(3), size(F,2) - rect(1) + 1); rect(4) = min(rect(4), size(F,1) - rect(2) + 1); F = imcrop(F, rect); M = imcrop(M, rect); end end Rfixed = imref2d(size(F)); end逐段说一下。im2single这步不能省,imregtform内部要做梯度和插值,uint8 在减法里会发生截断,表现为优化过程指标忽高忽低甚至直接报数据类型不匹配。imhistmatch只在单模态下开,多模态时两张图的灰度含义本来就不同,硬匹配会破坏互信息依赖的统计关系,反而拉低配准精度。
ROI 裁剪是性价比最高的一步。医学影像四周大量空气背景,这些像素对几何对齐没有任何信息量,却会占用互信息的采样点。裁掉之后配准速度能快一截,精度也更稳。imref2d是为了后面imwarp时明确输出网格,不然输出图尺寸会跟着变换矩阵变,融合显示时两张图对不上。
3.2 配准器构建:optimizer、metric 与金字塔层数
imregconfig返回的是带默认值的对象,直接改属性就行。把这段单独抽成build_registrar,是为了让入口脚本里看不到任何优化器细节。
function [optimizer, metric] = build_registrar(mode, cfg) % 根据模式组装优化器与相似性度量,并覆盖默认参数 switch lower(mode) case 'monomodal' [optimizer, metric] = imregconfig('monomodal'); optimizer.MaximumIterations = cfg.reg.maxIter; optimizer.GradientMagnitudeTolerance = cfg.reg.gradTol; optimizer.MinimumStepLength = cfg.reg.minStep; optimizer.MaximumStepLength = cfg.reg.maxStep; optimizer.RelaxationFactor = cfg.reg.relax; case 'multimodal' [optimizer, metric] = imregconfig('multimodal'); optimizer.MaximumIterations = cfg.reg.maxIter * 2; % 进化策略收敛慢 optimizer.GrowthFactor = cfg.reg.growth; optimizer.Epsilon = cfg.reg.epsilon; optimizer.InitialRadius = cfg.reg.initRadius; % 采样点与分箱数直接决定互信息的估计质量 metric.NumberOfSamples = cfg.reg.numSamples; metric.NumberOfHistogramBins = cfg.reg.numBins; otherwise error('build_registrar:unknownMode', '未知配准模式: %s', mode); end end参数含义逐个说清。MaximumIterations是迭代上限,单模态梯度下降一般 200 到 300 够用,多模态的进化策略因为每轮要评估多个候选,收敛慢,通常要给到两倍。GradientMagnitudeTolerance是提前终止条件,梯度小于这个值就认为到最优了,值调大能让它早点停,但可能停在半路。MinimumStepLength和MaximumStepLength是一对,前者防止步长衰减到零后还在空转,后者防止一开始步子迈太大直接跳出图像范围。
多模态这边,InitialRadius是初始搜索半径,单位是像素,默认值比较保守,初始偏差大时可以往上调。NumberOfSamples我一般不会低于 1000,低于这个数互信息曲线会抖得厉害,优化器容易在几个候选之间反复横跳,表现为跑很久但变换矩阵基本没动。
3.3 执行配准与几何变换保存
主干调用只有一行,但参数顺序和输出网格是两个高频翻车点。
function [tform, Mreg] = run_rigid(M, F, cfg) % M 是 moving(待配准),F 是 fixed(参考),顺序不能反 [optimizer, metric] = build_registrar('monomodal', cfg); tform = imregtform(M, F, cfg.reg.transform, optimizer, metric, ... 'PyramidLevels', cfg.reg.pyramidLevels, ... 'DisplayOptimization', cfg.reg.verbose); % 用固定图的参考网格做重采样,保证输出和 F 同尺寸同坐标系 Rfixed = imref2d(size(F)); Mreg = imwarp(M, tform, 'OutputView', Rfixed, ... 'InterpolationMethod', 'linear'); % 变换矩阵落地,下游直接 load 复用,不用重跑 save(fullfile(cfg.io.outDir, 'tform_rigid.mat'), 'tform'); endimregtform(moving, fixed, ...)的参数顺序,我见过不止一个人接反。接反之后函数照样跑完不报错,只是返回的是 fixed 到 moving 的变换,用在imwarp上结果就是图往反方向偏,越看越像算法不准,其实是顺序错了。
OutputView这参数也值得单说。不指定的时候,imwarp会按变换后的外接框输出,图像尺寸和原图不一样,跟 fixed 叠在一起显示时尺寸对不上,融合图会出现黑边或者错位。指定imref2d(size(F))之后输出网格和参考图完全一致,后面imshowpair、imfuse直接就能用。
保存tform是这个工程的一个关键设计。几何变换对象里存的是完整的变换矩阵,下次要处理同一序列的另一张图,直接imwarp(newM, loadedTform, 'OutputView', Rfixed)就行,不用重新跑优化。这一点在批量任务里能省掉大量时间。
3.4 结果呈现与变换复用
结果呈现不只是看一眼,还得能让人判断配准到底成没成。
function show_and_save(F, M, Mreg, cfg, name) % 三联图:配准前棋盘对比、配准后棋盘对比、红绿通道叠加 fig = figure('Visible', 'off', 'Position', [100 100 1200 400]); subplot(1,3,1); imshowpair(F, M, 'checkerboard'); % 配准前,棋盘格应错位 title('配准前'); subplot(1,3,2); imshowpair(F, Mreg, 'checkerboard'); % 配准后,棋盘格应连续 title('配准后'); subplot(1,3,3); imshowpair(F, Mreg, 'diff'); % 红绿差异图,绿色越少越对齐 title('差异图'); saveas(fig, fullfile(cfg.io.outDir, [name '_compare.png'])); close(fig); endcheckerboard模式是我最常用的判断方式:把两张图切成棋盘格交替显示,对齐好的话格子的结构应该是连续的,错位的话在格子边界上能看到明显断裂,一眼就能看出来。diff模式用红绿区分,绿色区域表示两张图亮度接近,红色表示差异大,配准后绿色占比应该明显上升。
这里有个容易忽略的点:棋盘格看起来连续,不代表几何精度就够。粗配准到像素级,棋盘格就已经很连续了,但要做病灶体积测量,还得看后面评估指标那一节的内容。
4. 多模态与形变配准:互信息配准和 demons 位移场两条支线
4.1 多模态:Mattes 互信息配准
单模态配准用均方差没问题,因为两张图同一个像素的灰度含义相同,对齐了灰度差就该最小。但多模态不一样,比如一张 T1 加权图和一张 T2 加权图,同一个解剖位置上一个亮一个暗,均方差会把「对齐」判成「差异大」,优化器直接跑反。这时候要换成互信息。
互信息衡量的是两张图的统计相关性,不要求灰度值相等,只要求「一个图亮的地方,另一个图也有规律地亮或暗」。Mattes 互信息是其中一个变种,用随机采样点估联合直方图,比全图统计快很多。
function [tform, Mreg, miVal] = run_multimodal(M, F, cfg) % 多模态配准:CT/MRI 之间、不同序列之间用这条支线 [optimizer, metric] = build_registrar('multimodal', cfg); tform = imregtform(M, F, 'rigid', optimizer, metric, ... 'PyramidLevels', cfg.reg.pyramidLevels); Rfixed = imref2d(size(F)); Mreg = imwarp(M, tform, 'OutputView', Rfixed, ... 'InterpolationMethod', 'linear'); % 配准前后各算一次互信息,用来判断这次配准是否有效 miBefore = metric_mi(M, F, cfg.reg.numBins); miAfter = metric_mi(Mreg, F, cfg.reg.numBins); miVal = struct('before', miBefore, 'after', miAfter, ... 'gain', miAfter - miBefore); end这段代码里我特意保留了配准前后的互信息对比。多模态配准不像单模态那么直观,棋盘格看着连续也可能是假的,光看视觉判断很容易被骗。配准后互信息应该明显高于配准前,如果gain接近零甚至为负,说明这次配准基本没起作用,得回头检查预处理和初始位置。
metric.NumberOfSamples和NumberOfHistogramBins是多模态这条线的两个核心参数。采样点少,估计方差大;分箱数太多,每箱里的样本稀,互信息会被噪声主导;太少,又分辨不出灰度分布的细节。64 个箱、5000 个采样点是个比较稳妥的起点,数据量大或者图像分辨率高的时候可以往上加。
4.2 形变配准:imregdemons 与位移场
刚体配准只能处理整体平移和旋转。软组织、呼吸运动、组织形变这类非线性变化,刚体模型根本描述不了,这时候要上形变配准。imregdemons实现的是 demons 算法,输出不是变换矩阵,而是一个位移场,每个像素都有自己的位移向量。
function [D, Mdef] = run_demons(M, F, cfg) % 形变配准:适用于软组织、呼吸运动等非刚性形变 % 迭代次数按金字塔从粗到细设置,[100 50 20] 表示三层 D = imregdemons(M, F, [100 50 20], ... 'AccumulatedFieldSmoothing', 1.2, ... % 平滑系数,越大位移场越平滑 'PyramidLevels', 3, ... 'DisplayWaitbar', false); Mdef = imwarp(M, D); % 位移场直接作为 imwarp 的第二个参数 endimregdemons(moving, fixed, iterations)的第三个参数是各层的迭代次数,从粗层到细层依次递减。粗层迭代多,先把大形变拉过来;细层迭代少,只做局部微调。AccumulatedFieldSmoothing控制位移场的平滑程度,值太小位移场会出现高频抖动,组织边界被撕开;值太大又会把真正的形变抹平,一般从 1.0 到 1.5 之间试。
形变配准的计算量比刚体大一个量级,因为位移场的尺寸和图像一样。512×512 的二维图还好,三维体数据直接就是 512×512×N×3 的数组,内存很容易顶不住。我一般的做法是先降采样做粗配准,再把位移场上采样回原尺寸作为初值,最后在原图上跑少量迭代。这条路径不是必须的,但数据一上三维就能体会到差别。
4.3 评估:互信息、NCC、Dice 三个指标怎么算
配准做完必须量化,不然全是感觉。工程里放了三个指标,覆盖不同场景。
互信息适合多模态,值越大越对齐。MATLAB 没有直接暴露imregtform内部用的度量值,所以得自己按联合直方图算一个。
function mi = metric_mi(A, B, nbins) % 基于联合直方图的互信息,值越大说明两图统计相关性越强 A = double(A(:)); B = double(B(:)); edgesA = linspace(min(A), max(A) + eps, nbins + 1); edgesB = linspace(min(B), max(B) + eps, nbins + 1); p = histcounts2(A, B, edgesA, edgesB); p = p / sum(p(:)); % 归一化成联合概率 pa = sum(p, 2); % 边缘概率 pb = sum(p, 1); % 只取联合概率非零的格子,避免 log(0) 产生 -Inf [ia, ib] = find(p > 0); pab = p(p > 0); mi = sum(pab .* log2(pab ./ (pa(ia) .* pb(ib)))); endhistcounts2一次性把两个方向的直方图都统计完,比手写双重循环快很多。p > 0的筛选是必须的,联合直方图里大量格子是空的,直接取 log 会得到 -Inf,整个结果就废了。这段代码里pa(ia)和pb(ib)都是列向量,点乘得到每个非零格子的边缘概率乘积,维度自然对齐。
NCC 适合单模态,用corr2一行就够,值域 -1 到 1,越接近 1 越对齐。Dice 系数用于二值掩膜,比如两张图都分割出了病灶区域,配准之后算两个掩膜的重合度,公式是两倍交集除以两个集合大小之和。
function d = metric_dice(A, B) % 二值掩膜的 Dice 相似系数,1 表示完全重合 A = logical(A); B = logical(B); d = 2 * nnz(A & B) / (nnz(A) + nnz(B) + eps); end三个指标各有适用面:同模态看 NCC 和 Dice,多模态看互信息,形变配准看 Dice 加位移场幅值。别指望一个指标打天下,我一般是配准后三个都算一遍存进 csv,哪次结果看着不对劲,翻表就能定位是哪类指标掉了。
5. 避坑与排查:配准跑歪、报错和跑不动的 5 条血泪记录
5.1 结果层面的两类翻车
现象一:配准后图像整体往反方向平移,越配越远。原因基本是两个。最常见的是imregtform的 moving 和 fixed 顺序接反,函数不报错,但返回的是反向变换。其次是没有指定OutputView,imwarp按变换后的外接框输出,图像尺寸变了,再和原图叠加时位置自然对不上。 解决:在run_rigid里固定参数顺序,函数签名写成run_rigid(M, F, cfg)并在注释里标明 M 是待配准、F 是参考;imwarp一律带'OutputView', imref2d(size(F))。不确定方向时,用imregcorr先估一个相位相关的平移量做交叉验证,两个结果差得离谱就说明有一处接反了。
现象二:配准只挪动了一两个像素就停了,棋盘格还是明显断裂。这是典型的局部极小值。初始位置偏差太大,金字塔层数又不够,优化器在低分辨率层就掉进了一个错误的谷底,后面几层只是在这个错误位置附近微调。 解决:先确认PyramidLevels至少给到 3;初始偏差超过图像尺寸十分之一时,把层数加到 4。再不行就上粗配准:把两张图降采样到四分之一,跑一次平移配准拿到大致位置,把结果作为初值传给完整配准。我一般会在入口加一句检查,配准前后互信息增益小于阈值就自动触发粗配准重跑一次,省得人工盯。
5.2 报错与性能层面的三类问题
现象三:报错提示输入必须是二维或数据类型不支持。原因通常是三通道 RGB 图没转灰度,或者 uint8 直接进了优化器。imregtform要求输入是二维灰度或三维单通道,uint8 在梯度和插值计算里会溢出。 解决:在prepare_pair里强制rgb2gray加im2single,这一步放在所有处理之前。加完之后同类报错基本就绝迹了。
现象四:多模态配准指标曲线抖动剧烈,跑满迭代次数也不收敛。原因是互信息的采样点落到了大片背景上。医学影像背景占比经常超过一半,随机采样很容易全采到空气,联合直方图基本是空的,互信息估计方差极大。 解决:先裁 ROI 再配准,这一步对多模态的提升比单模态更明显;把NumberOfSamples从默认值提到 5000 以上;分箱数控制在 64 附近,别贪多。如果数据本身对比度很低,可以先做一次高斯平滑再配准,能明显压住抖动。
现象五:三维数据跑形变配准,内存爆掉或者跑了半小时没动静。原因是位移场的维度和图像一样,三维数据下imregdemons内部要维护多个同尺寸的双精度数组,512×512×200 的体数据很容易把内存吃满。 解决:先降采样到一半或四分之一跑配准,再把位移场用imresize3上采样回原尺寸作为初值,最后在原图上只跑少量迭代。同时把各层迭代次数从默认的 [100 50 20] 降到 [40 20 10],精度损失不大,时间能省一大半。
提示:这五条里,顺序接反和没指定 OutputView 占了实际排查量的一大半。遇到「配准不准」的反馈,先别急着调参数,把这两点确认一遍,能省下大半天。
6. 二次开发:把配准接进批处理管线和评估表格
单对图像跑通之后,真正的工作量在批量。工程里main_register.m只处理一对图,实际项目中往往是几十上百个病例,每个病例又是多个时间点。这时候不需要重写配准逻辑,加一层循环和一张结果表就够了。
| 改造点 | 入口 | 改动量 | 风险 |
|---|---|---|---|
| 批量遍历病例 | 新增batch_register.m | 小 | 文件命名不规范导致配对错 |
| 变换矩阵复用 | run_rigid返回值 | 小 | 参考网格不一致 |
| 结果汇总成表 | save_result.m | 中 | 指标列类型不统一 |
| 换配准算法 | build_registrar | 中 | 参数默认值不适用 |
| 接图形界面 | 新增 app 文件 | 大 | 长任务阻塞 UI |
批处理的核心是把「配对、跑、存、记」四件事拆开。配对靠文件名规则,跑调用现成函数,存走save_result,记就是往表格里追加一行。
function batch_register(cfg) % 批量配准:按文件名前缀配对 fixed 与 moving fixedFiles = dir(fullfile(cfg.io.fixedDir, '*.png')); results = table(); for k = 1:numel(fixedFiles) name = erase(fixedFiles(k).name, '.png'); movingPath = fullfile(cfg.io.movingDir, [name '.png']); if ~isfile(movingPath) warning('跳过 %s:找不到对应的 moving 图', name); continue; end try [F, M] = prepare_pair(fullfile(cfg.io.fixedDir, fixedFiles(k).name), ... movingPath, cfg); [tform, Mreg] = run_rigid(M, F, cfg); % 三个指标一起算,方便后续横向对比 mi = metric_mi(Mreg, F, cfg.reg.numBins); ncc = corr2(Mreg(:), F(:)); results = [results; table({name}, mi, ncc, ... 'VariableNames', {'caseName', 'MI', 'NCC'})]; save_result(F, Mreg, tform, cfg, name); % 存图 + 存变换矩阵 catch ME % 单个病例失败不中断整批,错误写日志 warning('病例 %s 配准失败: %s', name, ME.message); end end writetable(results, fullfile(cfg.io.outDir, 'metrics.csv')); end这段代码里有两个细节值得说。try-catch是必须的,批量任务里总有那么一两个病例数据有问题,不能让一个异常把整批打断;但也不能静默吞掉,warning里带上病例名和错误信息,出问题能定位。表格用table而不是 cell 数组,是因为writetable直接出 csv,后续在别的工具里做统计方便得多。
再往深一层改,就是把配准接到分割或测量流程里。配准输出的tform可以直接作用到分割掩膜上,让两次扫描的病灶区域落在同一个坐标系里,再做体积差值。这一步的关键是掩膜重采样时用最近邻插值,别用线性插值,否则二值边界会被插出中间值,Dice 算出来偏低。
最后说个我自己的习惯。早些年做配准,跑完看一眼棋盘格觉得挺连续,就直接把结果交给下游做测量了,后来发现有几例的位移方向根本就是反的,只是棋盘格在那个尺度上看不出差别。从那以后我每次跑完批量任务,都会强制走一遍「配准前后互信息增益 + NCC 变化」的双指标检查,增益为负或者变化小于阈值的病例全部打回重跑,再不济也要人工过一眼棋盘格。这个习惯拦下过不少看着正常、实际错位的结果,比事后返工便宜太多。希望帮到你。
本文还有配套的精品资源,点击获取