Pytorch图像去噪实战(七):Noise2Noise自监督图像去噪实战,没有干净图也能训练模型

Pytorch图像去噪实战(七):Noise2Noise自监督图像去噪实战,没有干净图也能训练模型


一、问题场景:真实项目里根本没有干净图

前面几篇文章中,我们默认有 clean image,也就是干净图像。

训练数据通常是:

text 复制代码
noisy -> clean

但在真实项目里,经常遇到一个很现实的问题:

我们只有带噪图片,没有对应的干净图片。

比如:

  • 夜间监控图像
  • 医学低剂量图像
  • 老照片扫描图
  • 工业相机采集图
  • 用户上传的真实图片

这种情况下,如果没有 clean target,普通监督学习就很难训练。

一开始我也尝试过人工构造干净图,比如用传统滤波先处理一遍作为伪标签。

但效果很差,因为伪标签本身就模糊,会把模型带偏。

后来我采用了 Noise2Noise 的思路:

不需要干净图,只需要同一场景下两张不同噪声版本的图。


二、Noise2Noise的核心思想

传统监督去噪:

text 复制代码
noisy_image -> clean_image

Noise2Noise训练方式:

text 复制代码
noisy_image_a -> noisy_image_b

前提是:

  • 两张图对应同一个干净信号
  • 噪声是独立随机的
  • 噪声均值接近0

模型在大量样本上学习后,会趋向恢复共同的干净结构,而不是随机噪声。


三、为什么noisy到noisy也能学?

假设真实图像是 x,两张带噪图分别是:

text 复制代码
y1 = x + n1
y2 = x + n2

其中 n1 和 n2 是独立噪声。

训练目标:

text 复制代码
model(y1) -> y2

因为 n2 是随机的,模型无法预测具体噪声,只能学习稳定存在的 x。

最终模型会学到接近 clean image 的输出。

这就是 Noise2Noise 最有意思的地方。


四、工程适用场景

Noise2Noise特别适合:

  • 同一场景可多次采集
  • 连续视频帧
  • 医学影像重复采样
  • 工业检测多次曝光
  • 没有clean标签的数据

如果你只有单张带噪图,Noise2Noise不一定适合,可以考虑 Noise2Void 或 Blind-Spot Network。


五、工程目录结构

复制代码
noise2noise_denoise/
├── data/
│   ├── noisy_a/
│   └── noisy_b/
├── models/
│   └── unet.py
├── dataset.py
├── train.py
├── eval.py
└── utils.py

这里 noisy_a 和 noisy_b 中的图片要一一对应。

比如:

text 复制代码
noisy_a/001.png
noisy_b/001.png

六、数据集实现

dataset.py

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


class Noise2NoiseDataset(Dataset):
    def __init__(self, noisy_a_dir, noisy_b_dir):
        self.noisy_a_paths = sorted([
            os.path.join(noisy_a_dir, name)
            for name in os.listdir(noisy_a_dir)
            if name.lower().endswith((".jpg", ".png", ".jpeg"))
        ])

        self.noisy_b_paths = sorted([
            os.path.join(noisy_b_dir, name)
            for name in os.listdir(noisy_b_dir)
            if name.lower().endswith((".jpg", ".png", ".jpeg"))
        ])

        assert len(self.noisy_a_paths) == len(self.noisy_b_paths)

        self.transform = transforms.Compose([
            transforms.Resize((256, 256)),
            transforms.ToTensor()
        ])

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

    def __getitem__(self, idx):
        img_a = Image.open(self.noisy_a_paths[idx]).convert("L")
        img_b = Image.open(self.noisy_b_paths[idx]).convert("L")

        img_a = self.transform(img_a)
        img_b = self.transform(img_b)

        return img_a, img_b

七、模型选择:使用UNet作为基础网络

Noise2Noise不是一个具体网络,而是一种训练方式。

这里我们用一个轻量 UNet。

models/unet.py

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


class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()

        self.net = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, 3, padding=1),
            nn.ReLU(inplace=True),

            nn.Conv2d(out_channels, out_channels, 3, padding=1),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.net(x)


class SimpleUNet(nn.Module):
    def __init__(self):
        super().__init__()

        self.pool = nn.MaxPool2d(2)

        self.enc1 = ConvBlock(1, 64)
        self.enc2 = ConvBlock(64, 128)
        self.bottleneck = ConvBlock(128, 256)

        self.up2 = nn.ConvTranspose2d(256, 128, 2, 2)
        self.dec2 = ConvBlock(256, 128)

        self.up1 = nn.ConvTranspose2d(128, 64, 2, 2)
        self.dec1 = ConvBlock(128, 64)

        self.out = nn.Conv2d(64, 1, 1)

    def forward(self, x):
        e1 = self.enc1(x)
        e2 = self.enc2(self.pool(e1))

        b = self.bottleneck(self.pool(e2))

        d2 = self.up2(b)
        d2 = torch.cat([d2, e2], dim=1)
        d2 = self.dec2(d2)

        d1 = self.up1(d2)
        d1 = torch.cat([d1, e1], dim=1)
        d1 = self.dec1(d1)

        return self.out(d1)

