CT 重建从零开始(四):低剂量 CT 的噪声模型与 PSNR / SSIM

几乎每篇低剂量 CT 去噪论文都会给一张表:剂量多少、PSNR 多少、SSIM 多少。但这两个问题很少有人讲透:低剂量的噪声到底长什么样?PSNR 和 SSIM 到底是怎么算出来的?

本篇把这两件事讲清楚。结论先放这里:噪声要加在投影域,而且是 Poisson 噪声;PSNR 和 SSIM 的数值强烈依赖 data_range 和图像的取值范围,不交代清楚就没法比较。文中所有数字都是实测结果,代码在第五节,可以直接复现。

一、噪声从哪里来

(一)里讲过,探测器测到的是光子数,取对数之后才是线积分。由 Beer--Lambert 定律,沿一条射线:

I ˉ = I 0   e − p \bar{I} = I_0\, e^{-p} Iˉ=I0e−p

I 0 I_0 I0 是入射光子数(无衰减时探测器的计数), p p p 是这条射线上衰减系数的线积分, I ˉ \bar{I} Iˉ 是期望计数。

光子到达探测器是一个计数过程,实测计数服从 Poisson 分布:

I ∼ Poisson ( I 0   e − p ) I \sim \text{Poisson}\left(I_0\, e^{-p}\right) I∼Poisson(I0e−p)

真实探测器还有电子噪声,通常建模为再加一项高斯噪声: I ∼ Poisson ( I 0 e − p ) + N ( 0 , σ e 2 ) I \sim \text{Poisson}(I_0 e^{-p}) + \mathcal{N}(0, \sigma_e^2) I∼Poisson(I0e−p)+N(0,σe2)。光子数很少时电子噪声才变得显著,本篇的实验只考虑 Poisson 部分。

重建用的是取对数后的估计:

p ^ = − ln ⁡ I I 0 \hat{p} = -\ln \frac{I}{I_0} p^=−lnI0I

二、噪声有多大: Var ( p ^ ) ≈ e p / I 0 \text{Var}(\hat{p}) \approx e^{p}/I_0 Var(p^)≈ep/I0

对 p ^ \hat{p} p^ 做一阶泰勒展开(delta 方法),Poisson 分布的方差等于均值,得到:

Var ( p ^ ) ≈ Var ( I ) I ˉ 2 = 1 I ˉ = e p I 0 \text{Var}(\hat{p}) \approx \frac{\text{Var}(I)}{\bar{I}^2} = \frac{1}{\bar{I}} = \frac{e^{p}}{I_0} Var(p^)≈Iˉ2Var(I)=Iˉ1=I0ep

这个式子有三个直接推论:

  1. 噪声不是均匀的。 p p p 越大(射线穿过的组织越厚、越致密),透射光子越少,噪声越大。实验里( I 0 = 10 4 I_0 = 10^4 I0=104),线积分小于 0.2 的射线上 p ^ \hat{p} p^ 的标准差约 0.010,最长的那批射线上约 0.203,差了 20 倍。所以"在图像上加均匀高斯噪声"模拟低剂量,是不对的。
  2. 噪声与剂量的关系是平方根。 剂量正比于 I 0 I_0 I0(管电流 × 时间,即 mAs)。FBP 是线性运算,投影域方差 ∝ 1 / I 0 \propto 1/I_0 ∝1/I0,图像噪声方差也 ∝ 1 / I 0 \propto 1/I_0 ∝1/I0,即图像噪声标准差 ∝ 1 / 剂量 \propto 1/\sqrt{\text{剂量}} ∝1/剂量。剂量降到 1/4,噪声标准差翻倍。
  3. 光子极少时公式失效。 计数可能为 0, ln ⁡ 0 \ln 0 ln0 无定义,实践中要截断或做专门处理;此时近似也不再准确,表现为条纹状的光子饥饿伪影。

用 200 次独立仿真验证第一条: I 0 = 10 4 I_0 = 10^4 I0=104 时, p > 0.5 p > 0.5 p>0.5 的射线上,实测方差与 e p / I 0 e^p/I_0 ep/I0 之比的中位数为 1.002。

三、PSNR:先说清楚 MAX 是多少

PSNR 定义为:

