论文精读|FBPConvNet:先 FBP 再 U-Net,深度学习 CT 重建的起点

前面几篇我们实测过:同一个体模,30 个角度的 FBP 只有 22.14 dB,满屏条纹;换成 SART 加 TV 能到 36.35 dB,但要迭代 200 次,还要手调正则权重。

2016 年底,EPFL 的 Jin、McCann、Froustey 和 Unser 给出了另一条路:先用 FBP 粗重建,再让一个 U-Net 把条纹去掉。这就是 FBPConvNet。方法简单到一句话就能说完,却成了后来深度学习 CT 重建论文里最常用的对比基线之一。

这篇按"它解决什么问题 → 为什么这样设计 → 代码里怎么实现 → 实验说明了什么 → 读的时候要注意什么"的顺序拆开讲。文中的代码全部摘自作者开源仓库,数字全部来自论文原文。

  • 论文:K. H. Jin, M. T. McCann, E. Froustey, M. Unser. Deep Convolutional Neural Network for Inverse Problems in Imaging. IEEE Transactions on Image Processing, 2017, 26(9): 4509--4522(arXiv:1611.03679)
  • 代码:https://github.com/panakino/FBPConvNet(MATLAB + MatConvNet)

一、要解决的问题:稀疏视角

论文的实验场景是稀疏视角 CT:投影角度从上千个减到几十、一百多个。角度少了,FBP 会产生大量条纹伪影;正则化迭代方法(比如 TV)能压住条纹,但有两个现实问题:

  1. 慢 。每一步迭代都要计算一次正投影和反投影(或 H ∗ H H^*H H∗H)。论文里的 TV 方法重建一张 512 × 512 512 \times 512 512×512 的图约需 7 分钟,这还是在正则参数已经选好之后。
  2. 难调。正则权重要手工挑,换一批数据可能就要重调。

FBPConvNet 想要的是:重建质量接近甚至超过迭代方法,速度接近 FBP。

二、核心观察:迭代算法本来就长得像 CNN

这篇论文最有意思的部分不是网络,而是它给"为什么用 CNN"找的理由。

第一步:法算子是卷积。 设前向算子为 H H H,伴随为 H ∗ H^* H∗。论文证明了一类算子(满足一定分解条件)的法算子 H ∗ H H^*H H∗H 是卷积。CT 的 Radon 变换就属于这一类:由中心切片定理, R ∗ R R^*R R∗R 是一个卷积,频域响应为

h ^ ( ω ) = 1 ∥ ω ∥ \hat{h}(\boldsymbol{\omega}) = \frac{1}{\lVert \boldsymbol{\omega} \rVert} h^(ω)=∥ω∥1

这和(一)里讲的一致:直接反投影会让图像变模糊,模糊核在频域是 1 / ∥ ω ∥ 1/\lVert \boldsymbol{\omega} \rVert 1/∥ω∥,所以 FBP 要先乘一个 ramp 滤波器把它补回来。

第二步:迭代一步 = 卷积 + 偏置 + 逐点非线性。 以稀疏正则的经典问题为例:

arg ⁡ min ⁡ a ∥ y − H W a ∥ 2 2 + λ ∥ a ∥ 1 \arg\min_{\mathbf{a}} \lVert \mathbf{y} - \mathbf{H}\mathbf{W}\mathbf{a} \rVert_2^2 + \lambda \lVert \mathbf{a} \rVert_1 argamin∥y−HWa∥22+λ∥a∥1

W \mathbf{W} W 是小波之类的稀疏变换, x = W a \mathbf{x} = \mathbf{W}\mathbf{a} x=Wa 是图像。用 ISTA 求解,每一步是:

a k + 1 = S λ / L ( 1 L W ∗ H ∗ y + ( I − 1 L W ∗ H ∗ H W ) a k ) \mathbf{a}^{k+1} = S_{\lambda/L}\left( \frac{1}{L}\mathbf{W}^*\mathbf{H}^*\mathbf{y} + \left(\mathbf{I} - \frac{1}{L}\mathbf{W}^*\mathbf{H}^*\mathbf{H}\mathbf{W}\right)\mathbf{a}^k \right) ak+1=Sλ/L(L1W∗H∗y+(I−L1W∗H∗HW)ak)

