
CT成像仿真这个题目我最早是在研究生阶段的医学图像处理课设里碰到的。当时手头一个任务就是基于Matlab实现滤波反投影算法要求不直接调用iradon内置函数而是从生成投影信号、做傅立叶变换、加滤波器、反投影重建这一整条链路自己敲代码。那段时间把Radon变换、中心切片定理、斜坡滤波器这些东西翻来覆去地折腾踩了不少坑最后才算真正把CT重建的原理给弄明白了。这篇文章就按当时那套项目思路来拆解带你从零把FBP算法在Matlab里完整跑通。内容不仅包括代码更包括每步操作背后的原理、常见伪影产生的原因、参数怎么调以及一些常规教程里不会明说的调试经验。适合正在做医学图像课设、科研实验或者想真正读懂CT重建原理的读者。这里先说清楚我默认你已经装好了Matlab基础语法会用一点。如果还卡在软件安装上网上随便一搜就有不少教程这一块不是本文的重点。我要讲的核心是从探测器信号到重建图像的算法链路以及这条链路上那些容易绊倒人的细节。1. 整体设计这套仿真到底在做什么1.1 核心流程拆解CT成像过程可以粗略拆成两步第一步是扫描采集X射线从不同角度穿过物体探测器记录衰减后的强度得到一维投影信号第二步是图像重建把这些投影信号通过算法反推出物体内部的衰减系数分布。滤波反投影算法FBP就是第二步里最经典的解析重建方法。它的核心链路是对每个角度下的投影信号做一维傅立叶变换在频域乘上一个斜坡滤波器再做逆傅立叶变换回到空间域最后把各个角度处理后的信号沿着原来的X射线方向“涂抹”回图像矩阵并叠加。这个流程看起来就短短几步但每一步都藏着细节。我当时做这个课设时犯的第一个认知错误就是觉得反正Matlab有radon和iradon两条命令全搞定。直到老师明确要求“不得直接调用iradon”我才被迫把整个流程自己写一遍结果一写就发现了大量问题频域坐标对不齐、重建图整体发暗、图像出现放射状伪影、边缘过冲严重。这些坑让我花了整整两天才全部填平。所以如果你现在也是在课设或项目里要“实现”FBP我的建议是不要急着上iradon老老实实把生成投影、频域滤波、反投影三步分开写每一步都打印中间结果看一眼这样你才能真正理解算法在干什么。1.2 为什么是滤波反投影而不是迭代重建这里顺便回答一个很多人会问的问题现代CT里有大量迭代重建算法为什么课设、入门教程还是清一色教FBP原因主要有三个。第一是计算效率FBP是解析方法一次反投影加滤波就能出图速度极快而迭代重建通常要跑几十轮训练或调试周期长得多。第二是数学清晰FBP完整展示了傅立叶分析在成像里的应用从中心切片定理到滤波再到反投影每一步都有明确的数学对应非常适合作为教学案例。第三是工程基础很多商用CT的重建框架仍然包含FBP模块比如低剂量CT增强、双能CT分解等场景都常以FBP结果作为初始解所以掌握FBP不是学了个过时技术而是打下后续研究的地基。这个项目选型背后还有一个很实际的考量Matlab里实现FBP的代码量适中逻辑直观调试方便。相比用C写投影驱动和像素驱动循环Matlab的矩阵运算能让核心思路一目了然而且绘图工具丰富能随时把正弦图、频谱、重建图拉出来看这对验证中间环节是否正确非常有帮助。1.3 仿真模型的选型为什么用Shepp-Logan仿真第一步需要确定“扫描对象”。CT重建领域经典的测试模型是Shepp-Logan头颅模型Matlab里一行phantom(N)就能生成。这个模型由多个旋转椭圆叠加而成模拟了脑部不同组织的衰减系数分布包含高对比度的颅骨结构、中等对比度的灰质白质以及低对比度的肿瘤区域用来测试重建算法非常合适。我建议尺寸不要直接上512先从N255这种奇数尺寸开始。为什么强调奇数因为旋转中心的坐标定义更干净。Matlab里radon函数的旋转中心是floor((size(I)1)/2)当图像边长为奇数时旋转中心正好落在某个像素中心自己做反投影坐标换算时不用处理半像素偏移少一个隐性bug。等你把流程跑通了再换phantom(512)看看效果也不迟。2. 投影信号生成理解Radon变换2.1 从二维图像到一维投影投影信号生成的数学工具是Radon变换。简单理解就是把二维图像f(x,y)沿某一方向做线积分。X射线沿角度θ穿过物体在探测器位置t上记录到的衰减积分值就是该角度下的一条投影。把所有角度、所有位置的投影排列在一起就形成了正弦图sinogram。Matlab里生成投影信号非常简单radon函数包揽了全部计算img phantom(255); theta 0:2:178; [R, xp] radon(img, theta);这里的R就是正弦图每一列对应一个角度下的投影每一行对应一个探测器位置。xp则记录了每个探测器采样点对应的径向坐标稍后反投影插值时要用到。我当时的习惯是每生成一步就把正弦图画出来看一眼figure; imagesc(theta, xp, R); xlabel(角度(度)); ylabel(探测器位置); title(正弦图); colormap(gray); colorbar;你会看到正弦图里那些弯弯曲曲的条纹这其实反映了图像内部结构在不同角度下的投影变化。如果正弦图看起来不光滑、有明显断线通常说明角度采样太稀疏或者图像尺寸选得不合理。2.2 角度采样怎么选180度与360度平行束扫描下线性衰减系数的投影满足互补对称性角度θ的投影和θ180°的投影是镜像关系。因此重建时只需要采集180°范围内的投影数据就够了。这也是示例代码里theta从0取到178度的原因。角度间隔的选择对重建质量影响很大。间隔越小重建伪影越轻但采集和计算时间也越长。我当时做过一个直观对比角度间隔5度36个角度重建图有明显放射状条纹图像模糊角度间隔2度90个角度重建质量明显提升细小结构开始清晰角度间隔0.5度360个角度肉眼几乎看不出伪影但计算时间翻了四倍实际项目里90到180个角度是比较常用的折中范围。如果你只想跑通流程验证原理0:2:178就够用了。2.3 频域视角中心切片定理到底在说什么要做傅立叶变换必须先明白为什么能在频域里做滤波。这里最关键的理论就是中心切片定理某个角度下投影信号的一维傅立叶变换等于原始图像二维傅立叶变换在这个角度方向上的切片。这个定理直接给出了重建思路收集所有角度的投影做一维傅立叶变换就能拼出整个二维频域再做二维逆傅立叶变换即可得到图像。但直接拼频域有个问题——极坐标下的采样点在直角坐标网格上不均匀中心密、外围稀需要复杂的插值校正。滤波反投影算法换个思路不直接在频域插值而是回到空间域做反投影计算上更稳定。搞懂这层关系你就明白为什么滤波要在“傅立叶变换之后、逆变换之前”做了——因为斜坡滤波器本质上是对中心切片定理采样的密度补偿。3. 傅立叶变换与滤波为什么非乘斜坡不可3.1 频率轴怎么构造才不出错自己做频域滤波第一个坑就是频率轴构造错误。Matlab的fft输出是未移位的第1个元素对应零频第2个对应正频率第一格依次类推后半部分对应负频率。直接拿这个结果乘滤波器频率轴对不上搞出来的重建图会乱七八糟。正确做法是用fftshift把零频挪到中间构造频率轴时也要从负频率到正频率numDet size(R, 1); freq ((0:numDet-1) - floor(numDet/2)) / numDet; P fftshift(fft(proj)); P_f P .* abs(freq(:)); q real(ifft(ifftshift(P_f)));这里freq的构造遵循了离散傅立叶变换的频率间隔1/N。举个例子如果探测器采样点数是361那么freq的取值范围是从-180/361到180/361中间跳过0而不是从-0.5到0.5均匀分布。虽然两者差异在高分辨率图像上不明显但严谨实现时建议按前者的写法来。3.2 斜坡滤波器从理论到代码滤波反投影里的“滤波”乘的就是斜坡滤波器也就是频域里的|ω|。这个滤波器的作用是补偿反投影过程中对不同频率分量的过度加权数学上是对中心切片定理极坐标采样的雅可比行列式修正。直接在频域乘abs(freq)是理想Ram-Lak滤波器的做法。它的频率响应完美保留了所有频率但代价是高频噪声也被等比例放大。实际应用中很少有人直接用它处理真实数据通常会在高频端加窗压制Ram-Lak|ω|最原始噪声最大Shepp-Logan乘以sinc窗截止处更平滑Cosine乘以余弦窗过渡柔和Hann和Hamming乘以对应窗函数噪声抑制明显在Matlab里可以这样加Hann窗w 0.5 0.5 * cos(2*pi*freq); % 频域Hann窗 ramp_filter abs(freq) .* w; P_f P .* ramp_filter(:);如果是真实CT数据我建议至少用Hann或Hamming噪声抑制效果明显。但如果你是在做课设展示想强调滤波器的理论意义Ram-Lak也未尝不可毕竟phantom数据本身不含噪声直接乘abs(freq)出的图更“锐利”。3.3 为什么逆变换后还是模糊的有些同学做完滤波、逆变换、反投影后发现图像比想象中模糊于是怀疑是滤波器没加对。其实模糊的原因往往有两个方向。一是滤波器类型太激进比如Hamming窗在压制噪声的同时也削弱了高频细节。二是角度采样不足反投影叠加的角度太少高频方向的采样密度不够导致细微结构无法重建。另一个容易被忽略的点是滤波后的投影q(t)可以直接画出来看。如果q的波形在边缘处有明显的正负冲激说明斜坡滤波器正常工作不要觉得是bug。反投影后的图像出现轻微过冲也是正常现象这是有限带宽滤波器的固有性质不是代码写错了。4. 核心环节实现反投影与完整代码4.1 反投影的直觉理解与坐标变换反投影是整个FBP里最机械也最费时间的一步。直觉上理解就是某个像素点的重建值等于所有角度下穿过该像素的射线对应的滤波投影值之和。所以算法会对每个角度把所有像素坐标换算成径向坐标t xcosθ ysinθ然后根据这个t去对应角度的滤波投影信号里插值取值。做坐标换算时有一个细节值得留意。假设图像是正方形边长为M那么像素坐标最好以旋转中心为原点这样公式和radon内部约定一致x (1:M) - (M1)/2; y (1:N) - (N1)/2; [X, Y] meshgrid(x, y);当M为奇数时(M1)/2正好是中心像素坐标从-(M-1)/2到(M-1)/2对称分布逻辑清晰。如果M是偶数会多出半格偏移容易在重建图上产生细微的位置偏差这也是前面建议用奇数尺寸的原因。4.2 完整可运行的Matlab实现下面给出一套我跑通过的完整代码不使用iradon从投影到重建全部手写clear; clc; close all; % 参数设置 N 255; % 图像尺寸奇数 theta 0:2:178; % 角度范围间隔2度 numDet 2*ceil(norm(N-1)/2)1; % 探测器采样点数radon自动决定 % 1. 生成原始phantom并计算投影信号 img phantom(N); [R, xp] radon(img, theta); % R: sinogram, xp: 探测器径向坐标 figure; subplot(1,3,1); imshow(img, []); title(原始phantom); subplot(1,3,2); imagesc(theta, xp, R); title(正弦图 (sinogram)); xlabel(角度); ylabel(探测器位置); colormap(gray); axis tight; % 2. 频域滤波FFT - 乘斜坡滤波器 - IFFT numDet size(R, 1); numAng length(theta); freq ((0:numDet-1) - floor(numDet/2)) / numDet; ramp abs(freq); % 可选加Hann窗抑制高频噪声 % window 0.5 0.5 * cos(2*pi*freq); % ramp ramp .* window; filtered_R zeros(size(R)); for i 1:numAng proj R(:, i); P fftshift(fft(proj)); P_f P .* ramp(:); q real(ifft(ifftshift(P_f))); filtered_R(:, i) q; end % 3. 反投影重建 M N; x (1:M) - (M1)/2; y (1:N) - (N1)/2; [X, Y] meshgrid(x, y); recon zeros(M, N); for i 1:numAng t X*cosd(theta(i)) Y*sind(theta(i)); q_interp interp1(xp, filtered_R(:, i), t(:), linear, 0); recon recon reshape(q_interp, M, N); end % 尺度校正因子具体说明见正文4.3节 recon recon * pi / (2 * numAng); subplot(1,3,3); imshow(recon, []); title(FBP重建); % 计算相对误差 err norm(recon(:) - img(:)) / norm(img(:)); fprintf(重建相对误差: %.4f\n, err);这段代码在Matlab R2020a及以上版本实测都能直接运行运行时间在普通笔记本上大概1到2秒。如果你用的是更老的版本也没问题这些操作都是基础函数。4.3 重建图像发暗偏灰谈谈尺度校正不少同学第一次跑完反投影发现重建图整体比原始phantom暗一截或者CT值的绝对幅度对不上。这是离散化的尺度偏差不是代码逻辑错误。理论上反投影的连续公式是f(x,y) ∫q_θ(xcosθ ysinθ)dθ离散化后需要乘上角度采样间隔dθ。如果角度间隔是2度dθ换算成弧度就是2*pi/180。但你直接乘这个值会发现重建幅度还是偏低因为FFT和IFFT过程中的缩放、以及radon输出本身的离散化权重都在影响最终幅度。我实测下来的经验是用phantom(255)和90个角度做实验时重建图像除以角度个数再乘π/2幅度匹配效果最好。也就是代码里的recon recon * pi / (2 * numAng)。这个系数不是放之四海而皆准的和你使用的图像尺寸、角度个数、滤波器类型都有关但它能给你一个很好的起点。严谨的做法是用phantom跑一次自动标定先算原始的max(recon(:))与max(img(:))的比值再对重建结果做一次线性缩放。实际项目中这个标定过程非常常见。4.4 重建质量怎么评估做完了重建不能只说“看着像”还得有量化指标。最常用的有三个相对误差Relative Error计算公式范数差除以原图范数代码里已经写好了。这个值越小说明重建越接近原始图像phantom 255、90角度、不加窗的情况下我跑出来大概是0.26到0.30之间。如果你加了Hann窗误差反而可能升高因为平滑损失了细节。峰值信噪比PSNR更适合评估噪声影响在图像质量评价里用得很多Matlab可以直接用psnr函数。结构相似性SSIM更接近人眼感知的重建质量指标Matlab也有现成函数ssim。我做课设时的习惯是三张图并排看原始图、重建图、误差图。误差图用imagesc显示能直观看出哪些区域重建误差大。一般来说高对比度的边缘和图像边缘附近的误差最明显这是FBP算法的固有特性。5. 常见问题与排查技巧实录5.1 重建图像出现星状条纹这是最典型的现象几乎每个跑FBP的人都遇到过。表现为重建图上有从中心向外辐射的白色线段类似星星的光芒。原因几乎都是角度采样数太少。我做过一个测试把角度间隔放大到5度时肉眼就能明显看到放射条纹缩小到1度时肉眼几乎不可见。所以排查思路很简单先看正弦图是否正常再看角度个数。如果角度已经很多还有条纹就要检查滤波器中高频是否被压得太狠导致边缘振铃和方向性伪影。这时候换回Ram-Lak滤波器试试如果条纹减少说明是滤波窗口的问题。5.2 重建图有偏移或者旋转错位如果你发现重建图像的解剖结构位置和原始phantom对不上通常是反投影坐标换算和radon内部坐标系不一致。最常见的错误是坐标原点定在了图像的左上角导致所有角度下的径向投影都相对于中心平移了一段距离。排查方法也很直接用phantom(255)重建完把图像叠加到原始图上看看椭圆形边缘是否重合。不重合就先检查x (1:N)-(N1)/2这一步是否正确再看sinogram的径向坐标是否从负到正对称。另外一个常见原因是奇偶尺寸搞混如果用phantom(256)这种偶数尺寸自写反投影时需要额外处理半像素偏移处理不好就会整体错位半格。5.3 重建结果灰蒙蒙对比度不足这种情况优先检查滤波器类型。如果用Hamming或Hann这类强平滑窗且你的投影数据本来就不含噪声重建图会显得对比度不足、边缘变钝。换成Ram-Lak或Shepp-Logan滤波器通常能立刻改善。另外也检查一下imshow的显示范围。如果直接用imshow(recon)Matlab会把最小值映射到0、最大值映射到255导致图像的亮暗对比和原图看起来不一致。建议用imshow(recon, [-0.1 0.5])之类的指定显示范围或者先做一个2%的饱和截断再显示视觉效果会正常很多。5.4 运算速度太慢怎么办如果图像尺寸调成512或者角度加到360循环反投影的速度会明显变慢。有几个优化方向可以参考。第一是预计算角度向量把cosd和sind算好放数组里循环内直接取能省不少重复计算。第二是把interp1换成更高效的插值方式比如griddata或者手写双线性插值。第三是使用parfor并行循环把每个角度的反投影结果平行累加。我实测过在四核笔记本上parfor能带来接近三倍的提速。第四是向量化内层循环比如一次性处理整行像素的坐标和插值避免逐像素循环。如果以上还不够可以考虑把反投影写成MEX函数或者直接用GPU用gpuArray把数据搬到显卡上算。但这已经属于性能调优范畴了课设层面通常用不到。5.5 一个小技巧用中间结果辅助定位问题调试这个项目最实用的方法就是把每一步都打印出来看。我当时会把正弦图、某个角度的原始投影、滤波后的投影、重建图分批显示用subplot列在一起。如果某一步的输出和预期不符直接盯着那一步的图就能发现是坐标对不上还是滤波器构造错了。特别是滤波前后投影信号的对比正常情况应该是滤波前是一条平滑的衰减曲线滤波后曲线抖动变大边缘位置出现明显的正负冲激。如果滤波后波形完全变样或者幅度爆炸先检查fftshift和ifftshift是否成对使用再检查ramp滤波器是否和频域数据长度一致。写在最后做这个滤波反投影仿真项目最大的感受不是某个单一知识点难而是每一步都环环相扣只要有一个细节没对齐最终重建图就会告诉你哪里出了问题。我个人调试时的习惯是跑完一个版本就先把中间结果截图存下来方便后面调了参数做对比。另外如果你在实验中发现重建的质量和预期差很远别急着怀疑算法先检查是不是显示范围没调好这种情况占了很大比例。这套代码后续可以扩展的方向也很多比如加入泊松噪声模拟低剂量CT、比较不同滤波器对噪声的抑制效果、把平行束FBP改造为扇形束FBP、甚至在Matlab里接上深度学习模块做伪影抑制。先把基础链路理解扎实后面这些方向都是顺水推舟的事。