当前位置: 首页 > news >正文

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

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


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

前面几篇文章中,我们默认有 clean image,也就是干净图像。
训练数据通常是:

noisy -> clean

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

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

比如:

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

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

一开始我也尝试过人工构造干净图,比如用传统滤波先处理一遍作为伪标签。
但效果很差,因为伪标签本身就模糊,会把模型带偏。

后来我采用了 Noise2Noise 的思路:

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


二、Noise2Noise的核心思想

传统监督去噪:

noisy_image -> clean_image

Noise2Noise训练方式:

noisy_image_a -> noisy_image_b

前提是:

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

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


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

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

y1 = x + n1 y2 = x + n2

其中 n1 和 n2 是独立噪声。

训练目标:

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 中的图片要一一对应。

比如:

noisy_a/001.png noisy_b/001.png

六、数据集实现

dataset.py

importosfromPILimportImagefromtorch.utils.dataimportDatasetimporttorchvision.transformsastransformsclassNoise2NoiseDataset(Dataset):def__init__(self,noisy_a_dir,noisy_b_dir):self.noisy_a_paths=sorted([os.path.join(noisy_a_dir,name)fornameinos.listdir(noisy_a_dir)ifname.lower().endswith((".jpg",".png",".jpeg"))])self.noisy_b_paths=sorted([os.path.join(noisy_b_dir,name)fornameinos.listdir(noisy_b_dir)ifname.lower().endswith((".jpg",".png",".jpeg"))])assertlen(self.noisy_a_paths)==len(self.noisy_b_paths)self.transform=transforms.Compose([transforms.Resize((256,256)),transforms.ToTensor()])def__len__(self):returnlen(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)returnimg_a,img_b

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

Noise2Noise不是一个具体网络,而是一种训练方式。
这里我们用一个轻量 UNet。

models/unet.py

importtorchimporttorch.nnasnnclassConvBlock(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))defforward(self,x):returnself.net(x)classSimpleUNet(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)defforward(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)returnself.out(d1)

八、训练代码

train.py

importtorchfromtorch.utils.dataimportDataLoaderfromdatasetimportNoise2NoiseDatasetfrommodels.unetimportSimpleUNetdeftrain():device=torch.device("cuda"iftorch.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()forepochinrange(1,81):model.train()total_loss=0fornoisy_a,noisy_binloader: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}")ifepoch%10==0:torch.save(model.state_dict(),f"noise2noise_epoch_{epoch}.pth")if__name__=="__main__":train()

九、推理代码

importtorchfromPILimportImageimporttorchvision.transformsastransformsimporttorchvision.utilsasvutilsfrommodels.unetimportSimpleUNet device=torch.device("cuda"iftorch.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)withtorch.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盲点网络实战,只有单张带噪图也能训练

http://www.cnnetsun.cn/news/2147493.html

相关文章:

  • Pytorch图像去噪实战(十):Restormer图像去噪实战,用高效Transformer解决高分辨率去噪问题
  • Flowframes终极指南:免费AI视频插帧工具让普通视频秒变流畅大片
  • 别再手动排期了!用Microsoft Project 2007三步搞定你的第一个项目计划(附WBS实战)
  • 终极指南:如何用Deep3D免费将2D视频秒变沉浸式3D立体影像
  • 氛!某插件肆意搜集信息,吾爱论坛站长打造完美替代品来救场
  • 如何用BiliTools跨平台工具箱轻松下载B站视频:完整指南
  • BepInEx Unity插件框架架构演进:从Mono到IL2CPP的技术突破与性能优化路径
  • 【仅限持牌机构技术负责人可见】:某头部支付平台PHP国密迁移内部白皮书节选(含性能损耗压测数据:TPS下降≤3.7%,密钥轮换耗时<86ms)
  • CircuitJS1 Desktop Mod:零基础入门电子电路仿真的完整指南
  • 当ISO镜像不再需要实体光驱:WinCDEmu的驱动级虚拟化方案
  • **超融合架构下的自动化运维:基于Python的容器化部署与监控实战**在现代数据中心演进中,**超融合架构(Hyper-Converg
  • YooAsset:企业级Unity资源管理框架的架构设计与实施指南
  • 如何快速掌握Charticulator:零代码图表设计的完整入门指南
  • 模型选型背后的成本工程:DeepSeek-V4、GPT-5.5与中国大模型API成本全解析
  • 绝地求生罗技鼠标宏压枪脚本:5分钟从新手到精准射击高手
  • AJ-Captcha行为验证码技术架构深度解析:构建智能人机识别系统的实践指南
  • 告别打包烦恼:用Auto.js Pro 9.0.0 + VSCode插件高效开发手机自动化脚本(附Scrcpy投屏技巧)
  • 任务分配的底层逻辑:告别 “能者多劳”,让每个人都 “物尽其用”
  • GLM-4.1V-9B-Base保姆级教程:Web界面UI功能分区与交互逻辑详解
  • Win11Debloat:Windows 11终极优化工具,5分钟还你一个干净高效的系统
  • 免费Switch模拟器Ryujinx:在PC上畅玩任天堂游戏的终极指南
  • 英雄联盟国服换肤神器:R3nzSkin免费解锁全皮肤完整教程
  • 29000+ 个 AI Skill 怎么选?这个工具帮你 30 秒找到最佳选择(附方法论)
  • 从MES到ERP:一份简历讲透你的技术栈演进,让猎头主动找上门
  • 别再只改主干网络了!YOLOv5模型轻量化避坑指南:从MobileNetV3、ShuffleNetV2到GhostNet的全面对比实验
  • 如何永久免费使用IDM?开源激活脚本完整指南
  • 终极Windows注册表取证分析:RegRipper3.0专业指南
  • 别再手动拼接字符串了!用Qt的QDateTime轻松搞定日志时间戳(附完整代码)
  • 如何用Autoticket大麦网自动抢票工具3倍提升抢票成功率?终极实战指南
  • 基于Java开发的制造业MES生产管理系统源码(含ERP集成模块)