PSNR = 10 log ⁡ 10 MAX 2 MSE , MSE = 1 n ∑ j ( x j − y j ) 2 \text{PSNR} = 10 \log_{10} \frac{\text{MAX}^2}{\text{MSE}}, \quad \text{MSE} = \frac{1}{n}\sum_{j}(x_j - y_j)^2 PSNR=10log10MSEMAX2,MSE=n1j∑(xj−yj)2

x x x 是重建结果, y y y 是参考图像, MAX \text{MAX} MAX 是图像取值的动态范围,在 scikit-image 里叫 data_range。

公式很简单,坑全在 MAX \text{MAX} MAX:

  • MSE 不变, MAX \text{MAX} MAX 一变,PSNR 就变。 把 MAX 从 2000 换成 4096,PSNR 会多出 20 log ⁡ 10 ( 4096 / 2000 ) ≈ 6.23 20\log_{10}(4096/2000) \approx 6.23 20log10(4096/2000)≈6.23 dB,图像本身一点没变。
  • CT 图像常用 HU 表示 ,各论文取的范围不同:有的用 − 1000 , 1000 -1000, 1000 −1000,1000,有的用 12 位范围 4096,有的先归一化到 0 , 1 0, 1 0,1,有的只在一个显示窗内算。这些设置下的 PSNR 不能直接比。
  • 截断(clip)也会改变结果。 重建结果可能有负值或越界值,算指标前截不截断、截到哪里,都要写清楚。
  • 不同版本的库默认值可能不同 ,务必显式传入 data_range,不要依赖默认值。

四、SSIM:不只看误差,还看结构

PSNR 只看逐像素误差,对"误差长什么样"不敏感:同样的 MSE,均匀的细噪声和一块被抹平的病灶,PSNR 可以一样。SSIM(Wang 等,2004)试图更接近人眼感知,在每个局部窗口里比较亮度、对比度和结构三项:

SSIM ( x , y ) = ( 2 μ x μ y + C 1 ) ( 2 σ x y + C 2 ) ( μ x 2 + μ y 2 + C 1 ) ( σ x 2 + σ y 2 + C 2 ) \text{SSIM}(x, y) = \frac{(2\mu_x\mu_y + C_1)(2\sigma_{xy} + C_2)}{(\mu_x^2 + \mu_y^2 + C_1)(\sigma_x^2 + \sigma_y^2 + C_2)} SSIM(x,y)=(μx2+μy2+C1)(σx2+σy2+C2)(2μxμy+C1)(2σxy+C2)

μ \mu μ、 σ 2 \sigma^2 σ2、 σ x y \sigma_{xy} σxy 分别是窗口内的均值、方差和协方差, C 1 = ( K 1 L ) 2 C_1 = (K_1 L)^2 C1=(K1L)2、 C 2 = ( K 2 L ) 2 C_2 = (K_2 L)^2 C2=(K2L)2 是防止分母为零的常数, L L L 就是 data_range,通常 K 1 = 0.01 K_1 = 0.01 K1=0.01、 K 2 = 0.03 K_2 = 0.03 K2=0.03。逐窗口计算后取平均,就是整幅图的 SSIM。

SSIM 的坑比 PSNR 更多:

  • 窗口设置不同,数值不同。 原论文用 11 × 11 11 \times 11 11×11、 σ = 1.5 \sigma = 1.5 σ=1.5 的高斯窗;scikit-image 的 structural_similarity 默认用 7 × 7 7 \times 7 7×7 均匀窗。要复现原论文设置,需要传 gaussian_weights=True, sigma=1.5, use_sample_covariance=False。本实验里两种设置差别不大(如 I 0 = 10 4 I_0 = 10^4 I0=104 时为 0.5569 与 0.5519),但不能假定总是如此。
  • L L L 不只出现在常数里。 C 1 C_1 C1、 C 2 C_2 C2 都随 L L L 变化,所以 data_range 设错,SSIM 同样会变。实验里同一张图, L L L 从 2000 改成 4096,SSIM 从 0.6892 升到 0.8583。
  • SSIM 不具有平移不变性。 亮度项 2 μ x μ y + C 1 μ x 2 + μ y 2 + C 1 \frac{2\mu_x\mu_y + C_1}{\mu_x^2 + \mu_y^2 + C_1} μx2+μy2+C12μxμy+C1 依赖绝对均值。同一张图从 0 , 1 0, 1 0,1 线性映射到 − 1000 , 1000 -1000, 1000 −1000,1000 HU,并把 data_range 同比放大,PSNR 不变(都是 27.66 dB),SSIM 却从 0.5569 变成 0.6892。

