Pytorch图像去噪实战(五):FFDNet可控图像去噪实战,用噪声强度图解决不同噪声等级问题

Pytorch图像去噪实战(五):FFDNet可控图像去噪实战,用噪声强度图解决不同噪声等级问题


一、问题场景:同一个模型面对不同噪声强度时效果不稳定

前面几篇我们做了 DnCNN、UNet、ResUNet 和 Attention UNet。

这些模型在固定噪声强度下效果不错,但在真实项目中,我遇到了一个很麻烦的问题:

用户上传的图片噪声强度不一致,同一个模型无法稳定处理所有情况。

比如:

  • 有些图只是轻微压缩噪声
  • 有些图是低光高ISO噪声
  • 有些图是截图二次压缩噪声
  • 有些图是扫描件颗粒噪声

如果模型训练时只见过 sigma=25,那么面对 sigma=10 时可能过度去噪,面对 sigma=50 时又去不干净。

我一开始的做法是训练多个模型:

  • sigma=15 一个模型
  • sigma=25 一个模型
  • sigma=50 一个模型

但工程上非常麻烦:

  • 模型多
  • 推理复杂
  • 部署成本高
  • 噪声估计不准时效果会崩

因此这一篇我们做一个更工程化的方案:FFDNet


二、FFDNet解决什么问题?

FFDNet的核心思想是:

不只输入带噪图像,还额外输入一张噪声强度图。

也就是说,模型输入不再是:

text 复制代码
noisy_image

而是:

text 复制代码
noisy_image + noise_level_map

这样模型就知道当前图片大概有多脏,从而控制去噪力度。


三、为什么噪声强度图很有用?

普通模型的问题是:模型自己猜噪声强度。

FFDNet的问题建模方式是:我们直接告诉模型噪声强度。

比如:

  • sigma=15:轻度去噪
  • sigma=25:中度去噪
  • sigma=50:强力去噪

这在工程中非常实用,因为我们可以根据业务场景动态调整去噪强度。


四、工程目录结构

复制代码
ffdnet_denoise/
├── data/
│   ├── train/
│   └── val/
├── models/
│   └── ffdnet.py
├── dataset.py
├── train.py
├── eval.py
└── utils.py

五、数据集构建:返回噪声强度图

FFDNet训练时需要返回三个数据:

  • noisy:带噪图
  • sigma_map:噪声强度图
  • clean:干净图

dataset.py

python 复制代码
import os
import random
import torch
from PIL import Image
from torch.utils.data import Dataset
import torchvision.transforms as transforms


class FFDNetDataset(Dataset):
    def __init__(self, root_dir, patch_size=128):
        self.paths = [
            os.path.join(root_dir, name)
            for name in os.listdir(root_dir)
            if name.lower().endswith((".jpg", ".png", ".jpeg"))
        ]

        self.patch_size = patch_size
        self.to_tensor = transforms.ToTensor()

    def __len__(self):
        return len(self.paths)

    def __getitem__(self, idx):
        img = Image.open(self.paths[idx]).convert("L")

        w, h = img.size
        if w >= self.patch_size and h >= self.patch_size:
            x = random.randint(0, w - self.patch_size)
            y = random.randint(0, h - self.patch_size)
            img = img.crop((x, y, x + self.patch_size, y + self.patch_size))
        else:
            img = img.resize((self.patch_size, self.patch_size))

        clean = self.to_tensor(img)

        sigma = random.uniform(0, 50)
        sigma_value = sigma / 255.0

        noise = torch.randn_like(clean) * sigma_value
        noisy = torch.clamp(clean + noise, 0.0, 1.0)

        sigma_map = torch.ones_like(clean) * sigma_value

        return noisy, sigma_map, clean

六、FFDNet模型实现

这里我们实现一个简化版 FFDNet。

核心是把 noisy 和 sigma_map 在通道维度拼接:

python 复制代码
x = torch.cat([noisy, sigma_map], dim=1)

models/ffdnet.py

python 复制代码
import torch
import torch.nn as nn


class FFDNet(nn.Module):
    def __init__(self, in_channels=2, out_channels=1, features=64):
        super().__init__()

        layers = []

        layers.append(nn.Conv2d(in_channels, features, 3, padding=1))
        layers.append(nn.ReLU(inplace=True))

        for _ in range(10):
            layers.append(nn.Conv2d(features, features, 3, padding=1))
            layers.append(nn.BatchNorm2d(features))
            layers.append(nn.ReLU(inplace=True))

        layers.append(nn.Conv2d(features, out_channels, 3, padding=1))

        self.net = nn.Sequential(*layers)

    def forward(self, noisy, sigma_map):
        x = torch.cat([noisy, sigma_map], dim=1)
        residual = self.net(x)
        return noisy - residual

七、训练代码

train.py

python 复制代码
import torch
from torch.utils.data import DataLoader
from dataset import FFDNetDataset
from models.ffdnet import FFDNet


def train():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    dataset = FFDNetDataset("data/train")
    loader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=4)

    model = FFDNet().to(device)

    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
    criterion = torch.nn.L1Loss()

    for epoch in range(1, 51):
        model.train()
        total_loss = 0

        for noisy, sigma_map, clean in loader:
            noisy = noisy.to(device)
            sigma_map = sigma_map.to(device)
            clean = clean.to(device)

            pred = model(noisy, sigma_map)
            loss = criterion(pred, clean)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        print(f"Epoch {epoch}, Loss: {total_loss / len(loader):.6f}")

        if epoch % 10 == 0:
            torch.save(model.state_dict(), f"ffdnet_epoch_{epoch}.pth")


