【图像去噪】论文复现:适合新手小白的Pytorch版本CBDNet复现!轻松跑通训练和测试代码!简单修改路径即可训练自己的数据集!代码详细注释!数据处理、模型训练和验证、推理测试全流程讲解!

第一次来请先看【专栏介绍文章】:

本文亮点:

  • 跑通训练和测试代码,轻松运行,保证无任何运行问题
  • 详解CBDNet源码,数据处理、模型训练和验证、推理测试全流程讲解,无论是科研还是应用,新手小白都能看懂,学习阅读毫无压力,去噪入门必看
  • 理论和源码结合,进一步加深理解算法原理、明确训练和测试流程;
  • 更换路径和相关参数即可训练自己的图像数据集,无论是灰度图还是RGB图均可;
  • 去噪前后图像对比,噪声对比
  • 可计算测试集评估指标。补充了PSNR和SSIM的计算代码。


前言

论文题目:Toward Convolutional Blind Denoising of Real Photographs —— 迈向真实照片的卷积盲去噪

论文地址:Toward Convolutional Blind Denoising of Real Photographs

论文源码:https://github.com/GuoShi28/CBDNet

对应的论文精读:【图像去噪】论文精读:Toward Convolutional Blind Denoising of Real Photographs(CBDNet)

由于源代码是matlab实现,这里我们使用另一个高star的Pytorch版本代码进行复现:

https://github.com/IDKiro/CBDNet-pytorch

一、跑通代码 (Quick Start)

1.1 数据集准备

SIDD数据集、Syn数据集、预训练模型下载地址:GoogleDrive (科学上网)在这里插入图片描述
数据集特点:SIDD是真实的噪声数据集,Syn是合成噪声数据集。图像数据切成256×256的patch,原图(GT)和加噪(NOISY)后的图放在同一文件夹下并按序对应。
在这里插入图片描述
在这里插入图片描述
建立如下路径:

~/
  data/
    SIDD_train/
      ... (scene id)
    Syn_train/
      ... (id)
    DND/
      images_srgb/
        ... (mat files)
      ... (mat files)
  save_model/
    checkpoint.pth.tar

注:如果要训练自己的数据集,按同样命名方式(GT_SRGB和NOISY_SRGB)和放置方式(GT和NOISY放在同一个文件夹下)后(不一定要切块),路径对应即可。

1.2 训练

修改train.py下的参数:

parser.add_argument('--bs', default=32, type=int, help='batch size') # 批次
parser.add_argument('--ps', default=128, type=int, help='patch size') # 块大小
parser.add_argument('--lr', default=2e-4, type=float, help='learning rate') # 学习率
parser.add_argument('--epochs', default=5000, type=int, help='sum of epochs') # epoch数量

如果训练的是自己的数据集,则修改第70行代码中的路径为自己数据集的路径,参数个数为自己数据集的子文件夹个数

train_dataset = Real('./data/SIDD_train/', 320, args.ps) + Syn('./data/Syn_train/', 100, args.ps)

如果是单GPU训练,则注释掉下面这一行:

model = nn.DataParallel(model)

执行train.py后,控制台会输出相应的信息,save_model中会保存训练模型。
在这里插入图片描述

1.3 测试

本节对应predict.py。输入带噪声图像,输出去噪后的图像。

首先,将训练好的模型或者源码已给的模型放在save_model文件夹下。
然后,修改参数:

parser.add_argument('--input_filename', type=str, default='test_pic/1-1.bmp')
parser.add_argument('--output_filename', type=str, default='test_pic/1-1_denoising.bmp')

Windows下直接添加default路径为自己的测试图片路径,如test_pic/1-1.bmp,输出图像保存位置为test_pic/1-1_denoising.bmp。

Linux下执行命令:

python predict.py input_filename output_filename

输出路径下已经保存好去噪后的图像。

去噪前后对比:

Noisy
Denoisy

Nam数据集:

Noisy__
Denoisy

注:真实图像盲去噪没有高斯白噪非盲去噪效果明显,需要放大查看。

二、源码解析

本节涉及的代码段为详细的带注释源码。

2.1 数据预处理

本节对应dataset文件夹(包)下的loader.py。