所以一篇论文报 SSIM,至少要交代三件事:图像的取值范围(或 HU 窗)、data_range、窗口设置。

五、实验:剂量、噪声与指标

设置与前几篇一致: 256 × 256 256 \times 256 256×256 的 Shepp--Logan 体模,取值 0 , 1 0, 1 0,1;180 个角度的平行束,FBP(ramp 滤波)重建。为了让透射率处在合理范围,仿真时把衰减系数缩小 10 倍,最长射线的线积分约 6.6,对应透射率约 0.1%。噪声在投影域按 Poisson 分布加入,指标在 0 , 1 0, 1 0,1 上截断后计算,data_range=1。

python 复制代码
import numpy as np
from skimage.data import shepp_logan_phantom
from skimage.transform import radon, iradon, rescale
from skimage.metrics import peak_signal_noise_ratio as psnr
from skimage.metrics import structural_similarity as ssim

img = rescale(shepp_logan_phantom(), 0.64)           # 256x256,取值 [0, 1],与前几篇相同
theta = np.linspace(0., 180., 180, endpoint=False)   # 180 个角度
p = radon(img / 10, theta=theta)                     # 无噪声线积分(衰减系数缩小 10 倍)

def simulate(I0, seed=0):
    """在投影域加 Poisson 噪声:光子计数 -> 取对数"""
    rng = np.random.default_rng(seed)
    counts = rng.poisson(I0 * np.exp(-p))
    return -np.log(np.maximum(counts, 1) / I0)       # 计数为 0 时截到 1,避免 log(0)

def recon(sino):
    return iradon(sino * 10, theta=theta)            # 乘回 10,回到 [0, 1] 的尺度

def metrics(x):
    x = np.clip(x, 0, 1)
    return round(float(psnr(img, x, data_range=1)), 2), round(float(ssim(img, x, data_range=1)), 4)

# 1. 验证 Var(p_hat) ≈ e^p / I0
I0 = 1e4
reps = np.stack([simulate(I0, s) for s in range(200)])
m = p > 0.5
print("实测/理论方差:", round(float(np.median(reps.var(0)[m] / (np.exp(p[m]) / I0))), 3))

# 2. 不同剂量下的 FBP
clean = recon(p)
print("无噪声", metrics(clean))
for I0 in [1e6, 1e5, 4e4, 1e4, 2.5e3]:
    rec = recon(simulate(I0))
    print(f"I0={I0:g}", metrics(rec), "噪声标准差", round(float(np.std(rec - clean)), 4))

# 3. data_range 陷阱:同一张图,换个 data_range
rec = np.clip(recon(simulate(1e4)), 0, 1)
hu = lambda x: 2000 * x - 1000                       # 线性映射到 [-1000, 1000] HU
print("data_range=2000:", round(psnr(hu(img), hu(rec), data_range=2000), 2),
      round(ssim(hu(img), hu(rec), data_range=2000), 4))
print("data_range=4096:", round(psnr(hu(img), hu(rec), data_range=4096), 2),
      round(ssim(hu(img), hu(rec), data_range=4096), 4))

运行结果整理如下("噪声标准差"指有噪声重建与无噪声重建之差的标准差):

I 0 I_0 I0 相对剂量 PSNR (dB) SSIM 噪声标准差
无噪声 --- 30.91 0.8989 0
10 6 10^6 106 100 30.86 0.8900 0.0033
10 5 10^5 105 10 30.47 0.8256 0.0104
4 × 10 4 4 \times 10^4 4×104 4 29.87 0.7444 0.0165
10 4 10^4 104 1 27.66 0.5569 0.0331
2.5 × 10 3 2.5 \times 10^3 2.5×103 1/4 23.60 0.3873 0.0672

I 0 = 10 4 I_0 = 10^4 I0=104 的 27.66 dB 与(一)中 Ramp 滤波加噪声的结果一致。

从表里能读出三件事:

1. 平方根规律成立。 剂量从 4 降到 1,噪声标准差从 0.0165 变成 0.0331(×2.01);从 1 降到 1/4,变成 0.0672(×2.03);剂量从 10 降到 1,×3.18,接近 10 ≈ 3.16 \sqrt{10} \approx 3.16 10 ≈3.16。