S θ S_\theta Sθ 是软阈值, L L L 是 Lipschitz 常数。当 H ∗ H \mathbf{H}^*\mathbf{H} H∗H 和 W \mathbf{W} W 都是卷积时,括号里就是"对 a k \mathbf{a}^k ak 做一次滤波,再加一个偏置",外面套一个逐点非线性。把迭代展开,就是一层层"卷积 + 偏置 + 非线性",形式上和 CNN 一样。

第三步:所以先直接求逆,再交给 CNN。 对这类问题,总有一个快速的直接求逆(CT 里就是 FBP)。它包含了物理模型,但在问题病态时会带来伪影。剩下的去伪影,交给 CNN。

需要说明的是,作者明确表示:迭代算法只是动机,他们并不打算让网络去模仿某个展开的迭代算法,而是直接选用当时效果最好的 CNN 结构之一 U-Net。把迭代算法真正展开成网络的工作,是下一篇要讲的 Learned Primal-Dual。

三、方法:FBP + 带残差的 U-Net

1. 为什么先做 FBP

网络也可以直接从正弦图回归到图像,但那样它得自己学会极坐标到笛卡尔坐标的变换。先做 FBP,这一步就省掉了:物理模型交给 FBP,网络只需要在图像域里把"有条纹的图"映射成"没条纹的图",FBP 结果同时也是网络的一个好起点。

2. 为什么选 U-Net

论文给了三个理由:

  • 多尺度分解 。U-Net 用最大池化逐级下采样,越往中间,等效感受野越大。CT 的 H ∗ H H^*H H∗H 及其逆对应的卷积核不是紧支撑的,固定小卷积核的网络可能无法有效地对它求逆。这一点也对应迭代方法里的多分辨率小波。
  • 多通道。每层有多个特征图,对应 ISTA 里小波系数的多个子带,或 ADMM 里的多个分裂变量。
  • 残差学习 。在网络输入和输出之间加一条跳连,网络实际学的是"输出 − 输入",也就是伪影本身。论文称,这比不加跳连的同一网络有明显提升。

另外两处小改动:卷积用零填充,保证尺寸不变;最后一层换成 1 × 1 1 \times 1 1×1 卷积,把 64 个通道压成 1 个输出(原版 U-Net 做分割,输出前景、背景两个通道)。

3. 代码里的网络结构

网络定义在作者仓库 cnn_unet_init.m 的 unet 函数里。下面是这个函数构建网络的全部语句,原样摘录,未做删改:

matlab 复制代码
net.layers = {} ;
ch_length = opts.channel_in;
ch_length_out = opts.channel_out;
KerenlSize = 3;
zeroPad = floor(KerenlSize/2);

net = add_block(net, opts, '0', KerenlSize, KerenlSize, ch_length, 64, 1, zeroPad,1,1) ;
net = add_block(net, opts, '0', KerenlSize, KerenlSize, 64, 64, 1, zeroPad,1,1) ;
net = add_block(net, opts, '0', KerenlSize, KerenlSize, 64, 64, 1, zeroPad,1,1) ;
net = add_reg_toss(net, '0',1);
net = add_pool(net, opts,'0', 2, 0);


net = add_block(net, opts, '1_1', KerenlSize, KerenlSize, 64, 128, 1, zeroPad,1,1) ;
net = add_block(net, opts, '1_2', KerenlSize, KerenlSize, 128, 128, 1, zeroPad,1,1) ;
net = add_reg_toss(net, '1',2);
net = add_pool(net, opts,'1', 2, 0);


net = add_block(net, opts, '2_1', KerenlSize, KerenlSize, 128, 256, 1, zeroPad,1,1) ;
net = add_block(net, opts, '2_2', KerenlSize, KerenlSize, 256, 256, 1, zeroPad,1,1) ;
net = add_reg_toss(net,'2',3);
net = add_pool(net, opts,'2', 2, 0);


net = add_block(net, opts, '3_1', KerenlSize, KerenlSize, 256, 512, 1, zeroPad,1,1) ;
net = add_block(net, opts, '3_2', KerenlSize, KerenlSize, 512, 512, 1, zeroPad,1,1) ;
net = add_reg_toss(net,'3',4);
net = add_pool(net, opts,'3',2,0);


