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真实噪声去噪实战,解决合成噪声到真实噪声的泛化问题

相关推荐
懂点Tech14 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python
comedate14 小时前
SMLib 的 Examples 导读:PyAPI / SM_API / SMLib 三组示例怎么读
python·cad·smlib·examples·sm_api
云和数据.ChenGuang14 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
川石课堂软件测试15 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
美狐美颜SDK开放平台15 小时前
视频美颜SDK试用DEMO阶段应该测试什么?直播APP接入实操指南
开发语言·人工智能·深度学习·音视频·美颜sdk·直播美颜sdk·第三方美颜sdk
万化冥合15 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设
车软学堂15 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
天空依然蔚蓝16 小时前
分享一个自动检测网站在线状态的python程序
python·运维开发·1024程序员节
岁月宁静16 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent
codists17 小时前
《crafting interpreters》阅读笔记:p45-p45
python