2. PSNR 有"地板"。 无噪声时 PSNR 也只有 30.91 dB,这是 FBP 本身的离散化误差和条纹。剂量很高时噪声几乎不影响 PSNR( 10 6 10^6 106 时只掉 0.05 dB),剂量越低,噪声才逐渐成为主导。所以剂量减半,PSNR 并不会固定掉多少 dB,取决于噪声和其他误差谁大。

3. SSIM 对噪声更敏感。 10 5 10^5 105 时 PSNR 只降了 0.44 dB,SSIM 已从 0.8989 降到 0.8256。原因是 Shepp--Logan 体模大面积是均匀区域,噪声直接破坏了局部结构的一致性。

最后一组是 data_range 陷阱,同一张 I 0 = 10 4 I_0 = 10^4 I0=104 的重建图:

取值范围 data_range PSNR (dB) SSIM
0 , 1 0, 1 0,1 1 27.66 0.5569
− 1000 , 1000 -1000, 1000 −1000,1000 HU 2000 27.66 0.6892
− 1000 , 1000 -1000, 1000 −1000,1000 HU 4096 33.89 0.8583

图像完全相同,PSNR 能差 6 dB,SSIM 能从 0.56 变到 0.86。这比很多论文报告的"方法提升"还大。

六、这组实验要怎么看

  • 体模与真实数据差别很大。 Shepp--Logan 结构简单、背景为零,真实 CT 有纹理和复杂的噪声相关性,数字只用来说明规律,不代表临床图像的指标水平。
  • 只模拟了 Poisson 噪声。 没有电子噪声、散射、射束硬化和探测器串扰,真实低剂量数据会更复杂。
  • 单次仿真。 表中指标来自固定种子的一次仿真(方差验证除外),换种子会有小幅波动。
  • PSNR / SSIM 不等于诊断质量。 两者都不能完全反映低对比度病灶是否可见,临床评估还会用读片实验或基于任务的指标。

七、读论文时该看什么

带着本篇的结论去看低剂量去噪论文,可以先检查几件事:

  1. 低剂量数据是怎么来的:真实采集、厂商工具在投影域插入噪声,还是在图像上加噪声?最后一种与真实噪声差别最大。
  2. 剂量水平是多少 :比如 Mayo 数据集的四分之一剂量、LoDoPaB-CT 的 N 0 = 4096 N_0 = 4096 N0=4096,不同剂量的结果不能直接比。
  3. 指标怎么算 :HU 范围或显示窗、data_range、是否截断、SSIM 窗口设置。缺少这些信息的数字,复现时要格外小心。

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

参考文献

  1. Wang Z, Bovik A C, Sheikh H R, Simoncelli E P. Image quality assessment: from error visibility to structural similarity. IEEE Transactions on Image Processing, 2004, 13(4): 600--612.
  2. 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.

下一篇进入论文精读:FBPConvNet。它把 FBP 和 U-Net 拼在一起,是深度学习 CT 重建最常被引用的基线之一。它的主实验是稀疏视角,但后来大量低剂量去噪工作都拿它当对照,评价也用 PSNR / SSIM。有了本篇的噪声模型和指标,就能看懂这些对比到底比了什么。

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

相关推荐
闭包不眠2 小时前
手机拍的证件照怎么压缩到20k?3种方法(2026)
图像处理·人工智能·计算机视觉·智能手机·html
鬼手点金3 小时前
上下文:Agent 的眼睛
java·前端·人工智能·git·深度学习
我有酒两杯3 小时前
从一条样本看懂 Kev:数据、模型、训练与推理
人工智能·深度学习·机器学习·lora·大语言模型
小何好运暴富开心幸福4 小时前
LangChain 与 LangGraph 核心要点总结
深度学习·语言模型·langchain·知识图谱
roman_日积跬步-终至千里4 小时前
【数据Agent论文·表格事实与生成|15】LogicNLG:数值抄对了,逻辑结论就对了吗?
人工智能·深度学习·自然语言处理·数据分析
啥都鼓捣的小yao4 小时前
Transformer深入浅出
人工智能·深度学习·transformer
RobinDevNotes5 小时前
SGLang 上手:重复的前缀只算一次
人工智能·python·深度学习·sglang·c4python
CoderIsArt5 小时前
使用 Ultralytics YOLO 进行模型训练
人工智能·深度学习·yolo
梦仔生信进阶6 小时前
【机器学习】ACC = 1 是否意味着过拟合?还是数据泄露?一个好的模型应该是什么样的
人工智能·深度学习·机器学习