net = add_block(net, opts, '4_1', KerenlSize, KerenlSize, 512, 1024, 1, zeroPad,1,1) ;
net = add_block(net, opts, '4_2', KerenlSize, KerenlSize, 1024, 1024, 1, zeroPad,1,1) ;
net = add_block_convt(net, opts, '4_3', KerenlSize, KerenlSize, 1024, 512, 2, [0 1 0 1],1,1) ;

net = add_reg_concat(net, '5_0',4);
net = add_block(net, opts, '5_1', KerenlSize, KerenlSize, 1024, 512, 1, zeroPad,1,1) ;
net = add_block(net, opts, '5_2', KerenlSize, KerenlSize, 512, 512, 1, zeroPad,1,1) ;
net = add_block_convt(net, opts, '5_3', KerenlSize, KerenlSize, 512, 256, 2,  [0 1 0 1],1,1) ;


net = add_reg_concat(net, '6_0',3);
net = add_block(net, opts, '6_1', KerenlSize, KerenlSize, 512, 256, 1, zeroPad,1,1) ;
net = add_block(net, opts, '6_2', KerenlSize, KerenlSize, 256, 256, 1, zeroPad,1,1) ;
net = add_block_convt(net, opts, '6_3', KerenlSize, KerenlSize, 256, 128, 2,  [0 1 0 1],1,1) ;


net = add_reg_concat(net, '7_0',2);
net = add_block(net, opts, '7_1', KerenlSize, KerenlSize, 256, 128, 1, zeroPad,1,1) ;
net = add_block(net, opts, '7_2', KerenlSize, KerenlSize, 128, 128, 1, zeroPad,1,1) ;
net = add_block_convt(net, opts, '7_3', KerenlSize, KerenlSize, 128, 64, 2,  [0 1 0 1],1,1) ;

net = add_reg_concat(net, '8_0',1);
net = add_block(net, opts, '8_1', KerenlSize, KerenlSize, 128, 64, 1, zeroPad,1,1) ;
net = add_block(net, opts, '8_2', KerenlSize, KerenlSize, 64, 64, 1, zeroPad,1,1) ;
net = add_block(net, opts, '8_3', 1, 1, 64, ch_length_out, 1, 0,0,0) ;

对照着读(各函数的含义以同一文件和 vl_simplenn_fbpconvnet.m 中的实现为准):

  • add_block 依次加入卷积层、BN 层、ReLU 层,最后两个参数(batchOn、ReluOn)控制是否加 BN 和 ReLU;这里卷积核大小 KerenlSize = 3,零填充 1,所以尺寸不变;
  • 编码器第一级有 3 个卷积块(1→64→64→64),之后每级 2 个卷积块,通道数依次为 128、256、512、1024;
  • add_pool(..., 2, 0) 是步长 2 的 2 × 2 2 \times 2 2×2 最大池化,共 4 次下采样;
  • add_block_convt 是步长 2 的 3 × 3 3 \times 3 3×3 转置卷积(同样带 BN 和 ReLU),共 4 次上采样,每次通道数减半;
  • add_reg_toss 把当前特征图存进编号为 1~4 的寄存位,add_reg_concat 在解码器对应层级把它取出来,按通道和当前特征图拼接,这就是 U-Net 的跳连;
  • 最后一层 '8_3' 是 1 × 1 1 \times 1 1×1 卷积,64 通道压到 ch_length_out(main.m 中设为 1),最后两个参数是 0,0,不加 BN 和 ReLU。

一个小差异:论文附录伪代码写的是 B ( R ( ⋅ ) ) B(R(\cdot)) B(R(⋅)),即先 ReLU 再 BN;而代码里 add_block 的层顺序是卷积 → BN → ReLU。复现时以代码为准。

4. 残差和损失在代码里是怎么做的

论文说"在输入和输出之间加跳连",但上面的网络定义里并没有这条跳连。它在数据和评估两头实现。

训练时,cnn_fbpconvnet.m 里取数据的函数 getSimpleNNBatch 最后两行把标签换成了"标签 − 输入"。按 main.m 的设置,输入 imdb.images.noisy 是稀疏视角 FBP(lab_d),标签 imdb.images.orig 是全视角 FBP(lab_n):

matlab 复制代码
lowRes = images(:,:,1,:);
labels(:,:,1,:) = labels(:,:,1,:) - lowRes;