包含三个部分:

  • get_patch:切块函数。图像上随机位置裁剪指定大小的图像块,同时数据增强
# 获得图像块
def get_patch(imgs, patch_size):
	H = imgs[0].shape[0] # 图像宽高
	W = imgs[0].shape[1]

	ps_temp = min(H, W, patch_size) # 临时图像块大小

	# 随机位置
	xx = np.random.randint(0, W-ps_temp) if W > ps_temp else 0
	yy = np.random.randint(0, H-ps_temp) if H > ps_temp else 0

	# 遍历裁剪
	for i in range(len(imgs)):
		imgs[i] = imgs[i][yy:yy+ps_temp, xx:xx+ps_temp, :]

	# 随机增强:水平翻转、垂直翻转、旋转(变换维度hwc->whc)
	if np.random.randint(2, size=1)[0] == 1:
		for i in range(len(imgs)):
			imgs[i] = np.flip(imgs[i], axis=1)
	if np.random.randint(2, size=1)[0] == 1: 
		for i in range(len(imgs)):
			imgs[i] = np.flip(imgs[i], axis=0)
	if np.random.randint(2, size=1)[0] == 1:
		for i in range(len(imgs)):
			imgs[i] = np.transpose(imgs[i], (1, 0, 2))

	return imgs
  • Real:真实噪声图像数据集类。
# 真实噪声图像数据集封装
class Real(Dataset):
	def __init__(self, root_dir, sample_num, patch_size=128):
		'''

		Parameters
		----------
		root_dir: 数据集文件夹目录
		sample_num: 数据个数
		patch_size: 块大小
		'''

		self.patch_size = patch_size

		folders = glob.glob(root_dir + '/*')
		folders.sort()

		self.clean_fns = [None] * sample_num # 原始图像列表
		for i in range(sample_num):
			self.clean_fns[i] = []

		for ind, folder in enumerate(folders):
			clean_imgs = glob.glob(folder + '/*GT_SRGB*')
			clean_imgs.sort()

			for clean_img in clean_imgs:
				self.clean_fns[ind % sample_num].append(clean_img)

	def __len__(self):
		l = len(self.clean_fns)
		return l

	def __getitem__(self, idx):
		clean_fn = random.choice(self.clean_fns[idx]) # 随机选择一个原始图像

		clean_img = read_img(clean_fn) # 读取该图像
		noise_img = read_img(clean_fn.replace('GT_SRGB', 'NOISY_SRGB')) # 读取对应的噪声图像

		# 切块
		if self.patch_size > 0:
			[clean_img, noise_img] = get_patch([clean_img, noise_img], self.patch_size)

		#返回值顺序:维度为chw的噪声图像、维度为chw的原始图像、相同大小的全0Numpy、相同大小的全0Numpy(非对称损失使用)
		return hwc_to_chw(noise_img), hwc_to_chw(clean_img), np.zeros((3, self.patch_size, self.patch_size)), np.zeros((3, self.patch_size, self.patch_size))
  • Syn:合成噪声图像数据集。
# 合成噪声图像数据集封装
class Syn(Dataset):
	def __init__(self, root_dir, sample_num, patch_size=128):
		self.patch_size = patch_size

		folders = glob.glob(root_dir + '/*')
		folders.sort()

		self.clean_fns = [None] * sample_num
		for i in range(sample_num):
			self.clean_fns[i] = []

		for ind, folder in enumerate(folders):
			clean_imgs = glob.glob(folder + '/*GT_SRGB*')
			clean_imgs.sort()

			for clean_img in clean_imgs:
				self.clean_fns[ind % sample_num].append(clean_img)

	def __len__(self):
		l = len(self.clean_fns)
		return l

	def __getitem__(self, idx):
		clean_fn = random.choice(self.clean_fns[idx])

		clean_img = read_img(clean_fn)
		noise_img = read_img(clean_fn.replace('GT_SRGB', 'NOISY_SRGB'))
		sigma_img = read_img(clean_fn.replace('GT_SRGB', 'SIGMA_SRGB')) / 15.	# inverse scaling
		# syn_train中的sigma图像是改变亮度后的图像

		if self.patch_size > 0:
			[clean_img, noise_img, sigma_img] = get_patch([clean_img, noise_img, sigma_img], self.patch_size)

		# 生成图像最后是全1Numpy
		return hwc_to_chw(noise_img), hwc_to_chw(clean_img), hwc_to_chw(sigma_img), np.ones((3, self.patch_size, self.patch_size))