八、训练代码

train.py

python 复制代码
import torch
from torch.utils.data import DataLoader
from dataset import Noise2NoiseDataset
from models.unet import SimpleUNet


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

    dataset = Noise2NoiseDataset("data/noisy_a", "data/noisy_b")
    loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4)

    model = SimpleUNet().to(device)

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

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

        for noisy_a, noisy_b in loader:
            noisy_a = noisy_a.to(device)
            noisy_b = noisy_b.to(device)

            pred = model(noisy_a)
            loss = criterion(pred, noisy_b)

            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"noise2noise_epoch_{epoch}.pth")


if __name__ == "__main__":
    train()

九、推理代码

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


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

model = SimpleUNet().to(device)
model.load_state_dict(torch.load("noise2noise_epoch_80.pth", map_location=device))
model.eval()

img = Image.open("test_noisy.png").convert("L")

transform = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.ToTensor()
])

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

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

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

十、如果没有成对noisy图怎么办?

这是实际工程中最常见的问题。

如果没有同一场景的两张带噪图,可以考虑几种方案:

1. 从视频帧中构造

连续视频中相邻帧内容相近,可以近似作为 paired noisy 数据。

2. 多次采集

工业相机、医学设备、监控系统通常可以多次采样。

3. 用数据增强模拟第二噪声版本

如果只有 clean 不可得,但有一张 noisy,可以生成另一个噪声扰动版本。

不过这种方式严格来说不是真正的 Noise2Noise,效果要谨慎验证。


十一、踩坑记录

坑1:两张图没有对齐

Noise2Noise要求 noisy_a 和 noisy_b 内容一致。

如果两张图发生位移,模型会学糊。

解决:

  • 数据采集时固定相机
  • 做图像配准
  • 只使用稳定区域

坑2:噪声不是独立的

如果两张图噪声模式相同,比如固定条纹噪声,模型可能学到噪声。

解决:

  • 尽量使用独立采样
  • 增加数据量
  • 对固定噪声单独建模

坑3:训练结果偏模糊

原因可能是:

  • 图像未对齐
  • 数据量太少
  • L1目标本身偏保守

解决方式:

  • 使用patch训练
  • 加边缘损失
  • 提升数据质量

十二、效果验证

Noise2Noise的效果取决于数据条件。

如果满足:

  • 同一场景
  • 独立噪声
  • 图像对齐

那么它可以在没有clean标签的情况下获得不错效果。

方法 是否需要clean 适用场景
普通监督去噪 需要 合成数据
Noise2Noise 不需要 多次采样
Noise2Void 不需要 单图自监督

十三、适合收藏总结

Noise2Noise训练流程

  1. 准备两组对应noisy图
  2. noisy_a作为输入
  3. noisy_b作为目标
  4. 用UNet训练
  5. 推理时输入单张noisy图

避坑清单

  • 两张图必须对齐
  • 噪声最好独立
  • 数据量不能太少
  • 不适合严重运动场景
  • 固定噪声可能被模型学进去

十四、优化建议

可以继续改进:

  • 加图像配准模块
  • 用视频帧构造训练集
  • 加时间一致性损失
  • 使用更强UNet
  • 结合Noise2Void处理单图场景

结尾总结

Noise2Noise最有价值的地方在于:

它打破了图像去噪必须依赖clean标签的限制。

在真实项目中,干净图往往比模型更难获得。

如果你的业务场景可以采集多张同一对象的带噪图,Noise2Noise是非常值得尝试的方案。


下一篇预告

Pytorch图像去噪实战(八):Noise2Void盲点网络实战,只有单张带噪图也能训练

相关推荐
PSLoverS1 小时前
Navicat全局查找与替换字符突然失效怎么办_重置与缓存清理
jvm·数据库·python
广州灵眸科技有限公司1 小时前
瑞芯微(EASY EAI)RV1126B AI算法开发流程
人工智能·算法·机器学习
m0_602857762 小时前
如何提升SQL存储过程逻辑复用_封装通用存储过程函数
jvm·数据库·python
志栋智能3 小时前
运维超自动化:构建弹性IT架构的关键支撑
运维·服务器·网络·人工智能·架构·自动化
傻啦嘿哟3 小时前
如何在 Python 中使用 colorama 库来给输出添加颜色
开发语言·python
薛定猫AI3 小时前
【深度解析】Open Design:用本地优先架构重塑 AI UI 生成工作流
人工智能·ui·架构
forEverPlume3 小时前
mysql如何实现高可用集群架构_基于MHA环境搭建与部署
jvm·数据库·python
嵌入式小企鹅4 小时前
CPU供需趋紧、DeepSeek V4全链适配、小米开源万亿模型
人工智能·学习·开源·嵌入式·小米·算力·昇腾
草莓熊Lotso4 小时前
Vibe Coding 时代:LangChain 与 LangGraph 全链路解析
linux·运维·服务器·数据库·人工智能·mysql·langchain