评估时,evaluation.m 再把输入加回去:

matlab 复制代码
    gt=lab_n(:,:,1,iter);
    m=lab_d(:,:,1,iter);
    if strcmp(cmode,'gpu')
        res=vl_simplenn_fbpconvnet_eval(net,gpuArray((single(m))));
        rec=gather(res(end-1).x)+m;
    else
        res=vl_simplenn_fbpconvnet_eval(net,((single(m))));
        rec=(res(end-1).x)+m;
    end

m 是稀疏视角 FBP,gt 是全视角 FBP。MatConvNet 里 res(i+1).x 是第 i 层的输出;网络最后一层是 euclideanloss 损失层,所以 res(end-1).x 是它前面那个 1 × 1 1 \times 1 1×1 卷积层的输出,也就是网络预测的残差。输出加上 m 才是最终重建,对应论文附录伪代码最后一行 X ∗ ← X d + ( w ⋆ O 1 + b ) \mathbf{X}^* \leftarrow \mathbf{X}_d + (\mathbf{w} \star \mathbf{O}_1 + \mathbf{b}) X∗←Xd+(w⋆O1+b)。

损失层的实现在 vl_euclideanloss.m:

matlab 复制代码
  if nargin == 2 || (nargin == 3 && isempty(dzdy))
    Y = sqrt(sum(subsref((X - c) .^ 2, substruct('()', {':'}))));
  elseif nargin == 3 && ~isempty(dzdy)
    assert(numel(dzdy) == 1);
    Y = dzdy * (X - c);
  end

前向返回的是 ∥ X − c ∥ 2 \lVert X - c \rVert_2 ∥X−c∥2,反向传给上一层的梯度是 X − c X - c X−c。按反向传播的梯度看,实际优化的是平方误差 1 2 ∥ X − c ∥ 2 2 \frac{1}{2}\lVert X - c \rVert_2^2 21∥X−c∥22(对像素求和,不是求平均)。这里 X X X 是网络输出, c c c 是残差标签。

5. 训练设置:论文与代码对照

项目 论文 作者代码(main.m 等)
训练 / 测试 475 / 25 张 共 500 张,最后 5%(25 张)为测试
输入尺寸 512 × 512 512 \times 512 512×512 整图 patchSize = 512
batch size 1 1
动量 0.99 0.99
学习率 0.01 对数递减到 0.001 logspace(-2, -3, 20),第 20 轮后保持 0.001
截断 梯度截断,阈值 10 − 2 10^{-2} 10−2 gradMax = 1e-2,作用在每一步的参数更新量上
训练轮数 101 轮,约 15 小时(Titan Black) numEpochs = 151,提供的预训练模型为 net-epoch-151
数据增强 水平、垂直翻转 各以 0.5 概率翻转
图像范围 缩放到 0~550 ---

两处值得注意:一是代码里的"梯度截断"实际是对 学习率 × 动量项 逐元素截断到 − 10 − 2 , 10 − 2 -10\^{-2}, 10\^{-2} −10−2,10−2,也就是限制每一步参数的改变量;二是仓库只提供了椭圆数据集 ×20 的数据,README 也说明 SNR 可能与论文略有不同。

四、实验

1. 数据

数据集 内容 全视角正弦图 稀疏采样
椭圆 500 张随机椭圆,解析投影 729 像素 × 1000 角度 ×7(143 角度)、×20(50 角度)
生物医学 500 张 Mayo 低剂量挑战赛的 CT 图像,用 radon 生成投影 729 像素 × 1000 角度 ×7、×20
实验数据 瑞士光源 TOMCAT 线站的 377 张大鼠脑真实正弦图 1493 像素 × 721 角度 ×5(145 角度)、×14(52 角度)

生物医学数据的测试集来自与训练集不同的受试者;实验数据的测试集取最后 25 层,与训练集之间隔开 25 层。

参考图像是全视角 FBP,不是体模真值。 作者的理由是:真实数据永远拿不到真值,这样合成数据和真实数据可以用同一套流程处理。

2. 指标:不是 PSNR

论文用的是一种"回归 SNR":