if __name__ == "__main__":
    train()

八、推理时如何控制去噪强度?

这正是 FFDNet 最实用的地方。

如果你觉得图片噪声轻,就给小 sigma:

python 复制代码
sigma = 15 / 255.0

如果噪声很重,就给大 sigma:

python 复制代码
sigma = 50 / 255.0

完整推理代码:

python 复制代码
import torch
from PIL import Image
import torchvision.transforms as transforms
import torchvision.utils as vutils
from models.ffdnet import FFDNet


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = FFDNet().to(device)
model.load_state_dict(torch.load("ffdnet_epoch_50.pth", map_location=device))
model.eval()

img = Image.open("test.png").convert("L")
to_tensor = transforms.ToTensor()

noisy = to_tensor(img).unsqueeze(0).to(device)

sigma = 25 / 255.0
sigma_map = torch.ones_like(noisy) * sigma

with torch.no_grad():
    pred = model(noisy, sigma_map)
    pred = torch.clamp(pred, 0.0, 1.0)

vutils.save_image(pred.cpu(), "denoised.png")

九、为什么训练时sigma要随机?

如果只训练固定 sigma,比如 25,那么模型只会处理一种噪声。

FFDNet的优势来自:

python 复制代码
sigma = random.uniform(0, 50)

这让模型在训练时见到连续噪声强度,从而学会根据 sigma_map 控制去噪力度。


十、真实工程中的使用方式

在实际系统中,可以提供三个模式:

轻度去噪

python 复制代码
sigma = 10

适合轻微压缩噪声。

标准去噪

python 复制代码
sigma = 25

适合普通截图、扫描件。

强力去噪

python 复制代码
sigma = 50

适合低光、颗粒明显的图片。

这样用户可以根据视觉效果选择强度,工程体验会比单一模型好很多。


十一、踩坑记录

坑1:sigma_map范围写错

sigma_map必须和图像一样归一化到 0~1。

错误写法:

python 复制代码
sigma_map = torch.ones_like(clean) * 25

正确写法:

python 复制代码
sigma_map = torch.ones_like(clean) * 25 / 255.0

坑2:推理时忘记输入sigma_map

FFDNet不是普通单输入模型,推理时必须传两个输入:

python 复制代码
pred = model(noisy, sigma_map)

坑3:sigma给太大导致图像过度平滑

如果 sigma=80,可能会把真实纹理也当噪声去掉。

建议控制在:

text 复制代码
0 ~ 50

十二、效果验证

FFDNet的优势不是单点PSNR最高,而是:

同一个模型可以适配多个噪声强度。

实际效果表现:

噪声强度 普通UNet FFDNet
sigma=15 容易过度去噪 更自然
sigma=25 效果接近 稳定
sigma=50 去噪不足 更干净

十三、适合收藏总结

FFDNet完整流程

  1. 输入带噪图
  2. 构造噪声强度图
  3. noisy 与 sigma_map 拼接
  4. 模型预测残差噪声
  5. noisy - residual 得到结果

避坑清单

  • sigma_map必须归一化
  • 训练时sigma要随机
  • 推理必须传sigma_map
  • sigma过大会过度平滑
  • FFDNet适合做可控去噪

十四、优化建议

可以继续优化:

  • 把基础网络换成UNet
  • 加残差块
  • 加注意力模块
  • 对真实噪声做噪声估计
  • 结合图像质量评分自动选择sigma

结尾总结

FFDNet真正有价值的地方在于工程可控性。

很多时候,图像去噪不是追求一个固定模型处理所有情况,而是需要根据噪声强度灵活调整。

FFDNet提供了一个很实用的思路:

把噪声强度显式告诉模型,让模型按需去噪。


下一篇预告

Pytorch图像去噪实战(六):CBDNet真实噪声去噪实战,解决合成噪声到真实噪声的泛化问题

相关推荐
zh1570231 小时前
CSS如何让元素出现时带抖动_利用关键帧定义抖动动画
jvm·数据库·python
花月C1 小时前
Agent应用开发零基础入门:核心概念、环境配置与首次LLM调用
java·python
AGV算法笔记1 小时前
CVPR 2025顶级SLAM论文精读:MASt3R-SLAM如何用单目相机实现实时稠密三维重建?
深度学习·数码相机·机器人视觉·slam·三维重建·agv
【 】4231 小时前
从迭代器到生成器
python·迭代器·生成器
AC赳赳老秦2 小时前
网安工程师提效:用 OpenClaw 实现漏洞扫描报告生成、安全巡检自动化、日志合规审计
java·开发语言·前端·javascript·python·deepseek·openclaw
你数过天上的星星吗2 小时前
Python学习笔记二(函数、类与对象)
笔记·python·学习
四维迁跃2 小时前
如何排查SQL存储过程死锁_分析死锁日志与索引优化
jvm·数据库·python
m0_741173332 小时前
如何检测SQL注入风险_利用模糊测试技术发现漏洞
jvm·数据库·python
xcbrand2 小时前
餐饮品牌全案公司哪家可靠
运维·python