几乎每篇低剂量 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
这个式子有三个直接推论:
- 噪声不是均匀的。 p p p 越大(射线穿过的组织越厚、越致密),透射光子越少,噪声越大。实验里( I 0 = 10 4 I_0 = 10^4 I0=104),线积分小于 0.2 的射线上 p ^ \hat{p} p^ 的标准差约 0.010,最长的那批射线上约 0.203,差了 20 倍。所以"在图像上加均匀高斯噪声"模拟低剂量,是不对的。
- 噪声与剂量的关系是平方根。 剂量正比于 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,噪声标准差翻倍。
- 光子极少时公式失效。 计数可能为 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 不等于诊断质量。 两者都不能完全反映低对比度病灶是否可见,临床评估还会用读片实验或基于任务的指标。
七、读论文时该看什么
带着本篇的结论去看低剂量去噪论文,可以先检查几件事:
- 低剂量数据是怎么来的:真实采集、厂商工具在投影域插入噪声,还是在图像上加噪声?最后一种与真实噪声差别最大。
- 剂量水平是多少 :比如 Mayo 数据集的四分之一剂量、LoDoPaB-CT 的 N 0 = 4096 N_0 = 4096 N0=4096,不同剂量的结果不能直接比。
- 指标怎么算 :HU 范围或显示窗、
data_range、是否截断、SSIM 窗口设置。缺少这些信息的数字,复现时要格外小心。
这些方向的代表论文、开源代码和数据集,我整理在了一个开源列表里,可以按方向筛选、支持中文搜索:
参考文献
- 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.
- 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。有了本篇的噪声模型和指标,就能看懂这些对比到底比了什么。
如果这篇对你有帮助,欢迎收藏,有问题可以在评论区讨论。