SNR = max ⁡ a , b ∈ R 20 log ⁡ 10 ∥ x ∥ 2 ∥ x − a x ^ + b ∥ 2 \text{SNR} = \max_{a, b \in \mathbb{R}} 20 \log_{10} \frac{\lVert \mathbf{x} \rVert_2}{\lVert \mathbf{x} - a\hat{\mathbf{x}} + b \rVert_2} SNR=a,b∈Rmax20log10∥x−ax^+b∥2∥x∥2

先对重建结果做一个最优的线性变换 a x ^ + b a\hat{\mathbf{x}} + b ax^+b(最小二乘拟合),再算 SNR。代码 computeRegressedSNR.m 就是这样做的:

matlab 复制代码
A       =        [sumI2, sumI; sumI, numel(oracle)];
b       =        [sumIP;sumP]             ;
c       =        (A)\b                    ;
rec     =        c(1)*rec+c(2)            ;
err     =        sum((oracle(:)-rec(:)).^2)      ;
SNR     =        10*log10(sum(oracle(:).^2)/err) ;

它和 PSNR 有两点不同:分子是参考图像的能量而不是峰值的平方;而且允许整体缩放和平移,所以对亮度、对比度的整体偏差不敏感。上一篇讲过,PSNR、SSIM 本身就依赖 data_range;这里的 SNR 更是另一套定义,不能和其他论文的 PSNR 直接比。

3. 主要结果(平均 SNR,dB)

数据集 采样 FBP TV FBPConvNet
椭圆 143 角度(×7) 16.09 29.48 28.96
椭圆 50 角度(×20) 8.44 27.69 23.84
生物医学 143 角度(×7) 24.97 31.92 36.15
生物医学 50 角度(×20) 13.52 25.2 28.83
实验数据 145 角度(×5) 5.38 8.25 11.34
实验数据 52 角度(×14) 3.29 7.25 8.85

(实验数据 52 角度一行,论文表 III 写作"51 views (x14)",正文数据准备部分写的是 52,这里按正文。)

结论要分开看:

  • 椭圆数据上 TV 更好,×20 时领先近 4 dB。作者的解释是:椭圆体模是分片常数,正好是 TV 最擅长的情形。
  • 生物医学和真实数据上 FBPConvNet 更好。作者认为增益来自对细节的保留:TV 会出现典型的"卡通化",FBPConvNet 能保留更多纹理。用他们的话说,迭代方法要显式指定正则项,而 FBPConvNet 从数据里学了一个正则项。
  • 速度:FBP 约 200 ms,CNN 在 GPU 上约 200~300 ms,合计不到 1 秒;TV 约 7 分钟。

4. 消融:每个设计都有用吗

TIP 版本补了一组结构对比(生物医学数据,平均 SNR,dB):

采样 残差网络(无多尺度) 原版 U-Net(无残差) 在正弦图域补全 FBPConvNet
143 角度 34.18 28.65 34.21 35.28
50 角度 25.79 18.10 22.98 27.71
  • 去掉残差,下降最多:50 角度时从 27.71 掉到 18.10;
  • 去掉多尺度,也有 1~2 dB 的损失;
  • 把网络放在正弦图域、FBP 之前做,不如放在图像域。

注意这组实验因为显存限制,把全视角正弦图减到了 721 个角度,所以数字和上一张表不能直接对比。表头里的"×5"和"×14"也是相对 721 个角度说的(721/143 ≈ 5,721/50 ≈ 14)。

5. 泛化与稳定性

  • 换采样率会掉性能。用 143 角度训练的网络去处理 50 角度的输入,结果会残留大量条纹,要重新训练;论文在讨论部分把这列为主要局限。TIP 版本补充了实验:测试条件偏离训练条件时性能下降,但仍明显优于 FBP,用新数据再微调 10 轮(约 1.5 小时)能缓解。
  • 对噪声有一定容忍度。训练数据不加噪声,测试时在正弦图上加高斯白噪声:正弦图 SNR 降到 50 dB 以下之前,重建性能基本不变,之后逐渐下降。
  • 迁移到 MRI。作为概念验证,作者把同一网络用于 6 倍欠采样的膝关节 MRI(取模值处理复数),输入换成零填充逆傅里叶变换的结果,称为 BPConvNet。

五、读这篇论文时要注意的几点