2.2 CBDNet网络结构

2.2.1 网络结构回顾

本节对应model文件夹(包)下的cbdnet.py。
在这里插入图片描述
CBDNet由两个子网络构成:噪声估计子网和非盲去噪子网

  • 噪声估计子网:全连接网络FCN。5个卷积层,每个卷积层后跟ReLU,特征数32,卷积核大小3×3。
  • 噪声估计子网的输入为噪声图像(主要测试因相机ISP产生的真实噪声图像),输出为估计的noise level map。
  • 非盲去噪子网:16层U-Net结构,除最后一层外,每个卷积层后跟ReLU,不同层特征数与上图中一致。包含残差连接和转置卷积。
  • 非盲去噪子网的输入为噪声图像和noise level map堆叠在一起,所以输入通道数应为6;输出为去噪后的图像。

2.2.2 CBDNet的实现代码拆解

定义可以重复使用的公共结构"conv+ReLU":

# 公共模块:卷积+ReLU
class single_conv(nn.Module):
    def __init__(self, in_ch, out_ch):
        super(single_conv, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, padding=1),
            nn.ReLU(inplace=True)
        )

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

转置卷积上采样操作:

# 上采样
class up(nn.Module):
    def __init__(self, in_ch):
        super(up, self).__init__()
        self.up = nn.ConvTranspose2d(in_ch, in_ch//2, 2, stride=2)

    def forward(self, x1, x2):
        x1 = self.up(x1)
        
        # bchw
        diffY = x2.size()[2] - x1.size()[2]
        diffX = x2.size()[3] - x1.size()[3]

        # 上采样后大小可能不一致,补0
        x1 = F.pad(x1, (diffX // 2, diffX - diffX//2,
                        diffY // 2, diffY - diffY//2))

        # 跳跃连接
        x = x2 + x1
        return x

最后一个卷积层:

# 最后一个卷积层
class outconv(nn.Module):
    def __init__(self, in_ch, out_ch):
        super(outconv, self).__init__()
        self.conv = nn.Conv2d(in_ch, out_ch, 1)

    def forward(self, x):
        x = self.conv(x)
        return x

噪声估计子网:

# CNN_E:噪声估计子网,5个卷积层+ReLU
class FCN(nn.Module):
    def __init__(self):
        super(FCN, self).__init__()
        self.fcn = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, 3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, 3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, 3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 3, 3, padding=1),
            nn.ReLU(inplace=True)
        )
    
    def forward(self, x):
        return self.fcn(x)

非盲去噪网络:

# CNN_D:非盲去噪网络,U-Net结构,各层数以及连接方式与图2网络结构一致
class UNet(nn.Module):
    def __init__(self):
        super(UNet, self).__init__()
        
        self.inc = nn.Sequential(
            single_conv(6, 64),
            single_conv(64, 64)
        )

        self.down1 = nn.AvgPool2d(2)
        self.conv1 = nn.Sequential(
            single_conv(64, 128),
            single_conv(128, 128),
            single_conv(128, 128)
        )

        self.down2 = nn.AvgPool2d(2)
        self.conv2 = nn.Sequential(
            single_conv(128, 256),
            single_conv(256, 256),
            single_conv(256, 256),
            single_conv(256, 256),
            single_conv(256, 256),
            single_conv(256, 256)
        )

        self.up1 = up(256)
        self.conv3 = nn.Sequential(
            single_conv(128, 128),
            single_conv(128, 128),
            single_conv(128, 128)
        )

        self.up2 = up(128)
        self.conv4 = nn.Sequential(
            single_conv(64, 64),
            single_conv(64, 64)
        )

        self.outc = outconv(64, 3)

    def forward(self, x):
        inx = self.inc(x)

        down1 = self.down1(inx)
        conv1 = self.conv1(down1)

        down2 = self.down2(conv1)
        conv2 = self.conv2(down2)

        up1 = self.up1(conv2, conv1)
        conv3 = self.conv3(up1)

        up2 = self.up2(conv3, inx)
        conv4 = self.conv4(up2)

        out = self.outc(conv4)
        return out

整体结构:

# CBDNet整体网络结构
class Network(nn.Module):
    def __init__(self):
        super(Network, self).__init__()
        self.fcn = FCN()
        self.unet = UNet()
    
    def forward(self, x):
        noise_level = self.fcn(x) # 预测的noise level map
        concat_img = torch.cat([x, noise_level], dim=1) # 输入和noise level map都堆叠在一起(同FFDNet)
        out = self.unet(concat_img) + x
        return noise_level, out

2.2.3 损失函数实现

在这里插入图片描述
损失函数包含三项:重建损失、非对称损失、总变分损失。其中:

  • 重建损失是MSE损失(L2损失):在这里插入图片描述
  • 非对称损失:
    在这里插入图片描述
  • 总变分损失(TV):
    在这里插入图片描述
    代码实现如下:
# 损失函数实现
class fixed_loss(nn.Module):
    def __init__(self):
        super().__init__()
        
    def forward(self, out_image, gt_image, est_noise, gt_noise, if_asym):
        '''

        Parameters
        ----------
        out_image: 输出的去噪后图像
        gt_image: 原图,ground-truth
        est_noise: 估计的噪声
        gt_noise: 实际噪声
        if_asym: 真实数据是全0,合成数据是全1

        Returns : 总损失
        -------

        '''

        # 重建损失L_rec
        l2_loss = F.mse_loss(out_image, gt_image)

        # 非对称损失公式(4),α = 0.3
        # torch.lt: 逐元素小于比较
        asym_loss = torch.mean(if_asym * torch.abs(0.3 - torch.lt(gt_noise, est_noise).float()) * torch.pow(est_noise - gt_noise, 2))

        h_x = est_noise.size()[2] # 获取估计噪声宽高
        w_x = est_noise.size()[3]
        count_h = self._tensor_size(est_noise[:, :, 1:, :]) # 估计噪声有效宽高
        count_w = self._tensor_size(est_noise[:, :, : ,1:])

        # 宽高方向上总变差:相邻行行列之间的平方和
        h_tv = torch.pow((est_noise[:, :, 1:, :] - est_noise[:, :, :h_x-1, :]), 2).sum()
        w_tv = torch.pow((est_noise[:, :, :, 1:] - est_noise[:, :, :, :w_x-1]), 2).sum()

        # 总变差损失平均值
        tvloss = h_tv / count_h + w_tv / count_w

        # 参数λ1 = 0.5    λ2 = 0.05
        loss = l2_loss + 0.5 * asym_loss + 0.05 * tvloss

        return loss

    # 总像素数chw
    def _tensor_size(self, t):
        return t.size()[1]*t.size()[2]*t.size()[3]

2.3 训练CBDNet

本节对应train.py。训练过程没什么好说的,每个项目都大同小异。

代码如下:

import os, time, shutil
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F

from utils import AverageMeter
from dataset.loader import Real, Syn
from model.cbdnet import Network, fixed_loss


parser = argparse.ArgumentParser(description = 'Train')
parser.add_argument('--bs', default=32, type=int, help='batch size')
parser.add_argument('--ps', default=128, type=int, help='patch size')
parser.add_argument('--lr', default=2e-4, type=float, help='learning rate')
parser.add_argument('--epochs', default=5000, type=int, help='sum of epochs')
args = parser.parse_args()


def train(train_loader, model, criterion, optimizer):
	losses = AverageMeter()
	model.train()

	for (noise_img, clean_img, sigma_img, flag) in train_loader:
		input_var = noise_img.cuda()
		target_var = clean_img.cuda()
		sigma_var = sigma_img.cuda()
		flag_var = flag.cuda()

		noise_level_est, output = model(input_var)

		loss = criterion(output, target_var, noise_level_est, sigma_var, flag_var)

		losses.update(loss.item())

		optimizer.zero_grad()
		loss.backward()
		optimizer.step()
	
	return losses.avg


if __name__ == '__main__':
	save_dir = './weights/'

	model = Network()
	model.cuda()

	# 可选多卡训练
	# model = nn.DataParallel(model)

	# 接续训练
	if os.path.exists(os.path.join(save_dir, 'checkpoint.pth.tar')):
		# load existing model
		model_info = torch.load(os.path.join(save_dir, 'checkpoint.pth.tar'))
		print('==> loading existing model:', os.path.join(save_dir, 'checkpoint.pth.tar'))
		model.load_state_dict(model_info['state_dict'])
		optimizer = torch.optim.Adam(model.parameters())
		optimizer.load_state_dict(model_info['optimizer'])
		scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
		scheduler.load_state_dict(model_info['scheduler'])
		cur_epoch = model_info['epoch']
	else:
		if not os.path.isdir(save_dir):
			os.makedirs(save_dir)
		# create model
		optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
		# 余弦退火调度器,从初始lr开始,在T_max个epochs内逐渐降低
		scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
		cur_epoch = 0
		
	criterion = fixed_loss()
	criterion.cuda()

	# 读取训练集
	train_dataset = Real('./data/SIDD_train/', 320, args.ps) + Syn('./data/Syn_train/', 100, args.ps)
	train_loader = torch.utils.data.DataLoader(
		train_dataset, batch_size=args.bs, shuffle=True, num_workers=8, pin_memory=True, drop_last=True)

	# torch.utils.data.DataLoader 会将train_dataset中的Numpy自动转为Tensor

	# 训练过程
	for epoch in range(cur_epoch, args.epochs + 1):
		loss = train(train_loader, model, criterion, optimizer)
		scheduler.step()

		torch.save({
			'epoch': epoch + 1,
			'state_dict': model.state_dict(),
			'optimizer' : optimizer.state_dict(),
			'scheduler' : scheduler.state_dict()}, 
			os.path.join(save_dir, 'checkpoint.pth.tar'))

		print('Epoch [{0}]\t'
			'lr: {lr:.6f}\t'
			'Loss: {loss:.5f}'
			.format(
			epoch,
			lr=optimizer.param_groups[-1]['lr'],
			loss=loss))

有几个细节需要新手留意:

  1. torch.utils.data.DataLoader 会将train_dataset中的Numpy自动转为Tensor。这与常规的数据封装写法不同。通常在训练前,要将Numpy格式的图像数据转为Tensor,但本例省去了这一步骤,正是利用了torch.utils.data.DataLoader的特性。
  2. 训练使用了余弦退火调度器。由于epochs很大,使用余弦退火让训练更平滑。
  3. 由于源码是matlab实现的,编译器不同,设备性能也不同。所以,论文4.2中所写的训练参数可能不适用于Pytorch训练。这也是本文复现训练参数不同的原因。以搞懂算法,提升代码能力为目的学习,不必过分纠结参数细节的不同。

2.4 测试CBDNet

本节对应predict.py。功能是输入一张带噪声图像,保存去噪后的图像。

代码如下:

import os, time, scipy.io, shutil
import numpy as np
import torch
import torch.nn as nn
import argparse
import cv2

from model.cbdnet import Network
from utils import read_img, chw_to_hwc, hwc_to_chw

parser = argparse.ArgumentParser(description = 'Test')
parser.add_argument('--input_filename', type=str, default='test_pic/01_noise.png')
parser.add_argument('--output_filename', type=str, default='test_pic/01_noise_denoising1.png')
args = parser.parse_args()

save_dir = './save_model/'

model = Network()
model.cuda()
# model = nn.DataParallel(model)

model.eval()

if os.path.exists(os.path.join(save_dir, 'checkpoint.pth.tar')):
    # load existing model
    model_info = torch.load(os.path.join(save_dir, 'checkpoint.pth.tar'))
    model.load_state_dict(model_info['state_dict'])
else:
    print('Error: no trained model detected!')
    exit(1)

input_image = read_img(args.input_filename)
input_var =  torch.from_numpy(hwc_to_chw(input_image)).unsqueeze(0).cuda()

with torch.no_grad():
    _, output = model(input_var)

output_image = chw_to_hwc(output[0,...].cpu().numpy())
output_image = np.uint8(np.round(np.clip(output_image, 0, 1) * 255.))[: ,: ,::-1]

cv2.imwrite(args.output_filename, output_image)

注意:

  • input_filename和output_filename参数前要加“- -”,设置default为自己的图像路径
  • 如果用单卡训练保存的模型要注释掉model = nn.DataParallel(model)

utils封装的相关操作:

# 读取图像:归一化后转为numpy数组,类型为float32,即Numpy转Tensor的前一步格式
def read_img(filename):
	img = cv2.imread(filename)
	img = img[:,:,::-1] / 255.0
		
	img = np.array(img).astype('float32')

	return img

# cv2读取numpy形式图像为hwc,转成chw
def hwc_to_chw(img):
	return np.transpose(img, axes=[2, 0, 1]).astype('float32')


def chw_to_hwc(img):
	return np.transpose(img, axes=[1, 2, 0]).astype('float32')

图像流:cv2读取的图像是Numpy,维度(shape)为hwc。而Pytorch模型输入的Tensor为bchw,并且是归一化后的float类型(一般为float32)。所以需要将cv2读取的图像归一化转为float32类型的np,将维度hwc转为chw,模型输出后再逆归一化(*255),维度chw转为hwc,最后cv2保存。

记忆:cv2的相关操作就是维度为hwc的Numpy;模型操作就是维度为bchw的Tensor

测试结果展示:
在这里插入图片描述

三、思考与补充

3.1 思考

通过本文完整的复现,会发现gt_noise对应的是sigma_var,而sigma图像是在合成数据集Syn_train中出现的。反应到代码上为:

sigma_img = read_img(clean_fn.replace('GT_SRGB', 'SIGMA_SRGB')) / 15.	# inverse scaling
GT__
Noisy
Sigma

观察数据可知,sigma是GT的亮度变化。那么用除15的方式来模拟真实的噪声图像。换句话说,Sigma图像是相机ISP操作CRF得到的,除15是逆变换回去。具体的生成相机真实噪声的操作见CBDNet源码中的某些python操作可以实现。

真实图像Real类中的第三个返回值是全0占位符。所以,想要模拟相机拍摄的真实噪声还是比较困难,值得进一步思考和研究。

3.2 补充PSNR和SSIM的计算代码

在项目根目录下新建一个test_benchmark.py文件。

代码如下:

import os, time, scipy.io, shutil, glob
import numpy as np
import torch
import torch.nn as nn
import argparse
import cv2
import PIL.Image as pil_image
from torchvision import transforms
import utilss
from utilss import calc_psnr, calculate_ssim

from model.cbdnet import Network
from utils import read_img, chw_to_hwc, hwc_to_chw

from skimage.metrics import structural_similarity as compare_ssim
from skimage.metrics import peak_signal_noise_ratio as compare_psnr

parser = argparse.ArgumentParser(description = 'Test')
parser.add_argument('--GT_images', type=str, default='data/Nam_patch_GT')
parser.add_argument('--Noisy_images', type=str, default='data/Nam_patches')
args = parser.parse_args()

if not os.path.exists(args.output_dir):
    os.makedirs(args.output_dir)

save_dir = './weights/'

model = Network()
model.cuda()
# model = nn.DataParallel(model)

model.eval()

if os.path.exists(os.path.join(save_dir, 'checkpoint.pth.tar')):
    # load existing model
    model_info = torch.load(os.path.join(save_dir, 'checkpoint.pth.tar'))
    model.load_state_dict(model_info['state_dict'])
else:
    print('Error: no trained model detected!')
    exit(1)

GT_images_paths = glob.glob(args.GT_images + "/*.*")
Noisy_images_paths = glob.glob(args.Noisy_images + "/*.*")

benchmark_len = len(GT_images_paths)
sum_psnr = 0.0
sum_ssim = 0.0

# CV2读取并计算指标
for i in range(benchmark_len):
    filename = os.path.basename(Noisy_images_paths[i]).split('_')[0]
    print("image:", filename)

    GT_image = read_img(GT_images_paths[i])
    input_image = read_img(Noisy_images_paths[i])
    input_var = torch.from_numpy(hwc_to_chw(input_image)).unsqueeze(0).cuda()

    with torch.no_grad():
        _, output = model(input_var)

    output_image1 = chw_to_hwc(output[0, ...].cpu().numpy())
    output_image = np.uint8(np.round(np.clip(output_image1, 0, 1) * 255.))[: ,: ,::-1]

    cv2.imwrite(os.path.join(args.output_dir, '{}_denoisy.png'.format(filename)), output_image)

    # 计算指标
    psnr = compare_psnr(GT_image, output_image1, data_range=GT_image.max() - GT_image.min())
    ssim = compare_ssim(GT_image, output_image1, channel_axis=2, data_range=GT_image.max() - GT_image.min())

    sum_psnr += psnr
    sum_ssim += ssim

print('PSNR: {:.2f}'.format(sum_psnr / benchmark_len))
print('SSIM: {:.4f}'.format(sum_ssim / benchmark_len))

这里我使用matlab源码中给出的Nam测试块进行测试。计算指标使用skimage.metrics封装好的PSNR和SSIM计算函数。

计算思路:遍历GT和Nosiy的每张图像,GT用于比较,Noisy用于输入模型,计算输出output与GT之间的PSNR和SSIM。图像均为归一化后的float32类型的Numpy。

结果为:

PSNR: 39.48
SSIM: 0.9636

挖个坑:

  • 使用cv2、PIL、skimage不同方式读取图像、相同的skimage.metrics指标计算方式,计算出的PSNR和SSIM是不同的;
  • 使用相同读取方式,不使用skimage.metrics,而使用根据公式实现的PSNR和SSIM计算出的PSNR和SSIM也是不同的;
  • 读取方式相同,指标计算方式相同,在RGB通道和在YCBCr通道计算的PSNR和SSIM也是不同的,而且差别很大;

上述各种排列组合,计算出的指标都不同,而且差别巨大。后续出一篇文章单独讨论指标的计算,分析原因以及究竟应该使用哪种方式计算指标最合理,并用Python实现一个与matlab近似的计算方式,尽请期待!


至此本文结束。

如果本文对你有所帮助,请点赞收藏,创作不易,感谢您的支持!

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
U-Net是一种深度学习模型,最初用于生物医学图像分割,但它也可以应用于图像去噪任务。在PyTorch复现U-Net,你可以按照以下步骤操作: 1. **安装依赖**:首先确保已经安装了PyTorch及其相关的库,如torchvision。如果需要,可以运行`pip install torch torchvision`. 2. **网络结构搭建**:创建一个U-Net模型的核心部分,它包括编码器(逐渐降低分辨率,提取特征)和解码器(逐步增加分辨率,恢复细节)。可以参考论文《Image Segmentation through Deep Learning》中的架构。 ```python import torch.nn as nn from torch.nn import Conv2d, MaxPool2d, UpSample class UNetBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super(UNetBlock, self).__init__() self.encoder = nn.Sequential( Conv2d(in_channels, out_channels, kernel_size, stride=stride, padding=padding), nn.ReLU(), Conv2d(out_channels, out_channels, kernel_size, stride=stride, padding=padding) ) self.decoder = nn.Sequential( nn.ConvTranspose2d(out_channels, out_channels, kernel_size, stride=stride, padding=padding), nn.ReLU(), nn.Conv2d(out_channels, out_channels, kernel_size, stride=stride, padding=padding) ) def forward(self, x): skip_connection = x x = self.encoder(x) x = self.decoder(x) return torch.cat((x, skip_connection), dim=1) # 构建完整的U-Net模型 def create_unet(input_channels, num_classes): unet = nn.Sequential( nn.Conv2d(input_channels, 64, 3, padding=1), nn.MaxPool2d(2, 2), UNetBlock(64, 128), nn.MaxPool2d(2, 2), UNetBlock(128, 256), nn.MaxPool2d(2, 2), UNetBlock(256, 512), nn.MaxPool2d(2, 2), UNetBlock(512, 1024), nn.Upsample(scale_factor=2), UNetBlock(1024, 512), nn.Upsample(scale_factor=2), UNetBlock(512, 256), nn.Upsample(scale_factor=2), UNetBlock(256, 128), nn.Upsample(scale_factor=2), nn.Conv2d(128, num_classes, 1) ) return unet ``` 3. **训练和应用**:准备噪声图像数据、对应干净图像的数据集,然后定义损失函数(如MSE或SSIM)、优化器,并开始训练训练完成后,对新的噪声图像进行前向传播以获得去噪后的结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十小大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值