本文亮点:
- 跑通训练和测试代码,轻松运行,保证无任何运行问题;
- 详解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
输出路径下已经保存好去噪后的图像。
去噪前后对比:
|
|
Nam数据集:
|
|
注:真实图像盲去噪没有高斯白噪非盲去噪效果明显,需要放大查看。
二、源码解析
本节涉及的代码段为详细的带注释源码。
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))
有几个细节需要新手留意:
- torch.utils.data.DataLoader 会将train_dataset中的Numpy自动转为Tensor。这与常规的数据封装写法不同。通常在训练前,要将Numpy格式的图像数据转为Tensor,但本例省去了这一步骤,正是利用了torch.utils.data.DataLoader的特性。
- 训练使用了余弦退火调度器。由于epochs很大,使用余弦退火让训练更平滑。
- 由于源码是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
|
|
|
观察数据可知,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近似的计算方式,尽请期待!
至此本文结束。
如果本文对你有所帮助,请点赞收藏,创作不易,感谢您的支持!