FBPConvNet 的思路很清楚,但它的实验设置和今天的论文差别不小,引用或复现时要留意:

  1. 参考图像是全视角 FBP。 网络学的是"稀疏视角 FBP → 全视角 FBP",它的上限就是全视角 FBP,而全视角 FBP 自身也有离散化误差。今天很多论文用体模真值或正常剂量重建作参考,设置不同。
  2. TV 的参数是在测试集上调的。 TIP 版本写明,TV 的参数通过黄金分割搜索在测试集上取最优(arXiv v1 写的是训练集)。这对 TV 是有利的。所以 FBPConvNet 在两个更真实的数据集上胜出,结论更站得住;而比较两者差距时,也要把这一点考虑进去。
  3. "视角减少 20 倍 = 剂量减少 20 倍"有前提。 这要求每个角度的剂量不变。而论文没有提到在合成正弦图里加入噪声;真实场景下每个角度的光子数也会变少,噪声问题是另一回事(参见上一篇)。
  4. 指标是回归 SNR。 允许线性校正之后再算,不是 PSNR,也没有报 SSIM。
  5. 每个采样率单独训练一个网络。 这是后处理类方法的通病,后来的展开网络、扩散模型等方法,很大一部分动机就是把物理模型重新放回网络里,减轻这个问题。

六、它为什么重要

FBPConvNet 不是第一篇把 CNN 用在 CT 上的论文,作者在相关工作里也提到了同期的低剂量 CT 后处理工作,以及 Han 等人在其投稿后不久独立提出的稀疏视角多分辨率残差网络。它的影响力主要来自三点:

  • 给了一个清楚的理论动机:法算子是卷积的逆问题,迭代求解的结构和 CNN 一致;
  • 给了一个极简、好复现的方案:任何能做 FBP 的人都能接上一个 U-Net;
  • 代码、预训练模型和部分数据开源,论文也获得了 IEEE 信号处理学会最佳论文奖。

所以后来的方法,无论是展开网络、双域网络还是扩散模型,经常把"FBP + U-Net"列为基线。它也划出了一条清晰的分界:FBPConvNet 只在网络之前用了一次物理模型;之后的很多工作,都在想办法让网络在每一步都用上物理模型。

这些方向的代表论文、开源代码和数据集,我整理在了一个开源列表里,可以按方向筛选、支持中文搜索:

参考文献

  1. Jin K H, McCann M T, Froustey E, Unser M. Deep convolutional neural network for inverse problems in imaging. IEEE Transactions on Image Processing, 2017, 26(9): 4509--4522.
  2. Ronneberger O, Fischer P, Brox T. U-Net: Convolutional networks for biomedical image segmentation. MICCAI, 2015: 234--241.

下一篇精读 Learned Primal-Dual:它不再"先 FBP 再去伪影",而是把原始--对偶迭代算法展开成网络,让正投影和反投影出现在网络的每一层里。

如果这篇对你有帮助,欢迎收藏,有问题可以在评论区讨论。

相关推荐
墨子夕1 小时前
新造的人(卷一 烟火人间)
人工智能·程序人生·未来生活
qytklt1 小时前
GEO生成式引擎优化技术拆解:从大模型检索原理到AI搜索推荐实现路径
人工智能·geo生成式引擎优化
Elastic 中国社区官方博客1 小时前
Jina AI:Elasticsearch 集成
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索·jina
3D阅阅三维模型轻量化及浏览平台1 小时前
上传 3D 模型自动识别、一键报价,拆解 3D 阅阅报价底层逻辑(机加工 / 钣金)
人工智能·3d·机加工·钣金加工·模具加工
workflower1 小时前
超算中心聚焦高性能计算场景(HPC)
大数据·网络·人工智能·机器人·云计算·汽车
roman_日积跬步-终至千里1 小时前
【数据Agent论文·分析与报告评测|27】ComInsight:原子洞察正确,组合后的结论就可靠吗?
人工智能·算法·机器学习·数据分析
2601_965619691 小时前
AI视频制作:财经数据可视化视频怎么做:AI 自动生成图表动画的实操流程
人工智能·nginx·音视频
结局无敌1 小时前
React Native for OpenHarmony 实战:三方库 react-native-progress-steps 的鸿蒙化适配指南
javascript·人工智能·深度学习·react native·react.js·harmonyos